# Part IV: Runtime System
**Version:** 1.0 Draft
**Last Updated:** 2025-12-20
---
## 1. Introduction
The **runtime system** executes the graph constructed during wiring. It manages:
- Object lifecycle (initialization, start, stop, disposal)
- Time progression and scheduling
- Evaluation ordering and execution
- Event propagation and notification
```mermaid
graph TB
subgraph "Runtime Components"
G[Graph]
SCH[Scheduler]
CLK[Clock]
ENG[Engine]
N1[Node 1]
N2[Node 2]
N3[Node 3]
end
ENG --> SCH
ENG --> CLK
ENG --> G
G --> N1
G --> N2
G --> N3
SCH --> |"schedule"| N1
SCH --> |"schedule"| N2
CLK --> |"current time"| ENG
```
---
## 2. Lifecycle
All runtime components follow a consistent lifecycle:
### 2.1 State Machine
```mermaid
stateDiagram-v2
[*] --> Constructed
Constructed --> Initialized: initialise()
Initialized --> Started: start()
Started --> Evaluating: eval()
Evaluating --> Started: tick complete
Started --> Stopped: stop()
Stopped --> Started: start()
Stopped --> Disposed: dispose()
Disposed --> [*]
```
### 2.2 Lifecycle Methods
| Method | Description | Called When |
|--------|-------------|-------------|
| `initialise()` | One-time setup, allocate resources | Before first start |
| `start()` | Begin active operation | Each run/resume |
| `eval()` | Perform computation | Each scheduled tick |
| `stop()` | Pause operation | End run or pause |
| `dispose()` | Release all resources | Final cleanup |
### 2.3 Lifecycle Guarantees
| Guarantee | Description |
|-----------|-------------|
| Single Init | `initialise()` called exactly once |
| Matched Start/Stop | Every `start()` has matching `stop()` |
| Order | `initialise` → `start` → (eval)* → `stop` → `dispose` |
| Topological | Nodes initialized/started in dependency order |
| Reverse Dispose | Nodes disposed in reverse dependency order |
---
## 3. Graph
The **Graph** is the top-level runtime container:
### 3.1 Graph Structure
```mermaid
classDiagram
class Graph {
+parent_node: Node | None
+graph_id: str
+nodes: Sequence[Node]
+evaluation_clock: EvaluationClock
+engine_evaluation_clock: EngineEvaluationClock
+evaluation_engine: EvaluationEngine
+evaluation_time: datetime
+last_evaluation_time: datetime
+is_started: bool
+initialise()
+start()
+stop()
+dispose()
+schedule_node(node_ndx, when, force_set)
+evaluate_graph()
}
class Node {
+graph: Graph
+inputs: list[Input]
+output: Output
+eval()
}
class EvaluationClock {
+evaluation_time: datetime
+now: datetime
+cycle_time: timedelta
+next_cycle_evaluation_time: datetime
}
Graph "1" --> "*" Node
Graph "1" --> "1" EvaluationClock
```
### 3.2 Graph Properties
| Property | Type | Description |
|----------|------|-------------|
| `parent_node` | `Node | None` | Parent node if nested graph |
| `graph_id` | `str` | Unique graph identifier |
| `nodes` | `Sequence[Node]` | All nodes in topological order |
| `evaluation_time` | `datetime` | Current tick's time |
| `last_evaluation_time` | `datetime` | Time of last evaluation |
| `evaluation_clock` | `EvaluationClock` | User-facing clock |
| `engine_evaluation_clock` | `EngineEvaluationClock` | Engine clock (internal) |
| `evaluation_engine` | `EvaluationEngine` | Evaluation engine |
| `evaluation_engine_api` | `EvaluationEngineApi` | Engine API interface |
| `traits` | `GraphTraits` | Graph configuration traits |
| `receiver` | `_SenderReceiverState` | Push source event receiver |
| `schedule` | `list[datetime]` | Scheduled evaluation times |
| `is_started` | `bool` | True if graph is running |
---
## 4. Scheduler
The **Scheduler** manages when nodes are evaluated:
### 4.1 Scheduling Model
```mermaid
graph TB
subgraph "Scheduler"
PQ[Priority Queue
time → nodes]
NOW[Current Time]
READY[Ready Queue]
end
N1[Node 1] --> |"schedule_node(t1)"| PQ
N2[Node 2] --> |"schedule_node(t2)"| PQ
N3[Node 3] --> |"notify()"| READY
PQ --> |"pop at time"| READY
READY --> |"evaluate"| ENG[Engine]
```
### 4.2 Scheduling Operations
| Operation | Description |
|-----------|-------------|
| `schedule(node, time)` | Add node to be evaluated at `time` |
| `pop_scheduled_nodes(time)` | Get all nodes scheduled for `time` |
| `next_scheduled_time()` | Return next time with pending nodes |
| `has_pending()` | True if any nodes are scheduled |
### 4.3 Priority Ordering
Within a single tick, nodes are ordered by:
1. **Topological rank** (sources before dependents)
2. **Node ID** (for deterministic ordering within same rank)
```mermaid
graph LR
subgraph "Tick at T=100"
direction LR
R1[Rank 0
Sources]
R2[Rank 1
Compute]
R3[Rank 2
Compute]
R4[Rank 3
Sinks]
end
R1 --> R2 --> R3 --> R4
```
---
## 5. Clock
The **Clock** provides time management:
### 5.1 Clock Types
```mermaid
graph TD
CLK[Clock]
CLK --> SIM[Simulation Clock]
CLK --> RT[Real-Time Clock]
SIM --> |"Advances as fast as possible"| FAST[Fast-forward]
RT --> |"Tracks wall clock"| WALL[Wall Clock]
```
### 5.2 Clock Properties
| Property | Description |
|----------|-------------|
| `now` | Current time |
| `evaluation_time` | Time being evaluated |
| `push_time` | Source of async events |
| `cycle_id` | Current evaluation cycle ID |
### 5.3 Time Semantics
| Mode | Time Behavior |
|------|---------------|
| Simulation | Jumps directly to next scheduled event |
| Real-Time | Sleeps until next scheduled event |
---
## 6. Evaluation Loop
### 6.1 Main Loop
```mermaid
sequenceDiagram
participant E as Engine
participant S as Scheduler
participant G as Graph
participant N as Nodes
E->>S: Initialize
loop Until end_time or stop
E->>S: next_scheduled_time()
S-->>E: next_time
alt Simulation Mode
E->>G: Set evaluation_time = next_time
else Real-Time Mode
E->>E: Sleep until next_time
E->>G: Set evaluation_time = now()
end
E->>S: pop_scheduled_nodes()
S-->>E: nodes_to_evaluate
loop For each node in topological order
E->>N: eval()
N->>N: Read inputs
N->>N: Compute
N->>N: Write output
N-->>E: May schedule more nodes
end
E->>E: Increment cycle
end
```
### 6.2 Evaluation Phases Per Tick
| Phase | Description |
|-------|-------------|
| 1. Collect | Get all nodes scheduled for current time |
| 2. Sort | Order by topological rank |
| 3. Evaluate | Call `eval()` on each node |
| 4. Propagate | Handle notifications and scheduling |
| 5. Cleanup | Reset per-tick state |
### 6.3 Node Evaluation
```mermaid
sequenceDiagram
participant E as Engine
participant N as Node
participant I as Inputs
participant O as Output
E->>N: eval()
N->>I: Check modified inputs
alt Any active input modified
N->>I: Read values
N->>N: Execute function
alt Has output
N->>O: Write result
O->>O: Mark modified
O->>E: Notify dependents
end
end
```
---
## 7. Notification System
### 7.1 Notification Flow
```mermaid
graph TB
O[Output Modified]
O --> N1[Notify Bound Inputs]
N1 --> N2[Check Active Status]
N2 --> |"Active"| S[Schedule Node]
N2 --> |"Passive"| SKIP[Skip]
S --> Q[Add to Scheduler Queue]
```
### 7.2 Notification Idempotency
Notifications are deduplicated per evaluation time:
```python
def notify(self, evaluation_time):
if self._notify_time != evaluation_time:
self._notify_time = evaluation_time
self._propagate_to_parent()
self._schedule_node()
```
### 7.3 Parent Chain Notification
For nested time-series (bundles, collections):
```mermaid
graph TB
C[Child TS Modified]
C --> P1[Parent TSB Field]
P1 --> P2[Parent TSB]
P2 --> N[Owning Node Input]
N --> S[Scheduler]
```
---
## 8. Input/Output Binding
### 8.1 Binding Model
```mermaid
graph LR
subgraph "Node A"
OA[Output]
end
subgraph "Node B"
IB[Input]
end
subgraph "Node C"
IC[Input]
end
OA --> |"bind"| IB
OA --> |"bind"| IC
```
### 8.2 Binding Operations
| Operation | Description |
|-----------|-------------|
| `bind_output(output)` | Connect input to output |
| `unbind_output()` | Disconnect from output |
| `bound` | True if connected to an output |
### 8.3 Binding Effects
When binding occurs:
1. Input marks itself as bound
2. Input subscribes to output notifications
3. If output is valid, input copies current value
4. If input is active, node is scheduled
---
## 9. Active/Passive Subscriptions
### 9.1 Subscription Model
| State | Behavior |
|-------|----------|
| **Active** | Input triggers node evaluation on modification |
| **Passive** | Input receives updates but doesn't trigger evaluation |
### 9.2 State Transitions
```mermaid
stateDiagram-v2
[*] --> Active: default
Active --> Passive: make_passive()
Passive --> Active: make_active()
```
### 9.3 Subscription Use Cases
```python
@compute_node
def sample(trigger: TS[bool], value: TS[int]) -> TS[int]:
"""Only emit when trigger fires, using current value."""
# 'value' could be passive - only read when trigger fires
return value.value
```
---
## 10. Modification Tracking
### 10.1 Tracking Invariant
```
modified = (last_modified_time == evaluation_time)
```
A time-series is **modified** if it was changed in the current tick.
### 10.2 Tracking Implementation
| Property | Description |
|----------|-------------|
| `last_modified_time` | Time of most recent modification |
| `modified` | Computed: `last_modified_time == now` |
### 10.3 Modification Propagation
```mermaid
sequenceDiagram
participant V as Value
participant O as Output
participant I as Inputs
participant N as Nodes
V->>O: set_value()
O->>O: Update last_modified_time
O->>O: Mark modified
O->>I: Notify bound inputs
I->>N: Schedule if active
```
---
## 11. Error Handling
### 11.1 Error Types
| Error Type | Description |
|------------|-------------|
| `GraphError` | General graph errors |
| `NodeError` | Node-specific errors |
| `SchedulerError` | Scheduling failures |
| `EvaluationError` | Runtime evaluation failures |
### 11.2 Error Propagation
```mermaid
graph TB
N[Node Error]
N --> EO[Error Output]
EO --> EH[Error Handler Node]
EH --> |"recover"| CONT[Continue]
EH --> |"fail"| STOP[Stop Graph]
```
### 11.3 Error Outputs
Nodes can have optional error outputs:
```python
@compute_node
def risky_operation(x: TS[int]) -> TSB[ValueWithError]:
try:
result = complex_calculation(x.value)
return {"value": result, "error": None}
except Exception as e:
return {"value": None, "error": str(e)}
```
---
## 12. Execution Modes
### 12.1 Simulation Mode
```mermaid
graph LR
T1[t=0] --> |"instant"| T2[t=100]
T2 --> |"instant"| T3[t=500]
T3 --> |"instant"| T4[t=1000]
```
**Characteristics:**
- Time jumps to next scheduled event
- No push sources allowed
- Deterministic execution
- Reproducible results
### 12.2 Real-Time Mode
```mermaid
graph LR
T1[t=0] --> |"sleep 100ms"| T2[t=100]
T2 --> |"sleep 400ms"| T3[t=500]
T3 --> |"sleep 500ms"| T4[t=1000]
```
**Characteristics:**
- Time tracks wall clock
- Push sources allowed
- Non-deterministic (external events)
- May process late events
### 12.3 Mode Comparison
| Aspect | Simulation | Real-Time |
|--------|------------|-----------|
| Time progression | As fast as possible | Wall clock |
| Push sources | Prohibited | Allowed |
| Determinism | Guaranteed | Not guaranteed |
| Reproducibility | Full | Limited |
| Use case | Backtesting | Live operation |
---
## 13. Injectable Types
### 13.1 Available Injectables
| Injectable | Description |
|------------|-------------|
| `STATE[T]` | Mutable state container |
| `SCHEDULER` | Node scheduling access |
| `CLOCK` | Time information |
| `REPLAY_STATE` | State with replay support |
| `OUTPUT` | Direct output access |
| `CONTEXT` | Runtime context |
### 13.2 Usage
```python
@compute_node
def stateful_counter(
trigger: TS[bool],
state: STATE[int] = 0
) -> TS[int]:
if trigger.value:
state.value += 1
return state.value
```
### 13.3 Injection Lifecycle
| Injectable | When Provided |
|------------|---------------|
| `STATE` | Created at initialization |
| `SCHEDULER` | Available from start |
| `CLOCK` | Available always |
---
## 14. Engine Configuration
### 14.1 Configuration Options
| Option | Description |
|--------|-------------|
| `start_time` | Evaluation start time |
| `end_time` | Evaluation end time (optional) |
| `run_mode` | SIMULATION or REAL_TIME |
| `trace` | Enable execution tracing |
### 14.2 run_graph Function
```python
def run_graph(
graph: GraphBuilder,
start_time: datetime = MIN_DT,
end_time: datetime | None = None,
run_mode: EvaluationMode = SIMULATION,
**kwargs
) -> dict[str, Any] | None:
"""Execute a graph."""
```
---
## 15. Thread Safety
### 15.1 Threading Model
| Component | Thread Safety |
|-----------|---------------|
| Scheduler | Single-threaded access |
| Evaluation | Single evaluation thread |
| Push sources | Multi-threaded injection |
| Internal state | Protected by evaluation model |
### 15.2 Push Source Threading
```mermaid
graph TB
subgraph "External Threads"
T1[Thread 1]
T2[Thread 2]
end
subgraph "Engine"
Q[Event Queue]
EV[Evaluation Thread]
end
T1 --> |"push"| Q
T2 --> |"push"| Q
Q --> |"drain"| EV
```
---
## 16. Implementation Notes
### 16.1 Push Source Node Processing Phase
Push source nodes have special handling in the evaluation loop that occurs BEFORE normal compute nodes:
```mermaid
sequenceDiagram
participant EQ as Event Queue
participant EN as Engine
participant PN as Push Nodes
participant CN as Compute Nodes
Note over EQ,CN: Evaluation Tick Start
EQ->>EN: Dequeue push messages
EN->>PN: Apply messages to push nodes
Note over PN: push_source_nodes_end boundary
EN->>CN: Evaluate compute nodes
Note over EQ,CN: Evaluation Tick End
```
If message application fails, messages are re-queued with backpressure signaling.
### 16.2 Field-Level Modification Tracking (TSB)
Time-series bundles support field-level modification tracking:
```python
# When a bundle field is modified:
field_output.set(new_value)
# This triggers parent chain notification:
self._parent_or_node.mark_child_modified(self, modified_time)
```
This enables:
- Downstream node scheduling when individual TSB fields change (not just the bundle)
- Efficient delta tracking for nested structures
- Parent chain notification for TSD and nested bundles
### 16.3 Node Evaluation Guard Conditions
Nodes evaluate only when ALL of the following are satisfied:
| Condition | Description |
|-----------|-------------|
| Required inputs valid | All non-optional inputs have valid values |
| Collection completeness | For `all_valid=True`, all collection elements valid |
| Trigger present | Either input modified OR scheduler triggered |
| Scheduler verification | For scheduler-using nodes, something actually triggered scheduling |
### 16.4 Node-Level Scheduling (NodeSchedulerImpl)
Each node can access its own scheduler via `_scheduler: SCHEDULER` injectable:
```python
@compute_node
def my_node(ts: TS[int], _scheduler: SCHEDULER = None) -> TS[int]:
if ts.modified:
_scheduler.schedule(MIN_TD * 5, tag="delayed") # Schedule future tick
return ts.value
if _scheduler.is_scheduled_now:
return -1 # Handle scheduled tick
```
**Features:**
- Sorted event list with optional tags
- Alarm support with callbacks
- `is_scheduled_now` property for checking current tick
- Wall clock scheduling for real-time mode
---
## 17. Reference Locations
| Component | Python Location | C++ Location |
|-----------|-----------------|--------------|
| Graph | `hgraph/_impl/_runtime/_graph.py` | `cpp/src/cpp/runtime/` |
| Node | `hgraph/_impl/_runtime/_node.py` | `cpp/src/cpp/runtime/` |
| Scheduler | `hgraph/_impl/_runtime/_scheduler.py` | `cpp/src/cpp/runtime/` |
| Evaluation Engine | `hgraph/_impl/_runtime/_evaluation_engine.py` | `cpp/src/cpp/runtime/` |
| Clock | `hgraph/_impl/_runtime/_clock.py` | `cpp/src/cpp/runtime/` |
---
## 17. Next Steps
Continue to:
- [05_TIME_SERIES_TYPES.md](05_TIME_SERIES_TYPES.md) - Time-series type details
- [06_NODE_TYPES.md](06_NODE_TYPES.md) - Node specifications
- [07_OPERATORS.md](07_OPERATORS.md) - Built-in operators