Components, registry, and peers¶
from nexus_uav import Component, Stage, Tick, Signal, Registry, register, Peer
One contract serves every role: a component subclasses Component,
states its per-tick work as a list of Stage, each declaring the Signals it reads and writes, and
overrides the lifecycle members it has. The loop runs each stage over the shared Tick. A
registry maps each schema a vehicle applies to the class that
builds it, and a peer is a process the run starts and stops on the
Peer contract. Bringing your own component walks through all four.
Component
¶
The contract every component fills, whatever its role.
A component states its per-tick work in :meth:stages, each stage declaring the signals it reads and
writes, which the loop wires before any stage runs. The members below are the whole of what the loop,
the builder and Sim read from a component, so they find none by name: a class overrides the ones it
has and inherits the rest. The role a component fills, which the schema that declares it states, fixes
where its stages run in the tick and who declares it; the contract is the same for every role.
| ATTRIBUTE | DESCRIPTION |
|---|---|
name |
The instance name, which ends the component's path,
TYPE:
|
requires |
The names of the peers the class needs, such as
TYPE:
|
needs_instance |
Whether the class takes the run's instance, the number the run's peers number their
ports from, as the keyword
TYPE:
|
attached |
Whether the component's peer has dialed in.
TYPE:
|
stages
¶
stages() -> list[Stage]
The component's per-tick work, in ring order: one or more stages, each declaring what it reads and writes. A device stage joins the captured graph; a host stage runs between replays. None by default, which fails the build: every component states at least one.
Source code in nexus_uav/_src/core/interfaces.py
95 96 97 98 99 100 | |
connect
¶
connect(links: Mapping[str, Any]) -> None
Open what the component needs for the run: bind a port, load a model. Nothing by default. The loop connects a component before the start, or after the graph capture when it waits on a peer.
| PARAMETER | DESCRIPTION |
|---|---|
links
|
The link of each peer the class requires, by the peer's name, as
TYPE:
|
Source code in nexus_uav/_src/core/interfaces.py
102 103 104 105 106 107 108 109 | |
close
¶
close() -> None
Release what :meth:connect opened, at the run's end. Nothing by default. Idempotent.
Source code in nexus_uav/_src/core/interfaces.py
111 112 | |
set_logger
¶
set_logger(logger) -> None
Take the Logger scoped to the component's path, or None when the run records nothing. A component
that logs a value with no fixed width live, a camera's frame or a guidance's markers, keeps it and logs
only its own row's name; every fixed-width signal the Recorder keeps itself. Nothing by default.
Source code in nexus_uav/_src/core/interfaces.py
114 115 116 117 118 | |
artifacts
¶
artifacts() -> dict
The files the component leaves for a reader after the run, by name, which Sim.artifacts collects
beside the recording. Empty by default.
Source code in nexus_uav/_src/core/interfaces.py
120 121 122 123 124 | |
ports
¶
ports() -> Mapping[str, dict]
The links the component knows of that leave the run, by name, each to the address a script opens its
client on, which the run's port map, Sim.ports, lists. Empty by default.
Source code in nexus_uav/_src/core/interfaces.py
126 127 128 129 130 | |
Stage
dataclass
¶
Stage(
name: str,
kind: Literal["device", "host"],
run: Callable[[Tick], Any],
reads: tuple[Signal, ...] = (),
writes: tuple[Signal, ...] = (),
)
One unit of a component's per-tick work, the shape every loop component states in its
stages(). A device stage joins the CUDA graph; a host stage runs between graph replays.
run(tick) does the work; a host stage returns False when it produced nothing because its
peer didn't answer, which a peer's preroll ticks on through and the steady loop ends the run on. Every
stage but the clock's, the physics substeps' and a sensor's host stage also runs once in the start, over
the settled state at sim time zero, before any capture. reads and writes are the signals the stage
reads and writes, which the loop wires before any stage runs.
Tick
dataclass
¶
Tick(
state: Any,
t: SimTime,
dt: float,
timeout: float | None = None,
base: int = 0,
done: bool = False,
control: Any = None,
)
The context one control tick hands every stage: the loop's own state, which a stage reads and
writes in place. A value between two components is a signal the stages declare, not a field here, a
sensor's sample among them. dt is one physics step, the control timestep over
physics_substeps. timeout bounds a host stage's wait on its peer. base is the index of the
vehicle's base body, its airframe, in state: a stage that reads the vehicle's true state reads that
row. control is Newton's control the physics steps with, whose inputs a command element writes.
A stage sets done to end the run, a guidance's when its mission is over: the loop completes the
tick and takes no further one.
Signal
¶
Signal(
name: str,
type: type,
*,
shape: tuple[int, ...] | None = None,
default: Any = None,
)
One value a stage reads or writes: a name, a type and, for a device type, a shape.
| PARAMETER | DESCRIPTION |
|---|---|
name
|
The name that wires a reader to its writer.
TYPE:
|
type
|
A Warp struct or value type, whose buffer lives on the device, or any other class, whose
buffer holds one object on the host. A reader that takes every writer of the signal declares
TYPE:
|
shape
|
The device buffer's shape. A reader reads the leading part of a wider buffer.
TYPE:
|
default
|
The value a reader takes until a component writes the signal, and for good when no
component writes it.
TYPE:
|
Source code in nexus_uav/_src/core/signals.py
61 62 63 64 65 66 67 | |
buffer
instance-attribute
¶
buffer: Any = None
The one buffer the builder hands the writer and every reader, before the capture; for a list input, the buffer of each writer.
writers
instance-attribute
¶
writers: tuple[str, ...] = ()
For a list input, the instance name of each writer whose buffer it holds, in the same order, which the builder sets when it wires the ring, so a reader of two or more writers has each one's name; empty otherwise.
device
property
¶
device: bool
Whether the signal's buffer lives on the device: its type is a Warp struct or value type.
read
¶
read() -> Any
The signal's value on the host, typed by the signal: a device buffer without its leading axis of one,
as an instance of its struct, read().position, or as an array of its other axes, and the object a host
buffer holds. A device value is a copy, which later writes leave alone. A list input reads one value per
writer, or its default when no component writes the signal.
Source code in nexus_uav/_src/core/signals.py
74 75 76 77 78 79 80 81 82 | |
write
¶
write(value: Any) -> None
Write the signal's value from the host, in place: a device buffer takes a copy of value, an
array of the signal's shape, so the next graph replay reads it, and a host buffer holds value.
A struct's value is a record per element, its fields in order, such as [(time, accel, gyro)].
Source code in nexus_uav/_src/core/signals.py
84 85 86 87 88 89 90 91 92 93 94 | |
Registry
¶
Registry(entries: dict[str, type | str] | None = None)
The classes a run resolves its names to: this registry's own rows, then the install's.
| PARAMETER | DESCRIPTION |
|---|---|
entries
|
The rows of this registry: each name to a class or an import path,
TYPE:
|
Source code in nexus_uav/_src/core/registry.py
29 30 | |
add
¶
add(name: str, target: type | str) -> None
Map name to target, a class or an import path, module:Class, replacing any row it had.
Source code in nexus_uav/_src/core/registry.py
32 33 34 | |
resolve
¶
resolve(name: str) -> type | None
The class name maps to: this registry's row, else the install's, or None when neither claims it.
An import path, or the install's entry point, imports its module here, not before, so a class with heavy imports costs nothing until a run declares its schema.
Source code in nexus_uav/_src/core/registry.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
register
¶
register(name: str, target: type | str) -> None
Map name to target, a class or an import path, module:Class, in the install's table, for a class a
script defines rather than installs.
Every registry that maps name itself keeps its own row.
Source code in nexus_uav/_src/core/registry.py
62 63 64 65 66 67 68 | |
Peer
¶
Bases: Protocol
A process the run starts, stops and can ask after.
start blocks until the process is running, or raises naming why it couldn't start. stop
is idempotent: a peer that never started, or stopped already, stays stopped. alive says
whether the process still runs; a peer that exited on its own reads dead, and a run waiting on
it fails at once. A peer that writes files a reader wants after the run, a console log, names
them in artifacts, which is optional. A peer that can tell its own error lines from the rest
of its console, such as the line PX4 prints for an airframe its tree lacks, returns them from
errors, which is optional too, and a run that fails quotes them in its RunError.
clock states how the peer keeps time, one of three ways. stepped: a component steps it each
tick, as PX4 SITL runs on the sim time the lockstep link carries. on_request: it answers a request
for a given sim time, as the Kit peer renders the frame a tick asks for. free: it runs on its own
wall clock, as a companion computer under an emulator does. Sim time and a free-running peer's clock
agree only while the run paces at a real-time factor of 1, so the build fails a run that declares one
at any other factor, before any peer starts, and the loop warns when the live factor falls below 0.95.
A peer's schema can declare a serial line, a relationship nexus:serial:<line> whose target is the prim of
the peer at the line's other end, as a companion computer's line to the flight controller. The run owns the
link, :mod:.serial: it makes one per line before any peer starts and hands each of the two peers the path of
its end in the keyword serial, the declaring peer by the line's name and the other by the declaring prim's
name. A peer with an end takes serial beside the run's other values and opens the path it names.
A peer's schema can declare an input, a relationship nexus:inputs:<signal> whose targets are the prims
whose components write that signal, one or more, as a camera's USB cable plugs into a companion computer. The
loop end of that link is the run's: once per tick, after the writers, it hands the peer each writer's value
through take(signal, writer, value), with the writer's instance name, which a peer with an input states.
The peer decides what it makes of a value and how it carries it. The loop hands a host value once, on the
write that produced it, so a frame reaches the peer at the camera's rate, and a device value every tick,
since a kernel writes it every tick. A take that raises fails the run, and
the error names the peer.
ContainerPeer
¶
ContainerPeer(*, instance: int, log_dir: str | PathLike)
Bases: Peer
A peer whose process is one container, on the Peer contract: the PX4 SITL and Kit peers subclass it,
and so does a project's.
A subclass states name, the peer's kind, and run_options, what its container runs. The class does the
rest. start removes a leftover container of this peer's name, runs the container under that name with the
label nexus.peer=<name>, and streams its console into console.log under the peer's folder. stop
removes the container, running or exited, and is idempotent. alive reads from the daemon whether the
container still runs.
The run hands every peer its folder, and the peer's files live under <log_dir>/<name>/, dir: its
console at log_path, console.log there, and whatever else it writes. The container's name,
container, is nexus-<name>-<pid>-<instance>, so two runs in one process keep both containers.
| PARAMETER | DESCRIPTION |
|---|---|
instance
|
The run's instance, which names the container.
TYPE:
|
log_dir
|
The run's folder, which holds the peer's folder.
TYPE:
|
Example
A project's peer that runs the image built from a folder it ships:
class Radio(ContainerPeer):
name = "radio"
def __init__(self, *, instance, log_dir, **run):
super().__init__(instance=instance, log_dir=log_dir)
def run_options(self):
return {"image": ensure_image(IMAGE_DIR, "radio"), "command": ["radio", "--serve"]}
Source code in nexus_uav/_src/peers/containers.py
231 232 233 234 235 236 | |
name
class-attribute
instance-attribute
¶
name: str = ''
The peer's kind: the value of its label, the stem of its container's name and the name of its folder.
run_options
¶
run_options() -> dict[str, Any]
What the container runs: image, command and the other keywords of the docker SDK's
containers.run, such as volumes, environment or labels. Called by start, so an image a
peer builds or pulls at start resolves here.
Source code in nexus_uav/_src/peers/containers.py
238 239 240 241 242 243 | |
start
¶
start() -> None
Run the container, after removing a leftover of its name; its console streams to log_path.
| RAISES | DESCRIPTION |
|---|---|
RuntimeError
|
The daemon is unreachable; see :func: |
DockerException
|
The daemon refused the run. |
Source code in nexus_uav/_src/peers/containers.py
245 246 247 248 249 250 251 252 253 254 255 | |
stop
¶
stop() -> None
Remove the container this peer started, running or exited. Idempotent.
Source code in nexus_uav/_src/peers/containers.py
257 258 259 260 261 262 | |
alive
¶
alive() -> bool
Whether the container still runs; one that exited, or that the daemon no longer holds, reads dead.
Source code in nexus_uav/_src/peers/containers.py
264 265 266 267 268 269 270 271 272 273 274 | |
ensure_image
¶
ensure_image(root: Path, name: str) -> str
Build the image from root when this machine lacks its tag, and return the tag.
The build prints its own output as it goes.
| RAISES | DESCRIPTION |
|---|---|
RuntimeError
|
The build failed, with the build's own error, or the daemon is unreachable. |
Source code in nexus_uav/_src/peers/containers.py
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |