Skip to content

Commit

Permalink
Merge pull request #594 from NREL/develop
Browse files Browse the repository at this point in the history
ERP outputs endpoint; set reopt_version in meta data programmatically
  • Loading branch information
hdunham authored Jul 29, 2024
2 parents 5c402c7 + 4ec5df9 commit 1e7e271
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 24 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ Classify the change according to the following categories:
##### Removed
### Patches

## v3.9.3
### Minor Updates
#### Added
- `/erp/inputs` endpoint (calls `erp_help()`, same as `/erp/help`)
- `/erp/outputs` endpoint that GETs the ERP output field info (calls `erp_outputs()`)
#### Changed
- Set **reopt_version** in **APIMeta** and **ERPMeta** programatically based on actual REopt.jl package version in Julia environment instead of hardcoded so doesn't need to be updated by hand

## v3.9.2
#### Added
- Added attribute `thermal_efficiency` to the arguments of http endpoint `chp_defaults`
Expand Down
17 changes: 9 additions & 8 deletions julia_src/http.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ end
function reopt(req::HTTP.Request)
d = JSON.parse(String(req.body))
error_response = Dict()
settings = d["Settings"]
if !isempty(get(d, "api_key", ""))
ENV["NREL_DEVELOPER_API_KEY"] = pop!(d, "api_key")
else
ENV["NREL_DEVELOPER_API_KEY"] = test_nrel_developer_api_key
delete!(d, "api_key")
end
settings = d["Settings"]
solver_name = get(settings, "solver_name", "HiGHS")
if solver_name == "Xpress" && !(xpress_installed=="True")
solver_name = "HiGHS"
Expand Down Expand Up @@ -140,23 +140,22 @@ function reopt(req::HTTP.Request)

if isempty(error_response)
@info "REopt model solved with status $(results["status"])."
response = Dict(
"results" => results,
"reopt_version" => string(pkgversion(reoptjl))
)
if results["status"] == "error"
response = Dict(
"results" => results
)
if !isempty(inputs_with_defaults_set_in_julia)
response["inputs_with_defaults_set_in_julia"] = inputs_with_defaults_set_in_julia
end
return HTTP.Response(400, JSON.json(response))
else
response = Dict(
"results" => results,
"inputs_with_defaults_set_in_julia" => inputs_with_defaults_set_in_julia
)
response["inputs_with_defaults_set_in_julia"] = inputs_with_defaults_set_in_julia
return HTTP.Response(200, JSON.json(response))
end
else
@info "An error occured in the Julia code."
error_response["reopt_version"] = string(pkgversion(reoptjl))
return HTTP.Response(500, JSON.json(error_response))
end
end
Expand All @@ -169,9 +168,11 @@ function erp(req::HTTP.Request)
results = Dict()
try
results = reoptjl.backup_reliability(erp_inputs)
results["reopt_version"] = string(pkgversion(reoptjl))
catch e
@error "Something went wrong in the ERP Julia code!" exception=(e, catch_backtrace())
error_response["error"] = sprint(showerror, e)
error_response["reopt_version"] = string(pkgversion(reoptjl))
end
GC.gc()
if isempty(error_response)
Expand Down
1 change: 0 additions & 1 deletion reoptjl/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ def obj_create(self, bundle, **kwargs):
meta = {
"run_uuid": run_uuid,
"api_version": 3,
"reopt_version": "0.47.2",
"status": "Validating..."
}
bundle.data.update({"APIMeta": meta})
Expand Down
1 change: 1 addition & 0 deletions reoptjl/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ class APIMeta(BaseModel, models.Model):
created = models.DateTimeField(auto_now_add=True)
reopt_version = models.TextField(
blank=True,
null=True,
default="",
help_text="Version number of the Julia package for REopt that is used to solve the problem."
)
Expand Down
2 changes: 2 additions & 0 deletions reoptjl/src/run_jump_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def run_jump_model(run_uuid):
if response.status_code == 500:
raise REoptFailedToStartError(task=name, message=response_json["error"], run_uuid=run_uuid, user_uuid=user_uuid)
results = response_json["results"]
reopt_version = response_json["reopt_version"]
if results["status"].strip().lower() != "error":
inputs_with_defaults_set_in_julia = response_json["inputs_with_defaults_set_in_julia"]
time_dict["pyjulia_run_reopt_seconds"] = time.time() - t_start
Expand Down Expand Up @@ -107,6 +108,7 @@ def run_jump_model(run_uuid):

profiler.profileEnd()
# TODO save profile times
APIMeta.objects.filter(run_uuid=run_uuid).update(reopt_version=reopt_version)
if status.strip().lower() != 'error':
update_inputs_in_database(inputs_with_defaults_set_in_julia, run_uuid)
process_results(results, run_uuid)
Expand Down
1 change: 1 addition & 0 deletions reoptjl/test/test_job_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def test_multiple_outages(self):
run_uuid = r.get('run_uuid')
resp = self.api_client.get(f'/v3/job/{run_uuid}/results')
r = json.loads(resp.content)
self.assertIn("reopt_version", r.keys())
results = r["outputs"]
self.assertEqual(np.array(results["Outages"]["unserved_load_series_kw"]).shape, (1,2,5))
self.assertEqual(np.array(results["Outages"]["generator_fuel_used_per_outage_gal"]).shape, (1,2))
Expand Down
4 changes: 2 additions & 2 deletions resilience_stats/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ def obj_create(self, bundle, **kwargs):

meta_dict = {
"run_uuid": erp_run_uuid,
"reopt_version": "0.45.0",
"status": "Validating..."
}

Expand Down Expand Up @@ -450,7 +449,8 @@ def process_erp_results(results: dict, run_uuid: str) -> None:
#TODO: get success or error status from julia
meta = ERPMeta.objects.get(run_uuid=run_uuid)
meta.status = 'Completed' #results.get("status")
meta.save(update_fields=['status'])
meta.reopt_version = results.pop("reopt_version")
meta.save(update_fields=['status','reopt_version'])
ERPOutputs.create(meta=meta, **results).save()


Expand Down
1 change: 1 addition & 0 deletions resilience_stats/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class ERPMeta(BaseModel, models.Model):
created = models.DateTimeField(auto_now_add=True)
reopt_version = models.TextField(
blank=True,
null=True,
default="",
help_text="Version number of the REopt Julia package that is used to calculate reliability."
)
Expand Down
22 changes: 20 additions & 2 deletions resilience_stats/tests/test_erp.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ def setUp(self):
self.reopt_base_erp = '/v3/erp/'
self.reopt_base_erp_results = '/v3/erp/{}/results/'
self.reopt_base_erp_help = '/v3/erp/help/'
self.reopt_base_erp_inputs = '/v3/erp/inputs/'
self.reopt_base_erp_outputs = '/v3/erp/outputs/'
self.reopt_base_erp_chp_defaults = '/v3/erp/chp_defaults/?prime_mover={0}&is_chp={1}&size_kw={2}'
self.post_sim_gens_batt_pv_wind = os.path.join('resilience_stats', 'tests', 'ERP_sim_gens_batt_pv_wind_post.json')
self.post_sim_large_stor = os.path.join('resilience_stats', 'tests', 'ERP_sim_large_stor_post.json')
Expand All @@ -42,6 +44,12 @@ def get_results_sim(self, run_uuid):
def get_help(self):
return self.api_client.get(self.reopt_base_erp_help)

def get_inputs(self):
return self.api_client.get(self.reopt_base_erp_inputs)

def get_outputs(self):
return self.api_client.get(self.reopt_base_erp_outputs)

def get_chp_defaults(self, prime_mover, is_chp, size_kw):
return self.api_client.get(
self.reopt_base_erp_chp_defaults.format(prime_mover, is_chp, size_kw),
Expand All @@ -66,7 +74,9 @@ def test_erp_long_duration_battery(self):
r_sim = json.loads(resp.content)
erp_run_uuid = r_sim.get('run_uuid')
resp = self.get_results_sim(erp_run_uuid)
results_sim = json.loads(resp.content)["outputs"]
r = json.loads(resp.content)
self.assertIn("reopt_version", r.keys())
results_sim = r["outputs"]

expected_result = ([1]*79)+[0.999543,0.994178,0.9871,0.97774,0.965753,0.949429,0.926712,0.899543,0.863584,0.826712,0.785616,0.736416,0.683105,0.626256,0.571005,0.519064,0.47226,0.429909,0.391553,0.357306,0]
#TODO: resolve bug where unlimted fuel markov portion of results goes to zero 1 timestep early
Expand Down Expand Up @@ -182,14 +192,22 @@ def test_erp_with_no_opt(self):
resp = self.get_results_sim(erp_run_uuid)
self.assertHttpOK(resp)

def test_erp_help_view(self):
def test_erp_help_views(self):
"""
Tests hiting the erp/help url to get defaults and other info about inputs
"""

resp = self.get_help()
self.assertHttpOK(resp)
resp = json.loads(resp.content)

resp = self.get_inputs()
self.assertHttpOK(resp)
resp = json.loads(resp.content)

resp = self.get_outputs()
self.assertHttpOK(resp)
resp = json.loads(resp.content)

resp = self.get_chp_defaults("recip_engine", True, 10000)
self.assertHttpOK(resp)
Expand Down
2 changes: 2 additions & 0 deletions resilience_stats/urls_v3plus.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
urlpatterns = [
re_path(r'^erp/(?P<run_uuid>[0-9a-f-]+)/results/?$', views.erp_results),
re_path(r'^erp/help/?$', views.erp_help),
re_path(r'^erp/inputs/?$', views.erp_inputs),
re_path(r'^erp/outputs/?$', views.erp_outputs),
re_path(r'^erp/chp_defaults/?$', views.erp_chp_prime_gen_defaults),
]
57 changes: 46 additions & 11 deletions resilience_stats/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,23 +104,58 @@ def erp_results(request, run_uuid):
resp['status'] = 'Error'
return JsonResponse(resp, status=500)

def get_erp_inputs_info():
d = dict()
d["reopt_run_uuid"] = ERPMeta.info_dict(ERPMeta)["reopt_run_uuid"]
# do models need to be passed in as arg?
d[ERPOutageInputs.key] = ERPOutageInputs.info_dict(ERPOutageInputs)
d[ERPPVInputs.key] = ERPPVInputs.info_dict(ERPPVInputs)
d[ERPWindInputs.key] = ERPWindInputs.info_dict(ERPWindInputs)
d[ERPElectricStorageInputs.key] = ERPElectricStorageInputs.info_dict(ERPElectricStorageInputs)
d[ERPGeneratorInputs.key] = ERPGeneratorInputs.info_dict(ERPGeneratorInputs)
d[ERPPrimeGeneratorInputs.key] = ERPPrimeGeneratorInputs.info_dict(ERPPrimeGeneratorInputs)
#TODO: add wind once implemented
return JsonResponse(d)

def erp_help(request):
"""
Served at host/erp/help
:param request:
:return: JSON response with all erp inputs
"""
try:
d = dict()
d["reopt_run_uuid"] = ERPMeta.info_dict(ERPMeta)["reopt_run_uuid"]
# do models need to be passed in as arg?
d[ERPOutageInputs.key] = ERPOutageInputs.info_dict(ERPOutageInputs)
d[ERPPVInputs.key] = ERPPVInputs.info_dict(ERPPVInputs)
d[ERPWindInputs.key] = ERPWindInputs.info_dict(ERPWindInputs)
d[ERPElectricStorageInputs.key] = ERPElectricStorageInputs.info_dict(ERPElectricStorageInputs)
d[ERPGeneratorInputs.key] = ERPGeneratorInputs.info_dict(ERPGeneratorInputs)
d[ERPPrimeGeneratorInputs.key] = ERPPrimeGeneratorInputs.info_dict(ERPPrimeGeneratorInputs)
#TODO: add wind once implemented
return JsonResponse(d)
resp = get_erp_inputs_info()
return resp

except Exception as e:
return JsonResponse({"Error": "Unexpected error in ERP help endpoint: {}".format(e.args[0])}, status=500)

def erp_inputs(request):
"""
Served at host/erp/inputs
:param request:
:return: JSON response with all erp inputs
"""
try:
resp = get_erp_inputs_info()
return resp

except Exception as e:
return JsonResponse({"Error": "Unexpected error in ERP inputs endpoint: {}".format(e.args[0])}, status=500)

def erp_outputs(request):
"""
Served at host/erp/outputs
:return: JSON response with all erp outputs
"""

try:
d = ERPOutputs.info_dict(ERPOutputs)
return JsonResponse(d)

except Exception as e:
return JsonResponse({"Error": "Unexpected error in ERP outputs endpoint: {}".format(e.args[0])}, status=500)

def erp_chp_prime_gen_defaults(request):
prime_mover = str(request.GET.get('prime_mover'))
is_chp = bool(request.GET.get('is_chp'))
Expand Down

0 comments on commit 1e7e271

Please sign in to comment.