mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
refactor(modelling): expose cascade_scores for the role-3 + bill cascade
Pull the cumulative-prefix scoring out of `marginal_impacts` into a reusable `cascade_scores(scorer, baseline, overlays) -> list[Score]` (index 0 the baseline, one calculator run per prefix) plus a pure `marginals_from_scores`. Each Score carries its SapResult, so the next slice's telescoping per-measure bill cascade can re-bill the same prefixes the role-3 attribution already scores — no extra `calculate` calls (ADR-0014 / ADR-0016). `marginal_impacts` now delegates; behaviour unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d36e42b582
commit
e79ffabfc5
2 changed files with 74 additions and 8 deletions
|
|
@ -34,17 +34,32 @@ class MeasureImpact:
|
||||||
energy_savings_kwh_per_yr: float
|
energy_savings_kwh_per_yr: float
|
||||||
|
|
||||||
|
|
||||||
def marginal_impacts(
|
def cascade_scores(
|
||||||
scorer: PackageScorer,
|
scorer: PackageScorer,
|
||||||
baseline: EpcPropertyData,
|
baseline: EpcPropertyData,
|
||||||
overlays: Sequence[EpcSimulation],
|
overlays: Sequence[EpcSimulation],
|
||||||
) -> list[MeasureImpact]:
|
) -> list[Score]:
|
||||||
"""Apply overlays cumulatively in order; return each one's marginal impact
|
"""Score the cumulative prefixes of `overlays` in order: index 0 is the
|
||||||
over the running state. The marginals telescope to the whole-package total."""
|
baseline (empty prefix), index i the state after the first i overlays. The
|
||||||
|
list has `len(overlays) + 1` entries — one calculator run each.
|
||||||
|
|
||||||
|
Each Score carries its `SapResult`, so the same cascade powers both the
|
||||||
|
role-3 marginal attribution (`marginals_from_scores`) and the telescoping
|
||||||
|
per-measure bill cascade — neither needs to re-score (ADR-0014 / ADR-0016)."""
|
||||||
|
return [
|
||||||
|
scorer.score(baseline, list(overlays[:prefix_length]))
|
||||||
|
for prefix_length in range(len(overlays) + 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def marginals_from_scores(scores: Sequence[Score]) -> list[MeasureImpact]:
|
||||||
|
"""Each measure's marginal impact from a precomputed cumulative-prefix
|
||||||
|
cascade (`scores[0]` is the baseline). Signed so positive is an improvement;
|
||||||
|
the marginals telescope to `scores[-1]` vs `scores[0]`."""
|
||||||
impacts: list[MeasureImpact] = []
|
impacts: list[MeasureImpact] = []
|
||||||
previous: Score = scorer.score(baseline, [])
|
for index in range(1, len(scores)):
|
||||||
for index in range(len(overlays)):
|
previous: Score = scores[index - 1]
|
||||||
current: Score = scorer.score(baseline, list(overlays[: index + 1]))
|
current: Score = scores[index]
|
||||||
impacts.append(
|
impacts.append(
|
||||||
MeasureImpact(
|
MeasureImpact(
|
||||||
sap_points=current.sap_continuous - previous.sap_continuous,
|
sap_points=current.sap_continuous - previous.sap_continuous,
|
||||||
|
|
@ -55,10 +70,19 @@ def marginal_impacts(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
previous = current
|
|
||||||
return impacts
|
return impacts
|
||||||
|
|
||||||
|
|
||||||
|
def marginal_impacts(
|
||||||
|
scorer: PackageScorer,
|
||||||
|
baseline: EpcPropertyData,
|
||||||
|
overlays: Sequence[EpcSimulation],
|
||||||
|
) -> list[MeasureImpact]:
|
||||||
|
"""Apply overlays cumulatively in order; return each one's marginal impact
|
||||||
|
over the running state. The marginals telescope to the whole-package total."""
|
||||||
|
return marginals_from_scores(cascade_scores(scorer, baseline, overlays))
|
||||||
|
|
||||||
|
|
||||||
def independent_option_impacts(
|
def independent_option_impacts(
|
||||||
scorer: PackageScorer,
|
scorer: PackageScorer,
|
||||||
baseline: EpcPropertyData,
|
baseline: EpcPropertyData,
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,10 @@ from domain.modelling.scoring.package_scorer import PackageScorer, Score
|
||||||
from domain.modelling.recommendation import MeasureOption
|
from domain.modelling.recommendation import MeasureOption
|
||||||
from domain.modelling.scoring.scoring import (
|
from domain.modelling.scoring.scoring import (
|
||||||
MeasureImpact,
|
MeasureImpact,
|
||||||
|
cascade_scores,
|
||||||
independent_option_impacts,
|
independent_option_impacts,
|
||||||
marginal_impacts,
|
marginal_impacts,
|
||||||
|
marginals_from_scores,
|
||||||
)
|
)
|
||||||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||||
from domain.sap10_calculator.calculator import Sap10Calculator
|
from domain.sap10_calculator.calculator import Sap10Calculator
|
||||||
|
|
@ -113,6 +115,46 @@ def test_single_overlay_marginal_is_its_improvement_over_baseline() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cascade_scores_returns_the_baseline_plus_one_score_per_prefix() -> None:
|
||||||
|
# Arrange
|
||||||
|
baseline: EpcPropertyData = build_epc()
|
||||||
|
scorer = _CountingScorer()
|
||||||
|
overlays = [_MAIN_CAVITY, _EXT1_CAVITY]
|
||||||
|
|
||||||
|
# Act
|
||||||
|
scores: list[Score] = cascade_scores(scorer, baseline, overlays)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
# baseline (empty prefix) + one score per cumulative prefix
|
||||||
|
assert len(scores) == 3
|
||||||
|
assert scorer.calls == 3
|
||||||
|
assert scores[0].sap_continuous == 0.0 # empty prefix
|
||||||
|
assert scores[1].sap_continuous == 2.0 # MAIN cavity (type 2)
|
||||||
|
assert scores[2].sap_continuous == 4.0 # + EXTENSION_1 cavity (type 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_marginals_from_scores_are_the_consecutive_prefix_deltas() -> None:
|
||||||
|
# Arrange
|
||||||
|
baseline: EpcPropertyData = build_epc()
|
||||||
|
scorer = PackageScorer(Sap10Calculator())
|
||||||
|
overlays = [_MAIN_CAVITY, _EXT1_CAVITY]
|
||||||
|
scores: list[Score] = cascade_scores(scorer, baseline, overlays)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
impacts: list[MeasureImpact] = marginals_from_scores(scores)
|
||||||
|
|
||||||
|
# Assert — each marginal is the delta over the previous prefix score
|
||||||
|
assert len(impacts) == 2
|
||||||
|
assert (
|
||||||
|
abs(impacts[0].sap_points - (scores[1].sap_continuous - scores[0].sap_continuous))
|
||||||
|
<= 1e-9
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
abs(impacts[1].sap_points - (scores[2].sap_continuous - scores[1].sap_continuous))
|
||||||
|
<= 1e-9
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_marginals_telescope_to_the_whole_package_total() -> None:
|
def test_marginals_telescope_to_the_whole_package_total() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
baseline: EpcPropertyData = build_epc()
|
baseline: EpcPropertyData = build_epc()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue