diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a2930af8..246ba4486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,13 @@ Classify the change according to the following categories: ##### Removed ### Patches +## v3.16.0 +### Minor Updates +#### Added +- Added **Site** inputs **sector**, **federal_sector_state**, and **federal_procurement_type** +- Alternative defaults used when **sector** is "federal" +- **GHPOutputs** field **hybrid_solution_type** + ## v3.15.1 ### Minor Updates ##### Added @@ -50,7 +57,6 @@ Classify the change according to the following categories: #### Added - `CST` (concentrating solar thermal) Intputs and Outputs models; see /help endpoint for model fields - `HighTempThermalStorage` Inputs and Outputs models; see /help endpoint for model fields - #### Changed Update the following inputs from the previous --> new values: - `Financial.offtaker_discount_rate_fraction`: 0.0638 --> 0.0624 diff --git a/ghpghx/resources.py b/ghpghx/resources.py index e9dc414ad..3a8bb8035 100644 --- a/ghpghx/resources.py +++ b/ghpghx/resources.py @@ -161,7 +161,6 @@ def obj_create(self, bundle, **kwargs): ghpghxOutputsM = GHPGHXOutputs(ghp_uuid=ghp_uuid, **results) ghpghxOutputsM.save() except Exception as e: - print(e) exc_type, exc_value, exc_traceback = sys.exc_info() message = "Error saving the results to the database" data["status"] = message diff --git a/julia_src/Dockerfile b/julia_src/Dockerfile index bb3f10f6a..f5ef20392 100644 --- a/julia_src/Dockerfile +++ b/julia_src/Dockerfile @@ -1,4 +1,4 @@ -FROM julia:1.10.2 +FROM julia:1.11.2 # Install NREL root certs for machines running on NREL's network. ARG NREL_ROOT_CERT_URL_ROOT="" diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index fe97a7a04..900b0cbc2 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -1,6 +1,6 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.10.2" +julia_version = "1.11.2" manifest_format = "2.0" project_hash = "b3c6037c53375dada9e36c23f668bcd0efac8f34" @@ -56,10 +56,11 @@ version = "0.9.4" [[deps.ArgTools]] uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" -version = "1.1.1" +version = "1.1.2" [[deps.Artifacts]] uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" [[deps.AxisArrays]] deps = ["Dates", "IntervalSets", "IterTools", "RangeArrays"] @@ -69,6 +70,7 @@ version = "0.4.7" [[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +version = "1.11.0" [[deps.BenchmarkTools]] deps = ["Compat", "JSON", "Logging", "Printf", "Profile", "Statistics", "UUIDs"] @@ -194,7 +196,7 @@ weakdeps = ["Dates", "LinearAlgebra"] [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.1.0+0" +version = "1.1.1+0" [[deps.CompositionsBase]] git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad" @@ -268,6 +270,7 @@ version = "1.0.0" [[deps.Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" [[deps.DelimitedFiles]] deps = ["Mmap"] @@ -349,6 +352,7 @@ weakdeps = ["Mmap", "Test"] [[deps.FileWatching]] uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +version = "1.11.0" [[deps.FixedPointNumbers]] deps = ["Statistics"] @@ -358,9 +362,9 @@ version = "0.8.5" [[deps.ForwardDiff]] deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "LogExpFunctions", "NaNMath", "Preferences", "Printf", "Random", "SpecialFunctions"] -git-tree-sha1 = "a2df1b776752e3f344e5116c06d75a10436ab853" +git-tree-sha1 = "910febccb28d493032495b7009dce7d7f7aee554" uuid = "f6369f11-7733-5829-9624-2563aa707210" -version = "0.10.38" +version = "1.0.1" [deps.ForwardDiff.extensions] ForwardDiffStaticArraysExt = "StaticArrays" @@ -371,6 +375,7 @@ version = "0.10.38" [[deps.Future]] deps = ["Random"] uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" +version = "1.11.0" [[deps.GDAL]] deps = ["CEnum", "GDAL_jll", "NetworkOptions", "PROJ_jll"] @@ -393,7 +398,7 @@ version = "3.11.2+0" [[deps.GMP_jll]] deps = ["Artifacts", "Libdl"] uuid = "781609d7-10c4-51f6-84f2-b8444358ff6d" -version = "6.2.1+6" +version = "6.3.0+0" [[deps.GeoFormatTypes]] git-tree-sha1 = "8e233d5167e63d708d41f87597433f59a0f213fe" @@ -451,9 +456,9 @@ version = "1.14.6+0" [[deps.HTTP]] deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "ExceptionUnwrapping", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "PrecompileTools", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] -git-tree-sha1 = "c67b33b085f6e2faf8bf79a61962e7339a81129c" +git-tree-sha1 = "f93655dc73d7a0b4a368e3c0bce296ae035ad76e" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "1.10.15" +version = "1.10.16" [[deps.HiGHS]] deps = ["HiGHS_jll", "MathOptInterface", "PrecompileTools", "SparseArrays"] @@ -495,6 +500,7 @@ version = "1.4.3" [[deps.InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +version = "1.11.0" [[deps.IntervalSets]] git-tree-sha1 = "dba9ddf07f77f60450fe5d2e2beb9854d9a49bd0" @@ -614,6 +620,7 @@ version = "1.4.0" [[deps.LazyArtifacts]] deps = ["Artifacts", "Pkg"] uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3" +version = "1.11.0" [[deps.LibCURL]] deps = ["LibCURL_jll", "MozillaCACerts_jll"] @@ -623,16 +630,17 @@ version = "0.6.4" [[deps.LibCURL_jll]] deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.4.0+0" +version = "8.6.0+0" [[deps.LibGit2]] deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +version = "1.11.0" [[deps.LibGit2_jll]] deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.6.4+0" +version = "1.7.2+0" [[deps.LibSSH2_jll]] deps = ["Artifacts", "Libdl", "MbedTLS_jll"] @@ -641,6 +649,7 @@ version = "1.11.0+1" [[deps.Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" [[deps.Libtiff_jll]] deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "LERC_jll", "Libdl", "Pkg", "Zlib_jll", "Zstd_jll"] @@ -657,6 +666,7 @@ version = "0.1.4" [[deps.LinearAlgebra]] deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +version = "1.11.0" [[deps.LittleCMS_jll]] deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libtiff_jll", "Pkg"] @@ -682,6 +692,7 @@ version = "0.3.29" [[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +version = "1.11.0" [[deps.LoggingExtras]] deps = ["Dates", "Logging"] @@ -726,9 +737,9 @@ uuid = "d7ed1dd3-d0ae-5e8e-bfb4-87a502085b8d" version = "500.600.201+0" [[deps.MacroTools]] -git-tree-sha1 = "72aebe0b5051e5143a079a4685a46da330a40472" +git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -version = "0.5.15" +version = "0.5.16" [[deps.MappedArrays]] git-tree-sha1 = "2dab0221fe2b0f2cb6754eaa743cc266339f527e" @@ -738,12 +749,13 @@ version = "0.4.2" [[deps.Markdown]] deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +version = "1.11.0" [[deps.MathOptInterface]] deps = ["BenchmarkTools", "CodecBzip2", "CodecZlib", "DataStructures", "ForwardDiff", "JSON3", "LinearAlgebra", "MutableArithmetics", "NaNMath", "OrderedCollections", "PrecompileTools", "Printf", "SparseArrays", "SpecialFunctions", "Test"] -git-tree-sha1 = "6723502b2135aa492a65be9633e694482a340ee7" +git-tree-sha1 = "c2514ede436071529470010540bc3a1441e8140c" uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" -version = "1.38.0" +version = "1.39.0" [[deps.MbedTLS]] deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "NetworkOptions", "Random", "Sockets"] @@ -754,7 +766,7 @@ version = "1.1.9" [[deps.MbedTLS_jll]] deps = ["Artifacts", "Libdl"] uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.28.2+1" +version = "2.28.6+0" [[deps.MicrosoftMPI_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] @@ -770,6 +782,7 @@ version = "1.2.0" [[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" +version = "1.11.0" [[deps.MosaicViews]] deps = ["MappedArrays", "OffsetArrays", "PaddedViews", "StackViews"] @@ -779,7 +792,7 @@ version = "0.3.4" [[deps.MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2023.1.10" +version = "2023.12.12" [[deps.MutableArithmetics]] deps = ["LinearAlgebra", "SparseArrays", "Test"] @@ -804,9 +817,9 @@ uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" version = "1.2.0" [[deps.OffsetArrays]] -git-tree-sha1 = "a414039192a155fb38c4599a60110f0018c6ec82" +git-tree-sha1 = "117432e406b5c023f665fa73dc26e79ec3630151" uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" -version = "1.16.0" +version = "1.17.0" [deps.OffsetArrays.extensions] OffsetArraysAdaptExt = "Adapt" @@ -816,14 +829,14 @@ version = "1.16.0" [[deps.OpenBLAS32_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] -git-tree-sha1 = "6065c4cff8fee6c6770b277af45d5082baacdba1" +git-tree-sha1 = "ece4587683695fe4c5f20e990da0ed7e83c351e7" uuid = "656ef2d0-ae68-5445-9ca0-591084a874a2" -version = "0.3.24+0" +version = "0.3.29+0" [[deps.OpenBLAS_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -version = "0.3.23+4" +version = "0.3.27+1" [[deps.OpenJpeg_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Libtiff_jll", "LittleCMS_jll", "Pkg", "libpng_jll"] @@ -850,9 +863,9 @@ version = "1.4.3" [[deps.OpenSSL_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "a9697f1d06cc3eb3fb3ad49cc67f2cfabaac31ea" +git-tree-sha1 = "9216a80ff3682833ac4b733caa8c00390620ba5d" uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.0.16+0" +version = "3.5.0+0" [[deps.OpenSpecFun_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] @@ -885,14 +898,20 @@ version = "0.5.12" [[deps.Parsers]] deps = ["Dates", "PrecompileTools", "UUIDs"] -git-tree-sha1 = "8489905bcdbcfac64d1daa51ca07c0d8f0283821" +git-tree-sha1 = "44f6c1f38f77cafef9450ff93946c53bd9ca16ff" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.8.1" +version = "2.8.2" [[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.10.0" +version = "1.11.0" + + [deps.Pkg.extensions] + REPLExt = "REPL" + + [deps.Pkg.weakdeps] + REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" [[deps.PooledArrays]] deps = ["DataAPI", "Future"] @@ -921,24 +940,22 @@ version = "2.4.0" [[deps.Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +version = "1.11.0" [[deps.Profile]] -deps = ["Printf"] uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" - -[[deps.REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +version = "1.11.0" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "e87732914598f85a8ba0ad3226df540b84dd1ec7" +git-tree-sha1 = "eec11e26eeeb249eb09daae0d1c3c21f4badc238" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.54.1" +version = "0.55.0" [[deps.Random]] deps = ["SHA"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" [[deps.RangeArrays]] git-tree-sha1 = "b9039e93773ddcfc828f12aadf7115b4b4d225f5" @@ -1030,6 +1047,7 @@ version = "1.4.8" [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +version = "1.11.0" [[deps.SimpleBufferStream]] git-tree-sha1 = "f305871d2f381d21527c770d4788c06c097c9bc1" @@ -1038,6 +1056,7 @@ version = "1.2.0" [[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" +version = "1.11.0" [[deps.SortingAlgorithms]] deps = ["DataStructures"] @@ -1048,13 +1067,13 @@ version = "1.2.1" [[deps.SparseArrays]] deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -version = "1.10.0" +version = "1.11.0" [[deps.SpecialFunctions]] deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] -git-tree-sha1 = "64cca0c26b4f31ba18f13f6c12af7c85f478cfde" +git-tree-sha1 = "41852b8679f78c8d8961eeadc8f62cef861a52e3" uuid = "276daf66-3868-5448-9aa4-cd146d93841b" -version = "2.5.0" +version = "2.5.1" [deps.SpecialFunctions.extensions] SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" @@ -1074,9 +1093,14 @@ uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" version = "1.4.3" [[deps.Statistics]] -deps = ["LinearAlgebra", "SparseArrays"] +deps = ["LinearAlgebra"] +git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0" uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -version = "1.10.0" +version = "1.11.1" +weakdeps = ["SparseArrays"] + + [deps.Statistics.extensions] + SparseArraysExt = ["SparseArrays"] [[deps.StringManipulation]] deps = ["PrecompileTools"] @@ -1093,7 +1117,7 @@ version = "1.11.0" [[deps.SuiteSparse_jll]] deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" -version = "7.2.1+1" +version = "7.7.0+0" [[deps.TOML]] deps = ["Dates"] @@ -1126,6 +1150,7 @@ version = "0.1.1" [[deps.Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +version = "1.11.0" [[deps.TestEnv]] deps = ["Pkg"] @@ -1146,9 +1171,11 @@ version = "1.5.2" [[deps.UUIDs]] deps = ["Random", "SHA"] uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +version = "1.11.0" [[deps.Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" [[deps.Unitful]] deps = ["Dates", "LinearAlgebra", "Random"] @@ -1210,7 +1237,7 @@ version = "1.1.3+0" [[deps.libblastrampoline_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" -version = "5.8.0+1" +version = "5.11.0+0" [[deps.libgeotiff_jll]] deps = ["Artifacts", "JLLWrappers", "LibCURL_jll", "Libdl", "Libtiff_jll", "PROJ_jll", "Pkg"] @@ -1227,7 +1254,7 @@ version = "1.6.47+0" [[deps.nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.52.0+1" +version = "1.59.0+0" [[deps.oneTBB_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] diff --git a/julia_src/http.jl b/julia_src/http.jl index 8e4b0a4fc..1c9e36d99 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -21,6 +21,40 @@ else @warn "Xpress solver is not setup, so only Settings.solver_choice = 'HiGHS', 'Cbc', or 'SCIP' options are available." end +function struct_to_dict(obj) + result = Dict{String, Any}() + if obj === nothing + return result + end + + field_names = fieldnames(typeof(obj)) + for field_name in field_names + field_value = getfield(obj, field_name) + field_name_str = string(field_name) + if field_name_str == "ref" || field_name_str == "mem" || field_name_str == "ptr" + continue + end + if field_value === nothing + result[field_name_str] = "" + elseif typeof(field_value) <: Vector && !isempty(field_value) + # Handle arrays + if all(x -> isstructtype(typeof(x)) || hasproperty(x, :__dict__), field_value) + result[field_name_str] = [struct_to_dict(item) for item in field_value if item !== nothing] + else + result[field_name_str] = collect(field_value) + end + elseif isstructtype(typeof(field_value)) || hasproperty(field_value, :__dict__) + # Nested struct + result[field_name_str] = struct_to_dict(field_value) + else + # Primitive types + result[field_name_str] = field_value + end + end + + return result +end + function reopt(req::HTTP.Request) d = JSON.parse(String(req.body)) error_response = Dict() @@ -40,6 +74,7 @@ function reopt(req::HTTP.Request) timeout_seconds = pop!(settings, "timeout_seconds") optimality_tolerance = pop!(settings, "optimality_tolerance") solver_attributes = SolverAttributes(timeout_seconds, optimality_tolerance) + run_bau = pop!(settings, "run_bau") ms = nothing if run_bau @@ -67,10 +102,14 @@ function reopt(req::HTTP.Request) # Catch handled/unhandled exceptions in optimization try results = reoptjl.run_reopt(ms, model_inputs) - inputs_with_defaults_from_easiur = [ + inputs_with_defaults_from_julia_financial = [ :NOx_grid_cost_per_tonne, :SO2_grid_cost_per_tonne, :PM25_grid_cost_per_tonne, :NOx_onsite_fuelburn_cost_per_tonne, :SO2_onsite_fuelburn_cost_per_tonne, :PM25_onsite_fuelburn_cost_per_tonne, - :NOx_cost_escalation_rate_fraction, :SO2_cost_escalation_rate_fraction, :PM25_cost_escalation_rate_fraction + :NOx_cost_escalation_rate_fraction, :SO2_cost_escalation_rate_fraction, :PM25_cost_escalation_rate_fraction, + :om_cost_escalation_rate_fraction, :elec_cost_escalation_rate_fraction, :existing_boiler_fuel_cost_escalation_rate_fraction, + :boiler_fuel_cost_escalation_rate_fraction, :chp_fuel_cost_escalation_rate_fraction, :generator_fuel_cost_escalation_rate_fraction, + :offtaker_tax_rate_fraction, :offtaker_discount_rate_fraction, :third_party_ownership, + :owner_tax_rate_fraction, :owner_discount_rate_fraction ] inputs_with_defaults_from_avert_or_cambium = [ :emissions_factor_series_lb_CO2_per_kwh, :emissions_factor_series_lb_NOx_per_kwh, @@ -78,38 +117,41 @@ function reopt(req::HTTP.Request) :renewable_energy_fraction_series ] if haskey(d, "CHP") - inputs_with_defaults_from_chp = [ + inputs_with_defaults_from_julia_chp = [ :installed_cost_per_kw, :tech_sizes_for_cost_curve, :om_cost_per_kwh, :electric_efficiency_full_load, :thermal_efficiency_full_load, :min_allowable_kw, :cooling_thermal_factor, :min_turn_down_fraction, :unavailability_periods, :max_kw, - :size_class, :electric_efficiency_half_load, :thermal_efficiency_half_load + :size_class, :electric_efficiency_half_load, :thermal_efficiency_half_load, + :macrs_option_years, :macrs_bonus_fraction, :federal_itc_fraction ] - chp_dict = Dict(key=>getfield(model_inputs.s.chp, key) for key in inputs_with_defaults_from_chp) + chp_dict = Dict(key=>getfield(model_inputs.s.chp, key) for key in inputs_with_defaults_from_julia_chp) else chp_dict = Dict() end if haskey(d, "SteamTurbine") - inputs_with_defaults_from_steamturbine = [ + inputs_with_defaults_from_julia_steamturbine = [ :size_class, :gearbox_generator_efficiency, :isentropic_efficiency, :inlet_steam_pressure_psig, :inlet_steam_temperature_degF, :installed_cost_per_kw, :om_cost_per_kwh, :outlet_steam_pressure_psig, :net_to_gross_electric_ratio, :electric_produced_to_thermal_consumed_ratio, - :thermal_produced_to_thermal_consumed_ratio + :thermal_produced_to_thermal_consumed_ratio, + :macrs_option_years, :macrs_bonus_fraction ] - steamturbine_dict = Dict(key=>getfield(model_inputs.s.steam_turbine, key) for key in inputs_with_defaults_from_steamturbine) + steamturbine_dict = Dict(key=>getfield(model_inputs.s.steam_turbine, key) for key in inputs_with_defaults_from_julia_steamturbine) else steamturbine_dict = Dict() end if haskey(d, "GHP") - inputs_with_defaults_from_ghp = [ + inputs_with_defaults_from_julia_ghp = [ :space_heating_efficiency_thermal_factor, - :cooling_efficiency_thermal_factor + :cooling_efficiency_thermal_factor, + :macrs_option_years, :macrs_bonus_fraction, :federal_itc_fraction ] - ghp_dict = Dict(key=>getfield(model_inputs.s.ghp_option_list[1], key) for key in inputs_with_defaults_from_ghp) + ghp_dict = Dict(key=>getfield(model_inputs.s.ghp_option_list[1], key) for key in inputs_with_defaults_from_julia_ghp) else ghp_dict = Dict() end if haskey(d, "ASHPSpaceHeater") - inputs_with_defaults_from_ashp = [ + inputs_with_defaults_from_julia_ashp = [ :max_ton, :installed_cost_per_ton, :om_cost_per_ton, :macrs_option_years, :macrs_bonus_fraction, :can_supply_steam_turbine, :can_serve_process_heat, :can_serve_dhw, :can_serve_space_heating, :can_serve_cooling, @@ -117,27 +159,27 @@ function reopt(req::HTTP.Request) :heating_reference_temps_degF, :cooling_cop_reference, :cooling_cf_reference, :cooling_reference_temps_degF ] - ashp_dict = Dict(key=>getfield(model_inputs.s.ashp, key) for key in inputs_with_defaults_from_ashp) + ashp_dict = Dict(key=>getfield(model_inputs.s.ashp, key) for key in inputs_with_defaults_from_julia_ashp) else ashp_dict = Dict() end if haskey(d, "ASHPWaterHeater") - inputs_with_defaults_from_ashp_wh = [ + inputs_with_defaults_from_julia_ashp_wh = [ :max_ton, :installed_cost_per_ton, :om_cost_per_ton, :macrs_option_years, :macrs_bonus_fraction, :can_supply_steam_turbine, :can_serve_process_heat, :can_serve_dhw, :can_serve_space_heating, :can_serve_cooling, :back_up_temp_threshold_degF, :sizing_factor, :heating_cop_reference, :heating_cf_reference, :heating_reference_temps_degF ] - ashp_wh_dict = Dict(key=>getfield(model_inputs.s.ashp_wh, key) for key in inputs_with_defaults_from_ashp_wh) + ashp_wh_dict = Dict(key=>getfield(model_inputs.s.ashp_wh, key) for key in inputs_with_defaults_from_julia_ashp_wh) else ashp_wh_dict = Dict() end if haskey(d, "CoolingLoad") - inputs_with_defaults_from_chiller = [ + inputs_with_defaults_from_julia_chiller = [ :cop ] - chiller_dict = Dict(key=>getfield(model_inputs.s.existing_chiller, key) for key in inputs_with_defaults_from_chiller) + chiller_dict = Dict(key=>getfield(model_inputs.s.existing_chiller, key) for key in inputs_with_defaults_from_julia_chiller) else chiller_dict = Dict() end @@ -147,15 +189,55 @@ function reopt(req::HTTP.Request) site_dict = Dict() end if haskey(d, "PV") - inputs_with_defaults_from_pv = [ - :size_class, :installed_cost_per_kw, :om_cost_per_kw + inputs_with_defaults_from_julia_pv = [ + :size_class, :installed_cost_per_kw, :om_cost_per_kw, :macrs_option_years, :macrs_bonus_fraction, :federal_itc_fraction ] - pv_dict = Dict(key=>getfield(model_inputs.s.pvs[1], key) for key in inputs_with_defaults_from_pv) + pv_dict = Dict(key=>getfield(model_inputs.s.pvs[1], key) for key in inputs_with_defaults_from_julia_pv) else pv_dict = Dict() - end + end + if haskey(d, "Wind") + inputs_with_defaults_from_julia_wind = [ + :macrs_option_years, :macrs_bonus_fraction, :federal_itc_fraction + ] + wind_dict = Dict(key=>getfield(model_inputs.s.wind, key) for key in inputs_with_defaults_from_julia_wind) + else + wind_dict = Dict() + end + if haskey(d, "ElectricStorage") + inputs_with_defaults_from_julia_electric_storage = [ + :macrs_option_years, :macrs_bonus_fraction, :total_itc_fraction + ] + electric_storage_dict = Dict(key=>getfield(model_inputs.s.storage.attr["ElectricStorage"], key) for key in inputs_with_defaults_from_julia_electric_storage) + else + electric_storage_dict = Dict() + end + if haskey(d, "ColdThermalStorage") + inputs_with_defaults_from_julia_cold_storage = [ + :macrs_option_years, :macrs_bonus_fraction, :total_itc_fraction + ] + cold_storage_dict = Dict(key=>getfield(model_inputs.s.storage.attr["ColdThermalStorage"], key) for key in inputs_with_defaults_from_julia_cold_storage) + else + cold_storage_dict = Dict() + end + if haskey(d, "HotThermalStorage") + inputs_with_defaults_from_julia_hot_storage = [ + :macrs_option_years, :macrs_bonus_fraction, :total_itc_fraction + ] + hot_storage_dict = Dict(key=>getfield(model_inputs.s.storage.attr["HotThermalStorage"], key) for key in inputs_with_defaults_from_julia_hot_storage) + else + hot_storage_dict = Dict() + end + if haskey(d, "HighTempThermalStorage") + inputs_with_defaults_from_julia_high_temp_storage = [ + :macrs_option_years, :macrs_bonus_fraction, :total_itc_fraction + ] + high_temp_storage_dict = Dict(key=>getfield(model_inputs.s.storage.attr["HighTempThermalStorage"], key) for key in inputs_with_defaults_from_julia_high_temp_storage) + else + high_temp_storage_dict = Dict() + end inputs_with_defaults_set_in_julia = Dict( - "Financial" => Dict(key=>getfield(model_inputs.s.financial, key) for key in inputs_with_defaults_from_easiur), + "Financial" => Dict(key=>getfield(model_inputs.s.financial, key) for key in inputs_with_defaults_from_julia_financial), "ElectricUtility" => Dict(key=>getfield(model_inputs.s.electric_utility, key) for key in inputs_with_defaults_from_avert_or_cambium), "Site" => site_dict, "CHP" => chp_dict, @@ -164,7 +246,12 @@ function reopt(req::HTTP.Request) "ExistingChiller" => chiller_dict, "ASHPSpaceHeater" => ashp_dict, "ASHPWaterHeater" => ashp_wh_dict, - "PV" => pv_dict + "PV" => pv_dict, + "Wind" => wind_dict, + "ElectricStorage" => electric_storage_dict, + "ColdThermalStorage" => cold_storage_dict, + "HotThermalStorage" => hot_storage_dict, + "HighTempThermalStorage" => high_temp_storage_dict ) catch e @error "Something went wrong in REopt optimization!" exception=(e, catch_backtrace()) @@ -419,6 +506,33 @@ function easiur_costs(req::HTTP.Request) return HTTP.Response(200, JSON.json(data)) end +function sector_defaults(req::HTTP.Request) + d = JSON.parse(String(req.body)) + @info "Getting sector dependent defaults..." + data = Dict() + error_response = Dict() + try + sector = d["sector"] + federal_sector_state = d["federal_sector_state"] + federal_procurement_type = d["federal_procurement_type"] + data = reoptjl.get_sector_defaults(;sector=sector, federal_procurement_type=federal_procurement_type, federal_sector_state=federal_sector_state) + if haskey(data, "error") + @info "An error occurred getting the sector defaults" + return HTTP.Response(400, JSON.json(data)) + end + if isempty(data) + @info "No sector defaults found for the provided inputs" + return HTTP.Response(400, JSON.json(Dict("error" => "No sector defaults found for the provided inputs"))) + end + catch e + @error "Something went wrong getting the sector defaults" exception=(e, catch_backtrace()) + error_response["error"] = sprint(showerror, e) + return HTTP.Response(500, JSON.json(error_response)) + end + @info "Sector defaults determined." + return HTTP.Response(200, JSON.json(data)) +end + function simulated_load(req::HTTP.Request) d = JSON.parse(String(req.body)) @@ -656,6 +770,7 @@ HTTP.register!(ROUTER, "GET", "/chp_defaults", chp_defaults) HTTP.register!(ROUTER, "GET", "/avert_emissions_profile", avert_emissions_profile) HTTP.register!(ROUTER, "GET", "/cambium_profile", cambium_profile) HTTP.register!(ROUTER, "GET", "/easiur_costs", easiur_costs) +HTTP.register!(ROUTER, "GET", "/sector_defaults", sector_defaults) HTTP.register!(ROUTER, "GET", "/simulated_load", simulated_load) HTTP.register!(ROUTER, "GET", "/absorption_chiller_defaults", absorption_chiller_defaults) HTTP.register!(ROUTER, "GET", "/ghp_efficiency_thermal_factors", ghp_efficiency_thermal_factors) diff --git a/reo/src/run_jump_model.py b/reo/src/run_jump_model.py index 9b56242d3..f2784719a 100644 --- a/reo/src/run_jump_model.py +++ b/reo/src/run_jump_model.py @@ -82,9 +82,6 @@ def run_jump_model(dfm, data, bau=False): # ADD JULIA EXCEPTION HERE exc_type, exc_value, exc_traceback = sys.exc_info() - print(exc_type) - print(exc_value) - print(exc_traceback) logger.error("REopt.py raise unexpected error: UUID: " + str(run_uuid)) raise UnexpectedError(exc_type, exc_value, traceback.format_tb(exc_traceback), task=name, run_uuid=run_uuid, user_uuid=user_uuid) else: diff --git a/reoptjl/migrations/0105_siteinputs_federal_procurement_type_and_more.py b/reoptjl/migrations/0105_siteinputs_federal_procurement_type_and_more.py new file mode 100644 index 000000000..a7d92d39c --- /dev/null +++ b/reoptjl/migrations/0105_siteinputs_federal_procurement_type_and_more.py @@ -0,0 +1,214 @@ +# Generated by Django 4.0.7 on 2025-09-30 17:21 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0104_cstinputs_can_waste_heat'), + ] + + operations = [ + migrations.AddField( + model_name='siteinputs', + name='federal_procurement_type', + field=models.TextField(blank=True, choices=[('fedowned_dirpurch', 'Fedowned Dirpurch'), ('fedowned_thirdparty', 'Fedowned Third Party'), ('privateowned_thirdparty', 'Privateowned Third Party')], default='', help_text="The capital procurement type if the site's sector is 'federal'. Options: ['fedowned_dirpurch', 'fedowned_thirdparty', 'privateowned_thirdparty']"), + ), + migrations.AddField( + model_name='siteinputs', + name='federal_sector_state', + field=models.TextField(blank=True, default='', help_text="The state where the site is located, if the site's sector is 'federal'. State can be written out or abbreviated."), + ), + migrations.AddField( + model_name='siteinputs', + name='sector', + field=models.TextField(blank=True, choices=[('commercial/industrial', 'Commercial'), ('federal', 'Federal')], default='commercial/industrial', help_text="The sector of the site. Options: ['federal', 'commercial/industrial']"), + ), + migrations.AlterField( + model_name='chpinputs', + name='federal_itc_fraction', + field=models.FloatField(blank=True, help_text='Percentage of capital costs that are credited towards federal taxes', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='chpinputs', + name='macrs_bonus_fraction', + field=models.FloatField(blank=True, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='chpinputs', + name='macrs_option_years', + field=models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True), + ), + migrations.AlterField( + model_name='coldthermalstorageinputs', + name='macrs_bonus_fraction', + field=models.FloatField(blank=True, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='coldthermalstorageinputs', + name='macrs_option_years', + field=models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True), + ), + migrations.AlterField( + model_name='coldthermalstorageinputs', + name='total_itc_fraction', + field=models.FloatField(blank=True, help_text='Total investment tax credit in percent applied toward capital costs', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='macrs_bonus_fraction', + field=models.FloatField(blank=True, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='macrs_option_years', + field=models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='total_itc_fraction', + field=models.FloatField(blank=True, help_text='Total investment tax credit in percent applied toward capital costs', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='financialinputs', + name='boiler_fuel_cost_escalation_rate_fraction', + field=models.FloatField(blank=True, help_text='Annual nominal boiler fuel cost escalation rate', null=True, validators=[django.core.validators.MinValueValidator(-1), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='financialinputs', + name='chp_fuel_cost_escalation_rate_fraction', + field=models.FloatField(blank=True, help_text='Annual nominal chp fuel cost escalation rate', null=True, validators=[django.core.validators.MinValueValidator(-1), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='financialinputs', + name='elec_cost_escalation_rate_fraction', + field=models.FloatField(blank=True, help_text='Annual nominal utility electricity cost escalation rate.', null=True, validators=[django.core.validators.MinValueValidator(-1), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='financialinputs', + name='existing_boiler_fuel_cost_escalation_rate_fraction', + field=models.FloatField(blank=True, help_text='Annual nominal existing boiler fuel cost escalation rate', null=True, validators=[django.core.validators.MinValueValidator(-1), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='financialinputs', + name='generator_fuel_cost_escalation_rate_fraction', + field=models.FloatField(blank=True, help_text='Annual nominal boiler fuel cost escalation rate', null=True, validators=[django.core.validators.MinValueValidator(-1), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='financialinputs', + name='offtaker_discount_rate_fraction', + field=models.FloatField(blank=True, help_text='Nominal energy offtaker discount rate. In single ownership model the offtaker is also the generation owner.', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='financialinputs', + name='offtaker_tax_rate_fraction', + field=models.FloatField(blank=True, help_text='Host tax rate', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(0.999)]), + ), + migrations.AlterField( + model_name='financialinputs', + name='om_cost_escalation_rate_fraction', + field=models.FloatField(blank=True, help_text='Annual nominal O&M cost escalation rate', null=True, validators=[django.core.validators.MinValueValidator(-1), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='financialinputs', + name='owner_discount_rate_fraction', + field=models.FloatField(blank=True, help_text='Nominal generation owner discount rate. Used for two party financing model. In two party ownership model the offtaker does not own the generator(s).', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='financialinputs', + name='owner_tax_rate_fraction', + field=models.FloatField(blank=True, help_text='Generation owner tax rate. Used for two party financing model. In two party ownership model the offtaker does not own the generator(s).', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(0.999)]), + ), + migrations.AlterField( + model_name='financialinputs', + name='third_party_ownership', + field=models.BooleanField(blank=True, help_text='Specify if ownership model is direct ownership or two party. In two party model the offtaker does not purcharse the generation technologies, but pays the generation owner for energy from the generator(s).', null=True), + ), + migrations.AlterField( + model_name='ghpinputs', + name='federal_itc_fraction', + field=models.FloatField(blank=True, help_text='Percentage of capital costs that are credited towards federal taxes', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='ghpinputs', + name='macrs_bonus_fraction', + field=models.FloatField(blank=True, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='ghpinputs', + name='macrs_option_years', + field=models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True), + ), + migrations.AlterField( + model_name='hightempthermalstorageinputs', + name='macrs_bonus_fraction', + field=models.FloatField(blank=True, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='hightempthermalstorageinputs', + name='macrs_option_years', + field=models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True), + ), + migrations.AlterField( + model_name='hightempthermalstorageinputs', + name='total_itc_fraction', + field=models.FloatField(blank=True, help_text='Total investment tax credit in percent applied toward capital costs', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='hotthermalstorageinputs', + name='macrs_bonus_fraction', + field=models.FloatField(blank=True, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='hotthermalstorageinputs', + name='macrs_option_years', + field=models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True), + ), + migrations.AlterField( + model_name='hotthermalstorageinputs', + name='total_itc_fraction', + field=models.FloatField(blank=True, help_text='Total investment tax credit in percent applied toward capital costs', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='pvinputs', + name='federal_itc_fraction', + field=models.FloatField(blank=True, help_text='Percentage of capital costs that are credited towards federal taxes', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='pvinputs', + name='macrs_bonus_fraction', + field=models.FloatField(blank=True, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='pvinputs', + name='macrs_option_years', + field=models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='macrs_bonus_fraction', + field=models.FloatField(blank=True, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='steamturbineinputs', + name='macrs_option_years', + field=models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True), + ), + migrations.AlterField( + model_name='windinputs', + name='federal_itc_fraction', + field=models.FloatField(blank=True, help_text='Percentage of capital costs that are credited towards federal taxes', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='windinputs', + name='macrs_bonus_fraction', + field=models.FloatField(blank=True, help_text='Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), + ), + migrations.AlterField( + model_name='windinputs', + name='macrs_option_years', + field=models.IntegerField(blank=True, choices=[(0, 'Zero'), (5, 'Five'), (7, 'Seven')], help_text='Duration over which accelerated depreciation will occur. Set to zero to disable', null=True), + ), + ] diff --git a/reoptjl/migrations/0106_ghpoutputs_number_of_boreholes_flipped_guess.py b/reoptjl/migrations/0106_ghpoutputs_number_of_boreholes_flipped_guess.py new file mode 100644 index 000000000..67b5e5bb9 --- /dev/null +++ b/reoptjl/migrations/0106_ghpoutputs_number_of_boreholes_flipped_guess.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.7 on 2025-10-01 00:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0105_siteinputs_federal_procurement_type_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='ghpoutputs', + name='number_of_boreholes_flipped_guess', + field=models.FloatField(blank=True, null=True), + ), + ] diff --git a/reoptjl/migrations/0107_ghpoutputs_hybrid_solution_type_and_more.py b/reoptjl/migrations/0107_ghpoutputs_hybrid_solution_type_and_more.py new file mode 100644 index 000000000..67c1b2e19 --- /dev/null +++ b/reoptjl/migrations/0107_ghpoutputs_hybrid_solution_type_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 4.0.7 on 2025-10-01 19:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0106_ghpoutputs_number_of_boreholes_flipped_guess'), + ] + + operations = [ + migrations.AddField( + model_name='ghpoutputs', + name='hybrid_solution_type', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='ghpoutputs', + name='iterations_auto_guess', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='ghpoutputs', + name='iterations_flipped_guess', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='ghpoutputs', + name='iterations_nonhybrid', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='ghpoutputs', + name='number_of_boreholes_auto_guess', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='ghpoutputs', + name='number_of_boreholes_nonhybrid', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='ghpoutputs', + name='solve_time_min', + field=models.FloatField(blank=True, null=True), + ), + ] diff --git a/reoptjl/migrations/0108_merge_20251003_1858.py b/reoptjl/migrations/0108_merge_20251003_1858.py new file mode 100644 index 000000000..0b48e2f80 --- /dev/null +++ b/reoptjl/migrations/0108_merge_20251003_1858.py @@ -0,0 +1,14 @@ +# Generated by Django 4.0.7 on 2025-10-03 18:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0105_alter_chpoutputs_initial_capital_costs_and_more'), + ('reoptjl', '0107_ghpoutputs_hybrid_solution_type_and_more'), + ] + + operations = [ + ] diff --git a/reoptjl/migrations/0109_remove_ghpoutputs_iterations_auto_guess_and_more.py b/reoptjl/migrations/0109_remove_ghpoutputs_iterations_auto_guess_and_more.py new file mode 100644 index 000000000..3820d7d08 --- /dev/null +++ b/reoptjl/migrations/0109_remove_ghpoutputs_iterations_auto_guess_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.0.7 on 2025-10-03 19:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0108_merge_20251003_1858'), + ] + + operations = [ + migrations.RemoveField( + model_name='ghpoutputs', + name='iterations_auto_guess', + ), + migrations.RemoveField( + model_name='ghpoutputs', + name='iterations_flipped_guess', + ), + migrations.RemoveField( + model_name='ghpoutputs', + name='iterations_nonhybrid', + ), + migrations.RemoveField( + model_name='ghpoutputs', + name='number_of_boreholes_auto_guess', + ), + migrations.RemoveField( + model_name='ghpoutputs', + name='number_of_boreholes_flipped_guess', + ), + migrations.RemoveField( + model_name='ghpoutputs', + name='number_of_boreholes_nonhybrid', + ), + migrations.RemoveField( + model_name='ghpoutputs', + name='solve_time_min', + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index 450625647..d535fe252 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -332,6 +332,15 @@ class SiteInputs(BaseModel, models.Model): primary_key=True ) + class SECTORS(models.TextChoices): + COMMERCIAL = 'commercial/industrial' + FEDERAL = 'federal' + + class FEDERAL_PROCUREMENT_TYPES(models.TextChoices): + FEDOWNED_DIRPURCH = 'fedowned_dirpurch' + FEDOWNED_THIRD_PARTY = 'fedowned_thirdparty' + PRIVATEOWNED_THIRD_PARTY = 'privateowned_thirdparty' + latitude = models.FloatField( validators=[ MinValueValidator(-90), @@ -374,6 +383,27 @@ class SiteInputs(BaseModel, models.Model): ) # don't provide mg_tech_sizes_equal_grid_sizes in the API, effectively force it to true (the REopt.jl default) + sector = models.TextField( + choices=SECTORS.choices, + blank=True, + null=False, + default=SECTORS.COMMERCIAL, + help_text=("The sector of the site. Options: ['federal', 'commercial/industrial']") + ) + federal_sector_state = models.TextField( + # not creating choices b/c hoping to get rid of this input ASAP and use lat/long instead + blank=True, + null=False, + default="", + help_text=("The state where the site is located, if the site's sector is 'federal'. State can be written out or abbreviated.") + ) + federal_procurement_type = models.TextField( + choices=FEDERAL_PROCUREMENT_TYPES.choices, + blank=True, + null=False, + default="", + help_text=("The capital procurement type if the site's sector is 'federal'. Options: ['fedowned_dirpurch', 'fedowned_thirdparty', 'privateowned_thirdparty']") + ) CO2_emissions_reduction_min_fraction = models.FloatField( validators=[ MinValueValidator(0), @@ -434,6 +464,16 @@ class SiteInputs(BaseModel, models.Model): help_text=("The outdoor air (dry-bulb) temperature in degrees Fahrenheit as determined by the site's location TMY3 data from the PVWatts call or user input. This is used for GHP COP and ASHP COP and CF values based on the default or custom mapping of those.") ) + def clean(self): + if self.sector == self.SECTORS.FEDERAL: + error_messages = {} + if self.federal_procurement_type == "": + error_messages["required inputs"] = "If sector is federal, must provide federal_procurement_type." + if self.federal_sector_state == "": + error_messages["required inputs"] = "If sector is federal, must provide federal_sector_state." + if error_messages: + raise ValidationError(error_messages) + class SiteOutputs(BaseModel, models.Model): key = "SiteOutputs" @@ -677,65 +717,65 @@ class FinancialInputs(BaseModel, models.Model): help_text="Analysis period in years. Must be integer." ) elec_cost_escalation_rate_fraction = models.FloatField( - default=0.0166, validators=[ MinValueValidator(-1), MaxValueValidator(1) ], blank=True, + null=True, help_text="Annual nominal utility electricity cost escalation rate." ) offtaker_discount_rate_fraction = models.FloatField( - default=0.0624, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text=("Nominal energy offtaker discount rate. In single ownership model the offtaker is also the " "generation owner.") ) offtaker_tax_rate_fraction = models.FloatField( - default=0.26, validators=[ MinValueValidator(0), MaxValueValidator(0.999) ], blank=True, + null=True, help_text="Host tax rate" ) om_cost_escalation_rate_fraction = models.FloatField( - default=0.025, validators=[ MinValueValidator(-1), MaxValueValidator(1) ], blank=True, + null=True, help_text="Annual nominal O&M cost escalation rate" ) owner_discount_rate_fraction = models.FloatField( - default=0.0624, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text=("Nominal generation owner discount rate. Used for two party financing model. In two party ownership " "model the offtaker does not own the generator(s).") ) owner_tax_rate_fraction = models.FloatField( - default=0.26, validators=[ MinValueValidator(0), MaxValueValidator(0.999) ], blank=True, + null=True, help_text=("Generation owner tax rate. Used for two party financing model. In two party ownership model the " "offtaker does not own the generator(s).") ) third_party_ownership = models.BooleanField( - default=False, blank=True, + null=True, help_text=("Specify if ownership model is direct ownership or two party. In two party model the offtaker does " "not purcharse the generation technologies, but pays the generation owner for energy from the " "generator(s).") @@ -906,39 +946,39 @@ class FinancialInputs(BaseModel, models.Model): help_text=("Annual nominal escalation rate of the public health cost of 1 tonne of PM2.5 emissions (as a decimal). The default value is calculated from the EASIUR model for a height of 150m.") ) generator_fuel_cost_escalation_rate_fraction = models.FloatField( - default=0.0197, validators=[ MinValueValidator(-1), MaxValueValidator(1) ], blank=True, + null=True, help_text=("Annual nominal boiler fuel cost escalation rate") ) existing_boiler_fuel_cost_escalation_rate_fraction = models.FloatField( - default=0.0348, validators=[ MinValueValidator(-1), MaxValueValidator(1) ], blank=True, + null=True, help_text=("Annual nominal existing boiler fuel cost escalation rate") ) boiler_fuel_cost_escalation_rate_fraction = models.FloatField( - default=0.0348, validators=[ MinValueValidator(-1), MaxValueValidator(1) ], blank=True, + null=True, help_text=("Annual nominal boiler fuel cost escalation rate") ) chp_fuel_cost_escalation_rate_fraction = models.FloatField( - default=0.0348, validators=[ MinValueValidator(-1), MaxValueValidator(1) ], blank=True, + null=True, help_text=("Annual nominal chp fuel cost escalation rate") ) @@ -2779,18 +2819,18 @@ class PV_LOCATION_CHOICES(models.TextChoices): help_text="Annual PV operations and maintenance costs in $/kW" ) macrs_option_years = models.IntegerField( - default=MACRS_YEARS_CHOICES.FIVE, choices=MACRS_YEARS_CHOICES.choices, blank=True, + null=True, help_text="Duration over which accelerated depreciation will occur. Set to zero to disable" ) macrs_bonus_fraction = models.FloatField( - default=1.0, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" ) macrs_itc_reduction = models.FloatField( @@ -2803,12 +2843,12 @@ class PV_LOCATION_CHOICES(models.TextChoices): help_text="Percent of the ITC value by which depreciable basis is reduced" ) federal_itc_fraction = models.FloatField( - default=0.3, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Percentage of capital costs that are credited towards federal taxes" ) state_ibi_fraction = models.FloatField( @@ -3223,18 +3263,18 @@ class WIND_SIZE_CLASS_CHOICES(models.TextChoices): help_text="Annual operations and maintenance costs in $/kW" ) macrs_option_years = models.IntegerField( - default=MACRS_YEARS_CHOICES.FIVE, choices=MACRS_YEARS_CHOICES.choices, blank=True, + null=True, help_text="Duration over which accelerated depreciation will occur. Set to zero to disable" ) macrs_bonus_fraction = models.FloatField( - default=1.0, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" ) macrs_itc_reduction = models.FloatField( @@ -3247,12 +3287,12 @@ class WIND_SIZE_CLASS_CHOICES(models.TextChoices): help_text="Percent of the ITC value by which depreciable basis is reduced" ) federal_itc_fraction = models.FloatField( - default=0.3, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Percentage of capital costs that are credited towards federal taxes" ) state_ibi_fraction = models.FloatField( @@ -3642,18 +3682,18 @@ class ElectricStorageInputs(BaseModel, models.Model): help_text="Annual O&M cost as a fraction of installed cost." ) macrs_option_years = models.IntegerField( - default=MACRS_YEARS_CHOICES.FIVE, choices=MACRS_YEARS_CHOICES.choices, blank=True, + null=True, help_text="Duration over which accelerated depreciation will occur. Set to zero to disable" ) macrs_bonus_fraction = models.FloatField( - default=1.0, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" ) macrs_itc_reduction = models.FloatField( @@ -3666,12 +3706,12 @@ class ElectricStorageInputs(BaseModel, models.Model): help_text="Percent of the ITC value by which depreciable basis is reduced" ) total_itc_fraction = models.FloatField( - default=0.3, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Total investment tax credit in percent applied toward capital costs" ) total_rebate_per_kw = models.FloatField( @@ -4432,18 +4472,18 @@ class CHPInputs(BaseModel, models.Model): #Financial and emissions macrs_option_years = models.IntegerField( - default=MACRS_YEARS_CHOICES.FIVE, choices=MACRS_YEARS_CHOICES.choices, blank=True, + null=True, help_text="Duration over which accelerated depreciation will occur. Set to zero to disable" ) macrs_bonus_fraction = models.FloatField( - default=1.0, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" ) macrs_itc_reduction = models.FloatField( @@ -4456,12 +4496,12 @@ class CHPInputs(BaseModel, models.Model): help_text="Percent of the ITC value by which depreciable basis is reduced" ) federal_itc_fraction = models.FloatField( - default=0.0, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Percentage of capital costs that are credited towards federal taxes" ) federal_rebate_per_kw = models.FloatField( @@ -6698,7 +6738,6 @@ class SIZE_CLASS_LIST(models.IntegerChoices): ) macrs_option_years = models.IntegerField( - default=MACRS_YEARS_CHOICES.FIVE, choices=MACRS_YEARS_CHOICES.choices, null=True, blank=True, @@ -6706,7 +6745,6 @@ class SIZE_CLASS_LIST(models.IntegerChoices): ) macrs_bonus_fraction = models.FloatField( - default=1.0, validators=[ MinValueValidator(0), MaxValueValidator(1) @@ -6904,18 +6942,18 @@ class HotThermalStorageInputs(BaseModel, models.Model): help_text="Thermal energy-based cost of TES (e.g. volume of the tank)" ) macrs_option_years = models.IntegerField( - default=MACRS_YEARS_CHOICES.FIVE, choices=MACRS_YEARS_CHOICES.choices, blank=True, + null=True, help_text="Duration over which accelerated depreciation will occur. Set to zero to disable" ) macrs_bonus_fraction = models.FloatField( - default=1.0, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" ) macrs_itc_reduction = models.FloatField( @@ -6928,12 +6966,12 @@ class HotThermalStorageInputs(BaseModel, models.Model): help_text="Percent of the ITC value by which depreciable basis is reduced" ) total_itc_fraction = models.FloatField( - default=0.3, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Total investment tax credit in percent applied toward capital costs" ) total_rebate_per_kwh = models.FloatField( @@ -7118,18 +7156,18 @@ class HighTempThermalStorageInputs(BaseModel, models.Model): help_text="Thermal energy-based cost of TES (e.g. volume of the tank)" ) macrs_option_years = models.IntegerField( - default=MACRS_YEARS_CHOICES.FIVE, choices=MACRS_YEARS_CHOICES.choices, blank=True, + null=True, help_text="Duration over which accelerated depreciation will occur. Set to zero to disable" ) macrs_bonus_fraction = models.FloatField( - default=1.0, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" ) macrs_itc_reduction = models.FloatField( @@ -7142,12 +7180,12 @@ class HighTempThermalStorageInputs(BaseModel, models.Model): help_text="Percent of the ITC value by which depreciable basis is reduced" ) total_itc_fraction = models.FloatField( - default=0.3, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Total investment tax credit in percent applied toward capital costs" ) total_rebate_per_kwh = models.FloatField( @@ -7334,18 +7372,18 @@ class ColdThermalStorageInputs(BaseModel, models.Model): help_text="Thermal energy-based cost of TES (e.g. volume of the tank)" ) macrs_option_years = models.IntegerField( - default=MACRS_YEARS_CHOICES.FIVE, choices=MACRS_YEARS_CHOICES.choices, blank=True, + null=True, help_text="Duration over which accelerated depreciation will occur. Set to zero to disable" ) macrs_bonus_fraction = models.FloatField( - default=1.0, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" ) macrs_itc_reduction = models.FloatField( @@ -7358,12 +7396,12 @@ class ColdThermalStorageInputs(BaseModel, models.Model): help_text="Percent of the ITC value by which depreciable basis is reduced" ) total_itc_fraction = models.FloatField( - default=0.3, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Total investment tax credit in percent applied toward capital costs" ) total_rebate_per_kwh = models.FloatField( @@ -8635,18 +8673,18 @@ class GHPInputs(BaseModel, models.Model): ) macrs_option_years = models.IntegerField( - default=MACRS_YEARS_CHOICES.ZERO, choices=MACRS_YEARS_CHOICES.choices, blank=True, + null=True, help_text="Duration over which accelerated depreciation will occur. Set to zero to disable" ) macrs_bonus_fraction = models.FloatField( - default=0.0, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" ) macrs_itc_reduction = models.FloatField( @@ -8659,12 +8697,12 @@ class GHPInputs(BaseModel, models.Model): help_text="Percent of the ITC value by which depreciable basis is reduced" ) federal_itc_fraction = models.FloatField( - default=0.3, validators=[ MinValueValidator(0), MaxValueValidator(1) ], blank=True, + null=True, help_text="Percentage of capital costs that are credited towards federal taxes" ) state_ibi_fraction = models.FloatField( @@ -8819,7 +8857,7 @@ class GHPOutputs(BaseModel, models.Model): avoided_capex_by_ghp_present_value = models.FloatField(null=True, blank=True) annual_thermal_production_mmbtu = models.FloatField(null=True, blank=True) annual_thermal_production_tonhour = models.FloatField(null=True, blank=True) - + hybrid_solution_type = models.TextField(null=True, blank=True) class CSTInputs(BaseModel, models.Model): key = "CST" @@ -9076,7 +9114,7 @@ def get_input_dict_from_run_uuid(run_uuid:str): ).get(run_uuid=run_uuid) def filter_none_and_empty_array(d:dict): - return {k: v for (k, v) in d.items() if v not in [None, [], {}]} + return {k: v for (k, v) in d.items() if v not in [None, [], {}, ""]} d = dict() d["user_uuid"] = meta.user_uuid diff --git a/reoptjl/src/process_results.py b/reoptjl/src/process_results.py index c1faad9e7..20b26e209 100644 --- a/reoptjl/src/process_results.py +++ b/reoptjl/src/process_results.py @@ -1,10 +1,10 @@ # REoptĀ®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt_API/blob/master/LICENSE. -from reoptjl.models import FinancialOutputs, APIMeta, PVOutputs, ElectricStorageOutputs,\ +from reoptjl.models import FinancialOutputs, APIMeta, PVOutputs, ElectricStorageInputs, ElectricStorageOutputs,\ ElectricTariffOutputs, SiteOutputs, ElectricUtilityOutputs,\ - GeneratorOutputs, ElectricLoadOutputs, WindOutputs, FinancialInputs,\ + GeneratorOutputs, ElectricLoadOutputs, WindInputs, WindOutputs, FinancialInputs,\ ElectricUtilityInputs, ExistingBoilerOutputs, CHPOutputs, CHPInputs, \ ExistingChillerOutputs, CoolingLoadOutputs, HeatingLoadOutputs,\ - HotThermalStorageOutputs, ColdThermalStorageOutputs, OutageOutputs,\ + HotThermalStorageInputs, HotThermalStorageOutputs, ColdThermalStorageInputs, ColdThermalStorageOutputs, OutageOutputs,\ REoptjlMessageOutputs, AbsorptionChillerOutputs, BoilerOutputs, SteamTurbineInputs, \ SteamTurbineOutputs, GHPInputs, GHPOutputs, ExistingChillerInputs, \ ElectricHeaterOutputs, ASHPSpaceHeaterOutputs, ASHPWaterHeaterOutputs, \ @@ -84,6 +84,10 @@ def process_results(results: dict, run_uuid: str) -> None: if "SteamTurbine" in results.keys(): SteamTurbineOutputs.create(meta=meta, **results["SteamTurbine"]).save() if "GHP" in results.keys(): + for pop_key in ["solve_time_min", "number_of_boreholes_nonhybrid", + "number_of_boreholes_auto_guess", "number_of_boreholes_flipped_guess", + "iterations_nonhybrid", "iterations_auto_guess", "iterations_flipped_guess"]: + results["GHP"].pop(pop_key, None) GHPOutputs.create(meta=meta, **results["GHP"]).save() if "ElectricHeater" in results.keys(): ElectricHeaterOutputs.create(meta=meta, **results["ElectricHeater"]).save() @@ -153,6 +157,21 @@ def update_inputs_in_database(inputs_to_update: dict, run_uuid: str) -> None: if inputs_to_update["PV"]: prune_update_fields(PVInputs, inputs_to_update["PV"]) PVInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["PV"]) + if inputs_to_update["Wind"]: + prune_update_fields(WindInputs, inputs_to_update["Wind"]) + WindInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["Wind"]) + if inputs_to_update["ElectricStorage"]: + prune_update_fields(ElectricStorageInputs, inputs_to_update["ElectricStorage"]) + ElectricStorageInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ElectricStorage"]) + if inputs_to_update["ColdThermalStorage"]: + prune_update_fields(ColdThermalStorageInputs, inputs_to_update["ColdThermalStorage"]) + ColdThermalStorageInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ColdThermalStorage"]) + if inputs_to_update["HotThermalStorage"]: + prune_update_fields(HotThermalStorageInputs, inputs_to_update["HotThermalStorage"]) + HotThermalStorageInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["HotThermalStorage"]) + if inputs_to_update["HighTempThermalStorage"]: + prune_update_fields(HighTempThermalStorageInputs, inputs_to_update["HighTempThermalStorage"]) + HighTempThermalStorageInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["HighTempThermalStorage"]) # TODO CST is not added to this inputs_with_defaults_set_in_julia dictionary in http.jl, IF we need to update any CST inputs if inputs_to_update.get("CST") is not None: prune_update_fields(CSTInputs, inputs_to_update["CST"]) diff --git a/reoptjl/test/posts/all_inputs_test.json b/reoptjl/test/posts/all_inputs_test.json index 7bfb1c979..f00b61591 100644 --- a/reoptjl/test/posts/all_inputs_test.json +++ b/reoptjl/test/posts/all_inputs_test.json @@ -58,7 +58,10 @@ "include_exported_elec_emissions_in_total": true, "include_exported_renewable_electricity_in_total": true, "min_resil_time_steps": 100, - "include_grid_renewable_fraction_in_RE_constraints": false + "include_grid_renewable_fraction_in_RE_constraints": false, + "sector": "commercial/industrial", + "federal_sector_state": "", + "federal_procurement_type": "" }, "Settings": { "timeout_seconds": 420, diff --git a/reoptjl/test/posts/sector_defaults_post.json b/reoptjl/test/posts/sector_defaults_post.json new file mode 100644 index 000000000..1df5abbae --- /dev/null +++ b/reoptjl/test/posts/sector_defaults_post.json @@ -0,0 +1,66 @@ +{ + "Settings": {"solver_name": "HiGHS", "run_bau": false}, + "Site": { + "latitude": 37.78, + "longitude": -122.45, + "sector": "federal", + "federal_procurement_type": "fedowned_dirpurch", + "federal_sector_state": "CA" + }, + "ElectricLoad": { + "doe_reference_name": "Hospital" + }, + "SpaceHeatingLoad": { + "doe_reference_name": "Hospital" + }, + "Financial": { + "om_cost_escalation_rate_fraction": 0.02, + "chp_fuel_cost_escalation_rate_fraction": 0.025 + }, + "ElectricTariff": { + "urdb_label": "5cef0a415457a33576f60fe2" + }, + "ExistingBoiler": { + "fuel_cost_per_mmbtu": 10.0 + }, + "CHP": { + "prime_mover": "recip_engine", + "max_kw": 100, + "fuel_cost_per_mmbtu": 10.0, + "can_supply_steam_turbine": true + }, + "PV": { + "min_kw": 1000.0, + "max_kw": 1000.0, + "federal_itc_fraction": 0.2 + }, + "SteamTurbine":{ + "min_kw": 100, + "max_kw": 100 + }, + "HighTempThermalStorage": { + "min_kwh": 10, + "max_kwh": 10, + "thermal_decay_rate_fraction": 0.0 + }, + "Wind": { + "min_kw": 100, + "max_kw": 100 + }, + "GHP": { + "require_ghp_purchase": true, + "building_sqft": 50000.0, + "can_serve_dhw": false, + "space_heating_efficiency_thermal_factor": 0.85, + "cooling_efficiency_thermal_factor": 0.6, + "ghpghx_inputs": [{ + "borehole_depth_ft": 400.0, + "simulation_years": 20, + "solver_eft_tolerance_f": 2.0, + "ghx_model": "TESS", + "tess_ghx_minimum_timesteps_per_hour": 1, + "max_sizing_iterations": 10, + "init_sizing_factor_ft_per_peak_ton": 300.0 + }] + } +} \ No newline at end of file diff --git a/reoptjl/test/test_http_endpoints.py b/reoptjl/test/test_http_endpoints.py index 8bad4f1e0..7848e8839 100644 --- a/reoptjl/test/test_http_endpoints.py +++ b/reoptjl/test/test_http_endpoints.py @@ -35,7 +35,6 @@ def test_chp_defaults(self): self.assertEqual(http_response["prime_mover"], "combustion_turbine") self.assertEqual(http_response["size_class"], 2) self.assertGreater(http_response["chp_elec_size_heuristic_kw"], 3500.0) - self.assertEqual(http_response["default_inputs"]["federal_itc_fraction"], 0.0) inputs = { "prime_mover": "micro_turbine", @@ -50,38 +49,7 @@ def test_chp_defaults(self): http_response = response.json() # Check the endpoint logic with the expected selection - self.assertEqual(http_response["default_inputs"]["federal_itc_fraction"], 0.0) - - inputs = { - "prime_mover": "combustion_turbine", - "size_class": 4, - "is_electric_only": "true", - "avg_electric_load_kw": 885.0247784246575, - "max_electric_load_kw": 1427.334 - } - - # Direct call of the http.jl endpoint /chp_defaults - julia_host = os.environ.get('JULIA_HOST', "julia") - response = requests.get("http://" + julia_host + ":8081/chp_defaults/", json=inputs) - http_response = response.json() - - # Check the endpoint logic with the expected selection - self.assertEqual(http_response["default_inputs"]["federal_itc_fraction"], 0.0) - - inputs = { - "prime_mover": "recip_engine", - "size_class": 4, - "is_electric_only": "true", - "avg_electric_load_kw": 885.0247784246575, - "max_electric_load_kw": 1427.334 - } - - # Call to the django view endpoint /chp_defaults which calls the http.jl endpoint - resp = self.api_client.get(f'/v3/chp_defaults', data=inputs) - view_response = json.loads(resp.content) - - # Check the endpoint logic with the expected selection - self.assertEqual(http_response["default_inputs"]["federal_itc_fraction"], 0.0) + self.assertEqual(http_response["size_class"], 3) def test_steamturbine_defaults(self): @@ -230,7 +198,6 @@ def test_avert_emissions_profile_endpoint(self): view_response = json.loads(resp.content) self.assertTrue("error" in view_response) - def test_cambium_profile_endpoint(self): # Call to the django view endpoint v3/cambium_profile which calls the http.jl endpoint #case 1: location in CONUS (Seattle, WA) @@ -295,6 +262,39 @@ def test_easiur_endpoint(self): self.assertHttpBadRequest(resp) view_response = json.loads(resp.content) self.assertTrue("error" in view_response) + + def test_sector_defaults_endpoint(self): + # Call to the django view endpoint dev/sector_defaults which calls the http.jl endpoint + inputs = { + "sector": "federal", + "federal_procurement_type": "fedowned_dirpurch", + "federal_sector_state": "CA" + } + resp = self.api_client.get(f'/v3/sector_defaults', data=inputs) + self.assertHttpOK(resp) + view_response = json.loads(resp.content) + for tech in ["GHP", "Wind", "PV", "CHP"]: + self.assertTrue(view_response.get(tech) is not None) + for key in ["macrs_option_years", "macrs_bonus_fraction", "federal_itc_fraction"]: + self.assertTrue(view_response[tech].get(key) is not None) + self.assertTrue(view_response.get("SteamTurbine") is not None) + for key in ["macrs_option_years", "macrs_bonus_fraction"]: + self.assertTrue(view_response["SteamTurbine"].get(key) is not None) + self.assertTrue(view_response.get("Storage") is not None) + for key in ["macrs_option_years", "macrs_bonus_fraction", "total_itc_fraction"]: + self.assertTrue(view_response["Storage"].get(key) is not None) + self.assertTrue(view_response.get("Financial") is not None) + for key in ["elec_cost_escalation_rate_fraction", "existing_boiler_fuel_cost_escalation_rate_fraction", "boiler_fuel_cost_escalation_rate_fraction", "chp_fuel_cost_escalation_rate_fraction", "generator_fuel_cost_escalation_rate_fraction"]: + self.assertTrue(view_response["Financial"].get(key) is not None) + inputs = { + "sector": "badsector", + "federal_procurement_type": "fedowned_dirpurch", + "federal_sector_state": "CA" + } + resp = self.api_client.get(f'/v3/sector_defaults', data=inputs) + self.assertHttpBadRequest(resp) + view_response = json.loads(resp.content) + self.assertTrue("error" in view_response) def test_ghp_endpoints(self): # Test /ghp_efficiency_thermal_factors diff --git a/reoptjl/test/test_job_endpoint.py b/reoptjl/test/test_job_endpoint.py index ed309b049..29e41cfb5 100644 --- a/reoptjl/test/test_job_endpoint.py +++ b/reoptjl/test/test_job_endpoint.py @@ -113,7 +113,6 @@ def test_process_reopt_error(self): assert(r['messages']['has_stacktrace']==True) assert(resp.status_code==400) - def test_thermal_in_results(self): """ Purpose of this test is to check that the expected thermal loads, techs, and storage are included in the results @@ -131,6 +130,7 @@ def test_thermal_in_results(self): r = json.loads(resp.content) inputs = r["inputs"] results = r["outputs"] + self.assertIn("CoolingLoad", list(inputs.keys())) self.assertIn("CoolingLoad", list(results.keys())) self.assertIn("CHP", list(results.keys())) @@ -150,6 +150,37 @@ def test_thermal_in_results(self): self.assertIn("AbsorptionChiller", list(results.keys())) self.assertIn("GHP", list(results.keys())) + def test_sector_defaults_from_julia(self): + # Test that the inputs_with_defaults_set_in_julia feature worked for sector defaults, consistent with /sector_defaults + post_file = os.path.join('reoptjl', 'test', 'posts', 'sector_defaults_post.json') + post = json.load(open(post_file, 'r')) + resp = self.api_client.post('/v3/job/', format='json', data=post) + self.assertHttpCreated(resp) + r = json.loads(resp.content) + run_uuid = r.get('run_uuid') + + resp = self.api_client.get(f'/v3/job/{run_uuid}/results') + r = json.loads(resp.content) + saved_inputs = r["inputs"] + + # Call to the django view endpoint /sector_defaults which calls the http.jl endpoint + inputs_defaults = post["Site"] + inputs_defaults.pop("latitude") + inputs_defaults.pop("longitude") + resp = self.api_client.get(f'/v3/sector_defaults', data=inputs_defaults) + defaults_view_response = json.loads(resp.content) + + for model_name, saved_model_inputs in saved_inputs.items(): + model_category = "Storage" if "Storage" in model_name else model_name + for input_key, default_input_val in defaults_view_response.get(model_category, {}).items(): + if input_key in post[model_name].keys(): + # Make sure we didn't overwrite user-input + self.assertEqual(saved_model_inputs.get(input_key), post[model_name][input_key]) + else: + # Check that default got assigned consistent with /sector_defaults + if model_name == "SteamTurbine" and input_key == "federal_itc_fraction": + continue #ST doesn't have federal_itc_fraction input + self.assertEqual(saved_model_inputs.get(input_key), default_input_val) def test_chp_defaults_from_julia(self): # Test that the inputs_with_defaults_set_in_julia feature worked for CHP, consistent with /chp_defaults @@ -294,9 +325,20 @@ def test_hybridghp(self): resp = self.api_client.get(f'/v3/job/{run_uuid}/results') r = json.loads(resp.content) - - # calculated_ghx_residual_value 117065.83 - self.assertAlmostEqual(r["outputs"]["GHP"]["ghx_residual_value_present_value"], 117065.83, delta=500) + + # TODO: add number_of_boreholes output to API and calculate ghx_residual_value_present_value here + self.assertAlmostEqual(r["outputs"]["GHP"]["ghpghx_chosen_outputs"]["number_of_boreholes"], 10, delta=.1) + total_ghx_ft = r["outputs"]["GHP"]["ghpghx_chosen_outputs"]["number_of_boreholes"] * r["outputs"]["GHP"]["ghpghx_chosen_outputs"]["length_boreholes_ft"] + ghx_only_capital_cost = total_ghx_ft * r["inputs"]["GHP"]["installed_cost_ghx_per_ft"] + useful_life = post['GHP']['ghx_useful_life_years'] + fin = r["inputs"]["Financial"] + analysis_years = fin["analysis_years"] + discount_rate = (1 - 1* fin["third_party_ownership"])*fin["offtaker_discount_rate_fraction"] + fin["third_party_ownership"]*fin["owner_discount_rate_fraction"] + ghx_residual_value = ghx_only_capital_cost * ( + (useful_life - analysis_years)/useful_life + )/((1 + discount_rate)**analysis_years) + # Note: this value is not independently calculated + self.assertAlmostEqual(r["outputs"]["GHP"]["ghx_residual_value_present_value"], ghx_residual_value, delta=5) def test_centralghp(self): post_file = os.path.join('reoptjl', 'test', 'posts', 'central_plant_ghp.json') diff --git a/reoptjl/urls.py b/reoptjl/urls.py index 73685f76c..eec71fae9 100644 --- a/reoptjl/urls.py +++ b/reoptjl/urls.py @@ -13,6 +13,7 @@ re_path(r'^avert_emissions_profile/?$', views.avert_emissions_profile), re_path(r'^cambium_profile/?$', views.cambium_profile), re_path(r'^easiur_costs/?$', views.easiur_costs), + re_path(r'^sector_defaults/?$', views.sector_defaults), re_path(r'^simulated_load/?$', views.simulated_load), re_path(r'^user/(?P[0-9a-f-]+)/summary/?$', views.summary), re_path(r'^user/(?P[0-9a-f-]+)/summary_by_chunk/(?P[0-9]+)/?$', views.summary_by_chunk), diff --git a/reoptjl/views.py b/reoptjl/views.py index e00a77660..38556999b 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -562,7 +562,6 @@ def pv_cost_defaults(request): return response except ValueError as e: - print(e.args) return JsonResponse({"Error": str(e.args[0])}, status=500) except KeyError as e: @@ -1648,6 +1647,41 @@ def easiur_costs(request): log.error(debug_msg) return JsonResponse({"Error": "Unexpected Error. Please check your input parameters and contact reopt@nrel.gov if problems persist."}, status=500) +def sector_defaults(request): + try: + inputs = { + "sector": request.GET['sector'], + "federal_procurement_type": request.GET['federal_procurement_type'], + "federal_sector_state": request.GET['federal_sector_state'] + } + julia_host = os.environ.get( + 'JULIA_HOST', + "julia" + ) + http_jl_response = requests.get( + "http://" + julia_host + ":8081/sector_defaults/", + json=inputs + ) + response = JsonResponse( + http_jl_response.json(), + status=http_jl_response.status_code + ) + return response + + except KeyError as e: + return JsonResponse({"Error. Missing Parameter": str(e.args[0])}, status=400) + + except ValueError as e: + return JsonResponse({"Error": str(e.args[0])}, status=400) + + except Exception: + + exc_type, exc_value, exc_traceback = sys.exc_info() + debug_msg = "exc_type: {}; exc_value: {}; exc_traceback: {}".format(exc_type, exc_value.args[0], + tb.format_tb(exc_traceback)) + log.error(debug_msg) + return JsonResponse({"Error": "Unexpected Error. Please check your input parameters and contact reopt@nrel.gov if problems persist."}, status=500) + # def fuel_emissions_rates(request): # try: