feat: add raw profile data for Solar 8760 column
Some checks are pending
CI / Engine — lint / typecheck / test (push) Waiting to run
CI / API — lint / typecheck / test (push) Waiting to run
CI / Web — typecheck / lint / build (push) Waiting to run

- Add load_wind_profile_25y to loader.py
- Store raw profile (0-1 normalized) in hourly_solar_profile
- Solar 8760 shows raw profile values from CSV file
- Compute averages at day/month/year levels from profile

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-16 14:36:25 +05:30
parent dba1e6990f
commit ff446bc34a
2 changed files with 32 additions and 8 deletions

View file

@ -115,3 +115,12 @@ def load_wind_profile(location_id: str) -> np.ndarray:
if data.shape[0] != 8760: if data.shape[0] != 8760:
raise ValueError(f"Wind profile {path} has {data.shape[0]} rows, expected 8760") raise ValueError(f"Wind profile {path} has {data.shape[0]} rows, expected 8760")
return data.astype(np.float64) return data.astype(np.float64)
def load_wind_profile_25y(location_id: str) -> np.ndarray:
"""Return 25-year wind profile array: (25 * 8760,) = (219000,).
Uses the 8760-hour profile expanded with leap year handling.
"""
source = load_wind_profile(location_id)
return _expand_to_25_years(source)

View file

@ -13,7 +13,7 @@ Pipeline order:
from __future__ import annotations from __future__ import annotations
import time import time
from dataclasses import dataclass from dataclasses import dataclass, field
from remodel_engine.capex.cost_items import ProjectCapacity, compute_capex from remodel_engine.capex.cost_items import ProjectCapacity, compute_capex
from remodel_engine.capex.idc import compute_idc from remodel_engine.capex.idc import compute_idc
@ -72,12 +72,15 @@ class _PipelineResult:
total_curtailed_mwh: float | None = None total_curtailed_mwh: float | None = None
total_mcp_revenue_cr: float | None = None total_mcp_revenue_cr: float | None = None
# Hourly generation: 25 years × 8760 hours = 219,000 values # Hourly generation: 25 years × 8760 hours = 219,000 values
solar_hourly: list[float] = None # type: ignore[assignment] solar_hourly: list[float] = field(default_factory=list) # type: ignore[assignment]
wind_hourly: list[float] = None # type: ignore[assignment] wind_hourly: list[float] = field(default_factory=list) # type: ignore[assignment]
# Capacity values for hourly display # Capacity values for hourly display
solar_dc_mwp: float = 0.0 solar_dc_mwp: float = 0.0
wind_mw: float = 0.0 wind_mw: float = 0.0
solar_ac_mw: float = 0.0 # type: ignore[assignment] solar_ac_mw: float = 0.0 # type: ignore[assignment]
# Raw profile data (0-1 normalized values from CSV)
solar_profile_25y: list[float] = field(default_factory=list) # type: ignore[assignment]
wind_profile_25y: list[float] = field(default_factory=list) # type: ignore[assignment]
def _run_pipeline(inputs: ScenarioInput, tariff: float) -> _PipelineResult: def _run_pipeline(inputs: ScenarioInput, tariff: float) -> _PipelineResult:
@ -93,6 +96,9 @@ def _run_pipeline(inputs: ScenarioInput, tariff: float) -> _PipelineResult:
solar_dc_mwp = 0.0 solar_dc_mwp = 0.0
solar_ac_mw = 0.0 solar_ac_mw = 0.0
wind_mw = 0.0 wind_mw = 0.0
# Raw profile data (0-1 normalized)
solar_profile_25y: list[float] = []
wind_profile_25y: list[float] = []
if inputs.solar is not None: if inputs.solar is not None:
sol_df = simulate_solar(inputs.solar) sol_df = simulate_solar(inputs.solar)
@ -103,6 +109,9 @@ def _run_pipeline(inputs: ScenarioInput, tariff: float) -> _PipelineResult:
solar_y1_cuf = float(annual_cuf(sol_df, inputs.solar.capacity_ac_mw).iloc[0]) solar_y1_cuf = float(annual_cuf(sol_df, inputs.solar.capacity_ac_mw).iloc[0])
solar_dc_mwp = inputs.solar.capacity_dc_mwp solar_dc_mwp = inputs.solar.capacity_dc_mwp
solar_ac_mw = inputs.solar.capacity_ac_mw solar_ac_mw = inputs.solar.capacity_ac_mw
# Get raw profile (normalized 0-1) from loader
from remodel_engine.catalog.loader import load_solar_profile_25y
solar_profile_25y = load_solar_profile_25y(inputs.solar.location_id).tolist()
# Store all 25 years hourly - convert DataFrame to flat list # Store all 25 years hourly - convert DataFrame to flat list
for year in range(25): for year in range(25):
year_data = sol_df[sol_df["year"] == year + 1]["ac_power_mw"].tolist() year_data = sol_df[sol_df["year"] == year + 1]["ac_power_mw"].tolist()
@ -118,6 +127,9 @@ def _run_pipeline(inputs: ScenarioInput, tariff: float) -> _PipelineResult:
] ]
wind_y1_plf = float(annual_plf(wnd_df, inputs.wind.capacity_mw).iloc[0]) wind_y1_plf = float(annual_plf(wnd_df, inputs.wind.capacity_mw).iloc[0])
wind_mw = inputs.wind.capacity_mw wind_mw = inputs.wind.capacity_mw
# Get raw profile from loader
from remodel_engine.catalog.loader import load_wind_profile_25y
wind_profile_25y = load_wind_profile_25y(inputs.wind.location_id).tolist()
# Store all 25 years hourly - convert DataFrame to flat list # Store all 25 years hourly - convert DataFrame to flat list
for year in range(25): for year in range(25):
year_data = wnd_df[wnd_df["year"] == year + 1]["ac_power_mw"].tolist() year_data = wnd_df[wnd_df["year"] == year + 1]["ac_power_mw"].tolist()
@ -327,6 +339,8 @@ def _run_pipeline(inputs: ScenarioInput, tariff: float) -> _PipelineResult:
solar_dc_mwp=solar_dc_mwp, solar_dc_mwp=solar_dc_mwp,
solar_ac_mw=solar_ac_mw, solar_ac_mw=solar_ac_mw,
wind_mw=wind_mw, wind_mw=wind_mw,
solar_profile_25y=solar_profile_25y,
wind_profile_25y=wind_profile_25y,
) )
@ -557,15 +571,16 @@ def run_scenario(inputs: ScenarioInput) -> ScenarioResult:
idx = year * 8760 + hour_idx idx = year * 8760 + hour_idx
solar_val = pipe.solar_hourly[idx] if pipe.solar_hourly and idx < len(pipe.solar_hourly) else 0.0 solar_val = pipe.solar_hourly[idx] if pipe.solar_hourly and idx < len(pipe.solar_hourly) else 0.0
wind_val = pipe.wind_hourly[idx] if pipe.wind_hourly and idx < len(pipe.wind_hourly) else 0.0 wind_val = pipe.wind_hourly[idx] if pipe.wind_hourly and idx < len(pipe.wind_hourly) else 0.0
# Raw profile values from CSV (0-1 normalized)
solar_profile_val = pipe.solar_profile_25y[idx] if pipe.solar_profile_25y and idx < len(pipe.solar_profile_25y) else 0.0
wind_profile_val = pipe.wind_profile_25y[idx] if pipe.wind_profile_25y and idx < len(pipe.wind_profile_25y) else 0.0
total_re = solar_val + wind_val total_re = solar_val + wind_val
hourly_total_re.append(total_re) hourly_total_re.append(total_re)
hourly_client_end.append(total_re * loss_factor) hourly_client_end.append(total_re * loss_factor)
hourly_load.append(client_load_mw) hourly_load.append(client_load_mw)
# Profile data: actual generation / capacity = normalized profile value # Raw profile (0-1) for Solar 8760 column, final output for Solar MW column
# Store: for solar = solar_val (which is DC output), for wind = wind_val hourly_solar_profile.append(solar_profile_val)
# User can compute: profile = solar_val / solar_dc_mwp hourly_wind_profile.append(wind_profile_val)
hourly_solar_profile.append(solar_val)
hourly_wind_profile.append(wind_val)
return ScenarioResult( return ScenarioResult(
inputs=inputs, inputs=inputs,