Skip to content

Commit f52f8e1

Browse files
Unwrap operator mass matrices before findall in Differentiation
Seeding a sparsity pattern from the mass matrix uses findall and getindex, which a SciMLOperator does not support, so a MatrixOperator mass matrix combined with a jac_prototype threw MethodError: keys(::MatrixOperator). The same pattern appeared at four places: prepare_user_sparsity and its sparsity unwrap in alg_utils.jl, and build_jac_config and sparsity_colorvec in derivative_wrappers.jl. Route all four through one concrete_mass_matrix helper, which reads the operator's cached array rather than re-evaluating it, since only the structural pattern matters and this runs before init.
1 parent dc7f91a commit f52f8e1

5 files changed

Lines changed: 91 additions & 8 deletions

File tree

lib/OrdinaryDiffEqDifferentiation/src/OrdinaryDiffEqDifferentiation.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ import DiffEqBase: OrdinaryDiffEqTag
5858
is_sparse(::Any) = false
5959
is_sparse_csc(::Any) = false
6060

61+
# Seeding a sparsity pattern from the mass matrix uses `findall`/`getindex`, which a
62+
# SciMLOperator does not support. `convert` reads the operator's currently-cached array
63+
# rather than re-evaluating it, which is what is wanted here: only the structural
64+
# pattern matters, and this runs before `init`.
65+
concrete_mass_matrix(mm) = mm
66+
concrete_mass_matrix(mm::AbstractSciMLOperator) = convert(AbstractMatrix, mm)
67+
6168
# These will error if called without the extension, but should never be called
6269
# on non-sparse types due to the is_sparse checks
6370
function nonzeros end

lib/OrdinaryDiffEqDifferentiation/src/alg_utils.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,21 +123,22 @@ function prepare_user_sparsity(ad_alg, prob)
123123
@. @view(jac_prototype[idxs]) = 1
124124
end
125125
else
126-
idxs = findall(!iszero, prob.f.mass_matrix)
126+
mm = concrete_mass_matrix(prob.f.mass_matrix)
127+
idxs = findall(!iszero, mm)
127128
for idx in idxs
128-
sparsity[idx] = prob.f.mass_matrix[idx]
129+
sparsity[idx] = mm[idx]
129130
end
130131

131132
if !isnothing(jac_prototype)
132133
for idx in idxs
133-
jac_prototype[idx] = prob.f.mass_matrix[idx]
134+
jac_prototype[idx] = mm[idx]
134135
end
135136
end
136137
end
137138
end
138139

139140
# KnownJacobianSparsityDetector needs an AbstractMatrix
140-
sparsity = sparsity isa MatrixOperator ? sparsity.A : sparsity
141+
sparsity = concrete_mass_matrix(sparsity)
141142

142143
color_alg = SciMLBase.has_colorvec(prob.f) ?
143144
ConstantColoringAlgorithm(

lib/OrdinaryDiffEqDifferentiation/src/derivative_wrappers.jl

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,9 @@ function build_jac_config(
295295
idxs = diagind(jac_prototype)
296296
@. @view(jac_prototype[idxs]) = 1
297297
else
298-
idxs = findall(!iszero, f.mass_matrix)
299-
@. @view(jac_prototype[idxs]) = @view(f.mass_matrix[idxs])
298+
mm = concrete_mass_matrix(f.mass_matrix)
299+
idxs = findall(!iszero, mm)
300+
@. @view(jac_prototype[idxs]) = @view(mm[idxs])
300301
end
301302
end
302303

@@ -480,8 +481,9 @@ function sparsity_colorvec(f::F, x) where {F}
480481
idxs = diagind(sparsity)
481482
@. @view(sparsity[idxs]) = 1
482483
else
483-
idxs = findall(!iszero, f.mass_matrix)
484-
@. @view(sparsity[idxs]) = @view(f.mass_matrix[idxs])
484+
mm = concrete_mass_matrix(f.mass_matrix)
485+
idxs = findall(!iszero, mm)
486+
@. @view(sparsity[idxs]) = @view(mm[idxs])
485487
end
486488
end
487489

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using OrdinaryDiffEqDifferentiation
2+
using SparseArrays
3+
using SciMLOperators
4+
using SciMLBase
5+
using ADTypes
6+
using Test
7+
8+
f!(du, u, p, t) = (du .= u; nothing)
9+
ad_alg = AutoForwardDiff()
10+
11+
# `findall`/`getindex` on the mass matrix need a concrete matrix; a SciMLOperator
12+
# (e.g. `MatrixOperator`) supports neither directly, so it must be unwrapped first.
13+
@testset "MatrixOperator mass matrix" begin
14+
M = sparse([1.0 2.0; 0.0 1.0])
15+
mass_matrix = MatrixOperator(M; update_func = (A, u, p, t) -> M)
16+
jac_prototype = sparse([1.0 0.0; 0.0 1.0])
17+
sparsity = copy(jac_prototype)
18+
odef = ODEFunction(f!; mass_matrix, jac_prototype, sparsity)
19+
prob = ODEProblem(odef, ones(2), (0.0, 1.0))
20+
21+
OrdinaryDiffEqDifferentiation.prepare_user_sparsity(ad_alg, prob)
22+
@test Matrix(prob.f.sparsity) == Matrix(M)
23+
@test Matrix(prob.f.jac_prototype) == Matrix(M)
24+
end
25+
26+
@testset "UniformScaling mass matrix (unchanged path)" begin
27+
jac_prototype = sparse([1.0 0.0; 0.0 1.0])
28+
sparsity = copy(jac_prototype)
29+
odef = ODEFunction(f!; jac_prototype, sparsity)
30+
prob = ODEProblem(odef, ones(2), (0.0, 1.0))
31+
32+
OrdinaryDiffEqDifferentiation.prepare_user_sparsity(ad_alg, prob)
33+
@test Matrix(prob.f.sparsity) == Matrix(jac_prototype)
34+
@test Matrix(prob.f.jac_prototype) == Matrix(jac_prototype)
35+
end
36+
37+
@testset "Plain sparse mass matrix (unchanged path)" begin
38+
M = sparse([1.0 2.0; 0.0 1.0])
39+
jac_prototype = sparse([1.0 0.0; 0.0 1.0])
40+
sparsity = copy(jac_prototype)
41+
odef = ODEFunction(f!; mass_matrix = M, jac_prototype, sparsity)
42+
prob = ODEProblem(odef, ones(2), (0.0, 1.0))
43+
44+
OrdinaryDiffEqDifferentiation.prepare_user_sparsity(ad_alg, prob)
45+
@test Matrix(prob.f.sparsity) == Matrix(M)
46+
@test Matrix(prob.f.jac_prototype) == Matrix(M)
47+
end
48+
49+
@testset "DiagonalOperator mass matrix" begin
50+
mass_matrix = DiagonalOperator([1.0, 2.0])
51+
jac_prototype = sparse([1.0 0.0; 0.0 1.0])
52+
sparsity = copy(jac_prototype)
53+
odef = ODEFunction(f!; mass_matrix, jac_prototype, sparsity)
54+
prob = ODEProblem(odef, ones(2), (0.0, 1.0))
55+
56+
OrdinaryDiffEqDifferentiation.prepare_user_sparsity(ad_alg, prob)
57+
@test Matrix(prob.f.sparsity) == [1.0 0.0; 0.0 2.0]
58+
@test Matrix(prob.f.jac_prototype) == [1.0 0.0; 0.0 2.0]
59+
end
60+
61+
# `sparsity` defaults to (and aliases) `jac_prototype`, which is how #2929's report
62+
# constructs the problem.
63+
@testset "MatrixOperator mass matrix, sparsity defaulted" begin
64+
M = sparse([1.0 2.0; 0.0 1.0])
65+
mass_matrix = MatrixOperator(M; update_func = (A, u, p, t) -> M)
66+
jac_prototype = sparse([1.0 0.0; 0.0 1.0])
67+
odef = ODEFunction(f!; mass_matrix, jac_prototype)
68+
prob = ODEProblem(odef, ones(2), (0.0, 1.0))
69+
70+
OrdinaryDiffEqDifferentiation.prepare_user_sparsity(ad_alg, prob)
71+
@test Matrix(prob.f.jac_prototype) == Matrix(M)
72+
end

lib/OrdinaryDiffEqDifferentiation/test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ end
3030
# Run functional tests
3131
if TEST_GROUP ("QA", "Sparse", "ModelingToolkit")
3232
@time @safetestset "DAE jacobian2W sparse" include("dae_jacobian2w_sparse_tests.jl")
33+
@time @safetestset "prepare_user_sparsity mass matrix" include("prepare_user_sparsity_tests.jl")
3334
@time @safetestset "nzval helpers" include("nzval_helpers_tests.jl")
3435
@time @safetestset "prepare_sparse_jac!" include("prepare_sparse_jac_tests.jl")
3536
@time @safetestset "OOP J_t Tracking" include("oop_jt_tracking_test.jl")

0 commit comments

Comments
 (0)