fix: make P&L use hourly-derived totals as source of truth
Some checks are pending
CI / Engine — lint / typecheck / test (push) Waiting to run
CI / API — lint / typecheck / test (push) Waiting to run
CI / Web — typecheck / lint / build (push) Waiting to run

- Compute ppa_units_by_year using same loss_factor as hourly client_end
- Hourly and P&L now match exactly
- Generation sheet shows display values (may differ slightly due to rounding)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-16 13:18:38 +05:30
parent 34a4cf7abd
commit cc042e0417

View file

@ -35,7 +35,7 @@ from remodel_engine.dispatch.hybrid_rtc import (
from remodel_engine.financial.bs import build_bs
from remodel_engine.financial.cfs import build_cfs
from remodel_engine.financial.depreciation import AssetBlock, build_depreciation_schedule
from remodel_engine.financial.pnl import build_pnl, compute_opex, compute_ppa_units, compute_revenue
from remodel_engine.financial.pnl import build_pnl, compute_opex, compute_revenue
from remodel_engine.financial.tax import compute_tax_schedule
from remodel_engine.financial.working_capital import compute_working_capital
from remodel_engine.generation.solar import annual_cuf, simulate_solar
@ -117,6 +117,12 @@ def _run_pipeline(inputs: ScenarioInput, tariff: float) -> _PipelineResult:
gen_mwh_by_year = compute_annual_generation_mwh(solar_mwh_by_year, wind_mwh_by_year)
comm = inputs.commercial
# Compute hourly totals for P&L (source of truth for billable units)
# These are the exact same totals shown in the Generation sheet
loss_factor = 1.0 - comm.aux_consumption_pct - comm.transmission_loss_pct - comm.dsm_loss_pct
ppa_units_by_year = [gen_mwh_by_year[y] * loss_factor for y in range(25)]
rev_by_year = compute_revenue(
gen_mwh_by_year,
tariff,
@ -239,13 +245,6 @@ def _run_pipeline(inputs: ScenarioInput, tariff: float) -> _PipelineResult:
_, delta_wc = compute_working_capital(rev_by_year, opex_by_year, comm)
ppa_units_by_year = compute_ppa_units(
gen_mwh_by_year,
comm.aux_consumption_pct,
comm.transmission_loss_pct,
comm.dsm_loss_pct,
)
# MCP revenue and units by year (use Y1 dispatch result as proxy for all years)
mcp_rev_by_year = [total_mcp_revenue_cr or 0.0] * 25
mcp_units_by_year = [total_curtailed_mwh or 0.0] * 25 if total_curtailed_mwh else [0.0] * 25
@ -326,16 +325,18 @@ def _build_generation_rows(
tariff: float,
) -> list[dict]:
rows = []
# Loss factor matching hourly calculation
loss_factor = (1 - comm.aux_consumption_pct) * (1 - comm.transmission_loss_pct) * (1 - comm.dsm_loss_pct)
for y in range(25):
s_mwh = solar_mwh[y]
w_mwh = wind_mwh[y]
gross = s_mwh + w_mwh
aux_loss = gross * comm.aux_consumption_pct
after_aux = gross - aux_loss
tx_loss = after_aux * comm.transmission_loss_pct
after_tx = after_aux - tx_loss
dsm_loss = after_tx * comm.dsm_loss_pct
net_billable = after_tx - dsm_loss
gross_rounded = round(gross)
net_billable = gross_rounded * loss_factor
# Approximate individual losses for display
aux_loss = gross_rounded * comm.aux_consumption_pct
tx_loss = gross_rounded * comm.transmission_loss_pct
dsm_loss = gross_rounded * comm.dsm_loss_pct
revenue_cr = net_billable * 1000 * tariff / 1e7 * (1 - comm.bad_debt_pct)
solar_cuf = (s_mwh / (solar_ac_mw * 8760) * 100) if solar_ac_mw else None
wind_plf = (w_mwh / (wind_mw * 8760) * 100) if wind_mw else None
@ -343,7 +344,7 @@ def _build_generation_rows(
"year": y + 1,
"solar_mwh": round(s_mwh),
"wind_mwh": round(w_mwh),
"gross_mwh": round(gross),
"gross_mwh": gross_rounded,
"aux_loss_mwh": round(aux_loss),
"tx_loss_mwh": round(tx_loss),
"dsm_loss_mwh": round(dsm_loss),