mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.34: round living area in Decimal arithmetic per RdSAP10 §15 — closes cert 2536 +0.0007 SAP residual
RdSAP10 §15 p.66 (Rounding of data):
"All internal floor areas and living area: 2 d.p."
Cert 2536 (3 habitable rooms → Table 27 fraction 0.30,
TFA 45.65 m^2) sits ON the HALF_UP rounding boundary:
0.30 (exact) * 45.65 = 13.6950
HALF_UP 2 d.p. = 13.70
(worksheet fLA = 13.70 / 45.65 = 0.3001)
Float arithmetic drops the spec product BELOW the boundary:
0.30 (binary) ~= 0.2999999...
product ~= 13.69499...
HALF_UP 2 d.p. = 13.69
(cascade fLA = 13.69 / 45.65 = 0.29989)
The 0.00021 fLA shortfall feeds straight into the worksheet
(91) -> (92) MIT blend, undershoots MIT by ~0.001 C, and
shaves 0.29 kWh off (98c) useful space heating — a +0.0007
SAP residual via the (211) main heating fuel x p/kWh.
Compute the product in Decimal so HALF_UP lands on the exact
.005 decimal boundary the spec defines. Certs that sit off the
boundary (e.g. 2800/4800: 0.30 x 46.87 = 14.0610 -> 14.06 in
both Decimal and float) are unaffected.
Cohort-2 distribution after S0380.31..S0380.34:
36 exact + 2 <=0.07 (was 35 exact + 3 <=0.07).
Cert 2536: +0.000715 -> -9.2e-8.
The remaining 2800 / 4800 +0.0007 residuals come from a
different cause (off the HALF_UP boundary) — defer to a
separate slice.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1b12f995f8
commit
61c215bf1f
2 changed files with 43 additions and 2 deletions
|
|
@ -51,6 +51,7 @@ from __future__ import annotations
|
|||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import Callable, Final, Literal, Optional
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
|
|
@ -120,7 +121,6 @@ from domain.sap10_calculator.worksheet.solar_gains import (
|
|||
from domain.sap10_calculator.worksheet.heat_transmission import (
|
||||
DwellingExposure,
|
||||
HeatTransmission,
|
||||
_AREA_ROUND_DP,
|
||||
_round_half_up,
|
||||
heat_transmission_from_cert,
|
||||
)
|
||||
|
|
@ -509,11 +509,23 @@ def _living_area_fraction(
|
|||
2 d.p. half-up, then divided back by TFA to yield the LINE_91 that
|
||||
feeds the §7 zone blend. This roundtrip is why fixtures lodge
|
||||
e.g. 0.3001 (= 17.04/56.79) rather than the raw 0.30 Table 27 entry.
|
||||
|
||||
The multiplication runs in Decimal arithmetic so HALF_UP rounding
|
||||
lands on the exact decimal boundary the spec defines. Float Table 27
|
||||
fractions (e.g. 0.30 → 0.2999999...) otherwise drop products that
|
||||
sit on the .005 boundary below the round-up threshold, e.g. cert
|
||||
2536 (3 rooms, TFA 45.65): exact 0.30 × 45.65 = 13.6950 → 13.70;
|
||||
float gives 13.69499... → 13.69, propagating a −0.0007 SAP residual
|
||||
via the §7 MIT blend.
|
||||
"""
|
||||
fraction = _living_area_fraction_default(habitable_rooms_count)
|
||||
if total_floor_area_m2 <= 0.0:
|
||||
return fraction
|
||||
living_area_m2 = _round_half_up(fraction * total_floor_area_m2, _AREA_ROUND_DP)
|
||||
living_area_m2 = float(
|
||||
(Decimal(str(fraction)) * Decimal(str(total_floor_area_m2))).quantize(
|
||||
Decimal("0.01"), rounding=ROUND_HALF_UP
|
||||
)
|
||||
)
|
||||
return living_area_m2 / total_floor_area_m2
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -633,6 +633,35 @@ def test_living_area_fraction_uses_rdsap_table_27_by_habitable_rooms() -> None:
|
|||
assert inputs_four.living_area_fraction == 0.25
|
||||
|
||||
|
||||
def test_living_area_rounds_half_up_at_2_dp_decimal_boundary_per_rdsap_15() -> None:
|
||||
# Arrange — RdSAP10 §15 p.66 ("Rounding of data") requires "All
|
||||
# internal floor areas and living area: 2 d.p." Cert 2536 lodges
|
||||
# 3 habitable rooms (Table 27 fraction 0.30) and TFA 45.65 m². The
|
||||
# exact-decimal product 0.30 × 45.65 = 13.6950 sits ON the HALF_UP
|
||||
# rounding boundary and must round to 13.70 (away from zero). Float
|
||||
# representation drops 0.30 to 0.299999... and the product to
|
||||
# 13.69499..., taking the boundary below 13.6950 — without Decimal
|
||||
# arithmetic the cascade gets 13.69 instead and lodges fLA = 0.29989
|
||||
# instead of the worksheet's 0.30011, leaving a +0.0007 SAP residual
|
||||
# via the §7 MIT blend.
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
_living_area_fraction, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
# Act
|
||||
fla_boundary = _living_area_fraction(habitable_rooms_count=3, total_floor_area_m2=45.65)
|
||||
fla_off_boundary = _living_area_fraction(habitable_rooms_count=3, total_floor_area_m2=46.87)
|
||||
|
||||
# Assert — worksheet cert 2536 dr87-0001-000889 line (91) = 0.3001:
|
||||
# 0.30 × 45.65 = 13.6950 lands ON the half-up boundary and the spec-
|
||||
# faithful living area is 13.70 → fLA = 13.70 / 45.65 = 0.30011.
|
||||
assert abs(fla_boundary - (13.70 / 45.65)) <= 1e-12
|
||||
# Cert 2800 / 4800 (TFA 46.87) sit OFF the boundary: 0.30 × 46.87
|
||||
# = 14.0610 rounds down to 14.06 under HALF_UP, so the Decimal path
|
||||
# matches the float path and fLA = 14.06 / 46.87.
|
||||
assert abs(fla_off_boundary - (14.06 / 46.87)) <= 1e-12
|
||||
|
||||
|
||||
def test_main_heating_efficiency_reads_sap_main_heating_code() -> None:
|
||||
# Arrange — Direction check: a gas combi (Table 4b code 102, 84% eff)
|
||||
# vs a non-condensing gas boiler (code 105, 70% eff) must show through
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue