mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.82: Table 12a Grid 2 dual-rate CO2 + PE for pumps/lighting/shower on off-peak certs
Per SAP 10.2 Table 12a Grid 2 (PDF p.191) + Table 12d / 12e (PDF p.194-195):
Table 12a Grid 2 row "All other uses" (lighting + pumps + locally
generated electricity + ... ) × tariff column:
SEVEN_HOUR → 0.90 high-rate fraction
TEN_HOUR → 0.80 high-rate fraction
Table 12d header (p.194): "Where electricity is the fuel used, the
relevant set of factors in the table below should be used to calculate
the monthly CO2 emissions INSTEAD of the annual average factor given
in Table 12."
Identical wording on Table 12e (p.195) for primary energy. The cascade
must therefore blend Table 12d / 12e high-rate × low-rate codes for the
end-uses billing through Grid 2 ALL_OTHER_USES — code 31/32 on 7-hour
and code 33/34 on 10-hour — weighted by each end-use's monthly kWh
profile.
S0380.65 landed this for `main_heating_co2_factor` via Grid 1 SH. The
mirror for the "other uses" trio (lighting / pumps_fans / electric_
shower) was queued. This slice closes it.
Implementation:
- New `_other_use_co2_factor_kg_per_kwh(other_use, tariff, monthly_kwh)`
helper mirrors `_main_heating_co2_factor_kg_per_kwh` but dispatches
through `other_use_high_rate_fraction(OtherUse.ALL_OTHER_USES,
tariff)`. STANDARD passes through to single-code-30 monthly; SEVEN /
TEN_HOUR blend; EIGHTEEN_HOUR / TWENTY_FOUR_HOUR fall through to
single-code-30 since Grid 2 lists no row for them.
- `_other_use_primary_factor(...)` is the PE-side mirror via Table 12e.
- Wired into `CalculatorInputs.{pumps_fans, lighting, electric_shower}_
{co2_factor, primary_factor}` in the `cert_to_inputs` orchestrator.
Cert 000565 movement at HEAD (this commit):
lighting_co2_factor_kg_per_kwh 0.1443 → 0.1483 (Δ +0.0040)
pumps_fans_co2_factor_kg_per_kwh 0.1387 → 0.1427 (Δ +0.0040)
electric_shower_co2_factor_kg_per_kwh 0.1391 → 0.1431 (Δ +0.0040)
→ CO2 residual Δ−8.92 → Δ−3.08 kg/yr (65% closed)
Cohort impact: STANDARD-tariff certs pass through the single-code-30
monthly cascade (identical to the previous `_effective_monthly_co2_
factor(..., _STANDARD_ELECTRICITY_FUEL_CODE)` call). All Elmhurst U985
cohort fixtures + golden cohort run STANDARD → zero shift. Cert 000565
is the only off-peak fixture; its CO2 closes by 5.9 kg/yr.
Test baseline: 554 pass + 8 expected `test_sap_result_pin[000565-*]`
fails (was 553 + 8 at S0380.81; one new test pinning the spec rule).
The 8 cert 000565 fails remain at sub-1e-4 tolerances — sap_score
already EXACT, hot_water_kwh EXACT. CO2 residual closer but not yet
< 1e-4 since lighting +2.2 kWh and pumps_fans +2.5 kWh sub-spec
residuals leak into CO2 too. Closes when those land.
Pyright net-zero on touched files (45 errors, matches baseline).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8626c5a932
commit
2d9cb995e6
2 changed files with 157 additions and 12 deletions
|
|
@ -1572,6 +1572,91 @@ def _main_heating_primary_factor(
|
|||
return high_frac * high_factor + (1.0 - high_frac) * low_factor
|
||||
|
||||
|
||||
def _other_use_co2_factor_kg_per_kwh(
|
||||
other_use: OtherUse,
|
||||
tariff: Tariff,
|
||||
monthly_kwh: tuple[float, ...],
|
||||
) -> Optional[float]:
|
||||
"""SAP 10.2 Table 12a Grid 2 (PDF p.191) + Table 12d (PDF p.194)
|
||||
dual-rate monthly CO2 factor for "other electricity uses" (lighting,
|
||||
pumps + fans, electric shower, etc.).
|
||||
|
||||
Per Table 12d header (p.194): "Where electricity is the fuel used,
|
||||
the relevant set of factors in the table below should be used to
|
||||
calculate the monthly CO2 emissions INSTEAD of the annual average
|
||||
factor given in Table 12." For STANDARD tariff this means single
|
||||
Table 12d code 30 monthly factors weighted by the end-use's profile.
|
||||
For Grid-2-eligible off-peak tariffs (SEVEN_HOUR / TEN_HOUR) the
|
||||
Grid 2 ALL_OTHER_USES / FANS_FOR_MECH_VENT high-rate fraction
|
||||
blends Table 12d high-rate × low-rate codes per:
|
||||
|
||||
F_blended = high_frac × F_high + (1 − high_frac) × F_low
|
||||
|
||||
Grid 2 doesn't list EIGHTEEN_HOUR / TWENTY_FOUR_HOUR rows; those
|
||||
tariffs fall through to single-code-30 monthly.
|
||||
|
||||
Mirrors `_main_heating_co2_factor_kg_per_kwh` for the Grid 2
|
||||
end-uses. Returns None when the cascade can't form a factor (zero
|
||||
monthly kWh in every month); callers fall back to the annual
|
||||
`_STANDARD_ELECTRICITY_FUEL_CODE` Table 12 value."""
|
||||
if tariff is Tariff.STANDARD:
|
||||
return _effective_monthly_co2_factor(
|
||||
monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
)
|
||||
try:
|
||||
high_frac = other_use_high_rate_fraction(other_use, tariff)
|
||||
except NotImplementedError:
|
||||
return _effective_monthly_co2_factor(
|
||||
monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
)
|
||||
codes = _TARIFF_HIGH_LOW_FUEL_CODES_TABLE_12.get(tariff)
|
||||
if codes is None:
|
||||
return _effective_monthly_co2_factor(
|
||||
monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
)
|
||||
high_code, low_code = codes
|
||||
high_factor = _effective_monthly_co2_factor(monthly_kwh, high_code)
|
||||
low_factor = _effective_monthly_co2_factor(monthly_kwh, low_code)
|
||||
if high_factor is None or low_factor is None:
|
||||
return None
|
||||
return high_frac * high_factor + (1.0 - high_frac) * low_factor
|
||||
|
||||
|
||||
def _other_use_primary_factor(
|
||||
other_use: OtherUse,
|
||||
tariff: Tariff,
|
||||
monthly_kwh: tuple[float, ...],
|
||||
) -> Optional[float]:
|
||||
"""SAP 10.2 Table 12a Grid 2 (PDF p.191) + Table 12e (PDF p.195)
|
||||
dual-rate monthly PE factor for "other electricity uses" — PE-side
|
||||
mirror of `_other_use_co2_factor_kg_per_kwh`. Same dispatch shape:
|
||||
STANDARD tariff → code 30 monthly cascade; SEVEN_HOUR / TEN_HOUR →
|
||||
Grid 2 ALL_OTHER_USES / FANS_FOR_MECH_VENT blend; EIGHTEEN_HOUR /
|
||||
TWENTY_FOUR_HOUR fall through to single-code-30. Returns None for
|
||||
the zero-monthly-kWh degenerate case."""
|
||||
if tariff is Tariff.STANDARD:
|
||||
return _effective_monthly_pe_factor(
|
||||
monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
)
|
||||
try:
|
||||
high_frac = other_use_high_rate_fraction(other_use, tariff)
|
||||
except NotImplementedError:
|
||||
return _effective_monthly_pe_factor(
|
||||
monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
)
|
||||
codes = _TARIFF_HIGH_LOW_FUEL_CODES_TABLE_12.get(tariff)
|
||||
if codes is None:
|
||||
return _effective_monthly_pe_factor(
|
||||
monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
)
|
||||
high_code, low_code = codes
|
||||
high_factor = _effective_monthly_pe_factor(monthly_kwh, high_code)
|
||||
low_factor = _effective_monthly_pe_factor(monthly_kwh, low_code)
|
||||
if high_factor is None or low_factor is None:
|
||||
return None
|
||||
return high_frac * high_factor + (1.0 - high_frac) * low_factor
|
||||
|
||||
|
||||
def _hot_water_co2_factor_kg_per_kwh(
|
||||
epc: EpcPropertyData,
|
||||
hw_monthly_kwh: tuple[float, ...],
|
||||
|
|
@ -4067,19 +4152,25 @@ def cert_to_inputs(
|
|||
epc,
|
||||
wh_result.output_monthly_kwh if wh_result is not None else (0.0,) * 12,
|
||||
),
|
||||
pumps_fans_co2_factor_kg_per_kwh=_effective_monthly_co2_factor(
|
||||
# SAP 10.2 Table 12a Grid 2 (p.191) + Table 12d (p.194): pumps,
|
||||
# lighting, and the electric-shower end-use all bill via the
|
||||
# "All other uses" row → on off-peak tariffs blend the high /
|
||||
# low Table 12d codes per the Grid 2 fraction. STANDARD tariff
|
||||
# passes through to single-code-30 monthly. Mirrors the main-
|
||||
# heating Grid 1 split landed in S0380.65.
|
||||
pumps_fans_co2_factor_kg_per_kwh=_other_use_co2_factor_kg_per_kwh(
|
||||
OtherUse.ALL_OTHER_USES, _rdsap_tariff(epc),
|
||||
_days_in_month_proportioned(pumps_fans_kwh, _DAYS_IN_MONTH),
|
||||
_STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
),
|
||||
lighting_co2_factor_kg_per_kwh=_effective_monthly_co2_factor(
|
||||
lighting_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
lighting_co2_factor_kg_per_kwh=_other_use_co2_factor_kg_per_kwh(
|
||||
OtherUse.ALL_OTHER_USES, _rdsap_tariff(epc), lighting_monthly_kwh,
|
||||
),
|
||||
electric_shower_kwh_per_yr=(
|
||||
wh_result.electric_shower_kwh_per_yr if wh_result is not None else 0.0
|
||||
),
|
||||
electric_shower_co2_factor_kg_per_kwh=_effective_monthly_co2_factor(
|
||||
electric_shower_co2_factor_kg_per_kwh=_other_use_co2_factor_kg_per_kwh(
|
||||
OtherUse.ALL_OTHER_USES, _rdsap_tariff(epc),
|
||||
wh_result.electric_shower_monthly_kwh if wh_result is not None else (0.0,) * 12,
|
||||
_STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
),
|
||||
pv_generation_kwh_per_yr=_pv_generation_kwh_per_yr(epc, climate),
|
||||
pv_export_credit_gbp_per_kwh=_pv_export_credit_gbp_per_kwh(),
|
||||
|
|
@ -4140,16 +4231,18 @@ def cert_to_inputs(
|
|||
secondary_heating_primary_factor=_secondary_heating_primary_factor(
|
||||
epc, energy_requirements_result.secondary_fuel_monthly_kwh,
|
||||
),
|
||||
pumps_fans_primary_factor=_effective_monthly_pe_factor(
|
||||
# PE-side mirror of the Grid 2 dual-rate CO2 blend above —
|
||||
# Table 12a Grid 2 (p.191) + Table 12e (p.195).
|
||||
pumps_fans_primary_factor=_other_use_primary_factor(
|
||||
OtherUse.ALL_OTHER_USES, _rdsap_tariff(epc),
|
||||
_days_in_month_proportioned(pumps_fans_kwh, _DAYS_IN_MONTH),
|
||||
_STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
),
|
||||
lighting_primary_factor=_effective_monthly_pe_factor(
|
||||
lighting_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
lighting_primary_factor=_other_use_primary_factor(
|
||||
OtherUse.ALL_OTHER_USES, _rdsap_tariff(epc), lighting_monthly_kwh,
|
||||
),
|
||||
electric_shower_primary_factor=_effective_monthly_pe_factor(
|
||||
electric_shower_primary_factor=_other_use_primary_factor(
|
||||
OtherUse.ALL_OTHER_USES, _rdsap_tariff(epc),
|
||||
wh_result.electric_shower_monthly_kwh if wh_result is not None else (0.0,) * 12,
|
||||
_STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
),
|
||||
fuel_cost=_fuel_cost(
|
||||
epc=epc,
|
||||
|
|
|
|||
|
|
@ -2090,6 +2090,58 @@ def test_air_source_heat_pump_main_heating_zeroes_table_3a_combi_loss_per_sap_4_
|
|||
)
|
||||
|
||||
|
||||
def test_lighting_co2_factor_blends_table_12a_grid_2_with_table_12d_dual_rate_on_off_peak_certs() -> None:
|
||||
"""SAP 10.2 Table 12a Grid 2 (PDF p.191) + Table 12d (PDF p.194) —
|
||||
"other electricity uses" (lighting, pumps + fans, electric shower) on
|
||||
an off-peak tariff blend the dual-rate Table 12d high/low monthly CO2
|
||||
factors per the Grid 2 ALL_OTHER_USES high-rate fraction. From the
|
||||
spec text on p.194:
|
||||
|
||||
"Where electricity is the fuel used, the relevant set of factors
|
||||
in the table below should be used to calculate the monthly CO2
|
||||
emissions INSTEAD of the annual average factor given in
|
||||
Table 12."
|
||||
|
||||
And Table 12a Grid 2 (PDF p.191) "Other electricity uses" row
|
||||
"All other uses" × 10-hour tariff = 0.80 high-rate fraction.
|
||||
|
||||
Cert 000565 is on a Dual meter routed via §12 Rule 3 (heat pump
|
||||
main → TEN_HOUR). The lighting CO2 factor must blend Table 12d
|
||||
code 34 (10h high) and code 33 (10h low) monthly factors weighted
|
||||
by the L11 lighting profile, NOT use code 30 alone (Slice S0380.65
|
||||
landed this for main_heating; lighting / pumps_fans / electric_
|
||||
shower were still on the code-30-only path).
|
||||
|
||||
Pre-S0380.82 cert 000565 cascade: lighting factor 0.1443 (code 30
|
||||
monthly × L11 profile). Post: 0.1469 (Grid 2 blend) — pushes the
|
||||
cohort CO2 residual from −8.92 kg/yr toward zero on the lighting
|
||||
+ pumps_fans + electric_shower trio.
|
||||
"""
|
||||
# Arrange — mapper-driven cohort fixture (Dual meter / TEN_HOUR
|
||||
# tariff, heat-pump main).
|
||||
from domain.sap10_calculator.worksheet.tests import (
|
||||
_elmhurst_worksheet_000565 as _w000565,
|
||||
)
|
||||
epc = _w000565.build_epc()
|
||||
|
||||
# Act
|
||||
inputs = cert_to_inputs(epc)
|
||||
|
||||
# Assert — lighting CO2 factor lifted above the code-30-only baseline
|
||||
# by the Grid 2 dual-rate blend. Pre-S0380.82 value 0.1443; post-fix
|
||||
# ≥ 0.146 per the 0.80-weighted code 34 + 0.20-weighted code 33
|
||||
# cascade.
|
||||
pre_fix_baseline = 0.1444 # code 30 monthly × L11 profile
|
||||
factor = inputs.lighting_co2_factor_kg_per_kwh
|
||||
assert factor is not None and factor > pre_fix_baseline + 0.001, (
|
||||
f"lighting_co2_factor_kg_per_kwh = {factor!r}; expected dual-rate "
|
||||
f"Grid 2 blend > {pre_fix_baseline + 0.001:.4f} per SAP 10.2 "
|
||||
f"Table 12a Grid 2 (p.191) + Table 12d (p.194). The cascade was "
|
||||
f"applying code 30 alone — must now blend code 34 (10h high) and "
|
||||
f"code 33 (10h low) at the 0.80 / 0.20 split."
|
||||
)
|
||||
|
||||
|
||||
def test_rdsap_10_table_32_prices_charge_mains_gas_hot_water_at_3p48_per_kwh() -> None:
|
||||
"""RdSAP 10 Specification §19.1 (PDF page 80-81) — the §10a fuel-cost
|
||||
block uses RdSAP 10 Table 32 (PDF page 95) prices, NOT SAP 10.2
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue