diff --git a/CHANGELOG.md b/CHANGELOG.md index fe992a55b..c4b978314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,16 @@ Classify the change according to the following categories: ##### Removed ### Patches +## v2.11.0 +### Minor Updates +##### Added +- Enabled hybrid GHX sizing within the GHP model through the **hybrid_ghx_sizing_method** variable + - User is able to select "Automatic" (REopt sizes GHX based on the smaller of the heating or cooling load), "Fractional" (GHX size is a user-defined fraction of the non-hybrid GHX size), or "None" (non-hybrid) + - Auxiliary heater and cooler are both currently only electric + - Outputs added to track the thermal production, electrical consumption, and size of the auxiliary unit +##### Changed +- Updated default value **init_sizing_factor_ft_per_peak_ton** from 246.1 to 75 for the `/ghpghx` endpoint + ## v2.10.1 ### Patches - Make **ERPOutageInputs** field **max_outage_duration** required diff --git a/ghpghx/migrations/0004_ghpghxoutputs_end_of_year_eft_f_and_more.py b/ghpghx/migrations/0004_ghpghxoutputs_end_of_year_eft_f_and_more.py new file mode 100644 index 000000000..f84210ca9 --- /dev/null +++ b/ghpghx/migrations/0004_ghpghxoutputs_end_of_year_eft_f_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.0.7 on 2023-03-07 01:46 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ghpghx', '0003_auto_20211222_1817'), + ] + + operations = [ + migrations.AddField( + model_name='ghpghxoutputs', + name='end_of_year_eft_f', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, help_text='End of year entering fluid temperature for all years in the last iteration of GHX sizing [degF]', null=True, size=None), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='peak_auxiliary_boiler_mmbtu_per_hour', + field=models.FloatField(blank=True, help_text='Peak auxiliary boiler consumption for boiler sizing [MMBtu/hr]', null=True), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='peak_auxiliary_cooling_tower_ton', + field=models.FloatField(blank=True, help_text='Peak auxiliary cooling tower consumption for cooling tower sizing [ton]', null=True), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='yearly_auxiliary_boiler_consumption_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, help_text='Hourly auxiliary boiler consumption, average across simulation years [MMBtu/hr]', null=True, size=None), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='yearly_auxiliary_cooling_tower_consumption_series_ton', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, help_text='Hourly auxiliary cooling tower consumption, average across simulation years [ton]', null=True, size=None), + ), + ] diff --git a/ghpghx/migrations/0005_remove_ghpghxoutputs_peak_auxiliary_boiler_mmbtu_per_hour_and_more.py b/ghpghx/migrations/0005_remove_ghpghxoutputs_peak_auxiliary_boiler_mmbtu_per_hour_and_more.py new file mode 100644 index 000000000..ead1708c6 --- /dev/null +++ b/ghpghx/migrations/0005_remove_ghpghxoutputs_peak_auxiliary_boiler_mmbtu_per_hour_and_more.py @@ -0,0 +1,75 @@ +# Generated by Django 4.0.7 on 2023-03-10 02:48 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ghpghx', '0004_ghpghxoutputs_end_of_year_eft_f_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='ghpghxoutputs', + name='peak_auxiliary_boiler_mmbtu_per_hour', + ), + migrations.RemoveField( + model_name='ghpghxoutputs', + name='peak_auxiliary_cooling_tower_ton', + ), + migrations.RemoveField( + model_name='ghpghxoutputs', + name='yearly_auxiliary_boiler_consumption_series_mmbtu_per_hour', + ), + migrations.RemoveField( + model_name='ghpghxoutputs', + name='yearly_auxiliary_cooling_tower_consumption_series_ton', + ), + migrations.AddField( + model_name='ghpghxinputs', + name='is_hybrid_ghx', + field=models.BooleanField(blank=True, default=True, help_text='If the GHP system uses a hybrid GHX with auxiliary heater or cooler', null=True), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='annual_aux_cooler_electric_consumption_kwh', + field=models.FloatField(blank=True, help_text='Annual auxiliary cooler electrical consumption [kWh]', null=True), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='annual_aux_heater_electric_consumption_kwh', + field=models.FloatField(blank=True, help_text='Annual auxiliary heater electrical consumption [kWh]', null=True), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='peak_aux_cooler_thermal_production_ton', + field=models.FloatField(blank=True, help_text='Peak auxiliary cooler thermal production for cooler sizing [ton]', null=True), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='peak_aux_heater_thermal_production_mmbtu_per_hour', + field=models.FloatField(blank=True, help_text='Peak auxiliary heater thermal production for heater sizing [MMBtu/hr]', null=True), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='yearly_aux_cooler_electric_consumption_series_kw', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, help_text='Hourly auxiliary cooler electrical consumption, average across simulation years [kW]', null=True, size=None), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='yearly_aux_cooler_thermal_production_series_kwt', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, help_text='Hourly auxiliary cooler thermal production, average across simulation years [kW-thermal]', null=True, size=None), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='yearly_aux_heater_electric_consumption_series_kw', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, help_text='Hourly auxiliary heater electrical consumption, average across simulation years [kW]', null=True, size=None), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='yearly_aux_heater_thermal_production_series_mmbtu_per_hour', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, help_text='Hourly auxiliary heater thermal production, average across simulation years [MMBtu/hr]', null=True, size=None), + ), + ] diff --git a/ghpghx/migrations/0006_alter_ghpghxinputs_is_hybrid_ghx.py b/ghpghx/migrations/0006_alter_ghpghxinputs_is_hybrid_ghx.py new file mode 100644 index 000000000..7be5d3e83 --- /dev/null +++ b/ghpghx/migrations/0006_alter_ghpghxinputs_is_hybrid_ghx.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.7 on 2023-03-10 05:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ghpghx', '0005_remove_ghpghxoutputs_peak_auxiliary_boiler_mmbtu_per_hour_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='ghpghxinputs', + name='is_hybrid_ghx', + field=models.BooleanField(blank=True, help_text='If the GHP system uses a hybrid GHX with auxiliary heater or cooler', null=True), + ), + ] diff --git a/ghpghx/migrations/0007_ghpghxoutputs_aux_heat_exchange_unit_type_and_more.py b/ghpghx/migrations/0007_ghpghxoutputs_aux_heat_exchange_unit_type_and_more.py new file mode 100644 index 000000000..bab32e3cd --- /dev/null +++ b/ghpghx/migrations/0007_ghpghxoutputs_aux_heat_exchange_unit_type_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.7 on 2023-03-10 07:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ghpghx', '0006_alter_ghpghxinputs_is_hybrid_ghx'), + ] + + operations = [ + migrations.AddField( + model_name='ghpghxoutputs', + name='aux_heat_exchange_unit_type', + field=models.TextField(blank=True, help_text='Specifies if the auxiliary heat exchange unit is a heater or cooler', null=True), + ), + migrations.AlterField( + model_name='ghpghxinputs', + name='is_hybrid_ghx', + field=models.BooleanField(blank=True, default=True, help_text='If the GHP system uses a hybrid GHX with auxiliary heater or cooler', null=True), + ), + ] diff --git a/ghpghx/migrations/0008_ghpghxinputs_aux_cooler_energy_use_intensity_kwe_per_kwt_and_more.py b/ghpghx/migrations/0008_ghpghxinputs_aux_cooler_energy_use_intensity_kwe_per_kwt_and_more.py new file mode 100644 index 000000000..f655db42c --- /dev/null +++ b/ghpghx/migrations/0008_ghpghxinputs_aux_cooler_energy_use_intensity_kwe_per_kwt_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.7 on 2023-03-12 04:18 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ghpghx', '0007_ghpghxoutputs_aux_heat_exchange_unit_type_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='ghpghxinputs', + name='aux_cooler_energy_use_intensity_kwe_per_kwt', + field=models.FloatField(blank=True, default=0.2, help_text='The energy use intensity of the auxiliary cooler [kWe/kWt]', null=True, validators=[django.core.validators.MinValueValidator(0.001), django.core.validators.MaxValueValidator(10.0)]), + ), + migrations.AddField( + model_name='ghpghxinputs', + name='aux_heater_thermal_efficiency', + field=models.FloatField(blank=True, default=0.98, help_text='The thermal efficiency (thermal_out/fuel_in) of the auxiliary heater', null=True, validators=[django.core.validators.MinValueValidator(0.001), django.core.validators.MaxValueValidator(10.0)]), + ), + ] diff --git a/ghpghx/migrations/0009_ghpghxinputs_hybrid_sizing_flag_and_more.py b/ghpghx/migrations/0009_ghpghxinputs_hybrid_sizing_flag_and_more.py new file mode 100644 index 000000000..365e4efaa --- /dev/null +++ b/ghpghx/migrations/0009_ghpghxinputs_hybrid_sizing_flag_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.7 on 2023-03-13 16:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ghpghx', '0008_ghpghxinputs_aux_cooler_energy_use_intensity_kwe_per_kwt_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='ghpghxinputs', + name='hybrid_sizing_flag', + field=models.FloatField(blank=True, default=1.0, help_text='Possible values: -2 (size for heating), -1.0 (size for cooling), 1.0 (non-hybrid), value between 0-1 (fraction of full GHE size)', null=True), + ), + migrations.AddField( + model_name='ghpghxinputs', + name='is_heating_electric', + field=models.BooleanField(blank=True, default=True, help_text='Set to True if heating is electric, false otherwise', null=True), + ), + ] diff --git a/ghpghx/migrations/0010_remove_ghpghxoutputs_end_of_year_eft_f_and_more.py b/ghpghx/migrations/0010_remove_ghpghxoutputs_end_of_year_eft_f_and_more.py new file mode 100644 index 000000000..bd6cd9406 --- /dev/null +++ b/ghpghx/migrations/0010_remove_ghpghxoutputs_end_of_year_eft_f_and_more.py @@ -0,0 +1,59 @@ +# Generated by Django 4.0.7 on 2023-03-20 17:02 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ghpghx', '0009_ghpghxinputs_hybrid_sizing_flag_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='ghpghxoutputs', + name='end_of_year_eft_f', + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='end_of_year_ghx_lft_f', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, help_text='End of year GHX leaving fluid temperature for all years in the last iteration of GHX sizing [degF]', null=True, size=None), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='ghx_soln_number_of_iterations', + field=models.IntegerField(blank=True, help_text='The number of iterations taken to get GHX sizing', null=True), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='max_yearly_ghx_lft_f', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, help_text='Maximum GHX leaving fluid temperature for all years in the last iteration of GHX sizing [degF]', null=True, size=None), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='min_yearly_ghx_lft_f', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, help_text='Minimum GHX leaving fluid temperature for all years in the last iteration of GHX sizing [degF]', null=True, size=None), + ), + migrations.AddField( + model_name='ghpghxoutputs', + name='yearly_ghx_lft_series_f', + field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(blank=True, null=True), blank=True, default=list, help_text='Hourly GHX leaving fluid temperature (lft), average across simulation years [kW]', null=True, size=None), + ), + migrations.AlterField( + model_name='ghpghxinputs', + name='aux_cooler_energy_use_intensity_kwe_per_kwt', + field=models.FloatField(blank=True, default=0.02, help_text='The energy use intensity of the auxiliary cooler [kWe/kWt]', null=True, validators=[django.core.validators.MinValueValidator(0.001), django.core.validators.MaxValueValidator(1.0)]), + ), + migrations.AlterField( + model_name='ghpghxinputs', + name='aux_heater_thermal_efficiency', + field=models.FloatField(blank=True, default=0.98, help_text='The thermal efficiency (thermal_out/fuel_in) of the auxiliary heater', null=True, validators=[django.core.validators.MinValueValidator(0.001), django.core.validators.MaxValueValidator(1.0)]), + ), + migrations.AlterField( + model_name='ghpghxinputs', + name='hybrid_sizing_flag', + field=models.FloatField(blank=True, default=1.0, help_text='Possible values: -2 (size for heating), -1.0 (size for cooling), 1.0 (non-hybrid), value between 0-1 (fraction of full GHX size)', null=True), + ), + ] diff --git a/ghpghx/migrations/0011_ghpghxinputs_hybrid_ghx_sizing_fraction_and_more.py b/ghpghx/migrations/0011_ghpghxinputs_hybrid_ghx_sizing_fraction_and_more.py new file mode 100644 index 000000000..c654e6f75 --- /dev/null +++ b/ghpghx/migrations/0011_ghpghxinputs_hybrid_ghx_sizing_fraction_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.7 on 2023-03-22 03:34 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ghpghx', '0010_remove_ghpghxoutputs_end_of_year_eft_f_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='ghpghxinputs', + name='hybrid_ghx_sizing_fraction', + field=models.FloatField(blank=True, default=0.6, help_text='Applies fraction to full GHX size for hybrid sizing (value between 0.1 - 1.0)', null=True, validators=[django.core.validators.MinValueValidator(0.1), django.core.validators.MaxValueValidator(1.0)]), + ), + migrations.AddField( + model_name='ghpghxinputs', + name='hybrid_ghx_sizing_method', + field=models.TextField(blank=True, default='None', help_text="Possible values: 'Fractional' (user inputs fraction of full GHX size), 'Automatic' (REopt determines based on the smaller heating or cooling load), 'None' (non-hybrid)", null=True), + ), + ] diff --git a/ghpghx/migrations/0012_remove_ghpghxinputs_is_hybrid_ghx.py b/ghpghx/migrations/0012_remove_ghpghxinputs_is_hybrid_ghx.py new file mode 100644 index 000000000..40c479cb9 --- /dev/null +++ b/ghpghx/migrations/0012_remove_ghpghxinputs_is_hybrid_ghx.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.7 on 2023-03-23 21:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ghpghx', '0011_ghpghxinputs_hybrid_ghx_sizing_fraction_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='ghpghxinputs', + name='is_hybrid_ghx', + ), + ] diff --git a/ghpghx/migrations/0013_alter_ghpghxinputs_init_sizing_factor_ft_per_peak_ton.py b/ghpghx/migrations/0013_alter_ghpghxinputs_init_sizing_factor_ft_per_peak_ton.py new file mode 100644 index 000000000..726fc9c83 --- /dev/null +++ b/ghpghx/migrations/0013_alter_ghpghxinputs_init_sizing_factor_ft_per_peak_ton.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.7 on 2023-03-28 18:32 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ghpghx', '0012_remove_ghpghxinputs_is_hybrid_ghx'), + ] + + operations = [ + migrations.AlterField( + model_name='ghpghxinputs', + name='init_sizing_factor_ft_per_peak_ton', + field=models.FloatField(blank=True, default=75, help_text='Initial guess of total feet of GHX boreholes (total feet = N bores * Length bore) based on peak ton heating/cooling [ft/ton]', validators=[django.core.validators.MinValueValidator(1.0), django.core.validators.MaxValueValidator(5000.0)]), + ), + ] diff --git a/ghpghx/models.py b/ghpghx/models.py index 68f2ecbe3..70cfca520 100644 --- a/ghpghx/models.py +++ b/ghpghx/models.py @@ -214,9 +214,26 @@ def clean(self): default=15, validators=[MinValueValidator(1), MaxValueValidator(15)], help_text="Maximum number of sizing iterations before the GHPGHX model times out") init_sizing_factor_ft_per_peak_ton = models.FloatField(blank=True, - default=246.1, validators=[MinValueValidator(1.0), MaxValueValidator(5000.0)], + default=75, validators=[MinValueValidator(1.0), MaxValueValidator(5000.0)], help_text="Initial guess of total feet of GHX boreholes (total feet = N bores * Length bore) based on peak ton heating/cooling [ft/ton]") - + + # Hybrid flag + hybrid_ghx_sizing_method = models.TextField(null=True, blank=True, default="None", + help_text="Possible values: 'Fractional' (user inputs fraction of full GHX size), 'Automatic' (REopt determines based on the smaller heating or cooling load), 'None' (non-hybrid)") + hybrid_sizing_flag = models.FloatField(null=True, blank=True, default=1.0, + help_text="Possible values: -2 (size for heating), -1.0 (size for cooling), 1.0 (non-hybrid), value between 0-1 (fraction of full GHX size)") + hybrid_ghx_sizing_fraction = models.FloatField(null=True, blank=True, default=0.6, + validators=[MinValueValidator(0.1), MaxValueValidator(1.0)], + help_text="Applies fraction to full GHX size for hybrid sizing (value between 0.1 - 1.0)") + is_heating_electric = models.BooleanField(null=True, blank=True, default=True, + help_text="Set to True if heating is electric, false otherwise") + aux_heater_thermal_efficiency = models.FloatField(null=True, blank=True, + default=0.98, validators=[MinValueValidator(0.001), MaxValueValidator(1.0)], + help_text="The thermal efficiency (thermal_out/fuel_in) of the auxiliary heater") + aux_cooler_energy_use_intensity_kwe_per_kwt = models.FloatField(null=True, blank=True, + default=0.02, validators=[MinValueValidator(0.001), MaxValueValidator(1.0)], + help_text="The energy use intensity of the auxiliary cooler [kWe/kWt]") + class GHPGHXOutputs(models.Model): # Outputs/results @@ -258,8 +275,44 @@ class GHPGHXOutputs(models.Model): help_text="Average cooling heatpump system coefficient of performance (COP) (includes ghx pump allocation)") solved_eft_error_f = models.FloatField(null=True, blank=True, help_text="Error between the solved GHPGHX system EFT and the max or min limit for EFT") - - + # Hybrid + yearly_aux_heater_thermal_production_series_mmbtu_per_hour = ArrayField(models.FloatField(null=True, blank=True), + default=list, null=True, blank=True, + help_text="Hourly auxiliary heater thermal production, average across simulation years [MMBtu/hr]") + yearly_aux_cooler_thermal_production_series_kwt = ArrayField(models.FloatField(null=True, blank=True), + default=list, null=True, blank=True, + help_text="Hourly auxiliary cooler thermal production, average across simulation years [kW-thermal]") + yearly_aux_heater_electric_consumption_series_kw = ArrayField(models.FloatField(null=True, blank=True), + default=list, null=True, blank=True, + help_text="Hourly auxiliary heater electrical consumption, average across simulation years [kW]") + yearly_aux_cooler_electric_consumption_series_kw = ArrayField(models.FloatField(null=True, blank=True), + default=list, null=True, blank=True, + help_text="Hourly auxiliary cooler electrical consumption, average across simulation years [kW]") + peak_aux_heater_thermal_production_mmbtu_per_hour = models.FloatField(null=True, blank=True, + help_text="Peak auxiliary heater thermal production for heater sizing [MMBtu/hr]") + peak_aux_cooler_thermal_production_ton = models.FloatField(null=True, blank=True, + help_text="Peak auxiliary cooler thermal production for cooler sizing [ton]") + annual_aux_heater_electric_consumption_kwh = models.FloatField(null=True, blank=True, + help_text="Annual auxiliary heater electrical consumption [kWh]") + annual_aux_cooler_electric_consumption_kwh = models.FloatField(null=True, blank=True, + help_text="Annual auxiliary cooler electrical consumption [kWh]") + end_of_year_ghx_lft_f = ArrayField(models.FloatField(null=True, blank=True), + default=list, null=True, blank=True, + help_text="End of year GHX leaving fluid temperature for all years in the last iteration of GHX sizing [degF]") + max_yearly_ghx_lft_f = ArrayField(models.FloatField(null=True, blank=True), + default=list, null=True, blank=True, + help_text="Maximum GHX leaving fluid temperature for all years in the last iteration of GHX sizing [degF]") + min_yearly_ghx_lft_f = ArrayField(models.FloatField(null=True, blank=True), + default=list, null=True, blank=True, + help_text="Minimum GHX leaving fluid temperature for all years in the last iteration of GHX sizing [degF]") + aux_heat_exchange_unit_type = models.TextField(null=True, blank=True, + help_text="Specifies if the auxiliary heat exchange unit is a heater or cooler") + yearly_ghx_lft_series_f = ArrayField(models.FloatField(null=True, blank=True), + default=list, null=True, blank=True, + help_text="Hourly GHX leaving fluid temperature (lft), average across simulation years [kW]") + ghx_soln_number_of_iterations = models.IntegerField(null=True, blank=True, + help_text="The number of iterations taken to get GHX sizing") + class ModelManager(object): def __init__(self): diff --git a/ghpghx/tests/posts/test_ghpghx_POST.json b/ghpghx/tests/posts/test_ghpghx_POST.json index 0e6a7b2a6..f96ad945b 100644 --- a/ghpghx/tests/posts/test_ghpghx_POST.json +++ b/ghpghx/tests/posts/test_ghpghx_POST.json @@ -5,5 +5,8 @@ "ghx_model": "TESS", "tess_ghx_minimum_timesteps_per_hour": 1, "max_sizing_iterations": 10, - "init_sizing_factor_ft_per_peak_ton": 300.0 + "init_sizing_factor_ft_per_peak_ton": 300.0, + "hybrid_ghx_sizing_method": "None", + "aux_heater_thermal_efficiency": 0.95, + "aux_cooler_energy_use_intensity_kwe_per_kwt": 0.02 } \ No newline at end of file diff --git a/ghpghx/tests/posts/test_hybrid_ghpghx_POST.json b/ghpghx/tests/posts/test_hybrid_ghpghx_POST.json new file mode 100644 index 000000000..3d89cacce --- /dev/null +++ b/ghpghx/tests/posts/test_hybrid_ghpghx_POST.json @@ -0,0 +1,12 @@ +{ + "borehole_depth_ft": 400.0, + "simulation_years": 25, + "solver_eft_tolerance_f": 2, + "ghx_model": "TESS", + "tess_ghx_minimum_timesteps_per_hour": 1, + "max_sizing_iterations": 15, + "init_sizing_factor_ft_per_peak_ton": 246.1, + "aux_heater_thermal_efficiency": 0.98, + "aux_cooler_energy_use_intensity_kwe_per_kwt": 0.02, + "hybrid_ghx_sizing_method": "Automatic" +} \ No newline at end of file diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 62dc19179..05405f471 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -308,9 +308,9 @@ version = "0.4.1" [[deps.GeoInterface]] deps = ["Extents"] -git-tree-sha1 = "e07a1b98ed72e3cdd02c6ceaab94b8a606faca40" +git-tree-sha1 = "0eb6de0b312688f852f347171aba888658e29f20" uuid = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" -version = "1.2.1" +version = "1.3.0" [[deps.GeoInterfaceRecipes]] deps = ["GeoInterface", "RecipesBase"] @@ -319,16 +319,16 @@ uuid = "0329782f-3d07-4b52-b9f6-d3137cf03c7a" version = "1.0.0" [[deps.GhpGhx]] -git-tree-sha1 = "19a4afe42a79f22612478a87d9d938c6a5087af2" +git-tree-sha1 = "c74e436a02552a1d20bae5c6b84bb27cf33e8eb9" repo-rev = "main" -repo-url = "https://github.com/NREL/GhpGhx.jl" +repo-url = "https://github.com/NREL/GhpGhx.jl.git" uuid = "7ce85f02-24a8-4d69-a3f0-14b5daa7d30c" version = "0.1.0" [[deps.Glob]] -git-tree-sha1 = "4df9f7e06108728ebf00a0a11edee4b29a482bb2" +git-tree-sha1 = "97285bbd5230dd766e9ef6749b80fc617126d496" uuid = "c27321d9-0574-5035-807b-f59d2c89b15c" -version = "1.3.0" +version = "1.3.1" [[deps.Graphics]] deps = ["Colors", "LinearAlgebra", "NaNMath"] @@ -368,9 +368,9 @@ version = "0.9.4" [[deps.InfrastructureModels]] deps = ["JuMP", "Memento"] -git-tree-sha1 = "88da90ad5d8ca541350c156bea2715f3a23836ce" +git-tree-sha1 = "dc1e2eba1a98aa457b629fe1723d9078ecb74340" uuid = "2030c09a-7f63-5d83-885d-db604e0e9cc0" -version = "0.7.6" +version = "0.7.7" [[deps.IniFile]] git-tree-sha1 = "f550e6e32074c939295eb5ea6de31849ac2c9625" @@ -663,9 +663,9 @@ uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" version = "0.5.5+0" [[deps.OrderedCollections]] -git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c" +git-tree-sha1 = "d78db6df34313deaca15c5c0b9ff562c704fe1ab" uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -version = "1.4.1" +version = "1.5.0" [[deps.PROJ_jll]] deps = ["Artifacts", "JLLWrappers", "LibCURL_jll", "Libdl", "Libtiff_jll", "Pkg", "SQLite_jll"] @@ -828,9 +828,9 @@ version = "0.1.1" [[deps.StaticArrays]] deps = ["LinearAlgebra", "Random", "StaticArraysCore", "Statistics"] -git-tree-sha1 = "6aa098ef1012364f2ede6b17bf358c7f1fbe90d4" +git-tree-sha1 = "b8d897fe7fa688e93aef573711cb207c08c9e11e" uuid = "90137ffa-7385-5640-81b9-e52037218182" -version = "1.5.17" +version = "1.5.19" [[deps.StaticArraysCore]] git-tree-sha1 = "6b7ba252635a5eff6a0b0664a41ee140a1c9e72a" diff --git a/reo/exceptions.py b/reo/exceptions.py index 62a54423b..225e6fe43 100644 --- a/reo/exceptions.py +++ b/reo/exceptions.py @@ -257,3 +257,18 @@ def __init__(self, exc_type, exc_value, exc_traceback, task='', run_uuid='', use message = "Error saving to database." super(SaveToDatabase, self).__init__(task=task, name=self.__name__, run_uuid=run_uuid, user_uuid=user_uuid, message=message, traceback=debug_msg) + +class GHXMaxIterationsError(REoptError): + """ + Catches the case where GHX sizing hits the maximum number of iterations indicating GHP solution may not have converged + + Attributes: + message - explanation of the error + """ + + __name__ = 'GHXMaxIterationsError' + + def __init__(self, task='', run_uuid='', user_uuid=''): + message = "The GHX sizing solution did not converge. This particular scenario may be difficult or impossible for the current REopt model to solve. Try adjusting the 'GHX simulation solver initial guess (ft/ton)' variable." + super(GHXMaxIterationsError, self).__init__(task=task, name=self.__name__, run_uuid=run_uuid, user_uuid=user_uuid, + message=message, traceback='') diff --git a/reo/migrations/0149_ghpmodel_aux_cooler_energy_use_intensity_kwe_per_kwt_and_more.py b/reo/migrations/0149_ghpmodel_aux_cooler_energy_use_intensity_kwe_per_kwt_and_more.py new file mode 100644 index 000000000..a1e093c3e --- /dev/null +++ b/reo/migrations/0149_ghpmodel_aux_cooler_energy_use_intensity_kwe_per_kwt_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.0.7 on 2023-02-28 23:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reo', '0148_alter_electrictariffmodel_coincident_peak_load_active_timesteps'), + ] + + operations = [ + migrations.AddField( + model_name='ghpmodel', + name='aux_cooler_energy_use_intensity_kwe_per_kwt', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='ghpmodel', + name='aux_cooler_installed_cost_us_dollars_per_ton', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='ghpmodel', + name='aux_heater_installed_cost_us_dollars_per_mmbtu_per_hr', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='ghpmodel', + name='aux_heater_thermal_efficiency', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='ghpmodel', + name='aux_heater_type', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='ghpmodel', + name='is_hybrid_ghx', + field=models.BooleanField(blank=True, null=True), + ), + ] diff --git a/reo/migrations/0150_remove_ghpmodel_is_hybrid_ghx.py b/reo/migrations/0150_remove_ghpmodel_is_hybrid_ghx.py new file mode 100644 index 000000000..2de31488c --- /dev/null +++ b/reo/migrations/0150_remove_ghpmodel_is_hybrid_ghx.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.7 on 2023-03-10 02:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reo', '0149_ghpmodel_aux_cooler_energy_use_intensity_kwe_per_kwt_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='ghpmodel', + name='is_hybrid_ghx', + ), + ] diff --git a/reo/migrations/0151_remove_ghpmodel_aux_cooler_energy_use_intensity_kwe_per_kwt_and_more.py b/reo/migrations/0151_remove_ghpmodel_aux_cooler_energy_use_intensity_kwe_per_kwt_and_more.py new file mode 100644 index 000000000..f70cb286a --- /dev/null +++ b/reo/migrations/0151_remove_ghpmodel_aux_cooler_energy_use_intensity_kwe_per_kwt_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 4.0.7 on 2023-03-12 04:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reo', '0150_remove_ghpmodel_is_hybrid_ghx'), + ] + + operations = [ + migrations.RemoveField( + model_name='ghpmodel', + name='aux_cooler_energy_use_intensity_kwe_per_kwt', + ), + migrations.RemoveField( + model_name='ghpmodel', + name='aux_heater_thermal_efficiency', + ), + ] diff --git a/reo/migrations/0152_ghpmodel_aux_unit_capacity_sizing_factor_on_peak_load.py b/reo/migrations/0152_ghpmodel_aux_unit_capacity_sizing_factor_on_peak_load.py new file mode 100644 index 000000000..cdd6dd6f1 --- /dev/null +++ b/reo/migrations/0152_ghpmodel_aux_unit_capacity_sizing_factor_on_peak_load.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.7 on 2023-03-13 21:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reo', '0151_remove_ghpmodel_aux_cooler_energy_use_intensity_kwe_per_kwt_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='ghpmodel', + name='aux_unit_capacity_sizing_factor_on_peak_load', + field=models.FloatField(blank=True, null=True), + ), + ] diff --git a/reo/migrations/0153_merge_20230329_1652.py b/reo/migrations/0153_merge_20230329_1652.py new file mode 100644 index 000000000..47d8dbc46 --- /dev/null +++ b/reo/migrations/0153_merge_20230329_1652.py @@ -0,0 +1,14 @@ +# Generated by Django 4.0.7 on 2023-03-29 16:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reo', '0149_alter_messagemodel_index_together_and_more'), + ('reo', '0152_ghpmodel_aux_unit_capacity_sizing_factor_on_peak_load'), + ] + + operations = [ + ] diff --git a/reo/models.py b/reo/models.py index 4237bfbdc..7e6816f61 100644 --- a/reo/models.py +++ b/reo/models.py @@ -1143,6 +1143,11 @@ class GHPModel(models.Model): ghpghx_response_uuids = ArrayField(models.TextField(null=True, blank=True), default=list, null=True) ghpghx_responses = ArrayField(PickledObjectField(null=True, editable=True), null=True, default=list) can_serve_dhw = models.BooleanField(null=True, blank=True) + + aux_heater_type = models.TextField(null=True, blank=True) + aux_heater_installed_cost_us_dollars_per_mmbtu_per_hr = models.FloatField(null=True, blank=True) + aux_cooler_installed_cost_us_dollars_per_ton = models.FloatField(null=True, blank=True) + aux_unit_capacity_sizing_factor_on_peak_load = models.FloatField(null=True, blank=True) macrs_option_years = models.IntegerField(null=True, blank=True) macrs_bonus_pct = models.FloatField(null=True, blank=True) macrs_itc_reduction = models.FloatField(null=True, blank=True) diff --git a/reo/nested_inputs.py b/reo/nested_inputs.py index 05a384aa8..7fb121fdc 100644 --- a/reo/nested_inputs.py +++ b/reo/nested_inputs.py @@ -2509,7 +2509,32 @@ def list_of_dict(input): "can_serve_dhw": { "type": "bool", "default": False, "description": "If GHP can serve the domestic hot water (DHW) portion of the heating load" + }, + "aux_heater_type": { + "type": "str", "default": "electric", + "description": "The type of auxiliary heater, 'electric' or 'natural_gas'" }, + "aux_heater_installed_cost_us_dollars_per_mmbtu_per_hr": { + "type": "float", + "min": 0.0, + "max": 1.0e6, + "default": 26000.0, + "description": "Installed cost of auxiliary heater for hybrid ghx in $/MMBtu/hr based on peak thermal production" + }, + "aux_cooler_installed_cost_us_dollars_per_ton": { + "type": "float", + "min": 0.0, + "max": 1.0e6, + "default": 400.0, + "description": "Installed cost of auxiliary cooler (e.g. cooling tower) for hybrid ghx in $/ton based on peak thermal production" + }, + "aux_unit_capacity_sizing_factor_on_peak_load": { + "type": "float", + "min": 1.0, + "max": 5.0, + "default": 1.2, + "description": "Factor on peak heating and cooling load served by the auxiliary heater/cooler used for determining heater/cooler installed capacity" + }, "macrs_option_years": { "type": "int", "restrict_to": macrs_schedules, diff --git a/reo/scenario.py b/reo/scenario.py index 6c9e622a0..dbfd8d6df 100644 --- a/reo/scenario.py +++ b/reo/scenario.py @@ -49,7 +49,7 @@ from reo.src import ghp from celery import shared_task, Task from reo.models import ModelManager -from reo.exceptions import REoptError, UnexpectedError, LoadProfileError, WindDownloadError, PVWattsDownloadError, RequestError +from reo.exceptions import REoptError, UnexpectedError, LoadProfileError, WindDownloadError, PVWattsDownloadError, RequestError, GHXMaxIterationsError from tastypie.test import TestApiClient from reo.utilities import TONHOUR_TO_KWHT, get_climate_zone_and_nearest_city from ghpghx.models import GHPGHXInputs @@ -389,6 +389,8 @@ def setup_pv(pv_dict, latitude, longitude, time_steps_per_hour): number_of_ghpghx = len(inputs_dict["Site"]["GHP"]["ghpghx_inputs"]) for i in range(number_of_ghpghx): ghpghx_post = inputs_dict["Site"]["GHP"]["ghpghx_inputs"][i] + hybrid_ghx_sizing_method = ghpghx_post.pop("hybrid_ghx_sizing_method", None) + ghpghx_post["latitude"] = inputs_dict["Site"]["latitude"] ghpghx_post["longitude"] = inputs_dict["Site"]["longitude"] # Only SpaceHeating portion of Heating Load gets served by GHP, unless allowed by can_serve_dhw @@ -410,6 +412,41 @@ def setup_pv(pv_dict, latitude, longitude, time_steps_per_hour): k_by_zone = copy.deepcopy(GHPGHXInputs.ground_k_by_climate_zone) climate_zone, nearest_city, geometric_flag = get_climate_zone_and_nearest_city(ghpghx_post["latitude"], ghpghx_post["longitude"], BuiltInProfile.default_cities) ghpghx_post["ground_thermal_conductivity_btu_per_hr_ft_f"] = k_by_zone[climate_zone] + + # Hybrid + # Determine if location is heating or cooling dominated + if hybrid_ghx_sizing_method == "Automatic": + determine_heat_cool_post = copy.deepcopy(ghpghx_post) + determine_heat_cool_post["simulation_years"] = 2 + determine_heat_cool_post["max_sizing_iterations"] = 1 + + determine_heat_cool_post_resp = client.post('/v1/ghpghx/', data=determine_heat_cool_post) + determine_heat_cool_post_resp_dict = json.loads(determine_heat_cool_post_resp.content) + + determine_heat_cool_uuid = determine_heat_cool_post_resp_dict.get('ghp_uuid') + determine_heat_cool_results_url = "/v1/ghpghx/"+determine_heat_cool_uuid+"/results/" + determine_heat_cool_results_resp = client.get(determine_heat_cool_results_url) + determine_heat_cool_results_resp_dict = json.loads(determine_heat_cool_results_resp.content) + temp_diff = determine_heat_cool_results_resp_dict["outputs"]["end_of_year_ghx_lft_f"][1] - determine_heat_cool_results_resp_dict["outputs"]["end_of_year_ghx_lft_f"][0] + + hybrid_sizing_flag = 1.0 + if temp_diff > 0: + hybrid_sizing_flag = -2.0 + elif temp_diff < 0: + hybrid_sizing_flag = -1.0 + + ghpghx_post["hybrid_sizing_flag"] = hybrid_sizing_flag + + elif hybrid_ghx_sizing_method == "Fractional": + # TODO: Update if default fractional sizing changes + hybrid_ghx_sizing_fraction = ghpghx_post.pop("hybrid_ghx_sizing_fraction", 0.6) + ghpghx_post["hybrid_sizing_flag"] = hybrid_ghx_sizing_fraction + + # Other hybrid inputs + ghpghx_post["is_heating_electric"] = False + if inputs_dict["Site"]["GHP"]["aux_heater_type"] == "electric": + ghpghx_post["is_heating_electric"] = True + # Call /ghpghx endpoint to size GHP and GHX ghpghx_post_resp = client.post('/v1/ghpghx/', data=ghpghx_post) ghpghx_post_resp_dict = json.loads(ghpghx_post_resp.content) @@ -417,6 +454,12 @@ def setup_pv(pv_dict, latitude, longitude, time_steps_per_hour): ghpghx_results_url = "/v1/ghpghx/"+ghpghx_uuid_list[i]+"/results/" ghpghx_results_resp = client.get(ghpghx_results_url) # same as doing ghpModelManager.make_response(ghp_uuid) ghpghx_results_resp_dict = json.loads(ghpghx_results_resp.content) + + if ghpghx_results_resp_dict["outputs"]["ghx_soln_number_of_iterations"] == ghpghx_results_resp_dict["inputs"]["max_sizing_iterations"]: + ghx_error = GHXMaxIterationsError(task=self.name, run_uuid=run_uuid, user_uuid=inputs_dict.get('user_uuid')) + ghx_error.save_to_db() + raise ghx_error + ghp_option_list.append(ghp.GHPGHX(dfm=dfm, response=ghpghx_results_resp_dict, **inputs_dict["Site"]["GHP"])) @@ -488,6 +531,9 @@ def setup_pv(pv_dict, latitude, longitude, time_steps_per_hour): e.save_to_db() raise e + if isinstance(e, GHXMaxIterationsError): + raise e + if hasattr(e, 'args'): if len(e.args) > 0: if e.args[0] == 'Unable to download wind data': diff --git a/reo/src/ghp.py b/reo/src/ghp.py index e35ffa5e4..2f83eb72a 100644 --- a/reo/src/ghp.py +++ b/reo/src/ghp.py @@ -21,7 +21,16 @@ def __init__(self, dfm, response, **kwargs): self.length_boreholes_ft = response["outputs"]["length_boreholes_ft"] self.yearly_total_electric_consumption_series_kw = response["outputs"]["yearly_total_electric_consumption_series_kw"] self.peak_combined_heatpump_thermal_ton = response["outputs"]["peak_combined_heatpump_thermal_ton"] - + # Hybrid fields + self.yearly_aux_heater_thermal_production_series_mmbtu_per_hour = response["outputs"]["yearly_aux_heater_thermal_production_series_mmbtu_per_hour"] + self.yearly_aux_cooler_thermal_production_series_kwt = response["outputs"]["yearly_aux_cooler_thermal_production_series_kwt"] + self.yearly_aux_heater_electric_consumption_series_kw = response["outputs"]["yearly_aux_heater_electric_consumption_series_kw"] + self.yearly_aux_cooler_electric_consumption_series_kw = response["outputs"]["yearly_aux_cooler_electric_consumption_series_kw"] + self.peak_aux_heater_thermal_production_mmbtu_per_hour = response["outputs"]["peak_aux_heater_thermal_production_mmbtu_per_hour"] + self.peak_aux_cooler_thermal_production_ton = response["outputs"]["peak_aux_cooler_thermal_production_ton"] + self.annual_aux_heater_electric_consumption_kwh = response["outputs"]["annual_aux_heater_electric_consumption_kwh"] + self.annual_aux_cooler_electric_consumption_kwh = response["outputs"]["annual_aux_cooler_electric_consumption_kwh"] + self.aux_heat_exchange_unit_type = response["outputs"]["aux_heat_exchange_unit_type"] if kwargs.get("require_ghp_purchase"): self.require_ghp_purchase = 1 @@ -33,6 +42,10 @@ def __init__(self, dfm, response, **kwargs): self.installed_cost_building_hydronic_loop_us_dollars_per_sqft = kwargs.get("installed_cost_building_hydronic_loop_us_dollars_per_sqft") self.om_cost_us_dollars_per_sqft_year = kwargs.get("om_cost_us_dollars_per_sqft_year") self.building_sqft = kwargs.get("building_sqft") + self.aux_heater_type = kwargs.get("aux_heater_type") + self.aux_heater_installed_cost_us_dollars_per_mmbtu_per_hr = kwargs.get("aux_heater_installed_cost_us_dollars_per_mmbtu_per_hr") + self.aux_cooler_installed_cost_us_dollars_per_ton = kwargs.get("aux_cooler_installed_cost_us_dollars_per_ton") + self.aux_unit_capacity_sizing_factor_on_peak_load = kwargs.get("aux_unit_capacity_sizing_factor_on_peak_load") # Heating and cooling loads served and electricity consumed by GHP # TODO with hybrid with auxiliary/supplemental heating/cooling devices, we may want to separate out/distiguish that energy @@ -50,6 +63,17 @@ def __init__(self, dfm, response, **kwargs): self.setup_om_cost() + # TODO finish fuel/cost/emissions + # if self.aux_heater_type == "natural_gas": + # self.fuel_burn_series_mmbtu_per_hour = [self.yearly_aux_heater_thermal_production_series_mmbtu_per_hour[i] / self.aux_heater_thermal_efficiency + # for i in range(len(self.yearly_aux_heater_thermal_production_series_mmbtu_per_hour))] + # self.aux_heater_yearly_fuel_burn_mmbtu = sum(self.fuel_burn_series_mmbtu_per_hour) + # self.yearly_emissions_lb_CO2 = dfm.boiler.emissions_factor_lb_CO2_per_mmbtu * self.aux_heater_yearly_fuel_burn_mmbtu + # self.yearly_emissions_lb_NOx = dfm.boiler.emissions_factor_lb_NOx_per_mmbtu * self.aux_heater_yearly_fuel_burn_mmbtu + # self.yearly_emissions_lb_SOx = dfm.boiler.emissions_factor_lb_SOx_per_mmbtu * self.aux_heater_yearly_fuel_burn_mmbtu + # self.yearly_emissions_lb_PM25 = dfm.boiler.emissions_factor_lb_PM25_per_mmbtu * self.aux_heater_yearly_fuel_burn_mmbtu + + dfm.add_ghp(self) def setup_installed_cost_curve(self): @@ -61,14 +85,17 @@ def setup_installed_cost_curve(self): # The GHX and hydronic loop cost are the y-intercepts ([$]) of the cost for each design self.ghx_cost = self.total_ghx_ft * self.installed_cost_ghx_us_dollars_per_ft self.hydronic_loop_cost = self.building_sqft * self.installed_cost_building_hydronic_loop_us_dollars_per_sqft + self.aux_heater_cost = self.aux_heater_installed_cost_us_dollars_per_mmbtu_per_hr * self.peak_aux_heater_thermal_production_mmbtu_per_hour * self.aux_unit_capacity_sizing_factor_on_peak_load + self.aux_cooler_cost = self.aux_cooler_installed_cost_us_dollars_per_ton * self.peak_aux_cooler_thermal_production_ton * self.aux_unit_capacity_sizing_factor_on_peak_load # The DataManager._get_REopt_cost_curve method expects at least a two-point tech_size_for_cost_curve to # to use the first value of installed_cost_us_dollars_per_kw as an absolute $ value and # the initial slope is based on the heat pump size (e.g. $/ton) of the cost curve for # building a rebate-based cost curve if there are less-than big_number maximum incentives self.tech_size_for_cost_curve = [0.0, big_number] - self.installed_cost_us_dollars_per_kw = [self.ghx_cost + self.hydronic_loop_cost, - self.installed_cost_heatpump_us_dollars_per_ton] + self.installed_cost_us_dollars_per_kw = [self.ghx_cost + self.hydronic_loop_cost + + self.aux_heater_cost + self.aux_cooler_cost, + self.installed_cost_heatpump_us_dollars_per_ton] # Using a separate call to _get_REopt_cost_curve in data_manager for "ghp" (not included in "available_techs") # and then use the value below for heat pump capacity to calculate the final absolute cost for GHP diff --git a/reo/tests/posts/test_ghp_POST.json b/reo/tests/posts/test_ghp_POST.json index dfe2336cc..90b8f87ce 100644 --- a/reo/tests/posts/test_ghp_POST.json +++ b/reo/tests/posts/test_ghp_POST.json @@ -43,8 +43,11 @@ "building_sqft": 50000.0, "can_serve_dhw": false, "space_heating_efficiency_thermal_factor": 0.85, - "cooling_efficiency_thermal_factor": 0.6 - + "cooling_efficiency_thermal_factor": 0.6, + "aux_heater_type": "electric", + "aux_heater_installed_cost_us_dollars_per_mmbtu_per_hr": 0.0, + "aux_cooler_installed_cost_us_dollars_per_ton": 0.0, + "aux_unit_capacity_sizing_factor_on_peak_load": 1.3 }, "PV":{ "max_kw": 0.0 diff --git a/reo/tests/posts/test_ghp_hybrid_POST.json b/reo/tests/posts/test_ghp_hybrid_POST.json new file mode 100644 index 000000000..c5424a32e --- /dev/null +++ b/reo/tests/posts/test_ghp_hybrid_POST.json @@ -0,0 +1,56 @@ +{ + "Scenario": { + "timeout_seconds": 400, + "optimality_tolerance_techs": 0.001, + "Site": { + "latitude": 39.73951619, + "longitude": -104.9890941762, + "land_acres": null , + "roof_squarefeet": null , + "Financial": { + "om_cost_escalation_pct": 0.0, + "escalation_pct": 0.0, + "boiler_fuel_escalation_pct": 0.0, + "offtaker_tax_pct": 0.0 , + "offtaker_discount_pct": 0.08 , + "third_party_ownership": false, + "owner_tax_pct": 0.0 , + "owner_discount_pct": 0.08, + "analysis_years": 25 + } , + "LoadProfile": { + "doe_reference_name": "LargeOffice" + } , + "LoadProfileBoilerFuel": { + "doe_reference_name": "LargeOffice" + } , + "LoadProfileChillerThermal": { + "doe_reference_name": "LargeOffice" + } , + "ElectricTariff": { + "blended_annual_rates_us_dollars_per_kwh": 0.10, + "blended_annual_demand_charges_us_dollars_per_kw": 10.0 + } , + "FuelTariff": { + "existing_boiler_fuel_type": "natural_gas" , + "boiler_fuel_blended_annual_rates_us_dollars_per_mmbtu": 10.0 + } , + "GHP": { + "require_ghp_purchase": true, + "building_sqft": 498588.0, + "can_serve_dhw": false, + "aux_heater_type": "electric", + "aux_heater_installed_cost_us_dollars_per_mmbtu_per_hr": 0.0, + "aux_cooler_installed_cost_us_dollars_per_ton": 0.0, + "aux_unit_capacity_sizing_factor_on_peak_load": 1.2 + }, + "PV":{ + "max_kw": 0.0 + }, + "Storage":{ + "max_kw": 0.0, + "max_kwh": 0.0 + } + } + } +} \ No newline at end of file diff --git a/reo/tests/test_ghp_hybrid.py b/reo/tests/test_ghp_hybrid.py new file mode 100644 index 000000000..cc07cee58 --- /dev/null +++ b/reo/tests/test_ghp_hybrid.py @@ -0,0 +1,84 @@ +# ********************************************************************************* +# REopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# Redistributions of source code must retain the above copyright notice, this list +# of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. +# ********************************************************************************* +import json +import os +import pandas as pd +from tastypie.test import ResourceTestCaseMixin +from django.test import TestCase +from reo.models import ModelManager + +class GHPTest(ResourceTestCaseMixin, TestCase): + REopt_tol = 1e-2 + + def setUp(self): + super(GHPTest, self).setUp() + self.reopt_base = '/v1/job/' + self.test_reopt_post = os.path.join('reo', 'tests', 'posts', 'test_ghp_hybrid_POST.json') + self.test_ghpghx_post = os.path.join('ghpghx', 'tests', 'posts', 'test_hybrid_ghpghx_POST.json') + + def get_ghpghx_response(self, data): + return self.api_client.post(self.ghpghx_base, format='json', data=data) + + def get_reopt_response(self, data): + return self.api_client.post(self.reopt_base, format='json', data=data) + + def test_ghp(self): + """ + + This tests the automatic sizing functionality of hybrid GHP + + """ + nested_data = json.load(open(self.test_reopt_post, 'rb')) + ghpghx_post = json.load(open(self.test_ghpghx_post, 'rb')) + + nested_data["Scenario"]["Site"]["GHP"]["ghpghx_inputs"] = [ghpghx_post] + + # Call REopt + resp = self.get_reopt_response(data=nested_data) + self.assertHttpCreated(resp) + r = json.loads(resp.content) + run_uuid = r.get('run_uuid') + d = ModelManager.make_response(run_uuid=run_uuid) + + ghp_uuid = d["outputs"]["Scenario"]["Site"]["GHP"]["ghp_chosen_uuid"] + print("GHP uuid chosen = ", ghp_uuid) + + # Output number of boreholes and heatpump sizing + n_boreholes = d["outputs"]["Scenario"]["Site"]["GHP"]["ghpghx_chosen_outputs"]["number_of_boreholes"] + heatpump_tons = d["outputs"]["Scenario"]["Site"]["GHP"]["ghpghx_chosen_outputs"]["peak_combined_heatpump_thermal_ton"] + aux_cooler_annual_thermal_production_kwht = sum(d["outputs"]["Scenario"]["Site"]["GHP"]["ghpghx_chosen_outputs"]["yearly_aux_cooler_thermal_production_series_kwt"]) + aux_heater_annual_thermal_production_mmbtu = sum(d["outputs"]["Scenario"]["Site"]["GHP"]["ghpghx_chosen_outputs"]["yearly_aux_heater_thermal_production_series_mmbtu_per_hour"]) + + # Comparison to TESS exe range + # TODO: Number of boreholes vary between runs with the same input + # self.assertAlmostEqual(n_boreholes, 45) + self.assertAlmostEqual(heatpump_tons, 824.927) + self.assertAlmostEqual(aux_cooler_annual_thermal_production_kwht, 0.0) + self.assertGreater(aux_heater_annual_thermal_production_mmbtu, 480.0) diff --git a/reo/tests/test_ghp_job.py b/reo/tests/test_ghp_job.py index 05bba3414..17888316b 100644 --- a/reo/tests/test_ghp_job.py +++ b/reo/tests/test_ghp_job.py @@ -88,6 +88,11 @@ def test_ghp(self): ghp_uuid = d["outputs"]["Scenario"]["Site"]["GHP"]["ghp_chosen_uuid"] print("GHP uuid chosen = ", ghp_uuid) + # TODO add natural_gas option with fuel cost and emissions - FIX to "boiler" fuel cost/emissions + # Still need to confirm the first cut at passing boiler emissions from dfm in ghp works, but also + # need to create a separate yearly GHP emissions param for reopt_model and zero out when no GHP + # also need to link aux heater fuel cost with fuel_params and fuel_tariff to get pwf_fuel for LCC cost + # Test GHP serving addressable fraction of space heating with VAV efficiency thermal knockdown heating_served_mmbtu = sum(d["outputs"]["Scenario"]["Site"]["GHP"]["ghpghx_chosen_outputs"]["heating_thermal_load_mmbtu_per_hr"]) expected_heating_served_mmbtu = 12000 * 0.8 * 0.9 * 0.7 * 0.85 # (fuel_mmbtu * boiler_effic * addressable_load * space_heat_frac * space_heating_efficiency_thermal_factor)