relayrl_env_trait 1.3.1

Traits for training and testing environments in the RelayRL framework.
Documentation

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 + Sync everywhere. All traits require Send + 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 across reset/step and 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. The flat_*_bytes accessors 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))
    }
}