Ports and connections¶
A scene becomes a system when its parts exchange values. LunCoSim expresses that exchange as typed USD properties and standard USD connections. The same authored wire feeds co-simulation, telemetry, the Ports panel, and the HTTP API; there is no second signal graph.
The three property namespaces¶
| Namespace | Direction | Meaning |
|---|---|---|
inputs:<name> |
Into the prim | A value the component consumes |
outputs:<name> |
Out of the prim | A value the component publishes |
connectors:<name> |
Acausal | A Modelica connector exposed on the component boundary |
A scalar port is a typed USD attribute. The type is part of the contract:
def Xform "Rover" (kind = "assembly")
{
float inputs:throttle = 0.0
float inputs:steer = 0.0
float outputs:speed = 0.0
double outputs:odometer = 0.0
}
inputs: and outputs: are causal: the producer writes, the consumer reads.
connectors: describe an acausal Modelica interface, where the equation system
decides the direction. A connector property without a connection is reported
as an orphan warning; it is not silently treated as a causal port.
Authoring a connection¶
A connection is authored on the property that consumes the value:
def Xform "DescentLander" (
prepend apiSchemas = ["PhysicsRigidBodyAPI", "LunCoProgramAPI"]
)
{
uniform token info:implementationSource = "sourceAsset"
uniform asset info:sourceAsset = @lunco://models/Lander.mo@
uniform bool lunco:program:realtimeSafe = true
float inputs:external_throttle = 0.0
float outputs:throttle
def Scope "MainPropulsion"
{
float inputs:valve_opening.connect = </DescentLander.outputs:throttle>
}
}
The target path is the source. In the composed stage the connection
resolves to a SimConnection with a source prim and property, a sink prim and
property, and a scalar type. The runtime compiles that graph into dense target
indices and propagates values at the fixed step.
A connection is not a value copy. It is a topology fact that survives recomposition. If the source is renamed or removed, the wire is reported as invalid rather than silently reading zero.
The public-output rule¶
A model output becomes externally connectable only when the component prim
declares the matching outputs:<name> property. A path mentioned only as a
connection target does not create a public port.
This rule prevents a connection from reaching into a model's private state. The model may compute hundreds of local variables; only the declared outputs are part of its interface.
def Xform "ReactionWheel" (
prepend apiSchemas = ["LunCoProgramAPI"]
)
{
uniform token info:implementationSource = "sourceAsset"
uniform asset info:sourceAsset = @twin://rover/models/ReactionWheel.mo@
# Public interface: these are the only values another prim may consume.
float outputs:torque = 0.0
float outputs:speed = 0.0
}
The matching Modelica class must declare the same names as outputs. A declaration mismatch is a compile-time interface error, not a runtime surprise.
Types, units, and bounds¶
The scalar port surface is f64 at the runtime boundary. USD may author a
narrower type (float, double, int, bool), and the adapter preserves the
authored type for validation and display. A write that would narrow a value at
the GPU or command boundary is checked there; the port registry itself keeps
the exact sample.
Port metadata carries:
| Field | Purpose |
|---|---|
type |
Authored scalar type |
unit |
Engineering unit, when known |
bounds |
Valid range, when authored or supplied by the owner |
source |
Backend label such as Modelica, Avian, or a script |
authority |
Which owner may write the port |
writable |
Whether a command may write it |
Compatible unit conversions are compiled into the connection fabric. An incompatible unit is a wiring error, not an invitation to guess a factor.
Direction and ownership¶
Every port has exactly one declared owner. The PortRegistry selects that
owner from the composed declarations and the live backend state. A source wire
retains the authored prim and port identity; wiring does not redirect a source
around the owner's decision.
A value that is both authored as an output and written by an imperative
producer is a conflict. OutputPorts stores values for an imperative producer;
it is not a second signal graph and must not shadow a solver-owned output.
For a Modelica participant:
- Checked DAE runtime inputs define writable ports.
- Numeric observables, including authored outputs and local states, define readable values that USD may export.
- Constants, runtime inputs, and parameters are not published as participant output samples.
- Reading a local state does not make it writable.
Initial admission¶
At t = 0, source interfaces and native physical definitions become available
before numerical admission. Acyclic participants initialize from finite
producer samples. Participants with complete initial inputs enter independent
preparation without delaying a separate coupled plan.
Required external outputs still wait for their registered initial expressions or samples. Absent values remain pending rather than defaulting to zero.
When the remaining participants depend on one another, the owner captures
their exact source, authored bindings, scene generation, and ordered wiring.
The synth.coupled-initialization hook emits a Modelica composite containing
those instances, their input bindings, and input probes. The composite is
compiled and initialized as a request-owned job; its finite observations must
agree with the connected graph within solver tolerance before the members are
released together.
A changed source or wire cannot release an old certificate. Failed certification reports a terminal runtime fault and holds the affected participants.
Reading the graph¶
QueryUsdPrim exposes the composed port surface. ReadPorts exposes live
values with owner-supplied metadata. ListPorts enumerates the declared names
for an entity.
The CausalTrace query joins one semantic edge to its authored binding,
selected port owner, USD/Avian admission state, current measured channels,
producer origin, and optional fixed-tick admission stamp:
let edge = intent_pulse(target, "release");
let trace = query("CausalTrace", #{
target: target,
correlation_id: edge.id,
});
An absent or pending stage is an incomplete path, not successful actuation.
Omitting correlation_id selects the newest trace for the target.
Authoring through the document¶
The Editor and scripts do not rewrite USDA text to add a wire. They use the
typed SetConnection operation:
SetConnection {
edit_target,
path: "/Rover",
name: "inputs:throttle",
type_name: "float",
sources: ["/Driver.outputs:throttle"],
}
SetConnection is journaled with its inverse and replayed onto the live stage.
The typed connection preflight resolves referenced providers and applied API
schemas through the edited document's own authoring recipe. Runtime-only
endpoints are checked against the live port registry, so another open Twin
cannot change whether an authored wire is valid.
Worked example¶
A drive-by-wire rover has a Modelica controller, a physics chassis, and a Rhai mission:
def Xform "Rover" (kind = "assembly")
{
def Scope "Controller" (
prepend apiSchemas = ["LunCoProgramAPI"]
)
{
uniform token info:implementationSource = "sourceAsset"
uniform asset info:sourceAsset = @twin://rover/models/Drive.mo@
uniform bool lunco:program:realtimeSafe = true
float inputs:throttle = 0.0
float inputs:steer = 0.0
float outputs:left_torque = 0.0
float outputs:right_torque = 0.0
}
def Xform "Chassis" (
prepend apiSchemas = ["PhysicsRigidBodyAPI"]
)
{
float inputs:left_torque.connect = </Rover/Controller.outputs:left_torque>
float inputs:right_torque.connect = </Rover/Controller.outputs:right_torque>
}
}
The mission script writes throttle and steer through SetPorts. The
controller reads them, computes wheel torques, and publishes the two outputs.
The chassis consumes the torques. The same values are visible in the Ports
panel and over the API without a second publication path.
Rules of thumb¶
- Declare every consumed value as an
outputs:property on the producer. - Author the wire on the consumer, pointing at the producer.
- Keep one owner per port. Do not shadow a solver output with an imperative write.
- Use
connectors:only for genuine acausal Modelica interfaces. - Expect missing initial samples to hold admission; do not substitute zero.
- Inspect a disputed value with
CausalTracebefore adding another wire. - Add connections through
SetConnection, never by editing text behind the document's back.
Continue with Programs, which attach the behavior that reads and writes these ports.