Skip to content

fix(identification): return the ordered Cholesky factor in data row order - #208

Merged
thomaspinder merged 2 commits into
mainfrom
fix/184-cholesky-row-order
Jul 29, 2026
Merged

thomaspinder merged 2 commits into
mainfrom
fix/184-cholesky-row-order

Conversation

@thomaspinder

Copy link
Copy Markdown
Collaborator

Summary

Fixes the P1 mislabelling bug #184 and implements #81's QR-based reordering in the same eight lines.

The bug: IdentifiedVAR.shock_matrix labels response rows with var_names (data order) and multiplies by MA coefficients built in data order — for every scheme. Cholesky.identify with a non-identity ordering returned chol(ΠΣΠ') with rows left in ordering order: mislabelled response rows and a mixed-coordinate Φ @ P product in IRF/FEVD/HD. The red-first proof is in the test commit — pre-fix, the structural zero sat at 2.04 in the cell its labels claimed, and reconstructed variances were transposed between variables.

The fix (#81's construction): the permuted factor is now obtained by LQ-via-QR of the row-permuted factor — qr((ΠL)') gives G = R' with GG' = ΠΣΠ' exactly, sign-fixed to a positive diagonal, then un-permuted back to data row order. Σ is never formed, so input conditioning is not squared (verified at cond 1.4e12: max relative error 1.5e-16, structural zeros exactly zero), and one batched decomposition per call is saved. Columns stay in ordering order, matching shock_coords — the contract is now stated at both the producer and consumer ends.

Breaking (pre-v0.1): any analysis using a custom Cholesky(ordering=...) will produce different — previously mislabelled — numbers. Identity orderings are byte-identical (fast path returns L unchanged; the pinned IRF/FEVD/HD regression file is untouched and green). A side effect worth knowing: rank-deficient input now yields a zero diagonal instead of LinAlgError — unreachable through the pipeline (volatility adapters always produce valid factors), accepted deliberately.

Notable blast radius: the monetary-policy tutorial's prose ("orderings A and B give identical policy IRFs") was false in the rendered notebook and becomes true on the next render — no docs edit needed.

Fixes #184
Closes #81

Review

Independently reviewed (verdict: approve, zero findings): the LQ construction re-derived from scratch, column-vs-row sign scaling explicitly discriminated numerically (row-scaling error 5.3 vs column 1.8e-15), a non-involution 4-cycle permutation stress-tested beyond the committed tests, and test discrimination proven by monkeypatching the old implementation back in — all six behavioural tests go red while the pin file stays green under both implementations.

Merge note

Lands cleanly before #183 (feat/143) — the only contact point is the _linalg import line (mechanical rebase; sigma_from_cholesky remains used by fitted.py).

ruff / ty / fast suite: all green (533 passed, 29 deselected).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV

thomaspinder and others added 2 commits July 29, 2026 08:29
…rder

`Cholesky.identify` rebuilt Sigma, permuted it, and returned
`cholesky(Pi Sigma Pi')` — a matrix whose rows are indexed by
`self.ordering`. `IdentifiedVAR.shock_matrix` labels that row axis with
`var_names` (data order) and `impulse_response`/`fevd` left-multiply it by
MA coefficients built in data order. For any non-identity ordering the
result was therefore mislabelled *and* the product `Phi @ P` mixed two
coordinate systems, so IRFs, FEVDs and historical decompositions were wrong
— silently, since the output still looked triangular.

Replace the Sigma round-trip with an LQ factorisation of `Pi @ L`, obtained
from `qr((Pi @ L).T)`: `G = R.T` is lower-triangular with
`G @ G.T = Pi Sigma Pi'`, columns are sign-fixed to give a positive
diagonal, and the rows are then un-permuted back into data order. This also
closes #81 — Sigma is never formed, so the conditioning of the
decomposition is not squared, and the structural zeros are exact rather
than rounded. Batch dims are addressed with an ellipsis, so the path no
longer assumes rank-4 input.

`SignRestriction` (`L @ Q`) and `ProxySVAR` (`L @ Q`) rotate columns only
and are unaffected. The identity-ordering fast path still returns `L`
byte-identically, so `tests/test_cholskey_irf_pins.py` is untouched.

Breaking for anyone using a non-identity `ordering`: those numbers change,
because the previous ones were wrong.

Fixes #184
Closes #81

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
Regression cover for the row-order bug. Unit level: the ordered factor
matches `cholesky(Pi Sigma Pi')` after row permutation, `identify` is
deterministic, and reordering then reordering back round-trips to `L`
(a transposed `perm`/`inv` pair survives a reversal but not this).

Pipeline level (`IdentifiedVAR` over the synthetic 2-var posterior, no
MCMC): the structural zero must land in the cell its *labels* claim, and
IRFs and FEVDs selected by `(response, shock)` label must be invariant to
relabelling the variables — case A runs the identity fast path on a
permuted posterior, case B runs the reordering path on the original. The
FEVD case squares Theta, so it catches mislabelling that a sign error would
otherwise mask. Both failed loudly before the fix.

Refs #184

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

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.8%. Comparing base (a378a8f) to head (e2305ac).

Additional details and impacted files
@@          Coverage Diff          @@
##            main    #208   +/-   ##
=====================================
  Coverage   94.8%   94.8%           
=====================================
  Files         40      40           
  Lines       2416    2419    +3     
  Branches     280     280           
=====================================
+ Hits        2292    2295    +3     
  Misses        81      81           
  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 merged commit 70a412c into main Jul 29, 2026
9 checks passed
@thomaspinder thomaspinder added the breaking-change Breaking changes label Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cholesky.identify returns shock-matrix rows in ordering order, mislabelled as data order Optional QR-based reordering for Cholesky.identify

2 participants