Architecture Overview
HORUS is built on four foundational concepts that work together to create a high-performance robotics runtime:
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:
| State | What Happens |
|---|---|
| Uninitialized | Node exists but hasn't started |
| Initializing | Setting up resources, connecting to hardware |
| Running | Actively processing - tick() called each cycle |
| Paused | Temporarily suspended, can resume instantly |
| Stopping | Cleaning up, releasing resources |
| Stopped | Fully shut down |
| Error | Something went wrong, but recoverable |
| Crashed | Unrecoverable 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:
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:
| Execution path | When it applies | Behavior |
|---|---|---|
| Ready dispatch | Default | BestEffort nodes run in parallel; each starts the instant its last dependency finishes |
| Deterministic | Scheduler::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 fallback | No 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:
| Feature | Purpose |
|---|---|
| WCET Monitoring | Detect nodes exceeding time budgets |
| Circuit Breaker | Isolate failing nodes automatically |
| Watchdog Timers | Detect hung nodes |
| Black Box | Flight 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:
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
PoolBackendtrait (only the/dev/shmmmap 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:
| Connection | Mechanism | Why |
|---|---|---|
| Camera → Detector | TensorPool | Large image, zero-copy |
| Detector → Planner | Topic | Multiple planners might subscribe |
| Planner → Controller | Topic | Monitoring tools can observe |
| Controller → Motors | Topic | Direct 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
| Path | p50 |
|---|---|
| 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:
- Nodes are the unit of composition - Build robots by connecting nodes
- Communication is explicit - No hidden data flow, everything goes through Topic
- The scheduler is your friend - Let it optimize; don't fight it
- Zero-copy by default - Large data should never be copied unnecessarily
- Safety is not optional - Circuit breakers, watchdogs, and black boxes are built in
Next Steps
- Quick Start - Build your first HORUS application
- Core Concepts: Nodes - Deep dive into the node model
- Core Concepts: Topic - Advanced pub/sub patterns
- Scheduler Configuration - Tuning for real-time