antioch-py 2.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of antioch-py might be problematic. Click here for more details.
- antioch/__init__.py +0 -0
- antioch/message.py +87 -0
- antioch/module/__init__.py +53 -0
- antioch/module/clock.py +62 -0
- antioch/module/execution.py +278 -0
- antioch/module/input.py +127 -0
- antioch/module/module.py +218 -0
- antioch/module/node.py +357 -0
- antioch/module/token.py +42 -0
- antioch/session/__init__.py +150 -0
- antioch/session/ark.py +504 -0
- antioch/session/asset.py +65 -0
- antioch/session/error.py +80 -0
- antioch/session/record.py +158 -0
- antioch/session/scene.py +1521 -0
- antioch/session/session.py +220 -0
- antioch/session/task.py +323 -0
- antioch/session/views/__init__.py +40 -0
- antioch/session/views/animation.py +189 -0
- antioch/session/views/articulation.py +245 -0
- antioch/session/views/basis_curve.py +186 -0
- antioch/session/views/camera.py +92 -0
- antioch/session/views/collision.py +75 -0
- antioch/session/views/geometry.py +74 -0
- antioch/session/views/ground_plane.py +63 -0
- antioch/session/views/imu.py +73 -0
- antioch/session/views/joint.py +64 -0
- antioch/session/views/light.py +175 -0
- antioch/session/views/pir_sensor.py +140 -0
- antioch/session/views/radar.py +73 -0
- antioch/session/views/rigid_body.py +282 -0
- antioch/session/views/xform.py +119 -0
- antioch_py-2.0.6.dist-info/METADATA +115 -0
- antioch_py-2.0.6.dist-info/RECORD +99 -0
- antioch_py-2.0.6.dist-info/WHEEL +5 -0
- antioch_py-2.0.6.dist-info/entry_points.txt +2 -0
- antioch_py-2.0.6.dist-info/top_level.txt +2 -0
- common/__init__.py +0 -0
- common/ark/__init__.py +60 -0
- common/ark/ark.py +128 -0
- common/ark/hardware.py +121 -0
- common/ark/kinematics.py +31 -0
- common/ark/module.py +85 -0
- common/ark/node.py +94 -0
- common/ark/scheduler.py +439 -0
- common/ark/sim.py +33 -0
- common/assets/__init__.py +3 -0
- common/constants.py +47 -0
- common/core/__init__.py +52 -0
- common/core/agent.py +296 -0
- common/core/auth.py +305 -0
- common/core/registry.py +331 -0
- common/core/task.py +36 -0
- common/message/__init__.py +59 -0
- common/message/annotation.py +89 -0
- common/message/array.py +500 -0
- common/message/base.py +517 -0
- common/message/camera.py +91 -0
- common/message/color.py +139 -0
- common/message/frame.py +50 -0
- common/message/image.py +171 -0
- common/message/imu.py +14 -0
- common/message/joint.py +47 -0
- common/message/log.py +31 -0
- common/message/pir.py +16 -0
- common/message/point.py +109 -0
- common/message/point_cloud.py +63 -0
- common/message/pose.py +148 -0
- common/message/quaternion.py +273 -0
- common/message/radar.py +58 -0
- common/message/types.py +37 -0
- common/message/vector.py +786 -0
- common/rome/__init__.py +9 -0
- common/rome/client.py +430 -0
- common/rome/error.py +16 -0
- common/session/__init__.py +54 -0
- common/session/environment.py +31 -0
- common/session/sim.py +240 -0
- common/session/views/__init__.py +263 -0
- common/session/views/animation.py +73 -0
- common/session/views/articulation.py +184 -0
- common/session/views/basis_curve.py +102 -0
- common/session/views/camera.py +147 -0
- common/session/views/collision.py +59 -0
- common/session/views/geometry.py +102 -0
- common/session/views/ground_plane.py +41 -0
- common/session/views/imu.py +66 -0
- common/session/views/joint.py +81 -0
- common/session/views/light.py +96 -0
- common/session/views/pir_sensor.py +115 -0
- common/session/views/radar.py +82 -0
- common/session/views/rigid_body.py +236 -0
- common/session/views/viewport.py +21 -0
- common/session/views/xform.py +39 -0
- common/utils/__init__.py +4 -0
- common/utils/comms.py +571 -0
- common/utils/logger.py +123 -0
- common/utils/time.py +42 -0
- common/utils/usd.py +12 -0
antioch/module/module.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import signal
|
|
4
|
+
import threading
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from antioch.module.execution import Execution
|
|
8
|
+
from antioch.module.node import Node
|
|
9
|
+
from common.ark.ark import Ark, Environment
|
|
10
|
+
from common.ark.module import ModuleParameter
|
|
11
|
+
from common.message import Message
|
|
12
|
+
from common.utils.comms import CommsSession
|
|
13
|
+
from common.utils.logger import Logger
|
|
14
|
+
from common.utils.time import now_us
|
|
15
|
+
|
|
16
|
+
AGENT_MODULE_READY_PATH = "_agent/module_ready"
|
|
17
|
+
AGENT_MODULE_START_PATH = "_agent/module_start"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ModuleReady(Message):
|
|
21
|
+
"""
|
|
22
|
+
Module ready message sent during handshake.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
_type = "antioch/agent/module_ready"
|
|
26
|
+
module_name: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ModuleStart(Message):
|
|
30
|
+
"""
|
|
31
|
+
Module start message received during handshake.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
_type = "antioch/agent/module_start"
|
|
35
|
+
global_start_time_us: int
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Module:
|
|
39
|
+
"""
|
|
40
|
+
Core module class for implementing Antioch modules in Python, which supports two
|
|
41
|
+
mode of operation:
|
|
42
|
+
|
|
43
|
+
> Container Mode (default): The module runs inside a Kubernetes pod managed by the
|
|
44
|
+
Antioch agent. Configuration is automatically loaded from environment variables
|
|
45
|
+
set by the agent (_MODULE_NAME, _ARK, _ENVIRONMENT, _DEBUG).
|
|
46
|
+
|
|
47
|
+
> Local Mode: The module runs standalone for testing and development. Configuration
|
|
48
|
+
must be explicitly provided via constructor parameters (module_name, ark, environment,
|
|
49
|
+
debug).
|
|
50
|
+
|
|
51
|
+
After creating a Module instance, you can access module metadata like module.ark,
|
|
52
|
+
module.parameters, module.environment, and module.debug to initialize resources
|
|
53
|
+
before registering node callbacks.
|
|
54
|
+
|
|
55
|
+
Users register node callbacks via register() and then call spin() to start execution.
|
|
56
|
+
The Ark will not start until spin() is called, which begins the handshake with the
|
|
57
|
+
agent and starts processing node callbacks.
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
# Container mode
|
|
61
|
+
module = Module()
|
|
62
|
+
module.register("node1", callback1)
|
|
63
|
+
module.register("node2", callback2)
|
|
64
|
+
module.spin()
|
|
65
|
+
|
|
66
|
+
# Local mode (for testing)
|
|
67
|
+
module = Module(
|
|
68
|
+
module_name="my_module",
|
|
69
|
+
environment=Environment.SIM,
|
|
70
|
+
ark=my_ark,
|
|
71
|
+
debug=True,
|
|
72
|
+
)
|
|
73
|
+
module.register("node1", callback1)
|
|
74
|
+
module.spin()
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
module_name: str
|
|
78
|
+
ark: Ark
|
|
79
|
+
environment: Environment
|
|
80
|
+
debug: bool
|
|
81
|
+
parameters: dict[str, "ModuleParameter"]
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
module_name: str | None = None,
|
|
86
|
+
ark: Ark | None = None,
|
|
87
|
+
environment: Environment = Environment.REAL,
|
|
88
|
+
debug: bool = False,
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
Initialize module.
|
|
92
|
+
|
|
93
|
+
:param module_name: Module name (for local mode).
|
|
94
|
+
:param ark: Ark definition (for local mode).
|
|
95
|
+
:param environment: Execution environment (for local mode).
|
|
96
|
+
:param debug: Enable debug mode for loggers (for local mode).
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
# Parse module configuration
|
|
100
|
+
self._is_local_mode = module_name is not None
|
|
101
|
+
if not self._is_local_mode:
|
|
102
|
+
module_name = os.environ.get("_MODULE_NAME")
|
|
103
|
+
ark = Ark.model_validate(json.loads(os.environ["_ARK"])) if "_ARK" in os.environ else None
|
|
104
|
+
environment = Environment(os.environ.get("_ENVIRONMENT", "real"))
|
|
105
|
+
debug = os.environ.get("_DEBUG", "false").lower() == "true"
|
|
106
|
+
|
|
107
|
+
# Validate required configuration
|
|
108
|
+
if module_name is None:
|
|
109
|
+
raise ValueError("Module is missing module name")
|
|
110
|
+
if ark is None:
|
|
111
|
+
raise ValueError("Module is missing ark configuration")
|
|
112
|
+
|
|
113
|
+
# Extract module config from Ark
|
|
114
|
+
module_config = next((m for m in ark.modules if m.name == module_name), None)
|
|
115
|
+
if module_config is None:
|
|
116
|
+
raise ValueError(f"Module '{module_name}' not found in Ark")
|
|
117
|
+
|
|
118
|
+
self.module_name = module_name
|
|
119
|
+
self.ark = ark
|
|
120
|
+
self.environment = environment
|
|
121
|
+
self.debug = debug
|
|
122
|
+
self.parameters = module_config.parameters
|
|
123
|
+
|
|
124
|
+
self._module_config = module_config
|
|
125
|
+
self._shutdown_event = threading.Event()
|
|
126
|
+
self._node_callbacks: dict[str, Callable[[Execution], None]] = {}
|
|
127
|
+
self._nodes: dict[str, Node] = {}
|
|
128
|
+
|
|
129
|
+
def register(self, name: str, callback: Callable[[Execution], None]) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Register a node callback.
|
|
132
|
+
|
|
133
|
+
:param name: Node name.
|
|
134
|
+
:param callback: User callback function invoked on each node execution.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
self._node_callbacks[name] = callback
|
|
138
|
+
|
|
139
|
+
def spin(self) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Initialize module and wait for shutdown signal.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
comms = CommsSession()
|
|
145
|
+
logger = Logger(comms, self.module_name, debug=self.debug, print_logs=True)
|
|
146
|
+
global_start_time_us = now_us() if self._is_local_mode else self._perform_agent_handshake(comms)
|
|
147
|
+
|
|
148
|
+
# Validate node callbacks
|
|
149
|
+
for node_name in self._node_callbacks:
|
|
150
|
+
if node_name not in self._module_config.nodes:
|
|
151
|
+
raise ValueError(f"Node '{node_name}' not in module config")
|
|
152
|
+
for node_name in self._module_config.nodes:
|
|
153
|
+
if node_name not in self._node_callbacks:
|
|
154
|
+
raise ValueError(f"Missing callback for node '{node_name}'")
|
|
155
|
+
|
|
156
|
+
# Start node threads
|
|
157
|
+
for name, callback in self._node_callbacks.items():
|
|
158
|
+
self._nodes[name] = Node(
|
|
159
|
+
module_name=self.module_name,
|
|
160
|
+
node_name=name,
|
|
161
|
+
module_config=self._module_config,
|
|
162
|
+
node_config=self._module_config.nodes[name],
|
|
163
|
+
ark_edges=self.ark.edges,
|
|
164
|
+
ark_modules=self.ark.modules,
|
|
165
|
+
global_start_time_us=global_start_time_us,
|
|
166
|
+
environment=self.environment,
|
|
167
|
+
debug=self.debug,
|
|
168
|
+
callback=callback,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Wait for shutdown signal
|
|
172
|
+
signal.signal(signal.SIGTERM, self._handle_shutdown)
|
|
173
|
+
signal.signal(signal.SIGINT, self._handle_shutdown)
|
|
174
|
+
self._shutdown_event.wait()
|
|
175
|
+
|
|
176
|
+
logger.info("Module exiting")
|
|
177
|
+
comms.close()
|
|
178
|
+
|
|
179
|
+
def join(self, timeout: float | None = None) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Wait for all node threads to finish.
|
|
182
|
+
|
|
183
|
+
Blocks until all node threads have completed execution or until
|
|
184
|
+
the timeout is reached. Useful for ensuring all nodes have finished
|
|
185
|
+
processing before the module exits.
|
|
186
|
+
|
|
187
|
+
:param timeout: Maximum time to wait per node in seconds (None for infinite).
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
for node in self._nodes.values():
|
|
191
|
+
node.join(timeout=timeout)
|
|
192
|
+
|
|
193
|
+
def _perform_agent_handshake(self, comms: CommsSession) -> int:
|
|
194
|
+
"""
|
|
195
|
+
Perform handshake with agent to receive global start time.
|
|
196
|
+
|
|
197
|
+
Publishes ready message and blocks waiting for start message containing
|
|
198
|
+
the global start time for synchronization.
|
|
199
|
+
|
|
200
|
+
:param comms: Comms session to use for communication.
|
|
201
|
+
:return: Global start time in microseconds.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
ready_publisher = comms.declare_publisher(AGENT_MODULE_READY_PATH)
|
|
205
|
+
start_subscriber = comms.declare_subscriber(AGENT_MODULE_START_PATH)
|
|
206
|
+
ready_publisher.publish(ModuleReady(module_name=self.module_name))
|
|
207
|
+
start_msg = start_subscriber.recv(ModuleStart)
|
|
208
|
+
return start_msg.global_start_time_us
|
|
209
|
+
|
|
210
|
+
def _handle_shutdown(self, _signum, _frame) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Handle shutdown signal by stopping all node threads and
|
|
213
|
+
exiting the module.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
for node in self._nodes.values():
|
|
217
|
+
node.stop()
|
|
218
|
+
self._shutdown_event.set()
|
antioch/module/node.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from threading import Event, Thread
|
|
4
|
+
|
|
5
|
+
from antioch.module.clock import Clock
|
|
6
|
+
from antioch.module.execution import Execution
|
|
7
|
+
from antioch.module.input import NodeInputBuffer
|
|
8
|
+
from antioch.module.token import Token, TokenType
|
|
9
|
+
from common.ark.ark import Environment
|
|
10
|
+
from common.ark.module import Module
|
|
11
|
+
from common.ark.node import Node as NodeConfig
|
|
12
|
+
from common.ark.scheduler import InputToken, NodeEdge, NodeStartEvent, OnlineScheduler
|
|
13
|
+
from common.ark.sim import SimNodeComplete, SimNodeStart
|
|
14
|
+
from common.utils.comms import CommsPublisher, CommsSession
|
|
15
|
+
from common.utils.logger import Logger
|
|
16
|
+
from common.utils.time import now_us
|
|
17
|
+
|
|
18
|
+
TOKEN_OUTPUT_PATH = "_token/{path}"
|
|
19
|
+
NODE_START_SUBSCRIBER_PATH = "_ark/node_start/{module}/{node}"
|
|
20
|
+
NODE_COMPLETE_PUBLISHER_PATH = "_ark/node_complete/{module}/{node}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Node:
|
|
24
|
+
"""
|
|
25
|
+
Independent node execution thread.
|
|
26
|
+
|
|
27
|
+
Each node runs in its own thread, walks the deterministic schedule independently,
|
|
28
|
+
and executes user callbacks at the appropriate logical times.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
module_name: str,
|
|
34
|
+
node_name: str,
|
|
35
|
+
module_config: Module,
|
|
36
|
+
node_config: NodeConfig,
|
|
37
|
+
ark_edges: list[NodeEdge],
|
|
38
|
+
ark_modules: list[Module],
|
|
39
|
+
global_start_time_us: int,
|
|
40
|
+
environment: Environment,
|
|
41
|
+
debug: bool,
|
|
42
|
+
callback: Callable[[Execution], None],
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize node and start thread immediately.
|
|
46
|
+
|
|
47
|
+
:param module_name: Module containing this node.
|
|
48
|
+
:param node_name: Name of this node.
|
|
49
|
+
:param module_config: Module configuration from Ark.
|
|
50
|
+
:param node_config: Node configuration.
|
|
51
|
+
:param ark_edges: All edges in the Ark.
|
|
52
|
+
:param ark_modules: All modules in the Ark.
|
|
53
|
+
:param global_start_time_us: Global start time (only used in real mode).
|
|
54
|
+
:param environment: Execution environment (SIM or REAL).
|
|
55
|
+
:param debug: Enable debug mode for loggers.
|
|
56
|
+
:param callback: User callback function to execute.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
self._module_name = module_name
|
|
60
|
+
self._node_name = node_name
|
|
61
|
+
self._module_config = module_config
|
|
62
|
+
self._node_config = node_config
|
|
63
|
+
self._ark_edges = ark_edges
|
|
64
|
+
self._ark_modules = ark_modules
|
|
65
|
+
self._global_start_time_us = global_start_time_us
|
|
66
|
+
self._environment = environment
|
|
67
|
+
self._callback = callback
|
|
68
|
+
|
|
69
|
+
self._comms = CommsSession()
|
|
70
|
+
self._logger = Logger(self._comms, module_name, debug=debug, print_logs=True)
|
|
71
|
+
self._execution_logger = Logger(self._comms, module_name, debug=debug, print_logs=True)
|
|
72
|
+
self._shutdown_requested = Event()
|
|
73
|
+
|
|
74
|
+
# Create input buffers for all inputs
|
|
75
|
+
self._input_buffers: dict[str, NodeInputBuffer] = {}
|
|
76
|
+
for input_name, input_config in node_config.inputs.items():
|
|
77
|
+
self._input_buffers[input_name] = NodeInputBuffer(input_name, input_config, self._comms)
|
|
78
|
+
|
|
79
|
+
# Create output publishers for all outputs
|
|
80
|
+
self._output_publishers: dict[str, CommsPublisher] = {}
|
|
81
|
+
for output_name, output_config in node_config.outputs.items():
|
|
82
|
+
path = TOKEN_OUTPUT_PATH.format(path=output_config.path)
|
|
83
|
+
self._output_publishers[output_name] = self._comms.declare_publisher(path)
|
|
84
|
+
|
|
85
|
+
# Create reusable execution context
|
|
86
|
+
self._execution = Execution(
|
|
87
|
+
name=node_config.name,
|
|
88
|
+
budget_us=node_config.budget_us,
|
|
89
|
+
parameters=module_config.parameters,
|
|
90
|
+
outputs=node_config.outputs,
|
|
91
|
+
logger=self._execution_logger,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Start thread immediately
|
|
95
|
+
thread_target = self._run_sim if self._environment == Environment.SIM else self._run_real
|
|
96
|
+
self._thread = Thread(target=thread_target, daemon=True)
|
|
97
|
+
self._thread.start()
|
|
98
|
+
|
|
99
|
+
def stop(self) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Stop the node and clean up resources.
|
|
102
|
+
|
|
103
|
+
Sets the shutdown event, causing the node's execution loop to exit,
|
|
104
|
+
and cleans up all communication resources.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
self._logger.info("Stopping node")
|
|
108
|
+
self._shutdown_requested.set()
|
|
109
|
+
|
|
110
|
+
# Close all input buffers
|
|
111
|
+
for input_buffer in self._input_buffers.values():
|
|
112
|
+
input_buffer.close()
|
|
113
|
+
|
|
114
|
+
# Close all output publishers
|
|
115
|
+
for publisher in self._output_publishers.values():
|
|
116
|
+
publisher.close()
|
|
117
|
+
|
|
118
|
+
# Close comms session
|
|
119
|
+
self._comms.close()
|
|
120
|
+
|
|
121
|
+
def join(self, timeout: float | None = None) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Wait for node thread to finish.
|
|
124
|
+
|
|
125
|
+
:param timeout: Maximum time to wait in seconds (None for infinite).
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
self._thread.join(timeout=timeout)
|
|
129
|
+
|
|
130
|
+
def _run_sim(self) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Sim mode execution loop.
|
|
133
|
+
|
|
134
|
+
Waits for SimNodeStart messages via async subscriber with timeout checking
|
|
135
|
+
for shutdown events.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
start_path = NODE_START_SUBSCRIBER_PATH.format(module=self._module_name, node=self._node_name)
|
|
139
|
+
complete_path = NODE_COMPLETE_PUBLISHER_PATH.format(module=self._module_name, node=self._node_name)
|
|
140
|
+
start_subscriber = self._comms.declare_async_subscriber(start_path)
|
|
141
|
+
complete_publisher = self._comms.declare_publisher(complete_path)
|
|
142
|
+
|
|
143
|
+
while not self._shutdown_requested.is_set():
|
|
144
|
+
# Wait for node start event with shutdown check
|
|
145
|
+
start = start_subscriber.recv_timeout(SimNodeStart, timeout=0.1)
|
|
146
|
+
if start is None:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
# Update logger times
|
|
150
|
+
self._logger.set_let(start.start_let_us)
|
|
151
|
+
self._execution.logger.set_let(start.start_let_us)
|
|
152
|
+
|
|
153
|
+
# Gather and filter input tokens
|
|
154
|
+
input_data = self._gather_inputs(start.start_let_us, start.input_tokens)
|
|
155
|
+
if input_data is None:
|
|
156
|
+
self._logger.warning(f"Skipping node {self._node_name} at {start.start_let_us} because of missing inputs")
|
|
157
|
+
self._publish_skipped_tokens(start.start_let_us, start.start_timestamp_us)
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
# Start execution with runtime data
|
|
161
|
+
self._execution.start(
|
|
162
|
+
let_us=start.start_let_us,
|
|
163
|
+
input_data=input_data,
|
|
164
|
+
hardware_reads=start.hardware_reads,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Execute user callback
|
|
168
|
+
try:
|
|
169
|
+
self._callback(self._execution)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
self._publish_error_tokens(start.start_let_us, str(e), self._execution._start_timestamp_us)
|
|
172
|
+
self._logger.error(f"Execution failed: {e}\n{traceback.format_exc()}")
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Check for budget overrun
|
|
176
|
+
if self._execution.elapsed_us > self._node_config.budget_us:
|
|
177
|
+
self._logger.error(f"Overrun: {self._execution.elapsed_us}us > {self._node_config.budget_us}us at {start.start_let_us}us")
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# Publish output tokens
|
|
181
|
+
for output_name, publisher in self._output_publishers.items():
|
|
182
|
+
payload = self._execution._output_data.get(output_name)
|
|
183
|
+
publisher.publish(
|
|
184
|
+
Token(
|
|
185
|
+
module_name=self._module_name,
|
|
186
|
+
node_name=self._node_name,
|
|
187
|
+
output_name=output_name,
|
|
188
|
+
let_us=start.start_let_us,
|
|
189
|
+
start_timestamp_us=self._execution._start_timestamp_us,
|
|
190
|
+
budget_us=self._node_config.budget_us,
|
|
191
|
+
status=TokenType.DATA if payload is not None else TokenType.SHORT_CIRCUIT,
|
|
192
|
+
payload=payload,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Publish completion (only if no overrun or error)
|
|
197
|
+
complete_publisher.publish(
|
|
198
|
+
SimNodeComplete(
|
|
199
|
+
module_name=self._module_name,
|
|
200
|
+
node_name=self._node_name,
|
|
201
|
+
completion_let_us=start.start_let_us + self._node_config.budget_us,
|
|
202
|
+
hardware_writes=self._execution._hardware_writes if self._execution._hardware_writes else None,
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _run_real(self) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Real mode execution loop.
|
|
209
|
+
|
|
210
|
+
Sleeps until event times using clock-based synchronization.
|
|
211
|
+
Creates scheduler locally since it's only needed in real mode.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
scheduler = OnlineScheduler(self._ark_edges, self._ark_modules)
|
|
215
|
+
clock = Clock(self._global_start_time_us, event=self._shutdown_requested)
|
|
216
|
+
|
|
217
|
+
while not self._shutdown_requested.is_set():
|
|
218
|
+
# Skip to next event for this node
|
|
219
|
+
event = scheduler.next()
|
|
220
|
+
if (
|
|
221
|
+
not isinstance(event, NodeStartEvent)
|
|
222
|
+
or (event.module, event.node) != (self._module_name, self._node_name)
|
|
223
|
+
or clock.let_us > event.start_let_us
|
|
224
|
+
):
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
# Sleep until event time with shutdown check
|
|
228
|
+
if not clock.wait_until(event.start_let_us):
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
# Update logger times
|
|
232
|
+
self._logger.set_let(event.start_let_us)
|
|
233
|
+
self._execution_logger.set_let(event.start_let_us)
|
|
234
|
+
|
|
235
|
+
# Gather and filter input tokens
|
|
236
|
+
input_data = self._gather_inputs(event.start_let_us, event.input_tokens)
|
|
237
|
+
if input_data is None:
|
|
238
|
+
self._logger.warning(f"Skipping node {self._node_name} at {event.start_let_us} because of missing inputs")
|
|
239
|
+
self._publish_skipped_tokens(event.start_let_us, now_us())
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
# Start execution with runtime data (no hardware reads in real mode)
|
|
243
|
+
self._execution.start(
|
|
244
|
+
let_us=event.start_let_us,
|
|
245
|
+
input_data=input_data,
|
|
246
|
+
hardware_reads={},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Execute user callback
|
|
250
|
+
try:
|
|
251
|
+
self._callback(self._execution)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
self._publish_error_tokens(event.start_let_us, str(e), self._execution._start_timestamp_us)
|
|
254
|
+
self._logger.error(f"Execution failed: {e}\n{traceback.format_exc()}")
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
# Check for budget overrun
|
|
258
|
+
if self._execution.elapsed_us > self._node_config.budget_us:
|
|
259
|
+
self._logger.error(f"Overrun: {self._execution.elapsed_us}us > {self._node_config.budget_us}us at {event.start_let_us}us")
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
# Publish output tokens
|
|
263
|
+
for output_name, publisher in self._output_publishers.items():
|
|
264
|
+
payload = self._execution._output_data.get(output_name)
|
|
265
|
+
publisher.publish(
|
|
266
|
+
Token(
|
|
267
|
+
module_name=self._module_name,
|
|
268
|
+
node_name=self._node_name,
|
|
269
|
+
output_name=output_name,
|
|
270
|
+
let_us=event.start_let_us,
|
|
271
|
+
start_timestamp_us=self._execution._start_timestamp_us,
|
|
272
|
+
budget_us=self._node_config.budget_us,
|
|
273
|
+
status=TokenType.DATA if payload is not None else TokenType.SHORT_CIRCUIT,
|
|
274
|
+
payload=payload,
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def _gather_inputs(self, let_us: int, expected_tokens: list[InputToken]) -> dict[str, list[Token]] | None:
|
|
279
|
+
"""
|
|
280
|
+
Gather input tokens and apply buffer policies.
|
|
281
|
+
|
|
282
|
+
Detects missing tokens (overruns) and adds them to buffers before execution.
|
|
283
|
+
|
|
284
|
+
:param let_us: Current logical execution time.
|
|
285
|
+
:param expected_tokens: List of InputToken objects expected for this execution.
|
|
286
|
+
:return: Dictionary of filtered tokens by input name, or None if required input missing.
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
# Add missing tokens as overruns to buffers
|
|
290
|
+
for tok in expected_tokens:
|
|
291
|
+
input_buffer = self._input_buffers[tok.target_input_name]
|
|
292
|
+
if input_buffer.get_token(tok.let_us, tok.source_module, tok.source_node) is None:
|
|
293
|
+
input_buffer.add_token(
|
|
294
|
+
Token(
|
|
295
|
+
module_name=tok.source_module,
|
|
296
|
+
node_name=tok.source_node,
|
|
297
|
+
output_name=tok.source_output_name,
|
|
298
|
+
let_us=tok.let_us,
|
|
299
|
+
budget_us=tok.budget_us,
|
|
300
|
+
start_timestamp_us=tok.let_us,
|
|
301
|
+
status=TokenType.OVERRUN,
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Prepare execution by applying buffer policies
|
|
306
|
+
input_data = {}
|
|
307
|
+
for input_name, input_buffer in self._input_buffers.items():
|
|
308
|
+
filtered_tokens = input_buffer.prepare_execution(let_us)
|
|
309
|
+
if filtered_tokens is None:
|
|
310
|
+
return None
|
|
311
|
+
input_data[input_name] = filtered_tokens
|
|
312
|
+
|
|
313
|
+
return input_data
|
|
314
|
+
|
|
315
|
+
def _publish_skipped_tokens(self, let_us: int, start_timestamp_us: int) -> None:
|
|
316
|
+
"""
|
|
317
|
+
Publish skipped tokens for all outputs.
|
|
318
|
+
|
|
319
|
+
:param let_us: Logical execution time.
|
|
320
|
+
:param start_timestamp_us: Start timestamp for token.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
for output_name, publisher in self._output_publishers.items():
|
|
324
|
+
publisher.publish(
|
|
325
|
+
Token(
|
|
326
|
+
module_name=self._module_name,
|
|
327
|
+
node_name=self._node_name,
|
|
328
|
+
output_name=output_name,
|
|
329
|
+
let_us=let_us,
|
|
330
|
+
start_timestamp_us=start_timestamp_us,
|
|
331
|
+
budget_us=self._node_config.budget_us,
|
|
332
|
+
status=TokenType.SKIPPED,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def _publish_error_tokens(self, let_us: int, error: str, start_timestamp_us: int) -> None:
|
|
337
|
+
"""
|
|
338
|
+
Publish error tokens for all outputs.
|
|
339
|
+
|
|
340
|
+
:param let_us: Logical execution time.
|
|
341
|
+
:param error: Error message.
|
|
342
|
+
:param start_timestamp_us: Start timestamp for token.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
for output_name, publisher in self._output_publishers.items():
|
|
346
|
+
publisher.publish(
|
|
347
|
+
Token(
|
|
348
|
+
module_name=self._module_name,
|
|
349
|
+
node_name=self._node_name,
|
|
350
|
+
output_name=output_name,
|
|
351
|
+
let_us=let_us,
|
|
352
|
+
start_timestamp_us=start_timestamp_us,
|
|
353
|
+
budget_us=self._node_config.budget_us,
|
|
354
|
+
status=TokenType.ERROR,
|
|
355
|
+
error=error,
|
|
356
|
+
)
|
|
357
|
+
)
|
antioch/module/token.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from common.message import Message
|
|
4
|
+
from common.utils.time import now_us
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TokenType(str, Enum):
|
|
8
|
+
"""
|
|
9
|
+
Token status types.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
DATA = "data"
|
|
13
|
+
SHORT_CIRCUIT = "short_circuit"
|
|
14
|
+
ERROR = "error"
|
|
15
|
+
OVERRUN = "overrun"
|
|
16
|
+
SKIPPED = "skipped"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Token(Message):
|
|
20
|
+
"""
|
|
21
|
+
Token representing data flow between nodes.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_type = "antioch/token"
|
|
25
|
+
module_name: str
|
|
26
|
+
node_name: str
|
|
27
|
+
output_name: str
|
|
28
|
+
let_us: int
|
|
29
|
+
budget_us: int
|
|
30
|
+
start_timestamp_us: int
|
|
31
|
+
status: TokenType
|
|
32
|
+
payload: bytes | None = None
|
|
33
|
+
error: str | None = None
|
|
34
|
+
|
|
35
|
+
def elapsed(self) -> int:
|
|
36
|
+
"""
|
|
37
|
+
Get elapsed time in microseconds since token creation.
|
|
38
|
+
|
|
39
|
+
:return: Elapsed time in microseconds.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
return now_us() - self.start_timestamp_us
|