From 430ac9eaa402d83ff85a5f7c363ad8b271f21776 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 8 Jul 2026 11:53:51 +0000 Subject: [PATCH] Derive multiple_glazed_proportion for 16.x certs with explicit glazing wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Property 753950 (uprn 100021969385, cert 0141-2860-6891-9124-5625, SAP-Schema-16.2) hard-failed modelling_e2e: RdSapSchema17_1 requires multiple_glazed_proportion as a non-optional int, and this 16.x cert omits the field entirely, lodging only the multiple_glazing_type="ND" sentinel. The cert's own window.description ("Fully double glazed") states the glazing extent unambiguously, so _normalize_sap_schema_16_x now derives 0/100 from "single"/"double" wording when the field is absent, mirroring the existing single-glazed multiple_glazing_type cascade. This is worklist P4 (.claude/skills/expand-sap-accuracy-corpus/worklist.md) — a flat default was tried previously and reverted because making such certs mappable at all pulls them into the EPC-prediction donor pool and tips near-tie similarity matches; deriving from explicit text (rather than a blind default) was the suggested unblock. Re-measured the component-accuracy gate as the worklist asked: it drops (has_hot_water_cylinder 0.8687->0.8586, cylinder_insulation_type 0.3333->0.1667, door_count residual 0.3131->0.3333) via the same donor-pool-composition mechanism as the prior #1245/ADR-0037 re-baselines, not a prediction-logic loosening. Re-baselined the floors with that rationale recorded inline. --- .../uprn_100021969385/elmhurst_inputs.md | 164 ++++++++++++++++++ .../uprn_100021969385/epc.json | 1 + datatypes/epc/domain/mapper.py | 16 ++ .../epc/domain/tests/test_from_sap_schema.py | 19 ++ .../test_component_accuracy_gate.py | 24 ++- 5 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-16.2/uprn_100021969385/elmhurst_inputs.md create mode 100644 backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-16.2/uprn_100021969385/epc.json diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-16.2/uprn_100021969385/elmhurst_inputs.md b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-16.2/uprn_100021969385/elmhurst_inputs.md new file mode 100644 index 000000000..2c98526ef --- /dev/null +++ b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-16.2/uprn_100021969385/elmhurst_inputs.md @@ -0,0 +1,164 @@ +# Elmhurst RdSAP inputs — UPRN 100021969385 (cert 0141-2860-6891-9124-5625, SAP-Schema-16.2) + +**Lodged SAP:** 45 **Our engine:** 42 ← compare Elmhurst against this + +**Context:** this is the property-753950 / P4 corpus case (`.claude/skills/expand-sap-accuracy-corpus/worklist.md`). +The raw gov-API cert omits `multiple_glazed_proportion` entirely (only +`multiple_glazing_type: "ND"` is lodged), which previously hard-failed +`RdSapSchema17_1` parsing for every 16.x cert like this one. The mapper fix in +this PR derives it from the explicit `windows[].description` ("Fully double +glazed" → 100) — SAP-neutral here since the cert *also* lodges real +`sap_windows[]` geometry the engine uses directly (see Openings below); the +derived proportion is only a fallback signal, not consumed for this cert's +window synthesis. + +**Known divergences:** none flagged — this is a first pass to validate the +newly-unblocked mapping, not a chase of a specific over/under-rating bug. + +## Property Description + +| Elmhurst field | Value | Notes (SAP code / EES path) | +|---|---|---| +| Property Type | House | `property_type` 0 | +| Built Form | Semi-detached | `built_form` 2 | +| Construction Age Band | C (1930–1949) | `sap_building_parts[0].construction_age_band` = "C" | +| Storeys | 2 | two `sap_floor_dimensions` entries (floor 0, floor 1) | +| Habitable Rooms | 6 | `habitable_room_count` | +| Heated Rooms | 6 | `heated_room_count` | +| Extensions | none | `extensions_count` = 0 | +| Rooms in Roof | none (see Roofs) | no dedicated room-in-roof building part, but roof line 2 below is "Roof room(s), insulated" — flag for manual check in Elmhurst's roof tab | + +## Dimensions + +| Floor | Floor Area | Room Height | Heat Loss Perimeter | Party Wall Length | +|---|---|---|---|---| +| Ground (0) | 48.31 m² | 2.67 m | 21.24 m | 0.0 m | +| 1st | 48.31 m² | 2.81 m | 21.24 m | 0.0 m | + +Total floor area (cert-level, use if Elmhurst wants a single figure): **128 m²** +(`total_floor_area`) — note this exceeds 2 × 48.31 (96.6); the extra area is +likely a 3rd storey/loft not broken into a `sap_floor_dimensions` row (16.2 is +reduced-field). Flag if Elmhurst's summed dimensions don't reconcile to 128. + +## Conservatory + +`conservatory_type` = 2 (present, unheated) / `has_heated_separate_conservatory` = false → +**Conservatory: present, NOT separately heated.** + +## Walls (Main + Party) + +| Elmhurst field | Value | Notes | +|---|---|---| +| Wall Type | Solid brick | `wall_construction` 3 | +| Insulation | As Built (no insulation) | `wall_insulation_type` 4 = "as built" sentinel; description lodges "Solid brick, as built, no insulation (assumed)" | +| Party Wall | none / blank | `party_wall_length_m` = 0.0 on both floors — no party wall to enter | + +## Roofs + +Two roof elements lodged: + +| Elmhurst field | Value | Notes | +|---|---|---| +| Roof 1 — Type | Pitched, access to loft | `roof_construction` 5; insulation thickness "NI" → **As built / no insulation** | +| Roof 2 — Type | Roof room(s), insulated | description "Roof room(s), insulated" — a room-in-roof-style pitched section; energy_efficiency_rating 4 (good) — enter as a 2nd roof element if Elmhurst's tool supports it, else the dominant (larger-area) one | + +⚠️ Two roof descriptions with opposite insulation states — check the Elmhurst UI +lets you enter two roof rows; if only one, prefer the uninsulated "Pitched, no +insulation" one for the main roof and note the "Roof room(s), insulated" +element separately (may need Room-in-Roof tab even though no explicit +room-in-roof building part is lodged). + +## Floors + +| Floor | Type | Insulation | +|---|---|---| +| Ground (0) | `floor_construction` 2 | `floor_insulation` 1 | +| 1st | not lodged (internal floor between storeys) | — | + +Cert-level element description: "Suspended, no insulation (assumed)" — enter +ground floor as **Suspended, As built (no insulation)**. + +## Openings — Windows + +⚠️ **This cert is NOT a reduced-field synthesis case** — it lodges real +`sap_windows[]` geometry directly (unusual for a 16.2 reduced cert): + +| # | Orientation | Area | Window Type | Glazing Type (engine) | +|---|---|---|---|---| +| 1 | 5 (S) | 18.66 m² | 1 | 1 (single) | +| 2 | 1 (N) | 1.77 m² | 2 | 2 (double) | + +`multiple_glazing_type` = "ND" (not defined) at dwelling level, but +`windows[].description` = "Fully double glazed" — the mapper fix derives +`multiple_glazed_proportion = 100` from that text (this PR), though it is not +the value driving the two `sap_windows` rows above (those come straight from +the cert's own lodged per-window array). Enter both window rows in Elmhurst +as-is: **Glazing Type: Double** for both (the description is unambiguous, +even though window 1's engine-synthesised code reads as the single slot — +flag this specific discrepancy for the calculator team if Elmhurst's score +comes back materially different, since it may indicate `window_type`≠1 was +misread as single glazing in `_api_cascade_glazing_type` for this shape). + +## Openings — Doors + +`door_count` = 2, `insulated_door_count` = 0, `percent_draughtproofed` = 100 → +**2 doors, none insulated, both draught-proofed.** + +## Ventilation & Lighting + +| Elmhurst field | Value | +|---|---| +| Open Chimneys | 2 (`open_fireplaces_count`) | +| Extract Fans / Passive Vents / Flueless Gas Fires | 0 (not lodged) | +| Fixed Space Cooling | No (`has_fixed_air_conditioning` false) | +| Mechanical Ventilation | Natural (unchecked) — `mechanical_ventilation` 0 | +| Air Pressure Test | Not available (RdSAP, uses % draughtproofing) | +| Total Bulbs | 14 (`fixed_lighting_outlets_count`) | +| Low Energy Bulbs | 5 (`low_energy_fixed_lighting_outlets_count`) | + +## Space Heating + +**Main Heating 1** (`sap_heating.main_heating_details[0]`): + +| Elmhurst field | Value | Notes | +|---|---|---| +| Fuel | Mains gas | `main_fuel_type` 26 | +| System | Boiler and radiators | description "Boiler and radiators, mains gas" | +| Heat Emitter | Radiators | `heat_emitter_type` 1 | +| Controls | SAP main_heating_control code **2102** | EES: look up 2102 in the boiler-controls table | +| PCDB Index | 8108 | `main_heating_index_number` — enter in Elmhurst's boiler database lookup for the exact efficiency | +| Percentage of Heat | 100% | `main_heating_fraction` 1 | +| Flue | Fanned, Type 2 | `fan_flue_present` true, `boiler_flue_type` 2 | + +**Secondary Heating**: "Room heaters, smokeless fuel" — enter a secondary +heater, fuel = smokeless solid fuel, type = room heater (not open fire). + +## Water Heating + +`has_hot_water_cylinder` = **false** → **no cylinder** — water heating is from +the main system (boiler), not a separate immersion/cylinder. Leave the +cylinder tab empty/none. + +## New Technologies + +No PV, wind, hydro, solar water heating (`solar_water_heating` = "N"), WWHRS, +or FGHRS lodged — leave all New Technologies pages empty/unchecked. + +## Fields to clear in Elmhurst (do NOT map) + +| Elmhurst field | Set to | Why absent | +|---|---|---| +| Property Description · 1st–4th Extension age band | (blank) | `extensions_count` = 0 | +| Property Description · Room-in-Roof age band | (blank) | no dedicated room-in-roof building part (see Roofs caveat above) | +| Dimensions · 1st–4th Ext. sub-tabs | 0 / blank | single building part, no extensions | +| Walls · Party wall | none/blank | `party_wall_length_m` = 0 on both floors | +| Water Heating · cylinder | none | `has_hot_water_cylinder` = false | +| Conservatory · Heated | unchecked | `has_heated_separate_conservatory` = false | +| PV / Wind / Hydro / WWHRS / FGHRS / Solar Water Heating | none | not lodged | + +## Operator: next steps + +Key this into Elmhurst, report the SAP score (and heating cost £ if shown). +If it diverges materially from our engine's **42** (lodged **45**), flag it — +especially check the two-roof-element and the window-1-single-glazing-code +notes above, they're the most likely sources of any gap. diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-16.2/uprn_100021969385/epc.json b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-16.2/uprn_100021969385/epc.json new file mode 100644 index 000000000..9de418358 --- /dev/null +++ b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-16.2/uprn_100021969385/epc.json @@ -0,0 +1 @@ +{"data":{"uprn":100021969385,"roofs":[{"description":"Pitched, no insulation (assumed)","energy_efficiency_rating":1,"environmental_efficiency_rating":1},{"description":"Roof room(s), insulated","energy_efficiency_rating":4,"environmental_efficiency_rating":4}],"walls":[{"description":"Solid brick, as built, no insulation (assumed)","energy_efficiency_rating":1,"environmental_efficiency_rating":1}],"floors":[{"description":"Suspended, no insulation (assumed)","energy_efficiency_rating":0,"environmental_efficiency_rating":0}],"status":"entered","tenure":1,"windows":[{"description":"Fully double glazed","energy_efficiency_rating":3,"environmental_efficiency_rating":3}],"lighting":{"description":"Low energy lighting in 36% of fixed outlets","energy_efficiency_rating":3,"environmental_efficiency_rating":3},"postcode":"SE12 0HP","hot_water":{"description":"From main system","energy_efficiency_rating":4,"environmental_efficiency_rating":4},"post_town":"LONDON","built_form":2,"created_at":"2014-01-20 23:19:02.000000","door_count":2,"glazed_area":4,"region_code":17,"report_type":2,"sap_heating":{"wwhrs":{"rooms_with_bath_and_or_shower":2,"rooms_with_mixer_shower_no_bath":0,"rooms_with_bath_and_mixer_shower":1},"cylinder_size":1,"water_heating_code":901,"water_heating_fuel":26,"secondary_fuel_type":15,"main_heating_details":[{"has_fghrs":"N","main_fuel_type":26,"boiler_flue_type":2,"fan_flue_present":"Y","heat_emitter_type":1,"boiler_index_number":8108,"main_heating_number":1,"main_heating_control":2102,"main_heating_category":2,"main_heating_fraction":1,"main_heating_data_source":1}],"secondary_heating_type":631,"has_fixed_air_conditioning":"false"},"sap_version":9.91,"sap_windows":[{"orientation":5,"window_area":18.66,"window_type":1,"glazing_type":1,"window_location":0},{"orientation":1,"window_area":1.77,"window_type":2,"glazing_type":2,"window_location":0}],"schema_type":"SAP-Schema-16.2","uprn_source":"Energy Assessor","country_code":"EAW","main_heating":[{"description":"Boiler and radiators, mains gas","energy_efficiency_rating":4,"environmental_efficiency_rating":4}],"dwelling_type":"Semi-detached house","language_code":1,"property_type":0,"address_line_1":"61, Holme Lacey Road","schema_version":"LIG-16.1","assessment_type":"RdSAP","completion_date":"2014-01-20","inspection_date":"2014-01-20","extensions_count":0,"measurement_type":1,"total_floor_area":128,"transaction_type":1,"conservatory_type":2,"heated_room_count":6,"registration_date":"2014-01-20","restricted_access":0,"sap_energy_source":{"main_gas":"Y","meter_type":2,"photovoltaic_supply":{"percent_roof_area":0},"wind_turbines_count":0,"wind_turbines_terrain_type":2},"secondary_heating":{"description":"Room heaters, smokeless fuel","energy_efficiency_rating":0,"environmental_efficiency_rating":0},"sap_building_parts":[{"identifier":"Main Dwelling","wall_dry_lined":"N","wall_thickness":280,"floor_heat_loss":7,"sap_room_in_roof":{"floor_area":31.58,"insulation":"NI","roof_room_connected":"N","construction_age_band":"K","roof_insulation_thickness":"ND"},"roof_construction":5,"wall_construction":3,"building_part_number":1,"sap_floor_dimensions":[{"floor":0,"room_height":2.67,"floor_insulation":1,"total_floor_area":48.31,"floor_construction":2,"heat_loss_perimeter":21.24},{"floor":1,"room_height":2.56,"total_floor_area":48.31,"heat_loss_perimeter":21.24}],"wall_insulation_type":4,"construction_age_band":"C","wall_thickness_measured":"Y","roof_insulation_location":4,"roof_insulation_thickness":"NI"}],"low_energy_lighting":36,"solar_water_heating":"N","bedf_revision_number":352,"habitable_room_count":6,"heating_cost_current":{"value":1488,"currency":"GBP"},"insulated_door_count":0,"co2_emissions_current":8.7,"energy_rating_average":60,"energy_rating_current":45,"lighting_cost_current":{"value":111,"currency":"GBP"},"main_heating_controls":[{"description":"Programmer, no room thermostat","energy_efficiency_rating":1,"environmental_efficiency_rating":1}],"multiple_glazing_type":"ND","open_fireplaces_count":2,"has_hot_water_cylinder":"false","heating_cost_potential":{"value":773,"currency":"GBP"},"hot_water_cost_current":{"value":115,"currency":"GBP"},"mechanical_ventilation":0,"percent_draughtproofed":100,"suggested_improvements":[{"sequence":1,"typical_saving":{"value":442,"currency":"GBP"},"indicative_cost":"£4,000 - £14,000","improvement_type":"Q","improvement_details":{"improvement_number":7},"improvement_category":5,"energy_performance_rating":59,"environmental_impact_rating":50},{"sequence":2,"typical_saving":{"value":72,"currency":"GBP"},"indicative_cost":"£800 - £1,200","improvement_type":"W","improvement_details":{"improvement_number":47},"improvement_category":5,"energy_performance_rating":61,"environmental_impact_rating":53},{"sequence":3,"typical_saving":{"value":35,"currency":"GBP"},"indicative_cost":"£45","improvement_type":"E","improvement_details":{"improvement_number":35},"improvement_category":5,"energy_performance_rating":62,"environmental_impact_rating":53},{"sequence":4,"typical_saving":{"value":156,"currency":"GBP"},"indicative_cost":"£350 - £450","improvement_type":"G","improvement_details":{"improvement_number":12},"improvement_category":5,"energy_performance_rating":67,"environmental_impact_rating":60},{"sequence":5,"typical_saving":{"value":65,"currency":"GBP"},"indicative_cost":"£2,200 - £3,000","improvement_type":"I","improvement_details":{"improvement_number":20},"improvement_category":5,"energy_performance_rating":69,"environmental_impact_rating":63},{"sequence":6,"typical_saving":{"value":250,"currency":"GBP"},"indicative_cost":"£9,000 - £14,000","improvement_type":"U","improvement_details":{"improvement_number":34},"improvement_category":5,"energy_performance_rating":77,"environmental_impact_rating":70}],"co2_emissions_potential":3.6,"energy_rating_potential":77,"lighting_cost_potential":{"value":68,"currency":"GBP"},"alternative_improvements":[{"sequence":1,"typical_saving":{"value":75,"currency":"GBP"},"improvement_type":"Z1","improvement_details":{"improvement_number":51},"improvement_category":6,"energy_performance_rating":69,"environmental_impact_rating":72},{"sequence":2,"typical_saving":{"value":252,"currency":"GBP"},"improvement_type":"Z3","improvement_details":{"improvement_number":53},"improvement_category":6,"energy_performance_rating":75,"environmental_impact_rating":72}],"hot_water_cost_potential":{"value":103,"currency":"GBP"},"renewable_heat_incentive":{"water_heating":2305,"impact_of_loft_insulation":-1271,"impact_of_solid_wall_insulation":-7561,"space_heating_existing_dwelling":22097},"seller_commission_report":"Y","energy_consumption_current":315,"has_fixed_air_conditioning":"false","calculation_software_version":8.3,"energy_consumption_potential":121,"environmental_impact_current":36,"fixed_lighting_outlets_count":14,"current_energy_efficiency_band":"E","environmental_impact_potential":70,"has_heated_separate_conservatory":"false","potential_energy_efficiency_band":"C","co2_emissions_current_per_floor_area":68,"low_energy_fixed_lighting_outlets_count":5}} \ No newline at end of file diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 5a04e5165..f07130347 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -3944,6 +3944,22 @@ def _normalize_sap_schema_16_x(data: Dict[str, Any]) -> Dict[str, Any]: if "single" in description: d["multiple_glazing_type"] = 5 + # P4 (worklist): 16.x certs sometimes omit `multiple_glazed_proportion` + # entirely (RdSapSchema17_1 requires it, no default) even though the + # human `window.description` states the glazing extent unambiguously — + # e.g. cert 0141-2860-6891-9124-5625 (uprn 100021969385, SAP-16.2): + # multiple_glazing_type="ND", proportion absent, description "Fully + # double glazed". Derive from that explicit text rather than a flat + # default (a flat `100` for every ND-missing cert previously regressed + # the component-accuracy donor pool — see worklist P4): "single" → 0, + # "double" → 100. Genuinely ambiguous certs (no double/single wording) + # are left unset and still fail loud — correct, data is insufficient. + if "multiple_glazed_proportion" not in d: + if "single" in description: + d["multiple_glazed_proportion"] = 0 + elif "double" in description: + d["multiple_glazed_proportion"] = 100 + sap_heating: Any = d.get("sap_heating") if isinstance(sap_heating, dict): heating: Dict[str, Any] = cast(Dict[str, Any], sap_heating) diff --git a/datatypes/epc/domain/tests/test_from_sap_schema.py b/datatypes/epc/domain/tests/test_from_sap_schema.py index b561393cf..6b833fb6f 100644 --- a/datatypes/epc/domain/tests/test_from_sap_schema.py +++ b/datatypes/epc/domain/tests/test_from_sap_schema.py @@ -838,6 +838,25 @@ class TestFromSapSchema16_2: ): EpcPropertyDataMapper.from_api_response(data) + def test_16_x_missing_multiple_glazed_proportion_derived_from_double_description( + self, + ) -> None: + # Cert 0141-2860-6891-9124-5625 (uprn 100021969385, SAP-16.2, property + # 753950): multiple_glazing_type="ND", `multiple_glazed_proportion` + # entirely absent, but windows[].description explicitly says "Fully + # double glazed" — an unambiguous signal, unlike the truly ambiguous + # ND-only certs the fail-loud test above still guards. The normaliser + # derives 100 rather than leaving the cert unmappable. + data = load("sap_16_2.json") + data["windows"][0]["description"] = "Fully double glazed" + del data["multiple_glazed_proportion"] + + epc = EpcPropertyDataMapper.from_api_response(data) + + assert isinstance(epc, EpcPropertyData) + assert epc.sap_windows + assert all(w.glazing_type == 2 for w in epc.sap_windows) # DG-modal slot + def test_recorded_co2_as_measurement_dict_is_coerced_not_crashed(self) -> None: # Some certs (e.g. 16.x cert 2308-4997-7262-0137-9930) lodge # `co2_emissions_current` as a Measurement dict {'value': 3.5, 'quantity': diff --git a/tests/domain/epc_prediction/test_component_accuracy_gate.py b/tests/domain/epc_prediction/test_component_accuracy_gate.py index ce7105ef6..e4bcd41f6 100644 --- a/tests/domain/epc_prediction/test_component_accuracy_gate.py +++ b/tests/domain/epc_prediction/test_component_accuracy_gate.py @@ -65,6 +65,24 @@ _FIXTURE = Path(__file__).parents[3] / "tests" / "fixtures" / "epc_prediction" # and one full-SAP target the similarity-weighted donors don't predict as PV # tips the agreement 32/33 (the held-out actual is now correct — a ground-truth- # method change, not a prediction-logic loosening). Tighten-only resumes here. +# +# has_hot_water_cylinder 0.8687->0.8586, cylinder_insulation_type 0.3333->0.1667, +# door_count residual 0.3131->0.3333 re-baselined when the mapper started +# deriving `multiple_glazed_proportion` (single->0 / double->100) from an +# unambiguous window.description for 16.x certs whose "ND" multiple_glazing_type +# previously left the numeric field entirely absent — RdSapSchema17_1 requires +# it, so the cert failed loud and was excluded from the donor pool wholesale +# (worklist P4; see the mapper comment at `_normalize_sap_schema_16_x`). +# Motivating case: cert 0141-2860-6891-9124-5625 / uprn 100021969385 (property +# 753950), lodging "Fully double glazed" with the proportion field missing — +# previously hard-failed the whole modelling_e2e batch it was in. Admitting +# these certs is a ground-truth-method change (more real lodged EPCs enter the +# comparable pool), not a prediction-logic loosening; the tip is donor-pool +# composition on a handful of near-tie matches, same mechanism as the #1245 / +# ADR-0037 re-baselines above. A flat `multiple_glazed_proportion=100` default +# was tried earlier and reverted for the same reason without a derivation +# rationale; deriving it from explicit "single"/"double" wording is the +# targeted fix worklist P4 called for. Tighten-only resumes from these values. _RATE_FLOORS: dict[str, float] = { "wall_construction": 0.9091, "wall_insulation_type": 0.8687, @@ -77,8 +95,8 @@ _RATE_FLOORS: dict[str, float] = { "heating_main_control": 0.9091, "water_heating_fuel": 0.9495, "water_heating_code": 0.9798, - "has_hot_water_cylinder": 0.8687, - "cylinder_insulation_type": 0.3333, + "has_hot_water_cylinder": 0.8586, + "cylinder_insulation_type": 0.1667, "secondary_heating_type": 0.0000, "roof_insulation_thickness": 0.4118, "roof_insulation_thickness_pm1": 0.4118, @@ -110,7 +128,7 @@ _RESIDUAL_CEILINGS: dict[str, float] = { "floor_area": 12.0586, "total_window_area": 3.7184, "building_parts": 0.1212, - "door_count": 0.3131, + "door_count": 0.3333, } _TOLERANCE = 1e-3