From 775232b4e72a8a85d66798946a6f776bca73623a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 29 Jun 2026 13:32:20 +0000 Subject: [PATCH 1/8] =?UTF-8?q?test(pv):=20credit=20PV=20only=20when=20con?= =?UTF-8?q?nected=20to=20dwelling=20meter=20(pv=5Fconnection)=20?= =?UTF-8?q?=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RdSAP 10 §11.1 / SAP 10.2 Appendix M: PV is included in a dwelling's assessment only if connected to the dwelling's own electricity meter. The gov-API pv_connection enum encodes this — 0=no PV, 1=present-but-not- connected, 2=connected. Corpus-validated (57 PV certs: pv_connection=1 MAE 4.48->1.22 without credit, 0/5 need it; pv_connection=2 needs it, MAE 0.98 vs 10.29) and Elmhurst-proven (connected SAP 87 vs not-connected 74). cert_to_inputs currently credits a pv_connection=1 array; the test pins that it must contribute zero generation. Adds pv_connection to make_minimal_sap10_epc. Co-Authored-By: Claude Opus 4.8 (1M context) --- domain/sap10_ml/tests/_fixtures.py | 2 ++ .../rdsap/test_cert_to_inputs.py | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/domain/sap10_ml/tests/_fixtures.py b/domain/sap10_ml/tests/_fixtures.py index f92a4995..d3fae7e2 100644 --- a/domain/sap10_ml/tests/_fixtures.py +++ b/domain/sap10_ml/tests/_fixtures.py @@ -267,6 +267,7 @@ def make_minimal_sap10_epc( sap_heating: Optional[SapHeating] = None, photovoltaic_arrays: Optional[list[PhotovoltaicArray]] = None, photovoltaic_supply_percent_roof_area: Optional[int] = None, + pv_connection: Optional[int] = None, mains_gas: bool = True, electricity_smart_meter_present: bool = False, gas_smart_meter_present: bool = False, @@ -308,6 +309,7 @@ def make_minimal_sap10_epc( sap_energy_source=SapEnergySource( mains_gas=mains_gas, meter_type="Single", + pv_connection=pv_connection, pv_battery_count=pv_battery_count, wind_turbines_count=wind_turbines_count, gas_smart_meter_present=gas_smart_meter_present, diff --git a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py index 2adeea02..b643bf0d 100644 --- a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py +++ b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py @@ -40,6 +40,7 @@ from domain.sap10_ml.tests._fixtures import ( make_floor_dimension, make_main_heating_detail, make_minimal_sap10_epc, + make_pv_array, make_sap_heating, make_window, ) @@ -8387,3 +8388,38 @@ def test_heat_pump_water_scop_not_applied_to_separate_immersion_dhw() -> None: # heat pump or a gas boiler (the HP water SCOP does not apply to it). assert hp_fuel > 0.0 assert abs(hp_fuel - boiler_fuel) <= 1e-6 + + +def test_pv_credited_only_when_connected_to_dwelling_meter_per_pv_connection() -> None: + # RdSAP 10 §11.1 / SAP 10.2 Appendix M: PV-generated electricity is + # included in a dwelling's assessment ONLY IF the array is connected to + # the dwelling's own electricity meter; an unconnected (communal / + # separately-metered) array contributes nothing to the dwelling's energy + # cost, CO2 or primary energy. The gov-API `sap_energy_source.pv_connection` + # enum encodes this: 0 = no PV, 1 = present but NOT connected, 2 = connected. + # + # Validated on the RdSAP-21.0.1 corpus (57 PV certs): every pv_connection=1 + # cert reconciles BETTER without the credit (MAE 4.48 -> 1.22, 0/5 need it), + # while pv_connection=2 certs need it (MAE 0.98 vs 10.29 without). Accredited + # Elmhurst proof: an identical dwelling rates SAP 87 with "Connected to + # Dwelling = Yes" (credit -£167) vs SAP 74 with "No" (credit £0). + array = [make_pv_array(peak_power=3.0)] + + def _gen(pv_connection: int) -> float: + epc = make_minimal_sap10_epc( + dwelling_type="Mid-terrace house", + total_floor_area_m2=70.0, + habitable_rooms_count=3, + country_code="ENG", + photovoltaic_arrays=array, + is_dwelling_export_capable=True, + pv_connection=pv_connection, + ) + return cert_to_inputs(epc).pv_generation_kwh_per_yr + + # pv_connection=2 (connected to the dwelling's meter) → PV serves the + # dwelling and is credited. + assert _gen(2) > 0.0 + # pv_connection=1 (present but NOT connected to the dwelling's meter) → + # the array contributes nothing to this dwelling's SAP. + assert _gen(1) == 0.0 From 71b304fa47ce66be166167d05a74bc13bc373485 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 29 Jun 2026 13:32:53 +0000 Subject: [PATCH 2/8] decrease lambda concurrency from 32 to 12 --- deployment/terraform/lambda/modelling_e2e/variables.tf | 2 +- scripts/trigger_modelling_e2e_sqs.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deployment/terraform/lambda/modelling_e2e/variables.tf b/deployment/terraform/lambda/modelling_e2e/variables.tf index 2595d86c..bcade99c 100644 --- a/deployment/terraform/lambda/modelling_e2e/variables.tf +++ b/deployment/terraform/lambda/modelling_e2e/variables.tf @@ -26,7 +26,7 @@ variable "reserved_concurrent_executions" { variable "maximum_concurrency" { type = number - default = 32 + default = 12 description = "Maximum concurrent Lambda invocations from the SQS trigger." } diff --git a/scripts/trigger_modelling_e2e_sqs.py b/scripts/trigger_modelling_e2e_sqs.py index e0d060d9..a4c6a4df 100644 --- a/scripts/trigger_modelling_e2e_sqs.py +++ b/scripts/trigger_modelling_e2e_sqs.py @@ -43,15 +43,15 @@ COMPLETED_SINCE: datetime | None = datetime( DRY_RUN: bool = False # False → Lambda skips the Google Solar fetch (re-uses stored Solar data). -REFETCH_SOLAR: bool = True +REFETCH_SOLAR: bool = False # False → use stored lodged EPC for properties that have one; properties with no # stored lodged EPC are treated as EPC-less and routed to prediction (no API call). -REFETCH_EPC: bool = True +REFETCH_EPC: bool = False # False → use stored predicted EPC for EPC-less properties that have one; live # prediction still runs when no stored predicted EPC exists for the property. -REPREDICT_EPC: bool = True +REPREDICT_EPC: bool = False # --------------------------------------------------------------------------- _REPO_ROOT = Path(__file__).resolve().parents[1] From 66fa08af193b43c1c635d0fe7e1a37444fccf103 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 29 Jun 2026 13:33:48 +0000 Subject: [PATCH 3/8] update comment --- applications/modelling_e2e/handler.py | 29 +++++++++++++-------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/applications/modelling_e2e/handler.py b/applications/modelling_e2e/handler.py index 532d5fb4..c62fad73 100644 --- a/applications/modelling_e2e/handler.py +++ b/applications/modelling_e2e/handler.py @@ -231,7 +231,7 @@ def _get_engine() -> Engine: # everything up front through one short-lived read Session, closes it, # then writes each Property in a sequential Unit of Work — and the Unit of # Work resolves overrides on its own session — so no two Sessions overlap - # and a single connection suffices. 32 concurrent containers × 1 = 32 + # and a single connection suffices. 12 concurrent containers × 1 = 12 # against RDS. # # NullPool, not a fixed pool, enforces that as a *graceful* ceiling rather @@ -408,7 +408,9 @@ def _predict_epc( orchestrator_cm=_shared_engine_orchestrator, pass_task_orchestrator=True, ) -def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator, task_id: UUID) -> None: +def handler( + body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator, task_id: UUID +) -> None: trigger = ModellingE2ETriggerBody.model_validate(body) property_ids = trigger.property_ids portfolio_id = trigger.portfolio_id @@ -510,9 +512,7 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator, ) epc_repo = EpcPostgresRepository(read_session) stored_lodged_epcs: dict[int, EpcPropertyData] = ( - epc_repo.get_for_properties(property_ids) - if not refetch_epc - else {} + epc_repo.get_for_properties(property_ids) if not refetch_epc else {} ) stored_predicted_epcs: dict[int, EpcPropertyData] = ( epc_repo.get_predicted_for_properties(property_ids) @@ -535,9 +535,7 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator, spatial = _spatial_for(geospatial, uprn) restrictions = ( - spatial.restrictions - if spatial is not None - else PlanningRestrictions() + spatial.restrictions if spatial is not None else PlanningRestrictions() ) coordinates: Optional[Coordinates] = ( spatial.coordinates if spatial is not None else None @@ -547,10 +545,14 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator, if refetch_epc: epc: Optional[EpcPropertyData] = epc_client.get_by_uprn(uprn) elif stored_lodged is not None: - logger.info(f"property={pid} using stored lodged EPC (refetch_epc=False)") + logger.info( + f"property={pid} using stored lodged EPC (refetch_epc=False)" + ) epc = stored_lodged else: - epc = None # no stored lodged EPC; prediction path handles this property + epc = ( + None # no stored lodged EPC; prediction path handles this property + ) overrides = overlays_from(overrides_reader.overrides_for(pid)) predicted_epc: Optional[EpcPropertyData] = None @@ -567,9 +569,7 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator, landlord_overrides=overrides, ).effective_epc else: - logger.info( - f"property={pid} no lodged EPC — attempting prediction" - ) + logger.info(f"property={pid} no lodged EPC — attempting prediction") stored_predicted = stored_predicted_epcs.get(pid) if not repredict_epc and stored_predicted is not None: logger.info( @@ -620,8 +620,7 @@ def handler(body: dict[str, Any], context: Any, orchestrator: TaskOrchestrator, print_table=False, ) logger.info( - f"property={pid} modelling complete " - f"measures={len(plan.measures)}" + f"property={pid} modelling complete " f"measures={len(plan.measures)}" ) if dry_run: From 8606cab5f0e4f099ccc24c0e25a9475a04e68856 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 29 Jun 2026 13:40:51 +0000 Subject: [PATCH 4/8] =?UTF-8?q?fix(pv):=20credit=20PV=20only=20when=20conn?= =?UTF-8?q?ected=20to=20the=20dwelling's=20meter=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate PV generation/credit in cert_to_inputs on gov-API pv_connection: credit only when ==2 ('connected'); ==1 ('present but not connected to the dwelling's meter') contributes zero to the dwelling's cost/CO2/PE per RdSAP 10 §11.1 / SAP 10.2 Appendix M. Non-int (None / site-notes str) keeps the credit-if-array behaviour, so the Elmhurst/Summary + synthetic paths are unchanged (no regression). Corpus: all 5 pv_connection=1 PV certs move inside ±0.5 (e.g. 100051118081 +6.5→+0.5); MAE 0.760→0.740, within-0.5 73.8→74.3%, no regression (pv_connection=2 certs keep their credit). Also corrects a now-load-bearing latent bug: the solar-recommendation overlay tagged recommended arrays pv_connection=1 ('not connected') — which the new gate would zero. A new install connects to the dwelling's meter, so it must be 2; pinned by the overlay test. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../generators/solar_recommendation.py | 10 ++-- .../sap10_calculator/rdsap/cert_to_inputs.py | 50 ++++++++++++++++++- .../modelling/test_solar_recommendation.py | 4 ++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/domain/modelling/generators/solar_recommendation.py b/domain/modelling/generators/solar_recommendation.py index f5bf6e10..f97a5752 100644 --- a/domain/modelling/generators/solar_recommendation.py +++ b/domain/modelling/generators/solar_recommendation.py @@ -49,9 +49,13 @@ _BATTERY_CAPACITY_KWH = 5.0 # Watts → kilowatts for peak-power. _WATTS_PER_KW = 1000.0 # The dwelling's PV connects to its own meter (the after-cert §19 "Connected to -# the dwelling's meter: Yes"). Non-load-bearing for the SAP cascade; carried for -# fidelity. 1 = connected, the modal install case. -_PV_CONNECTED_TO_DWELLING = 1 +# the dwelling's meter: Yes"). LOAD-BEARING: `cert_to_inputs` credits PV to the +# dwelling's SAP only when `pv_connection == 2` ("connected"); value 1 means +# "present but NOT connected" and zeroes the credit. gov-API enum (corpus- and +# Elmhurst-validated): 0 = no PV, 1 = not connected, 2 = connected (the modal +# install case, 52 vs 5 on the RdSAP-21.0.1 corpus). A newly-installed +# recommended array is connected to the dwelling's own meter. +_PV_CONNECTED_TO_DWELLING = 2 # A roof plane within this many degrees of due north (0°/360°, Google compass # convention) is dropped: it generates little and is not worth panelling. The diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index 7520a440..1cdfd98e 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -3156,6 +3156,42 @@ def _pv_array_generation_kwh_per_yr( return _PV_MODULE_EFFICIENCY_FACTOR * array.peak_power * s * z +# gov-API `sap_energy_source.pv_connection` enum (RdSAP 10 §11.1 / +# SAP 10.2 Appendix M — "PV is included in the dwelling's assessment only +# if connected to the dwelling's electricity meter"): +# 0 = no PV +# 1 = PV present but NOT connected to the dwelling's own meter +# 2 = PV connected to the dwelling's own meter +# Validated on the RdSAP-21.0.1 corpus (57 PV certs): pv_connection=1 certs +# reconcile to the lodged SAP only WITHOUT a credit (MAE 4.48→1.22, 0/5 need +# it); pv_connection=2 certs need it (MAE 0.98 vs 10.29 without). Accredited +# Elmhurst proof: identical dwelling = SAP 87 connected vs SAP 74 not. +_PV_CONNECTION_CONNECTED_TO_DWELLING_METER: Final[int] = 2 + + +def _pv_connected_to_dwelling_meter(epc: EpcPropertyData) -> bool: + """Whether a lodged PV array may be credited to this dwelling's SAP, i.e. + whether it is connected to the dwelling's own electricity meter. + + Keyed on the gov-API integer `pv_connection`: only value 2 ("connected") + earns a credit; value 1 ("present but not connected" — a communal / + separately-metered array) contributes nothing to the dwelling's energy + cost, CO2 or primary energy, per RdSAP 10 §11.1 / SAP 10.2 Appendix M. + + A non-integer `pv_connection` (None, or the site-notes `str` form which + does not yet capture the connection flag) is NOT a determinate + "not connected" signal, so it preserves the existing credit-if-array + behaviour — no regression on the Elmhurst/Summary path or synthetic + CalculatorInputs. The Elmhurst extractor parses "Connected to the + dwelling's meter" today only as a parse stop-token; capturing its value + is a follow-up that would let this gate apply to that path too. + """ + pv_connection = epc.sap_energy_source.pv_connection + if isinstance(pv_connection, int): + return pv_connection == _PV_CONNECTION_CONNECTED_TO_DWELLING_METER + return True + + def _pv_generation_kwh_per_yr( epc: EpcPropertyData, climate: "int | PostcodeClimate", @@ -3170,7 +3206,13 @@ def _pv_generation_kwh_per_yr( roof area" PV figure (no detailed kWp): synthesize a single PV array with kWp = 0.12 × PV area, South orientation, 30° pitch, Modest overshading. + + Returns 0 when the array is not connected to the dwelling's own meter + (`_pv_connected_to_dwelling_meter` — gov-API `pv_connection=1`), per + RdSAP 10 §11.1 / SAP 10.2 Appendix M. """ + if not _pv_connected_to_dwelling_meter(epc): + return 0.0 arrays = epc.sap_energy_source.photovoltaic_arrays if not arrays: arrays = _synthesize_pv_arrays_from_percent_roof_area(epc) @@ -3217,7 +3259,13 @@ def _pv_monthly_generation_kwh( ) -> tuple[float, ...]: """SAP 10.2 Appendix M1 §2 (p.92) — monthly E_PV summed across all PV arrays. Annual sum matches `_pv_generation_kwh_per_yr` to - float precision.""" + float precision. + + Returns all-zero when the array is not connected to the dwelling's own + meter (`_pv_connected_to_dwelling_meter`), so the §10a cost split and the + CO2 / PE cascades all see no PV — mirroring the annual helper's gate.""" + if not _pv_connected_to_dwelling_meter(epc): + return (0.0,) * 12 arrays = epc.sap_energy_source.photovoltaic_arrays if not arrays: arrays = _synthesize_pv_arrays_from_percent_roof_area(epc) diff --git a/tests/domain/modelling/test_solar_recommendation.py b/tests/domain/modelling/test_solar_recommendation.py index f858a78b..43337ab5 100644 --- a/tests/domain/modelling/test_solar_recommendation.py +++ b/tests/domain/modelling/test_solar_recommendation.py @@ -89,6 +89,10 @@ def test_each_option_overlay_installs_per_segment_arrays_and_ensures_export() -> assert overlay is not None assert overlay.is_dwelling_export_capable is True assert overlay.pv_diverter_present is True + # A newly-installed recommended array is connected to the dwelling's own + # meter, so it must be tagged pv_connection=2 ("connected") — the value + # the SAP cascade credits. (1 = present-but-not-connected → zero credit.) + assert overlay.pv_connection == 2 arrays = overlay.photovoltaic_arrays assert arrays is not None and len(arrays) >= 1 assert all(isinstance(a, PhotovoltaicArray) for a in arrays) From 9927169e231a28acb043ef77ad41b22fffaae61d Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 29 Jun 2026 13:43:30 +0000 Subject: [PATCH 5/8] =?UTF-8?q?test(accuracy):=20ratchet=20SAP=20gauge=20a?= =?UTF-8?q?fter=20PV=20connection=20gate=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit within-0.5 floor 0.73->0.74 (now 0.741), MAE ceiling 0.762->0.740 (now 0.7397) on the fixed RdSAP-21.0.1 corpus. Log entry appended. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../epc_client/test_sap_accuracy_corpus.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/infrastructure/epc_client/test_sap_accuracy_corpus.py b/tests/infrastructure/epc_client/test_sap_accuracy_corpus.py index 496ddd31..cc57d28d 100644 --- a/tests/infrastructure/epc_client/test_sap_accuracy_corpus.py +++ b/tests/infrastructure/epc_client/test_sap_accuracy_corpus.py @@ -193,7 +193,7 @@ _CORPUS = Path( # within-0.5 71.6% -> 72.5%, MAE 0.819 -> 0.815. Surfaced by Khalim's Elmhurst # stress worksheet (simulated case 46): closed its last ventilation residual # (our Jan ACH 9.14 -> 9.0748 exact; SAP 29 -> 30 = accredited Elmhurst). -_MIN_WITHIN_HALF_SAP = 0.73 +_MIN_WITHIN_HALF_SAP = 0.74 # 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, @@ -238,7 +238,17 @@ _MIN_WITHIN_HALF_SAP = 0.73 # already computes 74. roof_insulation_location="ND" ⟺ party ceiling separates # the corpus classes with zero disagreement (all 190 party flats lodge "ND"); # the 4 mid/ground-floor flats this exposes all move toward lodged, 0 away. -_MAX_SAP_MAE = 0.762 +# Then 0.761 -> 0.740 (within-0.5 73.6% -> 74.1%) via the PV dwelling-meter +# connection gate (RdSAP 10 §11.1 / SAP 10.2 Appendix M): PV is credited to the +# dwelling only when gov-API `pv_connection == 2` ("connected to the dwelling's +# meter"); == 1 ("present but NOT connected" — communal / separately metered) +# now contributes zero to cost/CO2/PE. All 5 pv_connection=1 PV certs move +# inside ±0.5 (e.g. 100051118081 +6.5 -> +0.5); pv_connection=2 certs (52) keep +# their credit (corpus MAE 0.98 with vs 10.29 without). Khalim's Elmhurst proof: +# an identical dwelling rates SAP 87 with "Connected to Dwelling = Yes" (credit +# -£167) vs SAP 74 with "No" (credit £0). Enum decoded empirically: 0 = no PV, +# 1 = not connected, 2 = connected (the gov-API does not expose it elsewhere). +_MAX_SAP_MAE = 0.740 _MAX_CO2_MAE_TONNES = 0.09 # t CO2 / yr vs co2_emissions_current _MAX_PE_PER_M2_MAE = 3.5 # kWh / m2 / yr vs energy_consumption_current From 08d9e46481e113873e7347fe1e866fe8a48e87ec Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 29 Jun 2026 13:50:01 +0000 Subject: [PATCH 6/8] =?UTF-8?q?docs(pv):=20document=20gov-API=20pv=5Fconne?= =?UTF-8?q?ction=20enum=20on=20the=20domain=20field=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0 = no PV, 1 = not connected to dwelling meter, 2 = connected. Decoded empirically from the corpus + accredited Elmhurst (the gov RdSAP schema types it as a bare int). Points readers to the crediting gate. Co-Authored-By: Claude Opus 4.8 (1M context) --- datatypes/epc/domain/epc_property_data.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/datatypes/epc/domain/epc_property_data.py b/datatypes/epc/domain/epc_property_data.py index 17437f6f..895e4ab3 100644 --- a/datatypes/epc/domain/epc_property_data.py +++ b/datatypes/epc/domain/epc_property_data.py @@ -370,7 +370,12 @@ class SapEnergySource: wind_turbines_terrain_type: str # int in API, str (e.g. "Suburban") in site notes electricity_smart_meter_present: bool - pv_connection: Optional[Union[int, str]] = None # int from API; str from site notes + # gov-API enum (int): 0 = no PV, 1 = PV present but NOT connected to the + # dwelling's own electricity meter (communal / separately metered), 2 = PV + # connected to the dwelling's meter. Per RdSAP 10 §11.1 / SAP 10.2 Appendix M, + # PV is credited to the dwelling's SAP only when connected (== 2); see + # `_pv_connected_to_dwelling_meter` in cert_to_inputs. str from site notes. + pv_connection: Optional[Union[int, str]] = None photovoltaic_supply: Optional[PhotovoltaicSupply] = None photovoltaic_arrays: Optional[List[PhotovoltaicArray]] = None wind_turbine_details: Optional[WindTurbineDetails] = None From b1406bb48238e2fec3401bdd654b9a6dc9585e45 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 29 Jun 2026 14:03:23 +0000 Subject: [PATCH 7/8] Skip re-saving EPC data when it was read from DB unchanged Co-Authored-By: Claude Sonnet 4.6 --- applications/modelling_e2e/handler.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/applications/modelling_e2e/handler.py b/applications/modelling_e2e/handler.py index c62fad73..bf5e6110 100644 --- a/applications/modelling_e2e/handler.py +++ b/applications/modelling_e2e/handler.py @@ -166,7 +166,9 @@ class _PropertyWrite: scenario_id: int is_default: bool lodged_epc: Optional[EpcPropertyData] + lodged_epc_is_new: bool predicted_epc: Optional[EpcPropertyData] + predicted_epc_is_new: bool spatial: Optional[SpatialReference] solar: Optional[_SolarWrite] plan: Plan @@ -185,13 +187,13 @@ def _flush_writes(engine: Engine, writes: list[_PropertyWrite]) -> None: before a write is ever queued.""" with PostgresUnitOfWork(lambda: Session(engine)) as uow: for w in writes: - if w.lodged_epc is not None: + if w.lodged_epc is not None and w.lodged_epc_is_new: uow.epc.save( w.lodged_epc, property_id=w.property_id, portfolio_id=w.portfolio_id, ) - elif w.predicted_epc is not None: + elif w.predicted_epc is not None and w.predicted_epc_is_new: # Persist the synthesised EPC in the predicted slot (ADR-0031), so # the Baseline stage can re-hydrate it and downstream sees the # picture the Plan was modelled from. @@ -542,8 +544,10 @@ def handler( ) stored_lodged = stored_lodged_epcs.get(pid) + lodged_epc_is_new = False if refetch_epc: epc: Optional[EpcPropertyData] = epc_client.get_by_uprn(uprn) + lodged_epc_is_new = epc is not None elif stored_lodged is not None: logger.info( f"property={pid} using stored lodged EPC (refetch_epc=False)" @@ -555,6 +559,7 @@ def handler( ) overrides = overlays_from(overrides_reader.overrides_for(pid)) predicted_epc: Optional[EpcPropertyData] = None + predicted_epc_is_new = False if epc is not None: logger.info(f"property={pid} lodged EPC found") @@ -588,6 +593,7 @@ def handler( broaden=_broaden, predictor=predictor, ) + predicted_epc_is_new = True effective_epc = Property( identity=PropertyIdentity( portfolio_id=portfolio_id, @@ -649,8 +655,8 @@ def handler( # Queue this Property's writes rather than committing now — the # whole batch is persisted in one Unit of Work after the loop - # (see _flush_writes). The EPC is saved in its lodged or predicted - # slot (ADR-0031) at flush time depending on which is set here. + # (see _flush_writes). The *_is_new flags gate EPC saves so that + # EPCs read from DB unchanged are not re-written. accumulated.append( _PropertyWrite( property_id=pid, @@ -659,7 +665,9 @@ def handler( scenario_id=scenario_id, is_default=scenario.is_default, lodged_epc=epc, + lodged_epc_is_new=lodged_epc_is_new, predicted_epc=predicted_epc, + predicted_epc_is_new=predicted_epc_is_new, spatial=spatial, solar=solar_write, plan=plan, From 0718533d3a42ed786f77e4764e8d34f78455b82b Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 29 Jun 2026 14:09:24 +0000 Subject: [PATCH 8/8] turn on rds performance insights --- deployment/terraform/shared/main.tf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deployment/terraform/shared/main.tf b/deployment/terraform/shared/main.tf index bca65bb3..3ff5bda4 100644 --- a/deployment/terraform/shared/main.tf +++ b/deployment/terraform/shared/main.tf @@ -81,6 +81,10 @@ resource "aws_db_instance" "default" { publicly_accessible = true # Specify the CA certificate with the default RDS CA certificate ca_cert_identifier = "rds-ca-rsa2048-g1" + + # Performance Insights (7-day retention is free) + performance_insights_enabled = true + performance_insights_retention_period = 7 # Temporary to enfore immediate change apply_immediately = true # Set up storage type to gp3 for better performance