diff --git a/CHANGELOG.md b/CHANGELOG.md index 37d4a6034..d582c6dda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,24 @@ Classify the change according to the following categories: ##### Removed ### Patches +## v3.9.0 +### Minor Updates +#### Added +- In `reoptjl/models.py`, added the following fields: + - booleans **can_serve_dhw**, **can_serve_space_heating**, and **can_serve_process_heat** to models **CHPInputs**, **ExistingBoilerInputs**, **BoilerInputs**, **SteamTurbineInputs**, and **HotThermalStorageInputs** + - booleans **can_serve_space_heating** and **can_serve_process_heat** to model **GHPInputs** + - arrays **storage_to_dhw_load_series_mmbtu_per_hour**, **storage_to_space_heating_load_series_mmbtu_per_hour** and **storage_to_process_heat_load_series_mmbtu_per_hour** to model **HotThermalStorageOutputs** + - **heating_load_input** to model **AbsorptionChillerInputs** + - arrays **thermal_to_dhw_load_series_mmbtu_per_hour**, **thermal_to_space_heating_load_series_mmbtu_per_hour**, and **thermal_to_process_heat_load_series_mmbtu_per_hour** to models **CHPOutputs**, **ExistingBoilerOutputs**, **BoilerOutputs**, **SteamTurbineOutputs**, and **HotThermalStorageOutputs** + - boolean **retire_in_optimal** to **ExistingBoilerInputs** +- In `reopt.jl/models.py`, added new model **ProcessHeatLoadInputs** with references in `reoptjl/validators.py` and `reoptjl/views.py` +- Added process heat load to test scenario `reoptjl/test/posts/test_thermal_in_results.json` +- Added tests for the presence of process heat load and heat-load-specfic outputs to `test_thermal_in_results` within `reoptjl/test/test_job_endpoint.py` +#### Changed +- Point to REopt.jl v0.46.1 which includes bug fixes in net metering and updated PV resource data calls +#### Fixed +- Fix bug in setting default ElectricUtility.emissions_factor_CO2_decrease_fraction. Previously, user-input values were getting overwritten. + ## v3.8.0 ### Minor Updates #### Changed diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 204307475..ef21d5b2a 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -917,9 +917,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 = "d2bbcbb8344f19ed87ee8cb6a196d4ed8415255b" +git-tree-sha1 = "3c40f3939f79c3f66df69e9acc503fef614cdd63" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.45.0" +version = "0.46.1" [[deps.Random]] deps = ["SHA"] diff --git a/reoptjl/migrations/0059_processheatloadinputs_and_more.py b/reoptjl/migrations/0059_processheatloadinputs_and_more.py new file mode 100644 index 000000000..7c0bed303 --- /dev/null +++ b/reoptjl/migrations/0059_processheatloadinputs_and_more.py @@ -0,0 +1,241 @@ +# Generated by Django 4.0.7 on 2024-05-09 20:02 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import reoptjl.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0058_merge_20240425_1527'), + ] + + operations = [ + migrations.CreateModel( + name='ProcessHeatLoadInputs', + fields=[ + ('meta', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='ProcessHeatLoadInputs', serialize=False, to='reoptjl.apimeta')), + ('annual_mmbtu', models.FloatField(blank=True, help_text='Annual site process heat consumption, used to scale simulated load profile [MMBtu]', null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100000000.0)])), + ('fuel_loads_mmbtu_per_hour', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True), blank=True, default=list, help_text='Typical load over all hours in one year. Must be hourly (8,760 samples), 30 minute (17,520 samples), or 15 minute (35,040 samples). All non-net load values must be greater than or equal to zero. ', size=None)), + ], + bases=(reoptjl.models.BaseModel, models.Model), + ), + migrations.AddField( + model_name='absorptionchillerinputs', + name='heating_load_input', + field=models.TextField(blank=True, choices=[('DomesitHotWater', 'Domesithotwater'), ('SpaceHeating', 'Spaceheating'), ('ProcessHeat', 'Processheat')], help_text='Absorption chiller heat input - determines what heating load is added to by absorption chiller use', null=True), + ), + migrations.AddField( + model_name='boilerinputs', + name='can_serve_dhw', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if boiler can serve domestic hot water load', null=True), + ), + migrations.AddField( + model_name='boilerinputs', + name='can_serve_process_heat', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if boiler can serve process heat load', null=True), + ), + migrations.AddField( + model_name='boilerinputs', + name='can_serve_space_heating', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if boiler can serve space heating load', null=True), + ), + migrations.AddField( + model_name='boileroutputs', + name='thermal_to_dhw_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='boileroutputs', + name='thermal_to_process_heat_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='boileroutputs', + name='thermal_to_space_heating_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='chpinputs', + name='can_serve_dhw', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if CHP can serve hot water load', null=True), + ), + migrations.AddField( + model_name='chpinputs', + name='can_serve_process_heat', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if CHP can serve process heat load', null=True), + ), + migrations.AddField( + model_name='chpinputs', + name='can_serve_space_heating', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if CHP can serve space heating load', null=True), + ), + migrations.AddField( + model_name='chpoutputs', + name='thermal_to_dhw_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='chpoutputs', + name='thermal_to_process_heat_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='chpoutputs', + name='thermal_to_space_heating_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='existingboilerinputs', + name='can_serve_dhw', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if the existing boiler can serve domestic hot water load', null=True), + ), + migrations.AddField( + model_name='existingboilerinputs', + name='can_serve_process_heat', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if the existing boiler can serve process heat load', null=True), + ), + migrations.AddField( + model_name='existingboilerinputs', + name='can_serve_space_heating', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if the existing boiler can serve space heating load', null=True), + ), + migrations.AddField( + model_name='existingboilerinputs', + name='retire_in_optimal', + field=models.BooleanField(blank=True, default=False, help_text='Boolean indicator if the existing boiler is unavailable in the optimal case (still used in BAU)', null=True), + ), + migrations.AddField( + model_name='existingboileroutputs', + name='thermal_to_dhw_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='existingboileroutputs', + name='thermal_to_process_heat_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='existingboileroutputs', + name='thermal_to_space_heating_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='ghpinputs', + name='can_serve_process_heat', + field=models.BooleanField(blank=True, default=False, help_text='Boolean indicator if GHP can serve process heat load', null=True), + ), + migrations.AddField( + model_name='ghpinputs', + name='can_serve_space_heating', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if GHP can serve space heating load', null=True), + ), + migrations.AddField( + model_name='heatingloadoutputs', + name='annual_calculated_process_heat_boiler_fuel_load_mmbtu', + field=models.FloatField(blank=True, default=0, help_text='Annual site process heat boiler fuel load [MMBTU]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AddField( + model_name='heatingloadoutputs', + name='annual_calculated_process_heat_thermal_load_mmbtu', + field=models.FloatField(blank=True, default=0, help_text='Annual site process heat load [MMBTU]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AddField( + model_name='heatingloadoutputs', + name='process_heat_boiler_fuel_load_series_mmbtu_per_hour', + 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='Hourly process heat boiler fuel load [MMBTU/hr]', size=None), + ), + migrations.AddField( + model_name='heatingloadoutputs', + name='process_heat_thermal_load_series_mmbtu_per_hour', + 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='Hourly process heat load [MMBTU/hr]', size=None), + ), + migrations.AddField( + model_name='hotthermalstorageinputs', + name='can_serve_dhw', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if hot thermal storage can serve space heating load', null=True), + ), + migrations.AddField( + model_name='hotthermalstorageinputs', + name='can_serve_process_heat', + field=models.BooleanField(blank=True, default=False, help_text='Boolean indicator if hot thermal storage can serve process heat load', null=True), + ), + migrations.AddField( + model_name='hotthermalstorageinputs', + name='can_serve_space_heating', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if hot thermal storage can serve space heating load', null=True), + ), + migrations.AddField( + model_name='hotthermalstorageoutputs', + name='storage_to_dhw_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='hotthermalstorageoutputs', + name='storage_to_process_heat_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='hotthermalstorageoutputs', + name='storage_to_space_heating_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='steamturbineinputs', + name='can_serve_dhw', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if steam turbine can serve space heating load', null=True), + ), + migrations.AddField( + model_name='steamturbineinputs', + name='can_serve_process_heat', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if steam turbine can serve process heat load', null=True), + ), + migrations.AddField( + model_name='steamturbineinputs', + name='can_serve_space_heating', + field=models.BooleanField(blank=True, default=True, help_text='Boolean indicator if steam turbine can serve space heating load', null=True), + ), + migrations.AddField( + model_name='steamturbineoutputs', + name='thermal_to_dhw_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='steamturbineoutputs', + name='thermal_to_process_heat_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AddField( + model_name='steamturbineoutputs', + name='thermal_to_space_heating_load_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), default=list, size=None), + ), + migrations.AlterField( + model_name='heatingloadoutputs', + name='dhw_boiler_fuel_load_series_mmbtu_per_hour', + 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='Hourly domestic hot water boiler fuel load [MMBTU/hr]', size=None), + ), + migrations.AlterField( + model_name='heatingloadoutputs', + name='space_heating_boiler_fuel_load_series_mmbtu_per_hour', + 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='Hourly space heating boiler fuel load [MMBTU/hr]', size=None), + ), + migrations.AlterField( + model_name='heatingloadoutputs', + name='space_heating_thermal_load_series_mmbtu_per_hour', + 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='Hourly space heating load [MMBTU/hr]', size=None), + ), + migrations.AlterField( + model_name='heatingloadoutputs', + name='total_heating_boiler_fuel_load_series_mmbtu_per_hour', + 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='Hourly total boiler fuel load [MMBTU/hr]', size=None), + ), + migrations.AlterField( + model_name='pvinputs', + name='tilt', + field=models.FloatField(blank=True, help_text='PV system tilt angle. Default tilt is 20 degrees for fixed arrays (rooftop or ground-mounted) and 0 degrees for axis-tracking systems.', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(90)]), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index bb744b1b4..8d4e1ae11 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -1842,8 +1842,9 @@ def clean(self): elif self.outage_durations not in [None,[]]: self.outage_probabilities = [1/len(self.outage_durations)] * len(self.outage_durations) - if self.co2_from_avert or len(self.emissions_factor_series_lb_CO2_per_kwh) > 0: - self.emissions_factor_CO2_decrease_fraction = EMISSIONS_DECREASE_DEFAULTS.get("CO2e", None) # leave blank otherwise; the Julia Pkg will set to 0 unless site is in AK or HI + if (self.co2_from_avert or len(self.emissions_factor_series_lb_CO2_per_kwh) > 0) and self.emissions_factor_CO2_decrease_fraction == None: + # use default if not provided and using AVERT or custom EFs. Leave blank otherwise and the Julia Pkg will set to 0 unless site is in AK or HI. + self.emissions_factor_CO2_decrease_fraction = EMISSIONS_DECREASE_DEFAULTS.get("CO2e", None) if self.emissions_factor_NOx_decrease_fraction == None: self.emissions_factor_NOx_decrease_fraction = EMISSIONS_DECREASE_DEFAULTS.get("NOx", 0.0) @@ -4104,6 +4105,25 @@ class CHPInputs(BaseModel, models.Model): blank=True, help_text="Boolean indicator if CHP can supply steam to the steam turbine for electric production" ) + can_serve_dhw = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if CHP can serve hot water load" + ) + can_serve_space_heating = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if CHP can serve space heating load" + ) + can_serve_process_heat = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if CHP can serve process heat load" + ) + #Financial and emissions macrs_option_years = models.IntegerField( @@ -4456,6 +4476,18 @@ class CHPOutputs(BaseModel, models.Model): models.FloatField(null=True, blank=True), default = list, ) + thermal_to_dhw_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list, + ) + thermal_to_space_heating_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list, + ) + thermal_to_process_heat_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list, + ) def clean(): pass @@ -4836,6 +4868,13 @@ class ExistingBoilerInputs(BaseModel, models.Model): help_text="Existing boiler system efficiency - conversion of fuel to usable heating thermal energy." ) + retire_in_optimal = models.BooleanField( + default=False, + null=True, + blank=True, + help_text="Boolean indicator if the existing boiler is unavailable in the optimal case (still used in BAU)" + ) + fuel_renewable_energy_fraction = models.FloatField( validators=[ MinValueValidator(0), @@ -4915,6 +4954,27 @@ class ExistingBoilerInputs(BaseModel, models.Model): help_text="If the boiler can supply steam to the steam turbine for electric production" ) + can_serve_dhw = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if the existing boiler can serve domestic hot water load" + ) + + can_serve_space_heating = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if the existing boiler can serve space heating load" + ) + + can_serve_process_heat = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if the existing boiler can serve process heat load" + ) + # For custom validations within model. def clean(self): error_messages = {} @@ -4987,6 +5047,21 @@ class ExistingBoilerOutputs(BaseModel, models.Model): default = list, ) + thermal_to_dhw_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_space_heating_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_process_heat_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + def clean(self): # perform custom validation here. pass @@ -5151,6 +5226,28 @@ class BoilerInputs(BaseModel, models.Model): help_text="If the boiler can supply steam to the steam turbine for electric production" ) + can_serve_dhw = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if boiler can serve domestic hot water load" + ) + + can_serve_space_heating = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if boiler can serve space heating load" + ) + + can_serve_process_heat = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if boiler can serve process heat load" + ) + + # For custom validations within model. def clean(self): error_messages = {} @@ -5216,6 +5313,21 @@ class BoilerOutputs(BaseModel, models.Model): annual_thermal_production_mmbtu = models.FloatField(null=True, blank=True) + thermal_to_dhw_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_space_heating_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_process_heat_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + class SteamTurbineInputs(BaseModel, models.Model): @@ -5447,6 +5559,25 @@ class SIZE_CLASS_LIST(models.IntegerChoices): help_text="True/False for if technology has the ability to curtail energy production." ) + can_serve_dhw = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if steam turbine can serve space heating load" + ) + can_serve_space_heating = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if steam turbine can serve space heating load" + ) + can_serve_process_heat = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if steam turbine can serve process heat load" + ) + macrs_option_years = models.IntegerField( default=MACRS_YEARS_CHOICES.ZERO, choices=MACRS_YEARS_CHOICES.choices, @@ -5532,6 +5663,21 @@ class SteamTurbineOutputs(BaseModel, models.Model): default = list, ) + thermal_to_dhw_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_space_heating_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + thermal_to_process_heat_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + class HotThermalStorageInputs(BaseModel, models.Model): key = "HotThermalStorage" @@ -5675,6 +5821,25 @@ class HotThermalStorageInputs(BaseModel, models.Model): blank=True, help_text="Rebate per unit installed energy capacity" ) + can_serve_dhw = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if hot thermal storage can serve space heating load" + ) + can_serve_space_heating = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if hot thermal storage can serve space heating load" + ) + can_serve_process_heat = models.BooleanField( + default=False, + null=True, + blank=True, + help_text="Boolean indicator if hot thermal storage can serve process heat load" + ) + def clean(self): # perform custom validation here. @@ -5698,6 +5863,20 @@ class HotThermalStorageOutputs(BaseModel, models.Model): models.FloatField(null=True, blank=True), default = list, ) + storage_to_dhw_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + storage_to_space_heating_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) + + storage_to_process_heat_load_series_mmbtu_per_hour = ArrayField( + models.FloatField(null=True, blank=True), + default = list + ) def clean(self): # perform custom validation here. @@ -6192,6 +6371,55 @@ def clean(self): 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 +class ProcessHeatLoadInputs(BaseModel, models.Model): + # Process Heat + key = "ProcessHeatLoad" + + meta = models.OneToOneField( + APIMeta, + on_delete=models.CASCADE, + related_name="ProcessHeatLoadInputs", + primary_key=True + ) + + possible_sets = [ + ["fuel_loads_mmbtu_per_hour"], + ["annual_mmbtu"], + [], + ] + + annual_mmbtu = models.FloatField( + validators=[ + MinValueValidator(1), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + help_text=("Annual site process heat consumption, used " + "to scale simulated load profile [MMBtu]") + ) + + fuel_loads_mmbtu_per_hour = ArrayField( + models.FloatField( + blank=True + ), + default=list, + blank=True, + help_text=("Typical load over all hours in one year. Must be hourly (8,760 samples), 30 minute (17," + "520 samples), or 15 minute (35,040 samples). All non-net load values must be greater than or " + "equal to zero. " + ) + + ) + + def clean(self): + error_messages = {} + + # possible sets for defining load profile + if not at_least_one_set(self.dict, self.possible_sets): + error_messages["required inputs"] = \ + "Must provide at least one set of valid inputs from {}.".format(self.possible_sets) + class HeatingLoadOutputs(BaseModel, models.Model): key = "HeatingLoadOutputs" @@ -6224,7 +6452,19 @@ class HeatingLoadOutputs(BaseModel, models.Model): blank=True ), default=list, blank=True, - help_text=("Hourly domestic space heating load [MMBTU/hr]") + help_text=("Hourly space heating load [MMBTU/hr]") + ) + + process_heat_thermal_load_series_mmbtu_per_hour = ArrayField( + models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + blank=True + ), + default=list, blank=True, + help_text=("Hourly process heat load [MMBTU/hr]") ) total_heating_thermal_load_series_mmbtu_per_hour = ArrayField( @@ -6248,7 +6488,7 @@ class HeatingLoadOutputs(BaseModel, models.Model): blank=True ), default=list, blank=True, - help_text=("Hourly domestic hot water load [MMBTU/hr]") + help_text=("Hourly domestic hot water boiler fuel load [MMBTU/hr]") ) space_heating_boiler_fuel_load_series_mmbtu_per_hour = ArrayField( @@ -6260,7 +6500,19 @@ class HeatingLoadOutputs(BaseModel, models.Model): blank=True ), default=list, blank=True, - help_text=("Hourly domestic space heating load [MMBTU/hr]") + help_text=("Hourly space heating boiler fuel load [MMBTU/hr]") + ) + + process_heat_boiler_fuel_load_series_mmbtu_per_hour = ArrayField( + models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + blank=True + ), + default=list, blank=True, + help_text=("Hourly process heat boiler fuel load [MMBTU/hr]") ) total_heating_boiler_fuel_load_series_mmbtu_per_hour = ArrayField( @@ -6272,7 +6524,7 @@ class HeatingLoadOutputs(BaseModel, models.Model): blank=True ), default=list, blank=True, - help_text=("Hourly total heating load [MMBTU/hr]") + help_text=("Hourly total boiler fuel load [MMBTU/hr]") ) annual_calculated_dhw_thermal_load_mmbtu = models.FloatField( @@ -6297,6 +6549,17 @@ class HeatingLoadOutputs(BaseModel, models.Model): help_text=("Annual site space heating load [MMBTU]") ) + annual_calculated_process_heat_thermal_load_mmbtu = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + default=0, + help_text=("Annual site process heat load [MMBTU]") + ) + annual_calculated_total_heating_thermal_load_mmbtu = models.FloatField( validators=[ MinValueValidator(0), @@ -6330,6 +6593,17 @@ class HeatingLoadOutputs(BaseModel, models.Model): help_text=("Annual site space heating boiler fuel load [MMBTU]") ) + annual_calculated_process_heat_boiler_fuel_load_mmbtu = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + default=0, + help_text=("Annual site process heat boiler fuel load [MMBTU]") + ) + annual_calculated_total_heating_boiler_fuel_load_mmbtu = models.FloatField( validators=[ MinValueValidator(0), @@ -6419,6 +6693,12 @@ class AbsorptionChillerInputs(BaseModel, models.Model): 'hot_water' )) + HEATING_LOAD_INPUT = models.TextChoices('HEATING_LOAD_INPUT', ( + 'DomesticHotWater', + 'SpaceHeating', + 'ProcessHeat' + )) + thermal_consumption_hot_water_or_steam = models.TextField( blank=True, null=True, @@ -6508,6 +6788,13 @@ class AbsorptionChillerInputs(BaseModel, models.Model): help_text="Percent of upfront project costs to depreciate in year one in addition to scheduled depreciation" ) + heating_load_input = models.TextField( + blank=True, + null=True, + choices=HEATING_LOAD_INPUT.choices, + help_text="Absorption chiller heat input - determines what heating load is added to by absorption chiller use" + ) + def clean(self): pass @@ -6827,6 +7114,20 @@ class GHPInputs(BaseModel, models.Model): blank=True, help_text="If GHP can serve the domestic hot water (DHW) portion of the heating load" ) + + can_serve_space_heating = models.BooleanField( + default=True, + null=True, + blank=True, + help_text="Boolean indicator if GHP can serve space heating load" + ) + + can_serve_process_heat = models.BooleanField( + default=False, + null=True, + blank=True, + help_text="Boolean indicator if GHP can serve process heat load" + ) macrs_option_years = models.IntegerField( default=MACRS_YEARS_CHOICES.FIVE, @@ -7051,6 +7352,9 @@ def filter_none_and_empty_array(d:dict): try: d["DomesticHotWaterLoad"] = filter_none_and_empty_array(meta.DomesticHotWaterLoadInputs.dict) except: pass + try: d["ProcessHeatLoad"] = filter_none_and_empty_array(meta.ProcessHeatLoadInputs.dict) + except: pass + try: d["HotThermalStorage"] = filter_none_and_empty_array(meta.HotThermalStorageInputs.dict) except: pass diff --git a/reoptjl/test/posts/test_thermal_in_results.json b/reoptjl/test/posts/test_thermal_in_results.json index 2918f87a4..2be1bc8ce 100644 --- a/reoptjl/test/posts/test_thermal_in_results.json +++ b/reoptjl/test/posts/test_thermal_in_results.json @@ -37,6 +37,9 @@ "doe_reference_name": "Hospital", "annual_mmbtu": 500.0 }, + "ProcessHeatLoad": { + "annual_mmbtu": 100 + }, "ExistingBoiler": { "efficiency": 0.72, "production_type": "hot_water", diff --git a/reoptjl/test/test_job_endpoint.py b/reoptjl/test/test_job_endpoint.py index 9ccf40042..5ac8f3dc9 100644 --- a/reoptjl/test/test_job_endpoint.py +++ b/reoptjl/test/test_job_endpoint.py @@ -133,10 +133,18 @@ def test_thermal_in_results(self): self.assertIn("CoolingLoad", list(inputs.keys())) self.assertIn("CoolingLoad", list(results.keys())) self.assertIn("CHP", list(results.keys())) + self.assertIn("thermal_to_dhw_load_series_mmbtu_per_hour", list(results["CHP"].keys())) + self.assertIn("thermal_to_space_heating_load_series_mmbtu_per_hour", list(results["CHP"].keys())) + self.assertIn("thermal_to_dhw_load_series_mmbtu_per_hour", list(results["CHP"].keys())) self.assertIn("ExistingChiller",list(results.keys())) self.assertIn("ExistingBoiler", list(results.keys())) self.assertIn("HeatingLoad", list(results.keys())) + self.assertIn("process_heat_thermal_load_series_mmbtu_per_hour", list(results["HeatingLoad"].keys())) + self.assertIn("process_heat_boiler_fuel_load_series_mmbtu_per_hour", list(results["HeatingLoad"].keys())) self.assertIn("HotThermalStorage", list(results.keys())) + self.assertIn("storage_to_dhw_load_series_mmbtu_per_hour", list(results["HotThermalStorage"].keys())) + self.assertIn("storage_to_space_heating_load_series_mmbtu_per_hour", list(results["HotThermalStorage"].keys())) + self.assertIn("storage_to_dhw_load_series_mmbtu_per_hour", list(results["HotThermalStorage"].keys())) self.assertIn("ColdThermalStorage", list(results.keys())) self.assertIn("AbsorptionChiller", list(results.keys())) self.assertIn("GHP", list(results.keys())) @@ -226,7 +234,7 @@ def test_superset_input_fields(self): resp = self.api_client.get(f'/v3/job/{run_uuid}/results') r = json.loads(resp.content) results = r["outputs"] - self.assertAlmostEqual(results["Financial"]["npv"], -258533.19, places=-3) + self.assertAlmostEqual(results["Financial"]["npv"], -258533.19, delta=0.01*results["Financial"]["lcc"]) assert(resp.status_code==200) def test_steamturbine_defaults_from_julia(self): diff --git a/reoptjl/validators.py b/reoptjl/validators.py index b46a9307b..4e7238ea4 100644 --- a/reoptjl/validators.py +++ b/reoptjl/validators.py @@ -4,7 +4,7 @@ from reoptjl.models import MAX_BIG_NUMBER, APIMeta, ExistingBoilerInputs, UserProvidedMeta, SiteInputs, Settings, ElectricLoadInputs, ElectricTariffInputs, \ FinancialInputs, BaseModel, Message, ElectricUtilityInputs, PVInputs, ElectricStorageInputs, GeneratorInputs, WindInputs, SpaceHeatingLoadInputs, \ DomesticHotWaterLoadInputs, CHPInputs, CoolingLoadInputs, ExistingChillerInputs, HotThermalStorageInputs, ColdThermalStorageInputs, \ - AbsorptionChillerInputs, BoilerInputs, SteamTurbineInputs, GHPInputs + AbsorptionChillerInputs, BoilerInputs, SteamTurbineInputs, GHPInputs, ProcessHeatLoadInputs from django.core.exceptions import ValidationError from pyproj import Proj from typing import Tuple @@ -79,7 +79,8 @@ def __init__(self, raw_inputs: dict, ghpghx_inputs_validation_errors=None): ColdThermalStorageInputs, AbsorptionChillerInputs, SteamTurbineInputs, - GHPInputs + GHPInputs, + ProcessHeatLoadInputs ) self.pvnames = [] on_grid_required_object_names = [ @@ -521,6 +522,12 @@ def assign_ref_buildings_from_electric_load(self, load_to_assign): self.add_validation_error(load_to_assign, "doe_reference_name", f"Must provide DOE commercial reference building profiles either under {load_to_assign} or ElectricLoad") + """ + ProcessHeatLaod + """ + if "ProcessHeatLoad" in self.models.keys(): + self.clean_time_series("ProcessHeatLoad", "fuel_loads_mmbtu_per_hour") + """ Off-grid input keys validation """ diff --git a/reoptjl/views.py b/reoptjl/views.py index 6cea32daa..3e41f7f62 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -14,7 +14,7 @@ CoolingLoadOutputs, HeatingLoadOutputs, REoptjlMessageOutputs, HotThermalStorageInputs, HotThermalStorageOutputs,\ ColdThermalStorageInputs, ColdThermalStorageOutputs, AbsorptionChillerInputs, AbsorptionChillerOutputs,\ FinancialInputs, FinancialOutputs, UserUnlinkedRuns, BoilerInputs, BoilerOutputs, SteamTurbineInputs, \ - SteamTurbineOutputs, GHPInputs, GHPOutputs + SteamTurbineOutputs, GHPInputs, GHPOutputs, ProcessHeatLoadInputs import os import requests import numpy as np @@ -53,6 +53,7 @@ def help(request): d["ColdThermalStorage"] = ColdThermalStorageInputs.info_dict(ColdThermalStorageInputs) d["SpaceHeatingLoad"] = SpaceHeatingLoadInputs.info_dict(SpaceHeatingLoadInputs) d["DomesticHotWaterLoad"] = DomesticHotWaterLoadInputs.info_dict(DomesticHotWaterLoadInputs) + d["ProcessHeatLoad"] = ProcessHeatLoadInputs.info_dict(ProcessHeatLoadInputs) d["Site"] = SiteInputs.info_dict(SiteInputs) d["CHP"] = CHPInputs.info_dict(CHPInputs) d["AbsorptionChiller"] = AbsorptionChillerInputs.info_dict(AbsorptionChillerInputs) @@ -214,6 +215,9 @@ def results(request, run_uuid): try: r["inputs"]["DomesticHotWaterLoad"] = meta.DomesticHotWaterLoadInputs.dict except: pass + try: r["inputs"]["ProcessHeatLoad"] = meta.ProcessHeatLoadInputs.dict + except: pass + try: r["inputs"]["CHP"] = meta.CHPInputs.dict except: pass