From 9e9a8b117b169bd8e653b82bc8f034e646197f3f Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Mon, 5 Jan 2026 10:43:53 -0500 Subject: [PATCH 1/8] add size class endpoint for bess --- julia_src/http.jl | 46 ++++++++++++++++++++++++++++++++++++++++++++++ reoptjl/models.py | 16 +++++++++++++--- reoptjl/views.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index bb59014e1..1c472ac37 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -800,6 +800,51 @@ function pv_cost_defaults(req::HTTP.Request) end +function electric_storage_cost_defaults(req::HTTP.Request) + d = JSON.parse(String(req.body)) + float_vals = ["installed_cost_per_kw", "installed_cost_per_kwh", + "installed_cost_constant", "min_kw", "max_kw", + "electric_load_annual_peak", "electric_load_average"] + int_vals = ["size_class"] + string_vals = [] + 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 ElectricStorage cost defaults..." + data = Dict() + error_response = Dict() + try + data["installed_cost_per_kw"], data["installed_cost_per_kwh"], data["installed_cost_constant"], data["size_class"], data["size_kw_for_size_class"] = reoptjl.get_electric_storage_cost_params(; + (Symbol(k) => v for (k, v) in pairs(d))... + ) + catch e + @error "Something went wrong in the electric_storage_cost_defaults" exception=(e, catch_backtrace()) + error_response["error"] = sprint(showerror, e) + end + if isempty(error_response) + @info "ElectricStorage cost defaults determined." + response = data + return HTTP.Response(200, JSON.json(response)) + else + @info "An error occured in the electric_storage_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.") return HTTP.Response(500, JSON.json(error_response)) @@ -829,5 +874,6 @@ 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.register!(ROUTER, "GET", "/electric_storage_cost_defaults", electric_storage_cost_defaults) HTTP.register!(ROUTER, "GET", "/get_load_metrics", get_load_metrics) HTTP.serve(ROUTER, "0.0.0.0", 8081, reuseaddr=true) diff --git a/reoptjl/models.py b/reoptjl/models.py index 17a9560b3..33b32cbdc 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -3668,6 +3668,16 @@ class ElectricStorageInputs(BaseModel, models.Model): primary_key=True ) + size_class = models.IntegerField( + validators=[ + MinValueValidator(1), + MaxValueValidator(5) + ], + null=True, + blank=True, + help_text="ElectricStorage size class. Must be an integer value between 1 and 3. Default is calculated per ratio of annual peak and average load of given load profile." + ) + min_kw = models.FloatField( default=0, validators=[ @@ -3758,29 +3768,29 @@ 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=968.0, validators=[ MinValueValidator(0), MaxValueValidator(1.0e4) ], + null=True, blank=True, help_text="Total upfront battery power capacity costs (e.g. inverter and balance of power systems)" ) installed_cost_per_kwh = models.FloatField( - default=253.0, validators=[ MinValueValidator(0), MaxValueValidator(1.0e4) ], + null=True, blank=True, help_text="Total upfront battery costs" ) installed_cost_constant = models.FloatField( - default=222115.0, validators=[ MinValueValidator(0), MaxValueValidator(1.0e9) ], + null=True, blank=True, help_text="Fixed upfront cost for battery installation, independent of size." ) diff --git a/reoptjl/views.py b/reoptjl/views.py index ca24a5ff2..cc79247df 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -575,6 +575,45 @@ def pv_cost_defaults(request): log.debug(debug_msg) return JsonResponse({"Error": "Unexpected error in pv_cost_defaults endpoint. Check log for more."}, status=500) +def electric_storage_cost_defaults(request): + + if request.method == "POST": + inputs = json.loads(request.body) + else: + inputs = { + "installed_cost_per_kw": request.GET.get("installed_cost_per_kw"), + "installed_cost_per_kwh": request.GET.get("installed_cost_per_kwh"), + "installed_cost_constant" : request.GET.get("installed_cost_constant"), + "size_class" : request.GET.get("size_class"), + "min_kw": request.GET.get("min_kw"), + "max_kw": request.GET.get("max_kw"), + "electric_load_annual_peak": request.GET.get("electric_load_annual_peak"), + "electric_load_average": request.GET.get("electric_load_average") + } + + inputs = {k: v for k, v in inputs.items() if v is not None} + + 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: + 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 electric_storage_cost_defaults endpoint. Check log for more."}, status=500) + def simulated_load(request): try: # Build inputs dictionary to send to http.jl /simulated_load endpoint From 7382d1e0e1f924ddf907c61466c03fa801de5d83 Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Thu, 8 Jan 2026 18:41:18 -0500 Subject: [PATCH 2/8] make migrations and add right REopt branch --- julia_src/Manifest.toml | 6 ++-- ...ectricstorageinputs_size_class_and_more.py | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 reoptjl/migrations/0114_electricstorageinputs_size_class_and_more.py diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index b0930d9ab..0960dd10c 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -948,9 +948,11 @@ version = "1.11.0" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "00bb39c8f932a3320960f01adc139229c24e12b7" +git-tree-sha1 = "b4b50f8909ea73c613d0e265e46851687a406aad" +repo-rev = "electric_storage_size_class" +repo-url = "https://github.com/NREL/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.56.2" +version = "0.56.3" [[deps.Random]] deps = ["SHA"] diff --git a/reoptjl/migrations/0114_electricstorageinputs_size_class_and_more.py b/reoptjl/migrations/0114_electricstorageinputs_size_class_and_more.py new file mode 100644 index 000000000..7179fd9bf --- /dev/null +++ b/reoptjl/migrations/0114_electricstorageinputs_size_class_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.26 on 2026-01-08 23:39 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0113_merge_20251209_2338'), + ] + + operations = [ + migrations.AddField( + model_name='electricstorageinputs', + name='size_class', + field=models.IntegerField(blank=True, help_text='ElectricStorage size class. Must be an integer value between 1 and 3. Default is calculated per ratio of annual peak and average load of given load profile.', null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]), + ), + migrations.AlterField( + model_name='electricstorageinputs', + name='installed_cost_constant', + field=models.FloatField(blank=True, help_text='Fixed upfront cost for battery installation, independent of size.', null=True, 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, help_text='Total upfront battery power capacity costs (e.g. inverter and balance of power systems)', null=True, 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, help_text='Total upfront battery costs', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10000.0)]), + ), + ] From b71d82b89f983ed671ff3a2e03b9f45dbc473038 Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Thu, 8 Jan 2026 19:32:41 -0500 Subject: [PATCH 3/8] debug endpoint for storage size class --- reoptjl/urls.py | 1 + reoptjl/views.py | 20 +++++++++----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/reoptjl/urls.py b/reoptjl/urls.py index 6de54a354..c5ce4ded5 100644 --- a/reoptjl/urls.py +++ b/reoptjl/urls.py @@ -27,6 +27,7 @@ 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'^electric_storage_cost_defaults/?$', views.electric_storage_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), re_path(r'^get_load_metrics/?$', views.get_load_metrics), diff --git a/reoptjl/views.py b/reoptjl/views.py index cc79247df..05ced0e23 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -581,21 +581,19 @@ def electric_storage_cost_defaults(request): inputs = json.loads(request.body) else: inputs = { - "installed_cost_per_kw": request.GET.get("installed_cost_per_kw"), - "installed_cost_per_kwh": request.GET.get("installed_cost_per_kwh"), - "installed_cost_constant" : request.GET.get("installed_cost_constant"), - "size_class" : request.GET.get("size_class"), - "min_kw": request.GET.get("min_kw"), - "max_kw": request.GET.get("max_kw"), - "electric_load_annual_peak": request.GET.get("electric_load_annual_peak"), - "electric_load_average": request.GET.get("electric_load_average") + "installed_cost_per_kw": request.GET.get("installed_cost_per_kw") or None, + "installed_cost_per_kwh": request.GET.get("installed_cost_per_kwh") or None, + "installed_cost_constant" : request.GET.get("installed_cost_constant") or None, + "size_class" : request.GET.get("size_class") or None, + "min_kw": request.GET.get("min_kw") or 0, + "max_kw": request.GET.get("max_kw") or 1.0e9, + "electric_load_annual_peak": request.GET.get("electric_load_annual_peak") or 0, + "electric_load_average": request.GET.get("electric_load_average") or 0 } - inputs = {k: v for k, v in inputs.items() if v is not None} - try: julia_host = os.environ.get('JULIA_HOST', "julia") - http_jl_response = requests.get("http://" + julia_host + ":8081/pv_cost_defaults/", json=inputs) + http_jl_response = requests.get("http://" + julia_host + ":8081/electric_storage_cost_defaults/", json=inputs) response = JsonResponse( http_jl_response.json() ) From 9a2672391767ce95d716af439ad9f5ea1135ecc2 Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Thu, 10 Sep 2026 14:34:21 -0400 Subject: [PATCH 4/8] revive bess size class api branch --- julia_src/Manifest.toml | 4 +++- julia_src/http.jl | 2 +- ...e.py => 0126_electricstorageinputs_size_class_and_more.py} | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) rename reoptjl/migrations/{0114_electricstorageinputs_size_class_and_more.py => 0126_electricstorageinputs_size_class_and_more.py} (93%) diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index f5919635b..c49793b98 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -966,7 +966,9 @@ version = "1.11.0" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "57012c263c02d4d1362b4e248accbe3974e17c74" +git-tree-sha1 = "cbd0675c03f309a4a8bf4ff8048783d2c9c59f79" +repo-rev = "electric_storage_size_class" +repo-url = "https://github.com/NatLabRockies/REopt.jl.git" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" version = "0.61.1" diff --git a/julia_src/http.jl b/julia_src/http.jl index acc6603a4..6016531ac 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -278,7 +278,7 @@ function reopt(req::HTTP.Request) end if haskey(d, "ElectricStorage") inputs_with_defaults_from_julia_electric_storage = [ - :macrs_option_years, :macrs_bonus_fraction, :total_itc_fraction, :internal_efficiency_fraction + :macrs_option_years, :macrs_bonus_fraction, :total_itc_fraction, :internal_efficiency_fraction, :size_class ] electric_storage_dict = Dict(key=>getfield(model_inputs.s.storage.attr["ElectricStorage"], key) for key in inputs_with_defaults_from_julia_electric_storage) else diff --git a/reoptjl/migrations/0114_electricstorageinputs_size_class_and_more.py b/reoptjl/migrations/0126_electricstorageinputs_size_class_and_more.py similarity index 93% rename from reoptjl/migrations/0114_electricstorageinputs_size_class_and_more.py rename to reoptjl/migrations/0126_electricstorageinputs_size_class_and_more.py index 7179fd9bf..ba50fb8d5 100644 --- a/reoptjl/migrations/0114_electricstorageinputs_size_class_and_more.py +++ b/reoptjl/migrations/0126_electricstorageinputs_size_class_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.26 on 2026-01-08 23:39 +# Generated by Django 4.2.26 on 2026-09-10 17:12 import django.core.validators from django.db import migrations, models @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('reoptjl', '0113_merge_20251209_2338'), + ('reoptjl', '0125_chpoutputs_annual_thermal_curtailed_mmbtu_and_more'), ] operations = [ From 10f05ea5f0fd2839bbb22251e2231a997e8c8089 Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Thu, 10 Sep 2026 17:28:55 -0400 Subject: [PATCH 5/8] address copilot comments --- julia_src/http.jl | 4 +-- reoptjl/models.py | 4 +-- reoptjl/test/test_job_endpoint.py | 44 ++++++++++++++++++++++++++++++- reoptjl/views.py | 2 +- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/julia_src/http.jl b/julia_src/http.jl index 6016531ac..4548fabbb 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -278,7 +278,7 @@ function reopt(req::HTTP.Request) end if haskey(d, "ElectricStorage") inputs_with_defaults_from_julia_electric_storage = [ - :macrs_option_years, :macrs_bonus_fraction, :total_itc_fraction, :internal_efficiency_fraction, :size_class + :macrs_option_years, :macrs_bonus_fraction, :total_itc_fraction, :internal_efficiency_fraction, :size_class, :installed_cost_per_kw, :installed_cost_per_kwh, :installed_cost_constant ] electric_storage_dict = Dict(key=>getfield(model_inputs.s.storage.attr["ElectricStorage"], key) for key in inputs_with_defaults_from_julia_electric_storage) else @@ -914,7 +914,7 @@ function electric_storage_cost_defaults(req::HTTP.Request) response = data return HTTP.Response(200, JSON.json(response)) else - @info "An error occured in the electric_storage_cost_defaults endpoint" + @info "An error occurred in the electric_storage_cost_defaults endpoint" return HTTP.Response(500, JSON.json(error_response)) end end diff --git a/reoptjl/models.py b/reoptjl/models.py index 6848c2135..a71cd7d31 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -3671,11 +3671,11 @@ class ElectricStorageInputs(BaseModel, models.Model): size_class = models.IntegerField( validators=[ MinValueValidator(1), - MaxValueValidator(5) + MaxValueValidator(4) ], null=True, blank=True, - help_text="ElectricStorage size class. Must be an integer value between 1 and 3. Default is calculated per ratio of annual peak and average load of given load profile." + help_text="ElectricStorage size class. Must be an integer value between 1 and 4. Default is calculated per ratio of annual peak and average load of given load profile." ) ELECTRICSTORAGE_DISPATCH_STRATEGY = models.TextChoices('ELECTRICSTORAGE_DISPATCH_STRATEGY', ( "optimized", diff --git a/reoptjl/test/test_job_endpoint.py b/reoptjl/test/test_job_endpoint.py index 82e3f671e..7bfde5225 100644 --- a/reoptjl/test/test_job_endpoint.py +++ b/reoptjl/test/test_job_endpoint.py @@ -420,4 +420,46 @@ def test_pv_cost_defaults_update_from_julia(self): r = json.loads(resp.content) self.assertEqual(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 + self.assertAlmostEqual(r["inputs"]["PV"]["installed_cost_per_kw"], 2914.6, delta=0.05 * 2914.6) + + def test_electric_storage_cost_defaults_update_from_julia(self): + + input_data = dict() + + input_data["Financial"] = dict() + input_data["Financial"]["analysis_years"] = 20 + input_data["Financial"]["offtaker_discount_rate_fraction"] = 0.06 + input_data["Financial"]["offtaker_tax_rate_fraction"] = 0.26 + input_data["Financial"]["elec_cost_escalation_rate_fraction"] = 0.03 + input_data["Financial"]["om_cost_escalation_rate_fraction"] = 0.02 + + input_data["ElectricStorage"] = dict() + input_data["ElectricStorage"]["total_itc_fraction"] = 0.0 + input_data["ElectricStorage"]["macrs_option_years"] = 0 + input_data["ElectricStorage"]["macrs_bonus_fraction"] = 0.0 + + input_data["ElectricLoad"] = dict() + # Average load is 250 kw + input_data["ElectricLoad"]["loads_kw"] = [250.0] * 8760 + # Peak load determines size class. We use size class bounds to vary size class. + input_data["ElectricLoad"]["loads_kw"][6000] = 250.0 + 39 + input_data["ElectricLoad"]["year"] = 2025 + + input_data["Site"] = dict() + input_data["Site"]["longitude"] = -118.1164613 + input_data["Site"]["latitude"] = 34.5794343 + + input_data["ElectricTariff"] = dict() + input_data["ElectricTariff"]["blended_annual_energy_rate"] = 0.12 + input_data["ElectricTariff"]["blended_annual_demand_rate"] = 12.0 + + resp = self.api_client.post('/stable/job/', format='json', data=input_data) + 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.assertEqual(r["inputs"]["ElectricStorage"]["size_class"], 1) + self.assertAlmostEqual(r["inputs"]["PV"]["installed_cost_per_kw"], 705) \ No newline at end of file diff --git a/reoptjl/views.py b/reoptjl/views.py index aae28615c..ca6ca76ef 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -631,7 +631,7 @@ def electric_storage_cost_defaults(request): 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], + debug_msg = "exc_type: {}; exc_value: {}; exc_traceback: {}".format(str(exc_type), exc_value.args[0], tb.format_tb(exc_traceback)) log.debug(debug_msg) return JsonResponse({"Error": "Unexpected error in electric_storage_cost_defaults endpoint. Check log for more."}, status=500) From e8237503a1a2a35d0136da0c656ea44c195fa082 Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Wed, 16 Sep 2026 12:55:40 -0400 Subject: [PATCH 6/8] Adjust ElectricStorage defaults Update the default ElectricStorage O&M cost fraction from 0.025 to 0.04 per 2025 ATB defaults update and add a migration that enforces validation bounds for om_cost_fraction_of_installed_cost and size_class. --- ...ost_fraction_of_installed_cost_and_more.py | 24 +++++++++++++++++++ reoptjl/models.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 reoptjl/migrations/0127_alter_electricstorageinputs_om_cost_fraction_of_installed_cost_and_more.py diff --git a/reoptjl/migrations/0127_alter_electricstorageinputs_om_cost_fraction_of_installed_cost_and_more.py b/reoptjl/migrations/0127_alter_electricstorageinputs_om_cost_fraction_of_installed_cost_and_more.py new file mode 100644 index 000000000..9a539aa62 --- /dev/null +++ b/reoptjl/migrations/0127_alter_electricstorageinputs_om_cost_fraction_of_installed_cost_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.26 on 2026-09-16 16:54 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0126_electricstorageinputs_size_class_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='electricstorageinputs', + name='om_cost_fraction_of_installed_cost', + field=models.FloatField(blank=True, default=0.04, 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='size_class', + field=models.IntegerField(blank=True, help_text='ElectricStorage size class. Must be an integer value between 1 and 4. Default is calculated per ratio of annual peak and average load of given load profile.', null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4)]), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index a71cd7d31..82e94f85c 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -3902,7 +3902,7 @@ class ElectricStorageInputs(BaseModel, models.Model): 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, + default=0.04, validators=[ MinValueValidator(0), MaxValueValidator(1.0) From 076ca9db10672a54fc564285b3991d0a685f6ddc Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Mon, 21 Sep 2026 11:20:47 -0400 Subject: [PATCH 7/8] Update test_job_endpoint.py bug fix --- reoptjl/test/test_job_endpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reoptjl/test/test_job_endpoint.py b/reoptjl/test/test_job_endpoint.py index 7bfde5225..f3e3022a0 100644 --- a/reoptjl/test/test_job_endpoint.py +++ b/reoptjl/test/test_job_endpoint.py @@ -462,4 +462,4 @@ def test_electric_storage_cost_defaults_update_from_julia(self): r = json.loads(resp.content) self.assertEqual(r["inputs"]["ElectricStorage"]["size_class"], 1) - self.assertAlmostEqual(r["inputs"]["PV"]["installed_cost_per_kw"], 705) \ No newline at end of file + self.assertAlmostEqual(r["inputs"]["ElectricStorage"]["installed_cost_per_kw"], 705) \ No newline at end of file From d6b00f4013273eba6255f69a921f58ea9a631762 Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Mon, 21 Sep 2026 12:35:45 -0400 Subject: [PATCH 8/8] update wind cost defaults per ATB2025 --- .../0128_alter_windinputs_om_cost_per_kw.py | 19 +++++++++++++++++++ reoptjl/models.py | 10 +++++----- 2 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 reoptjl/migrations/0128_alter_windinputs_om_cost_per_kw.py diff --git a/reoptjl/migrations/0128_alter_windinputs_om_cost_per_kw.py b/reoptjl/migrations/0128_alter_windinputs_om_cost_per_kw.py new file mode 100644 index 000000000..824b085f2 --- /dev/null +++ b/reoptjl/migrations/0128_alter_windinputs_om_cost_per_kw.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.26 on 2026-09-21 16:34 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reoptjl', '0127_alter_electricstorageinputs_om_cost_fraction_of_installed_cost_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='windinputs', + name='om_cost_per_kw', + field=models.FloatField(blank=True, default=43, help_text='Annual operations and maintenance costs in $/kW', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000.0)]), + ), + ] diff --git a/reoptjl/models.py b/reoptjl/models.py index 82e94f85c..33900daf0 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -76,10 +76,10 @@ class MACRS_YEARS_CHOICES(models.IntegerChoices): } WIND_COST_DEFAULTS = { # size_class_to_installed_cost - "residential" : 7692.0, - "commercial" : 5776.0, - "medium" : 3807.0, - "large" : 2896.0 + "residential" : 8960.0, + "commercial" : 6782.0, + "medium" : 4368.0, + "large" : 3477.0 } def at_least_one_set(model, possible_sets): @@ -3411,7 +3411,7 @@ class WIND_SIZE_CLASS_CHOICES(models.TextChoices): help_text="Installed cost in $/kW. Default cost is determined based on size_class." ) om_cost_per_kw = models.FloatField( - default=42, + default=43, validators=[ MinValueValidator(0), MaxValueValidator(1.0e3)