Skip to content

Commit

Permalink
Merge branch 'develop' into norm-scale-load
Browse files Browse the repository at this point in the history
  • Loading branch information
Bill-Becker committed Dec 26, 2024
2 parents 4a992f9 + e52e720 commit bf5c08b
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 16 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The REopt API provides concurrent, multiple technology integration and optimizat

## Should I be using or modifying the REopt API or the REopt Julia Package?

The REopt Julia package will soon become the backend of the REopt API. That means that the optimization model will be contained in [REopt.jl](https://github.com/NREL/REopt.jl), and that a user could supply the same inputs to the API and Julia package and get the same results. So which should you use?
The REopt Julia package is the backend of the REopt API. That means that the optimization model is contained in [REopt.jl](https://github.com/NREL/REopt.jl), and that a user could supply the same inputs to the API and Julia package and get the same results. So which should you use?

**1. When and how to _use_ the REopt Julia package:**
- You want to be able to use the REopt model without incorporating an API call (and associated rate limits).
Expand Down
12 changes: 6 additions & 6 deletions ghpghx/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@ class GHPGHXInputs(models.Model):

# Single value inputs
borehole_depth_ft = models.FloatField(blank=True,
default=400.0, validators=[MinValueValidator(10.0), MaxValueValidator(600.0)],
default=443.0, validators=[MinValueValidator(10.0), MaxValueValidator(600.0)],
help_text="Vertical depth of each borehole [ft]")
ghx_header_depth_ft = models.FloatField(blank=True,
default=4.0, validators=[MinValueValidator(0.1), MaxValueValidator(50.0)],
default=6.6, validators=[MinValueValidator(0.1), MaxValueValidator(50.0)],
help_text="Depth under the ground of the GHX header pipe [ft]")
borehole_spacing_ft = models.FloatField(blank=True,
default=20.0, validators=[MinValueValidator(1.0), MaxValueValidator(100.0)],
help_text="Distance from the centerline of each borehole to the centerline of its adjacent boreholes [ft]")
borehole_diameter_inch = models.FloatField(blank=True,
default=5.0, validators=[MinValueValidator(0.25), MaxValueValidator(24.0)],
default=6.0, validators=[MinValueValidator(0.25), MaxValueValidator(24.0)],
help_text="Diameter of the borehole/well drilled in the ground [in]")
borehole_choices = [("rectangular", "rectangular"),
("hexagonal", "hexagonal")]
Expand All @@ -49,10 +49,10 @@ class GHPGHXInputs(models.Model):
default=0.16, validators=[MinValueValidator(0.01), MaxValueValidator(5.0)],
help_text="Wall thickness of the GHX pipe [in]")
ghx_pipe_thermal_conductivity_btu_per_hr_ft_f = models.FloatField(blank=True,
default=0.25, validators=[MinValueValidator(0.01), MaxValueValidator(10.0)],
default=0.23, validators=[MinValueValidator(0.01), MaxValueValidator(10.0)],
help_text="Thermal conductivity of the GHX pipe [Btu/(hr-ft-degF)]")
ghx_shank_space_inch = models.FloatField(blank=True,
default=2.5, validators=[MinValueValidator(0.5), MaxValueValidator(100.0)],
default=1.27, validators=[MinValueValidator(0.5), MaxValueValidator(100.0)],
help_text="Distance between the centerline of the upwards and downwards u-tube legs [in]")
# Default for ground_thermal_conductivity_btu_per_hr_ft_f varies by ASHRAE climate zone
ground_thermal_conductivity_btu_per_hr_ft_f = models.FloatField(blank=True,
Expand All @@ -65,7 +65,7 @@ class GHPGHXInputs(models.Model):
default=0.211, validators=[MinValueValidator(0.01), MaxValueValidator(5.0)],
help_text="Specific heat of the ground surrounding the borehole field")
grout_thermal_conductivity_btu_per_hr_ft_f = models.FloatField(blank=True,
default=1.0, validators=[MinValueValidator(0.01), MaxValueValidator(10.0)],
default=0.75, validators=[MinValueValidator(0.01), MaxValueValidator(10.0)],
help_text="Thermal conductivity of the grout material in a borehole [Btu/(hr-ft-degF)]")
ghx_fluid_specific_heat_btu_per_lb_f = models.FloatField(blank=True,
default=1.0, validators=[MinValueValidator(0.1), MaxValueValidator(10.0)],
Expand Down
14 changes: 14 additions & 0 deletions reoptjl/src/process_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand All @@ -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()
Expand All @@ -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]
51 changes: 51 additions & 0 deletions reoptjl/test/posts/ashp_defaults_update.json
Original file line number Diff line number Diff line change
@@ -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
}
}
17 changes: 16 additions & 1 deletion reoptjl/test/test_job_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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)
23 changes: 15 additions & 8 deletions reoptjl/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1097,17 +1097,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',
Expand All @@ -1128,15 +1129,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

Expand All @@ -1149,11 +1153,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',
Expand Down

0 comments on commit bf5c08b

Please sign in to comment.