from __future__ import annotations from datetime import datetime, timedelta, timezone from gibil.classes.models import ForecastKind, PowerForecastPoint, PowerForecastRun from gibil.classes.oracle.config import EnergyForecastConfig from gibil.classes.sigen.store import SigenStore class BaselineUsageOracle: """Forecasts near-future load from recent high-resolution Sigen history.""" model_version = "baseline_recent_load_v1" def __init__( self, sigen_store: SigenStore, config: EnergyForecastConfig, ) -> None: self.sigen_store = sigen_store self.config = config def forecast( self, target_times: list[datetime], issued_at: datetime | None = None, ) -> PowerForecastRun: if issued_at is None: issued_at = datetime.now(timezone.utc) lookback = timedelta(minutes=self.config.load_lookback_minutes) summary = self.sigen_store.load_recent_power_summary(lookback=lookback) latest = self.sigen_store.load_latest_snapshot() fallback_load_w = latest.load_power_w if latest else 0.0 p50 = self._number(summary.get("load_p50_w"), fallback_load_w) p10 = max(0.0, self._number(summary.get("load_p10_w"), p50 * 0.7)) p90 = max( self._number(summary.get("load_p90_w"), p50 * 1.5), p50 * 1.25, ) points = [ PowerForecastPoint( target_at=target_at, horizon_minutes=max( 0, round((target_at - issued_at).total_seconds() / 60) ), expected_power_w=p50, p10_power_w=p10, p50_power_w=p50, p90_power_w=p90, confidence=0.35, source="recent_sigen_load", model_version=self.model_version, metadata={ "lookback_minutes": self.config.load_lookback_minutes, "load_avg_w": summary.get("load_avg_w"), "load_max_w": summary.get("load_max_w"), }, ) for target_at in target_times ] return PowerForecastRun( issued_at=issued_at, kind=ForecastKind.LOAD, source="baseline_usage_oracle", model_version=self.model_version, points=points, ) def _number(self, value: object, fallback: float) -> float: if value is None: return float(fallback) return float(value)