Files
differential-equations/roadmap/features/05-discrete-callbacks.md
Connor Johnstone e3788bf607 Added the roadmap
2025-10-23 16:47:48 -04:00

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:

  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<ContinuousCallback> and Vec<DiscreteCallback>
    • Or unified Callback enum:
      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

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

  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