""" Scenario stress — transplanted from the retired shared ``core/calculations/scenarios.py`` with identical semantics. Forrester TEI risk-adjusts benefits *down* and costs *up*; scenarios stress both levers: * ``adoption`` scales nominal values (``year_values`` and ``initial``). * ``risk_delta`` is *added* to a benefit's risk factor and *subtracted* from a cost's (conservative = more uncertainty on benefits, less padding on costs), then clamped to [0, 1]. ``"moderate"`` is the identity — the headless default reproduces the published study. Note the counterintuitive corollary: the conservative scenario *lowers* costs PV, because 80% adoption shrinks consumption-priced usage and the clamp caps cost padding. """ from __future__ import annotations from copy import deepcopy SCENARIOS: dict[str, dict[str, float]] = { "conservative": {"adoption": 0.80, "risk_delta": 0.10}, "moderate": {"adoption": 1.00, "risk_delta": 0.00}, "aggressive": {"adoption": 1.15, "risk_delta": -0.05}, } def apply_scenario( items: list[dict], scenario: str = "moderate", *, multipliers: dict[str, dict[str, float]] | None = None, table: str | None = None, ) -> list[dict]: """ Deep-copied value rows with the scenario applied; inputs are not mutated. Each row needs ``year_values`` (year-string → float), optionally ``initial`` and ``risk_adjustment``, and a ``table`` of ``"benefits"`` or ``"costs"`` (or pass ``table=`` to force one) — the table decides the sign of ``risk_delta``. """ cfg = (multipliers or SCENARIOS).get(scenario) if cfg is None: raise KeyError(f"Unknown scenario: {scenario!r}") adoption = float(cfg.get("adoption", 1.0)) risk_delta = float(cfg.get("risk_delta", 0.0)) out: list[dict] = [] for raw in items: item = deepcopy(raw) item_table = item.get("table") or table or "benefits" item["table"] = item_table item["year_values"] = { k: float(v) * adoption for k, v in item["year_values"].items() } if item.get("initial") is not None: item["initial"] = float(item["initial"]) * adoption ra = float(item.get("risk_adjustment") or 0.0) new_ra = ra + risk_delta if item_table == "benefits" else ra - risk_delta item["risk_adjustment"] = max(0.0, min(1.0, new_ra)) out.append(item) return out