# 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 ```rust pub struct DiscreteCallback<'a, const D: usize, P> { /// Condition function: returns true when callback should fire pub condition: &'a dyn Fn(f64, SVector, &P) -> bool, /// Effect function: modifies ODE state pub effect: &'a dyn Fn(&mut ODE), /// 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: 1. After each successful step 2. Before continuous callback interpolation 3. Can also check before step (for preset times) ### Interaction with Continuous Callbacks Priority order: 1. Discrete callbacks (checked first) 2. 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 `DiscreteCallback` struct - [ ] Condition function field - [ ] Effect function field - [ ] `single_trigger` flag - [ ] `has_fired` state (if single_trigger) - [ ] Constructor - [ ] Convenience constructors - [ ] `new()` - full specification - [ ] `repeating()` - always repeat - [ ] `single()` - fire once only ### Integration with Problem - [ ] Update `Problem` to handle both callback types - [ ] Separate storage: `Vec` and `Vec` - [ ] Or unified `Callback` enum: ```rust pub enum Callback<'a, const D: usize, P> { Continuous(ContinuousCallback<'a, D, P>), Discrete(DiscreteCallback<'a, D, P>), } ``` - [ ] 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 ### Standard Discrete Callbacks Pre-built common callbacks: - [ ] **`stop_at_time(t_stop)`** - [ ] Condition: `t >= t_stop` - [ ] Effect: `stop` - [ ] Single trigger: true - [ ] **`max_iterations(n)`** - [ ] Requires iteration counter in Problem - [ ] Condition: `iteration >= n` - [ ] Effect: `stop` - [ ] **`periodic(interval, effect)`** - [ ] Fires every `interval` time units - [ ] Requires state to track last fire time ### 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 ```rust fn test_stop_at_time() { let params = (); fn derivative(_t: f64, y: Vector1, _p: &()) -> Vector1 { 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 ```rust // Callback that changes parameter at t=5.0 // Verify slope of solution changes at that point ``` ## References 1. **Julia Implementation**: - `DiffEqCallbacks.jl/src/discrete_callbacks.jl` - `OrdinaryDiffEq.jl` - check order of callback evaluation 2. **Design Patterns**: - "Event Handling in DifferentialEquations.jl" - DifferentialEquations.jl documentation on callback types 3. **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