test(accuracy): ratchet 74.2% -> 75.5% via 'insulated (assumed)' roof Table-18 fix

RdSAP 10 §5.11.4: a pitched roof lodging thickness "NI" with description
"...insulated (assumed)" was billed at the §5.11.4 observed-retrofit
50 mm row (U 0.68) while the SAME description lodged with "ND" fell
through to the Table 18 age-band default (0.40 for bands A-G). The
ND-vs-NI sentinel is lodging-software noise; "(assumed)" means the
insulation PRESENCE is an age-band assumption, not an observed
retrofit, so Table 18's "thickness cannot be determined" clause
governs both forms. The bare "insulated" description (observed
retrofit, no qualifier) keeps the 50 mm row.

Evidence: the 27-cert loc4+NI corpus cohort was systematically
under-rated (~-1 SAP; zero positive movers among "(assumed)" certs),
vs the 102-cert loc4+ND cohort already on Table 18 at ~0 bias.

Gauge: within-0.5 74.2% -> 75.5%, SAP MAE 0.721 -> 0.708, CO2 MAE
0.09 -> 0.074, PE MAE 3.5 -> 3.2 (floors ratcheted). Unit-pinned in
test_rdsap_uvalues (assumed -> Table 18; bare-insulated -> 0.68
regression guard); integration pin in test_heat_transmission updated;
RealCertExpectation pinned for 10094975827 (66 = lodged, was -1.87).

Ledger: C002 103001004 deferred pending Elmhurst arbitration
(single-immersion off-peak pricing + declared cylinder loss vs
Table-2); C006 quadruplet marked ⚠ (LIG-21.0 software honours lodged
0 extract fans literally vs the Elmhurst-validated 0=unknown->default
convention, worksheet case 46).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-07-02 19:06:26 +00:00
parent b9f1f428b9
commit c380377948
8 changed files with 1032 additions and 296 deletions

View file

@ -0,0 +1,366 @@
{
"uprn": 10094975827,
"roofs": [
{
"description": "Pitched, insulated (assumed)",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"walls": [
{
"description": "Solid brick, as built, no insulation (assumed)",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
}
],
"floors": [
{
"description": "(other premises below)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 3,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "B8 2NG",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "BIRMINGHAM",
"psv_count": 0,
"built_form": 3,
"created_at": "2026-05-14 13:27:59",
"door_count": 1,
"region_code": 6,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 1
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"cylinder_thermostat": "N",
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 1,
"main_heating_number": 1,
"main_heating_control": 2107,
"main_heating_category": 2,
"main_heating_fraction": 1,
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 18559
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.01,
"quantity": "m"
},
"window_height": {
"value": 1.61,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.66,
"quantity": "m"
},
"window_height": {
"value": 1.46,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.09,
"quantity": "m"
},
"window_height": {
"value": 1.29,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.09,
"quantity": "m"
},
"window_height": {
"value": 1.55,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.79,
"quantity": "m"
},
"window_height": {
"value": 1.92,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Top-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "Flat 2",
"address_line_2": "874 Washwood Heath Road",
"assessment_type": "RdSAP",
"completion_date": "2026-05-14",
"inspection_date": "2026-05-13",
"extensions_count": 0,
"measurement_type": 1,
"open_flues_count": 0,
"sap_flat_details": {
"level": 3,
"top_storey": "N",
"storey_count": 2,
"flat_location": 1,
"heat_loss_corridor": 0
},
"total_floor_area": 78,
"transaction_type": 8,
"conservatory_type": 1,
"heated_room_count": 3,
"other_flues_count": 0,
"registration_date": "2026-05-14",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 3,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 0,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 340,
"floor_heat_loss": 3,
"roof_construction": 4,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.44,
"total_floor_area": 77.87,
"party_wall_length": 11.57,
"heat_loss_perimeter": 25.03
}
],
"wall_insulation_type": 4,
"construction_age_band": "B",
"party_wall_construction": 1,
"wall_thickness_measured": "Y",
"roof_insulation_location": 4,
"roof_insulation_thickness": "NI",
"wall_insulation_thickness": "NI"
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 952,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 3.2,
"energy_rating_average": 60,
"energy_rating_current": 66,
"lighting_cost_current": {
"value": 56,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, TRVs and bypass",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"blocked_chimneys_count": 0,
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 735,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 197,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"schema_version_current": "LIG-21.0",
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 171,
"indicative_cost": "\u00a37,500 - \u00a311,000",
"improvement_type": "Q",
"improvement_details": {
"improvement_number": 7
},
"improvement_category": 5,
"energy_performance_rating": 70,
"environmental_impact_rating": 70
},
{
"sequence": 2,
"typical_saving": 45,
"indicative_cost": "\u00a3220 - \u00a3250",
"improvement_type": "G",
"improvement_details": {
"improvement_number": 14
},
"improvement_category": 5,
"energy_performance_rating": 72,
"environmental_impact_rating": 71
}
],
"co2_emissions_potential": 2.6,
"energy_rating_potential": 72,
"lighting_cost_potential": {
"value": 56,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0",
"hot_water_cost_potential": {
"value": 198,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2548.95,
"space_heating_existing_dwelling": 10093.12
},
"draughtproofed_door_count": 1,
"energy_consumption_current": 227,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "10.2.2.0",
"energy_consumption_potential": 181,
"environmental_impact_current": 64,
"cfl_fixed_lighting_bulbs_count": 0,
"current_energy_efficiency_band": "D",
"environmental_impact_potential": 71,
"led_fixed_lighting_bulbs_count": 8,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 41,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,321 @@
{
"uprn": 103001004,
"roofs": [
{
"description": "Pitched, Unknown loft insulation",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"walls": [
{
"description": "Cavity wall, filled cavity",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 2,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"lighting": {
"description": "Good lighting efficiency",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "NE33 1TN",
"hot_water": {
"description": "Electric immersion, off-peak",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 4
},
"post_town": "SOUTH SHIELDS",
"built_form": "NR",
"created_at": "2026-04-16 10:25:37",
"door_count": 0,
"region_code": 1,
"report_type": 2,
"sap_heating": {
"number_baths": 0,
"cylinder_size": 3,
"shower_outlets": [
{
"shower_outlet": {
"shower_wwhrs": 1,
"shower_outlet_type": 1
}
}
],
"cylinder_heat_loss": 1.8,
"number_baths_wwhrs": 0,
"water_heating_code": 903,
"water_heating_fuel": 29,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 20,
"heat_emitter_type": 0,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2307,
"main_heating_category": 6,
"main_heating_fraction": 1,
"sap_main_heating_code": 301,
"main_heating_data_source": 2,
"community_heat_distribution_type": 6
}
],
"immersion_heating_type": 2,
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"glazing_gap": 12,
"orientation": 7,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.21,
"window_height": 1.44,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": 12,
"orientation": 7,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.77,
"window_height": 1.14,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Community scheme",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Ground-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "3 Julius Court",
"address_line_2": "Mile End Road",
"assessment_type": "RdSAP",
"completion_date": "2026-04-16",
"inspection_date": "2026-04-16",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 1,
"top_storey": "N",
"storey_count": 1,
"flat_location": 0,
"heat_loss_corridor": 1
},
"total_floor_area": 31,
"transaction_type": 8,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 2,
"registration_date": "2026-04-16",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 1,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"extract_fans_count": 2,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.37,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 30.55,
"quantity": "square metres"
},
"party_wall_length": {
"value": 16.75,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 5.37,
"quantity": "metres"
}
}
],
"wall_insulation_type": 2,
"construction_age_band": "F",
"party_wall_construction": 0,
"wall_thickness_measured": "N",
"roof_insulation_location": 2,
"roof_insulation_thickness": "NI",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 2,
"heating_cost_current": {
"value": 251,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 1.2,
"energy_rating_average": 60,
"energy_rating_current": 50,
"lighting_cost_current": {
"value": 29,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Flat rate charging, TRVs",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 313,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 1054,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 35,
"currency": "GBP"
},
"indicative_cost": "\u00a3900 - \u00a31,200",
"improvement_type": "A",
"improvement_details": {
"improvement_number": 5
},
"improvement_category": 5,
"energy_performance_rating": 52,
"environmental_impact_rating": 81
},
{
"sequence": 2,
"typical_saving": {
"value": 598,
"currency": "GBP"
},
"indicative_cost": "\u00a320 - \u00a340",
"improvement_type": "C",
"improvement_details": {
"improvement_number": 1
},
"improvement_category": 5,
"energy_performance_rating": 68,
"environmental_impact_rating": 83
},
{
"sequence": 3,
"typical_saving": {
"value": 51,
"currency": "GBP"
},
"indicative_cost": "\u00a3600 - \u00a31,500",
"improvement_type": "Y",
"improvement_details": {
"improvement_number": 49
},
"improvement_category": 5,
"energy_performance_rating": 70,
"environmental_impact_rating": 83
}
],
"co2_emissions_potential": 1.0,
"energy_rating_potential": 70,
"lighting_cost_potential": {
"value": 29,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 308,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 5719.21,
"space_heating_existing_dwelling": 1270.63
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 367,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0342",
"energy_consumption_potential": 214,
"environmental_impact_current": 78,
"current_energy_efficiency_band": "E",
"environmental_impact_potential": 83,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 41,
"low_energy_fixed_lighting_bulbs_count": 5,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -998,13 +998,30 @@ def u_roof(
# (band J = 0.16), matching the cohort's "ND" (thickness None)
# vaulted roofs which already reach col (1) by falling through.
return _ROOF_BY_AGE.get(age_band.upper(), 0.4)
if (
age_band is not None
and insulation_thickness_mm == 0
and _described_as_insulated(description)
and description is not None
and "(assumed)" in description.lower()
):
# RdSAP renders "insulated (assumed)" when insulation PRESENCE is
# itself assumed (age-band / as-built assumption), not an observed
# retrofit — so the §5.11.4 50 mm footnote (below) does not apply;
# the thickness "cannot be determined" and the Table 18 age-band
# default governs (§5.11.4 opening clause). Identical
# "Pitched, insulated (assumed)" corpus dwellings lodge thickness
# "ND" (None → Table 18 by fallthrough) or "NI" (0 → previously the
# 50 mm row) purely per lodging software; both take Table 18.
table_18 = _FLAT_ROOF_BY_AGE if is_flat_roof else roof_by_age
return table_18.get(age_band.upper(), 0.4)
if insulation_thickness_mm == 0 and _described_as_insulated(description):
# Spec §5.11.4 (page 44 footnote): "If retrofit insulation
# present of unknown thickness use 50 mm". The cert encodes
# "thickness unknown but retrofit insulation present" as the
# "NI" sentinel which `_parse_thickness_mm` parses to 0. Without
# this override the Table 16 row-0 lookup below returns the
# uninsulated 2.30 W/m²K.
# "thickness unknown but OBSERVED retrofit insulation" as the
# "NI" sentinel (parsed to 0) + an unqualified "insulated"
# description (no "(assumed)"). Without this override the Table
# 16 row-0 lookup below returns the uninsulated 2.30 W/m²K.
return 0.68 # Table 16 row 50, "Insulation at joists at ceiling level"
if insulation_thickness_mm is not None:
# nearest tabulated thickness <= supplied (Table 16 column (1)

View file

@ -1059,16 +1059,41 @@ def test_u_roof_description_with_measured_transmittance_returns_parsed_value() -
assert result == pytest.approx(0.11, abs=0.001)
def test_u_roof_ni_thickness_with_insulated_description_applies_50mm_per_section_5_11_4() -> None:
# Arrange — 346 corpus certs lodge roof_insulation_thickness="NI"
# (Not Indicated, parsed to 0 by _parse_thickness_mm). When the
# description also signals retrofit insulation ("Pitched, insulated
# (assumed)" / "Flat, insulated" / "Roof room(s), insulated
# (assumed)"), RdSAP 10 §5.11.4 (page 44) footnote applies:
def test_u_roof_ni_thickness_with_observed_insulated_description_applies_50mm_per_section_5_11_4() -> None:
# Arrange — a roof lodging thickness "NI" (parsed to 0) whose
# description signals OBSERVED insulation without the "(assumed)"
# qualifier ("Pitched, insulated" — RdSAP renders the bare form when
# the assessor saw the insulation but not its depth). RdSAP 10
# §5.11.4 (page 44) footnote applies:
# "If retrofit insulation present of unknown thickness use 50 mm".
# That maps to Table 16 row "50 mm at joists at ceiling level" = 0.68
# W/m²K — vs the current 2.30 we return when thickness=0 hits the
# Table 16 row-0 lookup.
# W/m²K — vs the 2.30 the Table 16 row-0 lookup would give.
# Act
result = u_roof(
country=Country.ENG,
age_band="C",
insulation_thickness_mm=0, # parsed from "NI"
description="Pitched, insulated",
)
# Assert
assert result == pytest.approx(0.68, abs=0.01)
def test_u_roof_ni_thickness_with_assumed_insulated_description_uses_table18_default() -> None:
# Arrange — RdSAP renders "insulated (assumed)" when insulation
# presence is ASSUMED (age-band / as-built assumption), not an
# observed retrofit — so the §5.11.4 "retrofit insulation present of
# unknown thickness use 50 mm" footnote does NOT apply; the thickness
# "cannot be determined" and Table 18's age-band default governs
# (§5.11.4 opening clause). The corpus lodges the SAME description
# with thickness "ND" (102 certs → already Table 18 via the
# thickness-None fallthrough) or "NI" (27 certs → previously took the
# 50 mm row 0.68), purely per lodging software; both must land on the
# same Table 18 value (band C pitched → 0.40). The loc-4 NI cohort
# was systematically under-rated by ~1 SAP before this (e.g. corpus
# cert 10094975827 band B: Δ 1.87).
# Act
result = u_roof(
@ -1078,8 +1103,8 @@ def test_u_roof_ni_thickness_with_insulated_description_applies_50mm_per_section
description="Pitched, insulated (assumed)",
)
# Assert
assert result == pytest.approx(0.68, abs=0.01)
# Assert — Table 18 column (1) band C.
assert result == pytest.approx(0.40, abs=0.01)
def test_u_roof_ni_thickness_with_no_insulation_description_stays_at_2_30() -> None:
@ -1324,18 +1349,20 @@ def test_u_roof_vaulted_ni_unknown_band_j_uses_col1_age_band_not_50mm() -> None:
assert abs(result - 0.16) <= 1e-4
def test_u_roof_normal_pitched_ni_insulated_still_returns_50mm_per_5_11_4() -> None:
def test_u_roof_normal_pitched_ni_observed_insulated_still_returns_50mm_per_5_11_4() -> None:
# Arrange — regression guard: the is_sloping_ceiling flag defaults
# False, so a NORMAL pitched roof (with loft) lodged NI + "insulated
# (assumed)" must STILL hit the §5.11.4 retrofit-50 mm row (U=0.68).
# Same inputs as the sloping test above minus is_sloping_ceiling.
# False, so a NORMAL pitched roof (with loft) lodged NI + an
# unqualified "insulated" description (observed retrofit, no
# "(assumed)") must STILL hit the §5.11.4 retrofit-50 mm row
# (U=0.68). The "(assumed)" form routes to Table 18 instead — see
# test_u_roof_ni_thickness_with_assumed_insulated_description_uses_table18_default.
# Act
result = u_roof(
country=Country.ENG,
age_band="J",
insulation_thickness_mm=0,
description="Pitched, insulated (assumed)",
description="Pitched, insulated",
)
# Assert

File diff suppressed because it is too large Load diff

View file

@ -724,6 +724,21 @@ _EXPECTATIONS: Final[tuple[RealCertExpectation, ...]] = (
cert_num="uprn-4510053280",
sap_score=75,
),
# UPRN 10094975827 (corpus; no cert number lodged). RdSAP-Schema-21.0.1
# native — TOP-FLOOR FLAT, band B, solid brick, pitched roof lodged
# thickness "NI" + description "Pitched, insulated (assumed)", electric
# room heaters. The "insulated (assumed)" Table-18 cert: presence-assumed
# insulation is NOT the §5.11.4 observed-retrofit 50 mm row (0.68) — it
# takes the Table 18 age-band default (band B pitched → 0.40), the same
# value the "ND" lodgement of this description reaches. Was engine 64.13
# (Δ 1.87 vs lodged 66); the fix lands 65.83 → rounds to 66 = lodged.
# PINNED to the observed engine 66 = lodged.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_10094975827",
cert_num="uprn-10094975827",
sap_score=66,
),
)

View file

@ -245,15 +245,18 @@ def test_part_geometry_floorless_part_honours_full_key_contract() -> None:
assert early["cantilever_floor_area_m2"] == 0.0
def test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_section_5_11_4() -> None:
# Arrange — 346 corpus certs lodge roof_insulation_thickness="NI"
# with descriptions like "Pitched, insulated (assumed)". The
# retrofit-insulation signal in the description must flow from
# epc.roofs[0].description through heat_transmission_from_cert
# into u_roof so the 50 mm assumption fires per RdSAP 10 §5.11.4.
# Geometry: 100 m² ground floor → top floor area also 100 m² →
# roof_area = 100 m². roof_w_per_k expected = 0.68 × 100 = 68
# W/K (vs uninsulated 2.30 × 100 = 230 W/K).
def test_roof_insulated_assumed_with_ni_thickness_uses_table18_default() -> None:
# Arrange — corpus certs lodge roof_insulation_thickness="NI" with
# descriptions like "Pitched, insulated (assumed)". The description
# must flow from epc.roofs[0].description through
# heat_transmission_from_cert into u_roof, where "(assumed)"
# (presence assumed, not observed retrofit) routes to the Table 18
# age-band default per RdSAP 10 §5.11.4 — band C pitched → 0.40 —
# matching the "ND" lodgement of the same description. (The bare
# "insulated" form keeps the §5.11.4 50 mm row; unit-pinned in
# test_rdsap_uvalues.) Geometry: 100 m² ground floor → top floor
# area also 100 m² → roof_area = 100 m². roof_w_per_k expected =
# 0.40 × 100 = 40 W/K (vs uninsulated 2.30 × 100 = 230 W/K).
main = make_building_part(
identifier="Main Dwelling",
construction_age_band="C",
@ -287,7 +290,7 @@ def test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_section_5_11_4()
result = heat_transmission_from_cert(epc)
# Assert
assert result.roof_w_per_k == pytest.approx(68.0, abs=2.0)
assert result.roof_w_per_k == pytest.approx(40.0, abs=2.0)
def test_roof_insulation_location_rafters_drives_table16_column_2_api_int_path() -> None:

View file

@ -196,7 +196,18 @@ _CORPUS = Path(
# 0.74 -> 0.742 via the heat-pump water-heating 100% floor (App N3.7): cert
# 100110101713 moves inside +-0.5 (|err| 4.97 -> 0.49). See the _MAX_SAP_MAE
# log below for the paired space-heating PSR-extension + water-floor slices.
_MIN_WITHIN_HALF_SAP = 0.742
# 0.742 -> 0.755 (MAE 0.722 -> 0.708, CO2 0.09 -> 0.075, PE 3.5 -> 3.2) via
# the "insulated (assumed)" roof Table-18 fix (RdSAP 10 §5.11.4): a pitched
# roof lodging thickness "NI" + description "insulated (assumed)" was hitting
# the §5.11.4 retrofit-50 mm row (0.68) while the SAME description lodged
# with "ND" fell through to the Table 18 age-band default (e.g. 0.40 A-G) —
# the sentinel choice is lodging-software noise, and "(assumed)" means the
# insulation PRESENCE is an age-band assumption, not an observed retrofit,
# so Table 18's "thickness cannot be determined" clause governs both forms.
# The bare "insulated" description (observed retrofit) keeps the 50 mm row.
# The 27-cert loc4+NI cohort was systematically under-rated ~1 SAP (e.g.
# 10094975827 band B -1.87 -> inside). Unit-pinned in test_rdsap_uvalues.
_MIN_WITHIN_HALF_SAP = 0.755
# 0.793 -> 0.789 via the §12 Unknown-meter + dual-electric-immersion off-peak
# trigger (RdSAP 10 PDF p.62): Apartment 241 (main 691 + 903 dual immersion)
# -5.38 -> -1.05. Worksheet-validated on "simulated case 48" (Elmhurst SAP 57,
@ -266,9 +277,9 @@ _MIN_WITHIN_HALF_SAP = 0.742
# oversized-PSR water eff 0.60 x 128.55% = 77.13% is floored to 100% (=
# accredited Elmhurst (216)), taking the cert 68.03 -> 72.51 (|err| 4.97 ->
# 0.49, now inside +-0.5). In-range heat pumps keep their > 100% water COP.
_MAX_SAP_MAE = 0.722
_MAX_CO2_MAE_TONNES = 0.09 # t CO2 / yr vs co2_emissions_current
_MAX_PE_PER_M2_MAE = 3.5 # kWh / m2 / yr vs energy_consumption_current
_MAX_SAP_MAE = 0.708
_MAX_CO2_MAE_TONNES = 0.075 # t CO2 / yr vs co2_emissions_current
_MAX_PE_PER_M2_MAE = 3.2 # kWh / m2 / yr vs energy_consumption_current
def _load_corpus() -> list[dict[str, Any]]: