Skip to content

Commit e9d545e

Browse files
ChrisRackauckas-ClaudeChrisRackauckasclaude
authored
Supply the _costabs Dual overload SupernodalLU's matching documents (#1118)
`src/SupernodalLU/matching.jl` says of `_costabs`: Value types with no exact Float64 conversion (e.g. ForwardDiff.Dual) get an overload in the package extension that extracts the primal value. No such overload exists. It was left behind when the solver was vendored from PurePardiso.jl, so the comment documents behaviour the package does not have. MC64 matching decides its max-product assignment in Float64, so `_costabs` needs a real magnitude per entry. Without the overload, factoring a `Dual` matrix whose diagonal is missing or weak - exactly when `matching = :auto` engages - throws: MethodError: no method matching Float64(::ForwardDiff.Dual{Nothing, Float64, 1}) _costabs at matching.jl:206 [inlined] mc64_matching(::SparseMatrixCSC{Dual{Nothing, Float64, 1}, Int64}) Only the *ordering* of candidate pivots comes from these numbers; the factorization itself stays in Dual arithmetic, so taking the primal loses nothing. Reachable today through `LinearSolve.__init`. The public path is unaffected: `__dual_init` strips duals before any algorithm sees them, so `solve` and `init` on a Dual sparse problem factor in Float64 and never reach this code. `_scalarval` needs no such fix - returning `NaN` for a Dual is intended and documented there (`anorm` is informational; thresholds stay in native arithmetic). Test: an anti-diagonal matrix, which has no structural diagonal at all so matching always engages, checked in both value and derivative against the closed form. It fails on the unfixed code at the `snlu` call. GROUP=Core passes. Claude-Session: https://claude.ai/code/session_017rr42T1vFRRT8D7DMAmJzf Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 4c58ce2 commit e9d545e

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

ext/LinearSolveForwardDiffExt.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,4 +763,15 @@ function update_partials_list!(partial_matrix::SparseMatrixCSC, list_cache)
763763
return list_cache
764764
end
765765

766+
767+
# The MC64 matching in the vendored SupernodalLU solver runs its combinatorial
768+
# side (the max-product assignment) in Float64, so it needs a real magnitude
769+
# for a `Dual` entry. Only the *ordering* of candidate pivots is decided from
770+
# these numbers; the factorization itself stays in Dual arithmetic, so taking
771+
# the primal here loses nothing. Without this, `snlu` on a Dual matrix with a
772+
# missing or weak structural diagonal - exactly when `matching = :auto`
773+
# engages - throws `MethodError: no method matching Float64(::Dual)`.
774+
@inline LinearSolve.SupernodalLU._costabs(x::Dual) =
775+
LinearSolve.SupernodalLU._costabs(ForwardDiff.value(x))
776+
766777
end

test/Core/forwarddiff_overloads.jl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,3 +750,28 @@ struct ReinterpretTestTag end
750750
cache = init(LinearProblem(A, b), nothing)
751751
@test solve!(cache).u sol.u
752752
end
753+
754+
@testset "SupernodalLU matching accepts Dual entries" begin
755+
# MC64 matching decides its assignment in Float64, so it needs a real
756+
# magnitude per entry. An anti-diagonal matrix has no structural diagonal
757+
# at all, so `matching = :auto` engages and the factorization goes through
758+
# `_costabs` — which threw on Duals until the ForwardDiff extension
759+
# supplied the primal-extracting overload its docstring promised.
760+
SNLU = LinearSolve.SupernodalLU
761+
n = 40
762+
vals = [ForwardDiff.Dual{Nothing}(2.0 + i / n, 1.0) for i in 1:n]
763+
P = sparse(collect(n:-1:1), collect(1:n), vals, n, n)
764+
@test SNLU.needs_matching(P)
765+
F = SNLU.snlu(P)
766+
@test F.matched
767+
768+
# Solving must be right in both the value and the derivative. With
769+
# A(t) = diag-reversed(2 + i/n + t) and b fixed, each x_i = b_j / a_i, so
770+
# dx_i/dt = -b_j / a_i^2.
771+
b = randn(n)
772+
x = similar(b, eltype(vals))
773+
SNLU.solve!(x, F, ForwardDiff.Dual{Nothing}.(b, 0.0))
774+
@test ForwardDiff.value.(x) [b[n + 1 - i] / (2.0 + i / n) for i in 1:n] rtol = 1.0e-10
775+
@test ForwardDiff.partials.(x, 1)
776+
[-b[n + 1 - i] / (2.0 + i / n)^2 for i in 1:n] rtol = 1.0e-10
777+
end

0 commit comments

Comments
 (0)