diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e7df597c..6fa40b7fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/julia_src/http.jl b/julia_src/http.jl index 631da7486..6e8bbd544 100644 --- a/julia_src/http.jl +++ b/julia_src/http.jl @@ -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" @@ -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 @@ -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) diff --git a/reoptjl/api.py b/reoptjl/api.py index 4753f096e..e2a6264b7 100644 --- a/reoptjl/api.py +++ b/reoptjl/api.py @@ -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}) diff --git a/reoptjl/models.py b/reoptjl/models.py index e9419054b..7b6aeb964 100644 --- a/reoptjl/models.py +++ b/reoptjl/models.py @@ -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." ) diff --git a/reoptjl/src/run_jump_model.py b/reoptjl/src/run_jump_model.py index 6499068ff..34ab85812 100644 --- a/reoptjl/src/run_jump_model.py +++ b/reoptjl/src/run_jump_model.py @@ -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 @@ -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) diff --git a/reoptjl/test/test_job_endpoint.py b/reoptjl/test/test_job_endpoint.py index 5ac8f3dc9..8a41b24ce 100644 --- a/reoptjl/test/test_job_endpoint.py +++ b/reoptjl/test/test_job_endpoint.py @@ -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)) diff --git a/resilience_stats/api.py b/resilience_stats/api.py index fbe66104b..754247fe4 100644 --- a/resilience_stats/api.py +++ b/resilience_stats/api.py @@ -62,7 +62,6 @@ def obj_create(self, bundle, **kwargs): meta_dict = { "run_uuid": erp_run_uuid, - "reopt_version": "0.45.0", "status": "Validating..." } @@ -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() diff --git a/resilience_stats/models.py b/resilience_stats/models.py index f3aeb4b91..7da62888d 100644 --- a/resilience_stats/models.py +++ b/resilience_stats/models.py @@ -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." ) diff --git a/resilience_stats/tests/test_erp.py b/resilience_stats/tests/test_erp.py index fcd284360..e7b2e98d0 100644 --- a/resilience_stats/tests/test_erp.py +++ b/resilience_stats/tests/test_erp.py @@ -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') @@ -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), @@ -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 @@ -182,7 +192,7 @@ 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 """ @@ -190,6 +200,14 @@ def test_erp_help_view(self): 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) diff --git a/resilience_stats/urls_v3plus.py b/resilience_stats/urls_v3plus.py index 023b43161..9438b8960 100644 --- a/resilience_stats/urls_v3plus.py +++ b/resilience_stats/urls_v3plus.py @@ -6,5 +6,7 @@ urlpatterns = [ re_path(r'^erp/(?P[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), ] diff --git a/resilience_stats/views.py b/resilience_stats/views.py index 468a68453..d5d3b7add 100644 --- a/resilience_stats/views.py +++ b/resilience_stats/views.py @@ -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'))