diff --git a/CHANGELOG.md b/CHANGELOG.md index 9581b3d90..66fead55c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,12 @@ Classify the change according to the following categories: ##### Removed ### Patches +## peak-scaling +### Minor Updates +##### Added +- **ElectricLoad** input **monthly_peaks_kw**. Can be used to scale loads_kw or doe_reference loads to monthly peaks while maintaining monthly energy. + + ## v3.16.2 ### Patches - Added `CST` and `HighTempThermalStorage` to all/superset inputs test. diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 196aae6bd..6f16d2aa0 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -948,9 +948,11 @@ 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 = "103761fa0f7447377726347af656cde6ab1160cc" +git-tree-sha1 = "016a24bc463c0b3dd261877efc2d7850de70fcc9" +repo-rev = "peak-scaling" +repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.55.1" +version = "0.55.0" [[deps.Random]] deps = ["SHA"] diff --git a/julia_src/http.jl b/julia_src/http.jl index 1c9e36d99..85818357d 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -544,18 +544,18 @@ function simulated_load(req::HTTP.Request) end # Convert vectors which come in as Vector{Any} to Vector{Float} (within Vector{<:Real}) - vector_types = ["percent_share", "cooling_pct_share", "monthly_totals_kwh", "monthly_mmbtu", + vector_types = ["percent_share", "cooling_pct_share", "monthly_totals_kwh", "monthly_peaks_kw", "monthly_mmbtu", "monthly_tonhour", "monthly_fraction", "addressable_load_fraction", "load_profile"] for key in vector_types if key in keys(d) && typeof(d[key]) <: Vector{} - d[key] = convert(Vector{Real}, d[key]) + d[key] = convert(Vector{Float64}, d[key]) elseif key in keys(d) && key == "addressable_load_fraction" # Scalar version of input, convert Any to Real - d[key] = convert(Real, d[key]) + d[key] = convert(Float64, d[key]) end end - @info "Getting CRB Loads..." + @info "Getting Loads..." data = Dict() error_response = Dict() try diff --git a/reoptjl/migrations/0110_electricloadinputs_monthly_peaks_kw_and_more.py b/reoptjl/migrations/0110_electricloadinputs_monthly_peaks_kw_and_more.py new file mode 100644 index 000000000..3ad7ed1b0 --- /dev/null +++ b/reoptjl/migrations/0110_electricloadinputs_monthly_peaks_kw_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.24 on 2025-10-23 21:21 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0109_remove_ghpoutputs_iterations_auto_guess_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='electricloadinputs', + name='monthly_peaks_kw', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), blank=True, default=list, help_text="Monthly peak power consumption (an array 12 entries long), in kW, used to scale either loads_kw series (with normalize_and_scale_load_profile_input) or the simulated default building load profile for the site's climate zone.Monthly energy is maintained while scaling to the monthly peaks.", size=None), + ), + migrations.AlterField( + model_name='electricloadinputs', + name='monthly_totals_kwh', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), blank=True, default=list, help_text="Monthly site energy consumption (an array 12 entries long), in kWh, used to scale either loads_kw series (with normalize_and_scale_load_profile_input) or the simulated default building load profile for the site's climate zone", size=None), + ), + migrations.AlterField( + model_name='electricloadinputs', + name='normalize_and_scale_load_profile_input', + field=models.BooleanField(blank=True, default=False, help_text='Takes the input loads_kw and normalizes and scales it to the inputs annual_kwh, monthly_totals_kwh, and/or monthly_peaks_kw.'), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index d535fe252..822df77b3 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -1387,8 +1387,22 @@ class ElectricLoadInputs(BaseModel, models.Model): blank=True ), default=list, blank=True, - help_text=("Monthly site energy consumption from electricity series (an array 12 entries long), in kWh, used " - "to scale simulated default building load profile for the site's climate zone") + help_text=("Monthly site energy consumption (an array 12 entries long), in kWh, used to scale either loads_kw series " + "(with normalize_and_scale_load_profile_input) or the simulated default building load profile for the site's climate zone") + ) + monthly_peaks_kw = ArrayField( + models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(1.0e8) + ], + blank=True + ), + default=list, blank=True, + help_text=("Monthly peak power consumption (an array 12 entries long), in kW, used to scale either loads_kw series " + "(with normalize_and_scale_load_profile_input) or the simulated default building load profile for the site's climate zone." + "Monthly energy is maintained while scaling to the monthly peaks." + ) ) loads_kw = ArrayField( models.FloatField(blank=True), @@ -1401,7 +1415,7 @@ class ElectricLoadInputs(BaseModel, models.Model): normalize_and_scale_load_profile_input = models.BooleanField( blank=True, default=False, - help_text=("Takes the input loads_kw and normalizes and scales it to annual or monthly energy inputs.") + help_text=("Takes the input loads_kw and normalizes and scales it to the inputs annual_kwh, monthly_totals_kwh, and/or monthly_peaks_kw.") ) critical_loads_kw = ArrayField( models.FloatField(blank=True), diff --git a/reoptjl/test/posts/all_inputs_test.json b/reoptjl/test/posts/all_inputs_test.json index 8381b01b5..a2355ef95 100644 --- a/reoptjl/test/posts/all_inputs_test.json +++ b/reoptjl/test/posts/all_inputs_test.json @@ -35,6 +35,7 @@ "doe_reference_name": "MidriseApartment", "year": 2017, "monthly_totals_kwh": [], + "monthly_peaks_kw": [], "loads_kw": [], "critical_loads_kw": [], "loads_kw_is_net": true, diff --git a/reoptjl/test/posts/validator_post.json b/reoptjl/test/posts/validator_post.json index d4c8ce524..05f4e78ab 100644 --- a/reoptjl/test/posts/validator_post.json +++ b/reoptjl/test/posts/validator_post.json @@ -44,6 +44,20 @@ 1200, 1200 ], + "monthly_peaks_kw": [ + 1.5, + 1.5, + 1.5, + 1.5, + 1.5, + 1.5, + 1.5, + 1.5, + 1.5, + 1.5, + 1.5, + 1.5 + ], "outage_start_time_step": 11, "outage_end_time_step": 501, "critical_load_fraction": 0.5, diff --git a/reoptjl/views.py b/reoptjl/views.py index 38556999b..3832e36c0 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -582,10 +582,10 @@ def simulated_load(request): # Required for GET - will throw a Missing Error if not included if request.method == "GET": valid_keys = ["doe_reference_name","industrial_reference_name","latitude","longitude","load_type","percent_share","annual_kwh", - "monthly_totals_kwh","annual_mmbtu","annual_fraction","annual_tonhour","monthly_tonhour", + "monthly_totals_kwh","monthly_peaks_kw","annual_mmbtu","annual_fraction","annual_tonhour","monthly_tonhour", "monthly_mmbtu","monthly_fraction","max_thermal_factor_on_peak_load","chiller_cop", "addressable_load_fraction", "cooling_doe_ref_name", "cooling_pct_share", "boiler_efficiency", - "normalize_and_scale_load_profile_input", "year"] + "normalize_and_scale_load_profile_input", "year", "time_steps_per_hour"] for key in request.GET.keys(): k = key if "[" in key: @@ -658,7 +658,7 @@ def simulated_load(request): # TODO make year optional? inputs[field] = data[field] if inputs["load_type"] == "electric": - for energy in ["annual_kwh", "monthly_totals_kwh"]: + for energy in ["annual_kwh", "monthly_totals_kwh", "monthly_peaks_kw"]: if data.get(energy) is not None: inputs[energy] = data.get(energy) elif inputs["load_type"] in ["space_heating", "domestic_hot_water", "process_heat"]: @@ -669,7 +669,8 @@ def simulated_load(request): for energy in ["annual_tonhour", "monthly_tonhour"]: if data.get(energy) is not None: inputs[energy] = data.get(energy) - # TODO cooling, not in REopt.jl yet + if len(inputs["load_profile"]) != 8760: + inputs["time_steps_per_hour"] = data["time_steps_per_hour"] # TODO consider changing all requests to POST so that we don't have to do the weird array processing like percent_share[0], [1], etc? # json.dump(inputs, open("sim_load_post.json", "w"))