Repository navigation
Conversation
solve_discrete_riccati_system, which LQMarkov.stationary_values uses,
averaged the quadratic correction term over next-period Markov states
outside the inverse. The control is chosen before s_{t+1} is known, so
the expectation belongs inside. With P_bar_i = sum_j Π[i, j] P_j, each
P_i now gets the single-state Riccati update evaluated at P_bar_i. The
returned Ps are now the cost of following the returned Fs, and the Fs
are optimal.
The left factor of the correction term is formed directly, not as the
transpose of the right factor. In the transposed form, rounding
asymmetry in Ps is not damped by the closed loop. The iteration then
diverges on well-posed problems with an unstable A, including the
single-regime case.
The TestLQMarkov reference values came from the old recursion and are
regenerated. Two tests are added: Ps must equal the exact cost of
following Fs, and with one regime or identical regimes LQMarkov must
match LQ.
Closes #982.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It is a behaviour-changing numerical correctness fix to a core published library function with regenerated golden values whose matrix-case numbers could not be fully re-verified here, warranting expert human sign-off.
0 open findings
What changed in this PR
This PR fixes a correctness bug (#982) in solve_discrete_riccati_system, the Markov-jump Riccati solver used by LQMarkov.stationary_values. The previous recursion averaged the quadratic correction term over next-period Markov states outside the matrix inverse; since the control u_t is chosen before s_{t+1} is realized, the expectation P̄_i = Σ_j Π_ij P_j must enter inside the inverse. The fix makes LQMarkov return value matrices that are actually the cost of the returned policy and decision rules that are optimal.
Changes:
- Rewrote the per-state Riccati update in
_matrix_eqn.pyto formP̄_ionce per sweep and apply the single-state update with the expectation inside the inverse; the left factor is formed directly (not as a transpose) to avoid amplifying rounding asymmetry. - Updated the function docstring with the exact equation solved and corrected the
Psreturn type fromndim=2tondim=3. - Regenerated the golden values in the scalar/matrix/sequence tests and added two regression tests (
test_stationary_values_are_policy_costs,test_identical_regimes_match_lq) plus a_lq_markov_policy_costhelper.
| File | Description |
|---|---|
quantecon/_matrix_eqn.py |
Corrects the stacked Riccati recursion (expectation inside the inverse), documents the solved equation, and fixes the return-type docstring. |
quantecon/tests/test_lqcontrol.py |
Regenerates reference values to match the corrected recursion and adds policy-cost and identical-regime regression tests with a supporting helper. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Leaving this open for now. #983 by @jadhavgaurav, opened the day before this one, makes the same core fix. Its code also builds the left factor directly, so the problem with the transposed form described above doesn't apply to it, and the two PRs give the same results up to rounding. So one option is to push the extras from this PR onto #983:
They carry over with only the comment adapted to #983's inline expression. This PR's test file passes unchanged against #983's code, and |
|
Update: @jadhavgaurav offered to port this PR's extras (the docstring, the |
Document the stacked Riccati equations in the solve_discrete_riccati_system docstring (raw string, return type ndim=3), and comment why the left factor of the correction term is formed directly. Add the _lq_markov_policy_cost helper with test_stationary_values_are_policy_costs and test_identical_regimes_match_lq. The latter fails when the left factor is formed as the transpose of the right factor. Signed-off-by: Gaurav Jadhav <gaurav.vjadhav01@gmail.com>
|
Closing in favour of #983, which has the same fix plus this PR's docstring and tests. It's released in v0.13.1. |
This fixes the Markov-jump Riccati recursion in
solve_discrete_riccati_system, whichLQMarkov.stationary_valuesuses (#982).The control$u_t$ is chosen before $s_{t+1}$ is realized, so the expectation over next-period states belongs inside the inverse. For each state $i$ , the loop now forms $\bar P_i = \sum_j \Pi_{ij} P_j$ and applies the single-state update
The
Fanddformulas inLQMarkov.stationary_valueswere already correct givenPs, so they are unchanged. The function's docstring now states the equation it solves.One deliberate difference from the snippet in #982. The left factor$\beta A_i' \bar P_i B_i + N_i'$ is formed directly, not as $\bar P_i$ is exactly symmetric. In the transposed form, the rounding-level asymmetric part of $E \mapsto \beta(A'\bar E A + (BF)'\bar E(BF))$ , which has no closed-loop factor. It can then grow even though the optimal closed loop is stable, for example when $A$ is unstable. The iteration then diverges, or returns NaN, on well-posed problems that
(beta * Bs[i].T @ P_bar @ As[i] + Ns[i]).T. The two are equal only whenPsevolves asmainsolves, including single-regime ones. A comment in the code records this.Behaviour change
LQMarkov.stationary_valuesreturns differentPs,dsandFs, and socompute_sequencereturns different paths, whenever a row ofP, 0.40% indand 0.21% inF. On the example in #982,Fchanges by 1.9%. Problems with one regime, identical regimes, a permutationTests
test_stationary_scalar,test_stationary_matand the two sequence tests came from the old recursion. The new values were reproduced independently by Howard policy iteration with exact policy evaluation. The closest uses 5.2% of its assertion's tolerance.test_stationary_values_are_policy_costs. It checks thatPsequal the exact cost of following the returnedFson the two existing fixtures and on the BUG: LQMarkov / solve_discrete_riccati_system averages over next-period states outside the inverse #982 example. Onmainit fails, with a relative deviation of 3.8e-3 againstrtol=1e-6. Here it passes, with a relative error of about 1e-11.test_identical_regimes_match_lq. With one regime, or two identical regimes, and an unstableLQMarkovmust matchLQ. It passes with about a 12,000× margin, and fails if the left factor is written as a transpose.Verification
LQMarkovdocstring and checked the code against it line by line, includingFandd. On random models with m = 3 and nonzeroNandC, I minimized the right-hand side directly: the minimizer is-F xand the minimum isx'Px + d.x'Px + dwithin sampling error. The old values are off by 11 to 244 standard errors.max_iter=1000iterations at β = 0.99, the same as any correct variant. On all 600 single-regime problems it agrees withLQ, asmaindoes.fetch_nb_dependenciestests, which need network access my environment doesn't have.Once this is released, the notes about the discrepancy in
markov_jump_lq(QuantEcon/lecture-python-advanced.myst#392) can be removed.Closes #982
🤖 Generated with Claude Code