Repository navigation
Error clearly when PureUMFPACKFactorization is used without PureUMFPACK - #1144
ChrisRackauckas merged 2 commits into
Conversation
`PureUMFPACKFactorization` was the only extension-backed factorization
without a load guard in its constructor. `PureUMFPACK` is a weakdep and
`LinearSolvePureUMFPACKExt` holds the algorithm's only
`SciMLBase.solve!` method, so without `using PureUMFPACK` the algorithm
constructed fine and then fell through to the generic
`solve!(::LinearCache, ::AbstractFactorization)`, which calls
`do_factorization` -- for which no method exists:
ERROR: MethodError: no method matching do_factorization(
::PureUMFPACKFactorization, ::SparseMatrixCSC{Float64, Int64},
::Vector{Float64}, ::Vector{Float64})
`PureKLUFactorization` works out of the box only because `PureKLU` is a
hard dependency. Fixes SciML#1143.
The constructor now takes `throwerror = true` and checks for the
extension, matching `SparspakFactorization`, `CliqueTreesFactorization`,
`STRUMPACKFactorization`, `PardisoJL` and the rest.
The extension now solves with `\` rather than `PureUMFPACK.solve`.
PureUMFPACK 1.0 unexports `solve` and only marks it `public` on Julia
>= 1.11, so the qualified call would read as a non-public access when QA
runs on the 1.10 LTS. `\` is `solve(F, b)` with the default
`refine = 0`, so behavior is unchanged, and no new ExplicitImports
ignore entry is needed.
Requires PureUMFPACK 1, where a plain `using PureUMFPACK` no longer
shadows `solve`; the regression test uses that form deliberately.
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DR4AenbZvWqp2BMM42T27A
| # `\` rather than `PureUMFPACK.solve`: the latter is unexported and only | ||
| # marked `public` on Julia >= 1.11, so it reads as a non-public access on the | ||
| # LTS. `\` is `solve(F, b)` with the default `refine = 0`. | ||
| y = F \ cache.b |
There was a problem hiding this comment.
No, this isn't PureUMFPACK... it needs to use the PureUMFPACK solve on all versions...
There was a problem hiding this comment.
Fixed in d248eec — restored PureUMFPACK.solve(F, cache.b) on all versions.
For the record on the behavior question: PureLU is a plain struct (not <: Factorization) and Base.:\(F::PureLU, b::AbstractVector) = solve(F, b) at PureUMFPACK/src/interface.jl:179 is its only \ method, so it did land on PureUMFPACK's solve — which is why the numbers matched. But you're right that it should not go through a one-line forward in a package we do not control, and the version-dependence was bad on its own.
The concern that motivated \ also turned out to be unfounded. I was worried PureUMFPACK.solve would trip ExplicitImports on the 1.10 LTS, where PureUMFPACK cannot declare it public. It does get flagged there — confirmed on 1.10:
- `solve` is not public in `PureUMFPACK` but it was imported from `PureUMFPACK`
at ext/LinearSolvePureUMFPACKExt.jl:79:25
but all_qualified_accesses_are_public is already in ei_broken in test/qa/qa.jl (~90 pre-existing violations, tracked in #1058), so it never fails CI and needs no ignore entry. Reverting was free.
Running the QA group locally to check that also surfaced a real blocker I had missed: test/qa/Project.toml carried a second PureUMFPACK = "0.1" compat pin, which made the whole QA group unresolvable against PureUMFPACK 1. Bumped in the same commit.
Review feedback: the extension should call PureUMFPACK's solver explicitly on every Julia version rather than routing through `\`. `Base.:\(F::PureLU, b) = solve(F, b)` is a one-line forward this package does not control, so the indirection bought nothing. The concern that motivated `\` was that `PureUMFPACK.solve` reads as a non-public qualified access on the 1.10 LTS, where PureUMFPACK cannot declare it `public`. That is a non-issue: QA already marks `all_qualified_accesses_are_public` as `ei_broken` (~90 pre-existing violations, tracked in SciML#1058), so no ignore entry is needed either. `test/qa/Project.toml` carried a second `PureUMFPACK = "0.1"` compat pin that made the QA group unresolvable against PureUMFPACK 1. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DR4AenbZvWqp2BMM42T27A
Fixes #1143.
Note
Please ignore until reviewed by @ChrisRackauckas.
Important
Blocked on PureUMFPACK 1.0.0 registering. This sets
PureUMFPACK = "1", which is unresolvable until SciML/PureUMFPACK.jl#36 merges and releases. CI will fail to resolve before then.The bug
PureUMFPACKFactorizationwas the only extension-backed factorization without a load guard in its constructor.PureUMFPACKis a weakdep andLinearSolvePureUMFPACKExtholds the algorithm's onlySciMLBase.solve!method, so withoutusing PureUMFPACKthe algorithm constructed fine and then fell through to the genericsolve!(::LinearCache, ::AbstractFactorization)atsrc/factorization.jl:1, which callsdo_factorization— for which no method exists:PureKLUFactorizationworks in the reporter's script only becausePureKLUis a hard dependency. Nothing was wrong with the solver itself — the issue's ODE reproducer gives correct results once the package is loaded.Changes
Constructor guard.
PureUMFPACKFactorizationgainsthrowerror = trueand an extension check, matchingSparspakFactorization(src/factorization.jl:1872),CliqueTreesFactorization,STRUMPACKFactorization,PardisoJL, and the CUDA/Metal/HSL/MUMPS algorithms. The error is now:Extension solves with
\. PureUMFPACK 1.0 unexportssolveand only marks itpublicon Julia >= 1.11, soPureUMFPACK.solve(F, cache.b)would read as a non-public qualified access when QA runs on the 1.10 LTS.\issolve(F, b)with the defaultrefine = 0, so behavior is identical, and no new ExplicitImports ignore entry is needed. The now-unusedPureUMFPACKmodule import is dropped.Compat
PureUMFPACK = "1". Pinned rather than"0.1, 1"so the error message's advice (plainusing PureUMFPACK) is always correct — on 0.1 that form shadowssolveand breakssolve(prob, alg)(the reason for SciML/PureUMFPACK.jl#36).Regression test. A subprocess asserts the extension is absent, that the constructor errors, and that
throwerror = falsestill builds. The test file's header also switches fromimport PureUMFPACKto plainusing PureUMFPACK, deliberately, to lock in thatsolveis no longer shadowed.No version bump — this repo bumps versions in separate release PRs (#1138, #1131).
Verification
Run locally on Julia 1.11 with PureUMFPACK 1.0.0
Pkg.develop'd:GROUP=LinearSolvePureUMFPACKGROUP=CoreTesting LinearSolve tests passedPureKLUandPureUMFPACKagree to ~1e-14test/Core/resolve.jlalready doesusing PureUMFPACK: PureUMFPACK, so the extension is loaded there and the new guard does not fire.ExplicitImports run directly against
LinearSolvePureUMFPACKExtreports onlygetcolptr,@get_cacheval,LinearCacheandinit_cacheval— all four already covered by the ignore lists intest/qa/qa.jl, and none of themsolve. This PR adds no new QA violation.🤖 Generated with Claude Code
https://claude.ai/code/session_01DR4AenbZvWqp2BMM42T27A