Clock & Time API

HORUS provides a unified time system that transparently switches between real time, simulation time, and replay time. Your node code is identical in all three modes — the scheduler selects the clock backend.

Clock Backends

BackendActivated ByBehavior
WallClockDefault (normal mode)Passthrough to Instant::now(). Zero overhead.
SimClock.deterministic(true)Virtual time, advances by exact dt per tick.
ReplayClockScheduler::replay_from(path)Steps through the recording's tick timeline at its average tick period.
use horus::prelude::*;
use std::path::PathBuf;

// Normal mode — uses WallClock (default)
let mut scheduler = Scheduler::new();

// Deterministic mode — uses SimClock
let mut scheduler = Scheduler::new().deterministic(true);

// Replay mode — uses ReplayClock
let mut scheduler = Scheduler::replay_from(PathBuf::from("recording.horus"))
    .expect("failed to load scheduler recording");

Accessing Time in Nodes

Inside your tick() function, use the horus::time:: functions. These read from whichever clock backend is active:

use horus::prelude::*;

struct MyNode { acceleration: f64 }

impl Node for MyNode {
    fn name(&self) -> &str { "my_node" }

    fn tick(&mut self) {
        // Current time (TimeStamp)
        let now = horus::time::now();

        // Timestep for this tick — nominal 1/rate, not measured elapsed (Duration)
        let dt = horus::time::dt();

        // Total elapsed since scheduler start (Duration)
        let elapsed = horus::time::elapsed();

        // Use dt for physics integration
        let velocity = self.acceleration * dt.as_secs_f64();
    }
}

Deterministic Mode

When .deterministic(true) is set, the scheduler uses SimClock:

  • Time advances by one tick period per dependency-graph step, so it equals 1/tick_rate per tick only when every node collapses into a single step. Nodes spread across several .order() tiers advance virtual time once per tier
  • Two runs with the same inputs produce identical clock values
  • No dependency on CPU speed or system load
  • Essential for: reproducible tests, sim-to-real parity, CI determinism
use horus::prelude::*;

let mut scheduler = Scheduler::new()
    .tick_rate(100.hz())       // 100 Hz → dt = 10ms
    .deterministic(true);      // SimClock: virtual time

scheduler.add(PhysicsNode::new()).build();

// After 100 ticks: elapsed = exactly 1.000000000 second
// Regardless of how long the ticks actually took on CPU
scheduler.run();

Replay Mode

A whole recording is replayed with Scheduler::replay_from(path); a single recorded node is added to a live scheduler with .add_replay(path, priority). Both take a PathBuf and return a HorusResult.

  • Scheduler::replay_from steps time through an evenly-spaced tick clock derived from the .horus recording's average tick period — its total wall-clock span divided by its tick count
  • Nodes see the original average cadence; per-tick wall-clock jitter is not reproduced — replay preserves recorded inputs and execution order, not timing
  • Can mix live nodes with replay nodes in the same scheduler (.add_replay alone leaves the scheduler on WallClock)
use horus::prelude::*;
use std::path::PathBuf;

// Full replay — all nodes from recording
let mut scheduler = Scheduler::replay_from(PathBuf::from("session.horus"))
    .expect("failed to load scheduler recording");

// Mixed — 1 replay node + 1 live node
let mut scheduler = Scheduler::new();
// replay sensor data
scheduler.add_replay(PathBuf::from("sensor_recording.horus"), 0)
    .expect("failed to load recording");
// live planner
scheduler.add(PlannerNode::new()).build();
scheduler.run();

Measuring Elapsed Time

horus::time::now() returns a TimeStamp. Subtract two timestamps to get elapsed duration:

let t1 = horus::time::now();
// ... do work ...
let t2 = horus::time::now();
let work_time: Duration = t2 - t1;
// Or, if you only need the duration up to now:
let work_time: Duration = horus::time::since(t1);  // or t1.elapsed()

Clock Trait (Internal)

The Clock trait is internal (#[doc(hidden)]) — users should use horus::time::now(), horus::time::dt(), horus::time::elapsed() instead. It's documented here for framework contributors:

MethodDescription
now()Current ClockInstant
advance(dt)Advance by one tick's duration (no-op for WallClock)
reset()Reset to initial state (used by replay seek(0))
elapsed()Total time since clock construction

See Also