Adapter Reference¶
The complete field-by-field reference for the declarative adapter specs. Use it to match your environment or model shape to a feature, then look up the exact behavior of every field.
For the concepts (how the two sides connect and why), start with Adapters. For logic a spec cannot express, see Escape Hatches. For exact signatures and the autodoc, see Adapters. Examples live at examples/python/adapters.
Every snippet uses import rlmesh.adapters as adapt.
Role registry¶
A role is the string that matches an environment feature to a model input. Roles are an open vocabulary: any string works as long as the env tag and the model spec agree on it verbatim. The constants below are the well-known conventions RLMesh ships; reach for them so independently authored envs and models line up, and invent your own string for anything they do not cover.
Role strings carry a feature-kind prefix (image/, proprio/, text/, action/), not a domain prefix (two domains sharing proprio/joint_pos is intentional).
Constant |
Wire string |
Domain |
Kind |
Typical width / encoding |
|---|---|---|---|---|
|
|
core |
|
H×W×C frame (main/exterior camera) |
|
|
core |
|
H×W×C frame (second fixed camera) |
|
|
core |
|
H×W×C frame (wrist/hand camera) |
|
|
core |
|
string (task instruction) |
|
|
core |
|
N joints (embodiment-dependent) |
|
|
core |
|
N joints |
|
|
manipulation |
|
3 (Cartesian xyz) |
|
|
manipulation |
|
width follows the rotation encoding |
|
|
manipulation |
|
1+ (embodiment-dependent) |
|
|
manipulation |
|
3 (Cartesian delta) |
|
|
manipulation |
|
width follows the rotation encoding |
|
|
manipulation |
|
1 |
You always pin widths explicitly (dim/index on a part, dim on an actuator); a registered role with a fixed canonical width then validates that declared dim (e.g. eef_pos must be 3-D, a mismatch is a resolve error); it never supplies it. Rotation widths follow the declared encoding (see Vocabularies).
Registered vs. ad-hoc roles. A registered role (the table above) is a shared contract: independently authored envs and models line up on it without prior agreement, and the fixed-width ones validate their dim. An ad-hoc role (any other <kind>/<name> string) still resolves on verbatim agreement, but it draws a non-fatal authoring nudge, and at the managed-service publish boundary a curated tier may reject it (role_policy="forbid"). When a role is intentionally non-standard (a self-contained env/model pair you own, or a not-yet-blessed domain), mark it with the reserved x/ prefix: an x/... role is never nudged and always passes the publish gate, declaring “I know this isn’t standard.” For an action dim no model reads, prefer a role-less (opaque) actuator over an ad-hoc role.
Bimanual roles¶
Every manipulation role has a _2 variant for the second arm: EEF_POS_2, EEF_ROT_2, GRIPPER_POS_2, ACTION_DELTA_POS_2, ACTION_DELTA_ROT_2, ACTION_GRIPPER_2. The first (or only) arm uses the unsuffixed role; the second arm uses _2. A single-arm environment never declares _2, so a model part targeting it zero-fills on the observation side and drops the extra dims on the action side.
Vocabularies¶
Rotation encodings are a closed set (a remote client must resolve a spec with no code). Each has a fixed native width:
Encoding |
Width |
Notes |
|---|---|---|
|
4 |
quaternion, scalar-last |
|
4 |
quaternion, scalar-first |
|
3 |
rotation vector |
|
6 |
6-D continuous (Zhou et al.) |
|
6 |
6-D continuous, row-major packing |
|
3 |
Euler angles, XYZ |
Other vocabularies:
Vocabulary |
Values |
Default |
Notes |
|---|---|---|---|
Image layout |
|
|
axis order of the stored image |
Fit mode |
|
(none) |
how to reconcile an aspect mismatch |
dtype |
any NumPy dtype name |
|
string, e.g. |
Normalization is one overloaded field, normalize: False (off, the default), True (the conventional [0, 1]), or a (low, high) pair (e.g. (-1.0, 1.0)) to map into a specific range. One field, so an on/off flag can never disagree with a range, and False is an authoritative off-switch.
The environment side¶
An environment tags its observation and action spaces. Tags are sparse: they carry each entry’s role plus the few facts the gymnasium spaces cannot express (image layout, rotation encoding, an explicit range). Keys, widths, dtypes, and bounds are read from the spaces by the native join step at resolve time.
import rlmesh.adapters as adapt
tags = adapt.EnvTags(
observation={
"pixels": adapt.ImageTag(adapt.IMAGE_PRIMARY),
"eef_pos": adapt.StateTag(adapt.EEF_POS),
"eef_quat": adapt.StateTag(adapt.EEF_ROT, encoding="quat_xyzw"),
"gripper": adapt.StateTag(adapt.GRIPPER_POS),
"task": adapt.TextTag(adapt.INSTRUCTION),
},
action=adapt.Action(
adapt.Actuator(adapt.ACTION_DELTA_POS, dim=3),
adapt.Actuator(adapt.ACTION_DELTA_ROT, dim=3, encoding="axis_angle"),
adapt.Actuator(adapt.ACTION_GRIPPER, dim=1),
),
)
EnvTags takes observation and action. The observation is a recursive tree whose container type is the runtime container type:
Authored container |
Maps a space |
Example |
|---|---|---|
Python |
|
|
Python |
|
|
bare leaf |
a single space leaf |
|
Nesting is real dict nesting that mirrors a nested Dict space ({"agent": {"eef_pos": StateTag(...)}}); there are no dotted keys. A single-leaf observation is the bare leaf with no dict wrapper.
ImageTag¶
ImageTag: one camera image leaf.
Field |
Default |
What it declares |
When to use |
|---|---|---|---|
|
– |
the image role to match |
always |
|
|
axis order of the stored frame |
the env stores |
|
|
the camera renders 180° rotated from upright |
a known flipped mount |
StateTag¶
StateTag: one numeric proprioception leaf.
Field |
Default |
What it declares |
When to use |
|---|---|---|---|
|
– |
the state role to match |
always |
|
|
rotation encoding (single, or a native-first preference sequence) |
the role is a rotation |
|
|
|
the space leaves this leaf unbounded |
range only supplies bounds the space lacks. If the space declares finite bounds that disagree with it, resolution errors rather than silently overriding them.
TextTag¶
TextTag: a text leaf (typically the instruction). Single field: role (1st positional). Use it when the observation carries a string the model conditions on.
Split + Field¶
Some environments expose one flat numeric Box with fixed index ranges instead of a key per quantity (Metaworld is the common case). Split tags that single vector: it is a leaf, not a container, and the observation-side mirror of Action.
adapt.EnvTags(
observation=adapt.Split(
adapt.Field(adapt.EEF_POS, dim=3),
adapt.Field(adapt.EEF_ROT, dim=4, encoding="quat_xyzw"),
adapt.Field(adapt.GRIPPER_POS, dim=1),
adapt.Field(dim=10), # skip the object/goal indices the policy reads from pixels
),
action=adapt.Action(...),
)
Split(*Field) takes its fields positionally and needs at least one. Field widths must sum to the leaf width (checked at join). A Field:
Field |
Default |
What it declares |
When to use |
|---|---|---|---|
|
|
the role for this slice; |
name it, or skip with |
|
– (required, ≥ 1) |
element count of the slice |
always |
|
|
rotation encoding (single or preference sequence) |
the slice is a rotation |
|
|
|
the slice is unbounded in the space |
A role=None field advances the offset without producing a feature; use it to step over indices the model never reads. A skip carries no encoding or range.
The model side¶
A model fully specifies the payload it ingests and the action it emits, in its own conventions. ModelSpec takes input and output. The input tree’s container type is the payload container predict receives (a dict, a tuple, or a bare single leaf). A leaf carries no key (its position in the tree is the payload position), and a role may be reused across leaves.
spec = adapt.ModelSpec(
input={
"image": adapt.Image(adapt.IMAGE_PRIMARY, size=256, normalize=True),
"state": adapt.Concat(
adapt.EEF_POS,
adapt.State(adapt.EEF_ROT, encoding="rot6d"),
adapt.GRIPPER_POS,
),
"instruction": adapt.Text(adapt.INSTRUCTION),
},
output=adapt.Action(
adapt.Actuator(adapt.ACTION_DELTA_POS, dim=3),
adapt.Actuator(adapt.ACTION_DELTA_ROT, dim=6, encoding="rot6d"),
adapt.Actuator(adapt.ACTION_GRIPPER, dim=1, binary=True),
),
)
Image¶
Image: a camera input. Every field:
Field |
Default |
What it does |
When to use |
|---|---|---|---|
|
– |
match an env image |
always |
|
|
sugar that sets |
square targets (pass |
|
|
target height (keep env height if |
non-square target |
|
|
target width |
non-square target |
|
|
axis order the model wants |
the model wants |
|
|
channel count the model wants (3 RGB, 1 gray) |
make a channel mismatch an error instead of silent |
|
|
NumPy dtype of the result |
the model wants floats |
|
|
map 8-bit pixels: |
scale |
|
|
leading singleton axes to add |
the model wants a batch/time axis |
|
|
the model was trained on 180°-rotated frames |
training-time flip |
|
|
resize filter: |
match the training pipeline |
|
|
permit a target larger than the env resolution |
the model needs more pixels than the camera has |
|
|
reconcile an aspect mismatch: |
target aspect differs from the env |
|
|
zero-fill a black frame when the env lacks this camera |
the camera may be absent (needs |
|
|
fill value for the blank frame |
non-black fill |
|
|
buffer N frames on a new leading axis |
frame history (see Frame history) |
size is the idiomatic square form. fit accepts a preference sequence (("crop", "pad")); the resolver picks, per env, the first that does not need a disallowed upscale, so one spec can crop a large camera and letterbox a small one.
State¶
State: the single-part numeric input. Every field:
Field |
Default |
What it does |
When to use |
|---|---|---|---|
|
– |
match an env state feature |
always |
|
|
rotation encoding: single, preference sequence, or |
the part is a rotation |
|
|
keep the leading N elements |
truncate the source |
|
|
select one element after conversion |
pick a single scalar |
|
|
zero-fill when the env lacks the role |
the role may be absent |
|
|
|
model and env disagree on scale |
|
|
zero-pad the result to this length |
fixed-width input |
|
|
NumPy dtype of the result |
non-default dtype |
|
|
target shape for the result |
the model wants a specific shape |
|
|
emit a NumPy array or a plain |
the model wants a list |
dim and index are mutually exclusive (dim keeps the leading N, index selects one). When optional is set the fill width must be known without an env feature, so set one of index, dim, or encoding. range is a no-op when the env has no source range to map from; it does not clamp on its own.
A State is also a valid Concat part: its part fields (role, encoding, dim, index, optional, range) are taken, and its container fields (pad_to, dtype, reshape, container) must stay default when used as a part.
Concat¶
Concat: the multi-part state leaf, several roles packed into one tensor. Concat(*parts, pad_to=None, dtype="float32", reshape=None, container="array") needs at least one part. A part is a bare role string (sugar for a role-only State) or a State carrying part fields:
adapt.Concat(
adapt.EEF_POS, # bare role: no options needed
adapt.State(adapt.EEF_ROT, encoding="rot6d"), # State part: needs an encoding
adapt.GRIPPER_POS,
)
Parts are concatenated in order. The container-level fields (pad_to, dtype, reshape, container) apply to the concatenated result and behave as in State. A single-role state is State directly; Concat is the >1-part case (both serialize to the same wire form).
Text¶
Text: a text input.
Field |
Default |
What it does |
When to use |
|---|---|---|---|
|
– |
match an env text feature |
always |
|
|
emit a plain string or a single-element list |
the model wants a list |
|
|
value when the obs omits the feature; |
supply a fallback instruction |
Tokenization stays in the model; Text delivers the raw string.
Custom¶
Custom: a payload slot computed by host-language code. Set exactly one of transform= (an in-process callable, local only) or entrypoint= (a "module:callable" string, imported only under resolve(..., trust_entrypoints=True)). The rest of the spec stays declarative. See Escape Hatches for the full pattern and the trust model.
The action side¶
Action is shared by env tags and model specs. Action(*Actuator, clip=None) takes its actuators positionally and exposes a .dim property (the sum of component dims). clip is an optional (low, high) applied to the final vector.
Actuator: one contiguous slice of the action vector:
Field |
Default |
What it does |
When to use |
|---|---|---|---|
|
|
match the actuator across sides; |
usually |
|
– (required) |
dimensions this component occupies |
always |
|
|
rotation encoding (or a |
the component is a rotation |
|
|
|
declare/convert the value range |
|
|
the component is a binary decision (snap after range map) |
a gripper open/close |
|
|
multiply the model value |
env actuator is scaled |
|
|
negate the model value (explicit |
gripper sign correction |
|
|
subtract to recenter the decision boundary |
shift a |
|
|
clamp the mapped value to |
per-dim safety on a mixed-range action |
|
|
constant per dim of an opaque (role-less) actuator |
env-required dims no model reads |
scale, invert, and threshold declare a side’s actuator convention. They can be set on either side and compose as literal transforms applied after the declared formats (rotation, range) are bridged, model-side first (the model’s own output convention), then env-side (the env’s):
flowchart LR
a["rotation / range bridged"] --> b["model: scale → invert → threshold"]
b --> c["env: scale → invert → threshold"]
c --> d["binary"]
d --> e["clip"]
So an env declares its quirk once and every model inherits it; and a model whose own output differs from a shared env it cannot edit declares the bridge on its own actuator, e.g. a sign-flipped gripper as Actuator(ACTION_GRIPPER, dim=1, invert=True), or a sigmoid-probability gripper as binary=True, threshold=0.5, instead of hardcoding the env’s convention in predict(). binary snaps to a definite side after range mapping: >= 0 opens (+1), below closes (-1); a value exactly on the boundary opens rather than emitting an undefined 0.
clip is the exception; it stays env-side only: it clamps to the env actuator’s range (a final safety bound, not a convention), so declaring it on a model actuator is a resolve error.
A role-less actuator (Actuator(dim=N, fill=...) with no role) is opaque: it occupies N dims of the env action with the constant fill, matched by no model output (the action-side mirror of a role-less Field). Use it for dims the env requires but no model produces, such as a control-mode selector or base padding. A registered role with a fixed canonical dim (e.g. eef_pos is 3-D) also validates the declared dim; a mismatch is a resolve error.
Conversion semantics and policy¶
Each conversion the resolver can perform falls into one of four policies. Silent is always applied when declared; opt-in is off until you set the flag; advisory-warn succeeds but logs data loss; resolve-error fails resolution.
Conversion |
Policy |
Trigger |
|---|---|---|
Image resize (target ≤ env resolution) |
SILENT |
a smaller |
Layout transpose ( |
SILENT |
model |
Normalize |
SILENT |
|
dtype cast |
SILENT |
model |
Rotation encoding conversion |
SILENT |
model encoding differs (both known) |
Range map (affine) |
SILENT |
model |
|
SILENT |
declared on the actuator |
|
OPT-IN |
aspect mismatch; absent |
|
OPT-IN |
target > env resolution; absent → resolve error |
|
OPT-IN |
declaring it turns a channel-count mismatch into a resolve error (silent otherwise) |
|
OPT-IN |
env lacks the camera/role; absent → resolve error |
Crop |
ADVISORY-WARN |
|
Pad |
ADVISORY-WARN |
|
Zero-filled camera / state |
ADVISORY-WARN |
an |
Two axes: parsing and resolve¶
Spec handling has two independent stages. Parsing is split: publishing a spec is strict and rejects unknown fields, while reading one back is tolerant and round-trips it, so a newer peer’s spec does not break an older reader. Resolve is where a spec meets a concrete pair of spaces and the policy table above applies.
The bare-field taint rule: an unknown field on a known kind is a resolve error unless its name is prefixed x- or ext-. Prefixed extension fields are carried through untouched; an un-prefixed unknown field is treated as a typo and rejected.
Join-time validation is the final gate: when a tag and its gymnasium space disagree on class, width, encoding, or range, resolution errors rather than guessing.
Frame history (stack)¶
A model that conditions on a short history sets stack=N on an Image. The adapter keeps an episode-keyed rolling buffer of the last N processed frames and emits them on a new leading axis, padding the start of an episode with the first frame and clearing on reset.
adapt.Image(adapt.IMAGE_PRIMARY, size=256, stack=4)
Stacking is host-side on the local path and native in the core on the served path. Either way the environment still sends one frame per step, nothing extra crosses the wire.
Caution
Frame stacking is episode state held outside the model: host-side on the local path, in the core
on the served path (an episode-keyed buffer per vector lane). The spec’s stack round-trips through
to_json, the buffer clears on reset, and the env still sends one frame per step, so no frames
leak across episodes or lanes and nothing extra crosses the wire.
Match your shape¶
Find the row that matches your environment, then tag it:
My environment looks like… |
Tag it… |
|---|---|
|
a |
one flat |
a bare |
a |
a Python |
an upside-down camera |
|
quaternion proprioception |
|
two arms |
the role plus its |
Find the row that matches your model, then spec it:
My model wants… |
Spec it… |
|---|---|
a resized, normalized image |
|
channels-first |
|
stacked frames |
|
concatenated proprio with a rotation |
|
a binary gripper command |
|
an optional second camera |
|
an instruction string |
|
Common pitfalls¶
Symptom |
Cause |
Fix |
|---|---|---|
Wrong channel count slips through |
RGB vs grayscale not declared |
set |
Image axes scrambled |
HWC vs CHW mismatch |
set |
Rotation looks rotated wrong |
|
match the env’s exact encoding |
Values out of range |
scale mismatch |
set |
Resolve fails on a missing camera |
env lacks the role |
|
Resolve fails on upscale |
target larger than the camera |
|
Errors and describe()¶
Resolution raises AdapterResolutionError when a spec cannot be bridged to the spaces: a required role with no optional/zero-fill, a declared channel mismatch, an upscale without allow_upscale, an aspect mismatch without fit, an unsupported resample/dtype, an impossible encoding conversion, a bare unknown field on a known kind, or a join-time class/width/encoding/range disagreement between a tag and its space. The message names the offending leaf and what it expected.
Once resolution succeeds, call adapter.describe() to print the exact transforms the resolver chose (each resize, layout transpose, encoding conversion, range map, key remap, slice, and clip) before you run a single step. It is the fastest way to confirm the bridge is what you intended.
adapter = adapt.resolve(tags, env.observation_space, env.action_space, spec)
print(adapter.describe())