diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e33c1d1..95a227b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,14 @@ Classify the change according to the following categories: ##### Removed ### Patches +## v3.11.0 +### Minor Updates +##### Changed +- Require `year` for all custom 8760/35040 load profile inputs +- Truncate the last day of the year instead of the leap day for leap years +##### Added +- Option for ASHP to `force_dispatch` (default = true) which maximizes ASHP thermal output + ## v3.10.2 ### Minor Updates ##### Changed diff --git a/ghpghx/migrations/0019_alter_ghpghxinputs_borehole_depth_ft_and_more.py b/ghpghx/migrations/0019_alter_ghpghxinputs_borehole_depth_ft_and_more.py new file mode 100644 index 000000000..6740a733f --- /dev/null +++ b/ghpghx/migrations/0019_alter_ghpghxinputs_borehole_depth_ft_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 4.0.7 on 2025-01-14 21:50 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ghpghx', '0018_ghpghxinputs_wwhp_cooling_pump_fluid_flow_rate_gpm_per_ton_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='ghpghxinputs', + name='borehole_depth_ft', + field=models.FloatField(blank=True, default=443.0, help_text='Vertical depth of each borehole [ft]', validators=[django.core.validators.MinValueValidator(10.0), django.core.validators.MaxValueValidator(600.0)]), + ), + migrations.AlterField( + model_name='ghpghxinputs', + name='borehole_diameter_inch', + field=models.FloatField(blank=True, default=6.0, help_text='Diameter of the borehole/well drilled in the ground [in]', validators=[django.core.validators.MinValueValidator(0.25), django.core.validators.MaxValueValidator(24.0)]), + ), + migrations.AlterField( + model_name='ghpghxinputs', + name='ghx_header_depth_ft', + field=models.FloatField(blank=True, default=6.6, help_text='Depth under the ground of the GHX header pipe [ft]', validators=[django.core.validators.MinValueValidator(0.1), django.core.validators.MaxValueValidator(50.0)]), + ), + migrations.AlterField( + model_name='ghpghxinputs', + name='ghx_pipe_thermal_conductivity_btu_per_hr_ft_f', + field=models.FloatField(blank=True, default=0.23, help_text='Thermal conductivity of the GHX pipe [Btu/(hr-ft-degF)]', validators=[django.core.validators.MinValueValidator(0.01), django.core.validators.MaxValueValidator(10.0)]), + ), + migrations.AlterField( + model_name='ghpghxinputs', + name='ghx_shank_space_inch', + field=models.FloatField(blank=True, default=1.27, help_text='Distance between the centerline of the upwards and downwards u-tube legs [in]', validators=[django.core.validators.MinValueValidator(0.5), django.core.validators.MaxValueValidator(100.0)]), + ), + migrations.AlterField( + model_name='ghpghxinputs', + name='grout_thermal_conductivity_btu_per_hr_ft_f', + field=models.FloatField(blank=True, default=0.75, help_text='Thermal conductivity of the grout material in a borehole [Btu/(hr-ft-degF)]', validators=[django.core.validators.MinValueValidator(0.01), django.core.validators.MaxValueValidator(10.0)]), + ), + ] diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 6e28604ac..1803c1732 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -929,9 +929,9 @@ uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "67eade881254923d75cf80aac40ca8da2f17351e" +git-tree-sha1 = "324394f21cb7e2db3d9e7ebde19c4e83c5a64e0f" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.49.1" +version = "0.50.0" [[deps.Random]] deps = ["SHA"] diff --git a/reoptjl/migrations/0075_coolingloadinputs_year_and_more.py b/reoptjl/migrations/0075_coolingloadinputs_year_and_more.py new file mode 100644 index 000000000..9576cbe59 --- /dev/null +++ b/reoptjl/migrations/0075_coolingloadinputs_year_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.0.7 on 2025-01-14 19:46 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0074_alter_domestichotwaterloadinputs_normalize_and_scale_load_profile_input_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='coolingloadinputs', + name='year', + field=models.IntegerField(blank=True, help_text="Year of Custom Load Profile. If a custom load profile is uploaded via the thermal_loads_ton parameter, it is important that this year correlates with the electric load profile so that weekdays/weekends are determined correctly for the utility rate tariff. If a DOE Reference Building profile (aka 'simulated' profile) is used, the year is set to 2017 since the DOE profiles start on a Sunday.", null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(9999)]), + ), + migrations.AlterField( + model_name='domestichotwaterloadinputs', + name='year', + field=models.IntegerField(blank=True, help_text="Year of Custom Load Profile. If a custom load profile is uploaded via the fuel_loads_mmbtu_per_hour parameter, it is important that this year correlates with the electric load profile so that weekdays/weekends are determined correctly for the utility rate tariff. If a DOE Reference Building profile (aka 'simulated' profile) is used, the year is set to 2017 since the DOE profiles start on a Sunday.", null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(9999)]), + ), + migrations.AlterField( + model_name='electricloadinputs', + name='year', + field=models.IntegerField(blank=True, help_text="Year of Custom Load Profile. If a custom load profile is uploaded via the loads_kw parameter, it is important that this year correlates with the load profile so that weekdays/weekends are determined correctly for the utility rate tariff. If a DOE Reference Building profile (aka 'simulated' profile) is used, the year is set to 2017 since the DOE profiles start on a Sunday.", null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(9999)]), + ), + migrations.AlterField( + model_name='processheatloadinputs', + name='year', + field=models.IntegerField(blank=True, help_text="Year of Custom Load Profile. If a custom load profile is uploaded via the fuel_loads_mmbtu_per_hour parameter, it is important that this year correlates with the electric load profile so that weekdays/weekends are determined correctly for the utility rate tariff. If a Industrial Reference Building profile (aka 'simulated' profile) is used, the year is set to 2017 to be consistent with the DOE reference building year which starts on a Sunday.", null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(9999)]), + ), + migrations.AlterField( + model_name='spaceheatingloadinputs', + name='year', + field=models.IntegerField(blank=True, help_text="Year of Custom Load Profile. If a custom load profile is uploaded via the fuel_loads_mmbtu_per_hour parameter, it is important that this year correlates with the electric load profile so that weekdays/weekends are determined correctly for the utility rate tariff. If a DOE Reference Building profile (aka 'simulated' profile) is used, the year is set to 2017 since the DOE profiles start on a Sunday.", null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(9999)]), + ), + ] diff --git a/reoptjl/migrations/0076_ashpspaceheaterinputs_force_dispatch_and_more.py b/reoptjl/migrations/0076_ashpspaceheaterinputs_force_dispatch_and_more.py new file mode 100644 index 000000000..ba9831f00 --- /dev/null +++ b/reoptjl/migrations/0076_ashpspaceheaterinputs_force_dispatch_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 4.0.7 on 2025-01-18 04:04 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0075_coolingloadinputs_year_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='ashpspaceheaterinputs', + name='force_dispatch', + field=models.BooleanField(default=True, help_text='Boolean indicator that ASHP space heater outputs either maximum capacity or site load if true', null=True), + ), + migrations.AddField( + model_name='ashpwaterheaterinputs', + name='force_dispatch', + field=models.BooleanField(default=True, help_text='Boolean indicator that ASHP water heater outputs either maximum capacity or site load if true', null=True), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='force_into_system', + field=models.BooleanField(blank=True, help_text='Boolean indicator if ASHP water heater serves compatible thermal loads exclusively in optimized scenario', null=True), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='heating_cf_reference', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20.0)]), blank=True, default=list, help_text='Reference points for ASHP water heating system heating capacity factor(ratio of heating thermal power to rated capacity)', size=None), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='heating_cop_reference', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20.0)]), blank=True, default=list, help_text='Reference points for ASHP water heating system heating coefficient of performance (COP) (ratio of usable heating thermal energy produced per unit electric energy consumed)', size=None), + ), + migrations.AlterField( + model_name='ashpwaterheaterinputs', + name='heating_reference_temps_degF', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(-275.0), django.core.validators.MaxValueValidator(200.0)]), blank=True, default=list, help_text="Reference temperatures for ASHP water heating system's heating COP and CF [Fahrenheit]", size=None), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index f29d43060..97dbfd835 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -1213,7 +1213,6 @@ class ElectricLoadInputs(BaseModel, models.Model): "https://energy.gov/eere/buildings/commercial-reference-buildings") ) year = models.IntegerField( - default=2022, validators=[ MinValueValidator(1), MaxValueValidator(9999) @@ -4664,6 +4663,18 @@ class CoolingLoadInputs(BaseModel, models.Model): ) ) + year = models.IntegerField( + validators=[ + MinValueValidator(1), + MaxValueValidator(9999) + ], + null=True, blank=True, + help_text=("Year of Custom Load Profile. If a custom load profile is uploaded via the thermal_loads_ton parameter, it " + "is important that this year correlates with the electric load profile so that weekdays/weekends are " + "determined correctly for the utility rate tariff. If a DOE Reference Building profile (aka " + "'simulated' profile) is used, the year is set to 2017 since the DOE profiles start on a Sunday.") + ) + annual_fraction_of_electric_load = models.FloatField( validators=[ MinValueValidator(0), @@ -4714,10 +4725,6 @@ def clean(self): "The number of blended_doe_reference_names must equal the number of blended_doe_reference_percents." if not math.isclose(sum(self.blended_doe_reference_percents), 1.0): error_messages["blended_doe_reference_percents"] = "Sum must = 1.0." - - if self.doe_reference_name != "" or \ - len(self.blended_doe_reference_names) > 0: - self.year = 2017 # the validator provides an "info" message regarding this) if len(self.monthly_fractions_of_electric_load) > 0: if len(self.monthly_fractions_of_electric_load) != 12: @@ -5470,6 +5477,12 @@ class ASHPSpaceHeaterInputs(BaseModel, models.Model): help_text="Boolean indicator if ASHP space heater serves compatible thermal loads exclusively in optimized scenario" ) + force_dispatch = models.BooleanField( + null=True, + default=True, + help_text="Boolean indicator that ASHP space heater outputs either maximum capacity or site load if true" + ) + avoided_capex_by_ashp_present_value = models.FloatField( validators=[ MinValueValidator(0), @@ -5701,7 +5714,7 @@ class ASHPWaterHeaterInputs(BaseModel, models.Model): ), default=list, blank=True, - help_text=(("Reference points for ASHP space heating system heating coefficient of performance (COP) " + help_text=(("Reference points for ASHP water heating system heating coefficient of performance (COP) " "(ratio of usable heating thermal energy produced per unit electric energy consumed)")) ) @@ -5715,7 +5728,7 @@ class ASHPWaterHeaterInputs(BaseModel, models.Model): ), default=list, blank=True, - help_text=(("Reference points for ASHP space heating system heating capac)ity factor" + help_text=(("Reference points for ASHP water heating system heating capacity factor" "(ratio of heating thermal power to rated capacity)")) ) @@ -5729,7 +5742,7 @@ class ASHPWaterHeaterInputs(BaseModel, models.Model): ), default=list, blank=True, - help_text=(("Reference temperatures for ASHP space heating system's heating COP and CF [Fahrenheit]")) + help_text=(("Reference temperatures for ASHP water heating system's heating COP and CF [Fahrenheit]")) ) avoided_capex_by_ashp_present_value = models.FloatField( @@ -5756,13 +5769,19 @@ class ASHPWaterHeaterInputs(BaseModel, models.Model): force_into_system = models.BooleanField( null=True, blank=True, - help_text="Boolean indicator if ASHP space heater serves compatible thermal loads exclusively in optimized scenario" + help_text="Boolean indicator if ASHP water heater serves compatible thermal loads exclusively in optimized scenario" + ) + + force_dispatch = models.BooleanField( + null=True, + default=True, + help_text="Boolean indicator that ASHP water heater outputs either maximum capacity or site load if true" ) def clean(self): error_messages = {} if self.dict.get("min_allowable_ton") in [None, "", []] and self.dict.get("min_allowable_peak_capacity_fraction") in [None, "", []]: - self.min_allowable_peak_capacity_fraction = 0.5 + self.min_allowable_peak_capacity_fraction = 0.25 if self.dict.get("min_allowable_ton") not in [None, "", []] and self.dict.get("min_allowable_peak_capacity_fraction") not in [None, "", []]: error_messages["bad inputs"] = "At most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input to model {}".format(self.key) @@ -6908,7 +6927,6 @@ class SpaceHeatingLoadInputs(BaseModel, models.Model): ) year = models.IntegerField( - default=2022, validators=[ MinValueValidator(1), MaxValueValidator(9999) @@ -6979,10 +6997,6 @@ def clean(self): "The number of blended_doe_reference_names must equal the number of blended_doe_reference_percents." if not math.isclose(sum(self.blended_doe_reference_percents), 1.0): error_messages["blended_doe_reference_percents"] = "Sum must = 1.0." - - if self.doe_reference_name != "" or \ - len(self.blended_doe_reference_names) > 0: - self.year = 2017 # the validator provides an "info" message regarding this) if self.addressable_load_fraction == None: self.addressable_load_fraction = list([1.0]) # should not convert to timeseries, in case it is to be used with monthly_mmbtu or annual_mmbtu @@ -7088,7 +7102,6 @@ class DomesticHotWaterLoadInputs(BaseModel, models.Model): ) year = models.IntegerField( - default=2022, validators=[ MinValueValidator(1), MaxValueValidator(9999) @@ -7159,10 +7172,6 @@ def clean(self): "The number of blended_doe_reference_names must equal the number of blended_doe_reference_percents." if not math.isclose(sum(self.blended_doe_reference_percents), 1.0): error_messages["blended_doe_reference_percents"] = "Sum must = 1.0." - - if self.doe_reference_name != "" or \ - len(self.blended_doe_reference_names) > 0: - self.year = 2017 # the validator provides an "info" message regarding this) if self.addressable_load_fraction == None: self.addressable_load_fraction = list([1.0]) # should not convert to timeseries, in case it is to be used with monthly_mmbtu or annual_mmbtu @@ -7242,7 +7251,6 @@ class ProcessHeatLoadInputs(BaseModel, models.Model): ) year = models.IntegerField( - default=2022, validators=[ MinValueValidator(1), MaxValueValidator(9999) @@ -7250,28 +7258,15 @@ class ProcessHeatLoadInputs(BaseModel, models.Model): null=True, blank=True, help_text=("Year of Custom Load Profile. If a custom load profile is uploaded via the fuel_loads_mmbtu_per_hour parameter, it " "is important that this year correlates with the electric load profile so that weekdays/weekends are " - "determined correctly for the utility rate tariff. If a DOE Reference Building profile (aka " - "'simulated' profile) is used, the year is set to 2017 since the DOE profiles start on a Sunday.") + "determined correctly for the utility rate tariff. If a Industrial Reference Building profile (aka " + "'simulated' profile) is used, the year is set to 2017 to be consistent with the DOE reference building year which starts on a Sunday.") ) normalize_and_scale_load_profile_input = models.BooleanField( blank=True, default=False, help_text=("Takes the input fuel_loads_mmbtu_per_hour and normalizes and scales it to annual or monthly energy inputs.") - ) - - year = models.IntegerField( - default=2022, - validators=[ - MinValueValidator(1), - MaxValueValidator(9999) - ], - null=True, blank=True, - help_text=("Year of Custom Load Profile. If a custom load profile is uploaded via the fuel_loads_mmbtu_per_hour parameter, it " - "is important that this year correlates with the electric load profile so that weekdays/weekends are " - "determined correctly for the utility rate tariff. If a DOE Reference Building profile (aka " - "'simulated' profile) is used, the year is set to 2017 since the DOE profiles start on a Sunday.") - ) + ) blended_industrial_reference_names = ArrayField( models.TextField( @@ -7327,10 +7322,6 @@ def clean(self): "The number of blended_industrial_reference_names must equal the number of blended_industrial_reference_percents." if not math.isclose(sum(self.blended_industrial_reference_percents), 1.0): error_messages["blended_industrial_reference_percents"] = "Sum must = 1.0." - - if self.industrial_reference_name != "" or \ - len(self.blended_industrial_reference_names) > 0: - self.year = 2017 # the validator provides an "info" message regarding this) if self.addressable_load_fraction == None: self.addressable_load_fraction = list([1.0]) # should not convert to timeseries, in case it is to be used with monthly_mmbtu or annual_mmbtu diff --git a/reoptjl/test/posts/all_inputs_test.json b/reoptjl/test/posts/all_inputs_test.json index 4da22063c..fbc26998a 100644 --- a/reoptjl/test/posts/all_inputs_test.json +++ b/reoptjl/test/posts/all_inputs_test.json @@ -368,7 +368,8 @@ "can_serve_cooling": true, "force_into_system": false, "avoided_capex_by_ashp_present_value": 0, - "back_up_temp_threshold_degF": -10.0 + "back_up_temp_threshold_degF": -10.0, + "force_dispatch": false }, "ASHPWaterHeater": { "min_ton": 10.0, @@ -385,6 +386,7 @@ "heating_reference_temps_degF": [-5, 17, 47], "force_into_system": false, "avoided_capex_by_ashp_present_value": 0, - "back_up_temp_threshold_degF": -10.0 + "back_up_temp_threshold_degF": -10.0, + "force_dispatch": false } } \ No newline at end of file diff --git a/reoptjl/views.py b/reoptjl/views.py index 534e0445f..3dc915bc1 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -538,6 +538,9 @@ def simulated_load(request): inputs["longitude"] = float(request.GET['longitude']) # Optional load_type - will default to "electric" inputs["load_type"] = request.GET.get('load_type') + # Optional year parameter to shift the CRB profile from 2017 (also 2023) to the input year + if 'year' in request.GET: + inputs["year"] = int(request.GET['year']) if inputs["load_type"] == 'process_heat': expected_reference_name = 'industrial_reference_name'