Skip to content

Support multiple simultaneous events in VectorContinuousCallback#3230

Closed
ChrisRackauckas-Claude wants to merge 6 commits into
SciML:masterfrom
ChrisRackauckas-Claude:mp/mevcc-sublibrary
Closed

Support multiple simultaneous events in VectorContinuousCallback#3230
ChrisRackauckas-Claude wants to merge 6 commits into
SciML:masterfrom
ChrisRackauckas-Claude:mp/mevcc-sublibrary

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Port of SciML/DiffEqBase.jl#1229 to the DiffEqBase sublibrary in OrdinaryDiffEq.jl.

  • Adds simultaneous_events and prev_simultaneous_events tracking to CallbackCache
  • When multiple conditions in a VectorContinuousCallback trigger at the same time, all their affects are now called (not just the first one)
  • Splits apply_callback! into separate ContinuousCallback and VectorContinuousCallback methods
  • find_callback_time now returns the full bottom_sign array for VectorContinuousCallback instead of a single sign value

Fixes #3222

Test plan

  • Basic simultaneous event demo (two events at same time both trigger)
  • Bouncing balls example where all balls hit ground simultaneously
  • Existing callback tests still pass

🤖 Generated with Claude Code

Co-Authored-By: Mason Protter mason.protter@icloud.com

ChrisRackauckas and others added 4 commits March 24, 2026 13:15
…sCallback

Port of SciML/DiffEqBase.jl#1229 to the DiffEqBase sublibrary in OrdinaryDiffEq.

Changes:
- Add `simultaneous_events` and `prev_simultaneous_events` fields to `CallbackCache`
- Track which events fire at the same time in `find_callback_time`
- Return full `bottom_sign` array instead of single sign for VectorContinuousCallback
- Split `apply_callback!` into separate ContinuousCallback and VectorContinuousCallback
  methods, where the VectorContinuousCallback version iterates over all simultaneous
  events and calls affect!/affect_neg! for each
- Use `prev_simultaneous_events` instead of single `nudged_idx`/`vector_event_last_time`
  for nudging and root-finding bracket selection

Co-Authored-By: Mason Protter <mason.protter@icloud.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of calling affect!(integrator, event_index) per-event, VCC now calls
affect!(integrator, simultaneous_events) once with the whole Vector{Bool}.
This lets users write affects that respond to combinations of events.

Merged apply_callback! back to Union{ContinuousCallback, VectorContinuousCallback}.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Encodes crossing direction: 0 = not triggered, -1 = upcrossing
(prev_sign < 0), +1 = downcrossing (prev_sign > 0). This gives the
user's affect! function both which events fired and their crossing
directions in a single vector.

prev_simultaneous_events stays Vector{Bool} since the nudging logic
only needs to know whether an event fired, not its direction.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Document the new VCC affect! signature: affect!(integrator, events)
  where events::Vector{Int8} encodes triggered events with crossing
  direction (-1=upcrossing, +1=downcrossing, 0=not triggered)

- Update all VCC affect functions across test files to use the new
  (integrator, events) signature instead of (integrator, event_index)

- Tests that used separate affect_neg! with VCC now fold both
  directions into a single affect! using the Int8 direction info

- Affected test files:
  - lib/DiffEqBase/test/callbacks.jl
  - lib/DiffEqBase/test/downstream/callback_detection.jl
  - lib/DiffEqBase/test/downstream/community_callback_tests.jl
  - test/integrators/event_detection_tests.jl
  - test/integrators/event_repeat_tests.jl
  - test/integrators/ode_event_tests.jl

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ChrisRackauckas ChrisRackauckas changed the title WIP: support multiple simultaneous events in VectorContinuousCallback Support multiple simultaneous events in VectorContinuousCallback Mar 24, 2026
…state machine

Demonstrates how the new simultaneous events API enables users to handle
the case where a velocity zero crossing under one discrete state (SLIDING_LEFT)
transitions to a new state (SLIDING_RIGHT) where the corresponding condition
is immediately at zero. The rootfinder can't detect this crossing, but the
user can chain the transition logic in the affect! function.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Pkg.develop call used joinpath(dirname(lib_dir), "..") which
resolved to one directory above the repo root. dirname(lib_dir)
already points to the repo root.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ChrisRackauckas

Copy link
Copy Markdown
Member

This was merged into #3242

ChrisRackauckas added a commit that referenced this pull request Apr 27, 2026
PR #3230 was rolled into the v7 mega-PR #3242 and ships in 7.0.0, but
NEWS.md never picked up an entry for it. Two distinct user-visible
changes need calling out:

1. Behavior: when multiple conditions of the same VCC cross zero on the
   same step, all of them now fire. The old implementation invoked the
   user's affect! only for the first crossing.

2. Breaking API: the VCC affect! signature changed from
       affect!(integrator, event_index::Int)
   to
       affect!(integrator, simultaneous_events::Vector{Int8})
   where each entry is 0 / -1 / +1 to encode "did not fire" /
   "upcrossing" / "downcrossing". affect_neg! is no longer called for
   VCC since direction lives in the mask.

Add a "Callback changes" section right after "DiffEqBase changes"
(callbacks.jl lives in lib/DiffEqBase) with the encoding table, a
side-by-side migration example, links to #3230 / #3242 / #3549, and a
short note explaining why the mask is Int8 rather than Bool.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Apparent failure to catch a zero cross of a condition

2 participants