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/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/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)]), + ), + ] 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/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 ca24a5ff2..05ced0e23 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -575,6 +575,43 @@ 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") 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 + } + + try: + julia_host = os.environ.get('JULIA_HOST', "julia") + http_jl_response = requests.get("http://" + julia_host + ":8081/electric_storage_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