From d8d18135fb9254934830441a5b3fcbb9b6b4f213 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 20 Mar 2025 09:44:26 -0600 Subject: [PATCH 01/36] add new inputs --- reoptjl/models.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/reoptjl/models.py b/reoptjl/models.py index 47d088f7a..c222d20f6 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -786,6 +786,24 @@ class FinancialInputs(BaseModel, models.Model): default=0.0, help_text=("Only applicable when off_grid_flag is true. These per year costs are considered tax deductible for owner.") ) + min_initial_capital_costs_before_incentives = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(1e12) + ], + blank=True, + null=True, + help_text=("Minimum up-front capital cost for all technologies, excluding replacement costs and incentives.") + ) + max_initial_capital_costs_before_incentives = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(1e12) + ], + blank=True, + null=True, + help_text=("Maximum up-front capital cost for all technologies, excluding replacement costs and incentives.") + ) CO2_cost_per_tonne = models.FloatField( validators=[ MinValueValidator(0), From bad79cc91faace2633cac4d09ef764e1a0c77e06 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 15 Apr 2025 15:46:08 -0600 Subject: [PATCH 02/36] changelog and all_inputs test --- CHANGELOG.md | 7 ++++++- reoptjl/test/posts/all_inputs_test.json | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d2b8ec87..73c856100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,9 +26,14 @@ Classify the change according to the following categories: ##### Removed ### Patches +## add-capex-constraint +### Minor Updates +#### Added +- Add **Financial** inputs **min_initial_capital_costs_before_incentives** and **max_initial_capital_costs_before_incentives** + ## v3.12.0 ### Major Updates -### Added +#### Added - Add inputs: - **ElectricUtility.cambium_cef_metric** to utilize clean energy data from NREL's Cambium database - **ElectricUtility.renewable_energy_fraction_series** to supply a custom grid clean or renewable energy scalar or series diff --git a/reoptjl/test/posts/all_inputs_test.json b/reoptjl/test/posts/all_inputs_test.json index 6811af43c..78fa2a4a2 100644 --- a/reoptjl/test/posts/all_inputs_test.json +++ b/reoptjl/test/posts/all_inputs_test.json @@ -26,7 +26,9 @@ "PM25_onsite_fuelburn_cost_per_tonne": null, "NOx_cost_escalation_rate_fraction": null, "SO2_cost_escalation_rate_fraction": null, - "PM25_cost_escalation_rate_fraction": null + "PM25_cost_escalation_rate_fraction": null, + "min_initial_capital_costs_before_incentives": null, + "max_initial_capital_costs_before_incentives": null }, "ElectricLoad": { "annual_kwh": 190000.0, From 18e58c5e6ed275827b681688120f80239bf46481 Mon Sep 17 00:00:00 2001 From: Kadlec Date: Tue, 22 Apr 2025 10:45:58 -0600 Subject: [PATCH 03/36] testing ghp inputs --- ...1_ghpinputs_load_served_by_ghp_and_more.py | 29 +++++++++++++++++++ reoptjl/models.py | 26 +++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 reoptjl/migrations/0081_ghpinputs_load_served_by_ghp_and_more.py diff --git a/reoptjl/migrations/0081_ghpinputs_load_served_by_ghp_and_more.py b/reoptjl/migrations/0081_ghpinputs_load_served_by_ghp_and_more.py new file mode 100644 index 000000000..d6a2f6a59 --- /dev/null +++ b/reoptjl/migrations/0081_ghpinputs_load_served_by_ghp_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0.7 on 2025-04-22 16:44 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0080_electricloadoutputs_annual_electric_load_with_thermal_conversions_kwh_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='ghpinputs', + name='load_served_by_ghp', + field=models.TextField(blank=True, default='nonpeak', help_text='How to split between load served by GHP and load served by backup system'), + ), + migrations.AddField( + model_name='ghpinputs', + name='max_number_of_boreholes', + field=models.FloatField(blank=True, help_text='Maximum number of boreholes for GHX', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AddField( + model_name='ghpinputs', + name='max_ton', + field=models.FloatField(blank=True, help_text='Maximum thermal power size constraint for GHP [ton]', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index 47d088f7a..a525c268e 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -8261,6 +8261,32 @@ class GHPInputs(BaseModel, models.Model): blank=True, help_text="Maximum utility rebate" ) + max_ton = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + help_text=("Maximum thermal power size constraint for GHP [ton]") + ) + + max_number_of_boreholes = models.FloatField( + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + null=True, + blank=True, + help_text=("Maximum number of boreholes for GHX") + ) + + load_served_by_ghp = models.TextField( + null=False, + blank=True, + default="nonpeak", + help_text="How to split between load served by GHP and load served by backup system" + ) def clean(self): From 796ea99f258cefe72a4cba204e4eaf22ed6b0512 Mon Sep 17 00:00:00 2001 From: Kadlec Date: Tue, 22 Apr 2025 13:20:22 -0600 Subject: [PATCH 04/36] debugging test --- reoptjl/testing.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 reoptjl/testing.txt diff --git a/reoptjl/testing.txt b/reoptjl/testing.txt new file mode 100644 index 000000000..e69de29bb From e3011e1571e390f2e621fdd2ee0dc037c592ad2f Mon Sep 17 00:00:00 2001 From: Kadlec Date: Wed, 23 Apr 2025 14:42:46 -0600 Subject: [PATCH 05/36] update manifest.toml --- julia_src/Manifest.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 6729da562..4d0f7a141 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -922,9 +922,11 @@ 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 = "24f902b6f67ed1d4389d21b5d42f820036b182b4" +git-tree-sha1 = "9fe6797d461a708e7b853a7236d835b076c516b7" +repo-rev = "allow_presized_GHP_GHX" +repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.51.0" +version = "0.51.1" [[deps.Random]] deps = ["SHA"] From f523baa051bef837202f800f9910bae931b355f8 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Wed, 23 Apr 2025 15:52:51 -0600 Subject: [PATCH 06/36] Merge migration after merging develop --- reoptjl/migrations/0084_merge_20250423_2110.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 reoptjl/migrations/0084_merge_20250423_2110.py diff --git a/reoptjl/migrations/0084_merge_20250423_2110.py b/reoptjl/migrations/0084_merge_20250423_2110.py new file mode 100644 index 000000000..8ea012a9b --- /dev/null +++ b/reoptjl/migrations/0084_merge_20250423_2110.py @@ -0,0 +1,14 @@ +# Generated by Django 4.0.7 on 2025-04-23 21:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0081_ghpinputs_load_served_by_ghp_and_more'), + ('reoptjl', '0083_electricutilityoutputs_peak_grid_demand_kw_and_more'), + ] + + operations = [ + ] From cc225e822f248c7bf9efb41c998de1f8ef78344d Mon Sep 17 00:00:00 2001 From: Kadlec Date: Thu, 24 Apr 2025 08:47:59 -0600 Subject: [PATCH 07/36] adding ghp outputs --- ...nnual_thermal_production_mmbtu_and_more.py | 23 +++++++++++++++++++ reoptjl/models.py | 2 ++ 2 files changed, 25 insertions(+) create mode 100644 reoptjl/migrations/0085_ghpoutputs_annual_thermal_production_mmbtu_and_more.py diff --git a/reoptjl/migrations/0085_ghpoutputs_annual_thermal_production_mmbtu_and_more.py b/reoptjl/migrations/0085_ghpoutputs_annual_thermal_production_mmbtu_and_more.py new file mode 100644 index 000000000..fe4a5e62b --- /dev/null +++ b/reoptjl/migrations/0085_ghpoutputs_annual_thermal_production_mmbtu_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.7 on 2025-04-24 14:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0084_merge_20250423_2110'), + ] + + operations = [ + migrations.AddField( + model_name='ghpoutputs', + name='annual_thermal_production_mmbtu', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='ghpoutputs', + name='annual_thermal_production_tonhour', + field=models.FloatField(blank=True, null=True), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index b46237980..ddc2c0363 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -8435,6 +8435,8 @@ class GHPOutputs(BaseModel, models.Model): thermal_to_dhw_load_series_mmbtu_per_hour = ArrayField(models.FloatField(null=True, blank=True), default=list, null=True, blank=True) thermal_to_load_series_ton = ArrayField(models.FloatField(null=True, blank=True), default=list, null=True, blank=True) avoided_capex_by_ghp_present_value = models.FloatField(null=True, blank=True) + annual_thermal_production_mmbtu = models.FloatField(null=True, blank=True) + annual_thermal_production_tonhour = models.FloatField(null=True, blank=True) def get_input_dict_from_run_uuid(run_uuid:str): """ From 54f1fbc689321d7d0efdedefc0f4a7096b10674d Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 7 May 2025 22:44:21 -0600 Subject: [PATCH 08/36] point to develop --- julia_src/Manifest.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index fc3e1581e..059884dd5 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -922,7 +922,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 = "9946abe774e30d82f786e68296ad1fdf8bb7dba4" +git-tree-sha1 = "1592ea6abdb85725cd5504f4808575caea89727c" +repo-rev = "develop" +repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" version = "0.51.1" From 43740b3f954be988cec55ec937eb446823484527 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 7 May 2025 22:50:07 -0600 Subject: [PATCH 09/36] Create 0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py --- ...apital_costs_before_incentives_and_more.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 reoptjl/migrations/0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py diff --git a/reoptjl/migrations/0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py b/reoptjl/migrations/0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py new file mode 100644 index 000000000..340656305 --- /dev/null +++ b/reoptjl/migrations/0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.7 on 2025-05-08 04:49 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0084_merge_20250424_1814'), + ] + + operations = [ + migrations.AddField( + model_name='financialinputs', + name='max_initial_capital_costs_before_incentives', + field=models.FloatField(blank=True, help_text='Maximum up-front capital cost for all technologies, excluding replacement costs and incentives.', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000000.0)]), + ), + migrations.AddField( + model_name='financialinputs', + name='min_initial_capital_costs_before_incentives', + field=models.FloatField(blank=True, help_text='Minimum up-front capital cost for all technologies, excluding replacement costs and incentives.', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000000.0)]), + ), + ] From 4dd75106f757610855abe6dd7d74d1fb57493165 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 8 May 2025 12:52:50 -0600 Subject: [PATCH 10/36] add chp output --- CHANGELOG.md | 1 + .../0086_chpoutputs_initial_capital_costs.py | 18 ++++++++++++++++++ reoptjl/models.py | 4 ++++ 3 files changed, 23 insertions(+) create mode 100644 reoptjl/migrations/0086_chpoutputs_initial_capital_costs.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2637342fd..497d194a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Classify the change according to the following categories: ### Minor Updates #### Added - Add **Financial** inputs **min_initial_capital_costs_before_incentives** and **max_initial_capital_costs_before_incentives** +- Add **CHP** output **initial_capital_costs** ## v3.12.3 ### Minor Updates diff --git a/reoptjl/migrations/0086_chpoutputs_initial_capital_costs.py b/reoptjl/migrations/0086_chpoutputs_initial_capital_costs.py new file mode 100644 index 000000000..5107d9a19 --- /dev/null +++ b/reoptjl/migrations/0086_chpoutputs_initial_capital_costs.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.7 on 2025-05-08 18:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0085_financialinputs_max_initial_capital_costs_before_incentives_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='chpoutputs', + name='initial_capital_costs', + field=models.FloatField(blank=True, help_text='Initial capital costs of the CHP system, before incentives [\\$]', null=True), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index 48a758e22..94ef91791 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -4746,6 +4746,10 @@ class CHPOutputs(BaseModel, models.Model): models.FloatField(null=True, blank=True), default = list, ) + initial_capital_costs = models.FloatField( + null=True, blank=True, + help_text="Initial capital costs of the CHP system, before incentives [\$]" + ) def clean(): pass From f66cab9c9b8ddd48d7446e17e0c778ea7f17733a Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 9 May 2025 15:31:46 -0600 Subject: [PATCH 11/36] update help text to include units for min, max capital costs --- ...ts_max_initial_capital_costs_before_incentives_and_more.py | 4 ++-- reoptjl/models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/reoptjl/migrations/0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py b/reoptjl/migrations/0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py index 340656305..5a02b6559 100644 --- a/reoptjl/migrations/0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py +++ b/reoptjl/migrations/0085_financialinputs_max_initial_capital_costs_before_incentives_and_more.py @@ -14,11 +14,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='financialinputs', name='max_initial_capital_costs_before_incentives', - field=models.FloatField(blank=True, help_text='Maximum up-front capital cost for all technologies, excluding replacement costs and incentives.', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000000.0)]), + field=models.FloatField(blank=True, help_text='Maximum up-front capital cost for all technologies, excluding replacement costs and incentives [\$].', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000000.0)]), ), migrations.AddField( model_name='financialinputs', name='min_initial_capital_costs_before_incentives', - field=models.FloatField(blank=True, help_text='Minimum up-front capital cost for all technologies, excluding replacement costs and incentives.', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000000.0)]), + field=models.FloatField(blank=True, help_text='Minimum up-front capital cost for all technologies, excluding replacement costs and incentives [\$].', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000000.0)]), ), ] diff --git a/reoptjl/models.py b/reoptjl/models.py index 94ef91791..184df44a4 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -793,7 +793,7 @@ class FinancialInputs(BaseModel, models.Model): ], blank=True, null=True, - help_text=("Minimum up-front capital cost for all technologies, excluding replacement costs and incentives.") + help_text=("Minimum up-front capital cost for all technologies, excluding replacement costs and incentives [\$].") ) max_initial_capital_costs_before_incentives = models.FloatField( validators=[ @@ -802,7 +802,7 @@ class FinancialInputs(BaseModel, models.Model): ], blank=True, null=True, - help_text=("Maximum up-front capital cost for all technologies, excluding replacement costs and incentives.") + help_text=("Maximum up-front capital cost for all technologies, excluding replacement costs and incentives [\$].") ) CO2_cost_per_tonne = models.FloatField( validators=[ From 5b1c315df90f600c69717f505cd7a63157d0a4b8 Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 9 May 2025 15:51:25 -0600 Subject: [PATCH 12/36] updt to REopt052 --- julia_src/Manifest.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 059884dd5..03d5186ee 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -922,11 +922,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 = "1592ea6abdb85725cd5504f4808575caea89727c" -repo-rev = "develop" -repo-url = "https://github.com/NREL/REopt.jl.git" +git-tree-sha1 = "0790dface6f78d2cadae8020470a4e73ee531f93" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.51.1" +version = "0.52.0" [[deps.Random]] deps = ["SHA"] From 40cfbfc48167fce208b19d4bada474146f5ba441 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Fri, 9 May 2025 16:47:06 -0600 Subject: [PATCH 13/36] Temp change to REopt#pv-size-classes --- julia_src/Manifest.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index fc3e1581e..ff56432ab 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -922,7 +922,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 = "9946abe774e30d82f786e68296ad1fdf8bb7dba4" +git-tree-sha1 = "75cb1f711cd53b61cd5030ac26eeeb66fd969c1e" +repo-rev = "pv-size-classes" +repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" version = "0.51.1" From 2d186e9578c18d64dfb611a44d633459cce72cd5 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Fri, 9 May 2025 16:48:12 -0600 Subject: [PATCH 14/36] Add and modify PV cost inputs and outputs to base on size_class logic --- ...voutputs_installed_cost_per_kw_and_more.py | 39 +++++++++++++++++++ reoptjl/models.py | 15 ++++++- 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 reoptjl/migrations/0085_pvinputs_size_class_pvoutputs_installed_cost_per_kw_and_more.py diff --git a/reoptjl/migrations/0085_pvinputs_size_class_pvoutputs_installed_cost_per_kw_and_more.py b/reoptjl/migrations/0085_pvinputs_size_class_pvoutputs_installed_cost_per_kw_and_more.py new file mode 100644 index 000000000..d8d18ecdc --- /dev/null +++ b/reoptjl/migrations/0085_pvinputs_size_class_pvoutputs_installed_cost_per_kw_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.0.7 on 2025-05-09 22:46 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0084_merge_20250424_1814'), + ] + + operations = [ + migrations.AddField( + model_name='pvinputs', + name='size_class', + field=models.IntegerField(blank=True, help_text='PV size class. Must be an integer value between 1 and 5. Default is 2, representing commercial-scale', null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]), + ), + migrations.AddField( + model_name='pvoutputs', + name='installed_cost_per_kw', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='pvoutputs', + name='om_cost_per_kw', + field=models.FloatField(blank=True, null=True), + ), + migrations.AlterField( + model_name='pvinputs', + name='installed_cost_per_kw', + field=models.FloatField(blank=True, help_text='Installed PV cost in $/kW', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000.0)]), + ), + migrations.AlterField( + model_name='pvinputs', + name='om_cost_per_kw', + field=models.FloatField(blank=True, help_text='Annual PV operations and maintenance costs in $/kW', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000.0)]), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index fdef159a8..c1181bafe 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -2725,21 +2725,30 @@ class PV_LOCATION_CHOICES(models.TextChoices): blank=True, help_text="Maximum PV size constraint for optimization (upper bound on additional capacity beyond existing_kw). Set to zero to disable PV" ) + size_class = models.IntegerField( + validators=[ + MinValueValidator(1), + MaxValueValidator(5) + ], + null=True, + blank=True, + help_text="PV size class. Must be an integer value between 1 and 5. Default is 2, representing commercial-scale" + ) installed_cost_per_kw = models.FloatField( - default=1790, validators=[ MinValueValidator(0), MaxValueValidator(1.0e5) ], + null=True, blank=True, help_text="Installed PV cost in $/kW" ) om_cost_per_kw = models.FloatField( - default=18, validators=[ MinValueValidator(0), MaxValueValidator(1.0e3) ], + null=True, blank=True, help_text="Annual PV operations and maintenance costs in $/kW" ) @@ -3067,6 +3076,8 @@ class PVOutputs(BaseModel, models.Model): help_text="PV description for distinguishing between multiple PV models" ) size_kw = models.FloatField(null=True, blank=True) + installed_cost_per_kw = models.FloatField(null=True, blank=True) + om_cost_per_kw = models.FloatField(null=True, blank=True) lifecycle_om_cost_after_tax = models.FloatField(null=True, blank=True) lifecycle_om_cost_after_tax_bau = models.FloatField(null=True, blank=True) lifecycle_om_cost_bau = models.FloatField(null=True, blank=True) From 31fe2d3d289dc653d0492ef424087d786ece25b0 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Fri, 9 May 2025 16:49:01 -0600 Subject: [PATCH 15/36] Add pv_cost_defaults endpoint --- julia_src/http.jl | 56 ++++++++++++++++++++++++++++++++++++++++++++++- reoptjl/urls.py | 1 + reoptjl/views.py | 38 ++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index ebbfe6ea2..d45d490e6 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -144,6 +144,14 @@ function reopt(req::HTTP.Request) else site_dict = Dict() end + if haskey(d, "PV") + inputs_with_defaults_from_pv = [ + :size_class, :installed_cost_per_kw, :om_cost_per_kw + ] + pv_dict = Dict(key=>getfield(model_inputs.s.pvs[1], key) for key in inputs_with_defaults_from_pv) + else + pv_dict = Dict() + end inputs_with_defaults_set_in_julia = Dict( "Financial" => Dict(key=>getfield(model_inputs.s.financial, key) for key in inputs_with_defaults_from_easiur), "ElectricUtility" => Dict(key=>getfield(model_inputs.s.electric_utility, key) for key in inputs_with_defaults_from_avert_or_cambium), @@ -153,7 +161,8 @@ function reopt(req::HTTP.Request) "GHP" => ghp_dict, "ExistingChiller" => chiller_dict, "ASHPSpaceHeater" => ashp_dict, - "ASHPWaterHeater" => ashp_wh_dict + "ASHPWaterHeater" => ashp_wh_dict, + "PV" => pv_dict ) catch e @error "Something went wrong in REopt optimization!" exception=(e, catch_backtrace()) @@ -579,6 +588,50 @@ function get_ashp_defaults(req::HTTP.Request) end end +function pv_cost_defaults(req::HTTP.Request) + d = JSON.parse(String(req.body)) + float_vals = ["electric_load_annual_kwh", "site_land_acres", + "site_roof_squarefeet", "min_kw", "max_kw", + "kw_per_square_foot", "acres_per_kw"] + int_vals = ["size_class", "array_type"] + string_vals = ["location"] + bool_vals = [] + all_vals = vcat(int_vals, string_vals, float_vals, bool_vals) + # Process .json inputs and convert to correct type if needed + for k in all_vals + if !isnothing(get(d, k, nothing)) + # TODO improve this by checking if the type is not the expected type, as opposed to just not string + if k in float_vals && typeof(d[k]) == String + d[k] = parse(Float64, d[k]) + elseif k in int_vals && typeof(d[k]) == String + d[k] = parse(Int64, d[k]) + elseif k in bool_vals && typeof(d[k]) == String + d[k] = parse(Bool, d[k]) + end + end + end + + @info "Getting PV cost defaults..." + data = Dict() + error_response = Dict() + try + data["installed_cost_per_kw"], data["om_cost_per_kw"], data["size_class"], tech_sizes_for_cost_curve = reoptjl.get_pv_cost_params(; + (Symbol(k) => v for (k, v) in pairs(d))... + ) + catch e + @error "Something went wrong in the pv_cost_defaults" exception=(e, catch_backtrace()) + error_response["error"] = sprint(showerror, e) + end + if isempty(error_response) + @info "PV cost defaults determined." + response = data + return HTTP.Response(200, JSON.json(response)) + else + @info "An error occured in the pv_cost_defaults endpoint" + return HTTP.Response(500, JSON.json(error_response)) + end +end + function job_no_xpress(req::HTTP.Request) error_response = Dict("error" => "V1 and V2 not available without Xpress installation.") @@ -607,4 +660,5 @@ HTTP.register!(ROUTER, "GET", "/ground_conductivity", ground_conductivity) HTTP.register!(ROUTER, "GET", "/health", health) HTTP.register!(ROUTER, "GET", "/get_existing_chiller_default_cop", get_existing_chiller_default_cop) HTTP.register!(ROUTER, "GET", "/get_ashp_defaults", get_ashp_defaults) +HTTP.register!(ROUTER, "GET", "/pv_cost_defaults", pv_cost_defaults) HTTP.serve(ROUTER, "0.0.0.0", 8081, reuseaddr=true) diff --git a/reoptjl/urls.py b/reoptjl/urls.py index 89617a7cf..73685f76c 100644 --- a/reoptjl/urls.py +++ b/reoptjl/urls.py @@ -25,6 +25,7 @@ re_path(r'^get_existing_chiller_default_cop/?$', views.get_existing_chiller_default_cop), re_path(r'^job/generate_results_table/?$', views.generate_results_table), re_path(r'^get_ashp_defaults/?$', views.get_ashp_defaults), + re_path(r'^pv_cost_defaults/?$', views.pv_cost_defaults), re_path(r'^summary_by_runuuids/?$', views.summary_by_runuuids), re_path(r'^link_run_to_portfolios/?$', views.link_run_uuids_to_portfolio_uuid) ] diff --git a/reoptjl/views.py b/reoptjl/views.py index b8b8901fb..c847398e3 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -514,6 +514,44 @@ def get_ashp_defaults(request): log.debug(debug_msg) return JsonResponse({"Error": "Unexpected error in get_ashp_defaults endpoint. Check log for more."}, status=500) +def pv_cost_defaults(request): + inputs = { + "electric_load_annual_kwh": request.GET.get("electric_load_annual_kwh"), + "site_land_acres": request.GET.get("site_land_acres"), + "site_roof_squarefeet" : request.GET.get("site_roof_squarefeet"), + "min_kw": request.GET.get("min_kw"), + "max_kw": request.GET.get("max_kw"), + "kw_per_square_foot": request.GET.get("kw_per_square_foot"), + "acres_per_kw": request.GET.get("acres_per_kw"), + "size_class": request.GET.get("size_class"), + "array_type": request.GET.get("array_type"), + "location": request.GET.get("location") + } + + inputs = {k: v for k, v in inputs.items() if v is not None} + + print(inputs) + try: + julia_host = os.environ.get('JULIA_HOST', "julia") + http_jl_response = requests.get("http://" + julia_host + ":8081/pv_cost_defaults/", json=inputs) + response = JsonResponse( + http_jl_response.json() + ) + return response + + except ValueError as e: + print(e.args) + return JsonResponse({"Error": str(e.args[0])}, status=500) + + except KeyError as e: + return JsonResponse({"Error. Missing": str(e.args[0])}, status=500) + + except Exception: + exc_type, exc_value, exc_traceback = sys.exc_info() + debug_msg = "exc_type: {}; exc_value: {}; exc_traceback: {}".format(exc_type, exc_value.args[0], + tb.format_tb(exc_traceback)) + log.debug(debug_msg) + return JsonResponse({"Error": "Unexpected error in pv_cost_defaults endpoint. Check log for more."}, status=500) def simulated_load(request): try: From aa805d374c41044a87a64bc02ba3dde745a1bc8f Mon Sep 17 00:00:00 2001 From: bill-becker Date: Fri, 9 May 2025 16:49:44 -0600 Subject: [PATCH 16/36] Update PVInputs db instance from REopt.jl-processed data based on size_class --- reoptjl/src/process_results.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/reoptjl/src/process_results.py b/reoptjl/src/process_results.py index 5b5732d69..eee8c1942 100644 --- a/reoptjl/src/process_results.py +++ b/reoptjl/src/process_results.py @@ -8,7 +8,7 @@ REoptjlMessageOutputs, AbsorptionChillerOutputs, BoilerOutputs, SteamTurbineInputs, \ SteamTurbineOutputs, GHPInputs, GHPOutputs, ExistingChillerInputs, \ ElectricHeaterOutputs, ASHPSpaceHeaterOutputs, ASHPWaterHeaterOutputs, \ - SiteInputs, ASHPSpaceHeaterInputs, ASHPWaterHeaterInputs + SiteInputs, ASHPSpaceHeaterInputs, ASHPWaterHeaterInputs, PVInputs import numpy as np import sys import traceback as tb @@ -146,6 +146,9 @@ def update_inputs_in_database(inputs_to_update: dict, run_uuid: str) -> None: if inputs_to_update["ASHPWaterHeater"]: prune_update_fields(ASHPWaterHeaterInputs, inputs_to_update["ASHPWaterHeater"]) ASHPWaterHeaterInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ASHPWaterHeater"]) + if inputs_to_update["PV"]: + prune_update_fields(PVInputs, inputs_to_update["PV"]) + PVInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["PV"]) except Exception as e: exc_type, exc_value, exc_traceback = sys.exc_info() debug_msg = "exc_type: {}; exc_value: {}; exc_traceback: {}".format( From fe4bda721c44c936287aaf886aae7052ea4e223b Mon Sep 17 00:00:00 2001 From: bill-becker Date: Tue, 13 May 2025 12:37:08 -0600 Subject: [PATCH 17/36] Add tests for pv_cost_defaults endpoint and updating from Julia --- reoptjl/test/test_http_endpoints.py | 13 +++++++++++++ reoptjl/test/test_job_endpoint.py | 17 ++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/reoptjl/test/test_http_endpoints.py b/reoptjl/test/test_http_endpoints.py index 8b0e25ad6..8104a6197 100644 --- a/reoptjl/test/test_http_endpoints.py +++ b/reoptjl/test/test_http_endpoints.py @@ -404,4 +404,17 @@ def test_get_ashp_defaults(self): self.assertNotIn("cooling_cf_reference", view_response.keys()) self.assertEqual(view_response["sizing_factor"], 1.0) + def test_pv_cost_defaults(self): + inputs_dict = { + "electric_load_annual_kwh": 2000000.0, + "array_type": 1, + "site_roof_squarefeet": 50000 + } + + # Call to the django view endpoint /get_existing_chiller_default_cop which calls the http.jl endpoint + resp = self.api_client.get(f'/v3/pv_cost_defaults', data=inputs_dict) + view_response = json.loads(resp.content) + + self.assertEqual(view_response["size_class"], 3) + diff --git a/reoptjl/test/test_job_endpoint.py b/reoptjl/test/test_job_endpoint.py index 5dfe67391..97502460d 100644 --- a/reoptjl/test/test_job_endpoint.py +++ b/reoptjl/test/test_job_endpoint.py @@ -326,4 +326,19 @@ def test_ashp_defaults_update_from_julia(self): r = json.loads(resp.content) self.assertEquals(r["inputs"]["ASHPSpaceHeater"]["om_cost_per_ton"], 0.0) - self.assertEquals(r["inputs"]["ASHPSpaceHeater"]["sizing_factor"], 1.1) \ No newline at end of file + self.assertEquals(r["inputs"]["ASHPSpaceHeater"]["sizing_factor"], 1.1) + + def test_pv_cost_defaults_update_from_julia(self): + # Test that the inputs_with_defaults_set_in_julia feature worked for PV + post_file = os.path.join('reoptjl', 'test', 'posts', 'pv_cost_update.json') + post = json.load(open(post_file, 'r')) + resp = self.api_client.post('/stable/job/', format='json', data=post) + self.assertHttpCreated(resp) + r = json.loads(resp.content) + run_uuid = r.get('run_uuid') + + resp = self.api_client.get(f'/stable/job/{run_uuid}/results') + r = json.loads(resp.content) + + self.assertEquals(r["inputs"]["PV"]["size_class"], 2) + self.assertAlmostEqual(r["inputs"]["PV"]["installed_cost_per_kw"], 2914.6, delta=0.05 * 2914.6) \ No newline at end of file From 3c80282f807ee1404e611573963982b6e7604147 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Tue, 13 May 2025 12:37:50 -0600 Subject: [PATCH 18/36] Add post file for pv_cost_defaults updating from Julia test --- reoptjl/test/posts/pv_cost_update.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 reoptjl/test/posts/pv_cost_update.json diff --git a/reoptjl/test/posts/pv_cost_update.json b/reoptjl/test/posts/pv_cost_update.json new file mode 100644 index 000000000..a82875ad6 --- /dev/null +++ b/reoptjl/test/posts/pv_cost_update.json @@ -0,0 +1,18 @@ +{ + "Site": { + "latitude": 41.8809434, + "longitude": -72.2600655, + "land_acres": 0.5 + }, + "ElectricLoad": { + "annual_kwh": 2000000.0, + "doe_reference_name": "LargeOffice" + }, + "ElectricTariff": { + "blended_annual_demand_rate": 20.0, + "blended_annual_energy_rate": 0.12 + }, + "PV": { + "array_type": 0 + } +} \ No newline at end of file From 645ccb1c3768c095dcb8c15b5e0dadcac34d7ab1 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Wed, 14 May 2025 22:17:38 -0600 Subject: [PATCH 19/36] Add POSTability, size output, and new sizing inputs for pv_cost_defaults endpoint --- julia_src/Manifest.toml | 2 +- julia_src/http.jl | 5 +++-- reoptjl/views.py | 30 ++++++++++++++++++------------ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index ff56432ab..68332f50c 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -922,7 +922,7 @@ 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 = "75cb1f711cd53b61cd5030ac26eeeb66fd969c1e" +git-tree-sha1 = "3e9bc2a692b1b101d8637dfcd61da75686d269a2" repo-rev = "pv-size-classes" repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" diff --git a/julia_src/http.jl b/julia_src/http.jl index d45d490e6..cd12baa0e 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -592,7 +592,8 @@ function pv_cost_defaults(req::HTTP.Request) d = JSON.parse(String(req.body)) float_vals = ["electric_load_annual_kwh", "site_land_acres", "site_roof_squarefeet", "min_kw", "max_kw", - "kw_per_square_foot", "acres_per_kw"] + "kw_per_square_foot", "acres_per_kw", + "capacity_factor_estimate", "fraction_of_annual_kwh_to_size_pv"] int_vals = ["size_class", "array_type"] string_vals = ["location"] bool_vals = [] @@ -615,7 +616,7 @@ function pv_cost_defaults(req::HTTP.Request) data = Dict() error_response = Dict() try - data["installed_cost_per_kw"], data["om_cost_per_kw"], data["size_class"], tech_sizes_for_cost_curve = reoptjl.get_pv_cost_params(; + data["installed_cost_per_kw"], data["om_cost_per_kw"], data["size_class"], tech_sizes_for_cost_curve, data["size_kw_for_size_class"] = reoptjl.get_pv_cost_params(; (Symbol(k) => v for (k, v) in pairs(d))... ) catch e diff --git a/reoptjl/views.py b/reoptjl/views.py index c847398e3..39443415f 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -515,18 +515,24 @@ def get_ashp_defaults(request): return JsonResponse({"Error": "Unexpected error in get_ashp_defaults endpoint. Check log for more."}, status=500) def pv_cost_defaults(request): - inputs = { - "electric_load_annual_kwh": request.GET.get("electric_load_annual_kwh"), - "site_land_acres": request.GET.get("site_land_acres"), - "site_roof_squarefeet" : request.GET.get("site_roof_squarefeet"), - "min_kw": request.GET.get("min_kw"), - "max_kw": request.GET.get("max_kw"), - "kw_per_square_foot": request.GET.get("kw_per_square_foot"), - "acres_per_kw": request.GET.get("acres_per_kw"), - "size_class": request.GET.get("size_class"), - "array_type": request.GET.get("array_type"), - "location": request.GET.get("location") - } + + if request.method == "POST": + inputs = json.loads(request.body) + else: + inputs = { + "electric_load_annual_kwh": request.GET.get("electric_load_annual_kwh"), + "site_land_acres": request.GET.get("site_land_acres"), + "site_roof_squarefeet" : request.GET.get("site_roof_squarefeet"), + "min_kw": request.GET.get("min_kw"), + "max_kw": request.GET.get("max_kw"), + "kw_per_square_foot": request.GET.get("kw_per_square_foot"), + "acres_per_kw": request.GET.get("acres_per_kw"), + "size_class": request.GET.get("size_class"), + "array_type": request.GET.get("array_type"), + "location": request.GET.get("location"), + "capacity_factor_estimate": request.GET.get("capacity_factor_estimate"), + "fraction_of_annual_kwh_to_size_pv": request.GET.get("fraction_of_annual_kwh_to_size_pv") + } inputs = {k: v for k, v in inputs.items() if v is not None} From e989a286ea2ac296daabae3f903073167503ecd4 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Mon, 19 May 2025 15:03:46 -0600 Subject: [PATCH 20/36] Add battery cost constant terms and use branch of REopt.jl --- julia_src/Manifest.toml | 4 ++- ...cost_constant_replacement_year_and_more.py | 29 +++++++++++++++++++ reoptjl/models.py | 27 +++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 reoptjl/migrations/0085_electricstorageinputs_cost_constant_replacement_year_and_more.py diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index fc3e1581e..ed3d95fb6 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -922,7 +922,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 = "9946abe774e30d82f786e68296ad1fdf8bb7dba4" +git-tree-sha1 = "6615ec48a40b3098a72a76cfe0c46095fc015ec6" +repo-rev = "storage-cost-constraints-version2" +repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" version = "0.51.1" diff --git a/reoptjl/migrations/0085_electricstorageinputs_cost_constant_replacement_year_and_more.py b/reoptjl/migrations/0085_electricstorageinputs_cost_constant_replacement_year_and_more.py new file mode 100644 index 000000000..e7ef9d064 --- /dev/null +++ b/reoptjl/migrations/0085_electricstorageinputs_cost_constant_replacement_year_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0.7 on 2025-05-19 20:33 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0084_merge_20250424_1814'), + ] + + operations = [ + migrations.AddField( + model_name='electricstorageinputs', + name='cost_constant_replacement_year', + field=models.IntegerField(blank=True, default=10, help_text='Number of years from start of analysis period to apply replace_cost_constant.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(75)]), + ), + migrations.AddField( + model_name='electricstorageinputs', + name='installed_cost_constant', + field=models.FloatField(blank=True, default=0.0, help_text='Fixed upfront cost for battery installation, independent of size.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000.0)]), + ), + migrations.AddField( + model_name='electricstorageinputs', + name='replace_cost_constant', + field=models.FloatField(blank=True, default=0.0, help_text='Fixed replacement cost for battery, independent of size.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000.0)]), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index fdef159a8..00ca226bd 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -3532,6 +3532,15 @@ class ElectricStorageInputs(BaseModel, models.Model): blank=True, help_text="Total upfront battery costs" ) + installed_cost_constant = models.FloatField( + default=0.0, + validators=[ + MinValueValidator(0), + MaxValueValidator(1.0e9) + ], + blank=True, + help_text="Fixed upfront cost for battery installation, independent of size." + ) replace_cost_per_kw = models.FloatField( default=715.0, validators=[ @@ -3550,6 +3559,15 @@ class ElectricStorageInputs(BaseModel, models.Model): blank=True, help_text="Battery energy capacity replacement cost at time of replacement year" ) + replace_cost_constant = models.FloatField( + default=0.0, + validators=[ + MinValueValidator(0), + MaxValueValidator(1.0e9) + ], + blank=True, + help_text="Fixed replacement cost for battery, independent of size." + ) inverter_replacement_year = models.IntegerField( default=10, validators=[ @@ -3568,6 +3586,15 @@ class ElectricStorageInputs(BaseModel, models.Model): blank=True, help_text="Number of years from start of analysis period to replace battery" ) + cost_constant_replacement_year = models.IntegerField( + default=10, + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_YEARS) + ], + blank=True, + help_text="Number of years from start of analysis period to apply replace_cost_constant." + ) macrs_option_years = models.IntegerField( default=MACRS_YEARS_CHOICES.SEVEN, choices=MACRS_YEARS_CHOICES.choices, From 3a8a85f689b2f819921a53f7754886d84efb31a5 Mon Sep 17 00:00:00 2001 From: Kadlec Date: Wed, 21 May 2025 09:08:28 -0600 Subject: [PATCH 21/36] updating REopt.jl branch to develop --- julia_src/Manifest.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 4d0f7a141..3a3ad23cb 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -922,11 +922,11 @@ 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 = "9fe6797d461a708e7b853a7236d835b076c516b7" -repo-rev = "allow_presized_GHP_GHX" +git-tree-sha1 = "37371dea02a25459d46411025a82cad3a3229a3c" +repo-rev = "develop" repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.51.1" +version = "0.52.0" [[deps.Random]] deps = ["SHA"] From 47c267933f5a9a902f326c359f0ef9d6e89e355e Mon Sep 17 00:00:00 2001 From: bill-becker Date: Tue, 3 Jun 2025 21:38:44 -0600 Subject: [PATCH 22/36] Update defaults and add O&M term for ElectricStorage --- ...ost_fraction_of_installed_cost_and_more.py | 44 +++++++++++++++++++ reoptjl/models.py | 19 +++++--- 2 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 reoptjl/migrations/0086_electricstorageinputs_om_cost_fraction_of_installed_cost_and_more.py diff --git a/reoptjl/migrations/0086_electricstorageinputs_om_cost_fraction_of_installed_cost_and_more.py b/reoptjl/migrations/0086_electricstorageinputs_om_cost_fraction_of_installed_cost_and_more.py new file mode 100644 index 000000000..22718b0c6 --- /dev/null +++ b/reoptjl/migrations/0086_electricstorageinputs_om_cost_fraction_of_installed_cost_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 4.0.7 on 2025-06-04 00:05 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0085_electricstorageinputs_cost_constant_replacement_year_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='electricstorageinputs', + name='om_cost_fraction_of_installed_cost', + field=models.FloatField(blank=True, default=0.025, help_text='Annual O&M cost as a fraction of installed cost.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1.0)]), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='installed_cost_constant', + field=models.FloatField(blank=True, default=222115.0, help_text='Fixed upfront cost for battery installation, independent of size.', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000000000.0)]), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='installed_cost_per_kw', + field=models.FloatField(blank=True, default=905.0, help_text='Total upfront battery power capacity costs (e.g. inverter and balance of power systems)', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10000.0)]), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='installed_cost_per_kwh', + field=models.FloatField(blank=True, default=237.0, help_text='Total upfront battery costs', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10000.0)]), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='replace_cost_per_kw', + field=models.FloatField(blank=True, default=0.0, help_text='Battery power capacity replacement cost at time of replacement year', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10000.0)]), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='replace_cost_per_kwh', + field=models.FloatField(blank=True, default=0.0, help_text='Battery energy capacity replacement cost at time of replacement year', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10000.0)]), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index 00ca226bd..a1df0396a 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -3515,7 +3515,7 @@ class ElectricStorageInputs(BaseModel, models.Model): help_text="Flag to set whether the battery can be charged from the grid, or just onsite generation." ) installed_cost_per_kw = models.FloatField( - default=910.0, + default=905.0, validators=[ MinValueValidator(0), MaxValueValidator(1.0e4) @@ -3524,7 +3524,7 @@ class ElectricStorageInputs(BaseModel, models.Model): help_text="Total upfront battery power capacity costs (e.g. inverter and balance of power systems)" ) installed_cost_per_kwh = models.FloatField( - default=455.0, + default=237.0, validators=[ MinValueValidator(0), MaxValueValidator(1.0e4) @@ -3533,7 +3533,7 @@ class ElectricStorageInputs(BaseModel, models.Model): help_text="Total upfront battery costs" ) installed_cost_constant = models.FloatField( - default=0.0, + default=222115.0, validators=[ MinValueValidator(0), MaxValueValidator(1.0e9) @@ -3542,7 +3542,7 @@ class ElectricStorageInputs(BaseModel, models.Model): help_text="Fixed upfront cost for battery installation, independent of size." ) replace_cost_per_kw = models.FloatField( - default=715.0, + default=0.0, validators=[ MinValueValidator(0), MaxValueValidator(1.0e4) @@ -3551,7 +3551,7 @@ class ElectricStorageInputs(BaseModel, models.Model): help_text="Battery power capacity replacement cost at time of replacement year" ) replace_cost_per_kwh = models.FloatField( - default=318.0, + default=0.0, validators=[ MinValueValidator(0), MaxValueValidator(1.0e4) @@ -3595,6 +3595,15 @@ class ElectricStorageInputs(BaseModel, models.Model): blank=True, help_text="Number of years from start of analysis period to apply replace_cost_constant." ) + om_cost_fraction_of_installed_cost = models.FloatField( + default=0.025, + validators=[ + MinValueValidator(0), + MaxValueValidator(1.0) + ], + blank=True, + help_text="Annual O&M cost as a fraction of installed cost." + ) macrs_option_years = models.IntegerField( default=MACRS_YEARS_CHOICES.SEVEN, choices=MACRS_YEARS_CHOICES.choices, From 92796f3cb79f22624ab84425bc90f1c8e0da4343 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Tue, 3 Jun 2025 21:39:09 -0600 Subject: [PATCH 23/36] Temp update to branch of REopt.jl with storage constant and O&M --- julia_src/Manifest.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index ed3d95fb6..a4ab4c461 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -922,11 +922,11 @@ 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 = "6615ec48a40b3098a72a76cfe0c46095fc015ec6" +git-tree-sha1 = "dc4aef468cfb3b14e105e3f9c7874fdad01f2e95" repo-rev = "storage-cost-constraints-version2" repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.51.1" +version = "0.52.0" [[deps.Random]] deps = ["SHA"] From 730ed25f79d1efc96d6eea1257c4f648b2773580 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Tue, 3 Jun 2025 21:39:40 -0600 Subject: [PATCH 24/36] Add new ElectricStorage inputs to all_inputs_test.json --- reoptjl/test/posts/all_inputs_test.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/reoptjl/test/posts/all_inputs_test.json b/reoptjl/test/posts/all_inputs_test.json index 6811af43c..e99d72390 100644 --- a/reoptjl/test/posts/all_inputs_test.json +++ b/reoptjl/test/posts/all_inputs_test.json @@ -167,10 +167,13 @@ "can_grid_charge": true, "installed_cost_per_kw": 840.0, "installed_cost_per_kwh": 420.0, + "installed_cost_constant": 20000.0, "replace_cost_per_kw": 410.0, "replace_cost_per_kwh": 200.0, + "replace_cost_constant": 0.0, "inverter_replacement_year": 10, "battery_replacement_year": 10, + "cost_constant_replacement_year": 10, "macrs_option_years": 7, "macrs_bonus_fraction": 1.0, "macrs_itc_reduction": 0.5, From 3c950d73f515ab2dcfe0cef330872cba2601b2d4 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Tue, 3 Jun 2025 22:02:24 -0600 Subject: [PATCH 25/36] Merge migrations after merging add-capex-constraint --- reoptjl/migrations/0087_merge_20250604_0342.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 reoptjl/migrations/0087_merge_20250604_0342.py diff --git a/reoptjl/migrations/0087_merge_20250604_0342.py b/reoptjl/migrations/0087_merge_20250604_0342.py new file mode 100644 index 000000000..edafd4e60 --- /dev/null +++ b/reoptjl/migrations/0087_merge_20250604_0342.py @@ -0,0 +1,14 @@ +# Generated by Django 4.0.7 on 2025-06-04 03:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0086_chpoutputs_initial_capital_costs'), + ('reoptjl', '0086_electricstorageinputs_om_cost_fraction_of_installed_cost_and_more'), + ] + + operations = [ + ] From 5cbf872d0c25d9389ee62376fe52f2d4f2bdb1e7 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Tue, 3 Jun 2025 22:03:18 -0600 Subject: [PATCH 26/36] Add new BAU CapEx outputs fields to models --- ...tal_costs_after_incentives_bau_and_more.py | 23 +++++++++++++++++++ reoptjl/models.py | 8 +++++++ 2 files changed, 31 insertions(+) create mode 100644 reoptjl/migrations/0088_financialoutputs_initial_capital_costs_after_incentives_bau_and_more.py diff --git a/reoptjl/migrations/0088_financialoutputs_initial_capital_costs_after_incentives_bau_and_more.py b/reoptjl/migrations/0088_financialoutputs_initial_capital_costs_after_incentives_bau_and_more.py new file mode 100644 index 000000000..65c442ae1 --- /dev/null +++ b/reoptjl/migrations/0088_financialoutputs_initial_capital_costs_after_incentives_bau_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.7 on 2025-06-04 04:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0087_merge_20250604_0342'), + ] + + operations = [ + migrations.AddField( + model_name='financialoutputs', + name='initial_capital_costs_after_incentives_bau', + field=models.FloatField(blank=True, help_text='Up-front capital costs for BAU technologies such as ExistingBoiler and ExistingChiller, in present value.', null=True), + ), + migrations.AddField( + model_name='financialoutputs', + name='lifecycle_capital_costs_bau', + field=models.FloatField(blank=True, help_text='Net capital costs for BAU technologies such as ExistingBoiler and ExistingChiller, in present value.', null=True), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index 0e31467ce..caf75b7b4 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -982,6 +982,10 @@ class FinancialOutputs(BaseModel, models.Model): null=True, blank=True, help_text="Net capital costs for all technologies, in present value, including replacement costs and incentives." ) + lifecycle_capital_costs_bau = models.FloatField( + null=True, blank=True, + help_text="Net capital costs for BAU technologies such as ExistingBoiler and ExistingChiller, in present value." + ) microgrid_upgrade_cost = models.FloatField( null=True, blank=True, help_text=("Cost to make a distributed energy system islandable from the grid. Determined by multiplying the " @@ -996,6 +1000,10 @@ class FinancialOutputs(BaseModel, models.Model): null=True, blank=True, help_text="Up-front capital costs for all technologies, in present value, excluding replacement costs, including incentives." ) + initial_capital_costs_after_incentives_bau = models.FloatField( + null=True, blank=True, + help_text="Up-front capital costs for BAU technologies such as ExistingBoiler and ExistingChiller, in present value." + ) capital_costs_after_non_discounted_incentives_without_macrs = models.FloatField( null=True, blank=True, help_text="Capital costs for all technologies, including present value of replacement costs and incentives except for MACRS." From ddb81dc613260b8df4442eec6e40148790618d83 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Wed, 4 Jun 2025 13:02:38 -0600 Subject: [PATCH 27/36] Update default ElectricStorage costs to be in 2024 dollars --- ...geinputs_installed_cost_per_kw_and_more.py | 24 +++++++++++++++++++ reoptjl/models.py | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 reoptjl/migrations/0089_alter_electricstorageinputs_installed_cost_per_kw_and_more.py diff --git a/reoptjl/migrations/0089_alter_electricstorageinputs_installed_cost_per_kw_and_more.py b/reoptjl/migrations/0089_alter_electricstorageinputs_installed_cost_per_kw_and_more.py new file mode 100644 index 000000000..54f0e618c --- /dev/null +++ b/reoptjl/migrations/0089_alter_electricstorageinputs_installed_cost_per_kw_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.7 on 2025-06-04 16:59 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0088_financialoutputs_initial_capital_costs_after_incentives_bau_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='electricstorageinputs', + name='installed_cost_per_kw', + field=models.FloatField(blank=True, default=968.0, help_text='Total upfront battery power capacity costs (e.g. inverter and balance of power systems)', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10000.0)]), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='installed_cost_per_kwh', + field=models.FloatField(blank=True, default=253.0, help_text='Total upfront battery costs', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10000.0)]), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index caf75b7b4..5b02a19b2 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -3541,7 +3541,7 @@ class ElectricStorageInputs(BaseModel, models.Model): help_text="Flag to set whether the battery can be charged from the grid, or just onsite generation." ) installed_cost_per_kw = models.FloatField( - default=905.0, + default=968.0, validators=[ MinValueValidator(0), MaxValueValidator(1.0e4) @@ -3550,7 +3550,7 @@ class ElectricStorageInputs(BaseModel, models.Model): help_text="Total upfront battery power capacity costs (e.g. inverter and balance of power systems)" ) installed_cost_per_kwh = models.FloatField( - default=237.0, + default=253.0, validators=[ MinValueValidator(0), MaxValueValidator(1.0e4) From 264b44ba4eea07fd39d94d914d4c60c6945f57ad Mon Sep 17 00:00:00 2001 From: bill-becker Date: Wed, 4 Jun 2025 14:20:56 -0600 Subject: [PATCH 28/36] Add new Existing Boiler/Chiller inputs and outputs --- ...rinputs_installed_cost_dollars_and_more.py | 49 +++++++++++++++++++ reoptjl/models.py | 49 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 reoptjl/migrations/0090_existingboilerinputs_installed_cost_dollars_and_more.py diff --git a/reoptjl/migrations/0090_existingboilerinputs_installed_cost_dollars_and_more.py b/reoptjl/migrations/0090_existingboilerinputs_installed_cost_dollars_and_more.py new file mode 100644 index 000000000..8d6357e79 --- /dev/null +++ b/reoptjl/migrations/0090_existingboilerinputs_installed_cost_dollars_and_more.py @@ -0,0 +1,49 @@ +# Generated by Django 4.0.7 on 2025-06-04 19:28 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0089_alter_electricstorageinputs_installed_cost_per_kw_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='existingboilerinputs', + name='installed_cost_dollars', + field=models.FloatField(blank=True, default=0.0, help_text='Cost incurred in BAU scenario, as well as Optimal if needed still, in dollars', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AddField( + model_name='existingboilerinputs', + name='installed_cost_per_mmbtu_per_hour', + field=models.FloatField(blank=True, default=0.0, help_text="Thermal power capacity-based cost incurred in BAU and only based on what's needed in Optimal scenario", null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AddField( + model_name='existingboileroutputs', + name='size_mmbtu_per_hour_bau', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='existingchillerinputs', + name='installed_cost_dollars', + field=models.FloatField(blank=True, default=0.0, help_text='Cost incurred in BAU scenario, as well as Optimal if needed still, in dollars', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AddField( + model_name='existingchillerinputs', + name='installed_cost_per_ton', + field=models.FloatField(blank=True, default=0.0, help_text="Thermal power capacity-based cost incurred in BAU and only based on what's needed in Optimal scenario", null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100000000.0)]), + ), + migrations.AddField( + model_name='existingchilleroutputs', + name='size_ton', + field=models.FloatField(blank=True, null=True), + ), + migrations.AddField( + model_name='existingchilleroutputs', + name='size_ton_bau', + field=models.FloatField(blank=True, null=True), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index 5b02a19b2..71515822e 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -5053,6 +5053,28 @@ class ExistingChillerInputs(BaseModel, models.Model): help_text="Boolean indicator if the existing chiller is unavailable in the optimal case (still used in BAU)" ) + installed_cost_per_ton = models.FloatField( + default=0.0, + null=True, + blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + help_text="Thermal power capacity-based cost incurred in BAU and only based on what's needed in Optimal scenario" + ) + + installed_cost_dollars = models.FloatField( + default=0.0, + null=True, + blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + help_text="Cost incurred in BAU scenario, as well as Optimal if needed still, in dollars" + ) + def clean(self): pass @@ -5068,6 +5090,9 @@ class ExistingChillerOutputs(BaseModel, models.Model): primary_key=True ) + size_ton = models.FloatField(null=True, blank=True) + size_ton_bau = models.FloatField(null=True, blank=True) + thermal_to_storage_series_ton = ArrayField( models.FloatField( blank=True @@ -5268,6 +5293,29 @@ class ExistingBoilerInputs(BaseModel, models.Model): help_text="Existing boiler fuel type, one of natural_gas, landfill_bio_gas, propane, diesel_oil" ) + + installed_cost_per_mmbtu_per_hour = models.FloatField( + default=0.0, + null=True, + blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + help_text="Thermal power capacity-based cost incurred in BAU and only based on what's needed in Optimal scenario" + ) + + installed_cost_dollars = models.FloatField( + default=0.0, + null=True, + blank=True, + validators=[ + MinValueValidator(0), + MaxValueValidator(MAX_BIG_NUMBER) + ], + help_text="Cost incurred in BAU scenario, as well as Optimal if needed still, in dollars" + ) + can_supply_steam_turbine = models.BooleanField( default=False, blank=True, @@ -5334,6 +5382,7 @@ class ExistingBoilerOutputs(BaseModel, models.Model): ) size_mmbtu_per_hour = models.FloatField(null=True, blank=True) + size_mmbtu_per_hour_bau = models.FloatField(null=True, blank=True) annual_fuel_consumption_mmbtu = models.FloatField(null=True, blank=True) annual_fuel_consumption_mmbtu_bau = models.FloatField(null=True, blank=True) From 73d54536fc19c67c280a015aeeb81950fc2def8c Mon Sep 17 00:00:00 2001 From: bill-becker Date: Wed, 4 Jun 2025 14:22:21 -0600 Subject: [PATCH 29/36] Update REopt.jl branch for latest battery C and OM costs --- julia_src/Manifest.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index a4ab4c461..b6a488653 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -922,7 +922,7 @@ 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 = "dc4aef468cfb3b14e105e3f9c7874fdad01f2e95" +git-tree-sha1 = "7f41842737ce902942d5b1291f777ce55ef9adec" repo-rev = "storage-cost-constraints-version2" repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" From 5a3a21eed34f563c8932bd5ab0891750407d4c1d Mon Sep 17 00:00:00 2001 From: bill-becker Date: Wed, 4 Jun 2025 14:24:31 -0600 Subject: [PATCH 30/36] Merge migrations after merging add-ghp-inputs branch --- reoptjl/migrations/0091_merge_20250604_2023.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 reoptjl/migrations/0091_merge_20250604_2023.py diff --git a/reoptjl/migrations/0091_merge_20250604_2023.py b/reoptjl/migrations/0091_merge_20250604_2023.py new file mode 100644 index 000000000..8c83be0da --- /dev/null +++ b/reoptjl/migrations/0091_merge_20250604_2023.py @@ -0,0 +1,14 @@ +# Generated by Django 4.0.7 on 2025-06-04 20:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0085_ghpoutputs_annual_thermal_production_mmbtu_and_more'), + ('reoptjl', '0090_existingboilerinputs_installed_cost_dollars_and_more'), + ] + + operations = [ + ] From 04ff6aa3daedbd300bdbe60ec956a91cca367b9b Mon Sep 17 00:00:00 2001 From: bill-becker Date: Wed, 4 Jun 2025 14:39:43 -0600 Subject: [PATCH 31/36] Hard-code old ElectricStorage defaults for tests with LCC/NPV checks --- reoptjl/test/posts/all_inputs_test.json | 3 ++- reoptjl/test/posts/outage.json | 5 ++++- reoptjl/test/posts/pv_batt_emissions.json | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/reoptjl/test/posts/all_inputs_test.json b/reoptjl/test/posts/all_inputs_test.json index 5fcf96406..7bfb1c979 100644 --- a/reoptjl/test/posts/all_inputs_test.json +++ b/reoptjl/test/posts/all_inputs_test.json @@ -169,13 +169,14 @@ "can_grid_charge": true, "installed_cost_per_kw": 840.0, "installed_cost_per_kwh": 420.0, - "installed_cost_constant": 20000.0, + "installed_cost_constant": 0.0, "replace_cost_per_kw": 410.0, "replace_cost_per_kwh": 200.0, "replace_cost_constant": 0.0, "inverter_replacement_year": 10, "battery_replacement_year": 10, "cost_constant_replacement_year": 10, + "om_cost_fraction_of_installed_cost": 0.0, "macrs_option_years": 7, "macrs_bonus_fraction": 1.0, "macrs_itc_reduction": 0.5, diff --git a/reoptjl/test/posts/outage.json b/reoptjl/test/posts/outage.json index 1a212b871..5658437c5 100644 --- a/reoptjl/test/posts/outage.json +++ b/reoptjl/test/posts/outage.json @@ -75,8 +75,11 @@ "macrs_bonus_fraction": 1.0, "installed_cost_per_kw": 840.0, "installed_cost_per_kwh": 420.0, + "installed_cost_constant": 0.0, "replace_cost_per_kw": 410.0, - "replace_cost_per_kwh": 200.0 + "replace_cost_per_kwh": 200.0, + "replace_cost_constant": 0.0, + "om_cost_fraction_of_installed_cost": 0.0 }, "Financial": { "value_of_lost_load_per_kwh": 100.0, diff --git a/reoptjl/test/posts/pv_batt_emissions.json b/reoptjl/test/posts/pv_batt_emissions.json index fc926b1a9..4853c5b43 100644 --- a/reoptjl/test/posts/pv_batt_emissions.json +++ b/reoptjl/test/posts/pv_batt_emissions.json @@ -35,8 +35,11 @@ "macrs_bonus_fraction": 0.4, "replace_cost_per_kw": 460.0, "replace_cost_per_kwh": 230.0, + "replace_cost_constant": 0.0, "installed_cost_per_kw": 1000.0, "installed_cost_per_kwh": 500.0, + "installed_cost_constant": 0.0, + "om_cost_fraction_of_installed_cost": 0.0, "total_itc_fraction": 0.0 }, "ElectricTariff": { From d712766941d6eeb9596f96027df6240649de523d Mon Sep 17 00:00:00 2001 From: bill-becker Date: Thu, 12 Jun 2025 23:21:05 -0600 Subject: [PATCH 32/36] Update REopt.jl#pv-size-classes version with latest and develop Also remove debug print statement --- julia_src/Manifest.toml | 4 ++-- reoptjl/views.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index 68332f50c..ccd44b5d1 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -922,11 +922,11 @@ 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 = "3e9bc2a692b1b101d8637dfcd61da75686d269a2" +git-tree-sha1 = "a2b146b751f4e6aa589f9be3470445bff35336d7" repo-rev = "pv-size-classes" repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.51.1" +version = "0.52.0" [[deps.Random]] deps = ["SHA"] diff --git a/reoptjl/views.py b/reoptjl/views.py index 39443415f..d0797ccbd 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -536,7 +536,6 @@ def pv_cost_defaults(request): inputs = {k: v for k, v in inputs.items() if v is not None} - print(inputs) try: julia_host = os.environ.get('JULIA_HOST', "julia") http_jl_response = requests.get("http://" + julia_host + ":8081/pv_cost_defaults/", json=inputs) From b14bebaf7c788f615e4f5654d3ae97d7a3b51aea Mon Sep 17 00:00:00 2001 From: bill-becker Date: Thu, 12 Jun 2025 23:26:19 -0600 Subject: [PATCH 33/36] Merge migration after merging battery-cost branch --- reoptjl/migrations/0092_merge_20250613_0525.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 reoptjl/migrations/0092_merge_20250613_0525.py diff --git a/reoptjl/migrations/0092_merge_20250613_0525.py b/reoptjl/migrations/0092_merge_20250613_0525.py new file mode 100644 index 000000000..9d52ffd7d --- /dev/null +++ b/reoptjl/migrations/0092_merge_20250613_0525.py @@ -0,0 +1,14 @@ +# Generated by Django 4.0.7 on 2025-06-13 05:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0085_pvinputs_size_class_pvoutputs_installed_cost_per_kw_and_more'), + ('reoptjl', '0091_merge_20250604_2023'), + ] + + operations = [ + ] From f921bd5951760da213e3c510799a3b32888da287 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Fri, 13 Jun 2025 08:29:56 -0600 Subject: [PATCH 34/36] Update REopt.jl to develop branch --- julia_src/Manifest.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index ccd44b5d1..e44f27a8e 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -923,7 +923,7 @@ 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 = "a2b146b751f4e6aa589f9be3470445bff35336d7" -repo-rev = "pv-size-classes" +repo-rev = "develop" repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" version = "0.52.0" From 045f78e17bb4d6532d0fdf190e5ffaace23ed12a Mon Sep 17 00:00:00 2001 From: bill-becker Date: Fri, 13 Jun 2025 14:47:42 -0600 Subject: [PATCH 35/36] Update CHANGELOG with all of the merged branch changes --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 497d194a9..c7e0be752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,29 @@ Classify the change according to the following categories: ##### Removed ### Patches +## pv-cost +### Minor Updates +#### Changed +- Updated **PV.installed_cost_per_kw** and **PV.om_cost_per_kw** default values to reflect latest cost data. +### Added + +## battery-cost +### Minor Updates +#### Changed +- Updated **ElectricStorage.installed_cost_per_kw**, **ElectricStorage.installed_cost_per_kwh**, **ElectricStorage.replace_cost_per_kw**, and **ElectricStorage.replace_cost_per_kwh** default values to reflect latest cost data. +- Updated **ElectricStorage** cost defaults in `reoptjl/models.py` and `nested_inputs.py` for v2 and v3. + +## add-ghp-inputs +### Minor Updates +#### Added +- Added new **GHPInputs** fields for advanced ground heat exchanger sizing and configuration. +- Added **hybrid_ghx_sizing_method** input to allow user selection of GHX sizing approach. +- Added new outputs to **GHPOutputs** for reporting system sizes and borehole count. +#### Changed +- Updated GHP input validation and defaults in `reoptjl/models.py` and `validators.py`. +- Updated `/ghpghx` endpoint to support new GHP input fields. +- Added tests for new GHP input and output fields. + ## add-capex-constraint ### Minor Updates #### Added From b79819aba8599e28881081e830563a6abbf9b5a1 Mon Sep 17 00:00:00 2001 From: bill-becker Date: Fri, 13 Jun 2025 16:33:29 -0600 Subject: [PATCH 36/36] Update REopt.jl to registered v0.53.0 --- julia_src/Manifest.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index e44f27a8e..2ce97d73a 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -922,11 +922,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 = "a2b146b751f4e6aa589f9be3470445bff35336d7" -repo-rev = "develop" -repo-url = "https://github.com/NREL/REopt.jl.git" +git-tree-sha1 = "1b2ede642ebd6b9471500fbbd0b54d806edb669a" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.52.0" +version = "0.53.0" [[deps.Random]] deps = ["SHA"]