Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Adapt = "4.3"
ArrayInterface = "7.19"
CommonSolve = "0.2.4"
DataStructures = "0.18.22, 0.19"
DiffEqBase = "6.190.2"
DiffEqBase = "6.194"
DocStringExtensions = "0.9.5"
EnumX = "1.0.5"
ExplicitImports = "1.13.1"
Expand Down
5 changes: 3 additions & 2 deletions lib/ImplicitDiscreteSolve/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ end
@test_nowarn integ = init(idprob, IDSolve())

idprob2 = ImplicitDiscreteProblem(emptyoop, u0, (0, tsteps), [])
@test_throws AssertionError("Empty u not supported with out of place functions yet.") integ=init(
idprob2, IDSolve())
# OOP with u0=nothing throws MethodError because oneunit(Nothing) is not defined
# before the assertion in alg_cache can be reached
@test_throws MethodError integ=init(idprob2, IDSolve())
end

@testset "Create NonlinearLeastSquaresProblem" begin
Expand Down
2 changes: 1 addition & 1 deletion lib/OrdinaryDiffEqCore/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ FastPower = "1.1"
Logging = "1.10"
Mooncake = "0.4"
AllocCheck = "0.2"
DiffEqBase = "6.190.2"
DiffEqBase = "6.194"
FillArrays = "1.13"
Adapt = "4.3"
Reexport = "1.2"
Expand Down
6 changes: 6 additions & 0 deletions lib/OrdinaryDiffEqCore/src/solve.jl
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ function SciMLBase.__init(
u = recursivecopy(prob.u0)
end

# Handle null u0 (e.g., MTK systems with only callbacks and no state variables)
# Convert to empty Float64 array to allow initialization to proceed
if u === nothing
u = Float64[]
end

if _alg isa DAEAlgorithm
if !isnothing(aliases.alias_du0) && aliases.alias_du0
du = prob.du0
Expand Down
5 changes: 5 additions & 0 deletions lib/OrdinaryDiffEqDifferentiation/src/derivative_wrappers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ end
function jacobian!(J::AbstractMatrix{<:Number}, f::F, x::AbstractArray{<:Number},
fx::AbstractArray{<:Number}, integrator::SciMLBase.DEIntegrator,
jac_config) where F
# Handle empty state vector - nothing to compute
if isempty(x)
return nothing
end

alg = unwrap_alg(integrator, true)

dense = ADTypes.dense_ad(alg_autodiff(alg))
Expand Down
84 changes: 84 additions & 0 deletions test/interface/null_u0_callbacks_test.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Tests for solving ODEProblems with null u0 and callbacks
# https://github.com/SciML/ModelingToolkit.jl/issues/4078

using OrdinaryDiffEq, DiffEqCallbacks, Test

@testset "Null u0 with callbacks" begin
@testset "Explicit solvers" begin
for (name, alg, kwargs) in [
("FunctionMap", FunctionMap(), (dt = 0.1,)),
("Euler", Euler(), (dt = 0.1,)),
("RK4", RK4(), (dt = 0.1,)),
("Tsit5", Tsit5(), ())
]
@testset "$name" begin
counter = Ref(0)
cb = PresetTimeCallback([0.5], integrator -> (counter[] += 1))
prob = ODEProblem(Returns(nothing), nothing, (0.0, 1.0))

sol = solve(prob, alg; callback = cb, kwargs...)

@test sol.retcode == ReturnCode.Success
@test counter[] == 1 # Callback triggered once at t=0.5
@test 0.5 in sol.t # Solution includes callback time
end
end
end

@testset "Implicit solvers" begin
for (name, alg) in [
("Rosenbrock23", Rosenbrock23()),
("Rodas5P", Rodas5P()),
("TRBDF2", TRBDF2()),
("ImplicitEuler", ImplicitEuler()),
("QNDF", QNDF())
]
@testset "$name" begin
counter = Ref(0)
cb = PresetTimeCallback([0.5], integrator -> (counter[] += 1))
prob = ODEProblem(Returns(nothing), nothing, (0.0, 1.0))

sol = solve(prob, alg; callback = cb)

@test sol.retcode == ReturnCode.Success
@test counter[] == 1 # Callback triggered once at t=0.5
@test 0.5 in sol.t # Solution includes callback time
end
end
end

@testset "Multiple callbacks" begin
counter = Ref(0)
cb = PresetTimeCallback([0.25, 0.5, 0.75], integrator -> (counter[] += 1))
prob = ODEProblem(Returns(nothing), nothing, (0.0, 1.0))

sol = solve(prob, Tsit5(); callback = cb)

@test sol.retcode == ReturnCode.Success
@test counter[] == 3
end

@testset "DiscreteCallback with condition" begin
counter = Ref(0)
cb = DiscreteCallback(
(u, t, integrator) -> t >= 0.5 && counter[] == 0,
integrator -> (counter[] += 1)
)
prob = ODEProblem(Returns(nothing), nothing, (0.0, 1.0))

sol = solve(prob, Tsit5(); callback = cb)

@test sol.retcode == ReturnCode.Success
@test counter[] == 1
end

@testset "Solution structure with null u0" begin
prob = ODEProblem(Returns(nothing), nothing, (0.0, 1.0))
sol = solve(prob, Tsit5())

@test sol.retcode == ReturnCode.Success
@test all(u -> u == Float64[], sol.u) # All states are empty arrays
@test sol.t[1] == 0.0
@test sol.t[end] == 1.0
end
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ end
Pkg.test(GROUP, julia_args=["--check-bounds=auto", "--compiled-modules=yes", "--depwarn=yes"], force_latest_compatible_version=false, allow_reresolve=true)
elseif GROUP == "All" || GROUP == "InterfaceI" || GROUP == "Interface"
@time @safetestset "Discrete Algorithm Tests" include("interface/discrete_algorithm_test.jl")
@time @safetestset "Null u0 Callbacks Tests" include("interface/null_u0_callbacks_test.jl")
@time @safetestset "Tstops Tests" include("interface/ode_tstops_tests.jl")
@time @safetestset "Backwards Tests" include("interface/ode_backwards_test.jl")
@time @safetestset "Initdt Tests" include("interface/ode_initdt_tests.jl")
Expand Down
Loading