Skip to content

fix(scenario): finite shock-matrix guard; infer shock order in from_zero_restrictions - #223

Merged
thomaspinder merged 1 commit into
mainfrom
fix/187-188-longrun-followups
Jul 29, 2026
Merged

thomaspinder merged 1 commit into
mainfrom
fix/187-188-longrun-followups

Conversation

@thomaspinder

Copy link
Copy Markdown
Collaborator

Summary

Two follow-ups on the long-run-restrictions branch (#183), stacked on feat/143-long-run-restrictions:

  • Scenario methods surface raw LinAlgError on NaN shock-matrix draws #187: NaN shock-matrix draws (reachable via LongRunRestriction(on_undefined="nan"), and soon MaxShare/ZeroSignRestriction) hit the scenario engines as either an opaque SVD did not converge (structural_scenario) or — worse than the issue knew — a silent all-NaN counterfactual (np.linalg.inv on NaN does not raise on NumPy 2.4). A single _require_finite_shock_matrix guard now sits at the two seams where P enters the engines, counting the offending draws and pointing at on_undefined="raise". IRF/FEVD/HD deliberately keep propagating NaN (pinned by existing tests) — the guard is scenario-only. Tests assert the new error is not a LinAlgError (which subclasses ValueError).
  • from_zero_restrictions could infer shock order from restriction counts #188: from_zero_restrictions rejected genuinely-recursive patterns whose shock_names weren't pre-sorted. The shock order is now inferred — a shock's position equals the number of restriction lists it appears in (derived from the triangularity structure) — with the exact per-position set check unchanged as final validation, so non-recursive patterns still fail with the same messages. One existing test had encoded the bug (a valid-but-unsorted pattern asserted as rejected) and was rewritten to a genuine violation.

Closes #187
Closes #188

Gates

156 targeted tests green; branch fast suite 580 passed; ruff/ty 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

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.0%. Comparing base (866ca8f) to head (d41b593).

Additional details and impacted files
@@          Coverage Diff          @@
##            main    #223   +/-   ##
=====================================
  Coverage   95.0%   95.0%           
=====================================
  Files         45      45           
  Lines       3099    3109   +10     
  Branches     380     381    +1     
=====================================
+ Hits        2945    2955   +10     
  Misses       111     111           
  Partials      43      43           

☔ 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
thomaspinder force-pushed the feat/143-long-run-restrictions branch 2 times, most recently from fcbb968 to abff7b2 Compare July 29, 2026 14:11
@thomaspinder
thomaspinder changed the base branch from feat/143-long-run-restrictions to main July 29, 2026 20:54
…unts

Closes #187, closes #188.

#187: `counterfactual` and `structural_scenario` are linear solves in the
structural shock matrix P. With NaN draws — reachable today via
`LongRunRestriction(on_undefined="nan")`, and soon via other schemes —
the in-sample engine inverted P and returned an all-NaN counterfactual
silently, while the forecast engine died inside LAPACK as "SVD did not
converge" from `matrix_rank`. Neither named the cause. A single guard,
`_require_finite_shock_matrix`, now runs where P enters each engine
(`structural_shock_context` and `_forecast_shock_matrices`), counts the
NaN draws, names the `on_undefined="nan"` policy as the likely cause, and
points at `on_undefined="raise"` to catch it at identification time.

#188: `from_zero_restrictions` rejected genuinely recursive patterns whose
`shock_names` were not pre-sorted, though the order is recoverable. The
variable ordering already came from the restriction counts; the same count
read down the other axis gives the shock order — in a triangular pattern
the variable at position i is restricted by the shocks at positions
i+1..n-1, so the shock at position k appears in exactly k restriction
lists. Ties (non-recursive patterns) sort stably and are still caught by
the unchanged exact per-position set check, so the "not recursive" error
paths keep their messages. The caller's `shock_names` order is now
irrelevant to validity; the names still label the columns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
@thomaspinder
thomaspinder force-pushed the fix/187-188-longrun-followups branch from cb2facb to d41b593 Compare July 29, 2026 21:39
@thomaspinder
thomaspinder merged commit 41e5234 into main Jul 29, 2026
9 checks passed
@thomaspinder thomaspinder added the bug Something isn't working label Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

from_zero_restrictions could infer shock order from restriction counts Scenario methods surface raw LinAlgError on NaN shock-matrix draws

2 participants