From ff446bc34a4de6e2725ef24529d8fa25a2439eb5 Mon Sep 17 00:00:00 2001 From: Mannu Date: Sat, 16 May 2026 14:36:25 +0530 Subject: [PATCH] feat: add raw profile data for Solar 8760 column - 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 --- .../src/remodel_engine/catalog/loader.py | 9 ++++++ .../src/remodel_engine/scenarios/runner.py | 31 ++++++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/engine/src/remodel_engine/catalog/loader.py b/packages/engine/src/remodel_engine/catalog/loader.py index b9c753d..6f13aeb 100644 --- a/packages/engine/src/remodel_engine/catalog/loader.py +++ b/packages/engine/src/remodel_engine/catalog/loader.py @@ -115,3 +115,12 @@ def load_wind_profile(location_id: str) -> np.ndarray: if data.shape[0] != 8760: raise ValueError(f"Wind profile {path} has {data.shape[0]} rows, expected 8760") 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) diff --git a/packages/engine/src/remodel_engine/scenarios/runner.py b/packages/engine/src/remodel_engine/scenarios/runner.py index b9b1e40..bc30a1e 100644 --- a/packages/engine/src/remodel_engine/scenarios/runner.py +++ b/packages/engine/src/remodel_engine/scenarios/runner.py @@ -13,7 +13,7 @@ Pipeline order: from __future__ import annotations 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.idc import compute_idc @@ -72,12 +72,15 @@ class _PipelineResult: total_curtailed_mwh: float | None = None total_mcp_revenue_cr: float | None = None # Hourly generation: 25 years × 8760 hours = 219,000 values - solar_hourly: list[float] = None # type: ignore[assignment] - wind_hourly: list[float] = None # type: ignore[assignment] + solar_hourly: list[float] = field(default_factory=list) # type: ignore[assignment] + wind_hourly: list[float] = field(default_factory=list) # type: ignore[assignment] # Capacity values for hourly display solar_dc_mwp: float = 0.0 wind_mw: float = 0.0 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: @@ -93,6 +96,9 @@ def _run_pipeline(inputs: ScenarioInput, tariff: float) -> _PipelineResult: solar_dc_mwp = 0.0 solar_ac_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: 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_dc_mwp = inputs.solar.capacity_dc_mwp 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 for year in range(25): 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_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 for year in range(25): 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_ac_mw=solar_ac_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 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 + # 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 hourly_total_re.append(total_re) hourly_client_end.append(total_re * loss_factor) hourly_load.append(client_load_mw) - # Profile data: actual generation / capacity = normalized profile value - # Store: for solar = solar_val (which is DC output), for wind = wind_val - # User can compute: profile = solar_val / solar_dc_mwp - hourly_solar_profile.append(solar_val) - hourly_wind_profile.append(wind_val) + # Raw profile (0-1) for Solar 8760 column, final output for Solar MW column + hourly_solar_profile.append(solar_profile_val) + hourly_wind_profile.append(wind_profile_val) return ScenarioResult( inputs=inputs,