mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
11 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
78c57c0dc7 |
Slice S0380.94: RIR insulation "400+ mm PUR or PIR" extractor + mapper + cascade (RdSAP 10 Table 17 col 3b)
RdSAP 10 §5.11.3 + Table 17 (PDF p.42-43) "Roof room U-values when
insulation thickness is known". Column (3b) "Stud wall — PUR or PIR
optional" 400 mm row → 0.10 W/m²K. Cert 000565 Summary §8.1 BP[2] Ext2
(Detailed) lodges:
Stud Wall 2 2.00 × 2.00 400+ mm PUR or PIR Default U=0.10
Pre-slice three coupled bugs silently dropped the lodgement, routing
the cascade through the uninsulated Table 17 row 0 (U=2.30) — over-
counting Stud Wall 2 by (2.30 − 0.10) × 4 m² = +8.80 W/K on roof:
1. **Extractor regex** `_RIR_INSULATION_THICKNESS_RE = ^\d+\s*mm$`
failed to match the "400+ mm" bucket-cap form (Table 17's largest
tabulated row is annotated with a trailing "+" in the Summary).
2. **Extractor insulation_type allow-list** `("Mineral or EPS",
"PUR", "PIR")` failed to match the disjunction "PUR or PIR" — the
actual Summary form when the assessor doesn't distinguish PUR from
PIR. (Both columns Table 17 column (b) anyway.)
3. **Mapper thickness parser** `_elmhurst_rir_insulation_thickness_mm`
used the same `^\d+\s*mm$` regex — also failed on "400+ mm".
Plus a fourth coupled fix: the cascade's `_is_rigid_foam` checked a
frozenset `{"pur", "pir", "rigid"}` that didn't include the canonical
mapper-side code "rigid_foam" — even if the mapper translated "PUR or
PIR" → "rigid_foam", the cascade would route to column (a) mineral-
wool instead of column (b) rigid-foam.
Slice span (4 layers):
1. **Extractor regex** — `^\d+\+?\s*mm$` matches both "100 mm" and
"400+ mm".
2. **Extractor allow-list** — add "PUR or PIR" alongside individual
"PUR" / "PIR" + "Mineral or EPS".
3. **Mapper** — `_RIR_INSULATION_TYPE_TO_SAP10` canonicalises all
rigid-foam strings to "rigid_foam"; thickness parser regex matches
"400+ mm" → 400 mm int.
4. **Cascade** — `_RR_RIGID_FOAM_INSULATION_TYPES` adds "rigid_foam"
alongside the legacy "pur"/"pir"/"rigid" aliases.
Cert 000565 movement (HEAD `23aaa4fa` → this slice):
- cascade BP[2] Ext2 Stud Wall 2 U: 2.30 → 0.10 ✓ EXACT vs ws 0.10
- cascade roof_w_per_k: 43.44 → 34.64 (Δ−7.94 → Δ−16.74)
- sap_score: 29 ✓ EXACT unchanged
- sap_score_continuous: 28.81 → 29.02 (Δ+0.26 → Δ+0.51)
- space_heating_kwh: −427 → −685
- main_heating_fuel: −251 → −403
- hot_water_kwh: ✓ 0 EXACT unchanged
Closing one spec-correct sub-component while others remain non-spec-
correct drifts continuous SAP further; per user direction temporary
drift is acceptable as long as we're fixing true intermediate-value
problems — once every sub-component is spec-correct, the continuous
SAP error closes to zero by construction. The remaining −16.74 W/K
roof gap localises to:
- BP[0/1/3] missing RR residual area for Detailed-RR mode (§3.10.1
spec — cascade only handles Simplified mode today); +27.85 W/K
closure when wired.
- BP[4] Flat Ceiling 1 lodges "Unknown thickness, PUR or PIR" → ws
U=0.15; cascade over-counts at 2.30 (uninsulated). Elmhurst's
"Unknown PUR or PIR" → 200 mm convention is non-spec; the spec-
correct path falls back to Table 18 col 4 default (`u_rr_default
_all_elements`). Separate diagnostic slice.
Cohort safety: 21 other Elmhurst Summary fixtures lodge no RIR detailed
surfaces with "400+ mm" or "PUR or PIR" (modal cohort uses As Built /
None / no detailed surfaces). Existing "Mineral or EPS" tests at
`test_u_rr_stud_wall_table17_col3a_mineral_wool_100mm_returns_0_36`
remain green — the new aliases extend rather than replace.
Test baseline: 585 pass + 8 expected `000565` fails (was 583 + 8; +2
new tests). Pyright net-zero per touched file (0/32/1/65/13 preserved).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
23aaa4fa66 |
Slice S0380.93: floor above partially-heated space U=0.7 (RdSAP 10 §5.14)
RdSAP 10 §5.14 (PDF p.47) "U-value of floor above a partially heated space": > "The U-value of a floor above partially heated premises is taken as > 0.7 W/m²K. This applies typically for a flat above non-domestic > premises that are not heated to the same extent or duration as the > flat." Cert 000565 Ext1 lodges Summary §9 "Location: P Above partially heated space" + "Default U-value: 0.70". Worksheet line (28b) confirms "Exposed floor Ext1 ... 34.0000 0.7000 23.8000". Pre-slice the cascade routed BP[1] floor through the BS EN ISO 13370 ground-floor formula (the "else" branch of the floor U-value dispatch in `heat_transmission.py`) — producing cascade U=0.76 vs spec 0.70. Over-counted floor heat loss by (0.76 − 0.70) × 34 m² = +2.04 W/K on the part subtotal and on the total HTC. Slice span (4 layers): 1. **Helper** — `u_floor_above_partially_heated_space()` in `domain/sap10_ml/rdsap_uvalues.py`, verbatim spec constant 0.7 (no age-band / insulation-thickness inputs). Lives in `sap10_ml` per [[project-sap10_ml-deprecation]] (edit existing file fine). 2. **Schema** — `SapFloorDimension.is_above_partially_heated_space: bool = False` (parallel to existing `is_exposed_floor`). Mutually exclusive with the exposed-floor / basement-floor branches. 3. **Mapper** — new `_is_floor_above_partially_heated_space(location)` helper detecting "above partially heated" in the Elmhurst §9 floor location string. Plumbed into `_map_elmhurst_building_part` floor- dim construction; only applies to the ground floor (i==0). 4. **Cascade** — `heat_transmission.py` adds a new branch between the exposed-floor and ground-floor branches: `is_above_partial → u_floor_above_partially_heated_space()`. Cert 000565 movement (HEAD `a7894b11` → this slice): - cascade floor_w_per_k: 72.41 → 70.37 (Δ +10.74 → Δ +8.70) - cascade BP[1] floor U: 0.76 → 0.70 (✓ EXACT vs ws 0.70) - sap_score (integer): 29 ✓ EXACT (unchanged — at goal) - sap_score_continuous: 28.7663 → 28.8131 (+0.0468 drift) - space_heating_kwh: −367 → −427 (small drift further under) - main_heating_fuel: −216 → −251 (downstream of SH) - co2_kg_per_yr: −32 → −37 - total_fuel_cost_gbp: −23 → −27 - hot_water_kwh: ✓ 0 EXACT unchanged The small continuous-SAP drift is the expected arithmetic of closing a single component when adjacent components remain unclosed (floor +10.74 was cancelling thermal_bridging −11.76 + roof −7.94 at the net-HTC level). Per [[feedback-zero-error-strict]] + [[feedback- spec-citation-in-commits]] the spec-correct slice ships regardless of transient continuous-SAP drift; remaining residual components (floor +8.70 from BP[2] Ext2 lodged 200 mm insulation thickness; roof −7.94; thermal_bridging −11.76; walls −1.67) each get their own spec-cited slice. Cohort safety: only cert 000565 Ext1 in the cohort lodges "Above partially heated space". All other Elmhurst cohort fixtures + 9 golden + 38 cohort-2 API certs default to `is_above_partially_ heated_space=False` so cascade behaviour is unchanged. Test baseline: 583 pass + 8 expected `000565` fails (was 582 + 8; +1 new mapper-chain test). Pyright net-zero per touched file (1/65/1/32/13/13 preserved). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
8321863015 |
Slice S0380.91: party-wall Cavity-masonry-filled U=0.2 (RdSAP 10 Table 15 row 3)
RdSAP 10 §5.10 Table 15 (PDF p.42) "U-values of party walls":
Party wall type U
--------------------------------------------- ----
Solid masonry / timber frame / system built 0.0
Cavity masonry unfilled 0.5
Cavity masonry filled 0.2
Unable to determine, house or bungalow 0.25
Unable to determine, flat or maisonette* 0.0
Pre-slice the cascade collapsed CF (Cavity masonry filled) into the same
SAP10 wall_construction code 4 as CU (Cavity masonry unfilled), so the
filled-cavity row's spec U=0.2 was silently rounded up to the unfilled
U=0.5. The mapper at `_ELMHURST_PARTY_WALL_CODE_TO_SAP10["CF"]: 4` and
`_API_PARTY_WALL_CONSTRUCTION_TO_SAP10[3]: 4` both flagged this as a
known approximation since S0380.64; today's slice closes it.
Introduces a party-wall-only synthetic SAP10 code
`WALL_CAVITY_FILLED_PARTY = 11` (distinct from the main wall_construction
codes 1-10 since Table 15 treats filled vs unfilled cavity as separate
party-wall types). `u_wall` doesn't consume code 11 so main-wall U-value
cascades are unaffected. Cohort + golden audit: only cert 000565 Ext1
lodges CF on the Elmhurst side; zero golden certs lodge API code 3, so
flipping the dispatch is scoped to one BP.
Cert 000565 movement (HEAD
|
||
|
|
6c8bbbc9e2 |
Slice S0380.86: §5.6 thin-wall stone + §5.8 dry-line closes BP[0] alt1 cascade gap
RdSAP 10 §5.6 (PDF p.40) "U-values of uninsulated stone walls, age
bands A to E":
Table 12 — Default U-values of stone walls
Sandstone or limestone: U = 54.876 × W^(-0.561)
Granite or whinstone: U = 45.315 × W^(-0.513)
Where W is wall thickness in mm.
"Apply the adjustment according to Table 14: Insulation thickness
and corresponding resistance if wall is insulated or dry-lined
including lath and plaster."
Combined with §5.8 (PDF p.40) + Table 14 (PDF p.41) dry-line R = 0.17
m²K/W: U = 1 / (1/U₀ + 0.17).
Cert 000565 BP[0] Main alt1 is the cohort fixture: Stone Granite, age
band A (inherited from Main), 120 mm wall thickness, dry-lined.
§5.6 formula: U₀ = 45.315 × 120^(-0.513) ≈ 3.8871.
§5.8 + Table 14 dry-line: U = 1/(1/3.8871 + 0.17) ≈ **2.3405**.
→ matches worksheet U985-0001-000565 line (29a) "External walls Main
alt.1 ... SolidWallDensePlasterInsul, Solid, 0.0, 2.34" EXACT.
Pre-S0380.86 two coupled bugs blocked this path:
1. Mapper mis-name per [[feedback-no-misleading-insulation-type]]:
`_map_elmhurst_alternative_wall` routed the Elmhurst Summary §7
"Alternative Wall N Thickness" lodging (the WALL thickness)
onto `SapAlternativeWall.wall_insulation_thickness="120"`. The
cascade then mis-bucketed it as 100 mm insulation (bucket=100
→ _BRICK_INS_100 row at age A → U=0.32). The Elmhurst Summary
schema has no "Alternative Wall N Insulation Thickness" line at
all — `wall_insulation_thickness` on alts was always
semantically the wall thickness, never insulation.
2. `u_wall` had no §5.6 thin-wall stone branch. Stone constructions
fell through to Table 6 row values (designed for typical-
thickness ~300mm+ walls), which dramatically under-state heat
loss for sub-200mm stone.
Fix span:
- datatypes/epc/domain/epc_property_data.py:SapAlternativeWall:
new `wall_thickness_mm: Optional[int] = None` field, mirroring
`SapBuildingPart.wall_thickness_mm`.
- datatypes/epc/domain/mapper.py:_map_elmhurst_alternative_wall:
routes Elmhurst `a.thickness_mm` (Wall thickness) onto
`wall_thickness_mm`; leaves `wall_insulation_thickness=None`
on this path (no Elmhurst Summary alt-wall insulation-thickness
line exists).
- domain/sap10_ml/rdsap_uvalues.py:
new `_u_stone_thin_wall_age_a_to_e(construction, W)` helper
implements §5.6 Table 12 formulas. `u_wall` accepts a new
`wall_thickness_mm: Optional[int] = None` param; dispatches
§5.6 formula when (a) wall thickness lodged, (b) age band ∈
A-E, (c) construction ∈ {STONE_GRANITE, STONE_SANDSTONE}.
§5.8 + Table 14 R=0.17 applied on top when dry_lined=True.
- domain/sap10_calculator/worksheet/heat_transmission.py:
`_alt_wall_contribution_w_per_k` passes
`wall_thickness_mm=alt_wall.wall_thickness_mm` to `u_wall`.
Tests (7 new, AAA-structure):
- 5 in domain/sap10_ml/tests/test_rdsap_uvalues.py — granite at
120 mm with dry-line (U=2.34); granite raw formula (U=3.89);
sandstone (U=3.74); age-G gate (Table 6 row, NOT formula); no
wall_thickness fallback (Table 6 row 1.7).
- 2 in backend/documents_parser/tests/test_summary_pdf_mapper_chain
.py — mapper pin (wall_thickness_mm=120 on BP[0] alt1;
wall_insulation_thickness=None) and cascade pin (walls_w_per_k
≥ 595, post-S0380.85 was 555.93).
**Cert 000565 cascade walls: 555.93 → 602.40 W/K (worksheet 604.07;
0.27% residual).** BP[0] alt1 cascade U: 0.32 → 2.34. Cascade walls
within 2 W/K of worksheet target across S0380.85+.86 closure cycle.
Test baseline: 560 pass (was 558 + 7 new − 5 already passing pins
that moved) + 9 expected `test_sap_result_pin[000565-*]` fails
unchanged. Cohort + golden + cert 9501 unaffected: of the 6 cohort
fixtures only cert 000565 alt1 lodged a `wall_insulation_thickness`
value on `SapAlternativeWall` (audit confirmed) — and that value was
always semantically the wall thickness, so the rename is a fix not
a behaviour change. The API mapper path defaults `wall_thickness_mm`
to None (API schema doesn't yet surface alt-wall thickness; safe
forward-compat).
Per [[feedback-verify-handover-claims]]: the post-S0380.84 handover
predicted SH residual would close after the wall fixes. Empirically
SH grew +2591 → +6348 → +7924 across S0380.84/.85/.86 — confirming a
SEPARATE SH-channel over-count that's independent of fabric (each
+1 W/K of spec-correct walls adds ~33.5 kWh of cascade SH, vs the
worksheet's ~38.96 kWh/W/K rate). The walls fixes are spec-correct;
the SH over-count is now a single isolated open work-item for the
next slice (~+8 k kWh structural).
Pyright net-zero per touched file (test_rdsap_uvalues.py error count
actually decreased by 1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
647c1aad0e |
Slice S0380.85: Curtain Wall §5.18 dispatch closes BP[2] Ext2 cascade gap
RdSAP 10 §5.18 (PDF p.48) "Curtain wall - U-value and other parameters":
"If documentary evidence is available, use calculated U-value of the
whole curtain wall. Otherwise for the purpose of RdSAP, U= 2.0 W/m²K
for pre-2023 curtain walls, And for post-2023 (2024 in Scotland)
U-values as for windows given in Notes below Table 24."
Table 24 row "Double or triple glazed England/Wales: 2022 or later"
PVC/wood column = 1.4 W/m²K. Whole-wall curtain walls use Frame
Factor=1 per the §5.18 closer.
Pre-S0380.85 `WALL_CURTAIN=9` was defined at rdsap_uvalues.py:116 but
NOT included in `known_types`, so `u_wall(construction=9)` fell through
to `_DEFAULT_WALL_BY_AGE.get(band, WALL_CAVITY)` → cavity table at age
H = 0.60. Cert 000565 BP[2] Ext2 lodges `Type: CW Curtain Wall` +
`Curtain Wall Age: Post 2023` per Summary PDF §7; worksheet pins U=1.40
(matching the §5.18 Post-2023 PVC/wood row). Cascade under-counted
walls by Δ U=0.80 × area = −112.2 W/K on this BP — 70% of the
post-S0380.84 BP main-wall residual (−161 W/K total).
§5.18 keys the curtain-wall U-value on the per-BP installation age,
NOT on the dwelling-wide `construction_age_band` — cert 000565 is
age H (1991-1995) but the curtain wall itself was installed
Post-2023. Plumb a new optional field through the extractor → datatype
→ mapper → cascade so the §5.18 dispatch sees it.
Files touched (5-layer slice span):
- backend/documents_parser/elmhurst_extractor.py:
`_wall_details_from_lines` reads "Curtain Wall Age" via
`_local_val` so absent lines stay None (not "").
- datatypes/epc/surveys/elmhurst_site_notes.py:WallDetails:
`curtain_wall_age: Optional[str] = None` field added.
- datatypes/epc/domain/epc_property_data.py:SapBuildingPart:
`curtain_wall_age: Optional[str] = None` field added.
- datatypes/epc/domain/mapper.py:_map_elmhurst_building_part:
threads `walls.curtain_wall_age` onto SapBuildingPart.
- domain/sap10_ml/rdsap_uvalues.py:
new `_u_curtain_wall(curtain_wall_age)` helper + WALL_CURTAIN
dispatch in `u_wall` before the `known_types` lookup.
"Post 2023" / "Post-2023" → 1.4; everything else (incl. None)
→ 2.0 per §5.18 fallback.
- domain/sap10_calculator/worksheet/heat_transmission.py:
passes `curtain_wall_age=part.curtain_wall_age` to `u_wall`
on the main-wall path. (Alt-wall path unchanged — cert 000565
lodges CW only as a main wall, never as an alt sub-area; alt
coverage is a follow-up slice if a future cert exercises it.)
Tests (6 new, AAA-structure):
- 3 in domain/sap10_ml/tests/test_rdsap_uvalues.py — `u_wall` direct
unit tests for Post 2023 (1.4), Pre 2023 (2.0), and absent
lodging fallback (2.0).
- 3 in backend/documents_parser/tests/test_summary_pdf_mapper_chain
.py — extractor pin (BP[2] Ext2 surfaces "Post 2023", non-CW BPs
stay None), mapper pin (curtain_wall_age threaded to BP[2]
SapBuildingPart), cascade pin (`heat_transmission_from_cert`
walls subtotal ≥ 540 W/K — pre-S0380.85 was 443).
Cert 000565 cascade walls: 443 → 555.93 W/K (worksheet 604.07; 70%
closer). Test baseline: 558 pass (was 555 + 3 new) + 9 expected
`test_sap_result_pin[000565-*]` fails unchanged.
Per [[feedback-verify-handover-claims]]: the post-S0380.84 handover
predicted SH residual would close +2591 → ~+800 kWh after this slice,
but the cascade is actually OVER-counting SH despite walls being
UNDER-counted. Closing the wall under-count makes the SH residual
*larger* (+2591 → +6348). The wall fix is spec-correct; the SH
over-count is a separate channel that surfaces more sharply now. Per
[[feedback-spec-citation-in-commits]] + [[feedback-spec-floor-skepticism]]
+ the S0380.84 precedent, ship the spec-correct change and document
the surfaced gap for the next slice rather than reverting to the
compensating-bugs state.
Pyright net-zero on every touched file (existing pre-existing errors
unchanged). Cohort + golden + cert 9501 unaffected — curtain_wall_age
defaults to None on those certs and `u_wall` ignores it unless
`construction == WALL_CURTAIN`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
c144d444e2 |
Slice S0380.26: RdSAP10 §5.8 dry-lining adjustment on alt walls — closes cert 7700 -0.44 → +5e-5
Per RdSAP10 §5.8 final note + Table 14 page 41:
"For drylining including laths and plaster use Rinsulation = 0.17 m²K/W."
Applied additively to the base U-value of an otherwise-uninsulated wall:
U_adjusted = 1 / (1/U_base + 0.17) — rounded to 2 d.p. half-up.
Closed form for the cohort fixture (cavity-as-built age C, U_base=1.5):
1 / (1/1.5 + 0.17) = 1.19522... → 1.20 ✓ matches worksheet
Cert 7700-3362-0922-7022-3563 (Summary_000905.pdf / dr87-0001-000905.pdf)
is an End-Terrace house age C lodging:
- Main wall: CavityWallDensePlasterDenseBlock, Filled Cavity, U=0.70
- Alt wall 1: 14.44 m² Cavity As-Built, Dry-lining: Yes (worksheet
`CavityWallPlasterOnDabsDenseBlock`, U=1.20)
Pre-slice the Elmhurst alt-wall mapper hard-coded `wall_dry_lined="N"`
and the cascade ignored the field everywhere — alt-wall U routed to the
cavity-as-built default (1.50), giving fabric (33) 148.72 W/K vs
worksheet 144.38 (Δ +4.33 W/K = ~+0.44 SAP). Worksheet "SAP value" line
lodges unrounded SAP 63.4425.
Implementation:
1. `AlternativeWall.dry_lined: bool = False` on the Elmhurst surveys
dataclass.
2. Elmhurst extractor reads "Alternative Wall N Dry-lining: Yes/No"
into the new field.
3. `_map_elmhurst_alternative_wall` propagates `wall_dry_lined="Y"`
instead of the hard-coded "N".
4. `u_wall` gains a `dry_lined: bool = False` kwarg and a single
§5.8 adjustment site at the as-built bucket (bucket=0). Insulated
buckets already absorb the dry-lining R via Table 14.
5. `_alt_wall_w_per_k` passes `dry_lined=alt_wall.wall_dry_lined == "Y"`.
Scope is the alt-wall path only — main BPs in the corpus all lodge
`wall_dry_lined="N"` (or the Summary PDF omits the field for the main
wall), so the main-wall call site is untouched. Conservative regression
posture per the user's strict cohort-pin convention.
Cohort-2 outcome (38 certs, Summary path):
exact (<1e-4): 22 → **23** (+1: cert 7700 -0.44 → +4.87e-05)
0.07..0.5: 1 → **0** (-1: cert 7700 closes out)
0.5..1: 1 → 1 (cert 9796 unchanged — MIT precision floor)
RAISES: 0 → 0
Cohort-1 ASHP cohort untouched: all certs lodge wall_dry_lined="N", so
the alt-wall call site short-circuits to the original cascade. Verified
no regressions across the 22 previously-exact cohort-2 certs either.
Pyright net-zero on all 8 touched files (183 → 183).
Tests: 704 → 708 pass (+4 new: u_wall §5.8 adjustment fires
correctly; cavity-as-built unchanged without flag; insulated bucket
unaffected by flag; heat_transmission alt-wall delta = 14.44 × 0.30
W/K; cert 7700 full chain hits worksheet 63.4425 at < 1e-4),
10 expected fails unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
57fbf83b1e |
Slice S0380.18: u_party_wall flat default per RdSAP10 Table 15 footnote*
Closes cert 0036-6325-1100-0063-1226 (the cohort's first FLAT fixture)
from Δ -0.3737 → +0.2987 by applying the RdSAP 10 Table 15 footnote *
rule: flats/maisonettes with unknown party-wall construction default
to U=0.0 W/m²K (both sides are heated dwellings, no heat loss).
Worksheet dr87-0001-000910.pdf line ref (32) lodges:
Party walls Main 24.13 m² U=0.00 A×U = 0.0000 W/K
matching the Table 15 footnote *. The cascade was applying the U=0.25
*house* default to this lodging because:
- Elmhurst Summary lodged `party_wall_type='U Unable to determine'`
- mapper translated it to `party_wall_construction=0` (the cross-
mapper-parity "unknown" sentinel)
- `u_party_wall(0)` fell through to `return 0.25` (the final-branch
default — same path as `u_party_wall(None)`)
That produced cascade `party_walls_w_per_k = 24.13 × 0.25 = 6.03` W/K
of heat-loss excess, propagating through (39) HTC → (97)..(98c) space
heat demand → (211) main fuel kWh → (255) total cost → (257) ECF →
(258) SAP rating. Net effect: cascade SAP 62.3734 vs worksheet 62.7471.
Two-part fix:
1. `domain/sap10_ml/rdsap_uvalues.py:u_party_wall` — add
`is_flat: bool = False` keyword argument. When True AND
`party_wall_construction in (None, 0)` (both the API-mapper None
path and the Elmhurst-mapper 0 sentinel for "Unable to determine"),
return 0.0 instead of the house default 0.25. Spec citation: RdSAP
10 Table 15 footnote * ("for flats and maisonettes with unknown
party-wall construction").
2. `domain/sap10_calculator/worksheet/heat_transmission.py` — wire
the cascade to pass `is_flat=_is_flat_or_maisonette(epc.property
_type)`. Adds a new helper `_is_flat_or_maisonette` distinct from
the existing `_is_house` (which excludes bungalows from
*cantilever* detection — bungalows ARE houses for party-wall
purposes per the spec). The new helper checks both the descriptive
form ("Flat" / "Maisonette") and the SAP schema enum-as-string
form ("2" / "3" — per `datatypes/epc/domain/epc_codes.csv
property_type` rows: 0=House, 1=Bungalow, 2=Flat, 3=Maisonette,
4=Park home).
The schema-enum collision was the bug-fix-with-a-bug: an initial
implementation used "1"/"2" (Flat/Maisonette per intuition) but those
are actually Bungalow/Flat per the schema, which routed all 10
bungalow certs onto the flat path. Corrected pre-commit.
Cohort-2 Summary-path delta after slice:
cert 0036 (Flat) Δ -0.3737 → Δ +0.2987 ✓ improved by +0.67
10 bungalow certs unchanged (correctly NOT flat)
5 non-flat house certs in band unchanged (different root cause —
next slice)
Bungalow certs (cohort 1 + 2) verified unchanged at delta ≤ +0.04 each.
Tests added (5):
- `test_u_party_wall_unknown_for_flat_returns_table15_footnote_zero`
pins the spec rule on the helper.
- `test_u_party_wall_unknown_sentinel_zero_treated_as_unknown_for_flat`
pins the Elmhurst-mapper `0` sentinel parity.
- `test_u_party_wall_known_solid_still_returns_zero_when_is_flat_false`
pins precedence: explicit Solid code overrides the is_flat flag.
- `test_summary_0036_flat_unknown_party_wall_routes_to_u_zero` chain-
test through `from_elmhurst_site_notes` + cert_to_inputs +
calculate_sap_from_inputs to assert `party_walls_w_per_k == 0` at
1e-4 tolerance.
Pyright net-zero per file:
- domain/sap10_ml/rdsap_uvalues.py: 1 (baseline 1)
- domain/sap10_calculator/worksheet/heat_transmission.py: 13 (baseline 13)
- domain/sap10_ml/tests/test_rdsap_uvalues.py: 66 (baseline 66)
- backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0
Regression baseline: 698 pass + 10 fail (= prior 694 + 10 + 4 new).
Note: the remaining +0.2987 residual on cert 0036 is in (30) external
roof — worksheet lodges Ext1 flat roof Plasterboard insulated U=2.30
giving 2.51 W/K; cascade has roof_w_per_k=0 (Ext1 roof contribution
missing). Separate slice.
Spec refs:
- RdSAP 10 Table 15 ("U-values of party walls") row 4 — house unknown
default 0.25 W/m²K.
- RdSAP 10 Table 15 footnote * — flat/maisonette unknown default
0.0 W/m²K.
- `datatypes/epc/domain/epc_codes.csv` rows
`property_type,{0..4},...` — SAP/RdSAP schema property-type enum.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
76fdab42de |
Slice 102b: cylinder storage loss via SAP 10.2 Tables 2/2a/2b
SAP 10.2 §4 line 7690 (full spec PDF p.136) defines the cylinder storage
loss cascade for any cert with a hot water cylinder lodged:
(54) = V × L × VF × TF (Table 2 absence-of-declared-loss branch)
(55) = (54) (no manufacturer's declared loss)
(56)m = (55) × n_m (per spec, n_m = days in month)
where
L = Table 2 (PDF p.158) Note 1 formula for the lodged insulation type
(factory-insulated cylinders: 0.005 + 0.55/(t+4.0); loose jacket:
0.005 + 1.76/(t+12.8))
VF = Table 2a (PDF p.158) Note 2 closed form (120/V)^(1/3)
TF = Table 2b (PDF p.159) base 0.60 for indirect / electric-immersion
cylinders, × 1.3 if no thermostat, × 0.9 if DHW separately timed
Prior, `water_heating_from_cert` hard-coded `solar_storage_monthly_kwh
= zero12` and `_water_heating_worksheet_and_gains` had no path to
populate it. The new `cylinder_storage_loss_monthly_kwh` helper in
`worksheet/water_heating.py` exposes Tables 2 / 2a / 2b as small typed
functions plus a composite; the cert-side orchestrator in
`rdsap/cert_to_inputs.py::_cylinder_storage_loss_override` resolves
the lodged cylinder fields and injects the override.
Code → litres mapping ground-truthed against worksheet (47) line refs
in /sap worksheets/Additional data with api/<cert>/dr87-*.pdf for the
7-cert ASHP cohort: code 3 → 160 L (Medium, 6 certs) and code 4 →
210 L (Large, cert 9418). Codes 2 / 5 / 6 (Normal / Inaccessible /
Exact) absent from the cohort and not yet mapped.
Cylinder insulation type code → "factory_insulated" mapping
(_CYLINDER_INSULATION_TYPE_FACTORY = 1) ground-truthed against all 7
ASHP cohort worksheets ("Foam" lodgement → SAP 10.2 Table 2 Note 2
"factory-insulated cylinder where the insulation is applied in the
course of manufacture irrespective of the insulation material used").
RdSAP §3 default table (PDF p.57) — "Hot water separately timed:
Post-1998 boiler: Yes" — applied to heat-pump main heating systems
(cat 4) per the cohort worksheet evidence.
Cert 0380 (Mitsubishi ASHP, 160 L factory 50 mm, thermostat + separately
timed) lands the spec formula at worksheet (56) Jan = 36.9530 kWh/month
(test pinned at 1e-4); HW kWh/yr 242.21 → 431.38, recovering ~189 kWh/yr
of cylinder loss the cascade was previously dropping.
Cohort regression: cert 0390-2954 (oil boiler + 160 L cylinder) tightens
PE residual -28.6783 → -27.5026 kWh/m² and CO2 residual -2.7640 →
-2.6570 t/yr — both move closer to the lodged values (improvement).
Re-pinned with a slice-102b note.
Closed boiler chain tests (001479, 0330, 9501) unaffected: those certs
lodge has_hot_water_cylinder=false so the override stays None and the
existing zero-storage-loss default fires.
|
||
|
|
da5e7196c4 |
Slice 96: flat-roof U-value defaults — RdSAP 10 §5.11 Table 18 col (3)
Cert 0330 (mid-terrace boiler, Summary_000897.pdf) Summary path was at
Δ +0.4667 SAP vs worksheet 61.5993 because Ext1's flat roof fell through
`_ROOF_BY_AGE` (Table 18 column (1), pitched-roof "between joists"
defaults) to 0.40 W/m²K for age D — the spec value is 2.30 W/m²K from
column (3) "Flat roof" (RdSAP 10 spec page 45).
RdSAP 10 §5.11 Table 18 column (3) verbatim:
Age A,B,C,D → 2.30; E → 1.50; F → 0.68; G → 0.40; H,I → 0.35;
J,K → 0.25; L → 0.18; M → 0.15.
Footnote (a): "If the roof insulation is 'none' use U = 2.3 (all roof
types, except for thatched roofs)" — confirms the col-3 entries for
old ages are the uninsulated row, applied because cert 0330's Ext1
lodges "Flat" construction with no measured insulation thickness.
Changes:
- `_FLAT_ROOF_BY_AGE` added in rdsap_uvalues.py
- `u_roof` gains `is_flat_roof: bool = False` parameter
- `heat_transmission_from_cert` detects flat roofs from
`part.roof_construction_type` ("flat" substring) and routes through
the new column.
Effect on baseline:
- cert 0330 Summary chain test: RED Δ+0.4667 → GREEN at 1e-4 (worksheet
total fabric heat loss 237.7549 W/K matches cascade to 4 d.p.)
- cert 001479 Layer 4 chain test: unchanged (Main pitched, no flat
components)
- cohort certs 000477/000516: unchanged (no flat roofs)
- golden cert 0300-2747-7640-2526-2135: SAP residual +1 → 0 (improved),
Ext1 is genuinely flat; pe/co2 residuals re-pinned. The dwelling has
the same Main-pitched + Ext1-flat shape as cert 0330; same fix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a7b08a4e8f |
refactor: move docs/sap-spec/ contents into domain/sap10_calculator/
Locality of reference — SAP-specific docs, specs, and runtime data
now live alongside the calculator that consumes them, mirroring the
prior packages→domain layout moves.
Move targets:
- Narrative MDs → domain/sap10_calculator/docs/
NEXT_AGENT_PROMPT.md, HANDOVER_NEXT.md, SAP_CALCULATOR.md
- Spec PDFs → domain/sap10_calculator/docs/specs/
RdSAP 10 Specification 10-06-2025.pdf
PCDF_Spec_Rev-06b_12_May_2021.pdf
sap-10-2-full-specification-2025-03-14.pdf
sap-10-3-full-specification-2026-01-13.pdf
- PCDB runtime data → domain/sap10_calculator/tables/pcdb/data/
pcdb10.dat (8.3MB) + 7× pcdb_table_*.jsonl (18MB total)
Path code rewrites (load-bearing):
- tables/pcdb/__init__.py: replaced parents[4]/'docs'/'sap-spec' with
Path(__file__).resolve().parent/'data' for Table 105 JSONL loading.
- tables/pcdb/postcode_weather.py: same rebase for the pcdb10.dat path
read by _postcode_climate_table().
- tables/pcdb/etl.py __main__: same rebase for the manual ETL invocation
(source + output_dir both now point inside the package).
- tests/test_pcdb_etl.py: _PCDB_DAT_PATH now derives from
parents[1]/'tables'/'pcdb'/'data' (was parents[3]/'docs'/'sap-spec').
Citation rewrites:
- 12 .py docstrings and 4 .md docs (ADRs + READMEs + narrative docs)
had `docs/sap-spec/<file>` strings rewritten to their new locations.
- Two cases where the catch-all sed misfired (an ADR-0009 line about a
PCDB extract; the pcdb __init__.py docstring about ETL output) were
hand-corrected to point at tables/pcdb/data/ rather than docs/specs/.
docs/sap-spec/ is now empty (will be removed in a follow-up sweep or
left as a vestigial empty dir for future repurposing). ADRs 0009 and
0010 remain at docs/adr/ — they're part of the chronological
cross-cutting decision log, not calculator-specific narrative.
Verified:
- Calculator's 1e-4 production gate
(test_api_001479_full_chain_sap_matches_worksheet_pdf_exactly) GREEN.
- Wider sweep (domain/sap10_calculator/ + domain/sap10_ml/): 1654
passed / 20 failed — exact pre-move baseline. All 20 failures
pre-existing (10 hand-built skeleton + 4 cohort chain + 6 cohort
diff).
- Pyright net-zero on the 4 touched runtime/test files (0 errors)
and unchanged on heat_transmission.py (13) / cert_to_inputs.py (35) /
mapper.py (33).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
68401c517a |
refactor: lift-and-shift packages/domain/src/domain/ml → domain/sap10_ml
Sibling migration to the sap10_calculator move — `domain.ml` now lives
at the root-level layout (`domain/sap10_ml/`) matching the pattern
already used by `domain.addresses`, `domain.tasks`, `domain.postcode`,
and `domain.sap10_calculator`.
Changes:
- `git mv packages/domain/src/domain/ml → domain/sap10_ml` (19 files;
history preserved).
- Subpackage rename: `domain.ml` → `domain.sap10_ml`. 32 references
rewritten across .py and .md files: 11 internal + 21 external
(datatypes/epc/domain/mapper.py, 14 files in domain/sap10_calculator,
2 backend tests, 2 ADRs, 1 README, 1 design doc).
- Path-string updates: `pytest.ini` testpath
`packages/domain/src/domain/ml/tests` → `domain/sap10_ml/tests` so
ML tests stay in the default auto-discovered sweep. `CONTEXT.md`
also updated.
`packages/domain/src/domain/` is now empty — the workspace `domain/`
tree has been fully migrated. Together with the `domain/__init__.py`
deletions from the sap10_calculator commit (
|