diff --git a/CHANGELOG.md b/CHANGELOG.md index 36a410c04..f6e33c1d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,21 @@ Classify the change according to the following categories: ##### Removed ### Patches +## v3.10.2 +### Minor Updates +##### Changed +- Summary focus can now be a string with multiple types of focus such as `A,B,C` +##### Fixed +- Issue with `CHP.installed_cost_per_kw` not being an array when updating the inputs model object (which expects an array) in process_results.py, from Julia + +## v3.10.1 +### Minor Updates +##### Fixed +- ASHP min allowable sizing +- Prevent battery simultaneous charge/discharge +##### Changed +- Updated GHP to allow costs to be calculated for GHP and GHX separately and without running GhpGhx.jl, for district energy applications + ## v3.10.0 ### Minor Updates #### Added diff --git a/julia_src/Manifest.toml b/julia_src/Manifest.toml index feddf4486..b64b17376 100644 --- a/julia_src/Manifest.toml +++ b/julia_src/Manifest.toml @@ -917,9 +917,9 @@ uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" [[deps.REopt]] deps = ["ArchGDAL", "CSV", "CoolProp", "DataFrames", "Dates", "DelimitedFiles", "HTTP", "JLD", "JSON", "JuMP", "LinDistFlow", "LinearAlgebra", "Logging", "MathOptInterface", "Requires", "Roots", "Statistics", "TestEnv"] -git-tree-sha1 = "32499f329265d270e9f77c8831892772b5fbf28f" +git-tree-sha1 = "79c315746fe8274cf047d5d8d04be1b75020065b" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" -version = "0.48.0" +version = "0.48.1" [[deps.Random]] deps = ["SHA"] diff --git a/reoptjl/custom_table_config.py b/reoptjl/custom_table_config.py index d30a7c831..bfafcaea5 100644 --- a/reoptjl/custom_table_config.py +++ b/reoptjl/custom_table_config.py @@ -137,8 +137,8 @@ { "label" : "Results URL", "key" : "url", - "bau_value" : lambda df: f'=HYPERLINK("https://custom-table-download-reopt-stage.its.nrel.gov/tool/results/{safe_get(df, "webtool_uuid")}", "Results Link")', - "scenario_value": lambda df: f'=HYPERLINK("https://custom-table-download-reopt-stage.its.nrel.gov/tool/results/{safe_get(df, "webtool_uuid")}", "Results Link")' + "bau_value" : lambda df: f'=HYPERLINK("https://reopt.nrel.gov/tool/results/{safe_get(df, "webtool_uuid")}", "Results Link")', + "scenario_value": lambda df: f'=HYPERLINK("https://reopt.nrel.gov/tool/results/{safe_get(df, "webtool_uuid")}", "Results Link")' }, ##################################################################################################### ######################### System Capacities ############################# diff --git a/reoptjl/src/process_results.py b/reoptjl/src/process_results.py index 87023850b..5b5732d69 100644 --- a/reoptjl/src/process_results.py +++ b/reoptjl/src/process_results.py @@ -126,6 +126,8 @@ def update_inputs_in_database(inputs_to_update: dict, run_uuid: str) -> None: SiteInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["Site"]) if inputs_to_update["CHP"]: # Will be an empty dictionary if CHP is not considered + if inputs_to_update["CHP"].get("installed_cost_per_kw") and type(inputs_to_update["CHP"].get("installed_cost_per_kw")) == float: + inputs_to_update["CHP"]["installed_cost_per_kw"] = [inputs_to_update["CHP"]["installed_cost_per_kw"]] CHPInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["CHP"]) if inputs_to_update["SteamTurbine"]: # Will be an empty dictionary if SteamTurbine is not considered SteamTurbineInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["SteamTurbine"]) @@ -139,8 +141,10 @@ def update_inputs_in_database(inputs_to_update: dict, run_uuid: str) -> None: else: ExistingChillerInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ExistingChiller"]) if inputs_to_update["ASHPSpaceHeater"]: + prune_update_fields(ASHPSpaceHeaterInputs, inputs_to_update["ASHPSpaceHeater"]) ASHPSpaceHeaterInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ASHPSpaceHeater"]) if inputs_to_update["ASHPWaterHeater"]: + prune_update_fields(ASHPWaterHeaterInputs, inputs_to_update["ASHPWaterHeater"]) ASHPWaterHeaterInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ASHPWaterHeater"]) except Exception as e: exc_type, exc_value, exc_traceback = sys.exc_info() @@ -150,3 +154,13 @@ def update_inputs_in_database(inputs_to_update: dict, run_uuid: str) -> None: tb.format_tb(exc_traceback) ) log.debug(debug_msg) + +def prune_update_fields(model_obj, dict_to_update): + """ + REopt.jl may return more fields than the API has to update, so prune those extra ones before updating the model/db object + """ + field_names = [field.name for field in model_obj._meta.get_fields()] + dict_to_update_keys = list(dict_to_update.keys()) + for key in dict_to_update_keys: + if key not in field_names: + del dict_to_update[key] \ No newline at end of file diff --git a/reoptjl/test/posts/ashp_defaults_update.json b/reoptjl/test/posts/ashp_defaults_update.json new file mode 100644 index 000000000..d3302c1d6 --- /dev/null +++ b/reoptjl/test/posts/ashp_defaults_update.json @@ -0,0 +1,51 @@ +{ + "user_uuid": "1d2ef71e-fd93-4c4a-b5c3-1485a87f772e", + "webtool_uuid": "1ab7530f-74b8-4ea8-b8ed-f11bd953f61f", + "Settings": { + "optimality_tolerance": 0.001, + "solver_name": "HiGHS", + "off_grid_flag": false, + "include_climate_in_objective": false, + "include_health_in_objective": false + }, + "Meta": { + "address": "San Francisco CA USA" + }, + "Site": { + "latitude": 37.7749295, + "longitude": -122.4194155, + "include_exported_renewable_electricity_in_total": true, + "include_exported_elec_emissions_in_total": true, + "land_acres": 1000000.0, + "roof_squarefeet": 0 + }, + "ElectricLoad": { + "doe_reference_name": "Hospital" + }, + "ElectricTariff": { + "blended_annual_energy_rate": 0.15, + "blended_annual_demand_rate": 0.0 + }, + "ElectricUtility": { + "cambium_location_type": "GEA Regions", + "cambium_metric_col": "lrmer_co2e", + "cambium_scenario": "Mid-case", + "cambium_grid_level": "enduse" + }, + "SpaceHeatingLoad": { + "annual_mmbtu": 11570.916, + "doe_reference_name": "Hospital" + }, + "DomesticHotWaterLoad": { + "annual_mmbtu": 671.405, + "doe_reference_name": "Hospital" + }, + "ExistingBoiler": { + "fuel_type": "natural_gas", + "fuel_cost_per_mmbtu": 25.0 + }, + "ASHPSpaceHeater": { + "force_into_system": true, + "can_serve_cooling": false + } +} \ No newline at end of file diff --git a/reoptjl/test/test_job_endpoint.py b/reoptjl/test/test_job_endpoint.py index aebd2bf7a..579e16806 100644 --- a/reoptjl/test/test_job_endpoint.py +++ b/reoptjl/test/test_job_endpoint.py @@ -311,4 +311,19 @@ def test_centralghp(self): resp = self.api_client.get(f'/v3/job/{run_uuid}/results') r = json.loads(resp.content) - self.assertAlmostEqual(r["outputs"]["Financial"]["lifecycle_capital_costs"], 1046066.8, delta=1000) \ No newline at end of file + self.assertAlmostEqual(r["outputs"]["Financial"]["lifecycle_capital_costs"], 1046066.8, delta=1000) + + def test_ashp_defaults_update_from_julia(self): + # Test that the inputs_with_defaults_set_in_julia feature worked for ASHPSpaceHeater + post_file = os.path.join('reoptjl', 'test', 'posts', 'ashp_defaults_update.json') + post = json.load(open(post_file, 'r')) + resp = self.api_client.post('/stable/job/', format='json', data=post) + 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.assertEquals(r["inputs"]["ASHPSpaceHeater"]["om_cost_per_ton"], 0.0) + self.assertEquals(r["inputs"]["ASHPSpaceHeater"]["sizing_factor"], 1.1) \ No newline at end of file diff --git a/reoptjl/views.py b/reoptjl/views.py index 2bd5e1651..04b2177ae 100644 --- a/reoptjl/views.py +++ b/reoptjl/views.py @@ -1066,17 +1066,18 @@ def queryset_for_summary(api_metas,summary_dict:dict): ) if len(utility) > 0: for m in utility: - + if 'focus' not in summary_dict[str(m.meta.run_uuid)].keys(): + summary_dict[str(m.meta.run_uuid)]['focus'] = '' if m.outage_start_time_step is None: if len(m.outage_start_time_steps) == 0: - summary_dict[str(m.meta.run_uuid)]['focus'] = "Financial" + summary_dict[str(m.meta.run_uuid)]['focus'] += "Financial," else: - summary_dict[str(m.meta.run_uuid)]['focus'] = "Resilience" + summary_dict[str(m.meta.run_uuid)]['focus'] += "Resilience," summary_dict[str(m.meta.run_uuid)]['outage_duration'] = m.outage_durations[0] # all durations are same. else: # outage start timestep was provided, is 1 or more summary_dict[str(m.meta.run_uuid)]['outage_duration'] = m.outage_end_time_step - m.outage_start_time_step + 1 - summary_dict[str(m.meta.run_uuid)]['focus'] = "Resilience" + summary_dict[str(m.meta.run_uuid)]['focus'] += "Resilience," site = SiteOutputs.objects.filter(meta__run_uuid__in=run_uuids).only( 'meta__run_uuid', @@ -1097,15 +1098,18 @@ def queryset_for_summary(api_metas,summary_dict:dict): ) if len(site_inputs) > 0: for m in site_inputs: + # if focus key doesnt exist, create it + if 'focus' not in summary_dict[str(m.meta.run_uuid)].keys(): + summary_dict[str(m.meta.run_uuid)]['focus'] = '' try: # can be NoneType if m.renewable_electricity_min_fraction > 0: - summary_dict[str(m.meta.run_uuid)]['focus'] = "Clean-energy" + summary_dict[str(m.meta.run_uuid)]['focus'] += "Clean-energy," except: pass # is NoneType try: # can be NoneType if m.renewable_electricity_max_fraction > 0: - summary_dict[str(m.meta.run_uuid)]['focus'] = "Clean-energy" + summary_dict[str(m.meta.run_uuid)]['focus'] += "Clean-energy," except: pass # is NoneType @@ -1118,11 +1122,14 @@ def queryset_for_summary(api_metas,summary_dict:dict): ) if len(settings) > 0: for m in settings: + # if focus key doesnt exist, create it + if 'focus' not in summary_dict[str(m.meta.run_uuid)].keys(): + summary_dict[str(m.meta.run_uuid)]['focus'] = '' if m.off_grid_flag: - summary_dict[str(m.meta.run_uuid)]['focus'] = "Off-grid" + summary_dict[str(m.meta.run_uuid)]['focus'] += "Off-grid," if m.include_climate_in_objective or m.include_health_in_objective: - summary_dict[str(m.meta.run_uuid)]['focus'] = "Clean-energy" + summary_dict[str(m.meta.run_uuid)]['focus'] += "Clean-energy," tariffInputs = ElectricTariffInputs.objects.filter(meta__run_uuid__in=run_uuids).only( 'meta__run_uuid',