Skip to content

Event Callbacks

Event callbacks allow you to respond to detected events during propagation. Callbacks can log information, inspect state, modify the spacecraft state (for impulsive maneuvers), or control propagation flow.

Callback Function Signature

To define a callback, create a function matching the following signature:

def callback(epoch: Epoch, state: np.ndarray) -> tuple[np.ndarray, EventAction]:
    """
    Args:
        epoch: The epoch when the event occurred
        state: The spacecraft state vector at event time [x, y, z, vx, vy, vz]

    Returns:
        tuple: (new_state, action)
            - new_state: Modified state vector (or original if unchanged)
            - action: EventAction.CONTINUE or EventAction.STOP
    """
    # Process event...
    return (state, bh.EventAction.CONTINUE)
type DEventCallback = Box<
    dyn Fn(
        Epoch,                           // Event epoch
        &DVector<f64>,                   // Current state
        Option<&DVector<f64>>,           // Optional parameters
    ) -> (
        Option<DVector<f64>>,            // New state (None = unchanged)
        Option<DVector<f64>>,            // New params (None = unchanged)
        EventAction,                     // Continue or Stop
    ) + Send + Sync,
>;

EventAction Options

The callback return value includes an EventAction that controls propagation behavior:

Action Behavior
CONTINUE Continue propagation after processing the event
STOP Halt propagation immediately after the event

When to Use STOP

Use EventAction.STOP when:

  • A terminal condition has been reached (e.g., re-entry)
  • The propagation goal has been achieved
  • An error condition is detected
  • You want to examine state at a specific event before deciding to continue

When to Use CONTINUE

Use EventAction.CONTINUE for:

  • Logging and monitoring events
  • Impulsive maneuvers (state changes but propagation continues)
  • Intermediate waypoints
  • Data collection triggers

Defining Callbacks

Callbacks receive the event epoch and state, and return a tuple containing the (possibly modified) state and an action directive.

Logging Callback

A simple callback that logs event information without modifying state:

import numpy as np
import brahe as bh

# Initialize EOP data
bh.initialize_eop()

# Create initial epoch and state - elliptical orbit
epoch = bh.Epoch.from_datetime(2024, 1, 1, 12, 0, 0.0, 0.0, bh.TimeSystem.UTC)
oe = np.array([bh.R_EARTH + 500e3, 0.01, 45.0, 15.0, 30.0, 45.0])
state = bh.state_koe_to_eci(oe, bh.AngleFormat.DEGREES)

# Track callback invocations
callback_count = 0


# Define a logging callback
def logging_callback(event_epoch, event_state):
    """Log event details without modifying state."""
    global callback_count
    callback_count += 1

    # Compute orbital elements at event time
    koe = bh.state_eci_to_koe(event_state, bh.AngleFormat.DEGREES)
    altitude = koe[0] - bh.R_EARTH

    print(f"  Event #{callback_count}:")
    print(f"    Epoch: {event_epoch}")
    print(f"    Altitude: {altitude / 1e3:.1f} km")
    print(f"    True anomaly: {koe[5]:.1f} deg")

    # Return unchanged state with CONTINUE action
    return (event_state, bh.EventAction.CONTINUE)


# Define a callback that stops propagation
def stop_callback(event_epoch, event_state):
    """Stop propagation when event occurs."""
    print(f"  Stopping at {event_epoch}")
    return (event_state, bh.EventAction.STOP)


# Create propagator
prop = bh.NumericalOrbitPropagator(
    epoch,
    state,
    bh.NumericalPropagationConfig.default(),
    bh.ForceModelConfig.two_body(),
    None,
)

# Create time event with logging callback
event_log = bh.TimeEvent(epoch + 1000.0, "Log Event").with_callback(logging_callback)
prop.add_event_detector(event_log)

# Create another time event
event_log2 = bh.TimeEvent(epoch + 2000.0, "Log Event 2").with_callback(logging_callback)
prop.add_event_detector(event_log2)

# Propagate for half an orbit
orbital_period = bh.orbital_period(oe[0])
print("Propagating with logging callbacks:")
prop.propagate_to(epoch + orbital_period / 2)

print(f"\nCallback invoked {callback_count} times")

# Now demonstrate STOP action
print("\nDemonstrating STOP action:")
prop2 = bh.NumericalOrbitPropagator(
    epoch,
    state,
    bh.NumericalPropagationConfig.default(),
    bh.ForceModelConfig.two_body(),
    None,
)

# Event that stops propagation at t+500s
stop_event = bh.TimeEvent(epoch + 500.0, "Stop Event").with_callback(stop_callback)
prop2.add_event_detector(stop_event)

# Try to propagate for one full orbit
prop2.propagate_to(epoch + orbital_period)

# Check where propagation actually stopped
actual_duration = prop2.current_epoch - epoch
print(f"  Requested duration: {orbital_period:.1f}s")
print(f"  Actual duration: {actual_duration:.1f}s")
print(f"  Stopped early: {actual_duration < orbital_period}")

# Validate
assert callback_count == 2
assert actual_duration < orbital_period
use brahe as bh;
use bh::events::{DEventCallback, DTimeEvent, EventAction};
use bh::traits::DStatePropagator;
use nalgebra as na;
use std::f64::consts::PI;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

fn main() {
    // Initialize EOP data
    bh::initialize_eop().unwrap();

    // Create initial epoch and state - elliptical orbit
    let epoch = bh::Epoch::from_datetime(2024, 1, 1, 12, 0, 0.0, 0.0, bh::TimeSystem::UTC);
    let oe = na::SVector::<f64, 6>::new(bh::R_EARTH + 500e3, 0.01, 45.0, 0.0, 0.0, 0.0);
    let state = bh::state_koe_to_eci(oe, bh::AngleFormat::Degrees);

    // Track callback invocations
    let callback_count = Arc::new(AtomicUsize::new(0));

    // Define a logging callback
    let count_ref = callback_count.clone();
    let logging_callback: DEventCallback = Box::new(
        move |event_epoch: bh::Epoch,
              event_state: &na::DVector<f64>,
              _params: Option<&na::DVector<f64>>|
              -> (
            Option<na::DVector<f64>>,
            Option<na::DVector<f64>>,
            EventAction,
        ) {
            let count = count_ref.fetch_add(1, Ordering::SeqCst) + 1;

            // Compute orbital elements at event time
            let state_vec = na::SVector::<f64, 6>::from_column_slice(event_state.as_slice());
            let koe = bh::state_eci_to_koe(state_vec, bh::AngleFormat::Degrees);
            let altitude = koe[0] - bh::R_EARTH;

            println!("  Event #{}:", count);
            println!("    Epoch: {}", event_epoch);
            println!("    Altitude: {:.1} km", altitude / 1e3);
            println!("    True anomaly: {:.1} deg", koe[5]);

            // Return unchanged state with CONTINUE action
            (None, None, EventAction::Continue)
        },
    );

    // Define another logging callback for second event
    let count_ref2 = callback_count.clone();
    let logging_callback2: DEventCallback = Box::new(
        move |event_epoch: bh::Epoch,
              event_state: &na::DVector<f64>,
              _params: Option<&na::DVector<f64>>|
              -> (
            Option<na::DVector<f64>>,
            Option<na::DVector<f64>>,
            EventAction,
        ) {
            let count = count_ref2.fetch_add(1, Ordering::SeqCst) + 1;

            let state_vec = na::SVector::<f64, 6>::from_column_slice(event_state.as_slice());
            let koe = bh::state_eci_to_koe(state_vec, bh::AngleFormat::Degrees);
            let altitude = koe[0] - bh::R_EARTH;

            println!("  Event #{}:", count);
            println!("    Epoch: {}", event_epoch);
            println!("    Altitude: {:.1} km", altitude / 1e3);
            println!("    True anomaly: {:.1} deg", koe[5]);

            (None, None, EventAction::Continue)
        },
    );

    // Create propagator
    let mut prop = bh::DNumericalOrbitPropagator::new(
        epoch,
        na::DVector::from_column_slice(state.as_slice()),
        bh::NumericalPropagationConfig::default(),
        bh::ForceModelConfig::two_body_gravity(),
        None,
        None,
        None,
        None,
    )
    .unwrap();

    // Create time events with logging callbacks
    let event_log = DTimeEvent::new(epoch + 1000.0, "Log Event".to_string())
        .with_callback(logging_callback);
    prop.add_event_detector(Box::new(event_log));

    let event_log2 = DTimeEvent::new(epoch + 2000.0, "Log Event 2".to_string())
        .with_callback(logging_callback2);
    prop.add_event_detector(Box::new(event_log2));

    // Propagate for half an orbit
    let orbital_period = 2.0 * PI * (oe[0].powi(3) / bh::GM_EARTH).sqrt();
    println!("Propagating with logging callbacks:");
    prop.propagate_to(epoch + orbital_period / 2.0);

    let final_count = callback_count.load(Ordering::SeqCst);
    println!("\nCallback invoked {} times", final_count);

    // Now demonstrate STOP action
    println!("\nDemonstrating STOP action:");

    // Define a callback that stops propagation
    let stop_callback: DEventCallback = Box::new(
        move |event_epoch: bh::Epoch,
              _event_state: &na::DVector<f64>,
              _params: Option<&na::DVector<f64>>|
              -> (
            Option<na::DVector<f64>>,
            Option<na::DVector<f64>>,
            EventAction,
        ) {
            println!("  Stopping at {}", event_epoch);
            (None, None, EventAction::Stop)
        },
    );

    let mut prop2 = bh::DNumericalOrbitPropagator::new(
        epoch,
        na::DVector::from_column_slice(state.as_slice()),
        bh::NumericalPropagationConfig::default(),
        bh::ForceModelConfig::two_body_gravity(),
        None,
        None,
        None,
        None,
    )
    .unwrap();

    // Event that stops propagation at t+500s
    let stop_event =
        DTimeEvent::new(epoch + 500.0, "Stop Event".to_string()).with_callback(stop_callback);
    prop2.add_event_detector(Box::new(stop_event));

    // Try to propagate for one full orbit
    prop2.propagate_to(epoch + orbital_period);

    // Check where propagation actually stopped
    let actual_duration = prop2.current_epoch() - epoch;
    println!("  Requested duration: {:.1}s", orbital_period);
    println!("  Actual duration: {:.1}s", actual_duration);
    println!("  Stopped early: {}", actual_duration < orbital_period);

    // Validate
    assert_eq!(final_count, 2);
    assert!(actual_duration < orbital_period);
}

Attaching Callbacks to Events

Use the with_callback() method to attach a callback to any event detector:

1
2
3
4
5
6
7
8
# Create event
event = bh.TimeEvent(target_epoch, "My Event")

# Attach callback
event_with_callback = event.with_callback(my_callback_function)

# Add to propagator
prop.add_event_detector(event_with_callback)

The with_callback() method returns a new event detector with the callback attached, allowing method chaining.

State Modification

Callbacks can modify the spacecraft state by returning a new state vector. This is the mechanism for implementing impulsive maneuvers.

Modifying State

def velocity_change_callback(epoch, state):
    new_state = state.copy()

    # Add delta-v in velocity direction
    v = state[3:6]
    v_hat = v / np.linalg.norm(v)
    delta_v = 100.0  # m/s
    new_state[3:6] += delta_v * v_hat

    return (new_state, bh.EventAction.CONTINUE)

Physical Consistency

When modifying state, ensure physical consistency:

  • Position changes are unusual except for specific scenarios
  • Velocity changes should respect momentum conservation for realistic maneuvers
  • Large changes may cause numerical issues in subsequent integration steps

For complete impulsive maneuver examples, see Maneuvers.

Multiple Callbacks

Each event detector can have one callback. For multiple actions at the same event, either:

  1. Perform all actions within a single callback
  2. Create multiple event detectors at the same time/condition
1
2
3
4
5
6
# Single callback performing multiple actions
def multi_action_callback(epoch, state):
    log_event(epoch, state)
    record_telemetry(epoch, state)
    new_state = apply_correction(state)
    return (new_state, bh.EventAction.CONTINUE)

Callback Execution Order

When multiple events occur at the same epoch:

  1. Events are processed in the order their detectors were added
  2. State modifications from earlier callbacks are passed to later callbacks
  3. If any callback returns STOP, propagation halts after all callbacks execute

See Also