Skip to content

Error clearly when PureUMFPACKFactorization is used without PureUMFPACK - #1144

Merged
ChrisRackauckas merged 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:pureumfpack-extension-guard
Aug 7, 2026
Merged

ChrisRackauckas merged 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:pureumfpack-extension-guard

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member

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

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) at src/factorization.jl:1, 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 in the reporter's script only because PureKLU is 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. PureUMFPACKFactorization gains throwerror = true and an extension check, matching SparspakFactorization (src/factorization.jl:1872), CliqueTreesFactorization, STRUMPACKFactorization, PardisoJL, and the CUDA/Metal/HSL/MUMPS algorithms. The error is now:

PureUMFPACKFactorization requires that PureUMFPACK is loaded, i.e. `using PureUMFPACK`

Extension solves with \. PureUMFPACK 1.0 unexports solve and only marks it public on Julia >= 1.11, so PureUMFPACK.solve(F, cache.b) would read as a non-public qualified access when QA runs on the 1.10 LTS. \ is solve(F, b) with the default refine = 0, so behavior is identical, and no new ExplicitImports ignore entry is needed. The now-unused PureUMFPACK module import is dropped.

Compat PureUMFPACK = "1". Pinned rather than "0.1, 1" so the error message's advice (plain using PureUMFPACK) is always correct — on 0.1 that form shadows solve and breaks solve(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 = false still builds. The test file's header also switches from import PureUMFPACK to plain using PureUMFPACK, deliberately, to lock in that solve is 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:

Suite Result
GROUP=LinearSolvePureUMFPACK 9/9 pass
GROUP=Core Testing LinearSolve tests passed
Issue #1143 reproducer PureKLU and PureUMFPACK agree to ~1e-14

test/Core/resolve.jl already does using PureUMFPACK: PureUMFPACK, so the extension is loaded there and the new guard does not fire.

ExplicitImports run directly against LinearSolvePureUMFPACKExt reports only getcolptr, @get_cacheval, LinearCache and init_cacheval — all four already covered by the ignore lists in test/qa/qa.jl, and none of them solve. This PR adds no new QA violation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DR4AenbZvWqp2BMM42T27A

`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
Comment thread ext/LinearSolvePureUMFPACKExt.jl Outdated
# `\` 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No, this isn't PureUMFPACK... it needs to use the PureUMFPACK solve on all versions...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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
@ChrisRackauckas
ChrisRackauckas marked this pull request as ready for review August 7, 2026 01:53
@ChrisRackauckas
ChrisRackauckas merged commit a1712f5 into SciML:main Aug 7, 2026
58 of 62 checks passed
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.

Cannot solve ODE with PureUMFPACKFactorization

2 participants