Skip to content

Commit

Permalink
Merge branch 'develop' into custom-tables
Browse files Browse the repository at this point in the history
  • Loading branch information
Bill-Becker committed Sep 27, 2024
2 parents a75e513 + 6539cc2 commit c63e92a
Show file tree
Hide file tree
Showing 14 changed files with 606 additions and 138 deletions.
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ Classify the change according to the following categories:
##### Removed
### Patches

## Develop 2024-08-20
### Major Updates
## v3.10.0
### Minor Updates
#### Added
- Added new model **ElectricHeaterInputs**
- Added new model **ElectricHeaterOutputs**
Expand All @@ -36,6 +36,18 @@ Classify the change according to the following categories:
- Added new model **ASHPSpaceHeaterInputs**
- Added new model **ASHPSpaceHeaterOutputs**

## v3.9.4
### Minor Updates
#### Added
- **portfolio_uuid** is now a field that can be added to API objects
- **PortfolioUnlinkedRuns** tracks which run_uuids were separated from their portfolios
- `/user/<user_uuid>/unlink_from_portfolio/` endpoint (calls `views.unlink_from_portfolio`)
- `/summary_by_runuuids/` endpoint (calls `views.summary_by_runuuids`)
- `/link_run_to_portfolios/` endpoint (calls `views.link_run_to_portfolios`)

#### Changed
- `UnexpectedError`, added portfolio_uuid as a field that can be returned in case of errors

## v3.9.3
### Minor Updates
#### Added
Expand Down
6 changes: 2 additions & 4 deletions julia_src/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -917,11 +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 = "125328678059c4650d8024c0c25676527667eb22"
repo-rev = "add-ASHP"
repo-url = "https://github.com/NREL/REopt.jl.git"
git-tree-sha1 = "32499f329265d270e9f77c8831892772b5fbf28f"
uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6"
version = "0.47.2"
version = "0.48.0"

[[deps.Random]]
deps = ["SHA"]
Expand Down
37 changes: 35 additions & 2 deletions julia_src/http.jl
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,31 @@ function reopt(req::HTTP.Request)
else
ghp_dict = Dict()
end
if haskey(d, "ASHPSpaceHeater")
inputs_with_defaults_from_ashp = [
:max_ton, :installed_cost_per_ton, :om_cost_per_ton,
:macrs_option_years, :macrs_bonus_fraction, :can_supply_steam_turbine,
:can_serve_process_heat, :can_serve_dhw, :can_serve_space_heating, :can_serve_cooling,
:back_up_temp_threshold_degF, :sizing_factor, :heating_cop_reference, :heating_cf_reference,
:heating_reference_temps_degF, :cooling_cop_reference, :cooling_cf_reference,
:cooling_reference_temps_degF
]
ashp_dict = Dict(key=>getfield(model_inputs.s.ashp, key) for key in inputs_with_defaults_from_ashp)
else
ashp_dict = Dict()
end
if haskey(d, "ASHPWaterHeater")
inputs_with_defaults_from_ashp_wh = [
:max_ton, :installed_cost_per_ton, :om_cost_per_ton,
:macrs_option_years, :macrs_bonus_fraction, :can_supply_steam_turbine,
:can_serve_process_heat, :can_serve_dhw, :can_serve_space_heating, :can_serve_cooling,
:back_up_temp_threshold_degF, :sizing_factor, :heating_cop_reference, :heating_cf_reference,
:heating_reference_temps_degF
]
ashp_wh_dict = Dict(key=>getfield(model_inputs.s.ashp_wh, key) for key in inputs_with_defaults_from_ashp_wh)
else
ashp_wh_dict = Dict()
end
if haskey(d, "CoolingLoad")
inputs_with_defaults_from_chiller = [
:cop
Expand All @@ -125,7 +150,9 @@ function reopt(req::HTTP.Request)
"CHP" => chp_dict,
"SteamTurbine" => steamturbine_dict,
"GHP" => ghp_dict,
"ExistingChiller" => chiller_dict
"ExistingChiller" => chiller_dict,
"ASHPSpaceHeater" => ashp_dict,
"ASHPWaterHeater" => ashp_wh_dict
)
catch e
@error "Something went wrong in REopt optimization!" exception=(e, catch_backtrace())
Expand Down Expand Up @@ -525,12 +552,18 @@ function get_ashp_defaults(req::HTTP.Request)
@info("ASHP load served not provided. Using default of SpaceHeating.")
d["load_served"] = "SpaceHeating"
end
if !("force_into_system" in keys(d))
@info("ASHP force_into_system not provided. Using default of false.")
d["force_into_system"] = false
elseif typeof(d["force_into_system"]) == String
d["force_into_system"] = parse(Bool, d["force_into_system"])
end

@info "Getting default ASHP attributes..."
error_response = Dict()
try
# Have to specify "REopt.get_existing..." because http function has the same name
defaults = reoptjl.get_ashp_defaults(d["load_served"])
defaults = reoptjl.get_ashp_defaults(d["load_served"],d["force_into_system"])
catch e
@error "Something went wrong in the get_ashp_defaults endpoint" exception=(e, catch_backtrace())
error_response["error"] = sprint(showerror, e)
Expand Down
4 changes: 2 additions & 2 deletions reo/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,11 @@ class UnexpectedError(REoptError):

__name__ = 'UnexpectedError'

def __init__(self, exc_type, exc_value, exc_traceback, task='', run_uuid='', user_uuid='', message=None):
def __init__(self, exc_type, exc_value, exc_traceback, task='', run_uuid='', user_uuid='', portfolio_uuid='', message=None):
debug_msg = "exc_type: {}; exc_value: {}; exc_traceback: {}".format(exc_type, exc_value, exc_traceback)
if message is None:
message = "Unexpected Error."
super(UnexpectedError, self).__init__(task=task, name=self.__name__, run_uuid=run_uuid, user_uuid=user_uuid,
super(UnexpectedError, self).__init__(task=task, name=self.__name__, run_uuid=run_uuid, user_uuid=user_uuid, portfolio_uuid=portfolio_uuid,
message=message, traceback=debug_msg)


Expand Down
13 changes: 13 additions & 0 deletions reoptjl/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ def obj_create(self, bundle, **kwargs):
else:
webtool_uuid = None

if 'portfolio_uuid' in bundle.data.keys():

if type(bundle.data['portfolio_uuid']) == str:
if len(bundle.data['portfolio_uuid']) == len(run_uuid):
portfolio_uuid = bundle.data['portfolio_uuid']
else:
portfolio_uuid = ''
else:
portfolio_uuid = None

meta = {
"run_uuid": run_uuid,
"api_version": 3,
Expand All @@ -110,6 +120,9 @@ def obj_create(self, bundle, **kwargs):

if api_key != "":
bundle.data['APIMeta']['api_key'] = api_key

if portfolio_uuid is not None:
bundle.data['APIMeta']['portfolio_uuid'] = portfolio_uuid

log.addFilter(UUIDFilter(run_uuid))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.0.7 on 2024-08-20 16:09

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('reoptjl', '0060_processheatloadinputs_addressable_load_fraction_and_more'),
]

operations = [
migrations.CreateModel(
name='PortfolioUnlinkedRuns',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('portfolio_uuid', models.UUIDField()),
('user_uuid', models.UUIDField()),
('run_uuid', models.UUIDField(unique=True)),
],
),
migrations.AddField(
model_name='apimeta',
name='portfolio_uuid',
field=models.TextField(blank=True, default='', help_text='The unique ID of a portfolio (set of associated runs) created by the REopt Webtool. Note that this ID can be shared by several REopt API Scenarios and one user can have one-to-many portfolio_uuid tied to them.'),
),
migrations.AlterField(
model_name='apimeta',
name='reopt_version',
field=models.TextField(blank=True, default='', help_text='Version number of the Julia package for REopt that is used to solve the problem.', null=True),
),
]
14 changes: 14 additions & 0 deletions reoptjl/migrations/0070_merge_20240925_0600.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 4.0.7 on 2024-09-25 06:00

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('reoptjl', '0061_portfoliounlinkedruns_apimeta_portfolio_uuid_and_more'),
('reoptjl', '0069_financialoutputs_initial_capital_costs_after_incentives_without_macrs_and_more'),
]

operations = [
]
26 changes: 26 additions & 0 deletions reoptjl/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ class APIMeta(BaseModel, models.Model):
default="",
help_text="NREL Developer API key of the user"
)
portfolio_uuid = models.TextField(
blank=True,
default="",
help_text=("The unique ID of a portfolio (set of associated runs) created by the REopt Webtool. Note that this ID can be shared by "
"several REopt API Scenarios and one user can have one-to-many portfolio_uuid tied to them.")
)

class UserUnlinkedRuns(models.Model):
run_uuid = models.UUIDField(unique=True)
Expand All @@ -196,6 +202,17 @@ def create(cls, **kwargs):
obj.save()
return obj

class PortfolioUnlinkedRuns(models.Model):
portfolio_uuid = models.UUIDField(unique=False)
user_uuid = models.UUIDField(unique=False)
run_uuid = models.UUIDField(unique=True)

@classmethod
def create(cls, **kwargs):
obj = cls(**kwargs)
obj.save()
return obj

class UserProvidedMeta(BaseModel, models.Model):
"""
User provided values that are not necessary for running REopt
Expand Down Expand Up @@ -5477,6 +5494,12 @@ def clean(self):
if self.dict.get("min_allowable_ton") not in [None, "", []] and self.dict.get("min_allowable_peak_capacity_fraction") not in [None, "", []]:
error_messages["bad inputs"] = "At most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input to model {}".format(self.key)

if len(self.dict.get("heating_cop_reference")) != len(self.dict.get("heating_cf_reference")) or len(self.dict.get("heating_cop_reference")) != len(self.dict.get("heating_reference_temps_degF")):
error_messages["mismatched length"] = "Model {} inputs heating_cop_reference, heating_cf_reference, and heating_reference_temps_degF must all have the same length.".format(self.key)

if len(self.dict.get("cooling_cop_reference")) != len(self.dict.get("cooling_cf_reference")) or len(self.dict.get("cooling_cop_reference")) != len(self.dict.get("cooling_reference_temps_degF")):
error_messages["mismatched length"] = "Model {} inputs cooling_cop_reference, cooling_cf_reference, and cooling_reference_temps_degF must all have the same length.".format(self.key)

if error_messages:
raise ValidationError(error_messages)

Expand Down Expand Up @@ -5739,6 +5762,9 @@ def clean(self):
if self.dict.get("min_allowable_ton") not in [None, "", []] and self.dict.get("min_allowable_peak_capacity_fraction") not in [None, "", []]:
error_messages["bad inputs"] = "At most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input to model {}".format(self.key)

if len(self.dict.get("heating_cop_reference")) != len(self.dict.get("heating_cf_reference")) or len(self.dict.get("heating_cop_reference")) != len(self.dict.get("heating_reference_temps_degF")):
error_messages["mismatched length"] = "Model {} inputs heating_cop_reference, heating_cf_reference, and heating_reference_temps_degF must all have the same length.".format(self.key)

if error_messages:
raise ValidationError(error_messages)

Expand Down
6 changes: 5 additions & 1 deletion reoptjl/src/process_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
REoptjlMessageOutputs, AbsorptionChillerOutputs, BoilerOutputs, SteamTurbineInputs, \
SteamTurbineOutputs, GHPInputs, GHPOutputs, ExistingChillerInputs, \
ElectricHeaterOutputs, ASHPSpaceHeaterOutputs, ASHPWaterHeaterOutputs, \
SiteInputs
SiteInputs, ASHPSpaceHeaterInputs, ASHPWaterHeaterInputs
import numpy as np
import sys
import traceback as tb
Expand Down Expand Up @@ -138,6 +138,10 @@ def update_inputs_in_database(inputs_to_update: dict, run_uuid: str) -> None:
ExistingChillerInputs.create(meta=meta, **inputs_to_update["ExistingChiller"]).save()
else:
ExistingChillerInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ExistingChiller"])
if inputs_to_update["ASHPSpaceHeater"]:
ASHPSpaceHeaterInputs.objects.filter(meta__run_uuid=run_uuid).update(**inputs_to_update["ASHPSpaceHeater"])
if 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()
debug_msg = "exc_type: {}; exc_value: {}; exc_traceback: {}".format(
Expand Down
9 changes: 7 additions & 2 deletions reoptjl/test/test_http_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,23 +380,28 @@ def test_default_existing_chiller_cop(self):

def test_get_ashp_defaults(self):
inputs_dict = {
"load_served": "SpaceHeating"
"load_served": "SpaceHeating",
"force_into_system": "true"
}

# Call to the django view endpoint /get_existing_chiller_default_cop which calls the http.jl endpoint
resp = self.api_client.get(f'/v3/get_ashp_defaults', data=inputs_dict)
view_response = json.loads(resp.content)

self.assertEqual(view_response["installed_cost_per_ton"], 2250)
self.assertEqual(view_response["om_cost_per_ton"], 0.0)
self.assertEqual(view_response["sizing_factor"], 1.1)

inputs_dict = {
"load_served": "DomesticHotWater"
"load_served": "DomesticHotWater",
"force_into_system": "false"
}

# Call to the django view endpoint /get_existing_chiller_default_cop which calls the http.jl endpoint
resp = self.api_client.get(f'/v3/get_ashp_defaults', data=inputs_dict)
view_response = json.loads(resp.content)

self.assertNotIn("cooling_cf_reference", view_response.keys())
self.assertEqual(view_response["sizing_factor"], 1.0)


27 changes: 27 additions & 0 deletions reoptjl/test/test_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,30 @@ def boiler_validation(self):
# self.assertAlmostEqual(len(validator.models["ExistingBoiler"].fuel_cost_per_mmbtu), 8760)
# self.assertAlmostEqual(sum(validator.models["ExistingBoiler"].fuel_cost_per_mmbtu), 8760*0.5)

def ashp_validation(self):
"""
Ensure that bad inputs are caught by clean() for ASHP systems.
"""
post_file = os.path.join('job', 'test', 'posts', 'all_inputs_test.json')
post = json.load(open(post_file, 'r'))

post["APIMeta"]["run_uuid"] = uuid.uuid4()
post["ASHPSpaceHeater"]["cooling_cf_reference"] = []

validator = InputValidator(post)
validator.clean_fields()
validator.clean()
validator.cross_clean()
assert("mismatched length" in validator.validation_errors["ASHPSpaceHeater"].keys())

post = json.load(open(post_file, 'r'))
post["APIMeta"]["run_uuid"] = uuid.uuid4()
post["ASHPWaterHeater"]["min_allowable_ton"] = 1000
post["ASHPWaterHeater"]["min_allowable_peak_capacity_fraction"] = 0.9

validator = InputValidator(post)
validator.clean_fields()
validator.clean()
validator.cross_clean()
assert("bad inputs" in validator.validation_errors["ASHPWaterHeater"].keys())

5 changes: 4 additions & 1 deletion reoptjl/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@
re_path(r'^user/(?P<user_uuid>[0-9a-f-]+)/summary/?$', views.summary),
re_path(r'^user/(?P<user_uuid>[0-9a-f-]+)/summary_by_chunk/(?P<chunk>[0-9]+)/?$', views.summary_by_chunk),
re_path(r'^user/(?P<user_uuid>[0-9a-f-]+)/unlink/(?P<run_uuid>[0-9a-f-]+)/?$', views.unlink),
re_path(r'^user/(?P<user_uuid>[0-9a-f-]+)/unlink_from_portfolio/(?P<portfolio_uuid>[0-9a-f-]+)/(?P<run_uuid>[0-9a-f-]+)/?$', views.unlink_from_portfolio),
re_path(r'^ghp_efficiency_thermal_factors/?$', views.ghp_efficiency_thermal_factors),
re_path(r'^peak_load_outage_times/?$', views.peak_load_outage_times),
re_path(r'^invalid_urdb/?$', reoviews.invalid_urdb),
re_path(r'^schedule_stats/?$', reoviews.schedule_stats),
re_path(r'^get_existing_chiller_default_cop/?$', views.get_existing_chiller_default_cop),
re_path(r'^job/generate_custom_comparison_table/?$', views.generate_custom_comparison_table),
re_path(r'^get_ashp_defaults/?$', views.get_ashp_defaults)
re_path(r'^get_ashp_defaults/?$', views.get_ashp_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)
]
Loading

0 comments on commit c63e92a

Please sign in to comment.