Skip to content

Commit 7a32030

Browse files
authored
Merge pull request #693 from NREL/sim-load-post-fixes
/simulated_load POST request updates
2 parents aa1ea8b + 6c2c58b commit 7a32030

3 files changed

Lines changed: 174 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ Classify the change according to the following categories:
2626
##### Removed
2727
### Patches
2828

29+
## v3.17.5
30+
### Minor Updates
31+
##### Added
32+
- Tests in `test_http_endpoints.py` to test expected `/simulated_load` POST request functionality
33+
##### Changed
34+
- Makes `load_type` optional for both GET and POST requests to `/simulated_load`, with a default `load_type=electric`, consistent with the `simulated_load()` function in REopt.jl
35+
- Makes the `year` input not required for inputs with `doe_reference_name` for POST request to `/simulated_load`, consistent with GET request and `simulated_load()` function in REopt.jl
36+
##### Fixed
37+
- POST requests to `/simulated_load` with `doe_reference_name` input to pass along the lat/long inputs (was not passing those inputs through)
38+
39+
2940
## v3.17.4
3041
### Minor Updates
3142
##### Fixed

reoptjl/test/test_http_endpoints.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,128 @@ def test_simulated_load(self):
149149
v2_response = json.loads(resp.content)
150150
assert("Error" in v2_response.keys())
151151

152+
def test_simulated_load_post(self):
153+
154+
# Test 1: POST with normalize_and_scale_load_profile_input and load_profile
155+
load_profile = [100.0] * 8760 # Simple 8760 hourly load profile
156+
monthly_totals = [450000.0, 420000.0, 480000.0, 510000.0, 550000.0, 600000.0,
157+
620000.0, 610000.0, 570000.0, 520000.0, 470000.0, 440000.0] # 12 monthly totals in kWh to scale to
158+
inputs = {
159+
"normalize_and_scale_load_profile_input": True,
160+
"load_profile": load_profile,
161+
"year": 2021,
162+
"monthly_totals_kwh": monthly_totals
163+
}
164+
resp = self.api_client.post(f'/stable/simulated_load', format='json', data=inputs)
165+
self.assertHttpOK(resp)
166+
post_response = json.loads(resp.content)
167+
self.assertAlmostEqual(post_response["annual_kwh"], sum(monthly_totals), delta=10.0)
168+
169+
# Test 2: POST with doe_reference_name and monthly_totals_kwh, monthly_peaks_kw
170+
monthly_peaks = [900.0, 850.0, 950.0, 1000.0, 1100.0, 1200.0,
171+
1300.0, 1250.0, 1150.0, 1050.0, 950.0, 900.0] # 12 monthly peaks in kW (+/- 30%)
172+
inputs = {
173+
"doe_reference_name": "Hospital",
174+
"latitude": 36.12,
175+
"longitude": -115.5,
176+
"load_type": "electric",
177+
"monthly_totals_kwh": monthly_totals,
178+
"monthly_peaks_kw": monthly_peaks,
179+
"year": 2021
180+
}
181+
resp = self.api_client.post(f'/stable/simulated_load', format='json', data=inputs)
182+
self.assertHttpOK(resp)
183+
post_response = json.loads(resp.content)
184+
self.assertIn("loads_kw", post_response.keys())
185+
self.assertEqual(len(post_response["loads_kw"]), 8760)
186+
self.assertIn("annual_kwh", post_response.keys())
187+
188+
# Test 3: POST with doe_reference_name only (no monthly data and no load_type, defaults to electric)
189+
# and check consistency with GET request
190+
inputs = {
191+
"doe_reference_name": "LargeOffice",
192+
"latitude": 40.7128,
193+
"longitude": -74.0060,
194+
"annual_kwh": 1000000.0
195+
}
196+
resp = self.api_client.post(f'/stable/simulated_load', format='json', data=inputs)
197+
self.assertHttpOK(resp)
198+
post_response = json.loads(resp.content)
199+
self.assertAlmostEqual(post_response["annual_kwh"], 1000000.0, delta=1.0)
200+
201+
get_resp = self.api_client.get(f'/stable/simulated_load', data=inputs)
202+
self.assertHttpOK(get_resp)
203+
get_response = json.loads(get_resp.content)
204+
self.assertEqual(post_response["loads_kw"][:3], get_response["loads_kw"][:3])
205+
206+
# Test 4: POST with blended/hybrid buildings using arrays for doe_reference_name and percent_share
207+
inputs = {
208+
"doe_reference_name": ["LargeOffice", "FlatLoad"],
209+
"percent_share": [0.60, 0.40],
210+
"latitude": 36.12,
211+
"longitude": -115.5,
212+
"load_type": "electric",
213+
"annual_kwh": 1.5e7,
214+
"year": 2021
215+
}
216+
resp = self.api_client.post(f'/stable/simulated_load', format='json', data=inputs)
217+
self.assertHttpOK(resp)
218+
post_response = json.loads(resp.content)
219+
self.assertIn("loads_kw", post_response.keys())
220+
self.assertEqual(len(post_response["loads_kw"]), 8760)
221+
self.assertAlmostEqual(post_response["annual_kwh"], 1.5e7, delta=1.0)
222+
223+
# Test 5: Validation - Missing both normalize_and_scale_load_profile_input and doe_reference_name
224+
inputs = {
225+
"latitude": 36.12,
226+
"longitude": -115.5,
227+
"year": 2021,
228+
"monthly_totals_kwh": monthly_totals
229+
}
230+
resp = self.api_client.post(f'/stable/simulated_load', format='json', data=inputs)
231+
self.assertHttpBadRequest(resp)
232+
post_response = json.loads(resp.content)
233+
self.assertIn("Error", post_response.keys())
234+
self.assertIn("Missing either of", post_response["Error"])
235+
236+
# Test 6: Validation - normalize_and_scale_load_profile_input without year
237+
inputs = {
238+
"normalize_and_scale_load_profile_input": True,
239+
"load_profile": load_profile
240+
}
241+
resp = self.api_client.post(f'/stable/simulated_load', format='json', data=inputs)
242+
self.assertHttpBadRequest(resp)
243+
post_response = json.loads(resp.content)
244+
self.assertIn("Error", post_response.keys())
245+
self.assertIn("year is required", post_response["Error"])
246+
247+
# Test 7: Validation - doe_reference_name without latitude
248+
inputs = {
249+
"doe_reference_name": "Hospital",
250+
"longitude": -115.5,
251+
"year": 2021
252+
}
253+
resp = self.api_client.post(f'/stable/simulated_load', format='json', data=inputs)
254+
self.assertHttpBadRequest(resp)
255+
post_response = json.loads(resp.content)
256+
self.assertIn("Error", post_response.keys())
257+
self.assertIn("latitude and longitude are required", post_response["Error"])
258+
259+
# Test 8: POST with non-8760 load_profile and time_steps_per_hour
260+
load_profile_15min = [100.0] * 35040 # 8760 * 4 for 15-minute intervals
261+
inputs = {
262+
"normalize_and_scale_load_profile_input": True,
263+
"load_profile": load_profile_15min,
264+
"monthly_totals_kwh": monthly_totals,
265+
"year": 2021,
266+
"time_steps_per_hour": 4
267+
}
268+
resp = self.api_client.post(f'/stable/simulated_load', format='json', data=inputs)
269+
self.assertHttpOK(resp)
270+
post_response = json.loads(resp.content)
271+
self.assertAlmostEqual(post_response["annual_kwh"], sum(monthly_totals), delta=10.0)
272+
273+
152274
def test_avert_emissions_profile_endpoint(self):
153275
# Call to the django view endpoint dev/avert_emissions_profile which calls the http.jl endpoint
154276
#case 1: location in CONUS (Seattle, WA)

reoptjl/views.py

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -597,12 +597,13 @@ def simulated_load(request):
597597
inputs["latitude"] = float(request.GET['latitude']) # need float to convert unicode
598598
inputs["longitude"] = float(request.GET['longitude'])
599599
# Optional load_type - will default to "electric"
600-
inputs["load_type"] = request.GET.get('load_type')
600+
if 'load_type' in request.GET:
601+
inputs["load_type"] = request.GET["load_type"]
601602
# Optional year parameter to shift the CRB profile from 2017 (also 2023) to the input year
602603
if 'year' in request.GET:
603604
inputs["year"] = int(request.GET['year'])
604605

605-
if inputs["load_type"] == 'process_heat':
606+
if inputs.get("load_type") == 'process_heat':
606607
expected_reference_name = 'industrial_reference_name'
607608
else:
608609
expected_reference_name = 'doe_reference_name'
@@ -654,44 +655,60 @@ def simulated_load(request):
654655

655656
if request.method == "POST":
656657
data = json.loads(request.body)
657-
required_post_fields = ["load_type", "year"]
658-
either_required = ["normalize_and_scale_load_profile_input", "doe_reference_name"]
659-
optional = ["percent_share"]
658+
either_required = ["normalize_and_scale_load_profile_input", "doe_reference_name", "industrial_reference_name"]
660659
either_check = 0
661660
for either in either_required:
662-
if data.get(either) is not None:
661+
if either in data:
663662
inputs[either] = data[either]
664663
either_check += 1
665664
if either_check == 0:
666-
return JsonResponse({"Error": "Missing either of normalize_and_scale_load_profile_input or doe_reference_name."}, status=400)
665+
return JsonResponse({"Error": "Missing either of normalize_and_scale_load_profile_input or [doe or industrial]_reference_name."}, status=400)
667666
elif either_check == 2:
668-
return JsonResponse({"Error": "Both normalize_and_scale_load_profile_input and doe_reference_name were input; only input one of these."}, status=400)
669-
for field in required_post_fields:
670-
# TODO make year optional for doe_reference_name input
671-
inputs[field] = data[field]
672-
for opt in optional:
673-
if data.get(opt) is not None:
674-
inputs[opt] = data[opt]
675-
if data.get("normalize_and_scale_load_profile_input") is not None:
667+
return JsonResponse({"Error": "Both normalize_and_scale_load_profile_input and [doe or industrial]_reference_name were input; only input one of these."}, status=400)
668+
669+
# If normalize_and_scale_load_profile_input is true, year and load_profile are required
670+
if data.get("normalize_and_scale_load_profile_input") is True:
671+
if "year" not in data:
672+
return JsonResponse({"Error": "year is required when normalize_and_scale_load_profile_input is true."}, status=400)
673+
inputs["year"] = data["year"]
676674
if "load_profile" not in data:
677675
return JsonResponse({"Error": "load_profile is required when normalize_and_scale_load_profile_input is provided."}, status=400)
678676
inputs["load_profile"] = data["load_profile"]
679677
if len(inputs["load_profile"]) != 8760:
680678
if "time_steps_per_hour" not in data:
681679
return JsonResponse({"Error": "time_steps_per_hour is required when load_profile length is not 8760."}, status=400)
682680
inputs["time_steps_per_hour"] = data["time_steps_per_hour"]
683-
if inputs["load_type"] == "electric":
681+
682+
# If doe_reference_name is provided, latitude and longitude are required, year is optional
683+
if "doe_reference_name" in data or "industrial_reference_name" in data:
684+
if "latitude" not in data or "longitude" not in data:
685+
return JsonResponse({"Error": "latitude and longitude are required when doe_reference_name is provided."}, status=400)
686+
inputs["latitude"] = float(data["latitude"])
687+
inputs["longitude"] = float(data["longitude"])
688+
# year is optional for doe_reference_name, as it will default to 2017
689+
if "year" in data:
690+
inputs["year"] = data["year"]
691+
if "percent_share" in data:
692+
inputs["percent_share"] = data["percent_share"]
693+
694+
# Optional load_type determines required energy input options (default is "electric")
695+
load_type = data.get("load_type")
696+
if load_type is None:
697+
load_type = "electric" # default load_type for simulate_load()
698+
else:
699+
inputs["load_type"] = data["load_type"]
700+
if load_type == "electric":
684701
for energy in ["annual_kwh", "monthly_totals_kwh", "monthly_peaks_kw"]:
685-
if data.get(energy) is not None:
686-
inputs[energy] = data.get(energy)
687-
elif inputs["load_type"] in ["space_heating", "domestic_hot_water", "process_heat"]:
702+
if energy in data:
703+
inputs[energy] = data[energy]
704+
elif load_type in ["space_heating", "domestic_hot_water", "process_heat"]:
688705
for energy in ["annual_mmbtu", "monthly_mmbtu"]:
689-
if data.get(energy) is not None:
690-
inputs[energy] = data.get(energy)
691-
elif inputs["load_type"] == "cooling":
706+
if energy in data:
707+
inputs[energy] = data[energy]
708+
elif load_type == "cooling":
692709
for energy in ["annual_tonhour", "monthly_tonhour"]:
693-
if data.get(energy) is not None:
694-
inputs[energy] = data.get(energy)
710+
if energy in data:
711+
inputs[energy] = data[energy]
695712

696713
# json.dump(inputs, open("sim_load_post.json", "w"))
697714
julia_host = os.environ.get('JULIA_HOST', "julia")

0 commit comments

Comments
 (0)