Skip to content

FIX: Fix known bugs in MarkovChain (markov/mc_tools.jl) - #392

Merged
oyamad merged 2 commits into
masterfrom
fix-mc-tools
Jul 10, 2026
Merged

FIX: Fix known bugs in MarkovChain (markov/mc_tools.jl)#392
oyamad merged 2 commits into
masterfrom
fix-mc-tools

Conversation

@oyamad

@oyamad oyamad commented Jul 10, 2026

Copy link
Copy Markdown
Member

This PR fixes several bugs in markov/mc_tools.jl found in a code review, following up on #385/#391.

Fixes

  • gth_solve/gth_solve! silently returned an incorrect result (NaN-contaminated) for non-square input, since only size(A, 1) was read and the elimination runs under @inbounds. A DimensionMismatch is now thrown.
  • gth_solve! allocated temporaries in the elimination loop (row-slice copy in the pivot-scale sum and an out-of-place column scaling) and in the final normalization. These are now views/in-place: at n=200, allocations drop from 1201 (842 KiB) to 5 (322 KiB, essentially the input copy in gth_solve), with a ~10% speedup at small sizes (benchmark SUITE["mc_tools"]["gth_solve"], Add benchmarks for MarkovChain (markov/mc_tools.jl) #391).
  • check_stochastic_matrix used a fixed absolute tolerance of 5e-15 on row sums, which rejects valid large matrices: both the rounding of the entries (e.g. from row normalization) and the accumulation in the recomputed sum grow linearly with n, so e.g. a row-normalized random 1000x1000 matrix typically fails the check. The tolerance is now max(5e-15, n * eps(T)) for T<:AbstractFloat (with eps(Float64) otherwise); the floor keeps the current behavior for small matrices. This also makes Float32 transition matrices usable, which the fixed tolerance rejected essentially always.
  • test_mc_tools.jl defined a Base.isapprox overload for Vector{Vector{<:Real}} — type piracy that in fact never dispatched (by type invariance, Vector{Vector{Float64}} <: Vector{Vector{<:Real}} does not hold), and whose body compared the outer arguments, so it would have recursed infinitely if it ever ran. It is removed; the tests exercise the generic isapprox fallback as they always effectively did.

Tests

New test sets cover the non-square DimensionMismatch (for the Float64, Int, and in-place entry points), row-sum errors within and beyond the size-dependent tolerance, and construction from a row-normalized Float32 matrix.

🤖 Generated with Claude Code (Claude Fable 5)

- gth_solve: throw DimensionMismatch on non-square input, instead of
  silently returning an incorrect result
- gth_solve!: avoid temporary arrays from row/column slicing and
  out-of-place normalization (at n=200: 1201 allocations down to 5)
- check_stochastic_matrix: replace the fixed 5e-15 absolute row-sum
  tolerance, which rejects valid large matrices, with a
  size-dependent one; this also makes Float32 matrices usable
- test_mc_tools.jl: remove a type-pirated Base.isapprox overload
  that never dispatched due to type invariance (and would recurse
  infinitely if it did)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses correctness and performance issues in QuantEcon.jl’s Markov chain utilities (src/markov/mc_tools.jl), focused on making gth_solve/gth_solve! safer for invalid inputs and making stochastic-matrix validation scale appropriately with matrix size and element type.

Changes:

  • Add an explicit squareness check to gth_solve! (and therefore gth_solve) to throw DimensionMismatch instead of producing incorrect/NaN-contaminated results.
  • Reduce allocations in gth_solve! by switching to views and in-place scaling/normalization.
  • Replace the fixed row-sum tolerance in check_stochastic_matrix with a size- and eltype-dependent tolerance, and update tests accordingly (also removing a non-functional/type-pirating Base.isapprox overload in tests).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/markov/mc_tools.jl Adds dimension validation to gth_solve!, removes avoidable allocations via views/in-place ops, and scales stochastic-matrix row-sum tolerance with n * eps(T) (with a small-matrix floor).
test/test_mc_tools.jl Removes an invalid Base.isapprox overload and adds tests for non-square gth_solve inputs plus the updated row-sum tolerance behavior (including a Float32 case).

Corrections to the check_stochastic_matrix tolerance from review:

- Cap the tolerance at sqrt(eps(T)) so that it can never approach the
  scale of the entries: n * eps(Float16) reaches exactly 1 at n=1024,
  which accepted an all-zero Float16 matrix.
- For sparse matrices, scale with the maximum number of stored entries
  per row instead of n: the rounding error of a row does not grow with
  the total number of states, and n * eps(Float32) at n=100_000
  accepted rows 1% short of 1.
- Accumulate half/single precision row sums in Float64, so that large
  legitimately normalized low-precision matrices are not rejected by
  rounding introduced by the check itself.
- Fall back to eps(Float64) for abstract element types, for which
  eps(T) raises a MethodError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oyamad

oyamad commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

A review by ChatGPT caught three genuine flaws in the tolerance introduced here, fixed in 16f3575. (1) n * eps(T) was unbounded and reached exactly 1 for Float16 at n=1024, accepting an all-zero matrix; the tolerance is now capped at sqrt(eps) so it can never approach the scale of the entries. (2) For sparse matrices the accumulation length is now the maximum number of stored entries per row rather than n, so single-entry rows 1% short of 1 are rejected at any size. (3) Half/single-precision row sums are now accumulated in Float64 (P * ones(Float64, n)), so the check itself adds no low-precision rounding, and abstract element types fall back to eps(Float64) instead of raising a MethodError. Regression tests added for each rejected/accepted example; the existing normalized-Float32 acceptance test is retained.

🤖 Generated with Claude Code (Claude Fable 5)

@oyamad
oyamad merged commit 910887a into master Jul 10, 2026
10 checks passed
@oyamad
oyamad deleted the fix-mc-tools branch July 10, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants