- 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>
176 lines
5.7 KiB
Python
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()),
|
|
}
|