The python/ package is a synchronous client for the cev-sim protocols 1.2 through 1.5
headless supervisor. JavaScript remains the authoritative simulator; Python
owns Gymnasium/SB3 integration and NumPy tensors only.
To start from a saved environment without writing a run manifest, follow Getting started with the Python simulator.
From a checkout that already has npm install and Node.js >=22.22.2 <23:
python -m pip install -e './python[viz]'
python python/examples/02_astar_track.py --steps 50CevSimEnv(bundle) launches bin/cev-sim.js when cev-sim is not on PATH.
Omitting output_dir uses a temporary directory, disables artifacts, and
deletes that directory on close(). gym.make("CevSim-v0", bundle=...) uses
the same class.
from cev_sim import CevSimEnv
env = CevSimEnv("python/examples/bundles/straight.json")
try:
observation, info = env.reset(seed=123)
observation, reward, terminated, truncated, info = env.step(env.action_space.sample())
finally:
env.close()The other examples are 01_pure_pursuit.py, 03_rrt_star.py, and
04_mpc_kinematic.py. Each accepts --steps, --bundle, --seed, and
--render. Rendering needs the viz extra and the planning-oracle profile,
which those examples already select.
From a checkout:
python -m pip install -e ./python
python -m pip install -e './python[sb3]'The headless release builder emits a pure-Python wheel and sdist in the
coordinated internal candidate artifact. Verify release-manifest.json /
SHA256SUMS, then use
npm run artifacts:install -- --dist <download> --python-venv <venv> or
install the selected wheel directly. The matching cev-sim@0.2.0 npm tarball
provides the local supervisor executable; neither package is published to a
registry. See Headless release and CI gates and
CHANGELOG.md.
Python 3.10–3.13 is supported. The base package does not install
Stable-Baselines3 or PyTorch. Unit tests (pytest -m 'not integration')
collect without the sb3 extra; Gymnasium/SB3 integration tests require
./python[sb3,test]. JS-backed unit tests (test_bundle.py envelope/rfc8785
checks and the session fixture) skip unless node_modules is present.
Generated Protobuf bindings are committed and
must be regenerated, never edited, after an additive protocol change:
python python/scripts/generate_proto.py
python python/scripts/generate_proto.py --checkload_world(bundle) reads the verified route, obstacle footprints, drivable
surfaces, vehicle limits, start pose, and goal in the planar frame
(x, z, yaw). Forward is (cos(yaw), -sin(yaw)).
world.occupancy_grid(resolution_m, inflate_m=...) marks drivable free cells
as 0 and obstacle or off-road cells as 1.
planning-oracle@1 is opt-in. It keeps every measured-state tensor and adds
oracle/ego/pose, oracle/ego/velocity, oracle/ego/steering, other actors'
poses and footprints, and oracle/goal. The default profile remains
measured-state and still excludes oracle pose.
from cev_sim import (
PLANNING_ORACLE_OBSERVATION_PROFILE,
CevSimEnv,
EpisodeConfig,
load_world,
)
world = load_world("python/examples/bundles/straight.json")
env = CevSimEnv(
"python/examples/bundles/straight.json",
episode=EpisodeConfig(observation_profile=PLANNING_ORACLE_OBSERVATION_PROFILE),
)
observation, info = env.reset(
seed=1,
options={"start_pose": (world.start.x, world.start.z, world.start.yaw), "goal": (world.goal.x, world.goal.z)},
)options accepts start_pose=(x, z, yaw[, speed]) and goal=(x, z[, yaw]),
plus positive position_tolerance_m (default 1 m) and yaw_tolerance_rad
(default π). A start pose that overlaps an obstacle or leaves the drivable
surface is rejected. A goal reached within those tolerances ends the episode
successfully. The bundle route still drives task/*; planners should read
oracle/goal. Overrides are episode identity: the episode hash changes only
when a start pose or goal is present. CevSimVecEnv.set_reset_options carries
the same mapping, including automatic resets after a terminal step.
create_bundle(path) runs cev-sim create-bundle and writes a validated
bundle. npm run fixtures:planning refreshes
python/examples/bundles/straight.json and curve.json.
configure and launch are the session helpers. configure writes a
temporary free-drive bundle with cev-sim create-bundle --finish none, then
launch starts one worker. The tutorial is
python-getting-started.md.
import cev_sim
config = cev_sim.configure(environment="igvc", vehicle="big-car", goal=None)
with cev_sim.launch(config) as sim:
observation, info = sim.reset(seed=1)
observation, reward, terminated, truncated, info = sim.step(sim.action_space.sample())configure arguments:
| Argument | Default | Meaning |
|---|---|---|
environment |
required | Saved or built-in environment id. |
vehicle |
manifest car, big-car for the built-in run |
Saved or built-in vehicle id. A saved vehicle with a dynamics plant uses that plant when the package is installed in the same data directory. |
start |
bundle pose | (x, z), (x, z, yaw), or (x, z, yaw, speed). A two-number start takes its yaw from the nearest route segment. |
goal |
None |
(x, z) or (x, z, yaw). None is a free drive. |
goal_tolerance_m |
1.0 |
Success radius when goal is set. |
max_steps |
0 |
Policy-step limit. 0 does not truncate. |
view |
"planning" |
"planning" selects planning-oracle@1. "measured" selects measured-state. |
logging |
False |
False discards artifacts. A directory path keeps an evaluation log. |
data_dir |
CEV_SIM_DATA_DIR, then checkout server/data |
Saved environment and vehicle store. Omitted when only built-ins exist. |
launch(config, **env_kwargs) returns a CevSimEnv. Extra keywords are the
CevSimEnv arguments, such as render_mode="human". The environment is a
context manager. sim.run_config keeps the bundle directory alive until the
environment is closed. config.save(path) copies the bundle. config.world
is the PlanningWorld.
Free drive still contains a verified ego route, because a headless episode
requires one. --finish none replaces the finish-zone trigger with a trigger
on the flag cev-sim.free-drive.stop, which nothing sets. The episode then
ends on collision, off-road, max_steps, or when the caller stops.
A goal is a protocol 1.5 reset override, not a finish zone, so changing it
does not rebuild the bundle.
environments.list() and vehicles.list() return CatalogEntry records
(id, name, built_in) from cev-sim catalog.
CevSimEnv and gym.make("CevSim-v0") still default to measured-state.
configure is the call that defaults to the planning view.
After reset, sim.loop() stays true until the episode is terminated or
truncated. It does not step. sim.clock.steps() is the simulation step,
sim.clock.time() is elapsed seconds, and sim.clock.dt() is the seconds in
one simulation step. sim.pose(), sim.velocity(), sim.steering(), and
sim.goal() copy the latest planning-oracle values. goal() is the route end
during a free drive. Those four readers require the planning view.
Supply an existing Unix/TCP supervisor target, or omit both target and
launch to start a local supervisor. output_dir is optional. Pass it, with
an artifact policy, when episode artifacts should be kept.
from cev_sim import ArtifactPolicy, CevSimEnv, SupervisorLaunch
env = CevSimEnv(
"run-bundle.json",
output_dir="runs/gym",
launch=SupervisorLaunch(executable="cev-sim"),
artifact_policy=ArtifactPolicy(profile="training"),
)
try:
observation, info = env.reset(seed=123)
observation, reward, terminated, truncated, info = env.step(
env.action_space.sample()
)
finally:
env.close()reset(seed=N) uses exactly N as the simulator reset seed. Later unseeded
resets draw deterministic uint64 seeds from Gymnasium's generator. Resetting an
active or terminal episode finalizes it first. A terminal step() returns the
actual final observation; callers must reset before stepping again.
EpisodeConfig, ArtifactPolicy, ResourceLimits, and SupervisorLaunch
are frozen configuration values exported by cev_sim. The default episode
uses one fixed step per action, no policy-step bound, the measured-state and
default route-safety profiles, bundle-selected physics, and deterministic
state sensors. If the bundle manifest enables lidar3d, the client also adds
the locked DEFAULT_CPU_LIDAR_BACKEND selection and verifies that the
supervisor advertises deterministic-cpu-bvh-lidar version 1. CPU point
clouds are published to topics and SFLog; the default Python observation
remains the same flat measured-state dictionary.
Select measured-perception explicitly for policy-visible measured RGB and
LiDAR range/incidence tensors:
from cev_sim import CevSimEnv, EpisodeConfig, MEASURED_PERCEPTION_OBSERVATION_PROFILE
env = CevSimEnv(
"camera-run-bundle.json",
output_dir="runs/perception",
target="unix:/tmp/cev-sim.sock",
episode=EpisodeConfig(
observation_profile=MEASURED_PERCEPTION_OBSERVATION_PROFILE,
),
)Cameras select DEFAULT_GPU_SENSOR_BACKEND; LiDAR may use that backend or
DEFAULT_CPU_LIDAR_BACKEND. The profile preserves every measured-state/task
entry and adds uint8[height,width,4] camera values and
float32[elevation,azimuth,2] LiDAR range/incidence values. Depth,
semantic/instance IDs, detections, and other oracle products are never exposed
to the policy.
To connect without owning the supervisor:
env = CevSimEnv(
"run-bundle.json",
output_dir="runs/remote",
target="unix:/tmp/cev-sim.sock",
)PR 7 TCP is cleartext and unauthenticated. Use non-loopback TCP only behind an appropriate private network, firewall, VPN, or authenticated proxy. Closing an environment closes its batch and channel but never terminates an externally supplied supervisor.
CevSimVecEnv maps one supervisor batch to the SB3 VecEnv API. The
supervisor already provides one OS process per environment, so no Python
SubprocVecEnv wrapper is needed.
from stable_baselines3 import PPO
from cev_sim import EpisodeConfig, SupervisorLaunch
from cev_sim.sb3 import CevSimVecEnv
env = CevSimVecEnv(
"run-bundle.json",
8,
output_dir="runs/ppo",
launch=SupervisorLaunch(executable="cev-sim"),
episode=EpisodeConfig(max_episode_steps=1_000),
)
try:
PPO("MultiInputPolicy", env, n_steps=128, batch_size=256).learn(100_000)
finally:
env.close()SB3 terminal environments are finalized and reset before step_wait()
returns. The returned observation is the next episode's initial observation;
the real final observation is in info["terminal_observation"], normal
time-limit semantics are in info["TimeLimit.truncated"], and the finalized
JSON is in info["terminal_finalization"]. The complete finalization envelope
is retained in info["cev_sim.final_result"].
The client discovers capabilities using protocol 1.2 and negotiates up to 1.5.
Supervisors at protocol 1.5 advertise planning-oracle@1. Older supervisors
remain usable for measured-state and measured-perception. Start and goal
overrides are sent only after the negotiated minor is at least 5.
The client validates runtime name, profile schemas, backend versions, space layouts,
tensor names, dtype, shape, endianness, packed length,
boolean representation, and bounds. CPU/GPU LiDAR identities,
DEFAULT_CPU_LIDAR_BACKEND, DEFAULT_GPU_SENSOR_BACKEND, and
MEASURED_PERCEPTION_OBSERVATION_PROFILE are public package exports.
load_run_package(path) strictly streams the frozen USTAR profile, verifies
every entry and exact bundle digest, and returns LoadedRunPackage without
retaining asset bytes. SupervisorClient(..., package_inbox=...) admits it
through a context-managed AssetAdmission, which can be passed to
create_batch. Owned supervisors derive the inbox from their private socket
directory. External Unix supervisors require package_inbox; TCP, protocol
1.2/1.3, and supervisors without cev-sim.run-package@1 fail before staging.
Staging uses a 0600 temporary file, streaming hash, file fsync, atomic rename,
and directory fsync. Client shutdown releases outstanding admissions
best-effort. Ordinary Gymnasium/SB3 bundle inputs are unchanged.
On a protocol 1.2 Unix socket, tensors of at least 64 KiB use
grpc+unix+shared-memory-v1; smaller tensors stay inline. Python opens the
randomized region read-only without following symlinks where supported,
requires a private regular file owned by the current user, maps the whole
arena, validates the header before and after copying, and closes the mapping.
Copying is the default so observations and SB3 terminal observations survive
the next response generation. TCP and protocol 1.1 remain inline. Shared
references on another transport, invalid/stale generations, token/sequence or
tensor-spec mismatches, torn headers, truncated regions, and unsupported
discrete/nested layouts fail explicitly.
Malformed requests and infrastructure failures raise typed CevSimError
subclasses: CevSimConfigurationError, CevSimLaunchError,
CevSimCompatibilityError, CevSimTransportError,
CevSimSupervisorError, and CevSimEnvironmentError. A worker crash,
resource limit, timeout, or uncertain transport failure is never returned as
an RL truncation. A vector adapter fails closed after a partial batch error
because healthy peers may already have advanced.
Owned launch resolves an explicit executable, a cev-sim binary on PATH,
or bin/cev-sim.js from the checkout. A .js entrypoint is run with node
after checking Node.js >=22.22.2 <23. The command is not passed through a
shell or npx. close() first closes the batch, then the gRPC channel, asks
the supervisor process group to stop, escalates after bounded grace periods,
and removes its private Unix socket directory.
load_bundle retains received_bytes and their bundle_bytes_hash alongside
canonical_json and canonical_json_hash. An optional
expected_bundle_bytes_hash validates an externally supplied exact digest.
It rejects duplicate JSON keys and malformed UTF-8; v11 uses JCS, while v10
keeps its legacy serialization. These digests do not replace authoritative
JavaScript semantic verification.
Loaded v11 bundles declare identity_profile = "world-bound@2" and
required_protocol_minor = 3. The client checks both the protocol and the
advertised identity profile before creating a batch. Legacy v10 bundles remain
usable with protocol 1.2 supervisors, including explicitly launched local
supervisors. No local Python simulator or semantic hash implementation is added.