mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
docs: ADR-0038 + glossary — bound Solar PV array to the dwelling's own roof
ADR-0026 sized PV at 0.70 x Google maxArrayPanelsCount, assuming Google's roof is the dwelling's. Medium-imagery footprint conflation (semi-detached/terraced) returns the whole building's roof -> oversized arrays (property 742003: 55m2 bungalow -> ~16 kWp -> effective E/49.7 -> A/92.4 on solar alone). ADR-0038 bounds the array to the dwelling's own usable roof (roof_area-based budget, proximity segment selection anchored on the dwelling), subsuming the legacy GoogleSolarApi guards the rewrite dropped. CONTEXT: add Footprint Conflation + Dwelling-Roof Cap, extend Solar Potential (per-segment centre + area). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
56b365f488
commit
58f6b868a7
2 changed files with 90 additions and 2 deletions
12
CONTEXT.md
12
CONTEXT.md
|
|
@ -281,11 +281,19 @@ The catalogue classification of a retrofit measure (e.g. `solar_pv`, `loft_insul
|
|||
_Avoid_: measure (ambiguous), category
|
||||
|
||||
**Solar Potential**:
|
||||
The typed domain projection over a Property's Google Solar `buildingInsights` response (fetched by **Ingestion**, persisted whole as JSONB, read by Modelling — never re-fetched). It carries the per-**roof-segment** geometry the PV simulation needs — `azimuthDegrees` (→ SAP orientation octant), `pitchDegrees` (→ SAP pitch), per-segment panel counts, panel capacity, and `sunshineQuantiles` (→ overshading) — plus the candidate panel layouts (`solarPanelConfigs`). It is the source of the **PV Overlay**: the solar Recommendation Generator reads the Solar Potential (NOT the EPC's `photovoltaic_arrays`, which is the dwelling's *existing* PV) and builds competing PV Options from it. Distinct from existing/installed PV.
|
||||
The typed domain projection over a Property's Google Solar `buildingInsights` response (fetched by **Ingestion**, persisted whole as JSONB, read by Modelling — never re-fetched). It carries the per-**roof-segment** geometry the PV simulation needs — `azimuthDegrees` (→ SAP orientation octant), `pitchDegrees` (→ SAP pitch), per-segment panel counts, **per-segment centre + area** (for the **Dwelling-Roof Cap**), panel capacity, and `sunshineQuantiles` (→ overshading) — plus the candidate panel layouts (`solarPanelConfigs`). It is the source of the **PV Overlay**: the solar Recommendation Generator reads the Solar Potential (NOT the EPC's `photovoltaic_arrays`, which is the dwelling's *existing* PV) and builds competing PV Options from it. Distinct from existing/installed PV.
|
||||
_Avoid_: solar config (ambiguous — the API response vs the chosen array set), Google insights (the raw JSON, not the typed projection)
|
||||
|
||||
**Footprint Conflation**:
|
||||
When Google Solar (typically on medium-quality imagery, and most often for **semi-detached / terraced** homes) reports the **whole building's** roof for a single dwelling — merging the target with its attached neighbour — so the **Solar Potential** describes more roof than the dwelling owns. The failure mode the **Dwelling-Roof Cap** corrects; without it the array is sized to two or more homes.
|
||||
_Avoid_: duplicate surfaces (the legacy heuristic's name, not the concept)
|
||||
|
||||
**Dwelling-Roof Cap**:
|
||||
The bound that limits a **Solar PV Recommendation**'s array to the dwelling's *own* usable roof, defeating **Footprint Conflation** (ADR-0038). A surface-area budget — the EPC's RdSAP §3.8 roof plan area (`roof_area`) ÷ cos(pitch) × a usable fraction (~0.5, one non-north plane) — caps the panel count via `min(budget, 0.70 × maxArrayPanelsCount)`, so it is a **no-op on correctly-matched homes** and only bites under conflation. Segments are ranked by proximity to the **dwelling's** own lat/long (not Google's building centre) to keep the home's roof and drop the neighbour's. Used only for **sizing** — distinct from **Solar PV Eligibility**, which still uses the real Solar Potential, never a roof-area-from-floor-area estimate.
|
||||
_Avoid_: roof-area haircut (overloads the MCS coverage haircut), double-property halving (the legacy semi-detached-only mechanism this replaces)
|
||||
|
||||
**Solar PV Recommendation**:
|
||||
The single "Solar PV" **Recommendation** for a Property, carrying competing whole-array **Measure Options** built from the **Solar Potential** (ADR-0026). Up to **five conservatively-sized array configs** (capped, ranked by energy generation as the size-suitability proxy; deliberately *not* full-roof — imagery can miss obstructions, so a coverage/edge-setback haircut applies per MCS), each offered **with and without a battery** (≤ 10 Options). Each Option is priced at a **single price point** from the rate sheet (kWp band + scaffolding-by-elevation + optional battery/diverter) — no multi-product price variants. The PV Overlay sets `photovoltaic_arrays` (one per segment: peak_power, orientation, pitch, overshading) plus `pv_diverter_present`, `pv_connection`, and **`is_dwelling_export_capable=True`** (an export meter is ensured post-install); the battery variant adds `pv_batteries`.
|
||||
The single "Solar PV" **Recommendation** for a Property, carrying competing whole-array **Measure Options** built from the **Solar Potential** (ADR-0026). Up to **five conservatively-sized array configs** (capped, ranked by energy generation as the size-suitability proxy; deliberately *not* full-roof — imagery can miss obstructions, so a coverage/edge-setback haircut applies per MCS, and the **Dwelling-Roof Cap** bounds the array to the dwelling's own roof under **Footprint Conflation**), each offered **with and without a battery** (≤ 10 Options). Each Option is priced at a **single price point** from the rate sheet (kWp band + scaffolding-by-elevation + optional battery/diverter) — no multi-product price variants. The PV Overlay sets `photovoltaic_arrays` (one per segment: peak_power, orientation, pitch, overshading) plus `pv_diverter_present`, `pv_connection`, and **`is_dwelling_export_capable=True`** (an export meter is ensured post-install); the battery variant adds `pv_batteries`.
|
||||
_Avoid_: solar bundle (PV is competing sized Options, not one fixed bundle like ASHP)
|
||||
|
||||
**Solar PV Eligibility**:
|
||||
|
|
|
|||
80
docs/adr/0038-solar-pv-bounded-to-dwelling-roof.md
Normal file
80
docs/adr/0038-solar-pv-bounded-to-dwelling-roof.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
status: accepted (extends ADR-0026)
|
||||
---
|
||||
|
||||
# Solar PV array is bounded to the dwelling's own roof
|
||||
|
||||
ADR-0026 sized a PV array at **0.70 × Google `maxArrayPanelsCount`**, implicitly
|
||||
assuming Google's roof *is* the dwelling's roof. On medium-quality imagery —
|
||||
common for **semi-detached and terraced** homes — Google conflates the dwelling
|
||||
with its neighbour and returns the **whole building's** roof, so the array is
|
||||
sized to two (or more) homes. Property 742003 (portfolio 812) is a **55 m²
|
||||
bungalow** but Google reports a 158.8 m² roof / 363 m² footprint → a ~16 kWp
|
||||
array → the plan jumps effective **E/49.7 → A/92.4 on solar alone**. We now
|
||||
**bound the array to the dwelling's own usable roof**, derived from the EPC, so
|
||||
Google can only ever influence the array *within* the dwelling's physical roof.
|
||||
|
||||
## Decision
|
||||
|
||||
**A dwelling-roof area budget caps the array, and segment selection is anchored
|
||||
on the dwelling, not Google's building centre.**
|
||||
|
||||
- **Budget** = `roof_area(epc, MAIN) / cos(pitch) × USABLE_FRACTION`
|
||||
(`USABLE_FRACTION = 0.5` — one usable, non-north plane minus setbacks /
|
||||
obstructions; a documented, tunable constant alongside the existing
|
||||
`_USABLE_PANEL_FRACTION = 0.70`). `roof_area` is the RdSAP §3.8 plan area
|
||||
(`domain/building_geometry.py`); dividing by `cos(pitch)` converts it to the
|
||||
pitched-surface units Google reports.
|
||||
- **Final cap** = `min(area-budget panels, 0.70 × maxArrayPanelsCount)`. This is
|
||||
a **no-op on correctly-matched homes** (Google's roof ≈ the dwelling's, so the
|
||||
area budget is ≳ what Google offers) — it only bites when Google's roof
|
||||
materially exceeds the dwelling's, i.e. conflation.
|
||||
- **Trim unit = panels** (sub-segment partial fills allowed) — the budget is
|
||||
routinely smaller than a single roof segment (~33 m² here, vs a 40.5 m²
|
||||
segment).
|
||||
- **Segment selection by proximity:** rank Google's roof segments by distance
|
||||
from the **dwelling's own lat/long** (`property_details_spatial`, *not*
|
||||
Google's building `center`, which sits at the conflated centroid), keep the
|
||||
home's roof and drop the neighbour's, then fill the budget from the home's
|
||||
segments **by generation** (so a near-but-NE plane doesn't win over the home's
|
||||
SW plane). This requires the `SolarPotential` projection to carry each
|
||||
segment's **centre + area** (from `roofSegmentStats`, keyed by
|
||||
`segment_index`), which it previously dropped.
|
||||
- **Fallback:** when `roof_area` is missing or absurd, fall back to the existing
|
||||
0.70-Google cap — never emit a zero-panel or unbounded array.
|
||||
|
||||
## Considered options
|
||||
|
||||
- **Port the legacy `GoogleSolarApi.py` guards as-is** (`ROOF_AREA_TOLERANCE`
|
||||
correction, `exclude_likely_duplicate_surfaces` + `double_property` halving,
|
||||
`PERCENTAGE_OF_ROOF_LIMIT`). Rejected: fragile and case-specific — the
|
||||
duplicate-surface heuristic only fires for `built_form == "Semi-Detached" and
|
||||
extension_count == 0` (it would not fire for *this* bungalow), and relies on
|
||||
azimuth-similarity + distance-to-Google-centre tolerances. A single physical
|
||||
budget (this ADR) subsumes all three guards.
|
||||
- **OS building-footprint polygon clipping now** — geometrically intersect
|
||||
Google's segments with the dwelling's MasterMap outline. Deferred: no GIS
|
||||
footprint data is ingested today (we have OS *point* coordinates only). Logged
|
||||
as the principled future enhancement — it fixes *which* segments robustly and
|
||||
would let us delete the proximity heuristic entirely.
|
||||
- **Fill the budget strictly nearest-first.** Rejected: the nearest segment can
|
||||
be poorly oriented (here the nearest is NE). Within the home's own segments we
|
||||
fill by generation.
|
||||
- **Bound directly on Google's per-segment `areaMeters2`** instead of the EPC
|
||||
roof. Rejected as the primary basis: those areas are themselves the conflated
|
||||
roof; the EPC `roof_area` is the dwelling-scale ground truth. (We still read
|
||||
the segment areas — to convert the budget into a panel count per segment.)
|
||||
|
||||
## Consequences
|
||||
|
||||
- `SolarPotential` / `SolarRoofSegment` grow a per-segment **centre + area**;
|
||||
the Google `buildingInsights` → `SolarPotential` projection maps them from
|
||||
`roofSegmentStats`. `recommend_solar` / `select_conservative_configs` thread
|
||||
the dwelling's `roof_area` + anchor coordinate.
|
||||
- **SAP scores fall on conflated homes** (the intended correction); correctly
|
||||
matched homes are unchanged. Regression-anchored on property 742003
|
||||
(~16 kWp → ~6.5 kWp).
|
||||
- **Out of scope (separate ticket):** the solar bundle cost is flat (£6,550)
|
||||
across 5.2–16 kWp, so the optimiser had no cost reason to prefer a smaller
|
||||
array — with sizing bounded this stops mattering for the conflation case, but
|
||||
the cost curve saturating is its own issue.
|
||||
Loading…
Add table
Reference in a new issue