Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions julia_src/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
46 changes: 46 additions & 0 deletions julia_src/http.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -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)]),
),
]
16 changes: 13 additions & 3 deletions reoptjl/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[
Expand Down Expand Up @@ -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."
)
Expand Down
1 change: 1 addition & 0 deletions reoptjl/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
37 changes: 37 additions & 0 deletions reoptjl/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down