mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-22 08:48:38 +00:00
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>
37 lines
5.2 KiB
Markdown
37 lines
5.2 KiB
Markdown
# 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.
|