Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions reoptjl/custom_timeseries_table_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# custom_timeseries_table_config.py
from reoptjl.timeseries_table_helpers import safe_get_list, safe_get_value, safe_get

"""
Timeseries Table Configuration
===============================
This file defines configurations for timeseries Excel tables that display hourly or sub-hourly data.
Each configuration specifies which columns to include and how to extract the data.

Naming Convention:
------------------
Structure: custom_timeseries_<feature>

- `custom_timeseries_`: Prefix indicating a timeseries table configuration
- `<feature>`: Descriptive name for the specific timeseries configuration

Examples:
- custom_timeseries_energy_demand: Configuration for energy and demand rate timeseries
- custom_timeseries_emissions: Configuration for emissions timeseries
- custom_timeseries_loads: Configuration for load profiles

Guidelines:
- Use lowercase letters and underscores
- Keep names descriptive and concise
- Each configuration is a list of column dictionaries

Column Dictionary Structure:
-----------------------------
Each column configuration should have:
{
"label": str, # Column header text
"key": str, # Unique identifier for the column
"timeseries_path": str, # Dot-separated path to data in the results JSON (e.g., "outputs.ElectricLoad.load_series_kw")
"is_base_column": bool, # True if column comes from first scenario only, False if repeated for all scenarios
"units": str # Optional: Units to display in header (e.g., "($/kWh)", "(kW)")
}

Note: Formatting (Excel number formats, column widths, colors) is handled in views.py, not in this configuration file.

Special Column Types:
---------------------
1. DateTime column: Must have key="datetime" and will be auto-generated based on year and time_steps_per_hour
2. Base columns: Set is_base_column=True for columns that only use data from the first run_uuid
3. Scenario columns: Set is_base_column=False for columns that repeat for each run_uuid

Rate Name Headers:
------------------
For scenario columns (is_base_column=False), the column header will automatically include the rate name
from inputs.ElectricTariff.urdb_metadata.rate_name for each scenario.
"""

# Configuration for energy and demand rate timeseries
# This configuration specifies which data fields to extract from the results.
# Formatting (number formats, colors, widths) is handled in views.py
custom_timeseries_energy_demand = [
{
"label": "Date Timestep",
"key": "datetime",
"timeseries_path": lambda df: safe_get(df, "inputs.ElectricLoad.year"), # Used to generate datetime column based on year and time_steps_per_hour
"is_base_column": True
},
{
"label": "Load (kW)",
"key": "load_kw",
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricLoad.load_series_kw"),
"is_base_column": True
},
{
"label": "Peak Monthly Load (kW)",
"key": "peak_monthly_load_kw",
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricLoad.monthly_peaks_kw"), # 12-element array, needs special handling to repeat for each timestep
"is_base_column": True
},
{
"label": "Energy Charge",
"key": "energy_charge",
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricTariff.energy_rate_average_series"),
"is_base_column": False, # Repeats for each scenario
"units": "($/kWh)"
},
{
"label": "Demand Charge",
"key": "demand_charge",
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricTariff.demand_rate_average_series"),
"is_base_column": False, # Repeats for each scenario
"units": "($/kW)"
}
]

# Example configuration for emissions timeseries (can be expanded as needed)
custom_timeseries_emissions = [
{
"label": "Date Timestep",
"key": "datetime",
"timeseries_path": lambda df: safe_get(df, "inputs.ElectricLoad.year"),
"is_base_column": True
},
{
"label": "Grid Emissions",
"key": "grid_emissions",
"timeseries_path": lambda df: safe_get(df, "inputs.ElectricUtility.emissions_factor_series_lb_CO2_per_kwh"),
"is_base_column": True,
"units": "(lb CO2/kWh)"
},
{
"label": "Grid Energy",
"key": "grid_to_load",
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricUtility.electric_to_load_series_kw"),
"is_base_column": False,
"units": "(kWh)"
}
]

# Example configuration for load profiles (can be expanded as needed)
custom_timeseries_loads = [
{
"label": "Date Timestep",
"key": "datetime",
"timeseries_path": lambda df: safe_get(df, "inputs.ElectricLoad.year"),
"is_base_column": True
},
{
"label": "Total Load",
"key": "total_load",
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricLoad.load_series_kw"),
"is_base_column": True,
"units": "(kW)"
},
{
"label": "Critical Load",
"key": "critical_load",
"timeseries_path": lambda df: safe_get(df, "outputs.ElectricLoad.critical_load_series_kw"),
"is_base_column": True,
"units": "(kW)"
}
]
132 changes: 132 additions & 0 deletions reoptjl/timeseries_table_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# timeseries_table_helpers.py
from typing import Dict, Any, List
from datetime import datetime, timedelta
import calendar

def generate_datetime_column(year: int, time_steps_per_hour: int) -> List[str]:
"""
Generate datetime strings for the first column based on year and time_steps_per_hour.

Args:
year: The year for the datetime series
time_steps_per_hour: Number of time steps per hour (1, 2, or 4)

Returns:
List of datetime strings formatted as "M/D/YYYY H:MM"
"""
# Check if leap year and adjust days accordingly
is_leap = calendar.isleap(year)
total_days = 365 # Always use 365 days, even for leap years

# Calculate time step increment in minutes
minutes_per_step = 60 // time_steps_per_hour

datetime_list = []
start_date = datetime(year, 1, 1, 0, 0)

# Calculate total number of time steps
total_steps = total_days * 24 * time_steps_per_hour

for step in range(total_steps):
current_time = start_date + timedelta(minutes=step * minutes_per_step)
# Format: M/D/YYYY H:MM (Windows-compatible formatting)
month = current_time.month
day = current_time.day
year = current_time.year
hour = current_time.hour
minute = current_time.minute
formatted_time = f"{month}/{day}/{year} {hour}:{minute:02d}"
datetime_list.append(formatted_time)

return datetime_list


def get_monthly_peak_for_timestep(timestep_index: int, monthly_peaks: List[float], time_steps_per_hour: int) -> float:
"""
Get the monthly peak value for a given timestep index.

Args:
timestep_index: The index of the current timestep
monthly_peaks: List of 12 monthly peak values
time_steps_per_hour: Number of time steps per hour

Returns:
The monthly peak value for the month containing this timestep
"""
# Calculate which month this timestep belongs to
# Approximate days per month
days_in_months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

steps_per_day = 24 * time_steps_per_hour
cumulative_steps = 0

for month_idx, days in enumerate(days_in_months):
cumulative_steps += days * steps_per_day
if timestep_index < cumulative_steps:
return monthly_peaks[month_idx] if month_idx < len(monthly_peaks) else 0

# Default to last month if we're beyond December
return monthly_peaks[-1] if monthly_peaks else 0


def safe_get_list(data: Dict[str, Any], key: str, default: List = None) -> List:
"""
Safely get a list value from nested dictionary.

Args:
data: The dictionary to search
key: Dot-separated key path (e.g., "outputs.ElectricLoad.load_series_kw")
default: Default value if key not found

Returns:
The found list or default value
"""
if default is None:
default = []

keys = key.split('.')
current = data

try:
for k in keys:
if isinstance(current, dict):
current = current.get(k)
else:
return default

if current is None:
return default

return current if isinstance(current, list) else default
except (KeyError, TypeError, AttributeError):
return default


def safe_get_value(data: Dict[str, Any], key: str, default: Any = None) -> Any:
"""
Safely get a value from nested dictionary.

Args:
data: The dictionary to search
key: Dot-separated key path
default: Default value if key not found

Returns:
The found value or default
"""
keys = key.split('.')
current = data

try:
for k in keys:
if isinstance(current, dict):
current = current.get(k)
else:
return default

if current is None:
return default

return current
except (KeyError, TypeError, AttributeError):
return default
3 changes: 2 additions & 1 deletion reoptjl/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@
re_path(r'^pv_cost_defaults/?$', views.pv_cost_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),
re_path(r'^get_load_metrics/?$', views.get_load_metrics)
re_path(r'^get_load_metrics/?$', views.get_load_metrics),
re_path(r'^job/get_timeseries_table/?$', views.get_timeseries_table)
]
Loading