Architecture Overview

HORUS is built on four foundational concepts that work together to create a high-performance robotics runtime:

Loading diagram...
The Four Pillars of HORUS

The Node Model

Everything in HORUS is a Node. A node is an independent unit of computation with a well-defined lifecycle.

Why Nodes?

Robotics systems are inherently modular. A robot has sensors, actuators, planners, and controllers - each with different timing requirements and failure modes. By making each component a node, HORUS provides:

  • Isolation - A failing camera driver doesn't crash your motion controller
  • Composability - Mix and match nodes to build different robots
  • Testability - Test each node independently before integration
  • Reusability - Share nodes across projects via the package registry

Node Lifecycle

Every node follows the same lifecycle, ensuring predictable behavior:

Loading diagram...
Node State Machine
StateWhat Happens
UninitializedNode exists but hasn't started
InitializingSetting up resources, connecting to hardware
RunningActively processing - tick() called each cycle
PausedTemporarily suspended, can resume instantly
StoppingCleaning up, releasing resources
StoppedFully shut down
ErrorSomething went wrong, but recoverable
CrashedUnrecoverable failure

The Tick Model

Nodes don't run continuously - they tick. Each tick is a discrete unit of work:

fn tick(&mut self) {
    // Read inputs
    if let Some(sensor_data) = self.sensor_topic.recv() {
        // Process
        let command = self.compute_response(sensor_data);

        // Write outputs
        self.command_topic.send(command);
    }
}

This model enables:

  • Deterministic timing - Know exactly when each node runs
  • Profiling - Measure how long each tick takes
  • Scheduling intelligence - The scheduler can optimize execution order

Communication

Nodes need to exchange data. HORUS provides a single Topic API that automatically selects the optimal backend based on how many publishers and subscribers the topic has and whether the message type is plain-old-data (POD).

Topic: One API, Automatic Optimization

You always use the same Topic::new() call. HORUS detects the topology and picks the fastest backend for it. Every backend is shared-memory backed, so measured latency depends on where the two ends run and how many participants contend: from ~20ns (both ends on one thread) to ~280ns (cross-process with multiple publishers):

// Same API for all communication patterns
let topic: Topic<Image> = Topic::new("camera.image")?;
topic.send(&frame);

// Another node subscribes — same API
let topic: Topic<Image> = Topic::new("camera.image")?;
if let Some(frame) = topic.recv() {
    // Process frame
}

No configuration needed — the backend is selected and upgraded transparently as participants join or leave.

Cross-Process Communication

Topics work transparently across process boundaries using shared memory:

Loading diagram...
Transparent Cross-Process Communication

Data goes through shared memory with sub-microsecond latency. Plain-old-data types being broadcast to many subscribers get a dedicated broadcast path automatically.


The Scheduler

The scheduler is the brain of HORUS. It decides when and how nodes execute.

Why a Scheduler?

Without coordination, nodes would:

  • Fight for CPU resources
  • Miss real-time deadlines
  • Waste cycles waiting for data that hasn't arrived

The HORUS scheduler solves these problems with intelligent orchestration.

Execution Order

There is no mode switch to pick between. By default the scheduler hands Rt, Compute, Event and AsyncIo nodes to dedicated executors, and runs the remaining BestEffort nodes in parallel on the main thread using the dependency graph it builds from the topics they publish and subscribe. Turning on deterministic mode replaces all of that with one sequential main thread:

Loading diagram...
Default Parallel Dispatch vs. Deterministic Mode
Execution pathWhen it appliesBehavior
Ready dispatchDefaultBestEffort nodes run in parallel; each starts the instant its last dependency finishes
DeterministicScheduler::new().deterministic(true)No executor threads at all - every node ticks sequentially on the main thread in the same order each run, with the simulated clock advancing between graph steps. For replay and tick_once() testing
Sequential fallbackNo usable dependency graph (for example, a cycle was detected)Nodes tick one at a time in .order() priority sequence

Profiling & Execution Classes

The scheduler measures how each node behaves, and each node declares what kind of work it does:

  • Runtime Profiler - Tracks how long each node takes (mean, stddev, min/max)
  • Execution Classes - Nodes are annotated with an execution class (Rt, Compute, Event, AsyncIo, BestEffort) via the builder methods .rate(), .compute(), .on("topic"), .async_io(). Each class maps to a different scheduling strategy.

Safety Systems

Real robots need safety guarantees. The scheduler provides:

FeaturePurpose
WCET MonitoringDetect nodes exceeding time budgets
Circuit BreakerIsolate failing nodes automatically
Watchdog TimersDetect hung nodes
Black BoxFlight recorder for post-mortem analysis

Memory System

Large data (images, point clouds, ML tensors) needs special handling. Copying a 4K image between nodes would destroy performance.

Zero-Copy Design

HORUS uses shared memory pools for large data:

Loading diagram...
Zero-Copy: Write Once, Read Many

The image data is written once to shared memory. Each subscriber reads directly from the same memory location - no copying.

TensorPool

TensorPool manages shared memory allocation:

use horus::types::Tensor; // not in the prelude

// Auto-managed pool via Topic<Tensor>
let topic: Topic<Tensor> = Topic::new("camera.rgb")?;
let handle = topic.alloc_tensor(&[1080, 1920, 3], TensorDtype::U8, Device::cpu())?;

// Write data (only done once)
let data = handle.data_slice_mut()?;
camera.capture_into(data);

// Send through Topic - only the 168-byte descriptor is copied, not the image
topic.send_handle(&handle);

TensorPool characteristics:

  • Allocation takes a slot from a lock-free free stack; an alloc/release cycle costs on the order of a microsecond for small tensors and scales with tensor size, because a slot's data region is zeroed when it is released
  • Automatic reference counting
  • Works across processes
  • Pluggable data-region backend via the PoolBackend trait (only the /dev/shm mmap backend ships today; CUDA backends are planned)

Python Integration

Python nodes share the same memory pool:

import horus
import numpy as np

# Receive tensor from Rust node
tensor = topic.recv()

# Zero-copy numpy view - no data copied!
array = np.array(tensor, copy=False)

# Process with numpy/PyTorch
result = model.predict(array)

Data Flow Example

Here's how these concepts work together in a typical perception-to-action pipeline:

Loading diagram...
Perception-Planning-Control Pipeline
ConnectionMechanismWhy
Camera → DetectorTensorPoolLarge image, zero-copy
Detector → PlannerTopicMultiple planners might subscribe
Planner → ControllerTopicMonitoring tools can observe
Controller → MotorsTopicDirect pipeline connection

Total pipeline latency: Under 1 microsecond for message passing when all four nodes run in the same process. Every hop still goes through shared memory, so a hop that actually crosses a process boundary costs closer to 150-280 ns on its own. Tensor allocation is a separate cost that scales with tensor size.


Performance Summary

Pathp50
Topic send, both ends on one thread (SpscShm)~20 ns
Topic send, cross-thread 1P-1C (SpscShm)~63 ns
Topic one-way, cross-process 1P-1C (SpscShm)~151 ns
Topic one-way, cross-process 1P-MC (SpmcShm route)~191 ns
Topic broadcast, cross-process POD (PodShm)~223 ns

Measured with all_paths_latency on an Intel Core i7-10750H. Every topic is shared-memory backed, so what changes between rows is where the two ends run and how many participants contend - not the transport. The backend in parentheses names the route the benchmark exercised, not what auto-detection picks for that topology: detection maps 1P-1C to SpscShm, MP-1C to MpscShm, POD broadcast to PodShm, and non-POD broadcast/MPMC to FanoutShm. SpmcShm is never auto-selected - its consumers share one tail and compete for messages, so a real 1-publisher / many-subscriber POD topic gets PodShm instead. Full tables in Benchmarks.


Design Philosophy

HORUS is built on these principles:

  1. Nodes are the unit of composition - Build robots by connecting nodes
  2. Communication is explicit - No hidden data flow, everything goes through Topic
  3. The scheduler is your friend - Let it optimize; don't fight it
  4. Zero-copy by default - Large data should never be copied unnecessarily
  5. Safety is not optional - Circuit breakers, watchdogs, and black boxes are built in

Next Steps