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>
5.2 KiB
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 thePlanningRestrictionsvalue 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:
@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_typeonly 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
EpcSimulationgrows a third overlay surface (windows), and the applicator gains its first nested write (intoWindowTransmissionDetails) — 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
PlanningRestrictionsthreaded in — the same input the solid-wall generator already takes.