mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
feat(modelling): whole-dwelling LightingOverlay surface on EpcSimulation
Slice 1 of the lighting generator (ADR-0023): the first whole-dwelling, top-level overlay surface. LightingOverlay carries the four fixed-lighting bulb-count fields by their exact EPC names (all Optional, absolute counts) + EpcSimulation.lighting. The applicator's _fold_lighting writes the non-None counts directly onto the result EpcPropertyData by name (setattr) — simpler than ventilation's nested fold since the counts live top-level. Baseline unmutated; pyright strict clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b460f81233
commit
139c90c885
3 changed files with 81 additions and 0 deletions
|
|
@ -19,6 +19,7 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
)
|
||||
from domain.modelling.simulation import (
|
||||
EpcSimulation,
|
||||
LightingOverlay,
|
||||
VentilationOverlay,
|
||||
WindowOverlay,
|
||||
)
|
||||
|
|
@ -47,10 +48,22 @@ def apply_simulations(
|
|||
result.sap_ventilation = _fold_ventilation(
|
||||
result.sap_ventilation, simulation.ventilation
|
||||
)
|
||||
if simulation.lighting is not None:
|
||||
_fold_lighting(result, simulation.lighting)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _fold_lighting(epc: EpcPropertyData, overlay: LightingOverlay) -> None:
|
||||
"""Write a `LightingOverlay`'s non-``None`` bulb counts onto the (copied)
|
||||
dwelling's top-level fields by name — the four counts live directly on
|
||||
`EpcPropertyData`, so the fold writes onto it, not a nested object."""
|
||||
for overlay_field in fields(overlay):
|
||||
value = getattr(overlay, overlay_field.name)
|
||||
if value is not None:
|
||||
setattr(epc, overlay_field.name, value)
|
||||
|
||||
|
||||
def _fold_window(window: SapWindow, overlay: WindowOverlay) -> None:
|
||||
"""Write a `WindowOverlay`'s non-``None`` fields onto a (copied) window:
|
||||
``glazing_type`` flat on the window, ``u_value`` / ``solar_transmittance``
|
||||
|
|
|
|||
|
|
@ -65,6 +65,25 @@ class WindowOverlay:
|
|||
solar_transmittance: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LightingOverlay:
|
||||
"""All-optional partial of the dwelling's fixed-lighting bulb counts — the
|
||||
whole-dwelling lighting change a Measure Option makes (e.g. an all-LED
|
||||
upgrade — ADR-0023). Unlike a `BuildingPartOverlay` or `WindowOverlay` this
|
||||
targets no building part or window; its fields are the four **top-level**
|
||||
`EpcPropertyData` bulb counts, folded directly by name.
|
||||
|
||||
The counts are absolute target states, not deltas (an all-LED upgrade sets
|
||||
``led = total``, the rest 0). A `None` field means "leave the baseline value
|
||||
unchanged".
|
||||
"""
|
||||
|
||||
led_fixed_lighting_bulbs_count: Optional[int] = None
|
||||
cfl_fixed_lighting_bulbs_count: Optional[int] = None
|
||||
incandescent_fixed_lighting_bulbs_count: Optional[int] = None
|
||||
low_energy_fixed_lighting_bulbs_count: Optional[int] = None
|
||||
|
||||
|
||||
def _no_building_parts() -> dict[BuildingPartIdentifier, BuildingPartOverlay]:
|
||||
return {}
|
||||
|
||||
|
|
@ -85,3 +104,4 @@ class EpcSimulation:
|
|||
)
|
||||
windows: Mapping[int, WindowOverlay] = field(default_factory=_no_windows)
|
||||
ventilation: Optional[VentilationOverlay] = None
|
||||
lighting: Optional[LightingOverlay] = None
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
from domain.modelling.simulation import (
|
||||
BuildingPartOverlay,
|
||||
EpcSimulation,
|
||||
LightingOverlay,
|
||||
VentilationOverlay,
|
||||
WindowOverlay,
|
||||
)
|
||||
|
|
@ -219,3 +220,50 @@ def test_baseline_windows_are_not_mutated_by_a_window_overlay() -> None:
|
|||
assert (
|
||||
baseline.sap_windows[0].window_transmission_details.u_value == original_u
|
||||
)
|
||||
|
||||
|
||||
def test_apply_writes_dwelling_lighting_onto_top_level_bulb_counts() -> None:
|
||||
# Arrange — a whole-dwelling lighting change (no building part), e.g. an
|
||||
# all-LED upgrade folded onto the top-level bulb counts (ADR-0023).
|
||||
baseline: EpcPropertyData = build_epc()
|
||||
simulation = EpcSimulation(
|
||||
lighting=LightingOverlay(
|
||||
led_fixed_lighting_bulbs_count=8,
|
||||
cfl_fixed_lighting_bulbs_count=0,
|
||||
incandescent_fixed_lighting_bulbs_count=0,
|
||||
low_energy_fixed_lighting_bulbs_count=0,
|
||||
)
|
||||
)
|
||||
|
||||
# Act
|
||||
result: EpcPropertyData = apply_simulations(baseline, [simulation])
|
||||
|
||||
# Assert
|
||||
assert result.led_fixed_lighting_bulbs_count == 8
|
||||
assert result.cfl_fixed_lighting_bulbs_count == 0
|
||||
assert result.incandescent_fixed_lighting_bulbs_count == 0
|
||||
assert result.low_energy_fixed_lighting_bulbs_count == 0
|
||||
|
||||
|
||||
def test_baseline_lighting_is_not_mutated_by_a_lighting_overlay() -> None:
|
||||
# Arrange — 000490 lodges 8 low-energy-unknown bulbs, 0 LED.
|
||||
baseline: EpcPropertyData = build_epc()
|
||||
original_led: int = baseline.led_fixed_lighting_bulbs_count
|
||||
original_lel: int = baseline.low_energy_fixed_lighting_bulbs_count
|
||||
|
||||
# Act — fold an all-LED overlay (led = the 8 total).
|
||||
_: EpcPropertyData = apply_simulations(
|
||||
baseline,
|
||||
[
|
||||
EpcSimulation(
|
||||
lighting=LightingOverlay(
|
||||
led_fixed_lighting_bulbs_count=8,
|
||||
low_energy_fixed_lighting_bulbs_count=0,
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Assert — the baseline's counts are untouched.
|
||||
assert baseline.led_fixed_lighting_bulbs_count == original_led
|
||||
assert baseline.low_energy_fixed_lighting_bulbs_count == original_lel
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue