DifferentialEquations.jl VectorContinuousCallback vs CallbackSet

From the docs:

The [VectorContinuousCallback] Event Handling and Callback Functions · DifferentialEquations.jl) works like a vector of ContinuousCallbacks and lets the user specify which callback is called when.

Multiple callbacks can be chained together to form a CallbackSet . A CallbackSet is constructed by passing the constructor ContinuousCallback , DiscreteCallback , VectorContinuousCallback or other CallbackSet instances

What exactly is the purpose of a VectorContinousCallback if you could should just use a CallbackSet of multiple ContinuousCallback? This seems to be like two options trying to achieve the same thing.

Additionally the VectorContinousCallback also requires some if else synthax sugaring in the affect function which does not look clean.

function condition(out,u,t,integrator) # Event when event_f(u,t) == 0
  out[1] = u[1]
  out[2] = (u[3] - 10.0)u[3]
end

function affect!(integrator, idx)
  if idx == 1
    integrator.u[2] = -0.9integrator.u[2]
  elseif idx == 2
    integrator.u[4] = -0.9integrator.u[4]
  end
end

cb = VectorContinuousCallback(condition,affect!,2)

They have very different performance characteristics. VectofContinuousCallbacks will scale to thousands of callbacks, but CallbackSet will hit compile time issues at around 10 or so.

Alright that makes sense so far. But with that the question arises how the situation looks like for DiscreteCallbacks? A VectorDiscreteCallback doesn’t seem to exist, is there a reasoning behind that?

If possible I suggest to include your explanation somewhere in the docs because I think this is important to know when making design decisions as a user.

DiscreteCallbacks run off of a true false. You can easily do a “VectorDiscreteCallback” by just doing bool1 && bool2. Implicit state in the condition is then all that’s needed to know which one fired. So no extra machinery on the backend is required to do it.

VectorContinuousCallbacks on the other hand cannot be made from ContinuousCallbacks because it requires the ability to run multiple rootfinding problems simultaneously. root1 * root2 can work in a pinch, but there are many ways which this can fail (for example, 0 * NaN). Also the problem of attribution, i.e. finding which callback fired first, can become an issue. Using a CallbackSet of ContinuousCallbacks stacks them and if two fire in the same interval, it takes the one that fires first. But that requires doubling the interpolation, i.e. doing two completely independent rootfinds. If you have n of them, first of all you have an n type tuple (so compile times die), and then you also have n rootfinding problems so your runtimes die. It’s just not a feasible approach to scale. But VectorContinuousCallbacks have quite a bit of overhead in the scalar case, and a more cumbersome API, so leaving ContinuousCallbacks alone was the solution in the end.

I thought it was, but it wasn’t. See if this quenches your thirst:

Thanks a lot!
The updated docs look good to me.