Execution Modes

ℹ️Quick Answer: Which Mode Should I Use?

Use the default. Scheduler::new() runs your nodes in parallel, driven by their dependencies, which works for most robots — add .deterministic(true) when you need a reproducible execution order.

Your SituationRecommended Setup
Learning HORUSScheduler::new() with defaults
PrototypingScheduler::new()
Need maximum speedScheduler::new() with .compute() execution class on heavy nodes
Need a fixed, replayable execution order at a fixed tick rateScheduler::new().deterministic(true).tick_rate(1000_u64.hz()) with .rate() on each node (promotes to RT)

Don't overthink this. Start with Scheduler::new() and configure per-node execution classes as needed.

HORUS supports two execution modes to optimize for different robotics scenarios. You pick the mode on the scheduler (Scheduler::new() is parallel; .deterministic(true) is sequential) and tune individual nodes with per-node execution classes (.compute(), .on(topic), .async_io(), plus .rate()/.budget()/.deadline(), which promote a node to the RT class) — the scheduler handles the rest.

Sequential Mode

The reproducible-order choice: one node at a time, the same order every tick.

Sequential mode is opt-in via Scheduler::new().deterministic(true). With it, all nodes run one-by-one on the main thread in dependency/priority order, producing the same execution order every tick.

How It Works

  • Nodes execute one-by-one in priority order
  • Same execution order every tick
  • Identical execution order across runs, on a virtual clock (SimClock)

Characteristics

MetricValue
DeterministicYes
Multi-coreNo (single thread)
Best ForReplayable tests, record/replay debugging, fixed-order control loops

When to Use

  • Systems needing reproducible behaviour from one run to the next
  • Control loops where a single fixed node order is easier to reason about
  • Debugging complex timing issues
  • Record/replay workflows, where a recorded run has to replay identically

Enable sequential mode with Scheduler::new().deterministic(true). Add real-time constraints per node with .rate() (or .budget()/.deadline()), which promotes the node to the RT execution class automatically.

use horus::prelude::*;

// Fixed-order control loop — 1 kHz tick, RT execution class
let mut scheduler = Scheduler::new().deterministic(true).tick_rate(1000_u64.hz());
scheduler.add(safety_monitor).order(0).rate(1000_u64.hz()).done();
scheduler.add(controller).order(1).rate(1000_u64.hz()).done();
scheduler.run()?;

Parallel Mode

Multi-core execution for maximum throughput.

Parallel mode is what Scheduler::new() gives you by default: independent nodes execute concurrently, and nodes that depend on each other run in order — within the main-loop group.

How It Works

  • Runs independent BestEffort nodes concurrently
  • Respects dependency order (topic graph, or .order() tiers as fallback) among BestEffort nodes
  • Uses a thread pool for concurrent execution
⚠️Dependency order does not cross execution classes

The dependency graph is built over the nodes the main thread keeps. Before it is built, the class partition moves every RT, Compute, Event and AsyncIo node out to its own executor, and the graph is then rebuilt from what remains — so it contains no edge between a main-loop node and an executor-owned one, and none between two executor-owned nodes.

The RT executor is explicit that it provides no ordering guarantee between RT nodes: each becomes its own chain. If you need node B to observe node A's output within one tick, they must be in the same group. Across groups, order is whatever the threads happen to do, and B reads whatever A last published.

Characteristics

MetricValue
DeterministicNo
Multi-coreYes
Best ForMulti-sensor fusion, compute-heavy pipelines

When to Use

  • Multi-sensor robots
  • Compute-heavy pipelines
  • Systems with many independent nodes
  • When you have multiple CPU cores available

Parallelism is decided per node, not by .order() alone. .compute() nodes always run concurrently in the compute thread pool regardless of order. Default (BestEffort) nodes are parallelised by the dependency graph, which is built from the pub/sub metadata registered during init() and falls back to .order() tiers when there is none — same order value means one parallel step, different order values run sequentially. Topics first used inside tick() register too late to enter the graph, so those programs keep the tier ordering for the whole run:

use horus::prelude::*;

let mut scheduler = Scheduler::new();

// These sensor nodes can run in parallel (same order number)
scheduler.add(lidar_node).order(0).done();
scheduler.add(camera_node).order(0).done();
scheduler.add(imu_node).order(0).done();

// Fusion runs after all sensors (higher order number = runs later)
scheduler.add(fusion_node).order(1).done();
scheduler.run()?;

Mode Comparison

FeatureSequentialParallel
DeterministicYesNo
Multi-coreNoYes
Reproducible for audit/replayYesNo

Reproducibility is an engineering property, not a safety qualification: HORUS carries no functional-safety evidence and is validated in simulation only. See Certification Status before using either mode in a system that has to be certified.

Examples

Fixed-Order Control Loop

use horus::prelude::*;

// Deterministic 1 kHz control loop, every node on the RT execution class
let mut scheduler = Scheduler::new().deterministic(true).tick_rate(1000_u64.hz());
scheduler.add(safety_monitor).order(0).rate(1000_u64.hz()).done();
scheduler.add(force_feedback).order(1).rate(1000_u64.hz()).done();
scheduler.add(motion_controller).order(1).rate(1000_u64.hz()).done();
scheduler.run()?;

Racing Robot

use horus::prelude::*;

// Competition robot — maximum speed
let mut scheduler = Scheduler::new();

// .compute() nodes run concurrently in the compute pool — order is not what parallelises them
scheduler.add(vision_pipeline).compute().done();
scheduler.add(path_planner).compute().done();

// The control loop gets .rate(), which promotes it to the RT execution class
scheduler.add(motor_controller).rate(1000_u64.hz()).done();
scheduler.run()?;

Multi-Sensor Robot

use horus::prelude::*;

// Research robot with many sensors — parallel sensor processing
let mut scheduler = Scheduler::new();

// These run in parallel (same order number)
scheduler.add(lidar).order(0).done();
scheduler.add(camera).order(0).done();
scheduler.add(radar).order(0).done();
scheduler.add(imu).order(0).done();
// Fusion runs after all sensors (higher order number = runs later)
scheduler.add(fusion).order(1).done();
scheduler.run()?;

Next Steps