mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
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).
This commit is contained in:
parent
42ed38f77d
commit
bf99b1c7df
3 changed files with 61 additions and 37 deletions
|
|
@ -1916,7 +1916,13 @@ def _first_pv_battery(
|
|||
schema_pv_batteries: Any,
|
||||
) -> Optional[PvBatteries]:
|
||||
"""SapEnergySource.pv_batteries is a list in real-API certs and a single
|
||||
dataclass in the older synthetic fixture. Pick the first battery if any."""
|
||||
dataclass in the older synthetic fixture. Pick the first battery if any.
|
||||
|
||||
Real-API certs lift `battery_capacity` onto the item itself
|
||||
(`[{"battery_capacity": 5}]`); the synthetic fixture wraps it under
|
||||
`pv_battery` (`{"pv_battery": {"battery_capacity": 5}}`). Schema-level
|
||||
`PvBatteries` exposes both: prefer nested when present, fall back to
|
||||
the lifted flat value."""
|
||||
if schema_pv_batteries is None:
|
||||
return None
|
||||
if isinstance(schema_pv_batteries, list):
|
||||
|
|
@ -1925,9 +1931,17 @@ def _first_pv_battery(
|
|||
first = schema_pv_batteries[0]
|
||||
else:
|
||||
first = schema_pv_batteries
|
||||
if first.pv_battery is None:
|
||||
if first.pv_battery is not None:
|
||||
return PvBatteries(
|
||||
pv_battery=PvBattery(battery_capacity=first.pv_battery.battery_capacity)
|
||||
)
|
||||
flat_capacity = cast(
|
||||
Optional[float],
|
||||
first.battery_capacity, # pyright: ignore[reportUnknownMemberType]
|
||||
)
|
||||
if flat_capacity is None:
|
||||
return None
|
||||
return PvBatteries(pv_battery=PvBattery(battery_capacity=first.pv_battery.battery_capacity))
|
||||
return PvBatteries(pv_battery=PvBattery(battery_capacity=flat_capacity))
|
||||
|
||||
|
||||
def _first_shower_outlet(
|
||||
|
|
|
|||
|
|
@ -92,7 +92,16 @@ class PvBattery:
|
|||
class PvBatteries:
|
||||
# Real-API certs carry pv_batteries as a list (similar to shower_outlets);
|
||||
# the older synthetic fixture used a single-object wrapper.
|
||||
#
|
||||
# Two payload shapes coexist:
|
||||
# real API : [{"battery_capacity": 5}] — flat, lifted
|
||||
# synthetic: {"pv_battery": {"battery_capacity": 5}} — nested
|
||||
# `battery_capacity` is the lifted-flat field for the real-API shape;
|
||||
# `pv_battery` retains the legacy nested form for synthetic certs.
|
||||
# `_first_pv_battery` in the mapper prefers nested when present and
|
||||
# falls back to flat — covers both shapes without divergence.
|
||||
pv_battery: Optional[PvBattery] = None
|
||||
battery_capacity: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -256,93 +256,94 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
cert_number="0380-2471-3250-2596-8761",
|
||||
actual_sap=89,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=+8.0916,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0540,
|
||||
expected_pe_resid_kwh_per_m2=-4.0121,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0549,
|
||||
notes=(
|
||||
"Mitsubishi PUZ-WM50VHA PCDB 104568, semi-detached bungalow "
|
||||
"TFA 60.43 age D, PV 3 kWp + 5 kWh battery. Worksheet SAP "
|
||||
"88.5104. Slice S0380.45 wired SAP 10.2 Appendix M1 §3-4 "
|
||||
"β-split into the PE cascade: residual flipped from -14.60 "
|
||||
"to +8.09. The remaining +8 over-shoot points to the EPV "
|
||||
"magnitude bug (cascade thinks 2570 kWh PV / yr vs worksheet "
|
||||
"831 kWh / yr — 3× over-estimate), which keeps R_PV high and "
|
||||
"β low. Slice S0380.46 audits the EPV cascade — kWp "
|
||||
"interpretation, S lookup, or ZPV mapping."
|
||||
"88.5104. Slice S0380.48 surfaced the 5-kWh battery to the "
|
||||
"domain mapper: real-API certs lodge `pv_batteries` as "
|
||||
"`[{\"battery_capacity\": 5}]` (flat list) but the schema "
|
||||
"expected a nested `{\"pv_battery\": {\"battery_capacity\": "
|
||||
"5}}`. With battery surfaced, β rises 0.36 → 0.75 (vs "
|
||||
"worksheet 0.74), flipping PE residual +8.09 → -4.01. The "
|
||||
"remaining -4 under-shoot traces to the export PE factor "
|
||||
"(synthetic default 0.501 vs worksheet's effective monthly "
|
||||
"Table 12e ~0.427) plus a small β fine-tuning gap."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="0350-2968-2650-2796-5255",
|
||||
actual_sap=84,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=+2.7315,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0841,
|
||||
expected_pe_resid_kwh_per_m2=-3.5819,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0865,
|
||||
notes=(
|
||||
"Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with "
|
||||
"PV + 5 kWh battery. Worksheet SAP 84.1367. Slice S0380.45 "
|
||||
"shifted PE residual -7.78 → +2.73 via Appendix M1 β-split. "
|
||||
"Same EPV-magnitude shape as cert 0380 (see notes there)."
|
||||
"PV + 5 kWh battery. Worksheet SAP 84.1367. Slice S0380.48 "
|
||||
"battery-surface fix shifted PE residual +2.73 → -3.58."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="2225-3062-8205-2856-7204",
|
||||
actual_sap=89,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=+4.4804,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0711,
|
||||
expected_pe_resid_kwh_per_m2=-4.5030,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0729,
|
||||
notes=(
|
||||
"Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with "
|
||||
"PV + 5 kWh battery. Worksheet SAP 88.7921. Slice S0380.45 "
|
||||
"shifted PE residual -11.77 → +4.48 via Appendix M1 β-split."
|
||||
"PV + 5 kWh battery. Worksheet SAP 88.7921. Slice S0380.48 "
|
||||
"battery-surface fix shifted PE residual +4.48 → -4.50."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="2636-0525-2600-0401-2296",
|
||||
actual_sap=86,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=+3.4216,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0581,
|
||||
expected_pe_resid_kwh_per_m2=-4.1432,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0603,
|
||||
notes=(
|
||||
"Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with "
|
||||
"PV + 5 kWh battery + 3.74 m² cantilever + 12.76 m² alt wall. "
|
||||
"Worksheet SAP 86.2641. Slice S0380.45 shifted PE residual "
|
||||
"-9.65 → +3.42 via Appendix M1 β-split."
|
||||
"Worksheet SAP 86.2641. Slice S0380.48 battery-surface fix "
|
||||
"shifted PE residual +3.42 → -4.14."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="3800-8515-0922-3398-3563",
|
||||
actual_sap=86,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=+3.5809,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0142,
|
||||
expected_pe_resid_kwh_per_m2=-4.0132,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0166,
|
||||
notes=(
|
||||
"Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with "
|
||||
"PV + 5 kWh battery. Worksheet SAP 86.1458. Slice S0380.45 "
|
||||
"shifted PE residual -9.61 → +3.58 via Appendix M1 β-split."
|
||||
"PV + 5 kWh battery. Worksheet SAP 86.1458. Slice S0380.48 "
|
||||
"battery-surface fix shifted PE residual +3.58 → -4.01."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="9285-3062-0205-7766-7200",
|
||||
actual_sap=84,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=+3.1982,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0979,
|
||||
expected_pe_resid_kwh_per_m2=-3.4619,
|
||||
expected_co2_resid_tonnes_per_yr=-0.1003,
|
||||
notes=(
|
||||
"Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with "
|
||||
"PV + 5 kWh battery. Worksheet SAP 84.1369. Slice S0380.45 "
|
||||
"shifted PE residual -7.96 → +3.20 via Appendix M1 β-split."
|
||||
"PV + 5 kWh battery. Worksheet SAP 84.1369. Slice S0380.48 "
|
||||
"battery-surface fix shifted PE residual +3.20 → -3.46."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="9418-3062-8205-3566-7200",
|
||||
actual_sap=85,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=+4.6681,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0463,
|
||||
expected_pe_resid_kwh_per_m2=-3.7627,
|
||||
expected_co2_resid_tonnes_per_yr=-0.0485,
|
||||
notes=(
|
||||
"Daikin Altherma EDLQ05CAV3 PCDB 102421 (heating_duration "
|
||||
"code '24' — continuous, all days at Th) + 5 kWh battery. "
|
||||
"Worksheet SAP 84.6305. Slice S0380.45 shifted PE residual "
|
||||
"-7.30 → +4.67 via Appendix M1 β-split."
|
||||
"Worksheet SAP 84.6305. Slice S0380.48 battery-surface fix "
|
||||
"shifted PE residual +4.67 → -3.76."
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue