mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.42: Decimal HALF_UP per-window areas per RdSAP10 §15 — closes cert 1536
Cert 1536 lodged window dimensions including (0.65 × 0.70) × 3
windows. In float arithmetic 0.65 × 0.70 = 0.45499999999999996,
which the `_round_half_up(float, dp)` helper snaps to 0.45 vs the
spec answer 0.46 (Decimal: 0.65 × 0.70 = 0.4550 exact, HALF_UP at
2 d.p. = 0.46). The shortfall of 0.01 m² × 3 windows = 0.03 m²
under-counted as ~0.073 W/K of conduction loss vs the worksheet's
windows_w_per_k = 25.6354 — closing the cert 1536 residual at
+0.00152 to <2e-6.
Same class of bug as the S0380.34/35 living-area / gross-wall /
party-wall closures (Decimal HALF_UP at the 0.005 boundary that
float drops). RdSAP10 §15 (p.66) lists "all element areas (gross)
including window areas: 2 d.p." — Decimal is the only arithmetic
that matches that boundary deterministically.
Three cascade sites now use Decimal HALF_UP for per-window areas:
- heat_transmission.py: `_decimal_round_half_up_product(W, H, 2)`
replaces `_round_half_up(W × H, 2)` at the windows_w_per_k cascade
AND at the per-bp window-area accumulation (the wall-net deduction
branch must agree with the conduction branch for cascade-internal
consistency, per the existing comment at line 575-583).
- internal_gains.py: `_decimal_window_area_2dp(W, H)` replaces the
inline `_round_area_2dp(W × H)` in the daylight factor `g_l`
sum so §5 (66)..(67) sees the same per-window areas as §3 (27).
- solar_gains.py: same Decimal helper replaces `_round_area_2dp` in
`_wall_window_solar_gain_monthly_w` so §6 (74)..(81) area = (27).
The `_round_area_2dp` helpers were inlined per-module in pre-S0380.42
work; this slice deletes them since the Decimal-aware product
replaces all call sites. `_round_half_up` stays in heat_transmission
for non-product per-element area calls (single-value rounds).
Test impact:
- Cohort-2 cert 1536 API path: +0.00152 → -1e-6 (<1e-4 ✓).
Moves from _COHORT_2_API_OPEN to _COHORT_2_API_CLOSED. Cohort
distribution: 37/38 exact (was 34/38 at start of session);
only cert 2102 (-6.30 secondary-heating routing) remains open.
- Cohort-2 cert 0300/9380 unchanged (already <1e-4 after S0380.41).
- Cohort-1 ASHP 9/9 unchanged: <1e-4 on both paths.
- Elmhurst 6-cert worksheet sweep: unchanged (lodges
`window_width=area, window_height=1.0` per the Elmhurst lodging
convention — Decimal(area) × Decimal(1.0) = Decimal(area), no
rounding shift).
Test suite: 750 pass + 0 fail. Pyright net-zero per touched file
(heat_transmission 13/13; internal_gains 4/4 pre-existing; solar_gains
0/0; chain test 0/0).
Spec citation: RdSAP 10 Specification §15 "Rounding of data" p.66 —
"All element areas (gross) including window areas and conservatory
wall area: 2 d.p." Decimal is the float-precision-stable arithmetic
that matches this rule at the .005 boundary.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a96e6765ab
commit
e1b7b30c40
4 changed files with 44 additions and 28 deletions
|
|
@ -1912,6 +1912,7 @@ _COHORT_2_API_CLOSED: list[tuple[str, float]] = [
|
|||
("0390-2066-4250-2026-4555", 65.3253),
|
||||
("0464-3032-0205-4276-3204", 80.4533),
|
||||
("0652-3022-1205-2826-1200", 70.9577),
|
||||
("1536-9325-5100-0433-1226", 65.8928), # S0380.42 closure
|
||||
("2007-3011-9205-8136-3204", 68.3914),
|
||||
("2031-3007-0205-1296-3204", 64.1734),
|
||||
("2130-3018-4205-4686-5204", 71.3158),
|
||||
|
|
@ -1958,13 +1959,6 @@ _COHORT_2_API_CLOSED: list[tuple[str, float]] = [
|
|||
# API mapper likely lodges the secondary fuel differently. Probe
|
||||
# the API JSON's `secondary_heating` block first.
|
||||
_COHORT_2_API_OPEN: list[tuple[str, float, float]] = [
|
||||
# S0380.41 partially closed this cert: residual moved from +0.4445
|
||||
# to +0.0015 via the same RdSAP 21 → SAP 10.2 glazing-type alias
|
||||
# that closed 0300/9380 cleanly. A sub-2e-3 secondary tail remains
|
||||
# — likely a windows-area Decimal-rounding boundary case in the
|
||||
# cert's specific dimensions; investigate per the cohort closure
|
||||
# convention in Slice S0380.42.
|
||||
("1536-9325-5100-0433-1226", 65.8928, 65.894324),
|
||||
("2102-3018-0205-7886-5204", 63.8732, 57.570156),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,19 @@ def _round_half_up(value: float, dp: int) -> float:
|
|||
return -floor(-value * factor + 0.5) / factor
|
||||
|
||||
|
||||
def _decimal_round_half_up_product(a: float, b: float, dp: int) -> float:
|
||||
"""a × b in Decimal arithmetic, HALF_UP-quantised at `dp` decimal
|
||||
places. Mirrors `_round_half_up(a * b, dp)` but lands on the exact
|
||||
.005 spec boundary that float multiplication drops (e.g. window
|
||||
dimensions 0.65 × 0.70 = 0.4550 exact / 0.45499... in float —
|
||||
float `_round_half_up` snaps to 0.45 vs spec answer 0.46). Used
|
||||
for per-window area rounding under the RdSAP10 §15 "round per
|
||||
element area" convention; the §15 Σ-then-round counterpart is
|
||||
`_decimal_round_half_up_sum`."""
|
||||
d = Decimal(str(a)) * Decimal(str(b))
|
||||
return float(d.quantize(Decimal(10) ** -dp, rounding=ROUND_HALF_UP))
|
||||
|
||||
|
||||
_WALL_INSULATION_NONE: Final[int] = 4
|
||||
_DEFAULT_DOOR_AREA_M2: Final[float] = 1.85
|
||||
_DEFAULT_STOREY_HEIGHT_M: Final[float] = 2.5
|
||||
|
|
@ -476,8 +489,8 @@ def heat_transmission_from_cert(
|
|||
if windows_have_per_window_u:
|
||||
windows_w_per_k_total = 0.0
|
||||
for w in epc.sap_windows or []:
|
||||
a_w = _round_half_up(
|
||||
float(w.window_width) * float(w.window_height), _AREA_ROUND_DP
|
||||
a_w = _decimal_round_half_up_product(
|
||||
float(w.window_width), float(w.window_height), _AREA_ROUND_DP
|
||||
)
|
||||
u_raw_w = float(w.window_transmission_details.u_value) # type: ignore[union-attr]
|
||||
u_eff_w = (
|
||||
|
|
@ -583,9 +596,8 @@ def heat_transmission_from_cert(
|
|||
# cascade-internal consistency.
|
||||
for w in epc.sap_windows:
|
||||
idx = _window_bp_index(w.window_location, len(parts))
|
||||
area = _round_half_up(
|
||||
float(w.window_width) * float(w.window_height),
|
||||
_AREA_ROUND_DP,
|
||||
area = _decimal_round_half_up_product(
|
||||
float(w.window_width), float(w.window_height), _AREA_ROUND_DP,
|
||||
)
|
||||
window_area_by_bp[idx] += area
|
||||
if _window_on_alt_wall(w):
|
||||
|
|
|
|||
|
|
@ -24,19 +24,25 @@ factor for L2a daylighting calc).
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from enum import Enum
|
||||
from math import cos, exp, floor, pi
|
||||
from math import cos, exp, pi
|
||||
from typing import Final, Optional
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData, SapWindow
|
||||
|
||||
|
||||
def _round_area_2dp(value: float) -> float:
|
||||
"""Half-away-from-zero rounding to 2 d.p. matching heat_transmission.
|
||||
RdSAP 10 §15 "Rounding of data" (p.66): "All element areas (gross)
|
||||
including window areas: 2 d.p." Inlined rather than imported so this
|
||||
module doesn't reach into heat_transmission's private helpers."""
|
||||
return floor(value * 100.0 + 0.5) / 100.0
|
||||
def _decimal_window_area_2dp(width: float, height: float) -> float:
|
||||
"""W × H in Decimal arithmetic, HALF_UP-quantised at 2 d.p. Per
|
||||
RdSAP10 §15 "Rounding of data" (p.66) "All element areas (gross)
|
||||
including window areas: 2 d.p." — uses Decimal so the lookup lands
|
||||
on the exact .005 spec boundary that float multiplication drops
|
||||
(e.g. 0.65 × 0.70 = 0.4550 exact / 0.45499... in float — float
|
||||
rounding snaps to 0.45 vs spec 0.46). Matches `heat_transmission.
|
||||
_decimal_round_half_up_product` so the daylight factor's per-window
|
||||
areas agree with the fabric cascade."""
|
||||
d = Decimal(str(width)) * Decimal(str(height))
|
||||
return float(d.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
|
||||
|
||||
_DAYS_PER_YEAR: Final[float] = 365.0
|
||||
_APPLIANCES_E_A_COEFF: Final[float] = 207.8
|
||||
|
|
@ -600,7 +606,7 @@ def _daylight_factor_from_cert(
|
|||
# including window areas: 2 d.p." — mirrors solar_gains and heat_
|
||||
# transmission so G_L sees the same area as the fabric cascade.
|
||||
wall_g_l_numerator = sum(
|
||||
_round_area_2dp(float(w.window_width) * float(w.window_height))
|
||||
_decimal_window_area_2dp(float(w.window_width), float(w.window_height))
|
||||
* _g_light(w) * _frame_factor(w) * z_l
|
||||
for w in epc.sap_windows
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ coefficient sets (N, NE/NW, E/W, SE/SW, S).
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from enum import Enum
|
||||
from math import cos, floor, radians, sin
|
||||
from math import cos, radians, sin
|
||||
from typing import Final
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData, SapWindow
|
||||
|
|
@ -43,12 +44,15 @@ from domain.sap10_calculator.climate.appendix_u import (
|
|||
from domain.sap10_calculator.worksheet.internal_gains import OvershadingCategory
|
||||
|
||||
|
||||
def _round_area_2dp(value: float) -> float:
|
||||
"""Half-away-from-zero rounding to 2 d.p. matching heat_transmission.
|
||||
RdSAP 10 §15 "Rounding of data" (p.66): "All element areas (gross)
|
||||
including window areas: 2 d.p." Inlined rather than imported so this
|
||||
module doesn't reach into heat_transmission's private helpers."""
|
||||
return floor(value * 100.0 + 0.5) / 100.0
|
||||
def _decimal_window_area_2dp(width: float, height: float) -> float:
|
||||
"""W × H in Decimal arithmetic, HALF_UP-quantised at 2 d.p. Mirrors
|
||||
`_round_area_2dp(W × H)` but lands on the exact .005 spec boundary
|
||||
that float multiplication drops (e.g. 0.65 × 0.70 = 0.4550 exact /
|
||||
0.45499... in float — float rounding snaps to 0.45 vs spec 0.46).
|
||||
Matches `heat_transmission._decimal_round_half_up_product` so solar
|
||||
gains' per-window areas agree with the fabric cascade."""
|
||||
d = Decimal(str(width)) * Decimal(str(height))
|
||||
return float(d.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
|
||||
|
||||
|
||||
# Table 6d first column — winter solar access factor Z for heating gains.
|
||||
|
|
@ -331,7 +335,7 @@ def _vertical_window_gain_monthly_w(
|
|||
# RdSAP 10 §15 "Rounding of data" (p.66): "All element areas (gross)
|
||||
# including window areas: 2 d.p." — matches heat_transmission's per-
|
||||
# window area rounding so solar gains and conduction agree on area.
|
||||
area = _round_area_2dp(float(w.window_width) * float(w.window_height))
|
||||
area = _decimal_window_area_2dp(float(w.window_width), float(w.window_height))
|
||||
g_perp = _g_perpendicular(w)
|
||||
ff = _frame_factor(w)
|
||||
return tuple(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue