53 lines
2.5 KiB
Python
53 lines
2.5 KiB
Python
"""Pure score arithmetic. No model types, game rules, I/O, or policy thresholds.
|
|
|
|
Invalid or incomplete inputs raise ValueError. Callers own evidence checks and
|
|
fallbacks; missing evidence must never become a neutral score.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from collections.abc import Mapping
|
|
|
|
|
|
def _finite(value: float) -> bool:
|
|
return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)
|
|
|
|
|
|
def normalize_score(value: float, minimum: float, neutral: float, maximum: float) -> float:
|
|
"""Map an ordered scale to [-1, 1], with neutral at zero.
|
|
|
|
Each side is linear. Equal score intervals are a modeling assumption, not
|
|
a calibrated measure of game value. Values outside the scale are rejected.
|
|
"""
|
|
if not all(_finite(v) for v in (value, minimum, neutral, maximum)):
|
|
raise ValueError("score and scale must be finite numbers")
|
|
if not minimum < neutral < maximum:
|
|
raise ValueError("scale must satisfy minimum < neutral < maximum")
|
|
if not minimum <= value <= maximum:
|
|
raise ValueError("score is outside the scale")
|
|
span = maximum - neutral if value >= neutral else neutral - minimum
|
|
return (value - neutral) / span
|
|
|
|
|
|
def weighted_utility(components: Mapping[str, float], weights: Mapping[str, float]) -> float:
|
|
"""Combine complete normalized components using explicit nonnegative weights.
|
|
|
|
Weights must sum to one. Do not silently remove missing components or
|
|
renormalize weights. Confidence is not a component or a utility multiplier.
|
|
"""
|
|
if not weights or components.keys() != weights.keys():
|
|
raise ValueError("components must exactly match nonempty weights")
|
|
if any(not _finite(w) or w < 0 for w in weights.values()):
|
|
raise ValueError("weights must be finite and nonnegative")
|
|
if not math.isclose(sum(weights.values()), 1.0, rel_tol=0, abs_tol=1e-9):
|
|
raise ValueError("weights must sum to one")
|
|
if any(not _finite(v) or not -1 <= v <= 1 for v in components.values()):
|
|
raise ValueError("components must be finite normalized scores")
|
|
return sum(weights[axis] * components[axis] for axis in weights)
|
|
|
|
|
|
def rank_candidates(utilities: Mapping[str, float]) -> list[str]:
|
|
"""Rank highest utility first; ties retain input order. Empty input is valid."""
|
|
if any(not _finite(value) for value in utilities.values()):
|
|
raise ValueError("candidate utilities must be finite numbers")
|
|
return sorted(utilities, key=utilities.__getitem__, reverse=True)
|