Skip to content

feat(scenario): probabilistic conditioning and reverse stress via entropic tilting - #213

Open
thomaspinder wants to merge 6 commits into
mainfrom
feat/150-entropic-tilting
Open

feat(scenario): probabilistic conditioning and reverse stress via entropic tilting#213
thomaspinder wants to merge 6 commits into
mainfrom
feat/150-entropic-tilting

Conversation

@thomaspinder

Copy link
Copy Markdown
Owner

Summary

Soft probability targets over existing forecast draws, and the inversion into reverse stress — extending the hard-conditioning stack (ADPRR/Waggoner-Zha) per issue #150:

  • ProbabilityTarget / MomentTarget join the scenario vocabulary; ForecastResult.tilt(targets) / ConditionalForecastResult.tilt(targets) return a frozen TiltedForecastResult carrying minimum-KL tilting weights over the parent draws (held by reference), achieved-vs-requested targets, Kish ESS (+ degeneracy warning), and the exact KL divergence. Hard and soft conditions compose by chaining — pins hold pathwise on every draw, so preservation under reweighting is a theorem, not a code path (pinned bit-exactly in tests anyway).
  • Solver: exact closed-form two-mass weights for a single probability target (no optimiser); a logsumexp-stabilised convex dual (BFGS + Newton polish with the analytic Hessian = tilted covariance) for the general case, with joint-infeasibility detection (achieved-tolerance + λ-divergence guards) and empty-support errors that count the draws.
  • IdentifiedVAR.reverse_stress(variable, threshold, steps, ...): baseline event probability, the tilted distribution, and the most plausible shock cocktail — the tilted-weighted mean of retained structural shock draws, via a new _scenario.structural_forecast_draws that reproduces the scenario engine's RNG stream draw-for-draw (matched-seed nesting pinned bit-exactly under both constant and time-varying volatility, the latter mutation-verified). Plausibility reported as q = ‖cocktail‖² plus q_cal — the ADPRR calibration applied to the tilt's relative entropy, explicitly documented as an extension, not a citation (ADR-0009).
  • Weighted median/HDI implemented properly (Hazen-consistent weighted quantiles — exact against np.quantile(method="hazen") under uniform weights; min-width weighted HDI pinned against az.hdi). Fan-chart and cocktail plots included.
  • Docs: how-to, ADR-0009, CONTEXT.md vocabulary (with the "importance weights" avoid-term — no proposal distribution is involved), Robertson-Tallman-Whiteman bib entry.

Closes #150

Review

Planned by a Fable-tier planning agent; independently reviewed with the two-mass solution, dual gradient/Hessian, and the RTW Gaussian oracle re-derived from scratch and confirmed numerically. The reviewer endorsed all seven implementation deviations — including the implementer's catch that the plan's own weighted-quantile test was mathematically inconsistent (the mid-point convention is not replication-consistent; resolved via the exact Hazen equivalence). Two P2s (a missing time-varying matched-seed regression pin — verified to hold at diff 0.0 before pinning — and a one-word q_cal doc misattribution) fixed in a follow-up commit.

Tests

75 new tests: exact analytic oracles (two-mass weights/ESS/KL at 1e-13; RTW λ against an independent brentq root-find; dual vs direct SLSQP primal), hard-pin preservation bit-exact, RNG nesting pinned three ways plus the mutation-verified time-varying pin, weighted-quantile/HDI convention pins, and every guard path. Fast suite: 602 passed, 29 deselected. ruff/ty/docs build clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV

@codecov-commenter

codecov-commenter commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.57869% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.3%. Comparing base (866ca8f) to head (f04a7d4).

Files with missing lines Patch % Lines
src/impulso/_tilting.py 97.7% 2 Missing and 2 partials ⚠️
src/impulso/_scenario.py 90.4% 1 Missing and 1 partial ⚠️
src/impulso/identified.py 95.2% 1 Missing and 1 partial ⚠️
src/impulso/plotting/_tilted_forecast.py 96.6% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main    #213     +/-   ##
=======================================
+ Coverage   95.0%   95.3%   +0.3%     
=======================================
  Files         45      47      +2     
  Lines       3099    3498    +399     
  Branches     380     415     +35     
=======================================
+ Hits        2945    3336    +391     
- Misses       111     114      +3     
- Partials      43      48      +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

thomaspinder and others added 5 commits July 29, 2026 23:00
Adds the post-hoc reweighting half of #150. `ProbabilityTarget` and
`MomentTarget` join the scenario vocabulary; `_tilting.py` solves the
minimum-relative-entropy problem over existing forecast draws (closed
form for a single event probability, log-sum-exp-stabilised convex dual
otherwise) and owns the weighted quantile / HDI summaries.

`ForecastResult.tilt()` and `ConditionalForecastResult.tilt()` (which
`ScenarioResult` inherits) return a frozen `TiltedForecastResult` holding
the parent draws by reference plus the weights, the per-target
requested/achieved vectors, and the ESS / relative-entropy diagnostics.
Chaining is the supported way to mix hard and soft conditioning: pins
hold pathwise on every draw, and reweighting never moves a draw, so they
survive any tilt.

`ReverseStressResult` and `plot_reverse_stress` land here too so the
result surface stays in one file; the method that produces them follows.

Refs #150

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
…vent

Completes #150's structural half. `_scenario.structural_forecast_draws`
returns unconditional forecast draws together with the structural shocks
that generated them — neither `forecast()` nor the existing engines
expose those — reproducing `structural_scenario_engine`'s no-ingredient
branch under the same RNG stream contract, so matched seeds nest exactly.

`IdentifiedVAR.reverse_stress(variable, threshold, steps, ...)` runs
scenario analysis backwards: name the outcome, get the average structural
configuration that delivers it. The event is imposed by entropic tilting
(`probability=1.0` is exact conditioning) and the cocktail is the
tilted-weighted mean of the retained shocks, so it inherits the model's
own shock correlations rather than needing a projection norm. Its
magnitude `q = ‖E_w[ε]‖²` is in the same one-standard-deviation units as
the scenario plausibility statistic; `q_cal` extends the ADPRR binomial
calibration to the tilt's now-finite relative entropy.

The exogenous-regressor guards common to the forecast-side methods move
into `IdentifiedVAR._validate_forecast_exog` (pure extraction).

Refs #150

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
Documents the #150 layer. New how-to `probabilistic-conditioning` covers
stating a probability target, reading ESS / achieved-vs-requested before
trusting the bands, chaining a tilt after `conditional_forecast`, and the
reverse-stress walkthrough. ADR-0009 records the decision (post-hoc
reweighting rather than a second solver; chaining rather than mixing;
closed-form/dual split; cocktail as tilted conditional mean of retained
draws) and the rejected alternatives.

CONTEXT.md gains entropic tilting, probability/moment targets, effective
sample size, and reverse stress, and the plausibility entry now points at
the relative entropy as a real quantity rather than a reserved one.
Reference pages pick up the new targets, result objects, and plots; bib
adds Robertson-Tallman-Whiteman (2005) and Krueger-Clark-Ravazzolo (2017).

Refs #150

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
Review follow-up on #150. `structural_forecast_draws`'s docstring claims
matched-seed nesting under time-varying volatility, but the only coverage
ran under `Constant()`, where `_forecast_shock_matrices` consumes no
randomness — so a reordered stream would have gone unnoticed in exactly
the branch that can break. Adds a pin against `structural_scenario`'s
no-ingredient branch using the existing `_RngConsumingVol` stub; swapping
the two RNG consumers fails the new test and passes the old one.

Also corrects the reverse-stress how-to: `q_cal` calibrates the tilt's
relative entropy, not the cocktail magnitude `q`.

Refs #150

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
#237 made VARData reject exog columns that are constant within the
sample, so the all-ones "const" column this guard used no longer
constructs. Swap it for a draw from the shared `rng` fixture; the test
only needs the model to carry *some* exogenous data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
@thomaspinder
thomaspinder force-pushed the feat/150-entropic-tilting branch from 4b41c78 to c17f055 Compare July 29, 2026 21:01
…base conflict

The rebase conflict resolution merged the new bibliography entry into the
blanchardQuah1989 entry, dropping its closing brace. sphinxcontrib-bibtex
then failed to parse references.bib, breaking build-docs and docs-linkcheck.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add probabilistic conditioning and reverse-stress analysis

2 participants