7.0 KiB
Feature: Discrete Callbacks
Overview
Discrete callbacks trigger at discrete events based on conditions that don't require zero-crossing detection. Unlike continuous callbacks which detect sign changes, discrete callbacks check conditions at specific points (e.g., after each step, at specific times, when certain criteria are met).
Key Characteristics:
- Condition-based (not zero-crossing)
- Evaluated at discrete points (typically end of each step)
- No interpolation or root-finding needed
- Can trigger multiple times or once
- Complementary to continuous callbacks
Why This Feature Matters
- Common use cases: Time-based events, iteration limits, convergence criteria
- Simpler than continuous: No root-finding overhead
- Essential for many simulations: Parameter updates, logging, termination conditions
- Foundation for advanced callbacks: Basis for SavingCallback, TerminateSteadyState, etc.
Dependencies
- Existing callback infrastructure (continuous callbacks already implemented)
Implementation Approach
Callback Structure
pub struct DiscreteCallback<'a, const D: usize, P> {
/// Condition function: returns true when callback should fire
pub condition: &'a dyn Fn(f64, SVector<f64, D>, &P) -> bool,
/// Effect function: modifies ODE state
pub effect: &'a dyn Fn(&mut ODE<D, P>),
/// Fire only once, or every time condition is true
pub single_trigger: bool,
/// Has this callback already fired? (for single_trigger)
pub has_fired: bool,
}
Evaluation Points
Discrete callbacks are checked:
- After each successful step
- Before continuous callback interpolation
- Can also check before step (for preset times)
Interaction with Continuous Callbacks
Priority order:
- Discrete callbacks (checked first)
- Continuous callbacks (if any triggered, may interpolate backward)
Key Differences from Continuous
| Aspect | Continuous | Discrete |
|---|---|---|
| Detection | Zero-crossing with root-finding | Boolean condition |
| Timing | Exact (via interpolation) | At step boundaries |
| Cost | Higher (root-finding) | Lower (simple check) |
| Use case | Physical events | Logic-based events |
Implementation Tasks
Core Structure
-
Define
DiscreteCallbackstruct- Condition function field
- Effect function field
single_triggerflaghas_firedstate (if single_trigger)- Constructor
-
Convenience constructors
new()- full specificationrepeating()- always repeatsingle()- fire once only
Integration with Problem
-
Update
Problemto handle both callback types- Separate storage:
Vec<ContinuousCallback>andVec<DiscreteCallback> - Or unified
Callbackenum:pub enum Callback<'a, const D: usize, P> { Continuous(ContinuousCallback<'a, D, P>), Discrete(DiscreteCallback<'a, D, P>), }
- Separate storage:
-
Update solver loop in
Problem::solve()- After each successful step:
- Check all discrete callbacks
- If condition true and (!single_trigger || !has_fired):
- Apply effect
- Mark as fired if single_trigger
- Then check continuous callbacks
- After each successful step:
Standard Discrete Callbacks
Pre-built common callbacks:
-
stop_at_time(t_stop)- Condition:
t >= t_stop - Effect:
stop - Single trigger: true
- Condition:
-
max_iterations(n)- Requires iteration counter in Problem
- Condition:
iteration >= n - Effect:
stop
-
periodic(interval, effect)- Fires every
intervaltime units - Requires state to track last fire time
- Fires every
Testing
-
Basic discrete callback test
- Simple ODE
- Callback that stops at t=5.0
- Verify integration stops exactly at step containing t=5.0
-
Single trigger test
- Callback with single_trigger=true
- Condition that becomes true, false, true again
- Verify fires only once
-
Multiple triggers test
- Callback with single_trigger=false
- Condition that oscillates
- Verify fires each time condition is true
-
Combined callbacks test
- Both discrete and continuous callbacks
- Verify both types work together
- Discrete should fire first
-
State modification test
- Callback that modifies ODE parameters
- Verify effect persists
- Integration continues correctly
Benchmarking
- Compare overhead vs no callbacks
- Should be minimal (just boolean check)
- Compare vs continuous callback for same logical event
- Discrete should be faster
Documentation
- Docstring explaining discrete vs continuous
- When to use each type
- Examples:
- Stop at specific time
- Parameter update every N time units
- Terminate when condition met
- Integration with CallbackSet (future)
Testing Requirements
Stop at Time Test
fn test_stop_at_time() {
let params = ();
fn derivative(_t: f64, y: Vector1<f64>, _p: &()) -> Vector1<f64> {
Vector1::new(y[0])
}
let ode = ODE::new(&derivative, 0.0, 10.0, Vector1::new(1.0), ());
let dp45 = DormandPrince45::new();
let controller = PIController::default();
let stop_callback = DiscreteCallback::single(
&|t: f64, _y, _p| t >= 5.0,
&stop,
);
let mut problem = Problem::new(ode, dp45, controller)
.with_discrete_callback(stop_callback);
let solution = problem.solve();
// Should stop at first step after t=5.0
assert!(solution.times.last().unwrap() >= &5.0);
assert!(solution.times.last().unwrap() < &5.5); // Reasonable step size
}
Parameter Modification Test
// Callback that changes parameter at t=5.0
// Verify slope of solution changes at that point
References
-
Julia Implementation:
DiffEqCallbacks.jl/src/discrete_callbacks.jlOrdinaryDiffEq.jl- check order of callback evaluation
-
Design Patterns:
- "Event Handling in DifferentialEquations.jl"
- DifferentialEquations.jl documentation on callback types
-
Use Cases:
- Sundials documentation on user-supplied functions
- MATLAB ODE event handling
Complexity Estimate
Effort: Small (4-6 hours)
- Relatively simple addition
- Similar structure to existing continuous callbacks
- Main work is integration and testing
Risk: Low
- Straightforward concept
- Minimal changes to solver core
- Easy to test
Success Criteria
- DiscreteCallback struct defined and documented
- Integrated into Problem solve loop
- Single-trigger functionality works correctly
- Can combine with continuous callbacks
- All tests pass
- Performance overhead < 5%
- Documentation with examples
Future Enhancements
- CallbackSet for managing multiple callbacks
- Priority/ordering for callback execution
- PresetTimeCallback (fires at specific predetermined times)
- Integration with save points (saveat)
- Callback composition and chaining