Skip to content

Fall back to lu in factorize of a sparse Hermitian matrix whose LDLt fails - #1116

Merged
ViralBShah merged 1 commit into
mainfrom
vs/factorize-indefinite-hermitian
Oct 26, 2026
Merged

ViralBShah merged 1 commit into
mainfrom
vs/factorize-indefinite-hermitian

Conversation

@ViralBShah

@ViralBShah ViralBShah commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

factorize of a sparse indefinite Hermitian matrix threw ZeroPivotException when its LDLt needs pivoting. \ on the same matrix works, and so does dense factorize. cond(A, 1) threw too, because it goes through factorize.

Before

julia> A = sparse([0.0 1; 1 0]); b = [1.0, 2.0];

julia> A \ b
2-element Vector{Float64}:
 2.0
 1.0

julia> factorize(A) \ b
ERROR: ZeroPivotException: factorization encountered one or more zero pivots. Consider switching to a pivoted LU factorization.

julia> cond(A, 1)
ERROR: ZeroPivotException: factorization encountered one or more zero pivots. Consider switching to a pivoted LU factorization.

After

julia> typeof(factorize(A))
SparseArrays.UMFPACK.UmfpackLU{Float64, Int64}

julia> factorize(A) \ b
2-element Vector{Float64}:
 2.0
 1.0

julia> cond(A, 1)
1.0

The Hermitian method tries cholesky(A; check = false) first. If that fails, it called ldlt!(F, A), which throws when CHOLMOD's unpivoted LDLt meets a zero pivot. It now calls ldlt!(F, A; check = false), and when that also fails it returns lu(A), as the Hermitian \ does (cholmod.jl). So factorize now succeeds wherever \ does. A singular matrix still throws, now a SingularException from lu. The inferred return type of factorize(::Hermitian{T,SparseMatrixCSC{T,Int}}) widens from CHOLMOD.Factor{T,Int} to Union{CHOLMOD.Factor{T,Int}, UMFPACK.UmfpackLU{T,Int}} (UmfpackLU{Float64,Int} for Float32). factorize(::SparseMatrixCSC) already returned a seven-member union that includes both.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.73%. Comparing base (e4a42b4) to head (ad29290).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1116   +/-   ##
=======================================
  Coverage   94.73%   94.73%           
=======================================
  Files          16       16           
  Lines       10325    10325           
=======================================
  Hits         9781     9781           
  Misses        544      544           

☔ 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.

…DLt fails

`factorize` of an indefinite Hermitian sparse matrix whose LDLt needs pivoting threw
`ZeroPivotException`, while `\` falls back to `lu`. It now does the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ViralBShah
ViralBShah force-pushed the vs/factorize-indefinite-hermitian branch from dcb926b to ad29290 Compare October 26, 2026 14:58
@ViralBShah
ViralBShah merged commit 6ab13ee into main Oct 26, 2026
12 checks passed
@ViralBShah
ViralBShah deleted the vs/factorize-indefinite-hermitian branch October 26, 2026 21:02
ViralBShah added a commit that referenced this pull request Oct 26, 2026
…ing (#1126)

`cond(A, 1)` and `cond(A, Inf)` of a singular sparse matrix throw
`SingularException`. Dense `cond` returns `Inf` for these.

```julia
using SparseArrays, LinearAlgebra
for A in ([1.0 2; 3 6], [1.0 0; 2 0]), p in (1, Inf)
    println("cond(sparse($A), $p) = ", try cond(sparse(A), p) catch e; sprint(showerror, e) end)
end
```

**Before**
```
cond(sparse([1.0 2.0; 3.0 6.0]), 1) = SingularException: matrix is singular; factorization failed. Zero pivot found at index 0
cond(sparse([1.0 2.0; 3.0 6.0]), Inf) = SingularException: matrix is singular; factorization failed. Zero pivot found at index 0
cond(sparse([1.0 0.0; 2.0 0.0]), 1) = SingularException: matrix is singular; factorization failed. Zero pivot found at index 2
cond(sparse([1.0 0.0; 2.0 0.0]), Inf) = SingularException: matrix is singular; factorization failed. Zero pivot found at index 2
```

**After**
```
cond(sparse([1.0 2.0; 3.0 6.0]), 1) = Inf
cond(sparse([1.0 2.0; 3.0 6.0]), Inf) = Inf
cond(sparse([1.0 0.0; 2.0 0.0]), 1) = Inf
cond(sparse([1.0 0.0; 2.0 0.0]), Inf) = Inf
```

`cond` estimates the norm of the inverse with `opnormestinv`, which
solves with `factorize(A)`. A singular matrix makes `lu` throw. A
triangular or diagonal matrix gets no factorization object, so its solve
throws. `issuccess` on `lu(A; check = false)` would only cover the `lu`
branch, so `cond` catches `SingularException` instead, as dense `cond`
does. It then returns `convert(float(real(eltype(A))), Inf)`, the same
value and type as dense. A `Float32` matrix gives `Inf32`. The inferred
return types are unchanged.

A singular Hermitian matrix still throws `ZeroPivotException` from the
LDLt in `factorize`. That exception is not caught, because on `main` a
nonsingular indefinite matrix such as `[0.0 1; 1 0]` throws it too.
#1116 makes that path fall back to `lu`, which throws
`SingularException`. With both PRs a singular Hermitian matrix also
gives `Inf`; I checked this on #1116's branch with this change applied.
This PR does not depend on #1116.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ViralBShah added a commit that referenced this pull request Oct 27, 2026
…1132)

Stacked on #1131.

Since #1126 sparse `cond` wraps the whole `opnormestinv` estimator in a
`try`, so a `SingularException` from anywhere inside it becomes `Inf`,
and a singular triangular or diagonal matrix is only found once the
estimator has allocated its blocks and run its first solve. Raised in
the post-merge review of #1126.

**Before** (#1131), `cond(A, 1)` of a singular `A` with `n = 10^6` and
one zero on the diagonal
| `A` | time | allocated |
|---|---|---|
| diagonal | 64.6 ms | 125.3 MiB |
| lower bidiagonal | 16.1 ms | 68.7 MiB |

**After**
| `A` | time | allocated |
|---|---|---|
| diagonal | 9.4 ms | 56.6 MiB |
| lower bidiagonal | 2.6 ms | 0 |

Both return `Inf`, before and after. `factorize` throws a
`SingularException` for a singular LU, and for a singular Hermitian
matrix once Cholesky and LDLt have failed and it falls back to the LU
(#1116); only the triangular and diagonal factors it returns leave the
zero pivot to the solve. So `cond` now catches around `factorize` alone
and checks the diagonal of those factors with `_issingularfactor`, which
reuses the triangular solve's `_trisingular`, before it calls the
estimator; `opnormestinv(A, t)` keeps its behaviour and calls the new
`_opnormestinv(A, F, t)` with the factorization. A sweep of `cond`
against dense over `Int`, `Float32`, `Float64`, `ComplexF32`,
`ComplexF64`, `Rational{Int}`, `BigFloat` and `Complex{BigFloat}`, on
general, singular, triangular, diagonal and Hermitian matrices, gives
the same results as #1131 with a seeded RNG.

Written by Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@ViralBShah ViralBShah mentioned this pull request Oct 27, 2026
30 tasks done
ViralBShah added a commit that referenced this pull request Oct 27, 2026
Backports of the merged PRs labelled `backport 1.14`, cherry-picked with
`-x` in the order they were merged on `main`. `release-1.14` starts at
b0aa455 (#1104), the commit JuliaLang/julia's `release-1.14` pins, and
every PR merged on `main` since then is labelled, so this branch's tree
is identical to `main`'s at 9bfad27.

- [x] #1105
- [x] #1011
- [x] #1107
- [x] #1108
- [x] #1110
- [x] #1111
- [x] #1114
- [x] #1112
- [x] #1115
- [x] #1117
- [x] #1118
- [x] #1109
- [x] #1113
- [x] #1122
- [x] #1123
- [x] #1120
- [x] #1124
- [x] #1125
- [x] #1116
- [x] #1121
- [x] #1126
- [x] #1128
- [x] #1130
- [x] #1131
- [x] #1119
- [x] #1133
- [x] #1127
- [x] #1135
- [x] #1134
- [x] #1137

#1137 removes the first-call compile time that #1128 added, which the
TTFX check flagged on this PR.

#1132 merged into the branch of #1131 and is part of its commit.

Not bug fixes: #1011 adds a deprecation, #1109 changes the `sprand`
value stream for a value function, #1113 changes the result shape of
`SparseMatrixCSC` of a vector view, and #1119, #1133, #1134 and #1135
add methods for views. They are included because they are labelled and
1.14 is not released yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
@ViralBShah ViralBShah removed the backport 1.13 Change should be backported to release-1.13 label Oct 27, 2026
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.

1 participant