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) <noreply@anthropic.com>
Three corrections found by re-running property 742003 end-to-end:
- roofSegmentStats are POSITIONAL — real responses omit the segmentIndex field
the fixture happened to carry; key the centre/area lookup by array position.
- Base the cap on ground_floor_area (the footprint the roof covers), not the
greatest per-storey area; roof_area is the fallback.
- Clamp the basis by total_floor_area: predicted EPCs borrow the structural
template's geometry (742003: a 118.62 m² MAIN ground floor) decoupled from
the predicted 55 m² (ADR-0029), so without the clamp the cap reads the
template's larger footprint.
Result: 742003 plan A/92.4 (16 kWp) -> C/74.4 (6.4 kWp). 29 solar tests +
orchestration threading + products green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
select_conservative_configs now also caps panels by the dwelling's own usable
roof — min(0.70 × maxArrayPanelsCount, roof_area/cos(pitch) × 0.5 / panel_area)
— threaded from recommend_solar via roof_area(epc, MAIN) (ADR-0038). No-op on
correctly-matched homes; falls back to the Google cap when the EPC has no MAIN
part. Defeats Google footprint conflation (semi-detached/terraced).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce domain/modelling/measure_type.py — a StrEnum with one member per
modelled measure (the 15 the generators emit). A StrEnum so each member *is*
its string value: it persists straight into the `recommendation` varchar
column, is the optimiser's group-by key, and compares equal to the catalogue /
EPC strings — so it replaces the per-generator string constants with no
persistence or optimiser change.
Repoint every generator's measure-type constant/literal to a MeasureType
member (wall, solid_wall, roof, floor, glazing, lighting, ventilation,
heating, solar). Field annotations stay `str` for now; tightening them to
MeasureType is the next slice.
This is the enum the historical engine deferred (engine.py:970
"TODO - formalise property measure types into an enum") and the vocabulary the
forthcoming `considered_measures` allowlist will speak (mirroring the legacy
`inclusions`).
Suite green: tests/domain/modelling + orchestration + harness 253 pass + 3
xfail; pyright clean on the enum + generators.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slice 6 of the Solar PV Recommendation Generator (ADR-0026). `recommend_solar`
emits one "Solar PV" Recommendation of up to five conservatively-sized configs
× {no battery, battery} = ≤10 competing Options (a free Optimiser candidate).
Each Option folds a SolarOverlay built from the chosen config: one
PhotovoltaicArray per non-north segment (peak_power = panels × panelCapacityW /
1000; orientation/pitch from geometry; generation-calibrated overshading),
is_dwelling_export_capable set True absolutely, a diverter when the dwelling
has a cylinder (None for a combi), a 5 kWh battery for the battery variant, and
the per-config composite cost from Products.solar_bundle_cost.
Eligibility = house/bungalow ∧ not listed/heritage (blocks_internal, the same
gate as ASHP — a conservation area does NOT block PV) ∧ no existing PV ∧ a
feasible SolarPotential. Flats and existing-PV top-up are deferred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slice 4 of the Solar PV Recommendation Generator (ADR-0026).
`select_conservative_configs` turns Google's full solarPanelConfigs ladder
into up to five competing array configs for the Optimiser: drop north-facing
planes (within 30° of due north, wrap-aware), cap usable panels at ~70% of
maxArrayPanelsCount (imagery misses obstructions; MCS edge setback), collapse
rungs that trim to the same usable size keeping the higher-generation layout,
then sample five spanning min→max by expected generation. Returns () when
nothing usable remains.
Real London example → 5 rungs at 4/12/19/26/34 panels (all ≤34.3 = 70% of
49); synthetic cases pin the north-drop and the 70% cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slice 3 of the Solar PV Recommendation Generator (ADR-0026). Per roof segment,
back-solve the effective overshading factor ZPV from Google's expected
generation against SAP's own unshaded annual output:
ZPV = (yearlyEnergyDcKwh × 0.955) / (0.8 × kWp × S)
reusing the calculator's Appendix U3.3 annual solar radiation S via a new
public seam `pv_annual_solar_radiation_kwh_per_m2`. Dividing Google's
generation by SAP's S cancels orientation/tilt and isolates shading; the
result snaps to the RdSAP bucket {1:1.0, 2:0.8, 3:0.5, 4:0.35} via the
ADR-0026 midpoint cutpoints (≥0.90→1, 0.65–0.90→2, 0.425–0.65→3, <0.425→4;
ZPV>1→1). The real London example's planes all back-solve to ZPV>1 → code 1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>