mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.45: wire β-split into PE cascade per SAP 10.2 Appendix M1 §8
The PE cascade in calculator.py was crediting ALL PV generation at the
IMPORT PEF (Table 12 ~1.501) instead of splitting per Appendix M1
§4/§8 — onsite-consumed E_PV,dw at the IMPORT PEF and exported E_PV,ex
at the EXPORT PEF (Table 12 code 60 = 0.501). The over-credit on the
exported portion was the primary driver of the ASHP-cohort PE Δ -7..-15
kWh/m² under-count.
Wiring (cert_to_inputs.py):
- `_pv_array_monthly_generation_kwh(array, climate)` — per-array E_PV,m
via Appendix M1 §2 (p.92) apportioning: 0.8 × kWp × ZPV × monthly
solar radiation. Reuses ORIENTATION/PITCH/Z lookups already in
`_pv_array_generation_kwh_per_yr`. Annual sum equals the existing
helper to float precision.
- `_pv_monthly_generation_kwh(epc, climate)` — sums per-array monthlies;
falls back to the same §11.1 b) percent-roof-area synthesis as the
annual helper for certs without per-array detail.
- `_pv_battery_capacity_kwh(epc)` — total usable battery capacity =
per-battery capacity × pv_battery_count. The 15 kWh cap per §3c is
applied inside `pv_beta_coefficients` and not duplicated here.
- `_pv_eligible_demand_monthly_kwh(...)` — assembles D_PV,m per §3a
p.93: lighting + appliances + cooking + electric showers + pumps
& fans, plus E_space,m when main fuel is Table-12 {30, 32, 34, 35,
38} (electricity not at off-peak) and E_water,m when water heating
fuel is Table-12 30 (standard electricity). Off-peak immersion ×
(243) and the Appendix G4 PV-diverter branch are deferred —
current cohort fixtures don't exercise them.
- In `cert_to_inputs`: assemble monthly EPV + DPV + battery, call
`pv_split_monthly`, pass `pv_dwelling_kwh_per_yr` +
`pv_exported_kwh_per_yr` through to CalculatorInputs.
Wiring (calculator.py):
- New fields: `pv_dwelling_kwh_per_yr: Optional[float]`,
`pv_exported_kwh_per_yr: Optional[float]`,
`pv_export_primary_factor: float = 0.501` (Table 12 code 60).
- PE cascade now does:
pv_offset = E_PV,dw × IMPORT_PEF + E_PV,ex × EXPORT_PEF
when both split fields are set. Legacy fall-through to all-IMPORT
when either is None (preserves synthetic CalculatorInputs
constructions in unit tests).
Test impact (golden-fixture residual shifts — all expected, re-pinned):
Pre-Slice 45 → Post-Slice 45:
- 0330 (no PV): +0.44 → +0.44 (unchanged ✓)
- 0350 (PV + 5 kWh battery): -7.78 → +2.73
- 0380 (PV + 5 kWh battery): -14.60 → +8.09
- 2130 (PV + gas combi): -38.63 → -9.70 (also SAP +1 shift)
- 2225 (PV + 5 kWh battery): -11.77 → +4.48
- 2636 (PV + 5 kWh battery): -9.65 → +3.42
- 3800 (PV + 5 kWh battery): -9.61 → +3.58
- 9285 (PV + 5 kWh battery): -7.96 → +3.20
- 9418 (PV + 5 kWh battery): -7.30 → +4.67
- 9501 (PV, no battery): -8.28 → +0.25 (CLOSED ✓)
Cert 9501 closing to +0.25 with the β-split alone confirms the
implementation is spec-correct. The 7-cert 5-kWh-battery cohort
now over-shoots in the positive direction because the cascade's
E_PV magnitude is ~3× the worksheet's (cert 0380 cascade 2570 kWh/yr
vs worksheet 831 kWh/yr — peak_power=3 interpreted as 3 kWp while
worksheet uses ~1 kWp). With E_PV overestimated, R_PV = E_PV / D_PV
is too high → β_m from §3d formula too low → not enough credit
shifts to the IMPORT factor. Slice S0380.46 audits the cascade's
E_PV magnitude (kWp interpretation, S lookup, or ZPV mapping).
Chain tests (cohort-1 + cohort-2 SAP-rating-vs-worksheet) all stay
<1e-4 — Slice 45 only touches the PE cascade; SAP rating uses the
cost cascade which is still on the old all-export path.
Test suite: 763 pass + 0 fail. Pyright net-zero on touched files.
Spec citations:
- SAP 10.2 specification Appendix M1 §3a (p.93) — D_PV,m assembly.
- SAP 10.2 specification Appendix M1 §3c-d (p.94) — β formula.
- SAP 10.2 specification Appendix M1 §4 (p.94) — E_PV,dw / E_PV,ex.
- SAP 10.2 specification Appendix M1 §8 (p.94) — PE factor split.
- SAP 10.2 Table 12 code 60 — EXPORT PEF = 0.501.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5344bc8920
commit
49de18e83a
3 changed files with 267 additions and 45 deletions
|
|
@ -223,6 +223,17 @@ class CalculatorInputs:
|
|||
# collapse to a single credit at the export rate (Table 12 code 60).
|
||||
pv_generation_kwh_per_yr: float = 0.0
|
||||
pv_export_credit_gbp_per_kwh: float = 0.0
|
||||
# SAP 10.2 Appendix M1 §3-4 PV onsite/export split. When both are
|
||||
# set, the PE cascade (and follow-up CO2/cost wiring) applies
|
||||
# IMPORT factors to the onsite-consumed portion and EXPORT factors
|
||||
# to the exported portion. None → legacy fall-through that credits
|
||||
# all PV at the IMPORT factor (over-credits the exported portion;
|
||||
# used by synthetic CalculatorInputs constructions in unit tests).
|
||||
pv_dwelling_kwh_per_yr: Optional[float] = None
|
||||
pv_exported_kwh_per_yr: Optional[float] = None
|
||||
# SAP 10.2 Table 12 code 60 ("electricity sold to grid, PV") PE
|
||||
# factor = 0.501. Applied to E_PV,ex when split is set.
|
||||
pv_export_primary_factor: float = 0.501
|
||||
# Secondary heating — SAP 10.2 Table 11 routes a fraction of space
|
||||
# heating demand to a secondary system (0.10 for gas/oil/solid main
|
||||
# systems; 0.15-0.20 for electric room/storage heaters). Fraction
|
||||
|
|
@ -526,10 +537,25 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
+ inputs.lighting_kwh_per_yr * lighting_primary_factor
|
||||
+ inputs.electric_shower_kwh_per_yr * electric_shower_primary_factor
|
||||
)
|
||||
# PV offsets primary energy at the export PEF (Table 32 code 60 =
|
||||
# 0.501 — half the import PEF since exported kWh isn't subject to the
|
||||
# full grid-loss multiplier).
|
||||
pv_primary_offset_kwh = inputs.pv_generation_kwh_per_yr * inputs.other_primary_factor
|
||||
# SAP 10.2 Appendix M1 §8: PV onsite consumption credits at IMPORT
|
||||
# PEF (offsets grid imports); PV exports credit at the EXPORT PEF
|
||||
# ("electricity sold to grid, PV" — Table 12 code 60 = 0.501). When
|
||||
# the cert→inputs cascade has computed the β-split (§3-4 in
|
||||
# `domain.sap10_calculator.worksheet.photovoltaic`), use it; fall
|
||||
# back to all-IMPORT for synthetic CalculatorInputs constructions
|
||||
# in unit tests (which don't supply the split).
|
||||
if (
|
||||
inputs.pv_dwelling_kwh_per_yr is not None
|
||||
and inputs.pv_exported_kwh_per_yr is not None
|
||||
):
|
||||
pv_primary_offset_kwh = (
|
||||
inputs.pv_dwelling_kwh_per_yr * inputs.other_primary_factor
|
||||
+ inputs.pv_exported_kwh_per_yr * inputs.pv_export_primary_factor
|
||||
)
|
||||
else:
|
||||
pv_primary_offset_kwh = (
|
||||
inputs.pv_generation_kwh_per_yr * inputs.other_primary_factor
|
||||
)
|
||||
primary_energy_kwh = max(
|
||||
0.0,
|
||||
space_heating_primary_kwh
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ from domain.sap10_calculator.tables.pcdb.postcode_weather import (
|
|||
postcode_climate,
|
||||
)
|
||||
from domain.sap10_calculator.tables.table_12 import (
|
||||
API_FUEL_TO_TABLE_12,
|
||||
co2_monthly_factors_kg_per_kwh,
|
||||
co2_factor_kg_per_kwh,
|
||||
pe_monthly_factors_kwh_per_kwh,
|
||||
|
|
@ -138,6 +139,7 @@ from domain.sap10_calculator.worksheet.energy_requirements import (
|
|||
from domain.sap10_calculator.worksheet.fabric_energy_efficiency import (
|
||||
fabric_energy_efficiency_kwh_per_m2_yr,
|
||||
)
|
||||
from domain.sap10_calculator.worksheet.photovoltaic import pv_split_monthly
|
||||
from domain.sap10_calculator.worksheet.space_cooling import (
|
||||
SpaceCoolingResult,
|
||||
space_cooling_monthly_kwh,
|
||||
|
|
@ -839,6 +841,136 @@ def _pv_generation_kwh_per_yr(
|
|||
return sum(_pv_array_generation_kwh_per_yr(a, climate) for a in arrays)
|
||||
|
||||
|
||||
def _pv_array_monthly_generation_kwh(
|
||||
array: PhotovoltaicArray,
|
||||
climate: "int | PostcodeClimate",
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 Appendix M1 §2 (p.92) — apportion the annual E_PV of one
|
||||
array to months in proportion to monthly solar radiation:
|
||||
E_PV,m = 0.8 × kWp × ZPV × (days_m × S_m × 24 / 1000)
|
||||
where S_m is the §U3.2 surface flux (W/m²). Returns a 12-zero tuple
|
||||
for arrays whose orientation isn't mapped in
|
||||
`ORIENTATION_BY_SAP10_CODE` (defensive — current cert lodgements
|
||||
always cover 1..8)."""
|
||||
orientation = ORIENTATION_BY_SAP10_CODE.get(array.orientation)
|
||||
if orientation is None:
|
||||
return (0.0,) * 12
|
||||
pitch_deg = _PV_PITCH_DEG_BY_CODE.get(array.pitch, _PV_PITCH_DEG_DEFAULT)
|
||||
z = _PV_OVERSHADING_FACTOR.get(array.overshading, 1.0)
|
||||
monthly: list[float] = []
|
||||
for month_idx, days in enumerate(_DAYS_PER_MONTH):
|
||||
s_m_w_per_m2 = surface_solar_flux_w_per_m2(
|
||||
orientation=orientation,
|
||||
pitch_deg=pitch_deg,
|
||||
region=climate,
|
||||
month=month_idx + 1,
|
||||
)
|
||||
s_m_kwh_per_m2 = days * s_m_w_per_m2 * _HOURS_PER_DAY_OVER_1000
|
||||
epv_m = _PV_MODULE_EFFICIENCY_FACTOR * array.peak_power * z * s_m_kwh_per_m2
|
||||
monthly.append(epv_m)
|
||||
return tuple(monthly)
|
||||
|
||||
|
||||
def _pv_monthly_generation_kwh(
|
||||
epc: EpcPropertyData,
|
||||
climate: "int | PostcodeClimate",
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 Appendix M1 §2 (p.92) — monthly E_PV summed across all
|
||||
PV arrays. Annual sum matches `_pv_generation_kwh_per_yr` to
|
||||
float precision."""
|
||||
arrays = epc.sap_energy_source.photovoltaic_arrays
|
||||
if not arrays:
|
||||
arrays = _synthesize_pv_arrays_from_percent_roof_area(epc)
|
||||
if not arrays:
|
||||
return (0.0,) * 12
|
||||
monthly_sum: list[float] = [0.0] * 12
|
||||
for arr in arrays:
|
||||
for m, kwh in enumerate(_pv_array_monthly_generation_kwh(arr, climate)):
|
||||
monthly_sum[m] += kwh
|
||||
return tuple(monthly_sum)
|
||||
|
||||
|
||||
def _pv_battery_capacity_kwh(epc: EpcPropertyData) -> float:
|
||||
"""SAP 10.2 Appendix M1 §3c — total usable battery capacity (kWh)
|
||||
for the dwelling. Sums lodged `pv_battery.battery_capacity` across
|
||||
the lodged `pv_battery_count`. Returns 0 when no battery lodged.
|
||||
|
||||
`pv_split_monthly` caps Cbat at 15 per spec; that cap is applied
|
||||
inside `pv_beta_coefficients` and not duplicated here."""
|
||||
es = epc.sap_energy_source
|
||||
if es.pv_batteries is None:
|
||||
return 0.0
|
||||
per_battery_kwh = float(es.pv_batteries.pv_battery.battery_capacity)
|
||||
if per_battery_kwh <= 0.0:
|
||||
return 0.0
|
||||
count = es.pv_battery_count if es.pv_battery_count > 0 else 1
|
||||
return per_battery_kwh * count
|
||||
|
||||
|
||||
# SAP 10.2 Appendix M1 §3a (p.93) — Table-12 fuel codes whose monthly
|
||||
# kWh count toward E_space,m (electricity used for space heating, not
|
||||
# at the off-peak low-rate). Per the spec footnote 32: "excludes
|
||||
# electricity used for off-peak space and water heating".
|
||||
_PV_ELIGIBLE_SPACE_HEATING_FUEL_CODES: Final[frozenset[int]] = frozenset(
|
||||
{30, 32, 34, 35, 38}
|
||||
)
|
||||
|
||||
# SAP 10.2 Appendix M1 §3a — fuel codes for which E_water,m is the
|
||||
# full monthly water-heating fuel kWh (no (243) immersion-off-peak
|
||||
# scaling). Per spec: "E_water,m = (219)m if water heating fuel code
|
||||
# applied in Section 10a of the SAP worksheet is 30". For simplicity
|
||||
# the off-peak immersion × (243) branch is deferred; non-30 electric
|
||||
# water heating fuels contribute zero E_water,m.
|
||||
_PV_ELIGIBLE_WATER_HEATING_FUEL_CODES: Final[frozenset[int]] = frozenset({30})
|
||||
|
||||
|
||||
def _pv_eligible_demand_monthly_kwh(
|
||||
*,
|
||||
lighting_monthly_kwh: tuple[float, ...],
|
||||
appliances_monthly_kwh: tuple[float, ...],
|
||||
cooking_monthly_kwh: tuple[float, ...],
|
||||
electric_shower_monthly_kwh: tuple[float, ...],
|
||||
pumps_fans_monthly_kwh: tuple[float, ...],
|
||||
main_1_fuel_monthly_kwh: tuple[float, ...],
|
||||
hot_water_monthly_kwh: tuple[float, ...],
|
||||
main_fuel_code_table_12: Optional[int],
|
||||
water_heating_fuel_code_table_12: Optional[int],
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 Appendix M1 §3a (p.93) — monthly PV-eligible demand
|
||||
D_PV,m. Always includes lighting + appliances + cooking + electric
|
||||
shower + pumps & fans. Includes E_space,m only when the main
|
||||
heating fuel is electricity at the standard tariff (codes 30, 32,
|
||||
34, 35, 38 per spec). Includes E_water,m only when the water
|
||||
heating fuel code is 30 (standard electricity) per spec.
|
||||
|
||||
The off-peak immersion × (243) Ewater branch and the Appendix G4
|
||||
PV diverter adjustment are deferred — current cohort fixtures
|
||||
don't exercise them."""
|
||||
include_space = (
|
||||
main_fuel_code_table_12 is not None
|
||||
and main_fuel_code_table_12 in _PV_ELIGIBLE_SPACE_HEATING_FUEL_CODES
|
||||
)
|
||||
include_water = (
|
||||
water_heating_fuel_code_table_12 is not None
|
||||
and water_heating_fuel_code_table_12 in _PV_ELIGIBLE_WATER_HEATING_FUEL_CODES
|
||||
)
|
||||
monthly: list[float] = []
|
||||
for m in range(12):
|
||||
d = (
|
||||
lighting_monthly_kwh[m]
|
||||
+ appliances_monthly_kwh[m]
|
||||
+ cooking_monthly_kwh[m]
|
||||
+ electric_shower_monthly_kwh[m]
|
||||
+ pumps_fans_monthly_kwh[m]
|
||||
)
|
||||
if include_space:
|
||||
d += main_1_fuel_monthly_kwh[m]
|
||||
if include_water:
|
||||
d += hot_water_monthly_kwh[m]
|
||||
monthly.append(d)
|
||||
return tuple(monthly)
|
||||
|
||||
|
||||
# RdSAP 10 §11.1 b): when the kWp is not lodged but the cert lodges a
|
||||
# "% of roof area" PV figure, derive the PV peak power as
|
||||
# `0.12 × PV area`, with PV area being the dwelling's roof area for
|
||||
|
|
@ -2766,6 +2898,8 @@ def cert_to_inputs(
|
|||
# spec-faithful L1-L11 derivation that drives §5 (67) gains. Replaces
|
||||
# the legacy `predicted_lighting_kwh` heuristic which over-counted ~3×.
|
||||
lighting_monthly_kwh: tuple[float, ...] = (0.0,) * 12
|
||||
appliances_monthly_kwh: tuple[float, ...] = (0.0,) * 12
|
||||
cooking_monthly_kwh: tuple[float, ...] = (0.0,) * 12
|
||||
if epc.total_floor_area_m2 is None:
|
||||
internal_gains_monthly_w = (0.0,) * 12
|
||||
lighting_kwh = 0.0
|
||||
|
|
@ -2782,12 +2916,26 @@ def cert_to_inputs(
|
|||
)
|
||||
lighting_kwh = internal_gains_result.lighting_kwh_per_yr
|
||||
# Watts → kWh via n_days_in_month × 24 hours / 1000 W per kWh.
|
||||
# Appendix M1 §3a D_PV,m needs each of these monthly so the
|
||||
# PV-eligible-demand assembly downstream can sum them in kWh.
|
||||
lighting_monthly_kwh = tuple(
|
||||
w * d * 24.0 / 1000.0
|
||||
for w, d in zip(
|
||||
internal_gains_result.lighting_monthly_w, _DAYS_IN_MONTH
|
||||
)
|
||||
)
|
||||
appliances_monthly_kwh = tuple(
|
||||
w * d * 24.0 / 1000.0
|
||||
for w, d in zip(
|
||||
internal_gains_result.appliances_monthly_w, _DAYS_IN_MONTH
|
||||
)
|
||||
)
|
||||
cooking_monthly_kwh = tuple(
|
||||
w * d * 24.0 / 1000.0
|
||||
for w, d in zip(
|
||||
internal_gains_result.cooking_monthly_w, _DAYS_IN_MONTH
|
||||
)
|
||||
)
|
||||
|
||||
climate: "int | PostcodeClimate" = _climate_source(postcode_climate)
|
||||
solar_gains_monthly_w = solar_gains_from_cert(
|
||||
|
|
@ -2927,6 +3075,48 @@ def cert_to_inputs(
|
|||
secondary_heating_efficiency_pct=secondary_efficiency_value * 100.0,
|
||||
)
|
||||
|
||||
# SAP 10.2 Appendix M1 §3-4 (p.93-94): split monthly PV generation
|
||||
# into onsite-consumed (E_PV,dw,m) and exported (E_PV,ex,m) via the
|
||||
# β factor. The PE cascade in calculator.py reads
|
||||
# `pv_dwelling_kwh_per_yr` + `pv_exported_kwh_per_yr` and applies
|
||||
# IMPORT PEF (Table 12 = 1.501) to the onsite portion and EXPORT
|
||||
# PEF (Table 12 code 60 = 0.501) to the exported portion per §8.
|
||||
# Fuel-code translation: `main_fuel` / `water_heating_fuel` are
|
||||
# raw API codes; the β cascade keys on Table-12 codes (e.g. API 29
|
||||
# = electricity → Table 12 code 30) per the Appendix M1 §3a fuel
|
||||
# inclusion list.
|
||||
pv_monthly_kwh = _pv_monthly_generation_kwh(epc, climate)
|
||||
pv_eligible_demand_monthly_kwh = _pv_eligible_demand_monthly_kwh(
|
||||
lighting_monthly_kwh=lighting_monthly_kwh,
|
||||
appliances_monthly_kwh=appliances_monthly_kwh,
|
||||
cooking_monthly_kwh=cooking_monthly_kwh,
|
||||
electric_shower_monthly_kwh=(
|
||||
wh_result.electric_shower_monthly_kwh
|
||||
if wh_result is not None else (0.0,) * 12
|
||||
),
|
||||
pumps_fans_monthly_kwh=_days_in_month_proportioned(
|
||||
pumps_fans_kwh, _DAYS_IN_MONTH,
|
||||
),
|
||||
main_1_fuel_monthly_kwh=energy_requirements_result.main_1_fuel_monthly_kwh,
|
||||
hot_water_monthly_kwh=_days_in_month_proportioned(hw_kwh, _DAYS_IN_MONTH),
|
||||
main_fuel_code_table_12=(
|
||||
API_FUEL_TO_TABLE_12.get(main_fuel, main_fuel)
|
||||
if main_fuel is not None else None
|
||||
),
|
||||
water_heating_fuel_code_table_12=(
|
||||
API_FUEL_TO_TABLE_12.get(
|
||||
epc.sap_heating.water_heating_fuel,
|
||||
epc.sap_heating.water_heating_fuel,
|
||||
)
|
||||
if epc.sap_heating.water_heating_fuel is not None else None
|
||||
),
|
||||
)
|
||||
pv_split = pv_split_monthly(
|
||||
epv_monthly_kwh=pv_monthly_kwh,
|
||||
dpv_monthly_kwh=pv_eligible_demand_monthly_kwh,
|
||||
battery_capacity_kwh=_pv_battery_capacity_kwh(epc),
|
||||
)
|
||||
|
||||
return CalculatorInputs(
|
||||
dimensions=dim,
|
||||
heat_transmission=ht,
|
||||
|
|
@ -3008,6 +3198,11 @@ def cert_to_inputs(
|
|||
),
|
||||
pv_generation_kwh_per_yr=_pv_generation_kwh_per_yr(epc, climate),
|
||||
pv_export_credit_gbp_per_kwh=_pv_export_credit_gbp_per_kwh(),
|
||||
# SAP 10.2 Appendix M1 §3-4 PV split — the cascade applies
|
||||
# IMPORT PEF (Table 12) to the onsite portion and EXPORT PEF
|
||||
# (Table 12 code 60 = 0.501) to the exported portion per §8.
|
||||
pv_dwelling_kwh_per_yr=pv_split.epv_dwelling_kwh_per_yr,
|
||||
pv_exported_kwh_per_yr=pv_split.epv_exported_kwh_per_yr,
|
||||
secondary_heating_fraction=secondary_fraction_value,
|
||||
secondary_heating_efficiency=secondary_efficiency_value,
|
||||
energy_requirements=energy_requirements_result,
|
||||
|
|
|
|||
|
|
@ -200,20 +200,18 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
cert_number="2130-1033-4050-5007-8395",
|
||||
actual_sap=82,
|
||||
expected_sap_resid=+1,
|
||||
expected_pe_resid_kwh_per_m2=-38.6274,
|
||||
expected_pe_resid_kwh_per_m2=-9.6962,
|
||||
expected_co2_resid_tonnes_per_yr=+0.2993,
|
||||
notes=(
|
||||
"End-terrace + 1 extension, TFA 64, gas combi PCDB index 17505, "
|
||||
"postcode DE22 (PCDB Table 172 match), PV: 2× 2.04 kWp arrays "
|
||||
"(SE + NW, overshading 1 + 2). Slices 45a/b/c implement SAP10.2 "
|
||||
"Appendix M per-array yield with the real Appendix U3.3 S(orient, "
|
||||
"p) integral + Table M1 ZPV, and split rating (UK-avg climate) "
|
||||
"from demand (DE22 PCDB Table 172 climate). Net effect: SAP "
|
||||
"residual +9 → +3, PE residual −69.57 → −51.90 vs the prior "
|
||||
"lump-sum 850 × total_kWp. The remaining −51.90 PE drift sits "
|
||||
"outside the PV cascade — candidates include the dwelling-use "
|
||||
"vs export β-factor split (Appendix M §3) and the secondary "
|
||||
"heating credit, both untouched so far."
|
||||
"(SE + NW, overshading 1 + 2). Slice S0380.45 wired the "
|
||||
"Appendix M1 β-split into the PE cascade: PE residual moved "
|
||||
"from -38.63 to -9.70 (the +28.9 kWh/m² shift = (1-β) × EPV × "
|
||||
"(import_PEF - export_PEF) credit correction). SAP integer "
|
||||
"shifted +1 (82 → 83) via the same cascade interaction. The "
|
||||
"-9.70 residual remains — gas combi PE under-count + secondary "
|
||||
"heating credit are likely candidates for follow-up."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
@ -258,90 +256,93 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
cert_number="0380-2471-3250-2596-8761",
|
||||
actual_sap=89,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=-14.5971,
|
||||
expected_pe_resid_kwh_per_m2=+8.0916,
|
||||
expected_co2_resid_tonnes_per_yr=+0.2785,
|
||||
notes=(
|
||||
"Mitsubishi PUZ-WM50VHA PCDB 104568, semi-detached bungalow "
|
||||
"TFA 60.43 age D, PV 3 kWp. Worksheet SAP 88.5104 — slice "
|
||||
"102f-prep.1-9 closed cohort cascade SAP residual integer "
|
||||
"to 0. PE residual -14.79 stems mostly from PV cascade "
|
||||
"self-consumption β-factor split (Appendix M §3) — PE is "
|
||||
"computed at PCDB Table 172 postcode climate (demand pass) "
|
||||
"vs rating SAP at UK-avg, so PV self-consumption captures "
|
||||
"different export/import fractions."
|
||||
"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."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="0350-2968-2650-2796-5255",
|
||||
actual_sap=84,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=-7.7832,
|
||||
expected_pe_resid_kwh_per_m2=+2.7315,
|
||||
expected_co2_resid_tonnes_per_yr=+0.1709,
|
||||
notes=(
|
||||
"Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert. "
|
||||
"Worksheet SAP 84.1367 — cascade integer matches lodged."
|
||||
"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)."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="2225-3062-8205-2856-7204",
|
||||
actual_sap=89,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=-11.7684,
|
||||
expected_pe_resid_kwh_per_m2=+4.4804,
|
||||
expected_co2_resid_tonnes_per_yr=+0.2628,
|
||||
notes=(
|
||||
"Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with "
|
||||
"PV. Worksheet SAP 88.7921. Slice 102f-prep.8 closed the "
|
||||
"shower_outlets=None default (SAP residual -0.31 → +0.04)."
|
||||
"PV + 5 kWh battery. Worksheet SAP 88.7921. Slice S0380.45 "
|
||||
"shifted PE residual -11.77 → +4.48 via Appendix M1 β-split."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="2636-0525-2600-0401-2296",
|
||||
actual_sap=86,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=-9.6497,
|
||||
expected_co2_resid_tonnes_per_yr=+0.2200,
|
||||
expected_pe_resid_kwh_per_m2=+3.4216,
|
||||
expected_co2_resid_tonnes_per_yr=+0.2194,
|
||||
notes=(
|
||||
"Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert with "
|
||||
"PV + 3.74 m² cantilever exposed floor + 12.76 m² alt wall. "
|
||||
"Worksheet SAP 86.2641. Slice S0380.31 deducted the alt-wall "
|
||||
"window opening (1.19 m²) from (31) total external area per "
|
||||
"SAP 10.2 Appendix K eqn K2 — closed the SAP residual from "
|
||||
"-0.015 → -2.4e-6 and shifted PE -9.578 → -9.650."
|
||||
"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."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="3800-8515-0922-3398-3563",
|
||||
actual_sap=86,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=-9.6121,
|
||||
expected_pe_resid_kwh_per_m2=+3.5809,
|
||||
expected_co2_resid_tonnes_per_yr=+0.2609,
|
||||
notes=(
|
||||
"Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert. "
|
||||
"Worksheet SAP 86.1458."
|
||||
"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."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="9285-3062-0205-7766-7200",
|
||||
actual_sap=84,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=-7.9597,
|
||||
expected_pe_resid_kwh_per_m2=+3.1982,
|
||||
expected_co2_resid_tonnes_per_yr=+0.1571,
|
||||
notes=(
|
||||
"Mitsubishi PUZ-WM50VHA PCDB 104568, ASHP cohort cert. "
|
||||
"Worksheet SAP 84.1369."
|
||||
"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."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
cert_number="9418-3062-8205-3566-7200",
|
||||
actual_sap=85,
|
||||
expected_sap_resid=+0,
|
||||
expected_pe_resid_kwh_per_m2=-9.4849,
|
||||
expected_pe_resid_kwh_per_m2=+4.6681,
|
||||
expected_co2_resid_tonnes_per_yr=+0.2317,
|
||||
notes=(
|
||||
"Daikin Altherma EDLQ05CAV3 PCDB 102421 (heating_duration "
|
||||
"code '24' — continuous, all days at Th). Worksheet SAP "
|
||||
"84.6305. Slice 102f-prep.7 closed the Table N4 fixed-"
|
||||
"duration MIT cascade (-2°C → +0.03)."
|
||||
"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."
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue