Skip to content

Commit ff2f38e

Browse files
Guard JacobianCache callable for empty u, add empty u0 tests
Also guard the JacobianCache callable to early-return the empty Jacobian when length(u) == 0, preventing DI.jacobian! from being called with nothing di_extras. Add comprehensive test for the u0=nothing pathway: IIP and OOP NonlinearProblems with Float64[] u0, SymbolCache-based sys with observed quantities, parameter mutation via symbolic indexing, direct solve, caching interface, and step interface. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6a304a4 commit ff2f38e

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

lib/NonlinearSolveBase/src/jacobian.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ end
176176

177177
## Actually Compute the Jacobian
178178
function (cache::JacobianCache)(u)
179+
# Empty state vector — Jacobian is 0×0, nothing to compute
180+
length(u) == 0 && return cache.J
181+
179182
cache.stats.njacs += 1
180183
(; f, J, p) = cache
181184
if SciMLBase.isinplace(f)

test/core_tests.jl

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,3 +542,71 @@ end
542542
solve!(iter)
543543
@test precs.i == 1
544544
end
545+
546+
@testitem "Empty u0 (u0=nothing pathway)" tags = [:core] begin
547+
using SymbolicIndexingInterface
548+
549+
# Mimics MTK systems with no state variables — only parameters and observed quantities.
550+
# OrdinaryDiffEqCore converts u0=nothing to Float64[] before reaching NonlinearSolve.
551+
sys = SymbolCache(nothing, Dict(:a => 1, :b => 2), nothing)
552+
553+
function observed_fn(sym)
554+
if sym == :result
555+
return (u, p) -> p[1] + p[2]
556+
end
557+
return nothing
558+
end
559+
560+
@testset "IIP" begin
561+
f_iip!(resid, u, p) = nothing
562+
nlfunc = NonlinearFunction(f_iip!; observed = observed_fn, sys = sys)
563+
prob = NonlinearProblem{true}(nlfunc, Float64[], [3.0, 4.0])
564+
565+
# init/solve cycle
566+
cache = @test_nowarn init(prob, NewtonRaphson())
567+
@test state_values(cache) == Float64[]
568+
@test parameter_values(cache) == [3.0, 4.0]
569+
570+
# parameter mutation through symbolic indexing
571+
cache.ps[:a] = 10.0
572+
cache.ps[:b] = 20.0
573+
@test parameter_values(cache) == [10.0, 20.0]
574+
575+
sol = solve!(cache)
576+
@test SciMLBase.successful_retcode(sol)
577+
@test isempty(sol.u)
578+
end
579+
580+
@testset "OOP" begin
581+
f_oop(u, p) = Float64[]
582+
nlfunc = NonlinearFunction(f_oop; observed = observed_fn, sys = sys)
583+
prob = NonlinearProblem(nlfunc, Float64[], [5.0, 6.0])
584+
585+
sol = @test_nowarn solve(prob, NewtonRaphson())
586+
@test SciMLBase.successful_retcode(sol)
587+
@test isempty(sol.u)
588+
end
589+
590+
@testset "Default algorithm" begin
591+
f_iip!(resid, u, p) = nothing
592+
nlfunc = NonlinearFunction(f_iip!; observed = observed_fn, sys = sys)
593+
prob = NonlinearProblem{true}(nlfunc, Float64[], [1.0, 2.0])
594+
595+
sol = @test_nowarn solve(prob)
596+
@test SciMLBase.successful_retcode(sol)
597+
@test isempty(sol.u)
598+
end
599+
600+
@testset "Step interface" begin
601+
f_iip!(resid, u, p) = nothing
602+
nlfunc = NonlinearFunction(f_iip!; observed = observed_fn, sys = sys)
603+
prob = NonlinearProblem{true}(nlfunc, Float64[], [1.0, 2.0])
604+
605+
cache = init(prob, NewtonRaphson())
606+
for i in 1:10
607+
step!(cache)
608+
cache.force_stop && break
609+
end
610+
@test SciMLBase.successful_retcode(cache.retcode)
611+
end
612+
end

0 commit comments

Comments
 (0)