feat(fase1): assessment-method encoding — persisted dimension-scoped example + validator rule (F1)

This commit is contained in:
Kjell Tore Guttormsen 2026-07-07 08:15:44 +02:00
commit 69ca508677
4 changed files with 171 additions and 0 deletions

View file

@ -37,6 +37,15 @@ MAX_SAVING_FRACTION = 0.30
"""Policy cap: at most 30% of an affected item's cost is realistically recoverable as a
saving. The LP bounds the feasible saving by this fraction."""
_ENERGY_METHOD_MEASURE = "energy_efficiency"
_ENERGY_METHOD_MAX_FRACTION = 0.15
"""Step 9 (SC7-B): the IPMVP Option A method-specific cap. Option A measures only the KEY parameter
and STIPULATES the rest (operating hours), so the defensibly-verifiable saving is more conservative
than the generic policy cap deliberately STRICTER than ``MAX_SAVING_FRACTION`` so this rule is an
INDEPENDENT gate: it can reject a proposal the generic P90 stage passes (not redundant). The concrete
fraction is calibrated against the reference domain; the CONDITION (a method-scoped stricter cap) is
the encoded rule. Returns the same ``Rejection`` type a validator stage, not a new gate."""
_MC_SAMPLES = 512
_MC_SEED = 20260624
@ -126,6 +135,21 @@ def validate_proposal(proposal: SavingsProposal) -> ValidatedProposal | Rejectio
proposal=proposal,
reason=f"claimed saving {proposal.claimed_saving_nok:.0f} exceeds P90 feasible {p90:.0f}",
)
# Stage 5 (Step 9, SC7-B): a method-specific rule STRICTER than the generic cap. A proposal in
# the energy method (IPMVP Option A) must clear a lower, method-scoped feasible — an INDEPENDENT
# gate that can reject a proposal the P90 stage passed. Same ``Rejection`` type, not a new gate.
if proposal.measure == _ENERGY_METHOD_MEASURE:
method_feasible = _ENERGY_METHOD_MAX_FRACTION * sum(
it.total for it in proposal.affected_items
)
if proposal.claimed_saving_nok > method_feasible:
return Rejection(
proposal=proposal,
reason=(
f"claimed {proposal.claimed_saving_nok:.0f} exceeds the {_ENERGY_METHOD_MEASURE} "
f"method cap {method_feasible:.0f} (stricter than the generic P90)"
),
)
return ValidatedProposal(proposal=proposal, p10=p10, p50=p50, p90=p90, nominal_feasible=nominal)