Finalises the handover doc after S0380.49 ships the effective-monthly Table 12e PE factor for the PV split. Full cohort residual trajectory table across all four milestones (pre-44 / post-45 / post-48 / post-49), final cross-cascade architecture diagram, and the punch-list of open work (β fine-tuning, HP electricity demand, monthly E_PV distribution) — none in the β-split phase scope, each a candidate follow-up slice. Cluster PE residual closed by ~50% magnitude over the phase: -7..-14 → -2.8..-3.7 kWh/m². CO2 all <0.11 t/yr; SAP all exact.
7 KiB
Handover — PV β-factor split (6/6 COMPLETE)
Branch feature/per-cert-mapper-validation. This phase shipped
6 slices (S0380.44 → S0380.49) that implemented and wired the
full SAP 10.2 Appendix M1 β-factor across PE, CO2, and Cost cascades,
surfaced the real-API battery capacity, and wired effective-monthly
Table 12e PE factors for the PV split.
HEAD at handover end: e75198ce (Slice S0380.49).
Test suite: 763 pass + 0 fail.
Slices shipped this phase
| Slice | Commit | What | Spec |
|---|---|---|---|
| S0380.44 | 5344bc89 |
New module worksheet/photovoltaic.py with pv_split_monthly, pv_beta_coefficients, PhotovoltaicSplit + 13 unit tests |
Appendix M1 §3c-d (p.94), §4 (p.94) |
| S0380.45 | 49de18e8 |
Wired β-split into PE cascade | Appendix M1 §3a (p.93), §8 (p.94) |
| S0380.46 | 5b269f23 |
Wired β-split into CO2 cascade | Appendix M1 §7 (p.94), Table 12d code 60 |
| S0380.47 | 42ed38f7 |
Wired β-split into cost cascade (zero cohort impact — Table 32 collapses code 30 = code 60 = 13.19 p/kWh) | Appendix M1 §6 (p.94), Table 32 code 30/60 |
| S0380.48 | bf99b1c7 |
E_PV "magnitude bug" audit revealed the real bug: schema gap on pv_batteries[].battery_capacity flat shape. Schema fix + mapper fall-back surfaced the 5-kWh batteries. Cohort PE +2.7..+8.1 → -3.5..-4.5 |
Appendix M1 §3c (p.94) |
| S0380.49 | e75198ce |
Wired effective-monthly Table 12e PE factors (pv_dwelling_primary_factor + pv_exported_primary_factor) for the PV split. Cluster closed -3.5..-4.5 → -2.8..-3.7 |
Appendix M1 §8 (p.94), Table 12e code 30/60 |
Residual progress — full PE cohort trajectory
| Cert | Pre-S0380.44 | Post-S0380.45 | Post-S0380.48 | Post-S0380.49 |
|---|---|---|---|---|
| 0330 (no PV) | +0.44 | +0.44 | +0.44 | +0.44 |
| 0350 (PV+5kWh) | −7.78 | +2.73 | −3.58 | −2.96 |
| 0380 (PV+5kWh) | −14.60 | +8.09 | −4.01 | −3.06 |
| 2130 (PV gas) | −38.63 | −9.70 | −9.70 | −8.22 |
| 2225 (PV+5kWh) | −11.77 | +4.48 | −4.50 | −3.73 |
| 2636 (PV+5kWh) | −9.65 | +3.42 | −4.14 | −3.44 |
| 3800 (PV+5kWh) | −9.61 | +3.58 | −4.01 | −3.25 |
| 9285 (PV+5kWh) | −7.96 | +3.20 | −3.46 | −2.81 |
| 9418 (PV+5kWh) | −7.30 | +4.67 | −3.76 | −3.01 |
| 9501 (PV no battery) | −8.28 | +0.25 | +0.25 | +0.65 |
CO2 residuals all <0.11 t/yr; SAP scores all exact (except 2130 at +1 — pre-existing, a gas-combi/secondary-heating gap unrelated to PV). 9501 drifted slightly because its β=0.498 already matched worksheet exactly, so the factor correction surfaced a small previously-hidden gap. Cluster shows clean closure trajectory.
Remaining work (open front)
The 7-cert ASHP+battery cluster now sits at -2.8..-3.7 kWh/m² PE. The differential breakdown:
-
β fine-tuning (~1-2 kWh/m²): cascade β = 0.751-0.812 vs worksheet β = 0.7426 for cert 0380. This is a monthly D_PV distribution detail. The
_pv_eligible_demand_monthly_kwhhelper sums lighting/appliances/cooking/electric-shower/pumps-fans/main-1 /hot-water tuples; their relative monthly weighting affects β. -
Heat pump electricity demand (~1 kWh/m²): the ASHP cohort has main-heating-fuel = electricity. The cascade's D_PV inclusion list may not perfectly match the worksheet's footnote 32 restrictions ("excludes electricity used for off-peak space and water heating"). Verify against worksheet line refs for cert 0380.
-
Possible small E_PV or pv_split monthly distribution gaps (~0.5 kWh/m²): per-month E_PV in the cascade vs worksheet may differ marginally if
_pv_array_monthly_generation_kwhuses slightly different solar-flux interpolation than the worksheet.
Each of these is a candidate for a follow-up slice but not part of the β-split phase scope.
Architecture: cross-cascade β-split shape (final)
All three cascades use the uniform shape:
| Cascade | Dwelling factor (IMPORT) | Exported factor (EXPORT) |
|---|---|---|
| PE | pv_dwelling_primary_factor → fall back to other_primary_factor (1.501) |
pv_exported_primary_factor → fall back to pv_export_primary_factor (0.501) |
| CO2 | pv_dwelling_co2_factor_kg_per_kwh → fall back to no credit |
pv_exported_co2_factor_kg_per_kwh → fall back to no credit |
| Cost | pv_dwelling_import_price_gbp_per_kwh → Table 32 code 30 (13.19 p/kWh) |
pv_export_credit_gbp_per_kwh → Table 32 code 60 (13.19 p/kWh) |
Shared cross-cascade state:
pv_dwelling_kwh_per_yr+pv_exported_kwh_per_yronCalculatorInputscarry the β-split per-year totals.- All factor fields are
Optional[float] = None(defaults preserve the legacy synthetic-construction behaviour in unit tests). cert_to_inputspopulates them via_effective_monthly_co2_factor/_effective_monthly_pe_factoroverpv_split.epv_*_monthly_kwh, keyed on Table 12 code 30 for dwelling and code 60 for exported.
Test baseline at HEAD
PYTHONPATH=/workspaces/model python -m pytest \
backend/documents_parser/tests/test_summary_pdf_mapper_chain.py \
backend/documents_parser/tests/test_elmhurst_extractor.py \
backend/documents_parser/tests/test_elmhurst_end_to_end.py \
domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \
domain/sap10_calculator/worksheet/tests/test_water_heating.py \
domain/sap10_calculator/worksheet/tests/test_mean_internal_temperature.py \
domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py \
domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py \
domain/sap10_calculator/tests/test_pcdb_table_362_lookup.py \
domain/sap10_ml/tests/test_rdsap_uvalues.py \
datatypes/epc/schema/tests/test_schema_loading.py \
domain/sap10_calculator/worksheet/tests/test_photovoltaic.py \
--no-cov -q
Expected: 763 pass + 0 fail.
Conventions preserved
- 1e-4 across the board (feedback-one-e-minus-4-across-the-board)
- Worksheet, not API, is the target (feedback-worksheet-not-api-reference)
- Cross-mapper parity via cascade (feedback-cross-mapper-parity-via-cascade)
- Spec-floor skepticism (feedback-spec-floor-skepticism) AND
verify handover claims (feedback-verify-handover-claims) —
the original "E_PV magnitude bug" hypothesis was wrong (worksheet
E_PV matched cascade); the real bug was a schema-gap on
pv_batteries[].battery_capacity. Verified by reading the worksheet PDF directly. - Bigger slices OK for uniform-cohort work (feedback-bigger-slices-for-uniform-work)
- Golden residuals → ~0 (feedback-golden-residuals-near-zero) — cluster closed by ~50% magnitude across the phase
- AAA test convention (feedback-aaa-test-convention)
abs(diff) <= tolnotpytest.approx(feedback-abs-diff-over-pytest-approx)- Spec citation in commit messages (feedback-spec-citation-in-commits)
- One slice = one commit; stage by name (feedback-commit-per-slice)
- Pyright net-zero per touched file (feedback-zero-error-strict)