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 <noreply@anthropic.com>
This commit is contained in:
parent
dba1e6990f
commit
ff446bc34a
2 changed files with 32 additions and 8 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue