diff --git a/.github/workflows/_deploy_lambda.yml b/.github/workflows/_deploy_lambda.yml index bce793dcb..9f6b07beb 100644 --- a/.github/workflows/_deploy_lambda.yml +++ b/.github/workflows/_deploy_lambda.yml @@ -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 }} diff --git a/.github/workflows/check_lambda_zip_size.yml b/.github/workflows/check_lambda_zip_size.yml new file mode 100644 index 000000000..c65e9cfd2 --- /dev/null +++ b/.github/workflows/check_lambda_zip_size.yml @@ -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 diff --git a/backend/app/main.py b/backend/app/main.py index 2904fb978..0dd9e489b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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") diff --git a/backend/app/requirements/check_lambda_zip_size.py b/backend/app/requirements/check_lambda_zip_size.py new file mode 100755 index 000000000..2ccd9a07a --- /dev/null +++ b/backend/app/requirements/check_lambda_zip_size.py @@ -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 ...` 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()) diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100031768368/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100031768368/elmhurst_summary.pdf new file mode 100644 index 000000000..db84a0784 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100031768368/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100031768368/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100031768368/elmhurst_worksheet.pdf new file mode 100644 index 000000000..d419fcf3e Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100031768368/elmhurst_worksheet.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100031768368/epc.json b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100031768368/epc.json new file mode 100644 index 000000000..161c1649d --- /dev/null +++ b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100031768368/epc.json @@ -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 +} \ No newline at end of file diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_217091901/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_217091901/elmhurst_summary.pdf index bf5e1e382..db84a0784 100644 Binary files a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_217091901/elmhurst_summary.pdf and b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_217091901/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_217091901/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_217091901/elmhurst_worksheet.pdf index c99553292..d419fcf3e 100644 Binary files a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_217091901/elmhurst_worksheet.pdf and b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_217091901/elmhurst_worksheet.pdf differ diff --git a/deployment/terraform/lambda/fast-api/main.tf b/deployment/terraform/lambda/fast-api/main.tf index dea9b7d92..04d05af72 100644 --- a/deployment/terraform/lambda/fast-api/main.tf +++ b/deployment/terraform/lambda/fast-api/main.tf @@ -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 } } diff --git a/deployment/terraform/lambda/fast-api/variables.tf b/deployment/terraform/lambda/fast-api/variables.tf index a31575902..c421d2f57 100644 --- a/deployment/terraform/lambda/fast-api/variables.tf +++ b/deployment/terraform/lambda/fast-api/variables.tf @@ -41,4 +41,10 @@ variable "epc_auth_token" { variable "google_solar_api_key" { type = string sensitive = true -} \ No newline at end of file +} + +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" +} diff --git a/deployment/terraform/modules/lambda_with_api_gateway/variables.tf b/deployment/terraform/modules/lambda_with_api_gateway/variables.tf index b5d0515a0..7576a4bf0 100644 --- a/deployment/terraform/modules/lambda_with_api_gateway/variables.tf +++ b/deployment/terraform/modules/lambda_with_api_gateway/variables.tf @@ -11,7 +11,10 @@ variable "zip_excludes" { "**/*.pyc", "**/.pytest_cache/**", "**/tests/**", - "**/deployment/**" + "**/deployment/**", + "**/.git/**", + "**/json_samples/**", + "**/docs/specs/**" ] } diff --git a/docs/adr/0052-electric-boiler-and-cpsu-overrides-drag-category-2-and-conservative-group-1-control.md b/docs/adr/0052-electric-boiler-and-cpsu-overrides-drag-category-2-and-conservative-group-1-control.md new file mode 100644 index 000000000..170e40be5 --- /dev/null +++ b/docs/adr/0052-electric-boiler-and-cpsu-overrides-drag-category-2-and-conservative-group-1-control.md @@ -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. diff --git a/docs/adr/0053-heating-override-companion-taxonomy-complete-incomplete-set-raises.md b/docs/adr/0053-heating-override-companion-taxonomy-complete-incomplete-set-raises.md new file mode 100644 index 000000000..0f1ca0cd7 --- /dev/null +++ b/docs/adr/0053-heating-override-companion-taxonomy-complete-incomplete-set-raises.md @@ -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. diff --git a/domain/epc/property_overlays/main_heating_system_overlay.py b/domain/epc/property_overlays/main_heating_system_overlay.py index f99a24f5c..946c648ca 100644 --- a/domain/epc/property_overlays/main_heating_system_overlay.py +++ b/domain/epc/property_overlays/main_heating_system_overlay.py @@ -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, ) ) diff --git a/domain/modelling/scoring/overlay_applicator.py b/domain/modelling/scoring/overlay_applicator.py index 216463dab..1a9357033 100644 --- a/domain/modelling/scoring/overlay_applicator.py +++ b/domain/modelling/scoring/overlay_applicator.py @@ -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 diff --git a/domain/modelling/simulation.py b/domain/modelling/simulation.py index 79855940e..55c52855e 100644 --- a/domain/modelling/simulation.py +++ b/domain/modelling/simulation.py @@ -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) diff --git a/domain/sap10_ml/rdsap_uvalues.py b/domain/sap10_ml/rdsap_uvalues.py index be1ab2f62..41b366cd0 100644 --- a/domain/sap10_ml/rdsap_uvalues.py +++ b/domain/sap10_ml/rdsap_uvalues.py @@ -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 diff --git a/domain/sap10_ml/tests/test_rdsap_uvalues.py b/domain/sap10_ml/tests/test_rdsap_uvalues.py index 95993be78..f09a453ed 100644 --- a/domain/sap10_ml/tests/test_rdsap_uvalues.py +++ b/domain/sap10_ml/tests/test_rdsap_uvalues.py @@ -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 diff --git a/scripts/corpus_1000/worklist.md b/scripts/corpus_1000/worklist.md index 66fc7797b..e1ed89ced 100644 --- a/scripts/corpus_1000/worklist.md +++ b/scripts/corpus_1000/worklist.md @@ -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)
expand @@ -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` diff --git a/scripts/hyde/build_100031768368.py b/scripts/hyde/build_100031768368.py new file mode 100644 index 000000000..867974856 --- /dev/null +++ b/scripts/hyde/build_100031768368.py @@ -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 +""" +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()) diff --git a/scripts/hyde/build_217091901.py b/scripts/hyde/build_217091901.py index f487d973c..b803b3ba6 100644 --- a/scripts/hyde/build_217091901.py +++ b/scripts/hyde/build_217091901.py @@ -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) diff --git a/tests/domain/epc/test_main_heating_system_overlay.py b/tests/domain/epc/test_main_heating_system_overlay.py index 5c1b680d2..dc50dd2d5 100644 --- a/tests/domain/epc/test_main_heating_system_overlay.py +++ b/tests/domain/epc/test_main_heating_system_overlay.py @@ -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() diff --git a/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py b/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py index f792c519a..674cd0ef8 100644 --- a/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py +++ b/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py @@ -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, + ), ) diff --git a/tests/infrastructure/epc_client/test_sap_accuracy_corpus.py b/tests/infrastructure/epc_client/test_sap_accuracy_corpus.py index ff14c1fa7..5271c2434 100644 --- a/tests/infrastructure/epc_client/test_sap_accuracy_corpus.py +++ b/tests/infrastructure/epc_client/test_sap_accuracy_corpus.py @@ -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