Remodel/packages/engine/src/remodel_engine/catalog/phasing.py
Mannu e6dc39aa33 [S1-T12/T13] P&L revenue breakdown + collapsible rows + UI polish
- Engine: Add ppa_revenue_cr, mcp_revenue_cr, tariff, units to PnLRow
- Engine: Split PPA vs MCP revenue in P&L computation
- Web: Collapsible rows for PPA/MCP Revenue and Opex
- Web: Highlighted rows (Total Revenue, EBITDA, EBIT, PBT, PAT)
- Web: Units above Tariff in breakdown, bg-blue-50 highlight
- Fix sticky column z-index for horizontal scroll
- CLAUDE.md: Add project documentation

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 10:42:36 +05:30

176 lines
5.7 KiB
Python

"""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()),
}