diff --git a/Project.toml b/Project.toml index 0fe193a..0029501 100644 --- a/Project.toml +++ b/Project.toml @@ -1,10 +1,11 @@ name = "DelayDiffEq" uuid = "bcd4f6db-9728-5f36-b5f7-82caef46ccdb" -authors = ["Chris Rackauckas "] version = "5.61.1" +authors = ["Chris Rackauckas "] [deps] ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" +ConcreteStructs = "2569d6c7-a4a2-43d3-a901-331e8e4be471" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" FastBroadcast = "7034ab61-46d4-4ed7-9d0f-46aef9175898" @@ -22,12 +23,14 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" +SciMLLogging = "a6db7da4-7206-11f0-1eab-35f2a5dbe1d1" SimpleNonlinearSolve = "727e6d20-b764-4bd8-a329-72de5adea6c7" SimpleUnPack = "ce78b400-467f-4804-87d8-8f486da07d0a" SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" [compat] ArrayInterface = "7" +ConcreteStructs = "0.2.3" DataStructures = "0.18, 0.19" DiffEqBase = "6.187" FastBroadcast = "0.3.5" @@ -45,6 +48,7 @@ Printf = "1" RecursiveArrayTools = "3" Reexport = "1.0" SciMLBase = "2.115.0" +SciMLLogging = "1.4.0" SimpleNonlinearSolve = "2" SimpleUnPack = "1" SymbolicIndexingInterface = "0.3.36" diff --git a/src/DelayDiffEq.jl b/src/DelayDiffEq.jl index 10d6ab6..cd74723 100644 --- a/src/DelayDiffEq.jl +++ b/src/DelayDiffEq.jl @@ -28,7 +28,7 @@ import FastBroadcast: @.. using OrdinaryDiffEqNonlinearSolve: NLAnderson, NLFunctional using OrdinaryDiffEqCore: AbstractNLSolverCache, SlowConvergence, - alg_extrapolates, alg_maximum_order, initialize! + alg_extrapolates, alg_maximum_order, initialize!, ODEVerbosity using OrdinaryDiffEqRosenbrock: RosenbrockMutableCache using OrdinaryDiffEqFunctionMap: FunctionMap # using OrdinaryDiffEqDifferentiation: resize_grad_config!, resize_jac_config! @@ -49,6 +49,9 @@ using SciMLBase: CallbackSet, DAEProblem, DDEProblem, DESolution, ODEProblem, Re change_t_via_interpolation!, isadaptive using DiffEqBase: initialize! import DiffEqBase +using ConcreteStructs: @concrete +using SciMLLogging: AbstractVerbositySpecifier, AbstractVerbosityPreset, AbstractMessageLevel, None, Minimal, Standard, Detailed, All, + Silent, DebugLevel, InfoLevel, WarnLevel, ErrorLevel, @SciMLMessage import SciMLBase @@ -56,6 +59,7 @@ export Discontinuity, MethodOfSteps include("discontinuity_type.jl") include("functionwrapper.jl") +include("verbosity.jl") include("integrators/type.jl") include("integrators/utils.jl") diff --git a/src/fpsolve/fpsolve.jl b/src/fpsolve/fpsolve.jl index ba945d5..57e7dff 100644 --- a/src/fpsolve/fpsolve.jl +++ b/src/fpsolve/fpsolve.jl @@ -9,6 +9,11 @@ function SciMLBase.postamble!(fpsolver::FPSolver, integrator::DDEIntegrator) if OrdinaryDiffEqNonlinearSolve.nlsolvefail(fpsolver) integrator.stats.nfpconvfail += 1 + @SciMLMessage(lazy"Fixed-point iteration failed to converge at t = $(integrator.t) after $(fpsolver.iter) iterations", + integrator.opts.verbose, :residual_control) + else + @SciMLMessage(lazy"Fixed-point iteration converged at t = $(integrator.t) in $(fpsolver.iter) iterations", + integrator.opts.verbose, :residual_control) end integrator.force_stepfail = OrdinaryDiffEqNonlinearSolve.nlsolvefail(fpsolver) || integrator.force_stepfail diff --git a/src/fpsolve/functional.jl b/src/fpsolve/functional.jl index a340cb0..5f55a97 100644 --- a/src/fpsolve/functional.jl +++ b/src/fpsolve/functional.jl @@ -111,7 +111,10 @@ function compute_step_fixedpoint!( DiffEqBase.calculate_residuals!(atmp, dz, ode_integrator.u, integrator.u, opts.abstol, opts.reltol, opts.internalnorm, t) - opts.internalnorm(atmp, t) + residual = opts.internalnorm(atmp, t) + @SciMLMessage(lazy"Fixed-point iteration residual = $residual at t = $t", + opts.verbose, :residual_control) + residual end ## resize! diff --git a/src/integrators/utils.jl b/src/integrators/utils.jl index 177b2e9..237a754 100644 --- a/src/integrators/utils.jl +++ b/src/integrators/utils.jl @@ -189,6 +189,10 @@ function OrdinaryDiffEqCore.handle_discontinuities!(integrator::DDEIntegrator) d = OrdinaryDiffEqCore.pop_discontinuity!(integrator) order = d.order tdir_t = integrator.tdir * integrator.t + + @SciMLMessage(lazy"Handling discontinuity at t = $(integrator.t) with order $order", + integrator.opts.verbose, :discontinuity_tracking) + while OrdinaryDiffEqCore.has_discontinuity(integrator) && OrdinaryDiffEqCore.first_discontinuity(integrator) == tdir_t d2 = OrdinaryDiffEqCore.pop_discontinuity!(integrator) @@ -234,6 +238,14 @@ function add_next_discontinuities!(integrator, order, t = integrator.t) neutral = integrator.sol.prob.neutral next_order = neutral ? order : order + 1 + if neutral + @SciMLMessage(lazy"Adding next discontinuities for neutral DDE: order remains $order", + integrator.opts.verbose, :neutral_delay) + else + @SciMLMessage(lazy"Adding next discontinuities: order increases from $order to $next_order", + integrator.opts.verbose, :discontinuity_tracking) + end + # only track discontinuities up to order of the applied method alg_maximum_order = OrdinaryDiffEqCore.alg_maximum_order(integrator.alg) next_order <= alg_maximum_order + 1 || return diff --git a/src/solve.jl b/src/solve.jl index 8e1f0df..e288715 100644 --- a/src/solve.jl +++ b/src/solve.jl @@ -75,7 +75,7 @@ function SciMLBase.__init(prob::SciMLBase.AbstractDDEProblem, internalopnorm = opnorm, isoutofdomain = DiffEqBase.ODE_DEFAULT_ISOUTOFDOMAIN, unstable_check = DiffEqBase.ODE_DEFAULT_UNSTABLE_CHECK, - verbose = true, + verbose = DDEVerbosity(), timeseries_errors = true, dense_errors = false, advance_to_tstop = false, @@ -103,6 +103,19 @@ function SciMLBase.__init(prob::SciMLBase.AbstractDDEProblem, order_discontinuity_t0 = prob.order_discontinuity_t0 end + # Handle verbose argument: convert Bool or AbstractVerbosityPreset to DDEVerbosity + if verbose isa Bool + if verbose + verbose_spec = DDEVerbosity() + else + verbose_spec = DDEVerbosity(None()) + end + elseif verbose isa AbstractVerbosityPreset + verbose_spec = DDEVerbosity(verbose) + else + verbose_spec = verbose + end + if alg.alg isa CompositeAlgorithm && alg.alg.choice_function isa AutoSwitch auto = alg.alg.choice_function alg = MethodOfSteps(CompositeAlgorithm(alg.alg.algs, @@ -147,7 +160,13 @@ function SciMLBase.__init(prob::SciMLBase.AbstractDDEProblem, # no fixed-point iterations for constrained algorithms, # and thus `dtmax` should match minimal lag if isconstrained(alg) && has_constant_lags(prob) - dtmax = tdir * min(abs(dtmax), minimum(abs, constant_lags)) + min_lag = minimum(abs, constant_lags) + old_dtmax = abs(dtmax) + dtmax = tdir * min(old_dtmax, min_lag) + if min_lag < old_dtmax + @SciMLMessage(lazy"Constrained algorithm: limiting dtmax from $old_dtmax to $min_lag (minimum lag)", + verbose_spec, :constrained_step) + end end # get absolute and relative tolerances @@ -197,7 +216,7 @@ function SciMLBase.__init(prob::SciMLBase.AbstractDDEProblem, uBottomEltypeNoUnits, tTypeNoUnits, uprev, uprev2, f_with_history, t0, zero(tType), reltol_internal, p, calck, - Val(isinplace(prob))) + Val(isinplace(prob)), OrdinaryDiffEqCore.ODEVerbosity()) # separate statistics of the integrator and the history stats = SciMLBase.DEStats(0) @@ -294,7 +313,7 @@ function SciMLBase.__init(prob::SciMLBase.AbstractDDEProblem, typeof(d_discontinuities_internal), typeof(userdata), typeof(save_idxs), typeof(maxiters), typeof(tstops), - typeof(saveat), typeof(d_discontinuities), typeof(verbose)}(maxiters, + typeof(saveat), typeof(d_discontinuities), typeof(verbose_spec)}(maxiters, save_everystep, adaptive, abstol_internal, @@ -335,7 +354,7 @@ function SciMLBase.__init(prob::SciMLBase.AbstractDDEProblem, callback_set, isoutofdomain, unstable_check, - verbose, + verbose_spec, calck, force_dtmin, advance_to_tstop, @@ -394,7 +413,7 @@ function SciMLBase.__init(prob::SciMLBase.AbstractDDEProblem, callback_set, isoutofdomain, unstable_check, - verbose, + verbose_spec, calck, force_dtmin, advance_to_tstop, diff --git a/src/track.jl b/src/track.jl index 24d2711..86eb5f4 100644 --- a/src/track.jl +++ b/src/track.jl @@ -10,6 +10,9 @@ function track_propagated_discontinuities!(integrator::DDEIntegrator) interp_points = integrator.discontinuity_interp_points Θs = range(zero(integrator.t); stop = oneunit(integrator.t), length = interp_points) + @SciMLMessage(lazy"Tracking discontinuities for state-dependent delays in interval [$(integrator.t), $(integrator.t + integrator.dt)]", + integrator.opts.verbose, :state_dependent_delay) + # for dependent lags and previous discontinuities for lag in integrator.sol.prob.dependent_lags, discontinuity in integrator.tracked_discontinuities @@ -28,8 +31,12 @@ function track_propagated_discontinuities!(integrator::DDEIntegrator) # add new discontinuity of correct order at the estimated time point if integrator.sol.prob.neutral d = Discontinuity(t, discontinuity.order) + @SciMLMessage(lazy"Propagated discontinuity found at t = $t with order $(discontinuity.order) (neutral DDE)", + integrator.opts.verbose, :discontinuity_tracking) else d = Discontinuity(t, discontinuity.order + 1) + @SciMLMessage(lazy"Propagated discontinuity found at t = $t with order $(discontinuity.order + 1)", + integrator.opts.verbose, :discontinuity_tracking) end push!(integrator.opts.d_discontinuities, d) push!(integrator.opts.tstops, t) @@ -60,7 +67,11 @@ function discontinuity_function(integrator::DDEIntegrator, lag, T, t) ut = cache end - T + lag(ut, integrator.p, t) - t + lag_value = lag(ut, integrator.p, t) + @SciMLMessage(lazy"Evaluating state-dependent delay at t = $t: lag = $lag_value", + integrator.opts.verbose, :delay_evaluation) + + T + lag_value - t end """ diff --git a/src/utils.jl b/src/utils.jl index d24c59e..d5ebba1 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -298,7 +298,7 @@ function build_history_function(prob, alg, rate_prototype, reltol, differential_ uBottomEltypeNoUnits, tTypeNoUnits, ode_uprev, ode_uprev, ode_f, t0, zero(tType), reltol, p, calck, - Val(isinplace(prob))) + Val(isinplace(prob)), OrdinaryDiffEqCore.ODEVerbosity()) # build dense interpolation of history ode_alg_choice = iscomposite(alg) ? Int[] : nothing diff --git a/src/verbosity.jl b/src/verbosity.jl new file mode 100644 index 0000000..cac5340 --- /dev/null +++ b/src/verbosity.jl @@ -0,0 +1,218 @@ +""" + DDEVerbosity <: AbstractVerbositySpecifier + +Verbosity configuration for DelayDiffEq.jl solvers, providing fine-grained control over +diagnostic messages, warnings, and errors during DDE solution. + +# Fields + +## ODE Verbosity +- `ode_verbosity`: Verbosity configuration for the underlying ODE solver + +## Delay-Specific Group +- `discontinuity_tracking`: Messages about discontinuity propagation tracking +- `delay_evaluation`: Messages about delay term evaluation +- `constrained_step`: Messages when step size is constrained by discontinuities +- `residual_control`: Messages about residual control in implicit methods +- `neutral_delay`: Messages specific to neutral delay equations +- `state_dependent_delay`: Messages when state-dependent delays are detected/evaluated + +# Constructors + + DDEVerbosity(preset::AbstractVerbosityPreset) + +Create a `DDEVerbosity` using a preset configuration: +- `SciMLLogging.None()`: All messages disabled +- `SciMLLogging.Minimal()`: Only critical errors and fatal issues +- `SciMLLogging.Standard()`: Balanced verbosity (default) +- `SciMLLogging.Detailed()`: Comprehensive debugging information +- `SciMLLogging.All()`: Maximum verbosity + + DDEVerbosity(; ode_verbosity=nothing, delay_specific=nothing, kwargs...) + +Create a `DDEVerbosity` with group-level or individual field control. + +# Examples + +```julia +# Use a preset +verbose = DDEVerbosity(SciMLLogging.Standard()) + +# Set ODE verbosity and delay-specific group +verbose = DDEVerbosity( + ode_verbosity = ODEVerbosity(SciMLLogging.Detailed()), + delay_specific = SciMLLogging.InfoLevel() +) + +# Set individual fields +verbose = DDEVerbosity( + discontinuity_tracking = SciMLLogging.InfoLevel(), + delay_evaluation = SciMLLogging.WarnLevel() +) + +# Mix group and individual settings +verbose = DDEVerbosity( + delay_specific = SciMLLogging.InfoLevel(), # Set all delay-specific to InfoLevel + state_dependent_delay = SciMLLogging.WarnLevel() # Override specific field +) +``` +""" +@concrete struct DDEVerbosity <: AbstractVerbositySpecifier + # ODE solver verbosity + ode_verbosity + # Delay-specific options + discontinuity_tracking + delay_evaluation + constrained_step + residual_control + neutral_delay + state_dependent_delay +end + +# Group classifications +const delay_specific_options = (:discontinuity_tracking, :delay_evaluation, + :constrained_step, :residual_control, :neutral_delay, :state_dependent_delay) + +function option_group(option::Symbol) + if option in delay_specific_options + return :delay_specific + else + error("Unknown verbosity option: $option") + end +end + +# Get all options in a group +function group_options(verbosity::DDEVerbosity, group::Symbol) + if group === :delay_specific + return NamedTuple{delay_specific_options}(getproperty(verbosity, opt) + for opt in delay_specific_options) + else + error("Unknown group: $group") + end +end + +function DDEVerbosity(; + ode_verbosity = nothing, delay_specific = nothing, kwargs...) + # Validate group arguments + if delay_specific !== nothing && !(delay_specific isa AbstractMessageLevel) + throw(ArgumentError("delay_specific must be a SciMLLogging.AbstractMessageLevel, got $(typeof(delay_specific))")) + end + + if ode_verbosity !== nothing && !(ode_verbosity isa ODEVerbosity) + throw(ArgumentError("ode_verbosity must be an ODEVerbosity, got $(typeof(ode_verbosity))")) + end + + # Validate individual kwargs + for (key, value) in kwargs + if !(key in delay_specific_options) + throw(ArgumentError("Unknown verbosity option: $key. Valid options are: $(delay_specific_options)")) + end + if !(value isa AbstractMessageLevel) + throw(ArgumentError("$key must be a SciMLLogging.AbstractMessageLevel, got $(typeof(value))")) + end + end + + # Build arguments using NamedTuple for type stability + default_args = ( + ode_verbosity = ode_verbosity === nothing ? ODEVerbosity() : ode_verbosity, + discontinuity_tracking = Silent(), + delay_evaluation = Silent(), + constrained_step = Silent(), + residual_control = Silent(), + neutral_delay = Silent(), + state_dependent_delay = Silent() + ) + + # Apply group-level settings + final_args = if delay_specific !== nothing + NamedTuple{keys(default_args)}( + _resolve_arg_value(key, default_args[key], delay_specific) + for key in keys(default_args) + ) + else + default_args + end + + # Apply individual overrides + if !isempty(kwargs) + final_args = merge(final_args, NamedTuple(kwargs)) + end + + DDEVerbosity(values(final_args)...) +end + +# Constructor for verbosity presets following the hierarchical levels: +# None < Minimal < Standard < Detailed < All +# Each level includes all messages from levels below it plus additional ones +function DDEVerbosity(verbose::AbstractVerbosityPreset) + if verbose isa Minimal + # Minimal: Only fatal errors and critical warnings + DDEVerbosity( + ode_verbosity = ODEVerbosity(Minimal()), + discontinuity_tracking = Silent(), + delay_evaluation = Silent(), + constrained_step = Silent(), + residual_control = Silent(), + neutral_delay = Silent(), + state_dependent_delay = WarnLevel() + ) + elseif verbose isa Standard + # Standard: Everything from Minimal + non-fatal warnings + DDEVerbosity() + elseif verbose isa Detailed + # Detailed: Everything from Standard + debugging/solver behavior + DDEVerbosity( + ode_verbosity = ODEVerbosity(Detailed()), + discontinuity_tracking = InfoLevel(), + delay_evaluation = InfoLevel(), + constrained_step = InfoLevel(), + residual_control = InfoLevel(), + neutral_delay = InfoLevel(), + state_dependent_delay = WarnLevel() + ) + elseif verbose isa All + # All: Maximum verbosity - every possible logging message at InfoLevel + DDEVerbosity( + ode_verbosity = ODEVerbosity(All()), + discontinuity_tracking = InfoLevel(), + delay_evaluation = InfoLevel(), + constrained_step = InfoLevel(), + residual_control = InfoLevel(), + neutral_delay = InfoLevel(), + state_dependent_delay = InfoLevel() + ) + end +end + +@inline function DDEVerbosity(verbose::None) + DDEVerbosity( + ODEVerbosity(None()), + Silent(), + Silent(), + Silent(), + Silent(), + Silent(), + Silent() + ) +end + +# Helper function to resolve argument values based on group membership +@inline function _resolve_arg_value(key::Symbol, default_val, delay_specific) + if key === :ode_verbosity + return default_val + elseif key in delay_specific_options && delay_specific !== nothing + return delay_specific + else + return default_val + end +end + +function Base.getproperty(v::DDEVerbosity, s::Symbol) + if s in fieldnames(DDEVerbosity) + return getfield(v, s) + elseif s in fieldnames(ODEVerbosity) + return getfield(getfield(v, :ode_verbosity), s) + else + return error("type DDEVerbosity has no field ", s) + end +end \ No newline at end of file diff --git a/test/interface/default_solver.jl b/test/interface/default_solver.jl index deacc04..fcc518b 100644 --- a/test/interface/default_solver.jl +++ b/test/interface/default_solver.jl @@ -16,7 +16,7 @@ prob = DDEProblem(f!, [1.0], h, tspan; constant_lags = [0.2]) # Test that the dispatch is correctly defined # The solve will encounter cache compatibility issues but should dispatch correctly dispatch_works = try - sol = SciMLBase.__solve(prob; maxiters = 1, verbose = false) + sol = SciMLBase.__solve(prob; maxiters = 1) true catch e # Check if it's the expected cache error diff --git a/test/interface/verbosity.jl b/test/interface/verbosity.jl new file mode 100644 index 0000000..309d41d --- /dev/null +++ b/test/interface/verbosity.jl @@ -0,0 +1,175 @@ +using DelayDiffEq, OrdinaryDiffEqCore +using DelayDiffEq: DDEVerbosity +using SciMLLogging: SciMLLogging, AbstractMessageLevel +using Test + +@testset "DDEVerbosity Construction" begin + # Test default constructor + @testset "Default constructor" begin + v = DDEVerbosity() + @test v.ode_verbosity isa OrdinaryDiffEqCore.ODEVerbosity + @test v.discontinuity_tracking isa SciMLLogging.Silent + @test v.delay_evaluation isa SciMLLogging.Silent + @test v.constrained_step isa SciMLLogging.Silent + @test v.residual_control isa SciMLLogging.Silent + @test v.neutral_delay isa SciMLLogging.Silent + @test v.state_dependent_delay isa SciMLLogging.Silent + end + + # Test preset constructors + @testset "Preset: None" begin + v = DDEVerbosity(None()) + @test v.ode_verbosity isa OrdinaryDiffEqCore.ODEVerbosity + @test all(getfield(v, f) isa SciMLLogging.Silent + for f in fieldnames(DDEVerbosity) if f != :ode_verbosity) + end + + @testset "Preset: Minimal" begin + v = DDEVerbosity(Minimal()) + @test v.discontinuity_tracking isa SciMLLogging.Silent + @test v.delay_evaluation isa SciMLLogging.Silent + @test v.constrained_step isa SciMLLogging.Silent + @test v.residual_control isa SciMLLogging.Silent + @test v.neutral_delay isa SciMLLogging.Silent + @test v.state_dependent_delay isa SciMLLogging.WarnLevel + end + + @testset "Preset: Standard" begin + v = DDEVerbosity(Standard()) + @test v isa DDEVerbosity + end + + @testset "Preset: Detailed" begin + v = DDEVerbosity(Detailed()) + @test v.discontinuity_tracking isa SciMLLogging.InfoLevel + @test v.delay_evaluation isa SciMLLogging.InfoLevel + @test v.constrained_step isa SciMLLogging.InfoLevel + @test v.residual_control isa SciMLLogging.InfoLevel + @test v.neutral_delay isa SciMLLogging.InfoLevel + @test v.state_dependent_delay isa SciMLLogging.WarnLevel + end + + @testset "Preset: All" begin + v = DDEVerbosity(All()) + @test all(getfield(v, f) isa SciMLLogging.InfoLevel + for f in fieldnames(DDEVerbosity) if f != :ode_verbosity) + end + + # Test group-level constructor + @testset "Group-level construction" begin + v = DDEVerbosity(delay_specific = InfoLevel()) + @test v.discontinuity_tracking isa SciMLLogging.InfoLevel + @test v.delay_evaluation isa SciMLLogging.InfoLevel + @test v.constrained_step isa SciMLLogging.InfoLevel + @test v.residual_control isa SciMLLogging.InfoLevel + @test v.neutral_delay isa SciMLLogging.InfoLevel + @test v.state_dependent_delay isa SciMLLogging.InfoLevel + end + + # Test individual field construction + @testset "Individual field construction" begin + v = DDEVerbosity( + discontinuity_tracking = InfoLevel(), + delay_evaluation = WarnLevel(), + state_dependent_delay = DebugLevel() + ) + @test v.discontinuity_tracking isa SciMLLogging.InfoLevel + @test v.delay_evaluation isa SciMLLogging.WarnLevel + @test v.state_dependent_delay isa SciMLLogging.DebugLevel + @test v.constrained_step isa SciMLLogging.Silent + end + + # Test mixed group and individual settings + @testset "Mixed group and individual" begin + v = DDEVerbosity( + delay_specific = InfoLevel(), + state_dependent_delay = WarnLevel() # Override + ) + @test v.discontinuity_tracking isa SciMLLogging.InfoLevel + @test v.delay_evaluation isa SciMLLogging.InfoLevel + @test v.state_dependent_delay isa SciMLLogging.WarnLevel # Overridden + end + + # Test ODE verbosity passthrough + @testset "ODE verbosity" begin + ode_v = OrdinaryDiffEqCore.ODEVerbosity(Detailed()) + v = DDEVerbosity(ode_verbosity = ode_v) + @test v.ode_verbosity === ode_v + end + + # Test invalid arguments + @testset "Invalid arguments" begin + @test_throws ArgumentError DDEVerbosity(invalid_field = InfoLevel()) + @test_throws ArgumentError DDEVerbosity(delay_specific = "not a level") + @test_throws ArgumentError DDEVerbosity(discontinuity_tracking = 42) + end +end + +@testset "DDEVerbosity Property Access" begin + # Test direct field access (DDE-specific) + @testset "Direct DDE field access" begin + v = DDEVerbosity(discontinuity_tracking = InfoLevel()) + @test v.discontinuity_tracking isa SciMLLogging.InfoLevel + end + + # Test nested ODE field access via getproperty + @testset "Nested ODE field access" begin + ode_v = OrdinaryDiffEqCore.ODEVerbosity(dt_NaN = WarnLevel()) + v = DDEVerbosity(ode_verbosity = ode_v) + @test v.dt_NaN isa SciMLLogging.WarnLevel + @test v.max_iters isa SciMLLogging.AbstractMessageLevel + end + + # Test that invalid field access errors + @testset "Invalid field access" begin + v = DDEVerbosity() + @test_throws ErrorException v.nonexistent_field + end +end + +@testset "DDEVerbosity in solve" begin + # Define a simple DDE problem + function dde_func(du, u, h, p, t) + du[1] = -h(p, t - 1; idxs = 1) + end + h(p, t; idxs = nothing) = ones(1) + prob = DDEProblem(dde_func, [1.0], h, (0.0, 10.0)) + + # Test Bool verbose conversion + @testset "Bool verbose argument" begin + sol = solve(prob, MethodOfSteps(Tsit5()); verbose = true, dt = 0.1) + @test sol.retcode == ReturnCode.Success + + sol = solve(prob, MethodOfSteps(Tsit5()); verbose = false, dt = 0.1) + @test sol.retcode == ReturnCode.Success + end + + # Test AbstractVerbosityPreset conversion + @testset "Preset verbose argument" begin + sol = solve(prob, MethodOfSteps(Tsit5()); verbose = Standard(), dt = 0.1) + @test sol.retcode == ReturnCode.Success + + sol = solve(prob, MethodOfSteps(Tsit5()); verbose = None(), dt = 0.1) + @test sol.retcode == ReturnCode.Success + end + + # Test DDEVerbosity directly + @testset "DDEVerbosity argument" begin + v = DDEVerbosity(discontinuity_tracking = InfoLevel()) + @test_logs (:info, r"Handling discontinuity") match_mode=:any solve( + prob, MethodOfSteps(Tsit5()); verbose = v, dt = 0.1) + end + + # Test that integrator has correct verbosity type + @testset "Integrator verbosity type" begin + integrator = init(prob, MethodOfSteps(Tsit5()); verbose = true, dt = 0.1) + @test integrator.opts.verbose isa DDEVerbosity + + integrator = init(prob, MethodOfSteps(Tsit5()); verbose = Standard(), dt = 0.1) + @test integrator.opts.verbose isa DDEVerbosity + + v = DDEVerbosity(delay_evaluation = WarnLevel()) + integrator = init(prob, MethodOfSteps(Tsit5()); verbose = v, dt = 0.1) + @test integrator.opts.verbose === v + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 43f3d26..456c1f1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -57,6 +57,9 @@ if GROUP == "All" || GROUP == "Interface" @time @safetestset "Units Tests" begin include("interface/units.jl") end + @time @safetestset "Verbosity Tests" begin + include("interface/verbosity.jl") + end end if GROUP == "All" || GROUP == "Integrators"