Skip to content

Miss executions of ContinuousCallbacks due to simultaneous timing and numerical precision issues #3819

@sablonl

Description

@sablonl

I've encountered two related bugs with continuous events in ModelingToolkit/DifferentialEquations:
(I know this is a very bad way of doing Periodic/Discrete Callbacks but I guess that issue could occur in different cases)

Bug 1: Only first event executes when multiple events occur simultaneously

When multiple continuous events are scheduled to trigger at exactly the same time, only the first event in the array executes. The subsequent events that should fire at the same time are skipped entirely.

Expected behavior: All events that satisfy their trigger conditions at the same time should execute.

Actual behavior: Only the first event executes when multiple events have simultaneous trigger times.

Bug 2: Events skipped due to numerical precision in modulo operations

Some continuous events are being skipped when using modulo operations in event conditions, likely due to floating-point precision issues. The t % frequency ≈ 0 condition fails to trigger even when it should mathematically be true.

Expected behavior: Events should trigger reliably at their specified intervals, accounting for floating-point precision limitations.

MWE:

using Pkg
Pkg.activate(temp = true)

Pkg.add(["ModelingToolkit", "DifferentialEquations"])

using ModelingToolkit, DifferentialEquations
using ModelingToolkit: D_nounits as D, t_nounits as t

@variables begin
	# Define the variables used in the equations
	x(t)::Float64
	dum_t_a(t)::Float64
	dum_t_b(t)::Float64
end

eqs = [D(x) ~ -x
	D(dum_t_a) ~ 0
	D(dum_t_b) ~ 0] # Initial equations

function cb_a(_t)
	"Callback A executed at time: $_t" |> println
	return _t
end
@register_symbolic cb_a(_t)
function cb_b(_t)
	"Callback B executed at time: $_t" |> println
	return _t
end
@register_symbolic cb_b(_t)

tstops(frequency_a, frequency_b) = sort(unique(union(
	range(0.0, 10.0, step = frequency_a),
	range(0.0, 10.0, step = frequency_b),
)))
function test(frequency_a, frequency_b)
	evs = Any[]
	push!(evs,
		[t % frequency_a ~ 0] => [dum_t_a ~ cb_a(Pre(t))],
	)
	push!(evs,
		[t % frequency_b ~ 0] => [dum_t_b ~ cb_b(Pre(t))],
	)

	@named sys = ODESystem(eqs, t;
		continuous_events = evs)

	compiled_sys = mtkcompile(sys)

	ode_prob = ODEProblem(compiled_sys, [x => 1.0, dum_t_a => 0.0, dum_t_b => 0.0], (0.0, 10.0))

	sol = solve(ode_prob;
		tstops = tstops(frequency_a, frequency_b),
	);
	return nothing
end

function truth(frequency_a, frequency_b)
	vtstops = tstops(frequency_a, frequency_b)
	for t in vtstops
		if isapprox(t % frequency_a, 0, atol = 1e-6) # Because of reasons, t % frequency_a ≈ 0 does not work 
			println("Callback A executed at time: $t")
		end
		if isapprox(t % frequency_b, 0, atol = 1e-6)
			println("Callback B executed at time: $t")
		end
	end
end

test(1.0, 1.0)
truth(1.0, 1.0)
# output:
#       test(1.0, 1.0)                     |       truth(1.0, 1.0)

#                                          | Callback A executed at time: 0.0
#                                          | Callback B executed at time: 0.0
# Callback A executed at time: 1.0         | Callback A executed at time: 1.0
#                                          | Callback B executed at time: 1.0
# Callback A executed at time: 2.0         | Callback A executed at time: 2.0
#                                          | Callback B executed at time: 2.0
# Callback A executed at time: 3.0         | Callback A executed at time: 3.0
#                                          | Callback B executed at time: 3.0
# Callback A executed at time: 4.0         | Callback A executed at time: 4.0
#                                          | Callback B executed at time: 4.0
# Callback A executed at time: 5.0         | Callback A executed at time: 5.0
#                                          | Callback B executed at time: 5.0
# Callback A executed at time: 6.0         | Callback A executed at time: 6.0
#                                          | Callback B executed at time: 6.0
# Callback A executed at time: 7.0         | Callback A executed at time: 7.0
#                                          | Callback B executed at time: 7.0
# Callback A executed at time: 8.0         | Callback A executed at time: 8.0
#                                          | Callback B executed at time: 8.0
# Callback A executed at time: 9.0         | Callback A executed at time: 9.0
#                                          | Callback B executed at time: 9.0
# Callback A executed at time: 10.0        | Callback A executed at time: 10.0
#                                          | Callback B executed at time: 10.0

test(1.0, 1.15)
truth(1.0, 1.15)
# output:
#       test(1.0, 1.15)                    |       truth(1.0, 1.15)

#                                          | Callback A executed at time: 0.0
#                                          | Callback B executed at time: 0.0
# Callback A executed at time: 1.0         | Callback A executed at time: 1.0
# Callback B executed at time: 1.15        | Callback B executed at time: 1.15
# Callback A executed at time: 2.0         | Callback A executed at time: 2.0
# Callback B executed at time: 2.3         | Callback B executed at time: 2.3
# Callback A executed at time: 3.0         | Callback A executed at time: 3.0
#                                          | Callback B executed at time: 3.45
# Callback A executed at time: 4.0         | Callback A executed at time: 4.0
# Callback B executed at time: 4.6         | Callback B executed at time: 4.6
# Callback A executed at time: 5.0         | Callback A executed at time: 5.0
#                                          | Callback B executed at time: 5.75
# Callback A executed at time: 6.0         | Callback A executed at time: 6.0
#                                          | Callback B executed at time: 6.9
# Callback A executed at time: 7.0         | Callback A executed at time: 7.0
# Callback A executed at time: 8.0         | Callback A executed at time: 8.0
#                                          | Callback B executed at time: 8.05
# Callback A executed at time: 9.0         | Callback A executed at time: 9.0
# Callback B executed at time: 9.2         | Callback B executed at time: 9.2
# Callback A executed at time: 10.0        | Callback A executed at time: 10.0

Environment:

julia> versioninfo()
Julia Version 1.11.5
Commit 760b2e5b739 (2025-04-14 06:53 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 8 × 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
  WORD_SIZE: 64
  LLVM: libLLVM-16.0.6 (ORCJIT, tigerlake)
Threads: 1 default, 0 interactive, 1 GC (on 8 virtual cores)
Environment:
  JULIA_NUM_THREAD = 16
  JULIA_EDITOR = code
  JULIA_VSCODE_REPL = 1

julia> Pkg.status()
Status `/tmp/jl_wLkfu4/Project.toml`
  [0c46a032] DifferentialEquations v7.16.1
  [961ee093] ModelingToolkit v10.10.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions