"""Default phasing templates and drawdown curves. All phasing curves have monthly_pct summing to 1.0. All drawdown curves end at cum_pct[-1] == 1.0. """ from remodel_engine.schemas.capex import DrawdownCurve, PhasingCurve # --------------------------------------------------------------------------- # Phasing templates # --------------------------------------------------------------------------- def _even_phasing(n_months: int, id_: str, name: str) -> PhasingCurve: pct = 1.0 / n_months monthly = [round(pct, 8)] * n_months # Fix rounding: ensure exact sum of 1.0 monthly[-1] = round(1.0 - sum(monthly[:-1]), 8) return PhasingCurve(id=id_, name=name, monthly_pct=monthly) def _weighted_phasing( weights: list[float], id_: str, name: str ) -> PhasingCurve: total = sum(weights) monthly = [round(w / total, 8) for w in weights] monthly[-1] = round(1.0 - sum(monthly[:-1]), 8) return PhasingCurve(id=id_, name=name, monthly_pct=monthly) # 18-month: slow ramp (m1-6), peak (m7-15), final close (m16-18) _SOLAR_18MO_WEIGHTS = [ *[3, 4, 5, 6, 7, 8], # months 1-6 *[10, 10, 10, 10, 9, 8, 7, 6, 5], # months 7-15 *[4, 4, 4], # months 16-18 ] SOLAR_STANDARD_18MO = _weighted_phasing( _SOLAR_18MO_WEIGHTS, "solar_standard_18mo", "Solar Standard 18-month" ) # 24-month wind: procurement heavy in m3-12, installation m12-22, commissioning m23-24 _WIND_24MO_WEIGHTS = [ *[2, 3], # months 1-2: site prep *[7, 8, 8, 8, 7, 7, 7, 6], # months 3-10: procurement + civil *[6, 5, 5, 4, 4, 4, 4], # months 11-17: erection *[3, 3, 3, 2, 2, 1], # months 18-23: commissioning *[1], # month 24: final ] WIND_STANDARD_24MO = _weighted_phasing( _WIND_24MO_WEIGHTS, "wind_standard_24mo", "Wind Standard 24-month" ) # 36-month hybrid RTC: back-loaded for BESS and common infrastructure _HYBRID_36MO_WEIGHTS = [ *[1, 1, 2], # months 1-3: early mobilization *[3, 4, 4, 4, 4, 4], # months 4-9: civil + procurement *[5, 5, 5, 5, 4, 4], # months 10-15: peak spend *[4, 4, 3, 3, 3, 3], # months 16-21: installation *[3, 3, 3, 3, 3, 3], # months 22-27: BESS + commissioning *[3, 3, 3, 2, 2, 1], # months 28-33: balance *[1, 1, 1], # months 34-36: final retention ] HYBRID_RTC_36MO = _weighted_phasing( _HYBRID_36MO_WEIGHTS, "hybrid_rtc_36mo", "Hybrid RTC 36-month" ) PHASING_TEMPLATES: dict[str, PhasingCurve] = { "solar_standard_18mo": SOLAR_STANDARD_18MO, "wind_standard_24mo": WIND_STANDARD_24MO, "hybrid_rtc_36mo": HYBRID_RTC_36MO, } # --------------------------------------------------------------------------- # Drawdown curves # --------------------------------------------------------------------------- def _equity_curve_18mo() -> DrawdownCurve: """Solar 18-month equity drawdown: front-loaded, reaches ~105% by m12 then normalizes.""" cum = [ 0.10, 0.20, 0.30, 0.42, 0.54, 0.65, 0.75, 0.85, 0.92, 0.98, 1.02, 1.05, 1.05, 1.03, 1.01, 1.00, 1.00, 1.00, ] return DrawdownCurve( id="equity_solar_18mo", name="Equity Solar 18-month (bridge to 105%)", cum_pct=cum, allow_bridge=True, ) def _debt_curve_18mo() -> DrawdownCurve: """Solar 18-month debt drawdown: starts after equity bridge begins unwinding.""" cum = [ 0.00, 0.00, 0.00, 0.00, 0.02, 0.07, 0.14, 0.25, 0.37, 0.50, 0.62, 0.72, 0.82, 0.90, 0.96, 1.00, 1.00, 1.00, ] return DrawdownCurve( id="debt_solar_18mo", name="Debt Solar 18-month", cum_pct=cum, allow_bridge=False, ) def _equity_curve_24mo() -> DrawdownCurve: """Wind 24-month equity drawdown.""" cum = [ 0.08, 0.16, 0.24, 0.33, 0.42, 0.51, 0.60, 0.70, 0.78, 0.85, 0.92, 0.98, 1.02, 1.05, 1.05, 1.03, 1.01, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, ] return DrawdownCurve( id="equity_wind_24mo", name="Equity Wind 24-month (bridge to 105%)", cum_pct=cum, allow_bridge=True, ) def _debt_curve_24mo() -> DrawdownCurve: """Wind 24-month debt drawdown.""" cum = [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.03, 0.08, 0.16, 0.25, 0.35, 0.46, 0.57, 0.67, 0.75, 0.83, 0.90, 0.95, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, ] return DrawdownCurve( id="debt_wind_24mo", name="Debt Wind 24-month", cum_pct=cum, allow_bridge=False, ) def _equity_curve_36mo() -> DrawdownCurve: """Hybrid RTC 36-month equity drawdown.""" cum_24 = [ 0.05, 0.10, 0.16, 0.22, 0.28, 0.35, 0.42, 0.50, 0.57, 0.64, 0.71, 0.78, 0.85, 0.90, 0.95, 1.00, 1.03, 1.05, 1.05, 1.04, 1.03, 1.02, 1.01, 1.00, ] tail = [1.00] * 12 return DrawdownCurve( id="equity_hybrid_36mo", name="Equity Hybrid RTC 36-month (bridge to 105%)", cum_pct=cum_24 + tail, allow_bridge=True, ) def _debt_curve_36mo() -> DrawdownCurve: """Hybrid RTC 36-month debt drawdown.""" cum_24 = [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.02, 0.05, 0.10, 0.17, 0.25, 0.34, 0.44, 0.54, 0.62, 0.70, 0.78, 0.85, 0.90, 0.94, 0.97, 0.99, 1.00, 1.00, 1.00, ] tail = [1.00] * 12 return DrawdownCurve( id="debt_hybrid_36mo", name="Debt Hybrid RTC 36-month", cum_pct=cum_24 + tail, allow_bridge=False, ) DRAWDOWN_TEMPLATES: dict[str, tuple[DrawdownCurve, DrawdownCurve]] = { "solar_standard_18mo": (_equity_curve_18mo(), _debt_curve_18mo()), "wind_standard_24mo": (_equity_curve_24mo(), _debt_curve_24mo()), "hybrid_rtc_36mo": (_equity_curve_36mo(), _debt_curve_36mo()), }