RelayRL Environment Traits
The environment abstraction layer for RelayRL. This crate is intentionally tiny and
dependency-light (thiserror + uuid only): it defines how the runtime talks to an
environment, but contains no simulator, no tensor backend, and no runtime logic. The
relayrl_framework client drives any type that implements these traits.
System layout
All public items live in the [traits] module and are re-exported at the crate root:
- [
Environment]: the base contract every environment shares (observation/mask building, dtypes and dimensions, flat-bytes accessors, discreteness, and conversion into an [EnvironmentHandle]). - [
ScalarEnvironment]: one object per logical environment, stepped with a single action. - [
VectorEnvironment]: one object that owns a batch of logical environments, stepped with a batch of actions in a single call. - [
EnvironmentHandle]: a runtime-facing enum unifying boxed scalar and vector environments, with [DynScalarEnvironment] providing object-safe, clonable scalar envs. - Supporting types: [
EnvironmentUuid] (stable per-env identity), [EnvDType] / [EnvironmentKind], [ScalarEnvReset] / [VectorEnvReset], the byte aliases [Observation] / [Mask] / [Reward] / [Done] / [Truncated], and [TrainingPerformanceReturnFn] for custom training signals.
Scalar vs. vector execution
The framework may run many logical environments in parallel (one
[ScalarEnvironment] per worker) or a single batched simulator that implements
[VectorEnvironment]:
- Use [
ScalarEnvironment] when each sub-environment is its own object with a scalar step. A parallel runner holds many handles, assigns one stable [EnvironmentUuid] per sub-env, and steps each worker independently. - Use [
VectorEnvironment] when one implementation can apply a batch of actions keyed by [EnvironmentUuid] in a single call (GPU batching, vectorized physics, a remote batched service, etc.).
Design notes and implementor contracts
Send + Synceverywhere. All traits requireSend + Sync, so mutable simulation state should live behind interior mutability (e.g.Mutex, atomics) rather than&mut self— every method takes&self.- Opaque identity. Treat [
EnvironmentUuid] as opaque; the same uuid must refer to one logical env acrossreset/stepand any runtime routing. - Ordering. Unless your concrete type documents otherwise, callers should not assume
[
VectorEnvironment::step_bytes] output order matches input order; key results by [EnvironmentUuid]. - Errors are whole-operation. [
EnvironmentError] describes the entire call; partial success is not expressed in the type system. Surface per-env failures inside your info payloads if you need them. - Type-erased observations. [
Environment::build_observation] returns [std::any::Any] for framework integration; pair it with a documented downcasting convention. Theflat_*_bytesaccessors provide the byte-oriented path the runtime uses.
Quick start
A minimal scalar environment skeleton. Note that every method takes &self, so any
mutable state must use interior mutability:
use relayrl_env_trait::*;
use std::any::Any;
#[derive(Clone)]
struct MyEnv;
impl Environment for MyEnv {
fn run_environment(&self) -> Result<(), EnvironmentError> { Ok(()) }
fn build_observation(&self) -> Result<Box<dyn Any>, EnvironmentError> {
Ok(Box::new(vec![0u8; self.observation_dim()]))
}
fn build_mask(&self) -> Result<Box<dyn Any>, EnvironmentError> { Ok(Box::new(())) }
fn observation_dtype(&self) -> EnvDType { EnvDType::NdArray(EnvNdArrayDType::F32) }
fn action_dtype(&self) -> EnvDType { EnvDType::NdArray(EnvNdArrayDType::I64) }
fn observation_dim(&self) -> usize { 8 }
fn action_dim(&self) -> usize { 4 }
fn flat_observation_bytes(&self) -> Observation { vec![0u8; self.observation_dim()] }
fn flat_mask_bytes(&self) -> Mask { None }
fn action_is_discrete(&self) -> bool { true }
fn kind(&self) -> EnvironmentKind { EnvironmentKind::Scalar }
fn into_handle(self: Box<Self>) -> EnvironmentHandle {
EnvironmentHandle::Scalar(Box::new(*self))
}
}
impl ScalarEnvironment for MyEnv {
fn reset(&self) -> Result<ScalarEnvReset, EnvironmentError> {
Ok(ScalarEnvReset { observation: self.flat_observation_bytes(), info: None })
}
fn step_bytes(
&self,
_action: &[u8],
) -> Option<(Observation, Mask, Reward, Done, Truncated)> {
Some((self.flat_observation_bytes(), None, 0.0, false, false))
}
}