Skip to content

Commit ac8b3d5

Browse files
Restore dense pinv fallback for inverse Jacobian init
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
1 parent 41681c5 commit ac8b3d5

3 files changed

Lines changed: 36 additions & 25 deletions

File tree

lib/NonlinearSolveBase/ext/NonlinearSolveBaseSparseArraysExt.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ dense buffer before factorizing.
8787
"""
8888
function Utils.linsolve_workspace(A::AbstractSparseMatrix)
8989
dense_A = Matrix(A)
90-
workspace, _ = Utils.linsolve_workspace(dense_A)
90+
workspace, _ = invoke(Utils.linsolve_workspace, Tuple{AbstractMatrix}, dense_A)
9191
return workspace, dense_A
9292
end
9393

lib/NonlinearSolveBase/src/utils.jl

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,13 @@ function condition_number(J::AbstractVector)
226226
end
227227
condition_number(::Any) = -1
228228

229-
# Explicit inverse-Jacobian initialization for quasi-Newton update rules that store J⁻¹:
230-
# solve A * X = I with the default linear solver. Scalars, `Diagonal`s, and static
231-
# matrices take native fast paths (mirroring `construct_linear_solver`'s routing) instead
232-
# of the linear-solve cache.
229+
# Explicit inverse-Jacobian initialization for quasi-Newton update rules that store J⁻¹.
230+
# Dense matrices keep the previous `pinv` fallback semantics for rank-deficient
231+
# reinitializations; sparse and structured matrices solve A * X = I through LinearSolve.
233232
linsolve_workspace(A) = nothing, A
234233
linsolve_workspace(A::Diagonal) = nothing, A
235234
linsolve_workspace(A::SMatrix) = nothing, A
235+
linsolve_workspace(A::StridedMatrix) = nothing, A
236236
function linsolve_workspace(A::AbstractMatrix)
237237
LinearAlgebra.checksquare(A)
238238
# the linear solve promotes e.g. integer eltypes; the buffers must hold the promoted
@@ -268,6 +268,25 @@ function linsolve_identity!!(workspace, A::Diagonal)
268268
return Diagonal(D)
269269
end
270270
linsolve_identity!!(workspace, A::AbstractVector) = linsolve_identity!!(workspace, Diagonal(A))
271+
function linsolve_identity!!(workspace, A::StridedMatrix)
272+
LinearAlgebra.checksquare(A)
273+
if LinearAlgebra.istriu(A)
274+
issingular = any(iszero, @view(A[diagind(A)]))
275+
A_ = LinearAlgebra.UpperTriangular(A)
276+
!issingular && return LinearAlgebra.triu!(parent(inv(A_)))
277+
elseif LinearAlgebra.istril(A)
278+
A_ = LinearAlgebra.LowerTriangular(A)
279+
issingular = any(iszero, @view(A_[diagind(A_)]))
280+
!issingular && return LinearAlgebra.tril!(parent(inv(A_)))
281+
else
282+
F = LinearAlgebra.lu(A; check = false)
283+
if LinearAlgebra.issuccess(F)
284+
Ai = LinearAlgebra.inv!(F)
285+
return convert(typeof(parent(Ai)), Ai)
286+
end
287+
end
288+
return pinv(A)
289+
end
271290
# Static matrices solve `A X = I` through LinearSolve's static fast path directly (NOT
272291
# `construct_linear_solver`, whose `SMatrix` route is a plain `A \ b` that yields Inf/NaN
273292
# on a singular Jacobian). The static default's SVD rescue returns the finite min-norm

lib/NonlinearSolveBase/test/linsolve_workspace.jl

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using NonlinearSolveBase: NonlinearSolveBase, Utils
2-
using LinearSolve # linsolve_identity!! routes matrices through the LinearSolve default
2+
using LinearSolve # used by sparse and structured linsolve_identity!! workspaces
33
using LinearAlgebra
44
using Test
55

@@ -20,20 +20,15 @@ using Test
2020
@test Utils.linsolve_identity!!(workspace, A) inv(A_orig)
2121
# nothing workspace (preinverted-init path in QuasiNewton) still works
2222
@test Utils.linsolve_identity!!(nothing, A) inv(A_orig)
23-
# re-inverting the returned inverse (which aliases the workspace's solution
24-
# buffer) recovers A
23+
# re-inverting the returned inverse recovers A
2524
Ai_copy = copy(Utils.linsolve_identity!!(workspace, A))
2625
@test Utils.linsolve_identity!!(workspace, Ai_copy) A_orig
2726
@test Utils.linsolve_identity!!(workspace, Utils.linsolve_identity!!(workspace, A))
2827
A_orig
2928
end
3029
end
3130

32-
@testset "singular input takes the pivoted-QR rescue" begin
33-
# The result is the LinearSolve default algorithm's least-squares generalized
34-
# inverse from its singular-LU → pivoted-QR rescue, NOT the SVD `pinv` (an
35-
# intentional semantics change: same fitness for quasi-Newton initialization,
36-
# different finite matrix).
31+
@testset "dense singular input matches pinv" begin
3732
n = 20
3833
A_singular = let B = rand(n, n)
3934
B[:, 1] .= 0
@@ -45,32 +40,29 @@ end
4540
for A in (A_singular, A_singular_triu)
4641
A_orig = copy(A)
4742
workspace, _ = Utils.linsolve_workspace(A)
43+
@test workspace === nothing
4844
X = Utils.linsolve_identity!!(workspace, A)
4945
@test size(X) == size(A)
5046
@test all(isfinite, X)
51-
# X is a least-squares generalized inverse: A * X projects onto range(A)
47+
@test X pinv(A_orig)
5248
@test A * X * A A atol = 1.0e-8 * norm(A)
5349
@test A == A_orig
54-
# a subsequent nonsingular solve with the same workspace is unaffected
5550
B = rand(n, n) + n * I
5651
@test Utils.linsolve_identity!!(workspace, B) inv(B)
5752
end
5853
end
5954

60-
@testset "workspace reuse does not allocate a matrix copy" begin
55+
@testset "strided matrices use the native inverse path" begin
6156
n = 51
6257
A = rand(n, n) + n * I
63-
workspace, _ = Utils.linsolve_workspace(A)
64-
Utils.linsolve_identity!!(workspace, A) # compile
65-
# Steady state is the LinearSolve refactorization floor (`lu!` ipiv + wrapper); the
66-
# old code copied A (`lu`) and allocated a LAPACK getri work array, both O(n^2).
67-
allocs = @allocated Utils.linsolve_identity!!(workspace, A)
68-
@test allocs < sizeof(A)
58+
workspace, A_ret = Utils.linsolve_workspace(A)
59+
@test workspace === nothing && A_ret === A
60+
@test Utils.linsolve_identity!!(workspace, A) inv(A)
6961

7062
A_triu = triu(A)
71-
Utils.linsolve_identity!!(workspace, A_triu) # compile
72-
allocs_tri = @allocated Utils.linsolve_identity!!(workspace, A_triu)
73-
@test allocs_tri < sizeof(A)
63+
workspace, A_triu_ret = Utils.linsolve_workspace(A_triu)
64+
@test workspace === nothing && A_triu_ret === A_triu
65+
@test Utils.linsolve_identity!!(workspace, A_triu) inv(A_triu)
7466
end
7567

7668
@testset "sparse matrices route through the lincache (no sparse pinv)" begin

0 commit comments

Comments
 (0)