mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +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
|
||||
|
||||
|
||||
def marginal_impacts(
|
||||
def cascade_scores(
|
||||
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."""
|
||||
) -> list[Score]:
|
||||
"""Score the cumulative prefixes of `overlays` in order: index 0 is the
|
||||
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] = []
|
||||
previous: Score = scorer.score(baseline, [])
|
||||
for index in range(len(overlays)):
|
||||
current: Score = scorer.score(baseline, list(overlays[: index + 1]))
|
||||
for index in range(1, len(scores)):
|
||||
previous: Score = scores[index - 1]
|
||||
current: Score = scores[index]
|
||||
impacts.append(
|
||||
MeasureImpact(
|
||||
sap_points=current.sap_continuous - previous.sap_continuous,
|
||||
|
|
@ -55,10 +70,19 @@ def marginal_impacts(
|
|||
),
|
||||
)
|
||||
)
|
||||
previous = current
|
||||
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(
|
||||
scorer: PackageScorer,
|
||||
baseline: EpcPropertyData,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ from domain.modelling.scoring.package_scorer import PackageScorer, Score
|
|||
from domain.modelling.recommendation import MeasureOption
|
||||
from domain.modelling.scoring.scoring import (
|
||||
MeasureImpact,
|
||||
cascade_scores,
|
||||
independent_option_impacts,
|
||||
marginal_impacts,
|
||||
marginals_from_scores,
|
||||
)
|
||||
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
|
||||
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:
|
||||
# Arrange
|
||||
baseline: EpcPropertyData = build_epc()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue