From ff465b81b29d9d20e7e710176109f854237bb0af Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Fri, 17 Apr 2026 21:59:30 +0530 Subject: [PATCH 1/2] Fix FieldError for Magnus/Linear integrators with OrdinaryDiffEqDifferentiation (#3232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Magnus integrators (MagnusGL6, MagnusGL8, etc.) and other OrdinaryDiffEqLinearExponentialAlgorithm subtypes have no `autodiff` field β€” only `krylov`, `m`, `iop`. When OrdinaryDiffEqDifferentiation is loaded (e.g. via DifferentialEquations.jl), the generic `_alg_autodiff(::OrdinaryDiffEqExponentialAlgorithm{CS,AD})` dispatch would call `alg.autodiff` on these types, causing a FieldError crash. Fix: - Import OrdinaryDiffEqLinearExponentialAlgorithm into OrdinaryDiffEqDifferentiation - Add _alg_autodiff(::OrdinaryDiffEqLinearExponentialAlgorithm) returning Val{false}(), intercepting calls before the generic ExponentialAlgorithm dispatch that accesses the nonexistent field - The existing prepare_alg override in OrdinaryDiffEqCore (line 298) already handles the prepare_alg path correctly Also adds regression tests verifying _alg_autodiff, prepare_alg, and forwarddiffs_model all work correctly for LinearExponentialAlgorithm subtypes. --- .../src/OrdinaryDiffEqDifferentiation.jl | 1 + .../src/alg_utils.jl | 8 ++++++ .../test/differentiation_traits_tests.jl | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/lib/OrdinaryDiffEqDifferentiation/src/OrdinaryDiffEqDifferentiation.jl b/lib/OrdinaryDiffEqDifferentiation/src/OrdinaryDiffEqDifferentiation.jl index 8bb0cbe90f9..bca939c332e 100644 --- a/lib/OrdinaryDiffEqDifferentiation/src/OrdinaryDiffEqDifferentiation.jl +++ b/lib/OrdinaryDiffEqDifferentiation/src/OrdinaryDiffEqDifferentiation.jl @@ -36,6 +36,7 @@ using OrdinaryDiffEqCore: OrdinaryDiffEqAlgorithm, OrdinaryDiffEqAdaptiveImplici OrdinaryDiffEqImplicitAlgorithm, CompositeAlgorithm, OrdinaryDiffEqExponentialAlgorithm, OrdinaryDiffEqAdaptiveExponentialAlgorithm, + OrdinaryDiffEqLinearExponentialAlgorithm, StochasticDiffEqNewtonAlgorithm, StochasticDiffEqNewtonAdaptiveAlgorithm, StochasticDiffEqJumpNewtonAdaptiveAlgorithm, StochasticDiffEqJumpNewtonDiffusionAdaptiveAlgorithm, diff --git a/lib/OrdinaryDiffEqDifferentiation/src/alg_utils.jl b/lib/OrdinaryDiffEqDifferentiation/src/alg_utils.jl index 75b724ab495..c3a0de7f669 100644 --- a/lib/OrdinaryDiffEqDifferentiation/src/alg_utils.jl +++ b/lib/OrdinaryDiffEqDifferentiation/src/alg_utils.jl @@ -32,6 +32,12 @@ function _alg_autodiff( ) where {CS, AD, FDT, ST, CJ, Controller} return Val{AD}() end +# OrdinaryDiffEqLinearExponentialAlgorithm subtypes (Magnus integrators, LieEuler, +# CG methods, etc.) have NO autodiff field β€” their only fields are krylov, m, iop. +# They must be excluded before the generic ExponentialAlgorithm dispatch below. +function _alg_autodiff(::OrdinaryDiffEqLinearExponentialAlgorithm) + return Val{false}() +end function _alg_autodiff( alg::Union{ OrdinaryDiffEqExponentialAlgorithm{CS, AD}, @@ -66,6 +72,8 @@ Base.@pure function determine_chunksize(u, CS) end end + + function DiffEqBase.prepare_alg( alg::Union{ OrdinaryDiffEqAdaptiveImplicitAlgorithm{ diff --git a/lib/OrdinaryDiffEqDifferentiation/test/differentiation_traits_tests.jl b/lib/OrdinaryDiffEqDifferentiation/test/differentiation_traits_tests.jl index b00c3c8e3bb..a5923eea4e7 100644 --- a/lib/OrdinaryDiffEqDifferentiation/test/differentiation_traits_tests.jl +++ b/lib/OrdinaryDiffEqDifferentiation/test/differentiation_traits_tests.jl @@ -43,3 +43,31 @@ sol = solve(prob2, Rosenbrock23(autodiff = AutoForwardDiff(chunksize = 1))) sol = solve(prob2, Rosenbrock23(autodiff = AutoFiniteDiff())) @test β‰ˆ(good_sol[:, end], sol[:, end], rtol = 1.0e-2) + +# Regression test for issue #3232: +# OrdinaryDiffEqLinearExponentialAlgorithm subtypes (MagnusGL6, etc.) +# have no `autodiff` field; _alg_autodiff and prepare_alg must not crash. +using OrdinaryDiffEqDifferentiation: _alg_autodiff +using OrdinaryDiffEqCore: OrdinaryDiffEqLinearExponentialAlgorithm +using DiffEqBase: prepare_alg + +struct MockMagnusAlg <: OrdinaryDiffEqLinearExponentialAlgorithm + krylov::Bool + m::Int + iop::Int +end + +@testset "LinearExponentialAlgorithm autodiff traits (issue #3232)" begin + mock = MockMagnusAlg(false, 30, 0) + + # _alg_autodiff must return Val{false}() instead of accessing alg.autodiff + @test _alg_autodiff(mock) == Val{false}() + + # prepare_alg must return the algorithm unchanged (no AD preparation needed) + u0 = ones(2) + mock_prob = ODEProblem((du, u, p, t) -> du .= 0, u0, (0.0, 1.0)) + @test prepare_alg(mock, u0, nothing, mock_prob) === mock + + # forwarddiffs_model must return false + @test SciMLBase.forwarddiffs_model(mock) == false +end From 8f475399265ccd1a7b8b8d646377426379019401 Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Fri, 17 Apr 2026 23:30:59 +0530 Subject: [PATCH 2/2] Use real Magnus integrators in regression test for #3232 --- .../src/alg_utils.jl | 2 - .../test/differentiation_traits_tests.jl | 45 +++++++++---------- .../test/linear_method_tests.jl | 18 ++++++++ 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/lib/OrdinaryDiffEqDifferentiation/src/alg_utils.jl b/lib/OrdinaryDiffEqDifferentiation/src/alg_utils.jl index c3a0de7f669..073e88b4c5c 100644 --- a/lib/OrdinaryDiffEqDifferentiation/src/alg_utils.jl +++ b/lib/OrdinaryDiffEqDifferentiation/src/alg_utils.jl @@ -72,8 +72,6 @@ Base.@pure function determine_chunksize(u, CS) end end - - function DiffEqBase.prepare_alg( alg::Union{ OrdinaryDiffEqAdaptiveImplicitAlgorithm{ diff --git a/lib/OrdinaryDiffEqDifferentiation/test/differentiation_traits_tests.jl b/lib/OrdinaryDiffEqDifferentiation/test/differentiation_traits_tests.jl index a5923eea4e7..c5a9b72d714 100644 --- a/lib/OrdinaryDiffEqDifferentiation/test/differentiation_traits_tests.jl +++ b/lib/OrdinaryDiffEqDifferentiation/test/differentiation_traits_tests.jl @@ -45,29 +45,24 @@ sol = solve(prob2, Rosenbrock23(autodiff = AutoFiniteDiff())) @test β‰ˆ(good_sol[:, end], sol[:, end], rtol = 1.0e-2) # Regression test for issue #3232: -# OrdinaryDiffEqLinearExponentialAlgorithm subtypes (MagnusGL6, etc.) -# have no `autodiff` field; _alg_autodiff and prepare_alg must not crash. -using OrdinaryDiffEqDifferentiation: _alg_autodiff -using OrdinaryDiffEqCore: OrdinaryDiffEqLinearExponentialAlgorithm -using DiffEqBase: prepare_alg - -struct MockMagnusAlg <: OrdinaryDiffEqLinearExponentialAlgorithm - krylov::Bool - m::Int - iop::Int -end - -@testset "LinearExponentialAlgorithm autodiff traits (issue #3232)" begin - mock = MockMagnusAlg(false, 30, 0) - - # _alg_autodiff must return Val{false}() instead of accessing alg.autodiff - @test _alg_autodiff(mock) == Val{false}() - - # prepare_alg must return the algorithm unchanged (no AD preparation needed) - u0 = ones(2) - mock_prob = ODEProblem((du, u, p, t) -> du .= 0, u0, (0.0, 1.0)) - @test prepare_alg(mock, u0, nothing, mock_prob) === mock - - # forwarddiffs_model must return false - @test SciMLBase.forwarddiffs_model(mock) == false +# MagnusGL6 (and all OrdinaryDiffEqLinearExponentialAlgorithm subtypes) +# have no `autodiff` field. When OrdinaryDiffEqDifferentiation is loaded, +# _alg_autodiff must not crash by trying to access alg.autodiff. +using OrdinaryDiffEqLinear +using SciMLOperators: MatrixOperator + +@testset "MagnusGL6 solve with Differentiation loaded (issue #3232)" begin + function update_func!(A, u, p, t) + A[1, 1] = cos(t) + A[2, 1] = sin(t) + A[1, 2] = -sin(t) + A[2, 2] = cos(t) + end + A = MatrixOperator(ones(2, 2), update_func! = update_func!) + prob = ODEProblem(A, ones(2), (1.0, 6.0)) + + # This would crash with FieldError before the fix + sol = solve(prob, MagnusGL6(), dt = 1 / 10) + @test sol.retcode == ReturnCode.Success + @test length(sol.t) > 1 end diff --git a/lib/OrdinaryDiffEqLinear/test/linear_method_tests.jl b/lib/OrdinaryDiffEqLinear/test/linear_method_tests.jl index b49d19b0c1e..c5a62556ce9 100644 --- a/lib/OrdinaryDiffEqLinear/test/linear_method_tests.jl +++ b/lib/OrdinaryDiffEqLinear/test/linear_method_tests.jl @@ -267,3 +267,21 @@ test_setup = Dict(:alg => Vern9(), :reltol => 1.0e-14, :abstol => 1.0e-14) sim = analyticless_test_convergence(dts, prob, CayleyEuler(), test_setup) @test sim.π’ͺest[:l2] β‰ˆ 1 atol = 0.2 + +# Regression test for https://github.com/SciML/OrdinaryDiffEq.jl/issues/3232 +# Magnus/Linear integrators must not FieldError when OrdinaryDiffEqDifferentiation +# is loaded (which happens transitively via DifferentialEquations.jl). +@testset "Regression #3232: non-autonomous Magnus solve does not FieldError" begin + function update_func_3232!(A, u, p, t) + A[1, 1] = cos(t) + A[2, 1] = sin(t) + A[1, 2] = -sin(t) + A[2, 2] = cos(t) + end + A_3232 = MatrixOperator(ones(2, 2), update_func! = update_func_3232!) + prob_3232 = ODEProblem(A_3232, ones(2), (1.0, 6.0)) + for alg in (MagnusGL6(), MagnusGL4(), MagnusGL8(), LieEuler(), CG2(), CG3(), CG4a()) + sol = solve(prob_3232, alg, dt = 1 / 10) + @test sol.retcode == ReturnCode.Success + end +end