Skip to content

Commit 3b6c996

Browse files
committed
Refactor hourly rate table to generic timeseries table
Renamed hourly_rate_helpers.py to timeseries_table_helpers.py and updated all references accordingly. Added (but not currently used) custom_timeseries_table_config.py to define flexible timeseries table configurations. Refactored the hourly_rate_table view and URL to get_timeseries_table, enhanced Excel formatting with colored headers, and improved code organization for future extensibility.
1 parent f0957dd commit 3b6c996

File tree

4 files changed

+179
-14
lines changed

4 files changed

+179
-14
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# custom_timeseries_table_config.py
2+
from reoptjl.timeseries_table_helpers import safe_get_list, safe_get_value, safe_get
3+
4+
"""
5+
Timeseries Table Configuration
6+
===============================
7+
This file defines configurations for timeseries Excel tables that display hourly or sub-hourly data.
8+
Each configuration specifies which columns to include and how to extract the data.
9+
10+
Naming Convention:
11+
------------------
12+
Structure: custom_timeseries_<feature>
13+
14+
- `custom_timeseries_`: Prefix indicating a timeseries table configuration
15+
- `<feature>`: Descriptive name for the specific timeseries configuration
16+
17+
Examples:
18+
- custom_timeseries_energy_demand: Configuration for energy and demand rate timeseries
19+
- custom_timeseries_emissions: Configuration for emissions timeseries
20+
- custom_timeseries_loads: Configuration for load profiles
21+
22+
Guidelines:
23+
- Use lowercase letters and underscores
24+
- Keep names descriptive and concise
25+
- Each configuration is a list of column dictionaries
26+
27+
Column Dictionary Structure:
28+
-----------------------------
29+
Each column configuration should have:
30+
{
31+
"label": str, # Column header text
32+
"key": str, # Unique identifier for the column
33+
"timeseries_path": str, # Dot-separated path to data in the results JSON (e.g., "outputs.ElectricLoad.load_series_kw")
34+
"is_base_column": bool, # True if column comes from first scenario only, False if repeated for all scenarios
35+
"units": str # Optional: Units to display in header (e.g., "($/kWh)", "(kW)")
36+
}
37+
38+
Note: Formatting (Excel number formats, column widths, colors) is handled in views.py, not in this configuration file.
39+
40+
Special Column Types:
41+
---------------------
42+
1. DateTime column: Must have key="datetime" and will be auto-generated based on year and time_steps_per_hour
43+
2. Base columns: Set is_base_column=True for columns that only use data from the first run_uuid
44+
3. Scenario columns: Set is_base_column=False for columns that repeat for each run_uuid
45+
46+
Rate Name Headers:
47+
------------------
48+
For scenario columns (is_base_column=False), the column header will automatically include the rate name
49+
from inputs.ElectricTariff.urdb_metadata.rate_name for each scenario.
50+
"""
51+
52+
# Configuration for energy and demand rate timeseries
53+
# This configuration specifies which data fields to extract from the results.
54+
# Formatting (number formats, colors, widths) is handled in views.py
55+
custom_timeseries_energy_demand = [
56+
{
57+
"label": "Date Timestep",
58+
"key": "datetime",
59+
"timeseries_path": lambda df: safe_get(df, "inputs.ElectricLoad.year"), # Used to generate datetime column based on year and time_steps_per_hour
60+
"is_base_column": True
61+
},
62+
{
63+
"label": "Load (kW)",
64+
"key": "load_kw",
65+
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricLoad.load_series_kw"),
66+
"is_base_column": True
67+
},
68+
{
69+
"label": "Peak Monthly Load (kW)",
70+
"key": "peak_monthly_load_kw",
71+
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricLoad.monthly_peaks_kw"), # 12-element array, needs special handling to repeat for each timestep
72+
"is_base_column": True
73+
},
74+
{
75+
"label": "Energy Charge",
76+
"key": "energy_charge",
77+
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricTariff.energy_rate_average_series"),
78+
"is_base_column": False, # Repeats for each scenario
79+
"units": "($/kWh)"
80+
},
81+
{
82+
"label": "Demand Charge",
83+
"key": "demand_charge",
84+
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricTariff.demand_rate_average_series"),
85+
"is_base_column": False, # Repeats for each scenario
86+
"units": "($/kW)"
87+
}
88+
]
89+
90+
# Example configuration for emissions timeseries (can be expanded as needed)
91+
custom_timeseries_emissions = [
92+
{
93+
"label": "Date Timestep",
94+
"key": "datetime",
95+
"timeseries_path": lambda df: safe_get(df, "inputs.ElectricLoad.year"),
96+
"is_base_column": True
97+
},
98+
{
99+
"label": "Grid Emissions",
100+
"key": "grid_emissions",
101+
"timeseries_path": lambda df: safe_get(df, "inputs.ElectricUtility.emissions_factor_series_lb_CO2_per_kwh"),
102+
"is_base_column": True,
103+
"units": "(lb CO2/kWh)"
104+
},
105+
{
106+
"label": "Grid Energy",
107+
"key": "grid_to_load",
108+
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricUtility.electric_to_load_series_kw"),
109+
"is_base_column": False,
110+
"units": "(kWh)"
111+
}
112+
]
113+
114+
# Example configuration for load profiles (can be expanded as needed)
115+
custom_timeseries_loads = [
116+
{
117+
"label": "Date Timestep",
118+
"key": "datetime",
119+
"timeseries_path": lambda df: safe_get(df, "inputs.ElectricLoad.year"),
120+
"is_base_column": True
121+
},
122+
{
123+
"label": "Total Load",
124+
"key": "total_load",
125+
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricLoad.load_series_kw"),
126+
"is_base_column": True,
127+
"units": "(kW)"
128+
},
129+
{
130+
"label": "Critical Load",
131+
"key": "critical_load",
132+
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricLoad.critical_load_series_kw"),
133+
"is_base_column": True,
134+
"units": "(kW)"
135+
}
136+
]

reoptjl/hourly_rate_helpers.py renamed to reoptjl/timeseries_table_helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# hourly_rate_helpers.py
1+
# timeseries_table_helpers.py
22
from typing import Dict, Any, List
33
from datetime import datetime, timedelta
44
import calendar

reoptjl/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@
3030
re_path(r'^summary_by_runuuids/?$', views.summary_by_runuuids),
3131
re_path(r'^link_run_to_portfolios/?$', views.link_run_uuids_to_portfolio_uuid),
3232
re_path(r'^get_load_metrics/?$', views.get_load_metrics),
33-
re_path(r'^job/hourly_rate_table/?$', views.hourly_rate_table)
33+
re_path(r'^job/get_timeseries_table/?$', views.get_timeseries_table)
3434
]

reoptjl/views.py

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2366,10 +2366,10 @@ def get_bau_column(col):
23662366
##############################################################################################################################
23672367

23682368
##############################################################################################################################
2369-
################################################# START Hourly Rate Table #####################################################
2369+
################################################# START Get Timeseries Table #####################################################
23702370
##############################################################################################################################
23712371

2372-
def hourly_rate_table(request: Any) -> HttpResponse:
2372+
def get_timeseries_table(request: Any) -> HttpResponse:
23732373
"""
23742374
Generate an Excel file with hourly rate data for one or more scenarios.
23752375
Accepts multiple run_uuid values via GET request parameters.
@@ -2382,7 +2382,7 @@ def hourly_rate_table(request: Any) -> HttpResponse:
23822382
- Column 5: Demand Charge from first run_uuid ($/kW)
23832383
- Columns 6-7, 8-9, etc.: Energy and Demand charges for additional run_uuids
23842384
"""
2385-
from reoptjl.hourly_rate_helpers import (
2385+
from reoptjl.timeseries_table_helpers import (
23862386
generate_datetime_column,
23872387
get_monthly_peak_for_timestep,
23882388
safe_get_list,
@@ -2439,9 +2439,9 @@ def hourly_rate_table(request: Any) -> HttpResponse:
24392439
monthly_peaks = safe_get_list(first_scenario, 'outputs.ElectricLoad.monthly_peaks_kw', [])
24402440

24412441
# Log for debugging
2442-
log.info(f"hourly_rate_table - year: {year}, time_steps_per_hour: {time_steps_per_hour}")
2443-
log.info(f"hourly_rate_table - load_series length: {len(load_series)}, monthly_peaks length: {len(monthly_peaks)}")
2444-
log.info(f"hourly_rate_table - datetime_col length: {len(datetime_col)}")
2442+
log.info(f"get_timeseries_table - year: {year}, time_steps_per_hour: {time_steps_per_hour}")
2443+
log.info(f"get_timeseries_table - load_series length: {len(load_series)}, monthly_peaks length: {len(monthly_peaks)}")
2444+
log.info(f"get_timeseries_table - datetime_col length: {len(datetime_col)}")
24452445

24462446
# Create monthly peak column (repeat monthly peak for all timesteps in that month)
24472447
monthly_peak_col = [
@@ -2465,6 +2465,31 @@ def hourly_rate_table(request: Any) -> HttpResponse:
24652465
'text_wrap': True
24662466
})
24672467

2468+
# Define different header colors for each rate
2469+
rate_header_colors = [
2470+
"#50AEE9", # Blue (first rate)
2471+
'#2E7D32', # Green (second rate)
2472+
'#D32F2F', # Red (third rate)
2473+
'#F57C00', # Orange (fourth rate)
2474+
'#7B1FA2', # Purple (fifth rate)
2475+
'#0097A7', # Cyan (sixth rate)
2476+
'#C2185B', # Pink (seventh rate)
2477+
'#5D4037', # Brown (eighth rate)
2478+
]
2479+
2480+
# Create header formats for each rate
2481+
rate_header_formats = []
2482+
for color in rate_header_colors:
2483+
rate_header_formats.append(workbook.add_format({
2484+
'bold': True,
2485+
'bg_color': color,
2486+
'font_color': 'white',
2487+
'border': 1,
2488+
'align': 'center',
2489+
'valign': 'vcenter',
2490+
'text_wrap': True
2491+
}))
2492+
24682493
datetime_format = workbook.add_format({
24692494
'border': 1,
24702495
'align': 'center',
@@ -2523,8 +2548,12 @@ def hourly_rate_table(request: Any) -> HttpResponse:
25232548
# Get rate name from urdb_metadata
25242549
rate_name = safe_get_value(scenario['data'], 'inputs.ElectricTariff.urdb_metadata.rate_name', f'Scenario {scenario_idx + 1}')
25252550

2526-
worksheet.write(0, col_offset, f'Energy Charge {rate_name} ($/kWh)', header_format)
2527-
worksheet.write(0, col_offset + 1, f'Demand Charge {rate_name} ($/kW)', header_format)
2551+
# Use different colored header for each rate (cycle through colors if more than 8 rates)
2552+
rate_header = rate_header_formats[scenario_idx % len(rate_header_formats)]
2553+
2554+
# Add a return after "Charge"
2555+
worksheet.write(0, col_offset, f'Energy Charge: \n{rate_name} ($/kWh)', rate_header)
2556+
worksheet.write(0, col_offset + 1, f'Demand Charge: \n{rate_name} ($/kW)', rate_header)
25282557
col_offset += 2
25292558

25302559
# Write data rows
@@ -2549,7 +2578,7 @@ def hourly_rate_table(request: Any) -> HttpResponse:
25492578

25502579
# Log on first row for debugging
25512580
if row_idx == 0:
2552-
log.info(f"hourly_rate_table - scenario {scenario_idx}: energy_rates length: {len(energy_rates)}, demand_rates length: {len(demand_rates)}")
2581+
log.info(f"get_timeseries_table - scenario {scenario_idx}: energy_rates length: {len(energy_rates)}, demand_rates length: {len(demand_rates)}")
25532582

25542583
energy_rate = energy_rates[row_idx] if row_idx < len(energy_rates) else 0
25552584
demand_rate = demand_rates[row_idx] if row_idx < len(demand_rates) else 0
@@ -2571,13 +2600,13 @@ def hourly_rate_table(request: Any) -> HttpResponse:
25712600
output,
25722601
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
25732602
)
2574-
response['Content-Disposition'] = 'attachment; filename="hourly_rate_table.xlsx"'
2603+
response['Content-Disposition'] = 'attachment; filename="get_timeseries_table.xlsx"'
25752604
return response
25762605

25772606
except Exception as e:
2578-
log.error(f"Error in hourly_rate_table: {e}")
2607+
log.error(f"Error in get_timeseries_table: {e}")
25792608
return JsonResponse({"Error": f"An unexpected error occurred: {str(e)}"}, status=500)
25802609

25812610
##############################################################################################################################
2582-
################################################### END Hourly Rate Table #####################################################
2611+
################################################### END Get Timeseries Table #####################################################
25832612
##############################################################################################################################

0 commit comments

Comments
 (0)