Model/datatypes/epc
Khalim Conn-Kowlessar 2805e13d4d Slice S0380.48: surface real-API pv_batteries[].battery_capacity (5 kWh)
The 7-cert ASHP+battery PE cluster was overshooting by +2.7..+8.1 kWh/m²
after the PE β-split landed in S0380.45. The handover hypothesised an
E_PV magnitude bug ("cascade thinks 2570 kWh/yr vs worksheet 831"). The
worksheet PDF for cert 0380 (dr87-0001-000899.pdf line 233) was
verified to show **-2563.3692** kWh/yr — matching our cascade. The
real bug was different: the **5-kWh battery wasn't reaching the
cascade**, so β-coefficients used the no-battery branch (C1=1.61,
β≈0.36) instead of the 5-kWh branch (C1=1.12, β≈0.75).

Per SAP 10.2 Appendix M1 §3c-d (p.94): "C_bat is the usable capacity
of the battery in kWh, limited to a maximum value of 15 kWh. C_bat=0
if no battery present." Cert 0380 lodges `pv_battery_count: 1` and
`pv_batteries: [{"battery_capacity": 5}]` — but the schema's
`PvBatteries` dataclass had only `pv_battery: Optional[PvBattery]`,
matching the older synthetic fixture shape (nested
`{"pv_battery": {"battery_capacity": 5}}`). The real-API payload's
flat `battery_capacity: 5` was silently dropped during `from_dict`.

Two surgical changes:
- `datatypes/epc/schema/rdsap_schema_21_0_1.py`: add
  `battery_capacity: Optional[float] = None` as a sibling to
  `pv_battery` on `PvBatteries`. Synthetic-shape certs continue to
  populate the nested form; real-API certs now populate the flat form.
- `datatypes/epc/domain/mapper.py:_first_pv_battery`: prefer nested
  when present, fall back to the flat lifted field. Domain still
  exposes a single uniform `PvBatteries(pv_battery=PvBattery(...))`
  shape downstream.

Cohort impact (PE residual kWh/m² vs worksheet):

| Cert | Pre-S0380.48 | Post-S0380.48 |
|---|---:|---:|
| 0350 | +2.73 | -3.58 |
| 0380 | +8.09 | -4.01 |
| 2225 | +4.48 | -4.50 |
| 2636 | +3.42 | -4.14 |
| 3800 | +3.58 | -4.01 |
| 9285 | +3.20 | -3.46 |
| 9418 | +4.67 | -3.76 |

Cluster magnitude dropped from +2.7..+8.1 to -3.5..-4.5 — the cascade
now over-credits PV by ~4 kWh/m² (vs previously under-crediting by
~5 kWh/m²). The residual flipped sign because cascade β=0.75-0.81
slightly exceeds worksheet β=0.74 (read from page-3 line 233a/233b
ratio 1903.39/2563.37 = 0.7426). The remaining ~4 kWh/m² under-shoot
traces to two structural factors deferred until a fresh closure
slice ships:

1. The synthetic-default `pv_export_primary_factor = 0.501` is the
   annual Table 12 code-60 value. The worksheet uses the effective
   monthly Table 12e factor weighted by E_PV,ex,m (cert 0380: 0.4268
   = -0.074 differential). The cascade's `_effective_monthly_pe_
   factor` already computes the same weighting for PV — but the
   calculator's PV PE credit reads `inputs.other_primary_factor`
   (=1.501) and `inputs.pv_export_primary_factor` (=0.501) directly,
   bypassing the per-end-use effective-monthly cascade.
2. Cascade β slightly higher than worksheet (0.751 vs 0.7426 on
   cert 0380) — likely a monthly-distribution detail in D_PV.

SAP scores remain exact across the cohort (residual +0 every cert).
CO2 residuals all <0.11 t/yr (well within the 0.001-tolerance pin
range after re-pin). 9501 (PV no battery) preserved at +0.255 PE /
-0.047 CO2 — no regression. Re-pins all 7 golden fixtures in the
same slice per [[feedback-commit-per-slice]].

Pyright net-zero on touched files (32 errors before, 32 after).
2026-06-01 16:28:47 +00:00
..
domain Slice S0380.48: surface real-API pv_batteries[].battery_capacity (5 kWh) 2026-06-01 16:28:47 +00:00
loaders demo generated for use in address2uprn 2026-05-08 14:48:15 +00:00
schema Slice S0380.48: surface real-API pv_batteries[].battery_capacity (5 kWh) 2026-06-01 16:28:47 +00:00
search bolstering testing 2026-04-28 13:46:09 +00:00
surveys Slice S0380.26: RdSAP10 §5.8 dry-lining adjustment on alt walls — closes cert 7700 -0.44 → +5e-5 2026-06-01 16:28:46 +00:00
__init__.py testing out rebaselining 2026-02-12 22:25:03 +00:00
construction_age_band.py testing out rebaselining 2026-02-12 22:25:03 +00:00
efficiency.py beginning to assembly the parity class 2026-02-04 18:34:59 +00:00
floor.py preparing partiy class 2026-02-05 08:54:27 +00:00
fuel.py beginning to assembly the parity class 2026-02-04 18:34:59 +00:00
heating_controls.py beginning to assembly the parity class 2026-02-04 18:34:59 +00:00
hotwater.py beginning to assembly the parity class 2026-02-04 18:34:59 +00:00
main_heating.py beginning to assembly the parity class 2026-02-04 18:34:59 +00:00
property_type_built_form.py beginning to assembly the parity class 2026-02-04 18:34:59 +00:00
roof.py beginning to assembly the parity class 2026-02-04 18:34:59 +00:00
walls.py beginning to assembly the parity class 2026-02-04 18:34:59 +00:00
windows.py testing out rebaselining 2026-02-12 22:25:03 +00:00