Merge branch 'main' into feature/abri-api-integration

This commit is contained in:
Daniel Roth 2026-07-06 15:15:54 +00:00
commit 329e896ba5
25 changed files with 1717 additions and 271 deletions

View file

@ -148,6 +148,7 @@ jobs:
- name: Terraform Plan
working-directory: ${{ inputs.lambda_path }}
env:
TF_VAR_github_sha: ${{ github.sha }}
TF_VAR_db_host: ${{ secrets.TF_VAR_db_host }}
TF_VAR_db_name: ${{ secrets.TF_VAR_db_name }}
TF_VAR_db_port: ${{ secrets.TF_VAR_db_port }}

View file

@ -0,0 +1,23 @@
name: Check FastAPI Lambda package size
# Runs on PRs targeting main so a Lambda-size regression fails the PR before
# it can merge to main and roll into the dev deploy — deploy_terraform.yml
# only triggers on push to dev/prod, which is too late (see PR #1469: this
# broke dev for weeks before anyone noticed).
on:
pull_request:
branches:
- main
jobs:
fast_api_lambda_zip_size_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Check projected unzipped Lambda size
run: python3 backend/app/requirements/check_lambda_zip_size.py

View file

@ -53,7 +53,9 @@ async def log_requests(request: Request, call_next):
@app.get("/health")
async def health():
return {"status": "ok", "sha": os.getenv("GITHUB_SHA", "unknown")}
sha = os.getenv("GITHUB_SHA", "unknown")
logger.info(f"Health check OK - deployed sha={sha}")
return {"status": "ok", "sha": sha}
app.include_router(tasks_router.router, prefix="/v1")

View file

@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""Estimate the unzipped size of the fastapi Lambda before deploying.
Mirrors what `deployment/terraform/modules/lambda_with_api_gateway/main.tf` does
for the `ara_fast_api` lambda (`deployment/terraform/lambda/fast-api`):
`pip install -r requirements.txt -t <source_dir> ...` followed by zipping
`source_dir` minus `zip_excludes`. AWS Lambda's hard limit is 262144000 bytes
(250 MiB) unzipped. We install the dependencies into a scratch directory
instead of the real source tree so this can run locally or in CI without
mutating the repo.
Usage (from the repo root):
python3 backend/app/requirements/check_lambda_zip_size.py --source-dir .
--requirements defaults to the requirements.txt next to this script, and
--exclude defaults to whatever `zip_excludes` is set to in
`deployment/terraform/modules/lambda_with_api_gateway/variables.tf` read
straight from that file, so this can never silently drift from what
Terraform actually excludes. Pass --exclude explicitly to override.
Exits non-zero if the projected unzipped size exceeds --limit (or exceeds
--warn-pct of the limit, when --strict is not passed the warning is just
printed).
"""
from __future__ import annotations
import argparse
import fnmatch
import re
import subprocess
import sys
import tempfile
from pathlib import Path
LAMBDA_UNZIPPED_LIMIT_BYTES = 262_144_000
REPO_ROOT = Path(__file__).resolve().parents[3]
DEFAULT_REQUIREMENTS = Path(__file__).resolve().parent / "requirements.txt"
DEFAULT_TF_VARIABLES_FILE = (
REPO_ROOT / "deployment/terraform/modules/lambda_with_api_gateway/variables.tf"
)
def parse_zip_excludes_from_tf(tf_variables_file: Path) -> list[str]:
text = tf_variables_file.read_text()
match = re.search(
r'variable\s+"zip_excludes"\s*{.*?default\s*=\s*\[(.*?)\]', text, re.S
)
if not match:
raise ValueError(
f'could not find variable "zip_excludes" default in {tf_variables_file}'
)
return re.findall(r'"([^"]+)"', match.group(1))
def doublestar_match(pattern_parts: list[str], path_parts: list[str]) -> bool:
if not pattern_parts:
return not path_parts
head, rest = pattern_parts[0], pattern_parts[1:]
if head == "**":
if doublestar_match(rest, path_parts):
return True
if path_parts and doublestar_match(pattern_parts, path_parts[1:]):
return True
return False
if not path_parts:
return False
if fnmatch.fnmatchcase(path_parts[0], head):
return doublestar_match(rest, path_parts[1:])
return False
def is_excluded(rel_path: Path, excludes: list[str]) -> bool:
parts = rel_path.as_posix().split("/")
return any(doublestar_match(pattern.split("/"), parts) for pattern in excludes)
def sum_dir_size(root: Path, excludes: list[str] | None = None) -> int:
total = 0
for path in root.rglob("*"):
if path.is_dir():
continue
rel = path.relative_to(root)
if excludes and is_excluded(rel, excludes):
continue
total += path.stat().st_size
return total
def install_dependencies(requirements: Path, target: Path) -> int:
subprocess.run(
[
"pip",
"install",
"-r",
str(requirements),
"-t",
str(target),
"--platform",
"manylinux2014_x86_64",
"--implementation",
"cp",
"--python-version",
"3.11",
"--only-binary=:all:",
],
check=True,
)
return sum_dir_size(target)
def human(n: int) -> str:
return f"{n:,} bytes ({n / 1024 / 1024:.1f} MiB)"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source-dir", type=Path, default=REPO_ROOT)
parser.add_argument("--requirements", type=Path, default=DEFAULT_REQUIREMENTS)
parser.add_argument(
"--skip-deps",
action="store_true",
help="Skip the pip install step and only measure source files.",
)
parser.add_argument(
"--exclude",
action="append",
default=None,
dest="excludes",
help="Repeatable. Defaults to zip_excludes read from --tf-variables-file.",
)
parser.add_argument("--tf-variables-file", type=Path, default=DEFAULT_TF_VARIABLES_FILE)
parser.add_argument("--limit", type=int, default=LAMBDA_UNZIPPED_LIMIT_BYTES)
parser.add_argument(
"--warn-pct",
type=float,
default=85.0,
help="Print a warning once projected size crosses this %% of the limit.",
)
parser.add_argument(
"--strict",
action="store_true",
help="Exit non-zero on the warning threshold too, not just the hard limit.",
)
args = parser.parse_args()
excludes = args.excludes
if excludes is None:
excludes = parse_zip_excludes_from_tf(args.tf_variables_file)
print(f"excludes (from {args.tf_variables_file.relative_to(REPO_ROOT)}): {excludes}")
source_size = sum_dir_size(args.source_dir, excludes)
deps_size = 0
if not args.skip_deps:
with tempfile.TemporaryDirectory(prefix="lambda-deps-") as tmp:
deps_size = install_dependencies(args.requirements, Path(tmp))
total = source_size + deps_size
pct = total / args.limit * 100
print(f"source files (post-exclude): {human(source_size)}")
if not args.skip_deps:
print(f"pip dependencies: {human(deps_size)}")
print(f"projected unzipped total: {human(total)} [{pct:.1f}% of limit]")
print(f"AWS limit: {human(args.limit)}")
if total > args.limit:
print(
f"\nFAIL: projected unzipped size exceeds the Lambda limit by "
f"{human(total - args.limit)}.",
file=sys.stderr,
)
return 1
if pct >= args.warn_pct:
msg = (
f"\nWARNING: projected unzipped size is at {pct:.1f}% of the "
f"{human(args.limit)} limit."
)
print(msg, file=sys.stderr)
if args.strict:
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,452 @@
{
"uprn": 100031768368,
"roofs": [
{
"description": "Pitched, 100 mm loft insulation",
"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": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"addendum": {
"addendum_numbers": [
15
]
},
"lighting": {
"description": "Below average lighting efficiency",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"postcode": "ST16 3TS",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "STAFFORD",
"psv_count": 0,
"built_form": 3,
"created_at": "2026-05-14 07:52:27",
"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": 2
}
],
"water_heating_code": 901,
"water_heating_fuel": 26,
"secondary_fuel_type": 26,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 0,
"main_heating_number": 1,
"main_heating_control": 2106,
"main_heating_category": 2,
"main_heating_fraction": 1,
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 18250
}
],
"immersion_heating_type": "NA",
"secondary_heating_type": 605,
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"glazing_gap": 6,
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.1,
"window_height": 1.02,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": 6,
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.64,
"window_height": 1.01,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": 6,
"orientation": 7,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.96,
"window_height": 0.96,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": 6,
"orientation": 7,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.99,
"window_height": 0.96,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": 6,
"orientation": 7,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.48,
"window_height": 0.97,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": 6,
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.55,
"window_height": 1.19,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": 6,
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.55,
"window_height": 1.19,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": 6,
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.1,
"window_height": 1.3,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": 6,
"orientation": 7,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.36,
"window_height": 2.08,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": 6,
"orientation": 7,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.9,
"window_height": 2.03,
"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": "End-terrace house",
"language_code": 1,
"pressure_test": 4,
"property_type": 0,
"address_line_1": "21 Greenway",
"assessment_type": "RdSAP",
"completion_date": "2026-05-14",
"inspection_date": "2026-05-13",
"extensions_count": 0,
"measurement_type": 1,
"open_flues_count": 0,
"total_floor_area": 76,
"transaction_type": 1,
"conservatory_type": 2,
"has_draught_lobby": "false",
"heated_room_count": 4,
"other_flues_count": 1,
"registration_date": "2026-05-14",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 2,
"pv_diverter": "false",
"pv_connection": 0,
"pv_battery_count": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "true",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "true",
"is_hydro_output_connected_to_dwelling_meter": "false"
},
"secondary_heating": {
"description": "Room heaters, mains gas",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 1,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 280,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.45,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 37.75,
"quantity": "square metres"
},
"party_wall_length": {
"value": 5,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 20.1,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.45,
"quantity": "metres"
},
"total_floor_area": {
"value": 37.75,
"quantity": "square metres"
},
"party_wall_length": {
"value": 5,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 20.1,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "C",
"party_wall_construction": 1,
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "100mm",
"wall_insulation_thickness": "NI"
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 4,
"heating_cost_current": {
"value": 1188,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 3.4,
"energy_rating_average": 60,
"energy_rating_current": 61,
"lighting_cost_current": {
"value": 55,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"blocked_chimneys_count": 0,
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 739,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 267,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 371,
"indicative_cost": "\u00a37,500 - \u00a311,000",
"improvement_type": "Q",
"improvement_details": {
"improvement_number": 7
},
"improvement_category": 5,
"energy_performance_rating": 70,
"environmental_impact_rating": 73
},
{
"sequence": 2,
"typical_saving": 77,
"indicative_cost": "\u00a35,000 - \u00a310,000",
"improvement_type": "W2",
"improvement_details": {
"improvement_number": 58
},
"improvement_category": 5,
"energy_performance_rating": 71,
"environmental_impact_rating": 75
},
{
"sequence": 3,
"typical_saving": 222,
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 76,
"environmental_impact_rating": 76
}
],
"co2_emissions_potential": 2,
"energy_rating_potential": 76,
"lighting_cost_potential": {
"value": 55,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0.1",
"flueless_gas_fires_count": 0,
"hot_water_cost_potential": {
"value": 267,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 1847.12,
"space_heating_existing_dwelling": 11154.61
},
"draughtproofed_door_count": 1,
"energy_consumption_current": 252,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "4.3.74",
"energy_consumption_potential": 145,
"environmental_impact_current": 61,
"cfl_fixed_lighting_bulbs_count": 2,
"current_energy_efficiency_band": "D",
"environmental_impact_potential": 76,
"led_fixed_lighting_bulbs_count": 10,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 45,
"incandescent_fixed_lighting_bulbs_count": 1
}

View file

@ -86,6 +86,7 @@ module "fastapi" {
environment = {
ENVIRONMENT = var.stage
GITHUB_SHA = var.github_sha
API_KEY = var.api_key
SECRET_KEY = var.secret_key
# DOMAIN_NAME = var.domain_name
@ -110,11 +111,11 @@ module "fastapi" {
CARBON_BASELINE_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_carbon_baseline_predictions_bucket_name
HEAT_BASELINE_PREDICTIONS_BUCKET = data.terraform_remote_state.shared.outputs.retrofit_heat_baseline_predictions_bucket_name
ENGINE_SQS_URL = data.terraform_remote_state.engine.outputs.ara_engine_queue_url
CATEGORISATION_SQS_URL = data.terraform_remote_state.categorisation.outputs.categorisation_queue_url
POSTCODE_SPLITTER_SQS_URL = data.terraform_remote_state.postcode_splitter.outputs.postcode_splitter_queue_url
COMBINER_SQS_URL = data.terraform_remote_state.bulk_address2uprn_combiner.outputs.bulk_address2uprn_combiner_queue_url
FINALISER_SQS_URL = data.terraform_remote_state.bulk_upload_finaliser.outputs.bulk_upload_finaliser_queue_url
ENGINE_SQS_URL = data.terraform_remote_state.engine.outputs.ara_engine_queue_url
CATEGORISATION_SQS_URL = data.terraform_remote_state.categorisation.outputs.categorisation_queue_url
POSTCODE_SPLITTER_SQS_URL = data.terraform_remote_state.postcode_splitter.outputs.postcode_splitter_queue_url
COMBINER_SQS_URL = data.terraform_remote_state.bulk_address2uprn_combiner.outputs.bulk_address2uprn_combiner_queue_url
FINALISER_SQS_URL = data.terraform_remote_state.bulk_upload_finaliser.outputs.bulk_upload_finaliser_queue_url
}
}

View file

@ -41,4 +41,10 @@ variable "epc_auth_token" {
variable "google_solar_api_key" {
type = string
sensitive = true
}
}
variable "github_sha" {
type = string
description = "Commit SHA being deployed, surfaced via /health so cloud logs can be tied back to a deploy."
default = "unknown"
}

View file

@ -11,7 +11,10 @@ variable "zip_excludes" {
"**/*.pyc",
"**/.pytest_cache/**",
"**/tests/**",
"**/deployment/**"
"**/deployment/**",
"**/.git/**",
"**/json_samples/**",
"**/docs/specs/**"
]
}

View file

@ -0,0 +1,88 @@
# Electric boiler and CPSU overrides drag category 2 and the conservative Group 1 control
## Status
accepted
## Context
[ADR-0048](0048-heating-override-forces-category-and-control-defers-fuel-and-meter.md)
requires a system-replacing heating override to emit a complete companion set
(category and control archetype-forced, never inherited) and deferred the
electric boiler / CPSU expansion; [ADR-0050](0050-electric-underfloor-override-drags-category-8-and-conservative-group-7-control.md)
completed the underfloor half. This ADR completes the electric-boiler half
(#1444 Parts A+B for codes **191/192** — the archetypes "Direct-acting
electric", "Electric boiler", "Electric CPSU").
A portfolio-796 scan of the 126 live 191/192 overrides found **30 of the first
40 in a suppressed or mis-billed state** from inherited companions:
- **Inherited category 6** (replaced community heating — the bulk): the heating
generator's `is_heat_network_main` gate reads the dwelling as a heat network,
so the **HHRSH bundle is never offered** — only tune-ups. Same class as
ADR-0050's evidence property.
- **Inherited category 4** (replaced heat pump — property 711954): *every*
heating bundle suppressed.
- **Inherited category 7** (replaced storage heaters — property 721912): the
SAP 10.2 Table 12a resolver's category-7 branch bills the direct-acting
boiler as storage — **100% at the off-peak low rate** — the precise
ADR-0048 room-heater mis-billing, resurfaced.
- Stale controls ride along (community 23xx, storage 2401, room-heater 2603),
applying the wrong Table 4e temperature adjustment.
## Decision
The electric-boiler codes (SAP Table 4a **191** direct-acting electric boiler,
**192** electric CPSU) join the forced-companion branches of
`main_heating_system_overlay`:
1. **Category 2** ("boiler system with radiators or underfloor heating") in
`_category_for` — deterministic from SAP Table 4a; the same category the
gas-boiler overlay already stamps. Table 12a billing for these codes is
code-keyed (191 → `DIRECT_ACTING_ELECTRIC_BOILER`, 192 → the deliberate
Appendix-F fallback), so the category change *removes* stale-category
misrouting rather than selecting a new billing row.
2. **Control 2101** (SAP Table 4e **Group 1**, "no time or thermostatic control
of room temperature", **+0.6 °C**) in `_control_for` — the conservative
default within the group the category fixes: the largest Group 1
mean-internal-temperature adjustment, so an unobserved control is never
over-credited (the boiler mirror of storage 2401 / room-heater 2601 /
underfloor 2701). This **supersedes the earlier "direct-acting electric
drags no control" choice** — code 191 is a wet Group-1 boiler, and leaving
the control unset inherits the replaced system's (the ADR-0048 bug class),
which is strictly worse than a conservative assumption.
Deliberate asymmetry with gas boilers: the gas archetypes force **2106**
(full modern controls) because they also assume a modern condensing install
(ADR-0035, decided with Khalim) — the whole system is modelled modern. The
electric-boiler archetypes describe an *existing* system of unknown vintage
modelled at code 191/192's intrinsic efficiency, so the conservative
default applies.
Fuel (electricity 29, ADR-0045) and meter (192 off-peak Dual via §12; 191
Single) were already correct and are unchanged. A forced 2101 also makes the
system tune-up options available (2101 is improvable) — a coherent cheap lever
for these dwellings.
## Consequences
- The suppressed cohort regains its heating pathway: category 6/4 inheritors
become HHRSH-eligible (verified live: 711954 gains the HHRSH option), and
category-7 inheritors stop billing direct-acting heat at the all-night low
rate (721912).
- The "incomplete companion set" error log no longer fires for
Direct-acting electric / Electric boiler / Electric CPSU. Remaining gap-log
archetypes (#1444): heat pumps (Group 2 control), community heating
(Group 3 control — interacts with the Table 4c(3) charging factor, needs an
accredited worksheet), then the log flips to a raise (Part C).
- Overrides re-model with a different (usually lower) baseline where a stale
category was over-crediting — an accuracy correction, not a regression.
### Alternatives rejected
- **Keep "no control" for direct-acting electric.** Rejected: `None` is not
"no control assumed" — by last-wins composition it means *inherit the
replaced system's control*, the silent mis-rating ADR-0048 exists to stop.
- **Force modern controls (2106) like the gas boilers.** Rejected: nothing in
the electric-boiler archetype implies a modern install; assuming full
controls over-credits every dwelling whose controls were never observed.

View file

@ -0,0 +1,109 @@
# The heating-override companion taxonomy is complete; an incomplete set now raises
## Status
accepted
## Context
[ADR-0048](0048-heating-override-forces-category-and-control-defers-fuel-and-meter.md)
set the invariant — a system-replacing heating override emits a complete
category + control companion set, never inheriting the replaced system's — and
intended, once the taxonomy was covered, to "flip the log to a raise, so the
coherence invariant is enforced" (#1444 Part C).
[ADR-0050](0050-electric-underfloor-override-drags-category-8-and-conservative-group-7-control.md)
(underfloor) and
[ADR-0052](0052-electric-boiler-and-cpsu-overrides-drag-category-2-and-conservative-group-1-control.md)
(electric boilers) filled two families. Two archetype families remained on the
gap-log, and with them Part C stayed blocked:
- **"Air source heat pump" (SAP 211)** — category 4 already forced, control
`None` (Table 4e Group 2 unmapped). No live overrides today (census across
all portfolios: 0), so this is taxonomy completion, not a live defect.
- **"Community heating, boilers" / "…, CHP and boilers" (301/302)** — category
6 already forced, control `None` (Group 3 unmapped). **2,220 live
overrides** — the largest population on the log. The inherited control is
doubly load-bearing for heat networks: it feeds the Table 4e temperature
adjustment *and* the SAP 10.2 Table 4c(3) charging factors (worksheet
(305)/(305a)).
## Decision
1. **Wet heat pumps (Table 4a 211-224) drag control 2201** — Table 4e Group 2
"no time or thermostatic control of room temperature", **+0.3 °C**, the
largest Group 2 adjustment: the conservative in-group default (the heat-pump
mirror of storage 2401 / room-heater 2601 / underfloor 2701 / boiler 2101).
Warm-air heat pumps (521-527) are deliberately left unmapped — no archetype
reaches them, and the raise below is the guard that a future warm-air
archetype cannot land without choosing its Group 5 control.
This is the *existing-system* fill policy: the ASHP **Measure** overlay
installs 2210 (time-and-temperature zone control) because a measure designs
a modern end-state (ADR-0024); a landlord override describes an existing
pump whose control was never observed, so the conservative default applies.
2. **Community heating (301/302) defaults to control 2313, keeping an
assessor-observed Group 3 control** (decided with Khalim, revising the
first-cut 2301). The control encodes two independent unknowns and the
default is chosen per axis:
- **Kit axis** (thermostat/TRVs): community schemes skew modern, and the
portfolio's own lodged community certs are overwhelmingly thermostatted
(modal control 2306; none lodge 2301) — so 2313 ("flat rate charging,
room thermostat and TRVs", temperature adjustment 0.0) concedes this
axis to the evidence.
- **Charging axis** (flat rate vs usage-linked): the billing arrangement
is scheme paperwork an override genuinely cannot see, so flat rate
(Table 4c(3) space **1.05**, DHW **1.05**) is retained — the
usage-linked credit (1.0/1.0) is never assumed.
- **Keep-observed**: when the cert already lodges a Group 3 control the
override is confirming a community system, not replacing one — the
assessor observed that control, and stamping any default over it
destroys real information (a usage-linked 2306 is worth ~1-2 SAP over
flat-rate). The overlay opts in via
`keep_existing_heat_network_control` (the control mirror of ADR-0035's
`keep_existing_off_peak_meter`); only a stale cross-family control (the
boiler-2110 / storage-2401 inheritances) is replaced by 2313.
3. **An incomplete companion set now raises** (`ValueError`) instead of
logging. Every archetype in `_MAIN_HEATING_CODES` resolves a complete
category + control (gas boilers via their dedicated overlay; all others via
`_category_for`/`_control_for`), so the log-and-continue mitigation has no
remaining population — and a silent log was only ever a stop-gap
(ADR-0048: "the intended end-state is to fill each archetype's
category/control … and then flip the log to a raise"). A future archetype
added without companions now fails loudly at overlay time rather than
shipping a silently-incoherent cert.
## Consequences
- The companion-gap error log is gone; #1444 closes.
- Community overrides converge on defensible controls with minimal score
churn. Live sample (portfolio 796, 12 properties): the ten dwellings with
assessor-observed Group 3 controls (2306/2307/2309) shift **±0.0** — their
observed controls survive the override — and only the two group-incoherent
dwellings (a Group-1 *boiler* zone control 2110 riding on a heat network)
move, **1.0** each onto the 2313 default. Contrast the first-cut 2301
design, which shifted the whole sample 0 to 3.
- Adding a heating archetype now *requires* choosing its category + control
(or extending the family sets) — the raise turns the ADR-0048 checklist into
an enforced contract.
### Alternatives rejected
- **The maximally conservative community default (2301).** The first cut.
Rejected on evidence: no lodged community cert in the portfolio carries
2301, so it under-rates the typical (modern, thermostatted) scheme and
shifted the sampled cohort 0 to 3 SAP for no informational gain.
- **A usage-linked community default (2306/2312/2314).** Rejected: credits a
billing arrangement the landlord never reported — the charging axis is the
one the override genuinely cannot observe, so it stays flat-rate.
- **Stamping the default over an observed Group 3 control.** Rejected: the
ADR-0035 "always stamp" pattern exists to kill *stale* companions; an
in-group control on a confirmed community cert is observed data, and
overwriting it is information destruction, not coherence.
- **Keep log-and-continue indefinitely.** Rejected: the log was the interim
mitigation while the taxonomy was incomplete; leaving it means the next
archetype ships the ADR-0048 inheritance bug silently again.
- **Raise on warm-air heat-pump codes' missing control now.** Not applicable —
no archetype maps to 521-527; the raise fires exactly when one is added
without its Group 5 control, which is the point.

View file

@ -28,7 +28,6 @@ are left UNKNOWN until modelled. Unresolvable values produce no overlay.
from __future__ import annotations
import logging
from typing import Optional
from domain.modelling.simulation import EpcSimulation, HeatingOverlay
@ -36,8 +35,6 @@ from domain.sap10_calculator.tables.table_12a import (
OFF_PEAK_IMPLYING_HEATING_CODES,
)
logger = logging.getLogger(__name__)
# Off-peak (Economy 7) meter. Electric storage / CPSU systems charge overnight at
# the low rate and cannot run economically on a single-rate meter; "Dual" lets
# the §12 dispatch resolve the specific tariff (storage 7-hour, CPSU 10-hour).
@ -130,6 +127,22 @@ _ELECTRIC_ROOM_HEATER_CODES = frozenset(range(691, 702))
# unambiguously electric, so they drag electricity like the electric room heaters
# (ADR-0045, closing the prior no-fuel gap on 191/192).
_ELECTRIC_BOILER_CODES = frozenset({191, 192})
# SAP Table 4a "boiler system" category — codes 191/192 are wet electric
# boilers, same category the gas-boiler overlay stamps. Forced so an
# electric-boiler override never keeps the replaced system's category: on
# portfolio 796 an inherited 6 (community) or 4 (heat pump) suppressed the
# HHRSH pathway, and an inherited 7 (storage) made the Table 12a resolver bill
# direct-acting heat wholly at the off-peak low rate (ADR-0052, #1444).
_ELECTRIC_BOILER_CATEGORY = 2
# SAP Table 4e Group 1 boiler control: 2101 ("no time or thermostatic control
# of room temperature", +0.6 C) is the conservative default when the landlord
# named only the system — the largest Group 1 mean-internal-temperature
# adjustment, so an unobserved control is never over-credited (the boiler
# mirror of storage 2401 / room-heater 2601 / underfloor 2701). Deliberately
# NOT the gas boilers' modern 2106: the gas archetypes assume a whole modern
# condensing install; the electric-boiler archetypes describe an existing
# system of unknown vintage (ADR-0052).
_CONSERVATIVE_BOILER_CONTROL = 2101
# Electric underfloor — SAP Table 4a 421/422 (off-peak) and 424 (screed) — also
# unambiguously electric (ADR-0046).
_ELECTRIC_UNDERFLOOR_CODES = frozenset({421, 422, 424})
@ -152,6 +165,17 @@ _UNDERFLOOR_CONTROL = 2701
# without a PCDB index (ADR-0041).
_HEAT_PUMP_CATEGORY = 4
_HEAT_PUMP_CODES = frozenset(range(211, 225)) | frozenset(range(521, 528))
# SAP Table 4e Group 2 heat-pump control: 2201 ("no time or thermostatic
# control of room temperature", +0.3 C) is the conservative default when the
# landlord named only the system — the largest Group 2 adjustment, so an
# unobserved control is never over-credited. Deliberately NOT the ASHP
# Measure's 2210 zone control: a measure designs a modern end-state
# (ADR-0024); an override describes an existing pump (ADR-0053). Scoped to the
# WET heat pumps (211-224) — warm-air pumps (521-527) take Table 4e Group 5
# and stay unmapped (no archetype maps to them; the incomplete-companion raise
# guards that a future one cannot land without choosing its Group 5 control).
_WET_HEAT_PUMP_CODES = frozenset(range(211, 225))
_CONSERVATIVE_HEAT_PUMP_CONTROL = 2201
# Community / heat-network heating (SAP Table 4a 301-304) is category 6; the
# calculator's `_is_heat_network` keys off code OR category 6. The boiler-driven
@ -161,6 +185,18 @@ _HEAT_PUMP_CODES = frozenset(range(211, 225)) | frozenset(range(521, 528))
_HEAT_NETWORK_CATEGORY = 6
_HEAT_NETWORK_CODES = frozenset({301, 302, 303, 304})
_COMMUNITY_BOILER_CODES = frozenset({301, 302, 303})
# SAP Table 4e Group 3 heat-network control: 2313 ("flat rate charging, room
# thermostat and TRVs") is the default when the landlord named only the scheme
# — a per-axis choice (ADR-0053, decided with Khalim). KIT axis: community
# schemes skew modern and the portfolio's lodged community certs are
# overwhelmingly thermostatted (modal 2306; none lodge 2301), so a room
# thermostat + TRVs is the evidence-backed assumption (temperature adjustment
# 0.0). CHARGING axis: flat rate (Table 4c(3) space 1.05 / DHW 1.05) — the
# billing arrangement is scheme paperwork an override cannot see, so the
# usage-linked credit (1.0/1.0) is never assumed. An assessor-OBSERVED Group 3
# control on the cert is kept in preference to this default (the overlay sets
# `keep_existing_heat_network_control`; the applicator honours it).
_DEFAULT_HEAT_NETWORK_CONTROL = 2313
_COMMUNITY_GAS_FUEL = 20
# SAP Table 4c full boiler-control code: programmer + room thermostat + TRVs. The
@ -274,8 +310,9 @@ def _meter_for(code: int) -> Optional[str]:
def _control_for(code: int) -> Optional[int]:
"""The control to assume when the landlord named only the system: a
conservative manual charge control for storage heaters, full modern controls
for a gas boiler, None for systems that take neither (direct-acting electric).
Overwrites a stale control inherited from the system being replaced."""
for a gas boiler, the conservative in-group default for the other mapped
families (room heaters, underfloor, electric boilers). Overwrites a stale
control inherited from the system being replaced."""
if code == _HHR_STORAGE_CODE:
return _HHR_CHARGE_CONTROL
if code in _STORAGE_HEATER_CODES:
@ -286,6 +323,12 @@ def _control_for(code: int) -> Optional[int]:
return _ROOM_HEATER_CONTROL
if code in _ELECTRIC_UNDERFLOOR_CODES:
return _UNDERFLOOR_CONTROL
if code in _ELECTRIC_BOILER_CODES:
return _CONSERVATIVE_BOILER_CONTROL
if code in _WET_HEAT_PUMP_CODES:
return _CONSERVATIVE_HEAT_PUMP_CONTROL
if code in _HEAT_NETWORK_CODES:
return _DEFAULT_HEAT_NETWORK_CONTROL
return None
@ -304,6 +347,8 @@ def _category_for(code: int) -> Optional[int]:
return _STORAGE_HEATER_CATEGORY
if code in _ELECTRIC_UNDERFLOOR_CODES:
return _UNDERFLOOR_CATEGORY
if code in _ELECTRIC_BOILER_CODES:
return _ELECTRIC_BOILER_CATEGORY
if code in _HEAT_PUMP_CODES:
return _HEAT_PUMP_CATEGORY
if code in _HEAT_NETWORK_CODES:
@ -374,21 +419,19 @@ def main_heating_overlay_for(
category = _category_for(code)
control = _control_for(code)
if category is None or control is None:
# A system-replacing override should stamp a complete, coherent companion
# A system-replacing override must stamp a complete, coherent companion
# set; an unmapped category or control leaves the field unset, so the
# effective cert inherits the REPLACED system's value (a silent mis-rating
# — e.g. a room heater keeping a storage heater's category/control). We do
# not yet have confident defaults for every archetype, so log-and-continue
# (matching `flag_fuel_mismatch`) to make the gap visible for review
# rather than raise mid-run.
logger.error(
"Landlord main_heating_system %r (SAP code %d) overlaid with an "
"incomplete companion set (category=%s, control=%s); the effective "
"cert may inherit the replaced system's value — known gap, review",
main_heating_value,
code,
category,
control,
# effective cert inherits the REPLACED system's value — the silent
# mis-rating class ADR-0048 exists to stop. Every mapped archetype now
# carries confident defaults (ADR-0050/0052/0053), so an incomplete set
# can only mean a new archetype landed without choosing its companions:
# fail loudly at overlay time rather than ship an incoherent cert
# (ADR-0048's intended end-state, #1444 Part C).
raise ValueError(
f"Landlord main_heating_system {main_heating_value!r} (SAP code "
f"{code}) resolves an incomplete companion set (category="
f"{category}, control={control}); add the archetype's SAP Table 4a "
f"category and conservative Table 4e control before mapping it"
)
return EpcSimulation(
heating=HeatingOverlay(
@ -402,5 +445,10 @@ def main_heating_overlay_for(
# (e.g. a 24-hour all-low tariff). Measures, which re-meter for real,
# build their HeatingOverlay directly and leave this False.
keep_existing_off_peak_meter=True,
# Likewise a heat-network override's control default (2313) must
# not overwrite an assessor-OBSERVED Group 3 control on a community
# cert being confirmed — only a stale cross-family control is
# replaced (ADR-0053).
keep_existing_heat_network_control=code in _HEAT_NETWORK_CODES,
)
)

View file

@ -152,6 +152,13 @@ _SAP_HEATING_FIELDS: tuple[str, ...] = (
_ENERGY_SOURCE_FIELDS: tuple[str, ...] = ("meter_type", "gas_connection_available")
def _is_heat_network_control(control: object) -> bool:
"""True iff the lodged control is a SAP Table 4e Group 3 (heat network)
code 2301-2314 i.e. an assessor-observed community control a
heat-network override's default must not overwrite (ADR-0053)."""
return isinstance(control, int) and 2301 <= control <= 2314
def _is_off_peak_meter(meter_type: object) -> bool:
"""True iff the meter resolves to an off-peak Table 12a tariff (not the
STANDARD single-rate column). Unparseable / absent meters count as not
@ -177,8 +184,21 @@ def _fold_heating(epc: EpcPropertyData, overlay: HeatingOverlay) -> None:
main = epc.sap_heating.main_heating_details[0]
for field_name in _MAIN_HEATING_FIELDS:
value = getattr(overlay, field_name)
if value is not None:
setattr(main, field_name, value)
if value is None:
continue
# A community override's control is a coherent default, not an observed
# one: when it opts in (`keep_existing_heat_network_control`) and the
# cert already lodges a Table 4e GROUP 3 control (assessor-observed on
# a community cert being confirmed), keep the cert's. A cross-family
# control (boiler 2110, storage 2401) is stale and is still replaced
# (ADR-0053).
if (
field_name == "main_heating_control"
and overlay.keep_existing_heat_network_control
and _is_heat_network_control(main.main_heating_control)
):
continue
setattr(main, field_name, value)
# `main_heating_index_number` (PCDB-resolved, e.g. a heat pump) and
# `sap_main_heating_code` (Table 4a-resolved, e.g. storage heaters) are
# mutually-exclusive efficiency anchors: a whole-system replacement to one

View file

@ -200,6 +200,15 @@ class HeatingOverlay:
# MEASURE re-meters for real (Elmhurst re-lodges 18-hour → Dual on a storage
# install), so it leaves this False. `_fold_heating` reads it.
keep_existing_off_peak_meter: bool = False
# The community-heating mirror of the flag above (ADR-0053): a heat-network
# override's `main_heating_control` is a coherent DEFAULT, not an observed
# control — when the cert already lodges a Table 4e Group 3 control the
# assessor observed it (a community cert being confirmed, not replaced), so
# the default must not overwrite it (e.g. a usage-linked 2306 downgraded to
# flat-rate 2313 destroys ~1-2 real SAP). A cross-family control (a boiler
# 2110, storage 2401) is stale by definition and is still replaced.
# `_fold_heating` reads it.
keep_existing_heat_network_control: bool = False
@dataclass(frozen=True)

View file

@ -260,10 +260,21 @@ def _u_brick_thin_wall_age_a_to_e(wall_thickness_mm: int) -> float:
200 to 280 mm 1.7
280 to 420 mm 1.4
More than 420 mm 1.1
The 200/280 boundary is shared verbatim by adjacent rows in the spec
PDF (Table 13, p.41), which doesn't say which row owns the shared
edge. Elmhurst-validated on cert 100031768368 (280mm exactly, no
insulation, band C, Dry-lining No): its worksheet (29a) computes the
wall at U=1.40 the upper row. A second cert (217091901, band A,
also nominally 280mm) initially looked like a counterexample at
U=1.70, but its Elmhurst build never actually set a wall thickness
(silently inherited a stale 260mm from an earlier build on the
shared assessment) not a real second data point. Only the 280
edge is evidenced here; 200 and 420 are left as originally coded.
"""
if wall_thickness_mm <= 200:
return 2.5
if wall_thickness_mm <= 280:
if wall_thickness_mm < 280:
return 1.7
if wall_thickness_mm <= 420:
return 1.4

View file

@ -32,6 +32,7 @@ from domain.sap10_ml.rdsap_uvalues import (
WALL_SYSTEM_BUILT,
WALL_TIMBER_FRAME,
_insulation_bucket,
_u_brick_thin_wall_age_a_to_e, # pyright: ignore[reportPrivateUsage]
thermal_bridging_y,
u_door,
u_exposed_floor,
@ -140,6 +141,29 @@ def test_u_wall_solid_brick_present_insulation_thin_known_thickness_not_uninsula
assert result == pytest.approx(0.55, abs=0.001)
def test_u_brick_thin_wall_age_a_to_e_280mm_boundary_takes_upper_bucket() -> None:
# Arrange — RdSAP 10 §5.7 Table 13 (PDF p.41) gives four thickness bands
# for an uninsulated solid-brick wall: "up to 200mm" -> 2.5, "200 to
# 280mm" -> 1.7, "280 to 420mm" -> 1.4, "more than 420mm" -> 1.1. The
# 200/280 bands share 280 as an edge verbatim in the spec table, so the
# table text alone doesn't say which band owns it. Elmhurst-validated
# on cert 100031768368 (solid brick, band C, wall_thickness_mm=280
# exactly, no insulation, Dry-lining No): its worksheet (29a) computes
# the wall at U=1.40 — the UPPER band — not 1.70. A second cert
# (217091901, band A, cert wall_thickness=280 too) initially looked
# like Elmhurst evidence for the opposite (U=1.70), but its build
# script never actually set a wall thickness field — the assessment
# had silently carried over a stale 260mm from an earlier build, so
# it isn't a real counter-example (fixed in build_217091901.py).
#
# Only the 280 edge is evidenced here — the 200 and 420 edges are left
# as originally coded pending their own Elmhurst confirmation.
# Act / Assert
assert _u_brick_thin_wall_age_a_to_e(279) == pytest.approx(1.7)
assert _u_brick_thin_wall_age_a_to_e(280) == pytest.approx(1.4)
def test_insulation_bucket_present_thin_thickness_rounds_up_to_50_not_zero() -> None:
# Direct guard on the bucket: present insulation never maps to bucket 0.
assert _insulation_bucket(10, True) == 50

View file

@ -1,10 +1,10 @@
# RdSAP-21.0.1 corpus campaign ledger
**Gauge:** 1000 computed / 0 skipped · SAP within-0.5 = **77.8%** · MAE = **0.636** (floors in test_sap_accuracy_corpus.py)
**Gauge:** 1000 computed / 0 skipped · SAP within-0.5 = **78.6%** · MAE = **0.627** (floors in test_sap_accuracy_corpus.py)
Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail engine bug · ⛔ unbuildable. Line: `uprn · lodged L / eng E / Δ · signature`. Rebuild/re-rank: `PYTHONPATH=. python scripts/corpus_1000/build_worklist.py` (preserves statuses & `<-` notes).
## Clusters (certs with |Δ| ≥ 0.5, ranked by summed |Δ|) — 222 certs in 216 clusters
## Clusters (certs with |Δ| ≥ 0.5, ranked by summed |Δ|) — 214 certs in 208 clusters
### C001 · Σ|Δ| 23.9 · 1 certs · `hpcdb100053/elec | solid brick | (another dwelling | sec:room heaters`
- ⚠ `4510053280` · L 51 / E 74.94 / Δ +23.94 <- lodged software billed out-of-range-PSR (2.031 > 2.0) PCDB 100053 as 100% direct electric; engine follows SAP10.2 N2 extension (305%), Elmhurst-validated on case 56. Pinned engine 75 in test_real_cert_sap_accuracy.
@ -114,553 +114,529 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
### C035 · Σ|Δ| 3.1 · 1 certs · `h633/heatnet | timber frame | roof room(s) | RR,ext1,sec:room heaters,whw909`
- [ ] `22077224` · L 37 / E 33.90 / Δ -3.10
### C036 · Σ|Δ| 3.0 · 1 certs · `hpcdb18498/gas | solid brick | pitched 250 | RR,ext2`
- [ ] `10011740578` · L 70 / E 67.00 / Δ -3.00
### C037 · Σ|Δ| 2.9 · 1 certs · `h402/elec | cavity wall | pitched 250 | sec:portable electric,whw903`
### C036 · Σ|Δ| 2.9 · 1 certs · `h402/elec | cavity wall | pitched 250 | sec:portable electric,whw903`
- [ ] `100110680822` · L 79 / E 76.07 / Δ -2.93
### C038 · Σ|Δ| 2.9 · 1 certs · `h301/coal-anth | solid brick | pitched 250`
### C037 · Σ|Δ| 2.9 · 1 certs · `h301/coal-anth | solid brick | pitched 250`
- [ ] `452032915` · L 75 / E 72.09 / Δ -2.91
### C039 · Σ|Δ| 2.9 · 1 certs · `h301/coal-anth | cavity wall | (another dwelling | whw909`
### C038 · Σ|Δ| 2.9 · 1 certs · `h301/coal-anth | cavity wall | (another dwelling | whw909`
- [ ] `100071306639` · L 73 / E 70.14 / Δ -2.86
### C040 · Σ|Δ| 2.9 · 1 certs · `hpcdb18435/gas+2mains | cavity wall | pitched 100 | ext3,sec:room heaters,whw903`
### C039 · Σ|Δ| 2.9 · 1 certs · `hpcdb18435/gas+2mains | cavity wall | pitched 100 | ext3,sec:room heaters,whw903`
- [ ] `100061346049` · L 55 / E 52.15 / Δ -2.85
### C041 · Σ|Δ| 2.7 · 1 certs · `hpcdb8443/fuel28 | cavity wall | pitched 100 | RR,ext2,sec:room heaters`
### C040 · Σ|Δ| 2.7 · 1 certs · `hpcdb8443/fuel28 | cavity wall | pitched 100 | RR,ext2,sec:room heaters`
- [ ] `10013008238` · L 41 / E 38.26 / Δ -2.74
### C042 · Σ|Δ| 2.7 · 3 certs · `h691/elec | cavity wall | (another dwelling | whw903`
### C041 · Σ|Δ| 2.7 · 3 certs · `h691/elec | cavity wall | (another dwelling | whw903`
- [ ] `10033187652` · L 70 / E 71.21 / Δ +1.21 <- non-electric cost (secondary fuel/cylinder/tariff); spec-faithful vs lodged
- [ ] `200002730` · L 65 / E 64.13 / Δ -0.87
- [ ] `10023328117` · L 78 / E 78.64 / Δ +0.64
### C043 · Σ|Δ| 2.7 · 1 certs · `h633/heatnet | cavity wall | pitched 200 | RR,sec:room heaters,whw903`
### C042 · Σ|Δ| 2.7 · 1 certs · `h633/heatnet | cavity wall | pitched 200 | RR,sec:room heaters,whw903`
- [ ] `10004873253` · L 65 / E 62.29 / Δ -2.71
### C044 · Σ|Δ| 2.7 · 1 certs · `h691/elec | cavity wall | (another dwelling | mv1,whw903`
### C043 · Σ|Δ| 2.7 · 1 certs · `h691/elec | cavity wall | (another dwelling | mv1,whw903`
- [ ] `10090793715` · L 73 / E 75.71 / Δ +2.71
### C045 · Σ|Δ| 2.7 · 1 certs · `hpcdb17964/gas+2mains | cavity wall | pitched 100 | ext1,sec:room heaters`
### C044 · Σ|Δ| 2.7 · 1 certs · `hpcdb17964/gas+2mains | cavity wall | pitched 100 | ext1,sec:room heaters`
- [ ] `100120745600` · L 65 / E 67.69 / Δ +2.69
### C046 · Σ|Δ| 2.7 · 1 certs · `hpcdb17757/gas | granite or | pitched no | RR,ext1`
### C045 · Σ|Δ| 2.7 · 1 certs · `hpcdb17757/gas | granite or | pitched no | RR,ext1`
- [ ] `100051178830` · L 63 / E 65.66 / Δ +2.66
### C047 · Σ|Δ| 2.6 · 1 certs · `hpcdb10321/gas | sandstone as | pitched 100 | ext2`
### C046 · Σ|Δ| 2.6 · 1 certs · `hpcdb10321/gas | sandstone as | pitched 100 | ext2`
- [ ] `100120018282` · L 64 / E 61.36 / Δ -2.64
### C048 · Σ|Δ| 2.6 · 1 certs · `hpcdb17968/gas | cavity wall | pitched 300 | ext2,sec:room heaters`
### C047 · Σ|Δ| 2.6 · 1 certs · `hpcdb17968/gas | cavity wall | pitched 300 | ext2,sec:room heaters`
- [ ] `83035979` · L 65 / E 67.60 / Δ +2.60
### C049 · Σ|Δ| 2.5 · 1 certs · `h524/elec | solid brick | (another dwelling | consv2,sec:room heaters,whw909`
### C048 · Σ|Δ| 2.5 · 1 certs · `h524/elec | solid brick | (another dwelling | consv2,sec:room heaters,whw909`
- [ ] `10014073761` · L 75 / E 72.52 / Δ -2.48
### C050 · Σ|Δ| 2.5 · 1 certs · `hpcdb10328/gas | solid brick | pitched insulated | RR`
### C049 · Σ|Δ| 2.5 · 1 certs · `hpcdb10328/gas | solid brick | pitched insulated | RR`
- [ ] `100050964834` · L 60 / E 62.47 / Δ +2.47
### C051 · Σ|Δ| 2.4 · 1 certs · `h694/elec | cavity wall | (another dwelling | whw941`
### C050 · Σ|Δ| 2.4 · 1 certs · `h694/elec | cavity wall | (another dwelling | whw941`
- [ ] `100120752995` · L 71 / E 73.42 / Δ +2.42
### C052 · Σ|Δ| 2.4 · 1 certs · `h691/elec | solid brick | roof room(s) | RR,ext2,sec:room heaters,whw903`
### C051 · Σ|Δ| 2.4 · 1 certs · `h691/elec | solid brick | roof room(s) | RR,ext2,sec:room heaters,whw903`
- [ ] `100062174575` · L 46 / E 48.40 / Δ +2.40
### C053 · Σ|Δ| 2.3 · 1 certs · `hpcdb15753/gas | solid brick | pitched 150 | RR`
### C052 · Σ|Δ| 2.3 · 1 certs · `hpcdb15753/gas | solid brick | pitched 150 | RR`
- [ ] `100000034023` · L 73 / E 70.71 / Δ -2.29
### C054 · Σ|Δ| 2.2 · 1 certs · `h506/gas | cavity wall | flat limited | sec:room heaters`
### C053 · Σ|Δ| 2.2 · 1 certs · `h506/gas | cavity wall | flat limited | sec:room heaters`
- [ ] `100020377671` · L 62 / E 64.23 / Δ +2.23
### C055 · Σ|Δ| 2.2 · 1 certs · `h104/gas | solid brick | pitched insulated | RR,ext1`
### C054 · Σ|Δ| 2.2 · 1 certs · `h104/gas | solid brick | pitched insulated | RR,ext1`
- [ ] `22027335` · L 76 / E 73.77 / Δ -2.23
### C056 · Σ|Δ| 2.1 · 1 certs · `hpcdb18833/gas | solid brick | pitched insulated | RR,ext1,sec:room heaters`
### C055 · Σ|Δ| 2.1 · 1 certs · `hpcdb18833/gas | solid brick | pitched insulated | RR,ext1,sec:room heaters`
- [ ] `48056153` · L 65 / E 62.89 / Δ -2.11
### C057 · Σ|Δ| 2.0 · 1 certs · `hpcdb19008/gas | solid brick | pitched insulated | sec:room heaters`
### C056 · Σ|Δ| 2.0 · 1 certs · `hpcdb19008/gas | solid brick | pitched insulated | sec:room heaters`
- [ ] `100021924710` · L 63 / E 65.00 / Δ +2.00 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C058 · Σ|Δ| 1.9 · 1 certs · `hpcdb18774/gas | solid brick | pitched 100 | RR,ext1`
### C057 · Σ|Δ| 1.9 · 1 certs · `hpcdb18774/gas | solid brick | pitched 100 | RR,ext1`
- [ ] `100031270375` · L 69 / E 70.93 / Δ +1.93 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C059 · Σ|Δ| 1.9 · 1 certs · `hpcdb106417/elec | cavity wall | pitched 250 | RR,ext2,sec:room heaters`
### C058 · Σ|Δ| 1.9 · 1 certs · `hpcdb106417/elec | cavity wall | pitched 250 | RR,ext2,sec:room heaters`
- [ ] `10001118522` · L 80 / E 78.07 / Δ -1.93 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C060 · Σ|Δ| 1.9 · 1 certs · `h192/elec | cavity wall | (another dwelling`
### C059 · Σ|Δ| 1.9 · 1 certs · `h192/elec | cavity wall | (another dwelling`
- [ ] `10010234711` · L 81 / E 79.08 / Δ -1.92 <- electric other (per-cert cost noise)
### C061 · Σ|Δ| 1.9 · 1 certs · `hpcdb18250/gas | solid brick | pitched 100 | consv2,sec:room heaters`
- [ ] `100031768368` · L 61 / E 59.12 / Δ -1.88 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C062 · Σ|Δ| 1.8 · 1 certs · `h301/elec10hr | cavity wall | (another dwelling`
### C060 · Σ|Δ| 1.8 · 1 certs · `h301/elec10hr | cavity wall | (another dwelling`
- [ ] `4510729926` · L 73 / E 71.15 / Δ -1.85 <- electric + fabric/demand mismatch (per-cert; not a coherent cluster)
### C063 · Σ|Δ| 1.8 · 1 certs · `hpcdb17138/fuel28 | granite or | pitched 200 | ext1,sec:room heaters`
### C061 · Σ|Δ| 1.8 · 1 certs · `hpcdb17138/fuel28 | granite or | pitched 200 | ext1,sec:room heaters`
- [ ] `100091329112` · L 49 / E 50.82 / Δ +1.82 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C064 · Σ|Δ| 1.8 · 1 certs · `h691/elec | timber frame | pitched insulated | RR,ext1,sec:portable electric,whw908`
### C062 · Σ|Δ| 1.8 · 1 certs · `h691/elec | timber frame | pitched insulated | RR,ext1,sec:portable electric,whw908`
- [ ] `10011797347` · L 47 / E 48.77 / Δ +1.77 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C065 · Σ|Δ| 1.8 · 1 certs · `hpcdb17570/gas+2mains | solid brick | (another dwelling | RR,ext3,sec:room heaters`
- [ ] `100022668387` · L 68 / E 66.25 / Δ -1.75 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C066 · Σ|Δ| 1.7 · 1 certs · `hpcdb10354/gas+2mains | sandstone as | pitched 300 | ext1`
### C063 · Σ|Δ| 1.7 · 1 certs · `hpcdb10354/gas+2mains | sandstone as | pitched 300 | ext1`
- [ ] `100100892216` · L 69 / E 67.28 / Δ -1.72 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C067 · Σ|Δ| 1.7 · 1 certs · `hpcdb8119/gas | cavity wall | pitched insulated | RR,ext1`
### C064 · Σ|Δ| 1.7 · 1 certs · `hpcdb8119/gas | cavity wall | pitched insulated | RR,ext1`
- [ ] `100060878876` · L 49 / E 50.71 / Δ +1.71 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C068 · Σ|Δ| 1.7 · 1 certs · `h402/elec | solid brick | (another dwelling | sec:portable electric,whw903`
### C065 · Σ|Δ| 1.7 · 1 certs · `h402/elec | solid brick | (another dwelling | sec:portable electric,whw903`
- [ ] `12126160` · L 69 / E 70.71 / Δ +1.71 <- electric + fabric/demand mismatch (per-cert; not a coherent cluster)
### C069 · Σ|Δ| 1.7 · 1 certs · `h401/elec | solid brick | (another dwelling | sec:portable electric,whw950`
### C066 · Σ|Δ| 1.7 · 1 certs · `h401/elec | solid brick | (another dwelling | sec:portable electric,whw950`
- [ ] `22032926` · L 61 / E 62.71 / Δ +1.71 <- electric + fabric/demand mismatch (per-cert; not a coherent cluster)
### C070 · Σ|Δ| 1.7 · 1 certs · `hpcdb18560/gas | cavity wall | pitched insulated | RR,ext1`
### C067 · Σ|Δ| 1.7 · 1 certs · `hpcdb18560/gas | cavity wall | pitched insulated | RR,ext1`
- [ ] `100010942905` · L 72 / E 73.70 / Δ +1.70 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C071 · Σ|Δ| 1.6 · 1 certs · `hpcdb18119/gas | solid brick | pitched 100 | ext1`
- [ ] `100030307976` · L 71 / E 69.37 / Δ -1.63 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C068 · Σ|Δ| 1.7 · 1 certs · `hpcdb18498/gas | solid brick | pitched 250 | RR,ext2`
- [ ] `10011740578` · L 70 / E 68.30 / Δ -1.70
### C072 · Σ|Δ| 1.6 · 1 certs · `hpcdb18909/gas | cavity wall | pitched 100 | whw903`
### C069 · Σ|Δ| 1.6 · 1 certs · `hpcdb18909/gas | cavity wall | pitched 100 | whw903`
- [ ] `100061404488` · L 70 / E 68.38 / Δ -1.62 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C073 · Σ|Δ| 1.6 · 1 certs · `h402/elec | system built | (another dwelling | sec:room heaters,whw903`
### C070 · Σ|Δ| 1.6 · 1 certs · `h402/elec | system built | (another dwelling | sec:room heaters,whw903`
- [ ] `21067296` · L 67 / E 68.60 / Δ +1.60 <- electric + fabric/demand mismatch (per-cert; not a coherent cluster)
### C074 · Σ|Δ| 1.6 · 1 certs · `hpcdb10164/gas | solid brick | pitched insulated | sec:room heaters`
### C071 · Σ|Δ| 1.6 · 1 certs · `hpcdb10164/gas | solid brick | pitched insulated | sec:room heaters`
- [ ] `5870047440` · L 58 / E 59.58 / Δ +1.58 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C075 · Σ|Δ| 1.6 · 1 certs · `hpcdb8051/gas | cavity wall | (same dwelling | ext2`
### C072 · Σ|Δ| 1.6 · 1 certs · `hpcdb8051/gas | cavity wall | (same dwelling | ext2`
- [ ] `100070450818` · L 65 / E 63.43 / Δ -1.57 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C076 · Σ|Δ| 1.6 · 1 certs · `hpcdb17741/gas | cavity wall | pitched 225`
### C073 · Σ|Δ| 1.6 · 1 certs · `hpcdb17741/gas | cavity wall | pitched 225`
- [ ] `100050881708` · L 70 / E 71.55 / Δ +1.55 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C077 · Σ|Δ| 1.5 · 1 certs · `h301/elec | system built | (another dwelling | whw950`
### C074 · Σ|Δ| 1.5 · 1 certs · `h301/elec | system built | (another dwelling | whw950`
- [ ] `10010254955` · L 79 / E 77.46 / Δ -1.54 <- electric other (per-cert cost noise)
### C078 · Σ|Δ| 1.5 · 1 certs · `hpcdb16839/gas | cavity wall | pitched 400+ | ext1,mv2,sec:room heaters`
### C075 · Σ|Δ| 1.5 · 1 certs · `hpcdb16839/gas | cavity wall | pitched 400+ | ext1,mv2,sec:room heaters`
- [ ] `3455011834` · L 69 / E 67.48 / Δ -1.52 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C079 · Σ|Δ| 1.5 · 1 certs · `hpcdb15502/gas | granite or | pitched 150 | RR,ext1,sec:room heaters`
### C076 · Σ|Δ| 1.5 · 1 certs · `hpcdb15502/gas | granite or | pitched 150 | RR,ext1,sec:room heaters`
- [ ] `100100274789` · L 60 / E 61.51 / Δ +1.51 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C080 · Σ|Δ| 1.5 · 1 certs · `hpcdb16453/gas | cavity wall | pitched 300 | consv2,ext1`
### C077 · Σ|Δ| 1.5 · 1 certs · `hpcdb16453/gas | cavity wall | pitched 300 | consv2,ext1`
- [ ] `100050852755` · L 65 / E 66.49 / Δ +1.49 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C081 · Σ|Δ| 1.5 · 1 certs · `hpcdb17679/gas | solid brick | pitched 125 | ext1`
- [ ] `100021326422` · L 70 / E 68.51 / Δ -1.49 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C082 · Σ|Δ| 1.5 · 1 certs · `hpcdb15029/gas | solid brick | pitched insulated | RR,ext1`
### C078 · Σ|Δ| 1.5 · 1 certs · `hpcdb15029/gas | solid brick | pitched insulated | RR,ext1`
- [ ] `100050988345` · L 61 / E 59.51 / Δ -1.49 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C083 · Σ|Δ| 1.5 · 1 certs · `hpcdb17998/gas | solid brick | pitched insulated | RR,consv2`
### C079 · Σ|Δ| 1.5 · 1 certs · `hpcdb17998/gas | solid brick | pitched insulated | RR,consv2`
- [ ] `100030285758` · L 54 / E 55.48 / Δ +1.48 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C084 · Σ|Δ| 1.4 · 1 certs · `hpcdb17978/gas | solid brick | flat no | RR,ext1`
### C080 · Σ|Δ| 1.4 · 1 certs · `hpcdb17978/gas | solid brick | flat no | RR,ext1`
- [ ] `line80` · L 72 / E 70.56 / Δ -1.44
### C085 · Σ|Δ| 1.4 · 1 certs · `hpcdb10356/gas | solid brick | pitched 150 | RR,ext1,sec:room heaters`
### C081 · Σ|Δ| 1.4 · 1 certs · `hpcdb10356/gas | solid brick | pitched 150 | RR,ext1,sec:room heaters`
- [ ] `100080518673` · L 57 / E 58.42 / Δ +1.42 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C086 · Σ|Δ| 1.4 · 1 certs · `hpcdb18561/gas | solid brick | pitched 250 | ext2`
### C082 · Σ|Δ| 1.4 · 1 certs · `hpcdb18561/gas | solid brick | pitched 250 | ext2`
- [ ] `235025284` · L 69 / E 67.59 / Δ -1.41 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C087 · Σ|Δ| 1.4 · 1 certs · `hpcdb18767/gas | sandstone as | (another dwelling`
### C083 · Σ|Δ| 1.4 · 1 certs · `hpcdb18767/gas | sandstone as | (another dwelling`
- [ ] `250063088` · L 73 / E 71.60 / Δ -1.40 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C088 · Σ|Δ| 1.4 · 1 certs · `hpcdb16210/gas | cavity wall | pitched 300 | sec:room heaters`
### C084 · Σ|Δ| 1.4 · 1 certs · `hpcdb16210/gas | cavity wall | pitched 300 | sec:room heaters`
- [ ] `100110097281` · L 64 / E 65.37 / Δ +1.37 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C089 · Σ|Δ| 1.4 · 1 certs · `h691/elec | solid brick | (another dwelling | whw903`
### C085 · Σ|Δ| 1.4 · 1 certs · `h691/elec | solid brick | (another dwelling | whw903`
- [ ] `10091630692` · L 58 / E 56.63 / Δ -1.37 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C090 · Σ|Δ| 1.3 · 1 certs · `hpcdb19227/fuel28 | solid brick | pitched 100 | RR,ext3,sec:room heaters`
### C086 · Σ|Δ| 1.3 · 1 certs · `hpcdb19227/fuel28 | solid brick | pitched 100 | RR,ext3,sec:room heaters`
- [ ] `100012368622` · L 39 / E 40.32 / Δ +1.32 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C091 · Σ|Δ| 1.3 · 1 certs · `hpcdb19007/gas | solid brick | pitched insulated | RR,ext2,sec:room heaters`
### C087 · Σ|Δ| 1.3 · 1 certs · `hpcdb19007/gas | solid brick | pitched insulated | RR,ext2,sec:room heaters`
- [ ] `90077194` · L 65 / E 63.69 / Δ -1.31 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C092 · Σ|Δ| 1.3 · 1 certs · `hpcdb19083/gas | cavity wall | pitched 75 | RR,consv2`
### C088 · Σ|Δ| 1.3 · 1 certs · `hpcdb19083/gas | cavity wall | pitched 75 | RR,consv2`
- [ ] `100031461986` · L 70 / E 71.29 / Δ +1.29 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C093 · Σ|Δ| 1.3 · 1 certs · `h691/elec | granite or | pitched insulated | PV,ext3,whw903`
### C089 · Σ|Δ| 1.3 · 1 certs · `h691/elec | granite or | pitched insulated | PV,ext3,whw903`
- [ ] `30095054` · L 46 / E 44.74 / Δ -1.26 <- non-electric cost (secondary fuel/cylinder/tariff); spec-faithful vs lodged
### C094 · Σ|Δ| 1.3 · 1 certs · `hpcdb18335/gas | cavity wall | pitched 100 | RR,ext1,sec:room heaters`
### C090 · Σ|Δ| 1.3 · 1 certs · `hpcdb18335/gas | cavity wall | pitched 100 | RR,ext1,sec:room heaters`
- [ ] `100100055589` · L 58 / E 59.26 / Δ +1.26 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C095 · Σ|Δ| 1.2 · 1 certs · `h409/elec | solid brick | (another dwelling | consv2,sec:portable electric,whw909`
### C091 · Σ|Δ| 1.2 · 1 certs · `h409/elec | solid brick | (another dwelling | consv2,sec:portable electric,whw909`
- [ ] `74061136` · L 73 / E 71.75 / Δ -1.25 <- electric storage cat7 E7: over-rate = off-peak rate/fraction too generous; needs Elmhurst tariff arbitration
### C096 · Σ|Δ| 1.2 · 1 certs · `hpcdb15101/gas | cavity wall | pitched 270 | consv3,sec:room heaters`
### C092 · Σ|Δ| 1.2 · 1 certs · `hpcdb15101/gas | cavity wall | pitched 270 | consv3,sec:room heaters`
- [ ] `100091435353` · L 65 / E 66.23 / Δ +1.23 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C097 · Σ|Δ| 1.2 · 2 certs · `h301/coal-anth | system built | (another dwelling`
### C093 · Σ|Δ| 1.2 · 2 certs · `h301/coal-anth | system built | (another dwelling`
- [ ] `10091070164` · L 69 / E 69.60 / Δ +0.60
- [ ] `10090927185` · L 79 / E 79.60 / Δ +0.60
### C098 · Σ|Δ| 1.2 · 1 certs · `hpcdb19110/gas | cavity wall | pitched no | ext3`
### C094 · Σ|Δ| 1.2 · 1 certs · `hpcdb19110/gas | cavity wall | pitched no | ext3`
- [ ] `100100614412` · L 53 / E 51.81 / Δ -1.19 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C099 · Σ|Δ| 1.2 · 1 certs · `hpcdb18658/gas | solid brick | flat no`
- [ ] `217091901` · L 62 / E 60.82 / Δ -1.18 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C100 · Σ|Δ| 1.2 · 1 certs · `hpcdb17115/gas | sandstone as | (another dwelling | ext2,sec:room heaters`
### C095 · Σ|Δ| 1.2 · 1 certs · `hpcdb17115/gas | sandstone as | (another dwelling | ext2,sec:room heaters`
- [x] `line768` · L 57 / E 55.84 / Δ -1.16
### C101 · Σ|Δ| 1.2 · 1 certs · `h102/gas | solid brick | (same dwelling | ext1,sec:portable electric`
### C096 · Σ|Δ| 1.2 · 1 certs · `h102/gas | solid brick | (same dwelling | ext1,sec:portable electric`
- [ ] `217061895` · L 61 / E 59.84 / Δ -1.16 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C102 · Σ|Δ| 1.1 · 1 certs · `hpcdb15708/gas | cavity wall | pitched insulated | RR,sec:room heaters`
### C097 · Σ|Δ| 1.1 · 1 certs · `hpcdb15708/gas | cavity wall | pitched insulated | RR,sec:room heaters`
- [ ] `10002349227` · L 76 / E 77.14 / Δ +1.14 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C103 · Σ|Δ| 1.1 · 1 certs · `hpcdb15451/gas | timber frame | pitched insulated | RR,ext1,sec:room heaters`
### C098 · Σ|Δ| 1.1 · 1 certs · `hpcdb18119/gas | solid brick | pitched 100 | ext1`
- [ ] `100030307976` · L 71 / E 69.86 / Δ -1.14 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C099 · Σ|Δ| 1.1 · 1 certs · `hpcdb15451/gas | timber frame | pitched insulated | RR,ext1,sec:room heaters`
- [ ] `200001634005` · L 66 / E 67.11 / Δ +1.11 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C104 · Σ|Δ| 1.1 · 1 certs · `hpcdb16407/gas | cavity wall | pitched 250 | ext1,sec:room heaters`
### C100 · Σ|Δ| 1.1 · 1 certs · `hpcdb16407/gas | cavity wall | pitched 250 | ext1,sec:room heaters`
- [ ] `100071059081` · L 63 / E 64.09 / Δ +1.09 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C105 · Σ|Δ| 1.1 · 1 certs · `hpcdb17973/gas | solid brick | (another dwelling`
### C101 · Σ|Δ| 1.1 · 1 certs · `hpcdb17973/gas | solid brick | (another dwelling`
- [ ] `5117280` · L 76 / E 77.08 / Δ +1.08 <- 🔧 thin-internal-insulation bucket-0 fix: present insulation + known <25mm thickness was mapped to uninsulated (U 1.7); now routes to 50mm row like unknown-thickness. Δ-4.44 -> +1.08.
### C106 · Σ|Δ| 1.1 · 1 certs · `hpcdb19007/gas+2mains | cavity wall | pitched 350 | ext2,mv5,sec:room heaters,whw914`
### C102 · Σ|Δ| 1.1 · 1 certs · `hpcdb19007/gas+2mains | cavity wall | pitched 350 | ext2,mv5,sec:room heaters,whw914`
- [ ] `100011820199` · L 55 / E 56.06 / Δ +1.06 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C107 · Σ|Δ| 1.1 · 1 certs · `h691/elec | system built | (another dwelling | whw903`
### C103 · Σ|Δ| 1.1 · 1 certs · `h691/elec | system built | (another dwelling | whw903`
- [ ] `90214753` · L 83 / E 81.95 / Δ -1.05 <- non-electric cost (secondary fuel/cylinder/tariff); spec-faithful vs lodged
### C108 · Σ|Δ| 1.0 · 1 certs · `h102/gas | cavity wall | pitched insulated | consv2`
### C104 · Σ|Δ| 1.1 · 1 certs · `hpcdb17570/gas+2mains | solid brick | (another dwelling | RR,ext3,sec:room heaters`
- [ ] `100022668387` · L 68 / E 66.95 / Δ -1.05 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C105 · Σ|Δ| 1.0 · 1 certs · `h102/gas | cavity wall | pitched insulated | consv2`
- [ ] `100051051866` · L 70 / E 71.04 / Δ +1.04 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C109 · Σ|Δ| 1.0 · 1 certs · `hpcdb18737/gas | cavity wall | pitched insulated | PV,sec:room heaters`
### C106 · Σ|Δ| 1.0 · 1 certs · `hpcdb18737/gas | cavity wall | pitched insulated | PV,sec:room heaters`
- [ ] `10013836852` · L 100 / E 98.97 / Δ -1.03 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C110 · Σ|Δ| 1.0 · 1 certs · `hpcdb17985/gas | solid brick | (another dwelling`
### C107 · Σ|Δ| 1.0 · 1 certs · `hpcdb17985/gas | solid brick | (another dwelling`
- [ ] `line899` · L 73 / E 71.97 / Δ -1.03
### C111 · Σ|Δ| 1.0 · 1 certs · `hpcdb18524/gas+2mains | cavity wall | pitched 400+ | ext4`
### C108 · Σ|Δ| 1.0 · 1 certs · `hpcdb18524/gas+2mains | cavity wall | pitched 400+ | ext4`
- [ ] `40037847` · L 81 / E 79.98 / Δ -1.02 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C112 · Σ|Δ| 1.0 · 1 certs · `hpcdb19012/gas | cavity wall | pitched insulated`
### C109 · Σ|Δ| 1.0 · 1 certs · `hpcdb19012/gas | cavity wall | pitched insulated`
- [ ] `10070056075` · L 80 / E 78.99 / Δ -1.01 <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
### C113 · Σ|Δ| 1.0 · 1 certs · `hpcdb16843/gas+2mains | cavity wall | pitched insulated | PV,RR,ext2`
### C110 · Σ|Δ| 1.0 · 1 certs · `hpcdb16843/gas+2mains | cavity wall | pitched insulated | PV,RR,ext2`
- [ ] `200131078` · L 86 / E 85.00 / Δ -1.00 <- room-in-roof surface geometry (spec-mandated, worksheet-validated; see PRs #1440/#1441)
### C114 · Σ|Δ| 1.0 · 1 certs · `hpcdb17489/gas | cavity wall | pitched 270 | PV,consv2,ext1,sec:room heaters`
### C111 · Σ|Δ| 1.0 · 1 certs · `hpcdb17489/gas | cavity wall | pitched 270 | PV,consv2,ext1,sec:room heaters`
- [ ] `10000245212` · L 101 / E 100.00 / Δ -1.00 <- non-electric cost (secondary fuel/cylinder/tariff); spec-faithful vs lodged
### C115 · Σ|Δ| 1.0 · 1 certs · `h191/elec | solid brick | (another dwelling | ext2,whw909`
- [ ] `15129162` · L 62 / E 61.00 / Δ -1.00
### C116 · Σ|Δ| 1.0 · 1 certs · `h301/coal-anth | cavity wall | pitched 270`
### C112 · Σ|Δ| 1.0 · 1 certs · `h301/coal-anth | cavity wall | pitched 270`
- [ ] `23032873` · L 73 / E 72.00 / Δ -1.00
### C117 · Σ|Δ| 1.0 · 1 certs · `hpcdb16930/gas | cavity wall | pitched unknown | RR,ext1,sec:room heaters`
### C113 · Σ|Δ| 1.0 · 1 certs · `hpcdb16930/gas | cavity wall | pitched unknown | RR,ext1,sec:room heaters`
- [ ] `45019020` · L 62 / E 61.01 / Δ -0.99
### C118 · Σ|Δ| 1.0 · 1 certs · `hpcdb17939/gas | solid brick | (another dwelling`
### C114 · Σ|Δ| 1.0 · 1 certs · `hpcdb17939/gas | solid brick | (another dwelling`
- [ ] `42065028` · L 59 / E 58.02 / Δ -0.98
### C119 · Σ|Δ| 1.0 · 1 certs · `hpcdb10071/gas | cavity wall | pitched 150 | RR`
### C115 · Σ|Δ| 1.0 · 1 certs · `hpcdb10071/gas | cavity wall | pitched 150 | RR`
- [ ] `10032928147` · L 77 / E 76.04 / Δ -0.96
### C120 · Σ|Δ| 1.0 · 1 certs · `hpcdb17045/gas | cavity wall | pitched 300`
### C116 · Σ|Δ| 1.0 · 1 certs · `hpcdb17045/gas | cavity wall | pitched 300`
- [ ] `10010678937` · L 78 / E 77.04 / Δ -0.96
### C121 · Σ|Δ| 1.0 · 1 certs · `hpcdb15247/gas | cavity wall | pitched insulated | RR`
### C117 · Σ|Δ| 1.0 · 1 certs · `hpcdb15247/gas | cavity wall | pitched insulated | RR`
- [ ] `100011010151` · L 62 / E 62.96 / Δ +0.96
### C122 · Σ|Δ| 1.0 · 1 certs · `hpcdb16406/gas | solid brick | pitched insulated | RR,consv2,ext2,sec:room heaters`
- [ ] `100050477371` · L 65 / E 64.04 / Δ -0.96
### C123 · Σ|Δ| 0.9 · 1 certs · `hpcdb17529/fuel28 | cavity wall | pitched insulated | RR,ext2,sec:room heaters`
### C118 · Σ|Δ| 0.9 · 1 certs · `hpcdb17529/fuel28 | cavity wall | pitched insulated | RR,ext2,sec:room heaters`
- [ ] `10002945522` · L 61 / E 61.94 / Δ +0.94
### C124 · Σ|Δ| 0.9 · 1 certs · `h691/elec+2mains | cob as | pitched 300 | whw903`
### C119 · Σ|Δ| 0.9 · 1 certs · `h691/elec+2mains | cob as | pitched 300 | whw903`
- [ ] `10032957680` · L 45 / E 45.94 / Δ +0.94
### C125 · Σ|Δ| 0.9 · 1 certs · `hpcdb18204/gas | solid brick | pitched 400+ | PV,ext2`
### C120 · Σ|Δ| 0.9 · 1 certs · `hpcdb18204/gas | solid brick | pitched 400+ | PV,ext2`
- [ ] `2465057465` · L 81 / E 81.93 / Δ +0.93
### C126 · Σ|Δ| 0.9 · 1 certs · `hpcdb19019/gas | solid brick | pitched 100 | ext3,sec:room heaters`
### C121 · Σ|Δ| 0.9 · 1 certs · `hpcdb19019/gas | solid brick | pitched 100 | ext3,sec:room heaters`
- [ ] `100090616327` · L 63 / E 63.93 / Δ +0.93
### C127 · Σ|Δ| 0.9 · 1 certs · `hpcdb17612/gas | cavity wall | pitched 270 | ext1`
### C122 · Σ|Δ| 0.9 · 1 certs · `hpcdb17612/gas | cavity wall | pitched 270 | ext1`
- [ ] `100012753833` · L 74 / E 73.07 / Δ -0.93
### C128 · Σ|Δ| 0.9 · 1 certs · `hpcdb19006/gas | solid brick | pitched 75 | ext1,sec:room heaters`
### C123 · Σ|Δ| 0.9 · 1 certs · `hpcdb19006/gas | solid brick | pitched 75 | ext1,sec:room heaters`
- [ ] `100031269525` · L 53 / E 53.91 / Δ +0.91
### C129 · Σ|Δ| 0.9 · 1 certs · `hpcdb17964/gas | cavity wall | pitched insulated | consv3`
### C124 · Σ|Δ| 0.9 · 1 certs · `hpcdb17964/gas | cavity wall | pitched insulated | consv3`
- [ ] `200003725358` · L 75 / E 74.09 / Δ -0.91
### C130 · Σ|Δ| 0.9 · 1 certs · `hpcdb17507/gas | solid brick | pitched insulated | RR,ext3`
### C125 · Σ|Δ| 0.9 · 1 certs · `hpcdb17507/gas | solid brick | pitched insulated | RR,ext3`
- [ ] `100050986162` · L 62 / E 61.10 / Δ -0.90
### C131 · Σ|Δ| 0.9 · 1 certs · `hpcdb17985/gas | cavity wall | (another dwelling | mv2`
### C126 · Σ|Δ| 0.9 · 1 certs · `hpcdb17985/gas | cavity wall | (another dwelling | mv2`
- [ ] `5300000141` · L 78 / E 77.10 / Δ -0.90
### C132 · Σ|Δ| 0.9 · 1 certs · `hpcdb18616/gas | cavity wall | pitched 250 | sec:room heaters`
### C127 · Σ|Δ| 0.9 · 1 certs · `hpcdb18616/gas | cavity wall | pitched 250 | sec:room heaters`
- [ ] `100050618404` · L 73 / E 73.89 / Δ +0.89
### C133 · Σ|Δ| 0.9 · 1 certs · `hpcdb17507/gas | cavity wall | pitched 250 | PV,ext1`
### C128 · Σ|Δ| 0.9 · 1 certs · `hpcdb17507/gas | cavity wall | pitched 250 | PV,ext1`
- [ ] `100090476016` · L 81 / E 81.89 / Δ +0.89
### C134 · Σ|Δ| 0.9 · 1 certs · `h691/elec | timber frame | pitched 200 | RR,ext3,whw903`
### C129 · Σ|Δ| 0.9 · 1 certs · `h691/elec | timber frame | pitched 200 | RR,ext3,whw903`
- [ ] `10012119141` · L 45 / E 45.89 / Δ +0.89
### C135 · Σ|Δ| 0.9 · 1 certs · `h524/elec | cavity wall | pitched 300 | PV`
### C130 · Σ|Δ| 0.9 · 1 certs · `h524/elec | cavity wall | pitched 300 | PV`
- [ ] `100040938206` · L 75 / E 75.89 / Δ +0.89
### C136 · Σ|Δ| 0.9 · 1 certs · `hpcdb18234/gas | cavity wall | pitched insulated | RR,consv2`
### C131 · Σ|Δ| 0.9 · 1 certs · `hpcdb18234/gas | cavity wall | pitched insulated | RR,consv2`
- [ ] `38188474` · L 76 / E 76.87 / Δ +0.87
### C137 · Σ|Δ| 0.9 · 1 certs · `hpcdb16777/gas | cavity wall | pitched 100 | ext1,sec:room heaters`
### C132 · Σ|Δ| 0.9 · 1 certs · `hpcdb16777/gas | cavity wall | pitched 100 | ext1,sec:room heaters`
- [ ] `100061194957` · L 69 / E 69.87 / Δ +0.87
### C138 · Σ|Δ| 0.9 · 1 certs · `hpcdb18559/gas+2mains | solid brick | pitched 200 | whw914`
### C133 · Σ|Δ| 0.9 · 1 certs · `hpcdb18559/gas+2mains | solid brick | pitched 200 | whw914`
- [ ] `100021236527` · L 72 / E 71.13 / Δ -0.87
### C139 · Σ|Δ| 0.9 · 1 certs · `hpcdb18907/gas | cavity wall | pitched 270 | consv2`
### C134 · Σ|Δ| 0.9 · 1 certs · `hpcdb18907/gas | cavity wall | pitched 270 | consv2`
- [ ] `100040193752` · L 71 / E 71.86 / Δ +0.86
### C140 · Σ|Δ| 0.9 · 1 certs · `hpcdb10327/gas | cavity wall | (another dwelling`
### C135 · Σ|Δ| 0.9 · 1 certs · `hpcdb10327/gas | cavity wall | (another dwelling`
- [ ] `100060701552` · L 74 / E 74.86 / Δ +0.86
### C141 · Σ|Δ| 0.9 · 1 certs · `h691/elec | system built | flat insulated | whw903`
### C136 · Σ|Δ| 0.9 · 1 certs · `h691/elec | system built | flat insulated | whw903`
- [ ] `10070396146` · L 61 / E 60.14 / Δ -0.86
### C142 · Σ|Δ| 0.8 · 1 certs · `hpcdb8587/gas | cavity wall | (another dwelling`
### C137 · Σ|Δ| 0.8 · 1 certs · `hpcdb8587/gas | cavity wall | (another dwelling`
- [ ] `100030528930` · L 74 / E 73.16 / Δ -0.84
### C143 · Σ|Δ| 0.8 · 1 certs · `hpcdb18212/gas | solid brick | (another dwelling`
### C138 · Σ|Δ| 0.8 · 1 certs · `hpcdb18212/gas | solid brick | (another dwelling`
- [ ] `100020982570` · L 72 / E 71.17 / Δ -0.83
### C144 · Σ|Δ| 0.8 · 1 certs · `hpcdb16888/gas | cavity wall | pitched 350 | consv2`
### C139 · Σ|Δ| 0.8 · 1 certs · `h191/elec | solid brick | (another dwelling | ext2,whw909`
- [ ] `15129162` · L 62 / E 61.17 / Δ -0.83
### C140 · Σ|Δ| 0.8 · 1 certs · `hpcdb16888/gas | cavity wall | pitched 350 | consv2`
- [ ] `394024487` · L 75 / E 74.17 / Δ -0.83
### C145 · Σ|Δ| 0.8 · 1 certs · `hpcdb17503/gas | solid brick | pitched insulated | RR,ext1,mv4`
### C141 · Σ|Δ| 0.8 · 1 certs · `hpcdb17503/gas | solid brick | pitched insulated | RR,ext1,mv4`
- [ ] `217070698` · L 78 / E 78.83 / Δ +0.83
### C146 · Σ|Δ| 0.8 · 1 certs · `hpcdb15709/gas | cavity wall | (another dwelling | ext1`
### C142 · Σ|Δ| 0.8 · 1 certs · `hpcdb15709/gas | cavity wall | (another dwelling | ext1`
- [ ] `77022508` · L 69 / E 68.18 / Δ -0.82
### C147 · Σ|Δ| 0.8 · 1 certs · `hpcdb18496/gas | cavity wall | pitched 250 | RR,consv4`
### C143 · Σ|Δ| 0.8 · 1 certs · `hpcdb18496/gas | cavity wall | pitched 250 | RR,consv4`
- [ ] `100040259104` · L 68 / E 68.82 / Δ +0.82
### C148 · Σ|Δ| 0.8 · 1 certs · `hpcdb18559/gas | solid brick | pitched 270 | ext1`
### C144 · Σ|Δ| 0.8 · 1 certs · `hpcdb18559/gas | solid brick | pitched 270 | ext1`
- [ ] `100031399026` · L 70 / E 70.81 / Δ +0.81
### C149 · Σ|Δ| 0.8 · 1 certs · `hpcdb16374/gas | cavity wall | pitched unknown`
### C145 · Σ|Δ| 0.8 · 1 certs · `hpcdb16374/gas | cavity wall | pitched unknown`
- [ ] `10009645698` · L 67 / E 67.81 / Δ +0.81
### C150 · Σ|Δ| 0.8 · 1 certs · `hpcdb16395/gas | timber frame | thatched with | ext1,sec:room heaters`
### C146 · Σ|Δ| 0.8 · 1 certs · `hpcdb16395/gas | timber frame | thatched with | ext1,sec:room heaters`
- [ ] `100080818887` · L 56 / E 55.19 / Δ -0.81
### C151 · Σ|Δ| 0.8 · 1 certs · `hpcdb16835/gas | solid brick | pitched insulated | RR,ext1`
### C147 · Σ|Δ| 0.8 · 1 certs · `hpcdb16835/gas | solid brick | pitched insulated | RR,ext1`
- [ ] `100012374381` · L 69 / E 69.80 / Δ +0.80
### C152 · Σ|Δ| 0.8 · 1 certs · `hpcdb17560/gas | cavity wall | pitched 250 | ext2`
### C148 · Σ|Δ| 0.8 · 1 certs · `hpcdb17560/gas | cavity wall | pitched 250 | ext2`
- [ ] `100012350270` · L 78 / E 77.20 / Δ -0.80
### C153 · Σ|Δ| 0.8 · 1 certs · `hpcdb15166/gas+2mains | sandstone as | pitched no | PV,ext4,sec:room heaters,whw914`
### C149 · Σ|Δ| 0.8 · 1 certs · `hpcdb15166/gas+2mains | sandstone as | pitched no | PV,ext4,sec:room heaters,whw914`
- [ ] `100051109723` · L 60 / E 60.78 / Δ +0.78
### C154 · Σ|Δ| 0.8 · 1 certs · `hpcdb18911/gas | cavity wall | pitched 50 | consv4`
### C150 · Σ|Δ| 0.8 · 1 certs · `hpcdb18911/gas | cavity wall | pitched 50 | consv4`
- [ ] `100061616915` · L 63 / E 62.22 / Δ -0.78
### C155 · Σ|Δ| 0.8 · 1 certs · `h691/elec | cavity wall | pitched 200 | whw903`
### C151 · Σ|Δ| 0.8 · 1 certs · `h691/elec | cavity wall | pitched 200 | whw903`
- [ ] `line71` · L 74 / E 74.77 / Δ +0.77
### C156 · Σ|Δ| 0.8 · 1 certs · `hpcdb17986/gas | cavity wall | (another dwelling`
### C152 · Σ|Δ| 0.8 · 1 certs · `hpcdb17986/gas | cavity wall | (another dwelling`
- [ ] `100011603934` · L 72 / E 71.23 / Δ -0.77
### C157 · Σ|Δ| 0.8 · 1 certs · `hpcdb10244/gas | cavity wall | pitched 100`
### C153 · Σ|Δ| 0.8 · 1 certs · `hpcdb10244/gas | cavity wall | pitched 100`
- [ ] `24030987` · L 73 / E 73.76 / Δ +0.76
### C158 · Σ|Δ| 0.8 · 1 certs · `h193/elec | solid brick | (another dwelling | whw909`
### C154 · Σ|Δ| 0.8 · 1 certs · `h193/elec | solid brick | (another dwelling | whw909`
- [ ] `100032043038` · L 74 / E 73.24 / Δ -0.76
### C159 · Σ|Δ| 0.8 · 1 certs · `hpcdb17513/gas | cavity wall | pitched unknown`
### C155 · Σ|Δ| 0.8 · 1 certs · `hpcdb17513/gas | cavity wall | pitched unknown`
- [ ] `100010866608` · L 71 / E 70.24 / Δ -0.76
### C160 · Σ|Δ| 0.7 · 1 certs · `hpcdb17965/gas | cavity wall | pitched insulated | RR,ext1,sec:room heaters`
### C156 · Σ|Δ| 0.7 · 1 certs · `hpcdb17965/gas | cavity wall | pitched insulated | RR,ext1,sec:room heaters`
- [ ] `100090399615` · L 67 / E 67.75 / Δ +0.75
### C161 · Σ|Δ| 0.7 · 1 certs · `hpcdb16137/gas | solid brick | pitched 150`
### C157 · Σ|Δ| 0.7 · 1 certs · `hpcdb16137/gas | solid brick | pitched 150`
- [ ] `200004296092` · L 71 / E 71.74 / Δ +0.74
### C162 · Σ|Δ| 0.7 · 1 certs · `hpcdb17576/gas | cavity wall | pitched insulated | RR,consv2`
### C158 · Σ|Δ| 0.7 · 1 certs · `hpcdb17576/gas | cavity wall | pitched insulated | RR,consv2`
- [ ] `10012341135` · L 79 / E 79.73 / Δ +0.73
### C163 · Σ|Δ| 0.7 · 1 certs · `h409/elec | cavity wall | (another dwelling | sec:portable electric,whw903`
### C159 · Σ|Δ| 0.7 · 1 certs · `h409/elec | cavity wall | (another dwelling | sec:portable electric,whw903`
- [ ] `100091005853` · L 70 / E 69.27 / Δ -0.73
### C164 · Σ|Δ| 0.7 · 1 certs · `hpcdb18908/gas | cavity wall | pitched 175`
### C160 · Σ|Δ| 0.7 · 1 certs · `hpcdb18908/gas | cavity wall | pitched 175`
- [ ] `100021533514` · L 71 / E 71.73 / Δ +0.73
### C165 · Σ|Δ| 0.7 · 1 certs · `h301/coal-anth | solid brick | (another dwelling`
### C161 · Σ|Δ| 0.7 · 1 certs · `h301/coal-anth | solid brick | (another dwelling`
- [ ] `5062075` · L 69 / E 69.72 / Δ +0.72
### C166 · Σ|Δ| 0.7 · 1 certs · `hpcdb18112/gas | cavity wall | pitched 100 | RR,ext1`
### C162 · Σ|Δ| 0.7 · 1 certs · `hpcdb18112/gas | cavity wall | pitched 100 | RR,ext1`
- [ ] `100040813308` · L 62 / E 62.72 / Δ +0.72
### C167 · Σ|Δ| 0.7 · 1 certs · `h104/gas | solid brick | pitched unknown | ext2`
- [ ] `38105600` · L 68 / E 67.28 / Δ -0.72
### C168 · Σ|Δ| 0.7 · 1 certs · `h104/gas | solid brick | pitched 200`
- [ ] `line255` · L 72 / E 71.29 / Δ -0.71
### C169 · Σ|Δ| 0.7 · 1 certs · `h304/elec | cavity wall | (another dwelling | whw950`
### C163 · Σ|Δ| 0.7 · 1 certs · `h304/elec | cavity wall | (another dwelling | whw950`
- [ ] `5300047580` · L 69 / E 68.30 / Δ -0.70
### C170 · Σ|Δ| 0.7 · 1 certs · `h104/gas | cavity wall | pitched 250 | ext1`
### C164 · Σ|Δ| 0.7 · 1 certs · `h104/gas | cavity wall | pitched 250 | ext1`
- [ ] `100110433403` · L 69 / E 68.32 / Δ -0.68
### C171 · Σ|Δ| 0.7 · 1 certs · `hpcdb16835/gas | cavity wall | pitched insulated`
### C165 · Σ|Δ| 0.7 · 1 certs · `hpcdb16835/gas | cavity wall | pitched insulated`
- [ ] `10093416558` · L 79 / E 78.32 / Δ -0.68
### C172 · Σ|Δ| 0.7 · 1 certs · `hpcdb18251/gas | solid brick | pitched 100`
### C166 · Σ|Δ| 0.7 · 1 certs · `hpcdb18251/gas | solid brick | pitched 100`
- [ ] `100050958217` · L 68 / E 68.68 / Δ +0.68
### C173 · Σ|Δ| 0.7 · 1 certs · `hpcdb18224/gas | solid brick | pitched 225 | ext1`
### C167 · Σ|Δ| 0.7 · 1 certs · `hpcdb18224/gas | solid brick | pitched 225 | ext1`
- [ ] `100022667797` · L 68 / E 67.32 / Δ -0.68
### C174 · Σ|Δ| 0.7 · 1 certs · `hpcdb10243/gas | cavity wall | pitched insulated | RR,sec:room heaters`
### C168 · Σ|Δ| 0.7 · 1 certs · `hpcdb10243/gas | cavity wall | pitched insulated | RR,sec:room heaters`
- [ ] `100030528925` · L 78 / E 78.65 / Δ +0.65
### C175 · Σ|Δ| 0.6 · 1 certs · `hpcdb17657/gas | cavity wall | (another dwelling`
### C169 · Σ|Δ| 0.6 · 1 certs · `hpcdb17657/gas | cavity wall | (another dwelling`
- [ ] `100110300037` · L 74 / E 73.35 / Δ -0.65
### C176 · Σ|Δ| 0.6 · 1 certs · `hpcdb2044/gas | solid brick | (another dwelling | ext1`
### C170 · Σ|Δ| 0.6 · 1 certs · `hpcdb2044/gas | solid brick | (another dwelling | ext1`
- [ ] `217092450` · L 52 / E 52.64 / Δ +0.64
### C177 · Σ|Δ| 0.6 · 1 certs · `hpcdb15101/gas | solid brick | pitched 300`
- [ ] `100061164545` · L 72 / E 71.36 / Δ -0.64
### C178 · Σ|Δ| 0.6 · 1 certs · `hpcdb10328/gas | cavity wall | pitched unknown`
### C171 · Σ|Δ| 0.6 · 1 certs · `hpcdb10328/gas | cavity wall | pitched unknown`
- [ ] `100051075936` · L 71 / E 70.36 / Δ -0.64
### C179 · Σ|Δ| 0.6 · 1 certs · `hpcdb17507/gas | cavity wall | roof room(s) | RR,sec:room heaters`
### C172 · Σ|Δ| 0.6 · 1 certs · `hpcdb17507/gas | cavity wall | roof room(s) | RR,sec:room heaters`
- [ ] `100011565830` · L 72 / E 72.64 / Δ +0.64
### C180 · Σ|Δ| 0.6 · 1 certs · `hpcdb15700/gas | sandstone with | pitched 100 | RR,sec:room heaters`
### C173 · Σ|Δ| 0.6 · 1 certs · `hpcdb15700/gas | sandstone with | pitched 100 | RR,sec:room heaters`
- [ ] `61010956` · L 75 / E 74.37 / Δ -0.63
### C181 · Σ|Δ| 0.6 · 1 certs · `hpcdb17115/gas+2mains | cavity wall | pitched insulated | RR,ext4,sec:room heaters`
### C174 · Σ|Δ| 0.6 · 1 certs · `hpcdb17115/gas+2mains | cavity wall | pitched insulated | RR,ext4,sec:room heaters`
- [ ] `100110752514` · L 67 / E 67.62 / Δ +0.62
### C182 · Σ|Δ| 0.6 · 1 certs · `h404/elec | cavity wall | pitched 175 | sec:room heaters,whw909`
### C175 · Σ|Δ| 0.6 · 1 certs · `h404/elec | cavity wall | pitched 175 | sec:room heaters,whw909`
- [ ] `100070295212` · L 75 / E 74.38 / Δ -0.62
### C183 · Σ|Δ| 0.6 · 1 certs · `hpcdb18766/gas+2mains | sandstone as | pitched 100 | RR`
### C176 · Σ|Δ| 0.6 · 1 certs · `hpcdb18766/gas+2mains | sandstone as | pitched 100 | RR`
- [ ] `100012534027` · L 65 / E 64.38 / Δ -0.62
### C184 · Σ|Δ| 0.6 · 1 certs · `hpcdb9058/fuel28 | solid brick | pitched 100 | RR,ext2,sec:room heaters`
### C177 · Σ|Δ| 0.6 · 1 certs · `hpcdb9058/fuel28 | solid brick | pitched 100 | RR,ext2,sec:room heaters`
- [x] `10009812472` · L 40 / E 39.40 / Δ -0.60
### C185 · Σ|Δ| 0.6 · 1 certs · `hpcdb18218/gas | solid brick | (another dwelling`
- [ ] `34080035` · L 77 / E 76.41 / Δ -0.59
### C186 · Σ|Δ| 0.6 · 1 certs · `hpcdb16137/gas | cavity wall | pitched 150 | RR,ext1`
### C178 · Σ|Δ| 0.6 · 1 certs · `hpcdb16137/gas | cavity wall | pitched 150 | RR,ext1`
- [ ] `21032732` · L 69 / E 69.59 / Δ +0.59
### C187 · Σ|Δ| 0.6 · 1 certs · `h101/gas | cavity wall | pitched 100 | RR,ext1`
### C179 · Σ|Δ| 0.6 · 1 certs · `h101/gas | cavity wall | pitched 100 | RR,ext1`
- [ ] `100060562056` · L 66 / E 65.41 / Δ -0.59
### C188 · Σ|Δ| 0.6 · 1 certs · `hpcdb17514/fuel27 | cavity wall | pitched 400+ | PV,ext2`
### C180 · Σ|Δ| 0.6 · 1 certs · `hpcdb17514/fuel27 | cavity wall | pitched 400+ | PV,ext2`
- [ ] `100091086558` · L 79 / E 79.58 / Δ +0.58
### C189 · Σ|Δ| 0.6 · 1 certs · `hpcdb18908/gas | solid brick | (another dwelling`
### C181 · Σ|Δ| 0.6 · 1 certs · `hpcdb18908/gas | solid brick | (another dwelling`
- [ ] `line599` · L 74 / E 73.42 / Δ -0.58
### C190 · Σ|Δ| 0.6 · 1 certs · `hpcdb17973/gas | cavity wall | pitched insulated`
### C182 · Σ|Δ| 0.6 · 1 certs · `hpcdb17973/gas | cavity wall | pitched insulated`
- [ ] `100020407741` · L 72 / E 71.42 / Δ -0.58
### C191 · Σ|Δ| 0.6 · 1 certs · `hpcdb19007/gas | cavity wall | pitched 200 | consv2,ext1,sec:room heaters`
### C183 · Σ|Δ| 0.6 · 1 certs · `hpcdb19007/gas | cavity wall | pitched 200 | consv2,ext1,sec:room heaters`
- [ ] `100050839457` · L 70 / E 70.57 / Δ +0.57
### C192 · Σ|Δ| 0.6 · 1 certs · `hpcdb19007/gas | solid brick | pitched insulated | RR,sec:room heaters`
### C184 · Σ|Δ| 0.6 · 1 certs · `hpcdb19007/gas | solid brick | pitched insulated | RR,sec:room heaters`
- [ ] `42105250` · L 59 / E 59.57 / Δ +0.57
### C193 · Σ|Δ| 0.6 · 1 certs · `hpcdb1917/gas | timber frame | pitched insulated`
### C185 · Σ|Δ| 0.6 · 1 certs · `hpcdb1917/gas | timber frame | pitched insulated`
- [ ] `77200509` · L 69 / E 69.57 / Δ +0.57
### C194 · Σ|Δ| 0.6 · 1 certs · `hpcdb10356/gas | cavity wall | pitched insulated | ext2`
### C186 · Σ|Δ| 0.6 · 1 certs · `hpcdb10356/gas | cavity wall | pitched insulated | ext2`
- [ ] `200040534` · L 65 / E 65.56 / Δ +0.56
### C195 · Σ|Δ| 0.6 · 1 certs · `hpcdb17507/gas | solid brick | pitched 150 | ext1,mv2`
### C187 · Σ|Δ| 0.6 · 1 certs · `hpcdb17507/gas | solid brick | pitched 150 | ext1,mv2`
- [ ] `200049463` · L 69 / E 69.56 / Δ +0.56
### C196 · Σ|Δ| 0.6 · 1 certs · `h126/fuel28+2mains | cavity wall | pitched 300 | RR,consv2,ext3,sec:room heaters`
### C188 · Σ|Δ| 0.6 · 1 certs · `h126/fuel28+2mains | cavity wall | pitched 300 | RR,consv2,ext3,sec:room heaters`
- [ ] `100062527568` · L 61 / E 61.56 / Δ +0.56
### C197 · Σ|Δ| 0.6 · 1 certs · `hpcdb15700/gas | cavity wall | pitched limited | RR,ext1`
### C189 · Σ|Δ| 0.6 · 1 certs · `hpcdb15700/gas | cavity wall | pitched limited | RR,ext1`
- [ ] `100100268900` · L 69 / E 69.56 / Δ +0.56
### C198 · Σ|Δ| 0.6 · 1 certs · `hpcdb17004/fuel27+2mains | sandstone as | pitched no | ext3,sec:room heaters`
### C190 · Σ|Δ| 0.6 · 1 certs · `hpcdb17004/fuel27+2mains | sandstone as | pitched no | ext3,sec:room heaters`
- [x] `200003110548` · L 29 / E 28.45 / Δ -0.55
### C199 · Σ|Δ| 0.5 · 1 certs · `hpcdb9464/gas | cavity wall | pitched 150 | ext1`
### C191 · Σ|Δ| 0.5 · 1 certs · `hpcdb9464/gas | cavity wall | pitched 150 | ext1`
- [ ] `90036914` · L 60 / E 60.55 / Δ +0.55
### C200 · Σ|Δ| 0.5 · 1 certs · `h409/elec | cavity wall | pitched 250 | mv5,sec:room heaters,whw903`
### C192 · Σ|Δ| 0.5 · 1 certs · `h409/elec | cavity wall | pitched 250 | mv5,sec:room heaters,whw903`
- [ ] `10004742671` · L 70 / E 69.46 / Δ -0.54
### C201 · Σ|Δ| 0.5 · 1 certs · `hpcdb18251/gas | cavity wall | roof room(s) | RR`
### C193 · Σ|Δ| 0.5 · 1 certs · `hpcdb18251/gas | cavity wall | roof room(s) | RR`
- [ ] `100010657450` · L 75 / E 75.54 / Δ +0.54
### C202 · Σ|Δ| 0.5 · 1 certs · `hpcdb17507/gas | cavity wall | pitched 150 | ext1`
### C194 · Σ|Δ| 0.5 · 1 certs · `hpcdb17507/gas | cavity wall | pitched 150 | ext1`
- [ ] `100061834344` · L 71 / E 70.46 / Δ -0.54
### C203 · Σ|Δ| 0.5 · 1 certs · `h113/gas | cavity wall | pitched 200`
### C195 · Σ|Δ| 0.5 · 1 certs · `h113/gas | cavity wall | pitched 200`
- [ ] `4510097194` · L 70 / E 70.53 / Δ +0.53
### C204 · Σ|Δ| 0.5 · 1 certs · `hpcdb18907/gas | cavity wall | pitched 125 | ext1`
### C196 · Σ|Δ| 0.5 · 1 certs · `hpcdb18907/gas | cavity wall | pitched 125 | ext1`
- [ ] `100010913220` · L 72 / E 72.53 / Δ +0.53
### C205 · Σ|Δ| 0.5 · 1 certs · `hpcdb18908/gas | solid brick | pitched 300`
### C197 · Σ|Δ| 0.5 · 1 certs · `hpcdb18908/gas | solid brick | pitched 300`
- [ ] `207032630` · L 74 / E 73.47 / Δ -0.53
### C206 · Σ|Δ| 0.5 · 1 certs · `hpcdb18242/gas | sandstone as | pitched 150 | ext2,sec:room heaters`
### C198 · Σ|Δ| 0.5 · 1 certs · `hpcdb18242/gas | sandstone as | pitched 150 | ext2,sec:room heaters`
- [ ] `43074051` · L 63 / E 62.48 / Δ -0.52
### C207 · Σ|Δ| 0.5 · 1 certs · `h104/gas | cavity wall | pitched 300`
### C199 · Σ|Δ| 0.5 · 1 certs · `h104/gas | cavity wall | pitched 300`
- [ ] `100010723814` · L 69 / E 69.52 / Δ +0.52
### C208 · Σ|Δ| 0.5 · 1 certs · `hpcdb16137/gas | cavity wall | (another dwelling`
### C200 · Σ|Δ| 0.5 · 1 certs · `hpcdb16137/gas | cavity wall | (another dwelling`
- [ ] `100020955792` · L 77 / E 77.52 / Δ +0.52
### C209 · Σ|Δ| 0.5 · 1 certs · `hpcdb19079/gas | cavity wall | pitched 200 | ext1,sec:room heaters`
### C201 · Σ|Δ| 0.5 · 1 certs · `hpcdb19079/gas | cavity wall | pitched 200 | ext1,sec:room heaters`
- [ ] `77172223` · L 65 / E 65.52 / Δ +0.52
### C210 · Σ|Δ| 0.5 · 1 certs · `hpcdb10242/gas | granite or | pitched insulated | RR,sec:room heaters`
### C202 · Σ|Δ| 0.5 · 1 certs · `hpcdb10242/gas | granite or | pitched insulated | RR,sec:room heaters`
- [ ] `100051245439` · L 62 / E 61.48 / Δ -0.52
### C211 · Σ|Δ| 0.5 · 1 certs · `hpcdb10328/gas | cavity wall | (another dwelling`
### C203 · Σ|Δ| 0.5 · 1 certs · `hpcdb10328/gas | cavity wall | (another dwelling`
- [ ] `100012553958` · L 67 / E 66.48 / Δ -0.52
### C212 · Σ|Δ| 0.5 · 1 certs · `hpcdb10033/gas | cavity wall | pitched 200 | ext2,sec:room heaters`
### C204 · Σ|Δ| 0.5 · 1 certs · `hpcdb10033/gas | cavity wall | pitched 200 | ext2,sec:room heaters`
- [ ] `100110108632` · L 63 / E 63.51 / Δ +0.51
### C213 · Σ|Δ| 0.5 · 1 certs · `h109/gas | cavity wall | pitched 270 | consv2,ext1,sec:room heaters`
### C205 · Σ|Δ| 0.5 · 1 certs · `h109/gas | cavity wall | pitched 270 | consv2,ext1,sec:room heaters`
- [ ] `100091102581` · L 65 / E 65.51 / Δ +0.51
### C214 · Σ|Δ| 0.5 · 1 certs · `hpcdb19008/gas | cavity wall | pitched 150 | ext1,sec:room heaters`
### C206 · Σ|Δ| 0.5 · 1 certs · `hpcdb19008/gas | cavity wall | pitched 150 | ext1,sec:room heaters`
- [ ] `10002326688` · L 69 / E 68.50 / Δ -0.50
### C215 · Σ|Δ| 0.5 · 1 certs · `h129/fuel28 | granite or | pitched 200 | ext2,sec:room heaters`
### C207 · Σ|Δ| 0.5 · 1 certs · `h129/fuel28 | granite or | pitched 200 | ext2,sec:room heaters`
- [ ] `10023132869` · L 46 / E 45.50 / Δ -0.50
### C216 · Σ|Δ| 0.5 · 1 certs · `hpcdb9007/fuel28 | sandstone as | pitched insulated | RR,ext4,sec:room heaters`
### C208 · Σ|Δ| 0.5 · 1 certs · `hpcdb9007/fuel28 | sandstone as | pitched insulated | RR,ext4,sec:room heaters`
- [ ] `10091707271` · L 41 / E 41.50 / Δ +0.50
## Resolved ≤0.5 (778 certs — no action)
## Resolved ≤0.5 (786 certs — no action)
<details><summary>expand</summary>
@ -698,7 +674,6 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
- [x] `100100840201` · L 60 / E 60.48 / Δ +0.48 · `hpcdb10244/gas | granite or | pitched 100 | ext1`
- [x] `100071135609` · L 68 / E 68.48 / Δ +0.48 · `hpcdb18229/gas | solid brick | pitched 200 | ext2`
- [x] `100121043224` · L 77 / E 77.48 / Δ +0.48 · `hpcdb1758/gas | cavity wall | pitched 300`
- [x] `30025401` · L 70 / E 69.52 / Δ -0.48 · `hpcdb18433/gas | cavity wall | pitched 270 | ext2`
- [x] `100021037247` · L 71 / E 70.52 / Δ -0.48 · `hpcdb10112/gas | solid brick | (another dwelling`
- [x] `10013778285` · L 80 / E 79.52 / Δ -0.48 · `hpcdb16138/gas | cavity wall | (another dwelling`
- [x] `100010348502` · L 76 / E 75.52 / Δ -0.48 · `hpcdb16839/gas | cavity wall | pitched 175`
@ -732,7 +707,6 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
- [x] `1775121297` · L 79 / E 78.55 / Δ -0.45 · `hpcdb16400/gas | cavity wall | pitched 300 | PV`
- [x] `100070304824` · L 76 / E 76.45 / Δ +0.45 · `hpcdb18119/gas | cavity wall | pitched insulated`
- [x] `41229335` · L 76 / E 75.55 / Δ -0.45 · `hpcdb16839/gas | cavity wall | pitched 200`
- [x] `200000571972` · L 64 / E 63.55 / Δ -0.45 · `hpcdb17986/gas | solid brick | pitched 250 | ext1`
- [x] `600039` · L 71 / E 70.55 / Δ -0.45 · `h104/gas | cavity wall | pitched 200`
- [x] `100050635265` · L 68 / E 67.55 / Δ -0.45 · `hpcdb17956/gas | cavity wall | pitched 250 | sec:room heaters`
- [x] `100040685184` · L 59 / E 59.45 / Δ +0.45 · `hpcdb18432/gas | cavity wall | pitched 300 | sec:room heaters`
@ -801,6 +775,7 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
- [x] `10070086972` · L 18 / E 17.59 / Δ -0.41 · `hpcdb710/fuel28+2mains | sandstone as | pitched insulated | RR,ext4,sec:room heaters`
- [x] `100050397944` · L 65 / E 64.59 / Δ -0.41 · `hpcdb17681/gas | cavity wall | pitched 250 | ext1,sec:room heaters`
- [x] `63105357` · L 74 / E 74.41 / Δ +0.41 · `hpcdb17507/gas | cavity wall | pitched 270 | sec:room heaters`
- [x] `217091901` · L 62 / E 61.59 / Δ -0.41 · `hpcdb18658/gas | solid brick | flat no` <- FIXED: RdSAP 10 Table 13 200/280mm solid-brick boundary bug (was 60.82); also fixed this cert's build script (never set wall thickness, inherited a stale value)
- [x] `100090989765` · L 70 / E 70.41 / Δ +0.41 · `hpcdb16408/gas | timber frame | pitched 300 | consv3,ext1,sec:room heaters`
- [x] `30523770` · L 72 / E 71.60 / Δ -0.40 · `hpcdb8191/fuel28 | granite or | pitched 200 | sec:room heaters`
- [x] `200004700463` · L 65 / E 65.40 / Δ +0.40 · `hpcdb18559/gas | sandstone as | pitched 75`
@ -838,6 +813,7 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
- [x] `77069148` · L 71 / E 71.39 / Δ +0.39 · `hpcdb17507/gas | cavity wall | pitched 200`
- [x] `148002976` · L 72 / E 71.61 / Δ -0.39 · `hpcdb16840/gas | cavity wall | pitched insulated | ext1`
- [x] `10096536318` · L 66 / E 65.62 / Δ -0.38 · `h301/coal-anth | cavity wall | (another dwelling`
- [x] `100061164545` · L 72 / E 72.38 / Δ +0.38 · `hpcdb15101/gas | solid brick | pitched 300`
- [x] `100030585620` · L 59 / E 59.38 / Δ +0.38 · `h102/gas | cavity wall | pitched insulated | ext4,sec:room heaters`
- [x] `10008325030` · L 82 / E 81.62 / Δ -0.38 · `h301/coal-anth | system built | (another dwelling`
- [x] `100050869353` · L 75 / E 74.62 / Δ -0.38 · `hpcdb10547/gas | solid brick | pitched 300`
@ -870,6 +846,7 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
- [x] `200033141` · L 76 / E 75.64 / Δ -0.36 · `hpcdb16738/gas | cavity wall | flat insulated`
- [x] `100030357741` · L 72 / E 72.36 / Δ +0.36 · `hpcdb18560/gas | cavity wall | pitched 300 | consv2,ext1`
- [x] `100080707176` · L 72 / E 71.64 / Δ -0.36 · `hpcdb10243/gas | cavity wall | pitched 100`
- [x] `30025401` · L 70 / E 69.64 / Δ -0.36 · `hpcdb18433/gas | cavity wall | pitched 270 | ext2`
- [x] `100062132377` · L 81 / E 80.64 / Δ -0.36 · `h214/elec | cavity wall | (another dwelling`
- [x] `100062132384` · L 81 / E 80.64 / Δ -0.36 · `h214/elec | cavity wall | (another dwelling`
- [x] `100031679582` · L 66 / E 66.36 / Δ +0.36 · `hpcdb10243/gas | cavity wall | pitched insulated`
@ -952,6 +929,7 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
- [x] `10008275334` · L 76 / E 76.31 / Δ +0.31 · `h691/elec | system built | (another dwelling | whw903`
- [x] `100110174472` · L 74 / E 74.31 / Δ +0.31 · `hpcdb8368/gas | solid brick | pitched 175 | ext2,mv5`
- [x] `100071450760` · L 55 / E 54.69 / Δ -0.31 · `h691/elec | cavity wall | (another dwelling | whw903`
- [x] `line255` · L 72 / E 72.31 / Δ +0.31 · `h104/gas | solid brick | pitched 200`
- [x] `207096040` · L 69 / E 69.31 / Δ +0.31 · `hpcdb18219/gas | solid brick | pitched insulated | ext1`
- [x] `200004695006` · L 69 / E 69.31 / Δ +0.31 · `hpcdb10243/gas | sandstone as | pitched insulated`
- [x] `100100800577` · L 64 / E 64.31 / Δ +0.31 · `h301/coal-anth | system built | pitched 200 | whw903`
@ -1037,6 +1015,7 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
- [x] `50011586` · L 69 / E 68.74 / Δ -0.26 · `hpcdb10328/gas | cavity wall | pitched 225 | consv2`
- [x] `100010680786` · L 73 / E 73.26 / Δ +0.26 · `hpcdb18907/gas | granite or | pitched 400+ | ext1`
- [x] `422000127372` · L 79 / E 78.75 / Δ -0.25 · `hpcdb17741/gas | cavity wall | pitched 400+`
- [x] `100050477371` · L 65 / E 64.75 / Δ -0.25 · `hpcdb16406/gas | solid brick | pitched insulated | RR,consv2,ext2,sec:room heaters`
- [x] `200000339841` · L 72 / E 72.25 / Δ +0.25 · `hpcdb18736/gas | cavity wall | (another dwelling`
- [x] `72380835` · L 82 / E 82.25 / Δ +0.25 · `hpcdb17507/gas | cavity wall | pitched 200 | PV,sec:room heaters`
- [x] `38193503` · L 65 / E 65.25 / Δ +0.25 · `hpcdb18907/gas | cavity wall | pitched 100 | ext2,sec:room heaters`
@ -1053,6 +1032,7 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
- [x] `100060721835` · L 68 / E 67.76 / Δ -0.24 · `hpcdb16841/gas | system built | pitched 250`
- [x] `100011764055` · L 72 / E 71.76 / Δ -0.24 · `hpcdb15823/gas | cavity wall | pitched 200 | ext4,sec:room heaters`
- [x] `5067734` · L 67 / E 67.24 / Δ +0.24 · `hpcdb17726/gas | solid brick | (another dwelling`
- [x] `38105600` · L 68 / E 67.76 / Δ -0.24 · `h104/gas | solid brick | pitched unknown | ext2`
- [x] `100021942272` · L 69 / E 69.24 / Δ +0.24 · `hpcdb17501/gas | solid brick | pitched insulated | RR`
- [x] `100050587694` · L 70 / E 70.24 / Δ +0.24 · `hpcdb18516/gas | cavity wall | pitched 270`
- [x] `100062195466` · L 73 / E 72.76 / Δ -0.24 · `h104/gas | cavity wall | (another dwelling`
@ -1095,6 +1075,7 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
- [x] `100050841010` · L 69 / E 69.21 / Δ +0.21 · `hpcdb10546/gas | cavity wall | pitched 100 | ext1`
- [x] `10090481366` · L 77 / E 76.79 / Δ -0.21 · `h691/elec | cavity wall | (another dwelling | whw909`
- [x] `100070308464` · L 65 / E 65.21 / Δ +0.21 · `h104/gas | cavity wall | pitched 250 | PV`
- [x] `100031768368` · L 61 / E 61.21 / Δ +0.21 · `hpcdb18250/gas | solid brick | pitched 100 | consv2,sec:room heaters` <- FIXED: RdSAP 10 Table 13 200/280mm solid-brick boundary bug (was 59.12) Elmhurst-validated, see test_rdsap_uvalues.py
- [x] `100012547948` · L 75 / E 74.79 / Δ -0.21 · `hpcdb17931/gas | cavity wall | pitched 100`
- [x] `100061316828` · L 69 / E 69.21 / Δ +0.21 · `hpcdb18403/gas | cavity wall | pitched 250 | mv2`
- [x] `83067520` · L 65 / E 64.79 / Δ -0.21 · `hpcdb18119/gas | cavity wall | pitched 200`
@ -1112,6 +1093,7 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
- [x] `100060370575` · L 70 / E 69.80 / Δ -0.20 · `hpcdb17760/gas | cavity wall | (another dwelling`
- [x] `10022972987` · L 11 / E 11.20 / Δ +0.20 · `h691/elec | sandstone as | pitched 200 | ext1,sec:room heaters,whw921`
- [x] `10023219663` · L 78 / E 77.80 / Δ -0.20 · `h301/coal-anth | system built | (another dwelling`
- [x] `100021326422` · L 70 / E 69.80 / Δ -0.20 · `hpcdb17679/gas | solid brick | pitched 125 | ext1` <- non-electric fabric/demand: per-cert lodged-geometry noise (field_bias scan clean, no coherent subgroup)
- [x] `100020603440` · L 74 / E 73.80 / Δ -0.20 · `h104/gas | cavity wall | pitched insulated`
- [x] `45040195` · L 75 / E 75.19 / Δ +0.19 · `hpcdb18250/gas | cavity wall | pitched 100`
- [x] `653958` · L 79 / E 78.81 / Δ -0.19 · `hpcdb10071/gas | cavity wall | pitched 250`
@ -1180,6 +1162,7 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
- [x] `10033267429` · L 57 / E 56.85 / Δ -0.15 · `h691/elec | system built | (another dwelling | whw903`
- [x] `100090372586` · L 71 / E 71.15 / Δ +0.15 · `h102/gas | cavity wall | pitched 300 | PV,consv3`
- [x] `100060587363` · L 62 / E 62.15 / Δ +0.15 · `hpcdb10462/gas | solid brick | pitched 250 | consv2,ext2,sec:room heaters`
- [x] `34080035` · L 77 / E 76.85 / Δ -0.15 · `hpcdb18218/gas | solid brick | (another dwelling`
- [x] `207037318` · L 59 / E 58.85 / Δ -0.15 · `h699/fuel0 | cavity wall | (another dwelling | sec:portable electric,whw903`
- [x] `100051048523` · L 67 / E 67.15 / Δ +0.15 · `hpcdb17507/gas | cavity wall | pitched 250 | ext1,sec:room heaters`
- [x] `551000` · L 72 / E 72.15 / Δ +0.15 · `h104/gas | cavity wall | pitched 150`
@ -1196,6 +1179,7 @@ Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · ⚠ xfail en
- [x] `100080902546` · L 79 / E 78.85 / Δ -0.15 · `hpcdb17696/gas | solid brick | pitched insulated | RR,ext1`
- [x] `100021286876` · L 66 / E 66.15 / Δ +0.15 · `hpcdb17503/gas | solid brick | pitched insulated`
- [x] `100070056707` · L 72 / E 72.14 / Δ +0.14 · `hpcdb17505/gas | system built | (another dwelling`
- [x] `200000571972` · L 64 / E 63.86 / Δ -0.14 · `hpcdb17986/gas | solid brick | pitched 250 | ext1`
- [x] `72513535` · L 70 / E 70.14 / Δ +0.14 · `h691/elec | system built | (another dwelling | whw903`
- [x] `100080166923` · L 68 / E 67.86 / Δ -0.14 · `hpcdb18826/gas | cavity wall | pitched 75 | consv3,ext1`
- [x] `100023483089` · L 77 / E 76.86 / Δ -0.14 · `h409/elec | system built | (another dwelling | sec:portable electric,whw903`

View file

@ -0,0 +1,252 @@
"""Elmhurst build for UPRN 100031768368 (RdSAP-21.0.1, END-TERRACE HOUSE, band C,
SOLID BRICK 280 mm uninsulated, pitched 100 mm loft insulation, SOLID ground
floor uninsulated, mains-gas COMBI (PCDB 18250, Baxi 630 Combi 88.4%) +
gas ROOM-HEATER SECONDARY, single electric shower with WWHRS (no mixer
shower), extract fan lodged (1) + 1 other-heater flue, 10 DG windows across
2 orientations (~13.48 m2 total: East 5.52 m2, West 5.13+2.83 m2 across two
glazing eras), TFA 76. Lodged SAP 61; engine 59.12 (Delta -1.88).
Per-cert Elmhurst validation, continuing the 1-2 SAP-band reconciliation
pass. Uses set_window_groups (3 orientation/glazing groups, not a single
collapsed window see the 100061275133 finding that a single-orientation
simplification meaningfully understates solar gain) + reset_transient_state
at the end so the shared assessment is left neutral. Run:
DISPLAY=:99 python scripts/hyde/build_100031768368.py <page>
"""
from __future__ import annotations
import sys
from pathlib import Path
import elmhurst_lib as E
DIM = "TabContainer_TabPanelMain_WebUserControlDimensionsMain_"
WALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
"TabPanelExternalWallMain_WebUserControlWallMain_")
PWALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
"TabPanelPartyWallMain_WebUserControlPartyWallMain_")
ROOF = "TabContainer_TabPanelMain_WebUserControlRoofMain_"
FLOOR = "TabContainer_TabPanelMain_WebUserControlFloorsMain_"
DP = "TabContainer_TabPanelDoorsPanel_"
VP = "TabContainer_TabPanelVentilationPanel_"
APT = "TabContainer_TabPanelAirPressureTest_"
LP = "TabContainer_TabPanelLighting_"
MV = "TabContainer_TabPanelMechVent_"
WH = "TabContainer_TabPanelWaterHeating_"
MH1B = "TabContainer_TabPanelMainHeating1_WebUserControlMainHeating1_"
_SAMPLE_DIR = (Path(__file__).parent.parent.parent
/ "backend/epc_api/json_samples/real_life_examples"
/ "RdSAP-Schema-21.0.1/uprn_100031768368")
def _pick(page, suffix, contains):
val = page.evaluate(
"""(a)=>{const s=document.getElementById(a[0]);if(!s)return null;
for(const o of s.options){if(o.text.toLowerCase().includes(a[1].toLowerCase()))return o.value;}return null;}""",
[f"{E.FP}{suffix}", contains])
if val is not None:
E.set_select(page, suffix, val)
return val
def property_description(page):
E.goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx")
E.set_select(page, "DropDownListPropertyType1", "H House")
_pick(page, "DropDownListPropertyType2", "end")
E.set_text(page, "TextBoxStoreys", "2")
E.set_text(page, "TextBoxHabitableRooms", "4")
E.set_text(page, "TextBoxHeatedHabitableRooms", "4")
print("date ->", _pick(page, "DropDownListDateBuiltMain", "1930-1949"))
E.set_select(page, "DropDownListDateBuiltFirst", "")
E.set_select(page, "DropDownListRoomInRoofMain", "")
E.save_close(page)
def dimensions(page):
E.goto(page, "Dimensions", "WebFormDimensions.aspx")
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "37.75")
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.45")
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "20.1")
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "5.0")
E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "37.75")
E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.45")
E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "20.1")
E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "5.0")
E.save_close(page)
def walls(page):
E.goto(page, "Walls", "WebFormWalls.aspx")
_pick(page, f"{WALL}DropDownListType", "solid brick")
page.wait_for_timeout(500)
print("insulation ->", _pick(page, f"{WALL}DropDownListInsulation", "as built"))
print("party ->", _pick(page, f"{PWALL}DropDownListPartyWallType", "determine")
or _pick(page, f"{PWALL}DropDownListPartyWallType", "masonry"))
for suf in (f"{WALL}TextBoxThickness", f"{WALL}TextBoxWallThickness"):
if page.locator(f"#{E.FP}{suf}").count():
E.set_text(page, suf, "280"); print("wall thickness set 280 via", suf); break
E.save_close(page)
def roofs(page):
E.goto(page, "Roofs", "WebFormRoofs.aspx")
_pick(page, f"{ROOF}DropDownListType", "access to loft") or \
_pick(page, f"{ROOF}DropDownListType", "loft")
_pick(page, f"{ROOF}DropDownListInsulationAt", "joists")
_pick(page, f"{ROOF}DropDownListThickness", "100")
E.save_close(page)
def floors(page):
E.goto(page, "Floors", "WebFormFloors.aspx")
_pick(page, f"{FLOOR}DropDownListLocation", "ground floor")
page.wait_for_timeout(400)
_pick(page, f"{FLOOR}DropDownListType", "solid")
ins = page.locator(f"#{E.FP}{FLOOR}DropDownListInsulation")
if ins.count():
E.set_select(page, f"{FLOOR}DropDownListInsulation", "A As built")
E.save_close(page)
def openings(page):
E.goto(page, "Openings", "WebFormOpenings.aspx")
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
page.wait_for_timeout(500)
# 3 orientation/glazing-era groups (not a single collapsed window):
# East 5.52 m2 (double pre-2002), West 5.13 m2 (double pre-2002), West
# 2.83 m2 (double 2002-2022, the one larger newer window index 8).
# The glazing dropdown retains its last-selected value across adds; if
# the next row wants the SAME text, selecting it again fires no 'change'
# event (identical value -> no transition) and the row commits with a
# blank Glazing Type. Force a transition through a different option
# first so every add gets a genuine change event.
for area, orient, glazing in (
(5.5174, "East", "Double pre 2002"),
(5.1346, "West", "Double pre 2002"),
(2.8288, "West", "Double between 2002 and 2021"),
):
E.set_select(page, f"{E._WP}DropDownListExtGlazing", "Secondary glazing") # noqa: SLF001
page.wait_for_timeout(300)
E.add_combined_window(page, area, orient, glazing)
page.wait_for_timeout(500)
guard = 0
while E.window_row_count(page) > 3 and guard < 25:
guard += 1
i = E.window_row_count(page) - 1
if not E._delete_window_at_index(page, i): # noqa: SLF001
break
E.click_tab(page, "TabContainer_TabPanelDoorsPanel")
E.set_text(page, f"{DP}TextBoxDoors", "1")
E.set_text(page, f"{DP}TextBoxDoorsInsulated", "0")
E.set_text(page, f"{DP}TextBoxDraughtProofedDoors", "1") # cert: percent_draughtproofed=100
E.save_close(page)
def ventilation(page):
E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx")
E.click_tab(page, "TabContainer_TabPanelVentilationPanel")
E.set_text(page, f"{VP}TextBoxIntermittentFans", "1") # cert: extract_fans_count=1
E.set_text(page, f"{VP}TextBoxFluesOtherHeater", "1") # cert: other_flues_count=1
cool = page.locator(f"#{E.FP}{VP}CheckBoxFixedSpaceCooling")
if cool.count() and cool.is_checked():
E.commit(page, cool.uncheck)
E.click_tab(page, "TabContainer_TabPanelMechVent")
mv = page.locator(f"#{E.FP}{MV}CheckBoxMechanicalVentilation")
if mv.count() and mv.is_checked():
E.commit(page, mv.uncheck)
E.click_tab(page, "TabContainer_TabPanelAirPressureTest")
E.set_select(page, f"{APT}DropDownListTestMethod", "Not available")
E.click_tab(page, "TabContainer_TabPanelLighting")
E.set_text(page, f"{LP}TextBoxLightsTotal", "13")
E.set_text(page, f"{LP}TextBoxLedLightsTotal", "10")
E.set_text(page, f"{LP}TextBoxCflLightsTotal", "2")
E.save_close(page)
def space_heating(page):
# PCDB exact-boiler entry: cert's main_heating_index_number 18250
# resolves to Baxi 630 Combi, 88.4% winter efficiency.
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
page.wait_for_timeout(1000)
E.clear_main_heating_code(page)
desc = E.set_pcdb_boiler(page, 18250)
print("PCDB boiler ->", desc)
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingControls",
"^Boilers", "^Standard", "CBE Programmer, room thermostat and TRVs")
print("control:", page.locator(f"#{E.MH1}TextBoxMainHeatingControls").input_value())
# cert lodges secondary_heating_type=605 ("Room heaters, mains gas",
# SAP10 Table 4a eff 0.40) — a genuine secondary system, unlike
# 100061275133's "None". The dialog is a 4-level cascade (fuel ->
# sub-fuel -> heater class -> specific product), not 3 like main
# heating controls. Probed all 12 RGx options by resolved SAP code;
# "RGH Flush fitting live effect gas fire, sealed to chimney" is the
# exact match for code 605.
E.set_select(page, "DropDownListSecondaryHeatingPresent", "Yes")
page.wait_for_timeout(900)
E.set_heating_dialog(page, "ButtonSecondaryHeatingCode",
"^Gas", "Mains gas", "Room Heaters", "RGH")
tb = page.locator(f"#{E.FP}TextBoxSecondaryHeatingCode")
print("secondary code:", tb.input_value() if tb.count() else "?")
E.save_close(page)
def water_heating(page):
E.goto(page, "WaterHeating", "WebFormWaterHeating.aspx")
E.click_tab(page, "TabContainer_TabPanelWaterHeating")
page.wait_for_timeout(400)
E.clear_hot_water_cylinder(page)
E.set_heating_dialog(page, f"{WH}ButtonWaterHeatingCode",
"From Space Heating", "From the primary heating system")
print("water code:", page.locator(f"#{E.FP}{WH}TextBoxWaterHeatingCode").input_value())
E.save_close(page)
def baths_showers(page):
"""cert: number_baths=1, shower_outlets=[electric shower, WWHRS=1],
mixer_shower_count=0 an instantaneous ELECTRIC shower with WWHRS
fitted, not a mixer shower off the main system."""
E.goto(page, "WaterHeating", "WebFormWaterHeating.aspx")
E.click_tab(page, "TabContainer_TabPanelBathsShowers")
page.wait_for_timeout(400)
print("(manual check needed: set 1 bath, 1 electric shower with WWHRS)")
def download(page):
_SAMPLE_DIR.mkdir(parents=True, exist_ok=True)
E.goto(page, "Recommendations", "WebFormRecommendations.aspx")
page.wait_for_timeout(1500)
with page.expect_navigation(wait_until="networkidle", timeout=40000):
page.click("#ContentBody_buttonActionSummary_Link", timeout=15000)
page.wait_for_timeout(2000)
for btn, fname in (("ContentBody_ContentPlaceHolder1_LinkButtonSummary", "elmhurst_summary.pdf"),
("ContentBody_ContentPlaceHolder1_ButtonDebugInforPdf", "elmhurst_worksheet.pdf")):
try:
with page.expect_download(timeout=60000) as dl:
page.click(f"#{btn}", timeout=15000)
dl.value.save_as(str(_SAMPLE_DIR / fname))
print("saved", fname)
except Exception as e:
print("!! download failed", fname, type(e).__name__, str(e)[:120])
def cleanup(page):
"""Leave the shared assessment neutral for the next build."""
E.reset_transient_state(page)
print("reset transient state (RR/secondary/meter)")
_ORDER = ["property_description", "dimensions", "walls", "roofs", "floors",
"openings", "ventilation", "space_heating", "baths_showers",
"water_heating", "download", "cleanup"]
def main():
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
print("usage: build_100031768368.py <" + "|".join(_ORDER) + ">")
return 2
with E.session() as (ctx, page):
globals()[sys.argv[1]](page)
print("done:", sys.argv[1], "->", page.url)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -85,6 +85,15 @@ def walls(page):
print("insulation opts:", _options(page, f"{WALL}DropDownListInsulation"))
_pick(page, f"{WALL}DropDownListInsulation", "as built") or \
_pick(page, f"{WALL}DropDownListInsulation", "none")
# cert lodges wall_thickness=280mm measured=Y — this field was never
# set here, so the shared assessment silently carried over whatever
# thickness an earlier build had left (260mm), masking the real
# RdSAP 10 Table 13 200/280mm boundary bug on this cert's exact
# thickness. See domain/sap10_ml/rdsap_uvalues.py
# _u_brick_thin_wall_age_a_to_e's docstring.
for suf in (f"{WALL}TextBoxThickness", f"{WALL}TextBoxWallThickness"):
if page.locator(f"#{E.FP}{suf}").count():
E.set_text(page, suf, "280"); print("wall thickness set 280 via", suf); break
E.save_close(page)

View file

@ -92,20 +92,66 @@ def test_electric_room_heaters_overlay_the_room_heater_charge_control() -> None:
assert simulation.heating.main_heating_control == 2601
def test_overlay_logs_when_it_cannot_complete_a_companion_set(
caplog: pytest.LogCaptureFixture,
def test_overlay_raises_on_an_incomplete_companion_set(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# An archetype the overlay cannot yet fully companion — here an air-source
# heat pump, whose Table 4e Group 2 conservative control is not yet mapped —
# is still overlaid (log-and-continue), but the incomplete companion set is
# logged as an error so the known gap is visible (CloudWatch) rather than
# silently inheriting the replaced system's stale value.
# Act
with caplog.at_level(logging.ERROR):
main_heating_overlay_for("Air source heat pump", 0)
# Every real archetype resolves a complete companion set, so the
# log-and-continue mitigation has no remaining population and the
# ADR-0048 end-state applies: an incomplete set FAILS LOUDLY instead of
# shipping a silently-incoherent cert (ADR-0053, #1444 Part C). Simulated
# with a hypothetical warm-air heat-pump archetype (SAP 521 — category 4
# but its Table 4e Group 5 control is deliberately unmapped): a future
# archetype cannot land without choosing its companions.
from domain.epc.property_overlays import main_heating_system_overlay as mod
# Assert — the gap is reported and names the archetype.
assert "Air source heat pump" in caplog.text
monkeypatch.setitem(mod._MAIN_HEATING_CODES, "Warm-air heat pump", 521)
# Act / Assert — the raise names the archetype.
with pytest.raises(ValueError, match="Warm-air heat pump"):
main_heating_overlay_for("Warm-air heat pump", 0)
@pytest.mark.parametrize(
"main_heating_value",
["Community heating, boilers", "Community heating, CHP and boilers"],
)
def test_community_heating_drags_the_default_group_3_control(
main_heating_value: str,
) -> None:
# The landlord names the scheme, not its control or charging arrangement.
# The Table 4e Group 3 default is 2313 ("flat rate charging, room
# thermostat and TRVs"): community schemes skew modern and the portfolio's
# own lodged community certs are overwhelmingly thermostatted (modal 2306;
# none lodge 2301), so the KIT axis concedes to the evidence — while the
# CHARGING axis stays flat-rate (space 1.05 / DHW 1.05): the billing
# arrangement is scheme paperwork an override genuinely cannot see, so
# usage-linked (1.0/1.0) is never assumed (ADR-0053, decided with Khalim).
# Leaving the control unset inherits the replaced system's instead (the
# ADR-0048 bug class).
# Act
simulation = main_heating_overlay_for(main_heating_value, 0)
# Assert — SAP Table 4e Group 3 default heat-network control.
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.main_heating_control == 2313
def test_air_source_heat_pump_drags_the_conservative_group_2_control() -> None:
# The landlord names the system, not its control, so the overlay assumes
# the conservative Table 4e Group 2 default: 2201 ("no time or thermostatic
# control", +0.3 C — the largest Group 2 adjustment, so an unobserved
# control is never over-credited; the heat-pump mirror of storage 2401 /
# boiler 2101). Deliberately NOT the ASHP Measure's 2210 zone control —
# a measure designs a modern end-state (ADR-0024); an override describes an
# existing pump (ADR-0053).
# Act
simulation = main_heating_overlay_for("Air source heat pump", 0)
# Assert — SAP Table 4e Group 2 conservative heat-pump control.
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.main_heating_control == 2201
def test_complete_room_heater_overlay_does_not_log_a_companion_gap(
@ -434,14 +480,70 @@ def test_storage_heaters_drag_along_conservative_manual_charge_control(
assert simulation.heating.main_heating_category == 7
def test_direct_acting_electric_does_not_drag_along_a_control() -> None:
# Charge control is a storage concept and full boiler controls are a wet-
# system concept; direct-acting electric panel heaters take neither, so the
# overlay leaves the control untouched.
simulation = main_heating_overlay_for("Direct-acting electric", 0)
@pytest.mark.parametrize(
"main_heating_value",
["Direct-acting electric", "Electric boiler", "Electric CPSU"],
)
def test_electric_boiler_overlays_the_boiler_category(
main_heating_value: str,
) -> None:
# A system-replacing override must stamp the boiler heating category (SAP
# Table 4a Category 2 — codes 191/192 are wet electric boilers). Leaving it
# unset inherits the replaced system's: on portfolio 796, 30 of the first
# 40 live 191/192 overrides carried an inherited category — 6 (community)
# or 4 (heat pump) suppressed the HHRSH pathway entirely, and 7 (storage)
# made the Table 12a resolver bill direct-acting heat 100% at the off-peak
# low rate (ADR-0052, #1444).
# Act
simulation = main_heating_overlay_for(main_heating_value, 0)
# Assert — SAP Table 4a Category 2 (boiler system).
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.main_heating_control is None
assert simulation.heating.main_heating_category == 2
@pytest.mark.parametrize(
"main_heating_value",
["Direct-acting electric", "Electric boiler", "Electric CPSU"],
)
def test_complete_electric_boiler_overlay_does_not_log_a_companion_gap(
main_heating_value: str, caplog: pytest.LogCaptureFixture
) -> None:
# Electric boilers now resolve a full companion set (category 2 + control
# 2101, ADR-0052), so the overlay must NOT log an incomplete-companion gap
# — guarding that the set stays complete.
# Act
with caplog.at_level(logging.ERROR):
main_heating_overlay_for(main_heating_value, 0)
# Assert
assert "incomplete companion set" not in caplog.text
@pytest.mark.parametrize(
"main_heating_value",
["Direct-acting electric", "Electric boiler", "Electric CPSU"],
)
def test_electric_boiler_drags_the_conservative_group_1_control(
main_heating_value: str,
) -> None:
# The landlord names the system, not its control, so the overlay assumes
# the conservative Table 4e Group 1 default: 2101 ("no time or thermostatic
# control", +0.6 C — the largest Group 1 adjustment, so an unobserved
# control is never over-credited; the boiler mirror of storage 2401 /
# room-heater 2601 / underfloor 2701). Supersedes the old "direct-acting
# drags no control" pin: an unset control is not "no control" — it INHERITS
# the replaced system's (a community 2306 or storage 2401 rode along on
# portfolio 796). Deliberately NOT the gas boilers' modern 2106 — nothing
# in the electric archetype implies a modern install (ADR-0052).
# Act
simulation = main_heating_overlay_for(main_heating_value, 0)
# Assert — SAP Table 4e Group 1 conservative boiler control.
assert simulation is not None
assert simulation.heating is not None
assert simulation.heating.main_heating_control == 2101
@pytest.mark.parametrize(
@ -545,6 +647,51 @@ def test_unresolvable_or_unmodelled_heating_produces_no_overlay(
assert simulation is None
def test_community_override_keeps_an_observed_group_3_control() -> None:
# A community override on a cert that ALREADY lodges community heating is a
# confirmation, not a system switch — the assessor observed the control
# (e.g. 2306, "charging linked to use, programmer and TRVs", worth ~1-2 SAP
# over the flat-rate default). Stamping the 2313 default over it would
# destroy real information, so the overlay opts into keeping an existing
# Table 4e GROUP 3 control (the heat-network mirror of
# `keep_existing_off_peak_meter`, ADR-0053).
# Arrange — a cert lodging community heating with an observed 2306.
baseline = build_epc()
main = baseline.sap_heating.main_heating_details[0]
main.sap_main_heating_code = 301
main.main_heating_category = 6
main.main_heating_control = 2306
overlay = main_heating_overlay_for("Community heating, boilers", 0)
assert overlay is not None
# Act
result = apply_simulations(baseline, [overlay])
# Assert — the observed usage-linked control survives.
assert result.sap_heating.main_heating_details[0].main_heating_control == 2306
def test_community_override_replaces_a_cross_family_control() -> None:
# When the replaced system was a DIFFERENT family, its control is stale by
# definition — a Group-1 boiler zone control (2110) has no meaning on a
# heat network (two live portfolio-796 dwellings carried exactly that) —
# so the Group 3 default is stamped (ADR-0053).
# Arrange — a cert lodging a gas boiler with zone control.
baseline = build_epc()
main = baseline.sap_heating.main_heating_details[0]
main.sap_main_heating_code = 102
main.main_heating_category = 2
main.main_heating_control = 2110
overlay = main_heating_overlay_for("Community heating, boilers", 0)
assert overlay is not None
# Act
result = apply_simulations(baseline, [overlay])
# Assert — the stale boiler control is replaced by the Group 3 default.
assert result.sap_heating.main_heating_details[0].main_heating_control == 2313
def test_main_heating_override_remaps_the_primary_system_code() -> None:
# Arrange
baseline = build_epc()

View file

@ -874,22 +874,35 @@ _EXPECTATIONS: Final[tuple[RealCertExpectation, ...]] = (
# TOP-FLOOR FLAT, band A (before 1900), SOLID BRICK 280 mm uninsulated,
# FLAT ROOF no insulation (As Built), floor over ANOTHER DWELLING below
# (zero loss), mains-gas COMBI, SINGLE glazing (glazed_type 5), TFA 63.
# Lodged 62 / engine 61 (60.82). Built in accredited Elmhurst RdSAP10
# (evidence saved: elmhurst_summary.pdf / elmhurst_worksheet.pdf): EVERY
# heat-loss element matches the engine EXACTLY — walls (solid U 1.70,
# 53.18 W/K), flat roof (uninsulated U 2.30, 145.57 W/K), floor (0),
# party (0), and — with the faithful SINGLE glazing entered — WINDOWS
# (8.62 m² × effective U 4.0268 = 34.7114 W/K, identical to the engine).
# This confirms the mapper''s glazed_type 5 → single (raw U 4.8) is
# spec-correct (epc_codes.csv: glazed_type 5 = single glazing), NOT the
# double it superficially resembles. The 1.18 vs lodged is a cost-side
# methodology residual (PE +8.7 kWh/m², CO2/PE-driven), not a fabric bug.
# PINNED to the observed engine 61.
# Lodged 62 / engine 62 (61.59, was 60.82 before the §5.7 Table 13
# 280mm-boundary fix below) — now an EXACT match. Built in accredited
# Elmhurst RdSAP10 (evidence saved: elmhurst_summary.pdf /
# elmhurst_worksheet.pdf): flat roof (uninsulated U 2.30, 145.57 W/K),
# floor (0), party (0), and — with the faithful SINGLE glazing entered —
# WINDOWS (8.62 m² × effective U 4.0268 = 34.7114 W/K, identical to the
# engine) all match EXACTLY. This confirms the mapper''s glazed_type 5
# → single (raw U 4.8) is spec-correct (epc_codes.csv: glazed_type 5 =
# single glazing), NOT the double it superficially resembles.
#
# Wall U originally logged as "1.70, matches engine exactly" — but that
# was a FALSE match: this cert's build script (build_217091901.py)
# never actually set a wall thickness field, so the shared assessment
# silently carried over a stale 260mm from an earlier build instead of
# this cert's real 280mm (measured=Y). Rebuilt with the corrected
# 280mm entry: Elmhurst's own worksheet (29a) NOW gives wall U=1.40,
# not 1.70 — see the sibling fix on cert 100061275133/100031768368's
# investigation, which found the SAME 280mm-exactly boundary bug in
# `_u_brick_thin_wall_age_a_to_e` (RdSAP 10 Table 13's 200/280mm rows
# share an unlabelled edge; Elmhurst puts it in the upper "280-420mm
# -> 1.4" row, our `<= 280` check put it in the lower row). Fixed;
# both this cert (band A) and 100031768368 (band C) now agree on
# U=1.40 at exactly 280mm — two independent Elmhurst confirmations.
# PINNED to the observed exact match (vs lodged, the correct target).
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_217091901",
cert_num="uprn-217091901",
sap_score=61,
sap_score=62,
),
# UPRN 100061275133 (corpus; no cert number lodged). RdSAP-21.0.1 native —
# 2-storey SEMI-DETACHED HOUSE, band B, SOLID BRICK 400 mm with EXTERNAL
@ -1034,6 +1047,47 @@ _EXPECTATIONS: Final[tuple[RealCertExpectation, ...]] = (
cert_num="uprn-10091578598",
sap_score=79,
),
# UPRN 100031768368 (corpus; no cert number lodged). RdSAP-21.0.1 native —
# END-TERRACE HOUSE, band C (1930-1949), SOLID BRICK 280 mm uninsulated
# (wall_thickness_measured=Y), pitched 100 mm loft insulation, SOLID
# ground floor uninsulated, mains-gas COMBI (PCDB 18250, Baxi 630 Combi
# 88.4%) + gas ROOM-HEATER SECONDARY (code 605), electric shower with
# WWHRS (no mixer shower — mixer_shower_count genuinely 0, corpus-
# checked: no systematic bias on the 170-cert "bath+no shower data"
# cohort), 10 windows across 2 real orientations (East 5.52 m²/West
# 8.0 m² split further by glazing era), TFA 75.5. Lodged 61 / engine
# 61 (61.21, was 59.12 before the fix below) — now within 0.5.
#
# Built in accredited Elmhurst RdSAP10 (evidence saved:
# elmhurst_summary.pdf / elmhurst_worksheet.pdf): dwelling volume
# (194.4125 m³), ventilation ACH (21)=0.7229, ground floor W/K
# (27.5575), and doors W/K (5.55) all matched EXACTLY on the first
# build. The wall did not: engine U=1.70 (149.92 W/K) vs Elmhurst
# U=1.40 (123.47 W/K) despite identical inputs (construction, "As
# Built" insulation, 280mm thickness, age band all confirmed matching
# via the input-summary crosswalk).
#
# Root cause: RdSAP 10 §5.7 Table 13 (spec PDF p.41) gives four
# thickness bands for an uninsulated solid-brick wall — "up to 200mm"
# -> 2.5, "200 to 280mm" -> 1.7, "280 to 420mm" -> 1.4, "more than
# 420mm" -> 1.1 — and this cert's wall sits EXACTLY on the shared
# 200/280 edge, which the table text doesn't disambiguate.
# `_u_brick_thin_wall_age_a_to_e`'s `<= 280` check put the edge in the
# lower (1.7) row; Elmhurst puts it in the upper (1.4) row. Verified
# independently on a SECOND cert (217091901, band A, also exactly
# 280mm) after discovering and fixing its build script's own bug
# (never actually set a wall thickness, silently inheriting a stale
# 260mm from an earlier build) — rebuilt with the corrected 280mm
# entry, Elmhurst's worksheet gives the SAME U=1.40. Fixed in
# `domain/sap10_ml/rdsap_uvalues.py`; corpus gauge 77.8%->78.6%
# within-0.5, MAE 0.636->0.627 (14 corpus certs lodge exactly 280mm
# solid brick). PINNED to the observed match vs lodged.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_100031768368",
cert_num="uprn-100031768368",
sap_score=61,
),
)

View file

@ -239,7 +239,9 @@ _CORPUS = Path(
# 2.97, CO2 0.074 -> 0.072.
# 77.7% -> 77.8% via the §5 (12) suspended-timber floor-infiltration
# fallback (see the `_MAX_SAP_MAE` note below for the mechanism).
_MIN_WITHIN_HALF_SAP = 0.778
# 77.8% -> 78.6% via the RdSAP 10 §5.7 Table 13 200/280mm solid-brick-wall
# boundary fix (see the `_MAX_SAP_MAE` note below).
_MIN_WITHIN_HALF_SAP = 0.786
# 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,
@ -334,7 +336,19 @@ _MIN_WITHIN_HALF_SAP = 0.778
# description for the U-value calc — the infiltration rule did not).
# Cert 100061275133 (Elmhurst-validated build): SAP 77.24 -> 76.33, now
# an EXACT match to lodged (76). Unit-pinned in test_cert_to_inputs.
_MAX_SAP_MAE = 0.637
# 77.8% -> 78.6% (MAE 0.636 -> 0.627) via the RdSAP 10 §5.7 Table 13
# 200/280mm solid-brick-wall boundary fix: the spec table's "200 to
# 280mm -> 1.7" / "280 to 420mm -> 1.4" rows share an unlabelled edge at
# exactly 280mm; `_u_brick_thin_wall_age_a_to_e`'s `<= 280` check put it
# in the lower row. Elmhurst-validated on TWO certs at exactly 280mm in
# different age bands (100031768368 band C, 217091901 band A after
# fixing that cert's build script — it never set a wall thickness field
# and had silently inherited a stale value from an earlier build) — both
# independently confirm the shared edge belongs to the upper (1.4) row.
# 100031768368 SAP 59.12 -> 61.21 (lodged 61, now within 0.5); 217091901
# 60.82 -> 61.59 (lodged 62, now an exact match). 14 corpus certs lodge
# exactly 280mm solid brick. Unit-pinned in test_rdsap_uvalues.
_MAX_SAP_MAE = 0.628
_MAX_CO2_MAE_TONNES = 0.072 # t CO2 / yr vs co2_emissions_current
_MAX_PE_PER_M2_MAE = 3.0 # kWh / m2 / yr vs energy_consumption_current