Skip to content

Commit 7518dff

Browse files
Merge pull request #4724 from SciML/fbc/aij-autodiff
analyze_initialization_jacobian: finite differences by default, ADTypes autodiff option
2 parents 2eefd00 + 2a49645 commit 7518dff

2 files changed

Lines changed: 47 additions & 5 deletions

File tree

src/initialization.jl

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function MTKBase.get_initialization_problem_type(
4949
end
5050

5151
"""
52-
analyze_initialization_jacobian(prob; rtol = 1e-8, atol = 0.0, threshold = 1e-3, verbose = true)
52+
analyze_initialization_jacobian(prob; rtol = 1e-8, atol = 0.0, threshold = 1e-3, verbose = true, autodiff = nothing)
5353
5454
Diagnose rank deficiency of a system's initialization problem by inspecting the singular
5555
value decomposition of its residual Jacobian, and report both the **unknowns** that span
@@ -70,8 +70,11 @@ the offending degrees of freedom explicit:
7070
constrain any additional degree of freedom. These explain why an initialization can
7171
have more equations than unknowns yet still be underdetermined.
7272
73-
It evaluates the Jacobian of the initialization residual at the initial guess `u0` (via
74-
`ForwardDiff`) and computes its SVD. `prob` may be a problem that carries initialization
73+
It evaluates the Jacobian of the initialization residual at the initial guess `u0` and
74+
computes its SVD. By default the Jacobian is formed by central finite differences, which
75+
reuses the already-compiled residual function; pass an ADTypes backend via `autodiff`
76+
(e.g. `AutoForwardDiff()`) to differentiate through `DifferentiationInterface` instead,
77+
at the cost of compiling the residual for the backend's number types. `prob` may be a problem that carries initialization
7578
data (e.g. an `ODEProblem`/`DAEProblem` built from a `System`), or an initialization
7679
`NonlinearProblem`/`NonlinearLeastSquaresProblem` directly.
7780
@@ -83,6 +86,11 @@ data (e.g. an `ODEProblem`/`DAEProblem` built from a `System`), or an initializa
8386
- `threshold`: only unknowns/equations whose participation exceeds this value are
8487
reported.
8588
- `verbose`: print a human-readable report.
89+
- `autodiff`: `nothing` (default) computes the Jacobian by central finite differences
90+
with step `cbrt(eps)`, whose truncation error is far below the default rank tolerance
91+
and whose first call avoids recompiling the generated residual (the dominant cost of
92+
dual-number AD on large systems). Alternatively an ADTypes backend evaluated through
93+
`DifferentiationInterface`.
8694
8795
# Returns
8896
@@ -106,7 +114,7 @@ unknowns); a `redundancy` of `0` means full row rank (no redundant equations).
106114
rank deficient at a particular operating point, and vice versa.
107115
"""
108116
function analyze_initialization_jacobian(
109-
prob; rtol = 1.0e-8, atol = 0.0, threshold = 1.0e-3, verbose = true
117+
prob; rtol = 1.0e-8, atol = 0.0, threshold = 1.0e-3, verbose = true, autodiff = nothing
110118
)
111119
empty_result = (;
112120
jacobian = nothing, singular_values = Float64[], rank = 0,
@@ -133,7 +141,8 @@ function analyze_initialization_jacobian(
133141
else
134142
u -> f(u, p)
135143
end
136-
J = ForwardDiff.jacobian(residual, u0)
144+
J = autodiff === nothing ? _central_difference_jacobian(residual, u0) :
145+
DI.jacobian(residual, autodiff, u0)
137146
nrows, ncols = size(J)
138147
fact = svd(J; full = true)
139148
S = fact.S
@@ -210,6 +219,29 @@ end
210219
# Return the initialization `NonlinearProblem`/`NonlinearLeastSquaresProblem` carried by
211220
# `prob`, or `prob` itself if it is already a nonlinear problem, or `nothing` if there is
212221
# no initialization problem to analyze.
222+
# Dense Jacobian by central finite differences. Reuses the residual function already
223+
# compiled for the element type of `u0`; `ForwardDiff.jacobian` instead triggers a
224+
# dual-number recompilation of the generated residual, which dominates the runtime on
225+
# large systems. The O(h^2) truncation error (h = cbrt(eps)) is far below the default
226+
# singular-value tolerance of the rank analysis.
227+
function _central_difference_jacobian(residual, u0)
228+
T = eltype(u0)
229+
up = copy(u0)
230+
um = copy(u0)
231+
J = nothing
232+
for i in eachindex(u0)
233+
h = cbrt(eps(T)) * max(one(T), abs(u0[i]))
234+
up[i] = u0[i] + h
235+
um[i] = u0[i] - h
236+
col = (residual(up) .- residual(um)) ./ (2h)
237+
J === nothing && (J = similar(col, length(col), length(u0)))
238+
J[:, i] = col
239+
up[i] = u0[i]
240+
um[i] = u0[i]
241+
end
242+
return J === nothing ? zeros(T, 0, length(u0)) : J
243+
end
244+
213245
function _initialization_problem(prob)
214246
prob isa SciMLBase.AbstractNonlinearProblem && return prob
215247
f = prob.f

test/initialization_jacobian_analysis.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,13 @@ prob4 = ODEProblem(sys4, [a => 1.0, b => 2.0], (0.0, 1.0))
5555
res4 = analyze_initialization_jacobian(prob4; verbose = false)
5656
@test res4.nullity == 0
5757
@test res4.redundancy == 0
58+
59+
# The default finite-difference Jacobian must agree with an AD backend passed through
60+
# the `autodiff` option.
61+
using ADTypes: AutoForwardDiff
62+
res_ad = analyze_initialization_jacobian(prob; verbose = false, autodiff = AutoForwardDiff())
63+
res_fd = analyze_initialization_jacobian(prob; verbose = false)
64+
@test res_ad.rank == res_fd.rank
65+
@test res_ad.nullity == res_fd.nullity
66+
@test res_ad.redundancy == res_fd.redundancy
67+
@test isapprox(res_ad.jacobian, res_fd.jacobian; atol = 1.0e-6, rtol = 1.0e-6)

0 commit comments

Comments
 (0)