mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
docs: glazing eligibility, overlay + pricing design (ADR-0022 + CONTEXT)
Grill-with-docs outcome for the glazing generator. Single planning-picked Measure (double when unrestricted, secondary for conservation/listed/heritage), upgrading all single-glazed windows together. The overlay writes lodged U-value + solar-g directly into WindowTransmissionDetails (our calculator consumes those as inputs — it does NOT derive them from glazing_type, unlike Elmhurst) plus glazing_type for the §5 daylight factor; EpcSimulation gains a per-window `windows` surface. Priced flat average-per-window × single-glazed count. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1a807a4c4c
commit
0846b61304
2 changed files with 41 additions and 0 deletions
|
|
@ -246,6 +246,10 @@ _Avoid_: restricted measures (legacy collapsed conservation/listed/heritage into
|
|||
The rule fixing which single roof Measure the main-roof **Recommendation** offers, by roof type. One Measure per roof — never a menu the Optimiser chooses between (ADR-0021): **pitched with an accessible loft** (incl. **thatch** — the covering doesn't block insulating the loft floor) → **loft insulation** (laid flat at the ceiling joists, 300 mm); **pitched with a sloping ceiling** → **sloping-ceiling insulation** (at the rafters, 100 mm); **flat roof** → **flat-roof insulation** (200 mm); **pitched, no access** → none (can't reach the void). A **room-in-roof** takes neither loft nor sloping-ceiling insulation — it is insulated at its own slopes/stud-walls (rafter-area, not floor-area, quantity) as a distinct **room-in-roof insulation** Measure, currently **deferred** (pending retrofit-specialist examples). A Measure is offered only when the roof is genuinely uninsulated ("As Built" / "None" / 0 mm).
|
||||
_Avoid_: "roof insulation" (name the specific Measure — loft / sloping-ceiling / flat-roof / room-in-roof); "joist insulation" (use **loft insulation**, the established Measure Type)
|
||||
|
||||
**Glazing Eligibility**:
|
||||
The rule fixing the single glazing Measure the **Windows** Recommendation offers. We upgrade **only single-glazed windows** (a pragmatic scope — already-double/secondary/triple windows are left alone), **all of them together** as one Measure. Planning status **hard-picks** the Measure (not a choice the Optimiser makes — ADR-0022): unrestricted → **double glazing** (replace the units); a **conservation area** / **listed** / **heritage** building → **secondary glazing** (an internal second pane, since the external units can't be replaced). Priced at a flat **average price per window** × the count of single-glazed windows (we have per-window areas but no size-varying prices, so size is ignored). When the dwelling has no single-glazed windows, no Recommendation is offered.
|
||||
_Avoid_: "windows" as a Measure (name **double glazing** / **secondary glazing**); pricing glazing by area (it's per-window count × average)
|
||||
|
||||
### Valuation
|
||||
|
||||
**Property Valuation**:
|
||||
|
|
|
|||
37
docs/adr/0022-glazing-eligibility-and-overlay.md
Normal file
37
docs/adr/0022-glazing-eligibility-and-overlay.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Glazing Eligibility, Overlay, and Pricing
|
||||
|
||||
The glazing Recommendation Generator offers a windows upgrade. Three decisions are load-bearing and non-obvious: how planning status picks the Measure, how the Simulation Overlay models "replace the windows" (which is *not* what you'd guess from how Elmhurst works), and how the upgrade is priced.
|
||||
|
||||
## Decision
|
||||
|
||||
**Single dispatching generator, one planning-picked Measure, all single-glazed windows together.** `recommend_glazing(epc, products, restrictions)` emits one "Windows" Recommendation with a **single** Measure Option:
|
||||
|
||||
- **Scope:** only **single-glazed** windows (`glazing_type == 1`, SAP10.2 Table U2 "Single"; the Elmhurst mapper sends `"Single"`/`"Single glazing"` → 1 and the API passes int 1, so it's path-consistent). Already-double/secondary/triple windows are left untouched. No single-glazed windows → no Recommendation (this subsumes legacy's "skip if already well-glazed").
|
||||
- **Planning hard-picks the Measure** (not competing Options for the Optimiser, unlike wall EWI/IWI in ADR-0019): unrestricted → `double_glazing`; any of conservation-area / listed / heritage → `secondary_glazing` (an internal second pane — the external units can't be replaced on a protected/over-looked building). Reuses the `PlanningRestrictions` value object from the wall work.
|
||||
- **All single-glazed windows in one overlay**, upgraded as a block — the Optimiser takes or leaves the whole glazing job, matching how it's quoted/installed.
|
||||
|
||||
**The overlay writes *lodged U-values and g-values directly*, because our calculator consumes them as inputs — it does not derive them from the glazing type.** This is the surprising part. Elmhurst *derives* a window's U-value from its glazing type as a UI convenience, then lodges the resulting U and g as manufacturer data on the cert. Our calculator, when every window carries a per-window `WindowTransmissionDetails.u_value` (the manufacturer-lodged case — `heat_transmission.py:490`), reads **each window's lodged `u_value` directly** for heat loss and its lodged `solar_transmittance` for gains (`solar_gains.py:300`); `glazing_type` is *not* used for U or g in that path. So changing `glazing_type` alone would move nothing. The `WindowOverlay` therefore sets three fields:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class WindowOverlay: # all-optional partial of one SapWindow
|
||||
glazing_type: Optional[int] = None # → SapWindow.glazing_type (drives the §5 daylight g_L only)
|
||||
u_value: Optional[float] = None # → WindowTransmissionDetails.u_value (heat loss)
|
||||
solar_transmittance: Optional[float] = None # → WindowTransmissionDetails.solar_transmittance (gains)
|
||||
```
|
||||
|
||||
`EpcSimulation` gains a `windows: Mapping[int, WindowOverlay]` surface keyed by `sap_windows` index; `apply_simulations` folds `windows[i]` onto `sap_windows[i]`, routing `u_value`/`solar_transmittance` into its `WindowTransmissionDetails` (creating one if absent). `glazing_type` is still set so the §5 daylight/lighting factor (`internal_gains.py:544`, which *does* key on the code) matches the after-cert at 1e-4. Target values are pinned from the Elmhurst before→after cert (cert 001431: double → U 4.80→1.40, g 0.85→0.72; secondary → the lodged "Secondary glazing" spec).
|
||||
|
||||
**Pricing: flat average price per window × count of single-glazed windows.** We now have per-window areas, but no size-varying prices, so size is ignored: `Cost.total = (number of single-glazed sap_windows entries) × product.unit_cost_per_m2`, reusing that field as a per-window price exactly as `mechanical_ventilation` reuses it per-unit. Double vs secondary price differently for free, via their distinct `measure_type` catalogue rows. One `sap_windows` entry counts as one window (deterministic; a grouped-row undercount is a catalogue calibration concern, not a model one).
|
||||
|
||||
## Considered options
|
||||
|
||||
- **Set `glazing_type` only and let the calculator derive U/g.** Rejected: our calculator reads the lodged per-window U/g directly when present (the manufacturer-lodged case), so a glazing-type-only overlay would leave the SAP unchanged. We must write the U/g.
|
||||
- **Offer double and secondary as competing Options** (like EWI/IWI). Rejected: planning *determines* the Measure, so there's no choice to optimise — a single planning-picked Option is honest.
|
||||
- **Per-window Recommendations / area-based pricing.** Rejected: glazing is quoted and installed as a whole-job block, and we have no size-varying prices — a flat per-window average over the single-glazed count is the faithful model.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `EpcSimulation` grows a third overlay surface (`windows`), and the applicator gains its first **nested** write (into `WindowTransmissionDetails`) — deeper than the flat building-part fold, but required because that's where the cascade reads U/g.
|
||||
- Cascade pins depend on the glazing-label mapper coverage (cert 001431 lodges several unmapped glazing labels — `"Secondary glazing"`, `"Secondary glazing - Normal emissivity"`, `"Triple pre 2002"`, truncated `"Double…"`/`"Triple…"` variants); those must be mapped before the before→after cert parses.
|
||||
- Because planning hard-picks the Measure, the glazing generator needs the Property's `PlanningRestrictions` threaded in — the same input the solid-wall generator already takes.
|
||||
Loading…
Add table
Reference in a new issue