Adapters¶
Note
rlmesh.adapters is experimental: it may change or disappear. Pin versions; see Compatibility.
rlmesh.adapters derives the preprocessing and postprocessing between an environment and a model
from declarative descriptions, instead of a hand-written adapter per pair.
The split is asymmetric. An environment tags its observation and action spaces: it names the
semantic role of each entry plus the few facts the spaces cannot carry (image layout, rotation
encoding, an explicit value range). A model fully specifies the payload it ingests and the action it
emits. resolve() matches the two by role and produces an
Adapter; widths, dtypes, and keys come from the gymnasium spaces. See
Adapters for a guided walkthrough.
Install it with the NumPy backend:
pip install "rlmesh[numpy]"
Note
Adapters are entirely opt-in: the core Gymnasium loop never imports this package. Resolution and
plan application run in the native rlmesh-adapters core; this package keeps the host-language half:
spec construction and serialization, entrypoint-trust gating, custom callables, and the
custom-adapter base class.
Resolution¶
- rlmesh.adapters.resolve(env_tags, observation_space, action_space, model_spec, *, trust_entrypoints=False, check_inverse=True)[source]¶
Derive an
Adapterfor an env/model pair.- Parameters:
env_tags (EnvTags) – The environment’s observation/action tags.
observation_space (object) – The environment’s gymnasium observation space (or any space object RLMesh can parse, e.g. an
rlmesh.spacesspace or a nativeSpaceSpec).action_space (object) – The environment’s gymnasium action space.
model_spec (ModelSpec) – The model’s declared input/output format.
trust_entrypoints (bool) – Allow
module:callablestrings in custom inputs to be imported. Leave False for specs from untrusted sources; in-process callables are always allowed.check_inverse (bool) – Round-trip each two-armed
CustomEncodingon a probe at resolve time to catch a mispaired encode/decode. Set False for an intentionally non-invertible encoding.
- Returns:
Adapter applying observation preprocessing and action postprocessing.
- Raises:
AdapterResolutionError – If a model input or action component has no usable counterpart in the env tags and spaces.
- Return type:
- rlmesh.adapters.resolve_from_contract(contract, model_spec, *, trust_entrypoints=False, check_inverse=True)[source]¶
Derive an
Adapterfrom an env contract and a model spec.Reads the env’s tags from its contract metadata (published under
ENV_METADATA_KEYby a server set up withrlmesh.adapters.tag()) and its observation/action spaces from the contract, then resolves as inresolve().- Parameters:
contract (EnvContract) – The environment contract (e.g.
remote_env.env_contract).model_spec (ModelSpec) – The model’s declared input/output format.
trust_entrypoints (bool) – See
resolve().check_inverse (bool) – See
resolve().
- Raises:
AdapterResolutionError – If the contract carries no tags, or resolution fails.
- Return type:
- rlmesh.adapters.tag(env, tags, *, validate=True)[source]¶
Merge env tags into
env.metadata(underENV_METADATA_KEY) and return it.With
validate=True(default), check the tags against the env’s observation/action spaces via the nativejoin, raisingAdapterResolutionErrorif a role or width cannot be reconciled.- Parameters:
env (EnvT)
tags (EnvTags)
validate (bool)
- Return type:
EnvT
Model Spec¶
- class rlmesh.adapters.ModelSpec[source]¶
Bases:
objectDeclarative description of a model’s input payload and action output.
- inputs¶
Input features keyed into the model payload dict.
- action¶
Layout of the action vector produced by the model.
- class rlmesh.adapters.ImageInput[source]¶
Bases:
objectAn image input expected by a model.
- key¶
Key of the entry in the model input payload.
- Type:
str
- role¶
Semantic role matched against env image features.
- Type:
str
- height¶
Target image height, or None to keep the env height.
- Type:
int | None
- width¶
Target image width, or None to keep the env width.
- Type:
int | None
- layout¶
Axis layout the model expects.
- Type:
Literal[‘hwc’, ‘chw’]
- dtype¶
NumPy dtype name the model expects.
- Type:
str
- normalize¶
Scale 8-bit pixel values into
[0, 1]before casting.- Type:
bool
- lead_dims¶
Number of leading singleton axes to add (batch/time).
- Type:
int
- upside_down¶
Whether the model was trained on images rotated 180 degrees relative to the canonical upright orientation.
- Type:
bool
- resample¶
Resize algorithm the model’s training pipeline used:
"bilinear_aa"(antialiased triangle filter, PIL-compatible) or"bilinear"(4-tap half-pixel-center bilinear, OpenCV/torch-compatible).- Type:
str
- stack¶
Number of consecutive observations to stack on a new leading axis (frame history).
1(default) means no stacking. Stacking is applied host-side by the adapter, which buffers processed frames (padding with the first frame at the start of an episode) and clears them onreset– the env still sends one frame per step.- Type:
int
- size¶
Convenience for square targets – sets both
heightandwidth. Passsizeorheight/width, not both.- Type:
dataclasses.InitVar[int | None]
- class rlmesh.adapters.StateInput[source]¶
Bases:
objectA numeric state input expected by a model.
- key¶
Key of the entry in the model input payload.
- Type:
str
- components¶
Pieces concatenated (in order) to form the value.
- Type:
- pad_to¶
Zero-pad the concatenated vector to this length.
- Type:
int | None
- dtype¶
NumPy dtype name of the resulting value.
- Type:
str
- reshape¶
Optional target shape for the resulting value.
- Type:
tuple[int, …] | None
- container¶
Emit a NumPy array or a plain Python list.
- Type:
Literal[‘array’, ‘list’]
For a single-piece state, pass
role(and optionallyencoding/dim/index) instead ofcomponents– e.g.StateInput("state", role=EEF_POS)is shorthand forStateInput("state", components=(StateComponent(EEF_POS),)).
- class rlmesh.adapters.StateComponent[source]¶
Bases:
objectOne piece of a model state vector, sourced from an env state feature.
- role¶
Semantic role matched against env state features.
- Type:
str
- encoding¶
Rotation encoding the model expects for this piece.
- Type:
Literal[‘quat_xyzw’, ‘quat_wxyz’, ‘axis_angle’, ‘rot6d’, ‘rot6d_rowmajor’, ‘euler_xyz’] | rlmesh.adapters.specs.custom_encoding.CustomEncoding | None
- dim¶
Optional number of leading elements to keep from the source.
- Type:
int | None
- index¶
Optional single element to select after any conversion.
- Type:
int | None
- optional¶
Zero-fill this piece when the env does not declare the role, instead of failing resolution. The fill width comes from
index(one),dim, orencoding; one of them must be set so the width is known without an env feature.- Type:
bool
- range¶
Optional
(low, high)the model expects this piece in; when the env declares its own range, the value is affinely mapped from the env range to this one (symmetric to action ranges).- Type:
tuple[float, float] | None
- class rlmesh.adapters.TextInput[source]¶
Bases:
objectA text input expected by a model.
- key¶
Key of the entry in the model input payload.
- Type:
str
- role¶
Semantic role matched against env text features.
- Type:
str
- container¶
Emit a plain string or a single-element list.
- Type:
Literal[‘str’, ‘list’]
- default¶
Value used when the observation omits the feature; when None the key is omitted from the payload instead.
- Type:
str | None
Action Layout¶
The action layout is a shared vocabulary. An environment tags the action vector its step accepts;
a model declares the action vector it emits. The resolver converts between them per component.
- class rlmesh.adapters.ActionLayout[source]¶
Bases:
objectOrdered action components plus optional clipping bounds.
Components are passed positionally, mirroring the observation-side
StateLayout:ActionLayout(ActionComponent(DELTA_POS, 3), ActionComponent(GRIPPER, 1))
- components¶
Action components in vector order.
- Type:
- clip¶
Optional
(low, high)clip applied to the final vector.- Type:
tuple[float, float] | None
- class rlmesh.adapters.ActionComponent[source]¶
Bases:
objectOne contiguous slice of an action vector.
- role¶
Semantic role used for matching, e.g.
action/gripper.- Type:
str
- dim¶
Number of action dimensions occupied by this component.
- Type:
int
- encoding¶
Rotation encoding when the component is a rotation.
- Type:
Literal[‘quat_xyzw’, ‘quat_wxyz’, ‘axis_angle’, ‘rot6d’, ‘rot6d_rowmajor’, ‘euler_xyz’] | rlmesh.adapters.specs.custom_encoding.CustomEncoding | None
- range¶
Optional
(low, high)range of the component values.- Type:
tuple[float, float] | None
- scale¶
Optional multiplier applied to the model value for this role.
- Type:
float | None
- invert¶
Negate the model value for this role (equivalent to
scale=-1but explicit; the common gripper-sign correction).- Type:
bool
- threshold¶
Subtract this from the value, recentering the decision boundary – typically paired with
binaryso the snap splits atthresholdinstead of zero.- Type:
float | None
- binary¶
Whether the component encodes a binary decision (resolved adapters snap the value to a definite side after range mapping:
>= 0opens (+1), below closes (-1); a value exactly on the boundary opens rather than emitting an undefined0).- Type:
bool
scale,invert, andthresholdare env-side corrections: they declare the env actuator’s convention and are applied to the incoming model value after the declared formats (rotation, range) are bridged, in the order scale, invert, threshold, thenbinary. Declared once on the env, every model evaluated against it inherits the correction.
Escape Hatches¶
When a pairing needs logic a declarative spec cannot express, three mechanisms compose, most local
first. A custom input computes one payload key from the raw observation while the rest stays
spec-driven. A custom encoding handles a rotation convention the native crate does not ship. A
custom adapter subclasses AdapterBase to add stateful behavior, typically
by wrapping a resolved adapter and overriding only the stateful part.
Custom inputs¶
InlineCustomInput runs an in-process callable that maps the raw
observation to one payload key; it is local only. EntrypointCustomInput
names a module:callable string that is imported only when you pass
resolve(..., trust_entrypoints=True), so it can travel in a contract. A custom input receives the
environment’s own keys, not roles, and returns the entire payload key, so it does no role-matching,
dim/index, or range-mapping, and is observation-side only.
- class rlmesh.adapters.InlineCustomInput[source]¶
Bases:
objectA custom input computed in-process by a user callable.
Local only: the callable cannot be serialized, so a model spec carrying an
InlineCustomInputcannot be published in contract metadata. UseEntrypointCustomInputfor a spec that must travel.- key¶
Key of the entry in the model input payload.
- Type:
str
- transform¶
Callable taking the raw observation mapping.
- Type:
collections.abc.Callable[[collections.abc.Mapping[str, Any]], Any]
- class rlmesh.adapters.EntrypointCustomInput[source]¶
Bases:
objectA custom input computed by a
module:callableentrypoint.Serializable and publishable. The entrypoint is imported only when
resolve(..., trust_entrypoints=True); otherwise resolution refuses to import it.- key¶
Key of the entry in the model input payload.
- Type:
str
- entrypoint¶
A
module:callablestring.- Type:
str
Custom encodings¶
Rotation encodings are a closed vocabulary (the RotationEncoding set listed under
Vocabulary). You cannot register one from Python, because a spec is data that travels
in a contract and resolves on a remote client with no code. For a convention that is general and
stable, like a published model’s rot6d_rowmajor, add it first-party: a few lines on the native
RotationEncoding enum plus the Python Literal. It then works on both the observation and action
sides, serializes into the contract, and is conformance-tested once.
For a bespoke or proprietary convention, declare a CustomEncoding on the
nearest native base encoding (rot6d or a quaternion) and supply the host-side repacking. resolve
lowers the field to its base for the native core, so role-matching, range-mapping, and the
env-to-base conversion are unchanged; the adapter applies your transforms at the boundary:
from_base after the native conversion on the observation side, to_base before it on the action
side. Define the encoding once and reference it from both arms:
ROT6D_MINE = adapt.CustomEncoding(
base="rot6d", from_base=rot6d_to_mine, to_base=mine_to_rot6d, name="rot6d_mine"
)
The packing must preserve the base width, and an observation custom encoding must be the sole
component of a single-piece StateInput (the offset of a field interior to a multi-piece state is
env-dependent). At resolve time the two arms are round-tripped on a probe to catch a mispaired
encode/decode; pass resolve(..., check_inverse=False) to skip. The transforms are in-process
callables, so the spec is local; a serializable module:callable form is planned.
When the constraints do not fit (a width-changing repack, a rotation interior to a multi-field
state, or non-rotation feature engineering), drop to a custom AdapterBase or replace a whole
payload key with an InlineCustomInput. What none of these do is attach a custom encoding to a role
in the spec itself: the vocabulary stays closed so specs remain pure data that resolve on a remote
client with no code. Reach for the boundary wrapper for a one-off; upstream the encoding once you
want it attached to a role and reused.
Custom adapters¶
Subclass AdapterBase for stateful behavior a spec cannot describe (for
example temporal ensembling across action chunks, or a width-changing rotation repack interior to a
multi-field state). The usual shape wraps a resolved adapter and overrides only the stateful part.
Override reset() to clear episode state and wire it to the model
worker’s on_reset.
A pair override replaces the adapter for one specific (model, environment) pairing entirely, for cases like control-space conversion against a robot’s kinematic model. There is no special machinery: keep a registry keyed by the pair and consult it before resolving.
- class rlmesh.adapters.AdapterBase[source]¶
Bases:
ABC,Generic[ActionT]Base class for env-to-model adapters.
rlmesh.adapters.resolve()derives the spec-driven implementation (Adapter); subclass this directly to plug a fully custom pairing into anything built around adapters instead. Implement the two transforms;wrap_predictcomes for free. Custom adapters may hold state across steps (e.g. cache proprio fromtransform_obsfor use intransform_action) – that is their power over declarative specs. Overridereset()to clear any such state at episode boundaries.- abstractmethod transform_obs(raw_obs)[source]¶
Convert a raw env observation into the model input payload.
raw_obsis a mapping for a Dict space, or a bare array/leaf for a flat (non-Dict) space.- Parameters:
raw_obs (Mapping[str, Any] | object)
- Return type:
dict[str, Any]
- abstractmethod transform_action(raw_action)[source]¶
Convert a model action output into the env action.
- Parameters:
raw_action (object)
- Return type:
ActionT
- reset(env_index=None)[source]¶
Clear episode-scoped state, optionally for a single lane.
env_indexidentifies the vector lane whose episode rolled, orNonefor a whole-vector reset. The default does nothing (resolved adapters are stateless). Stateful custom adapters override this and wire it to the model worker’s per-lane reset so a single lane’s autoreset never wipes the other still-running lanes’ state.- Parameters:
env_index (int | None)
- Return type:
None
- property is_stateful: bool[source]¶
Whether the adapter carries per-stream state across steps.
A stateful adapter must keep affinity to its lane (one instance per
(session, route, env_index)) and so cannot yet be shared across the lanes of a vector env. Custom adapters default to stateful (the safe assumption);Adapterderives this from its frame history. The per-lane affinity manager that makes vectorized stateful adapters correct is not implemented yet.
- wrap_predict(predict_fn)[source]¶
Wrap a model predict function with both transforms.
The returned callable takes a raw env observation – a mapping, or a bare array/leaf for a flat (non-Dict) env – and returns an env-ready action, suitable for
rlmesh.numpy.Model.- Parameters:
predict_fn (Callable[[dict[str, Any]], object])
- Return type:
Callable[[Any], ActionT]
Adapter Objects¶
- final class rlmesh.adapters.Adapter[source]¶
Bases:
AdapterBase[object]A resolved env-to-model adapter; build instances with
resolve().Declarative plans run in the native
rlmesh-adapterscore; custom inputs run their host-language transforms on the raw Python observation, exactly as before.
Errors¶
Vocabulary¶
Semantic roles are an open vocabulary of wire strings matched verbatim between independently
authored tags and specs. The well-known conventions that ship with RLMesh are re-exported from the
package (single-sourced from the native crate): the domain-agnostic roles IMAGE_PRIMARY,
IMAGE_SECONDARY, INSTRUCTION, JOINT_POS, JOINT_VEL; the arm-manipulation observation roles
IMAGE_WRIST, EEF_POS, EEF_ROT, GRIPPER_POS; and the action roles ACTION_DELTA_POS,
ACTION_DELTA_ROT, ACTION_GRIPPER. Bimanual _2 variants exist for the per-arm roles EEF_POS,
EEF_ROT, GRIPPER_POS, ACTION_DELTA_POS, ACTION_DELTA_ROT, and ACTION_GRIPPER.
Rotation widths follow the declared encoding. rlmesh.adapters.ROTATION_DIMS maps each encoding to
its dimension count:
Encoding |
Dims |
Convention |
|---|---|---|
|
4 |
quaternion, scalar-last |
|
4 |
quaternion, scalar-first |
|
3 |
rotation vector |
|
6 |
first two columns of the rotation matrix, concatenated |
|
6 |
same two columns flattened row-major |
|
3 |
roll-pitch-yaw, extrinsic XYZ |
rot6d is the standard 6D rotation; rot6d_rowmajor exists for checkpoints trained on the
row-major interleaving. See Adapters for when to add an encoding versus reach
for a custom encoding.