v2.1.0 — first 2.x release¶
This is the first 2.x release. Breaking and Migrating from 1.x compare with 1.0.0; the other sections, Fixed in particular, also record changes made during 2.x development, some of them in features that are new since 1.0.0.
Theme: a framework other people's algorithms can plug into, with a lifecycle that has been verified end to end on both backends.
Breaking¶
- Configs are package data under
opencda_marl/_bundled/configs/<family>/with three-layer resolution (default.yaml→<scenario>/base.yaml→ profile). Scenario references are<family>/<scenario>/<profile>; extra roots via--marl-config-root/OPENCDA_MARL_CONFIG_ROOTSmay hold only a profile and inherit the other layers. - Algorithms are registered, not listed. Built-ins and external
packages go through the same
register_algorithm(...)with declaredPolicyCapabilities; the hard-coded name table and everyif algorithm == …dispatch are gone. Plugins are discovered through theopencda_marl.pluginsentry-point group,OPENCDA_MARL_PLUGINS, orMARL.plugins;python -m opencda_marl.pluginslists what is registered and from where. - The engine imports no learners. The
Policycontract lives inopencda_marl.api.policy; the eight first-party learners live inopencda_marl/builtin/algorithms/, which the wheel loads through its ownopencda_marl.pluginsentry point — the same door any external package uses (OPENCDA_MARL_PLUGINS/MARL.pluginswork too, and acontrib/drop-in folder auto-registers modules for cloned checkouts). Direct imports change fromopencda_marl.core.marl.algorithms.sactoopencda_marl.builtin.algorithms.sac, and the 1.x keyssac/td3are nowmasac/matd3(see Migrating from 1.x); the checkpoint format is unchanged. The wheel is the framework: it bundles the SUMO benchmark (configs, lite replay, network) and installs anopencda-marlCLI (run/list), so installing it gives a runnable SUMO environment with no repository checkout. There is no PyPI release yet:pixi run wheelbuilds the wheel. - Sealed kernel, one plugin system. The package is four zones, and a
test enforces their boundaries:
_kernel/owns the complete lifecycle (coordinator, both backend envs, world/scenario management, adapters, traffic, sensing, policy machinery) and never imports a concrete plugin;api/is the public surface — the data contracts, thePolicycontract and everyregister_*door (from opencda_marl.api import …is the one import external packages need);plugins/is the single plugin system — entry-point groupopencda_marl.plugins,$OPENCDA_MARL_PLUGINS,MARL.plugins, the contrib drop-in — with cross-kind API-version rollback and a startup freeze (plugins load once; no hot reload; a run's behaviour set is fixed at initialize);builtin/is the framework's own plugin set, loaded through the same doors. Every part of the loop is a registrable kind: algorithm, planner, agent type, reward calculator, observation extractor, intent provider, context encoder, and the reward/objective/preference profiles —python -m opencda_marl.pluginslists all of them with sources. The 1.x import paths (opencda_marl.core.*,opencda_marl.coordinator) are gone. - Config composes. A profile may
include:fragment YAMLs (merged in order under the profile's own keys), and a component may register avalidatehook that checks its own config section at resolution — a misconfigured plugin fails at initialize with its own message. - Multi-objective is the reward axis, not a mode. Objectives and
preference profiles live in
opencda_marl/api/(objectives.py,preferences.py); the preference sampler and regimes inopencda_marl/_kernel/traffic/; the intent reward accumulator and the policy-intent bridge inopencda_marl/_kernel/policy/; the spawn observation inopencda_marl/api/intent_observation.py; the route-action trace writer inopencda_marl/telemetry/trace.py; and the dependency-free schema env inopencda_marl/envs/. Themomarl/config family name stays — it labels the path + speed example family. - Evaluation is store-first. The legacy per-step
.npzarchives, the matplotlib plotter and the cross-agent PNG reporter are gone; evaluation modefullmaps tosummarywith a warning. The manager mirrors its per-episode statistics and final summary into the metrics store, so evaluation numbers appear in the monitor next to training runs. In a learner run they go into the learner's own store (Eval/*tags, oneevaluation_episodeevent per episode,final_summaryin the run's metadata), so the run is one monitor entry. Only a run without a store of its own (a baseline agent, or a learner withoutputs.metrics_storedisabled) gets a separateevaluation/...run, labelled with the learner key or the agent type and the modetrainoreval. - The Qt desktop dashboard and
--guiare retired; the web monitor (pixi run monitor, started by every run) is the framework's one GUI. - The SUMO intersection profiles are named
route_*(one per learner and action space). -
The bundled rule-based baseline drives differently.
RuleBasedAgent(agents.agent_type: rule_based,marl/intersection/rule_based) changed in the ways listed here, so rule-based numbers from 1.x do not reproduce on this release; use 1.x for them.- Follower law. The leader gap is bumper to bumper (the bounding
boxes, 2.4 m per half-length without one),
minimum_distance_bufferis that physical gap (3.0 m default, was a 5 m centre-to-centre distance), and the target speed is the leader's speed plus a braking curve (closing_decel, 1 m/s²) that reaches zero at the safe distance: a follower closes on a queue, stops at the buffer and moves off with its leader.following_speed_bufferis retired; a profile that still sets it gets one warning that the key is no longer read. - Stop on conflict. With
junction_stop_on_conflict(on by default) a vehicle that must yield at a predicted trajectory conflict brakes on ajunction_stop_decelcurve tojunction_stop_buffermetres before the entry and holds until the conflict clears; inside the junction the slow-down rule still applies, so the box is never blocked. - Following cap in junction management. Stage 1 also applies the car-following speed, taking the lower cap, so yielding to crossing traffic never drives into a stopped leader.
- Abreast guard (
abreast_guard, on by default). A same-direction vehicle withinabreast_longitudinal_mwhose lateral distance is belowabreast_lateral_mmakes the vehicle that is behind (ties by actor id) cap its speed at the neighbour's lessabreast_drop_kmhuntil the pair is no longer alongside. Insideabreast_hard_ma neighbour always binds; beyond it, only while it closes laterally byabreast_closing_mper step and moves. In a pair at rest inside the hard zone the rear vehicle creeps ahead atabreast_creep_kmhand ends the pair. - Lane hold (
lane_hold, on by default). The vehicle keeps its current lane, and drops back, while the planner's next lane is occupied alongside, the way SUMO's lane-change model waits for a gap. - Merge order (
lane_hold_geometry_priority, on by default). In a standing merge the vehicle ahead goes first, along the pair's shared heading; actor ids break ties only within 0.5 m.falseorders by actor id alone, which can hold a merging vehicle with the larger id and the standing vehicle behind it in place for good. - Forward corridor. Another vehicle is a leader only when its projected body overlaps the ego's forward driving corridor, so a queue in an adjacent lane no longer stops a clear lane.
- Junction distance resolved in 2 m steps instead of 5 m.
- Branch choice at forks. The conflict predictor follows the branch
whose heading is closest to the other vehicle's heading extrapolated
by its yaw rate, instead of always the first branch of
waypoint.next().
The bundled profile states
lane_hold: trueandlane_hold_geometry_priority: trueexplicitly. - Follower law. The leader gap is bumper to bumper (the bounding
boxes, 2.4 m per half-length without one),
-
Some observation features changed meaning, so checkpoints that use them see different inputs. On CARLA,
position_x/position_yare absolute map coordinates in the common CARLA frame (they repeatedrel_x/rel_ybefore); SUMO's are unchanged. For replay vehicles with assigned junction geometry,rel_x/rel_yare the map position minus that junction's centre anddist_to_intersectionis the planar distance to it, on both backends (CARLA used a nearest-waypoint vector). SUMO spawn-time path decisions now see their neighbours in the CARLA frame (see Fixed), so SUMO path-head checkpoints trained earlier saw different distance and TTC features. - CARLA baseline agents run
max_episodes. Abehavior,vanillaorrule_basedrun on CARLA was forced to one episode; it now runsscenario.simulation.max_episodes, as on SUMO. Every bundled family base sets 500, so a baseline profile of your own that sets no count now runs 500 episodes: addmax_episodes: 1to keep one, as the bundledmarl/intersectionbaseline profiles do.
Migrating from 1.x¶
| 1.x | 2.1.0 |
|---|---|
python opencda.py -t rule_based --marl (flat configs/marl/<name>.yaml) |
-t marl/intersection/rule_based --marl (<family>/<scenario>/<profile>) |
configs/ at the repository root |
package data under opencda_marl/_bundled/configs/; your own profiles go under a --marl-config-root |
MARL.algorithm: sac / td3 with sac: / td3: sections |
masac / matd3 with masac: / matd3: sections; the old keys are rejected |
opencda_marl.core.marl.algorithms.*, opencda_marl.core.agents.* |
opencda_marl.builtin.algorithms.*, opencda_marl.builtin.agents.*; register through opencda_marl.api |
checkpoints/<run>/, recordings/ at the root |
load_checkpoint: ral/<run> (bundled in opencda_marl/_bundled/checkpoints/ral/) and opencda_marl/_bundled/recordings/; run outputs land under artifacts/ |
--gui, pixi run marl-quick-test-gui |
removed; the web monitor starts with every run |
pixi run marl-eval-agents, pixi run fix-tensorboard |
removed (evaluation is a run with training_mode: false) |
pixi run tensorboard / tb on runs/, event files under runs/<class>/<timestamp>/ (sac, td3, qlearning, mappo, dqn) |
on artifacts/, which holds every family's event files under <outputs.tensorboard.dir>/<key>/<run>/, named by the registered key: sac becomes masac, td3 becomes matd3 and qlearning becomes q_learning; mappo and dqn keep their names, and the new keys pcmasac, pcmatd3 and pcmappo get their own directories. Event files written by 1.x stay under runs/ |
pixi run cda-platoon-town06 |
runs platoon_joining_town06_carla |
| a CARLA baseline profile ran one episode whatever it set | it runs max_episodes (500 from a bundled family base); set max_episodes: 1 for one |
Lifecycle¶
- One episode loop for CARLA and SUMO; the coordinator closes evaluation episodes explicitly on both backends (the evaluation manager previously never fired on SUMO and fired a step early on CARLA).
- CARLA world reloads carry the full learner state for every family — networks, optimizers, encoders, replay / rollout buffers, normalizers, route-intent providers.
- Resumed runs keep their episode numbering and checkpoint history
(
training_metadata.json); a configured checkpoint that fails to load aborts instead of scoring random weights. - SUMO: first episode finishes, every episode saves like CARLA, empty steps
are counted, one consistent SUMO install (libsumo pinned to the
environment's SUMO), torch / numpy / random seeded from
world.seed. MARL.pluginsis loaded by the coordinator at initialize, before either backend resolves a planner, an intent provider or an agent type, and for runs without a learner too; each plugin is imported once per process.- Metrics archives flush at close; record-mode traffic writes the replay file once.
- OpenDRIVE worlds skip pedestrian navigation generation by default
(
world.pedestrian_navigation: truerestores it): rebuilding the walker navmesh on every world reload left navigation-build jobs running beside training. Road geometry and vehicle physics are unchanged.
Learners¶
- MAPPO / PCMAPPO: per-trajectory GAE,
n_stepsrollouts, critic bootstrap, exact PPO ratio, opt-in value normalization, loaded checkpoints skip the random warmup,training_stepadvances per update. - SAC family: one packed LSTM encode and one actor forward per update, one
device readback, fused Polyak; opt-in CUDA-graph update (
--cuda-graph-updatefrom the launcher) that fails fast on capture errors. - TD3: batched context storage, warmup that ends on CARLA, per-piece checkpoint loading.
- All families share one neighbour ordering and one encode path; acting and
learning contexts are identical by construction. The neighbour-context
encoder is a registrable kind (
MARL.<algo>.context_encoder, defaultlstm), replaceable without touching a learner. - Single-agent learners (
multi_agent=False) declare whatselect_actionreturns through the class attributereturns_action_index:False(theBaseAlgorithmdefault, and DQN) means a target speed in km/h,True(tabular Q-learning) an index into its ownspeed_actionstable.
Infrastructure¶
- Metrics store + monitor: every run writes a per-run SQLite store
(
artifacts/metrics/<algo>/<run>/metrics.sqlite, where<algo>is the registered algorithm key); starting a simulation starts the monitor — a detached web service that outlives the run and is reused by later runs (outputs.monitor, a mapping or a bare bool), serving a dependency-free dashboard (static frontend: no build step) that overlays runs, smooths, lists episode summaries and draws the live traffic view. One monitor serves one artifacts root: a run reuses only a monitor of its own root (GET /api/rootreports the served directory and the servicepid), and when the port serves another root it takes the next one, up toport + 7. The liveness probe is a plain keep-alive request and the port is bound exclusively, so two monitors can no longer share a port on Windows. To stop the service, end thepidit reports; on Windows stop it before deleting or renaming its root, which it holds as its working directory andmonitor.log. TensorBoard event files are still written by default and are optional. - The monitor answers only loopback names when bound to loopback. A
monitor on
127.0.0.1(the default) serves only requests whoseHostis a loopback name (127.0.0.1,localhost,::1,*.localhost), which blocks DNS-rebinding reads from a web page.ssh -L 8765:127.0.0.1:8765is unaffected. A proxy that forwards under its own host name (Jupyter server-proxy, code-server/proxy, Open OnDemand, dev tunnels) gets a 403 naming the fix: start the monitor withpython -m opencda_marl.telemetry.monitor --root <root> --allow-host NAME(repeatable); later runs on the same root reuse it.opencda_marl.telemetry.monitor.ensure_service(root, host, port, allow_hosts=())passes the names to a service it starts. - Strict JSON everywhere. The store skips a non-finite scalar and
counts it in the run's meta (
skipped_nonfinite) instead of failing the whole flush batch; episode, event and meta payloads and every monitor response are strict JSON (a non-finite value becomesnull). - Experiment matrix:
--parallel Nslots with one CARLA server each,--cuda-visible-devices/--carla-gpusspreading, host-binary or Docker server restarts,--resume, process-tree timeouts, non-zero exit on any failed run, an evaluation launcher, a checkpoint-eval route and analyzers that no longer confuse evaluation slugs with training runs. The launcher is wired to the bundledmomarl/intersectionfamily — one learner (PCMAPPO) under every built-in path provider, eight profiles in all with the SUMOroute_*set — and the evaluation slug pattern is derived from its condition table, so the two cannot drift apart. Profiles outside the bundled family run directly throughopencda.py --marl-config-root, not through this launcher. The default--carla-restart-mode processstarts and restarts CARLA itself, so outside--dry-runit stops before the first run unlessCARLA_ROOT/CARLA_BINor--carla-root/--carla-binis set (no install path is guessed); servers managed elsewhere take--carla-restart-mode noneordocker.--clean-planned-slugsalso removes each slug's monitor store, so a relaunch no longer appends to the old run. A cell slug is<prefix><map>_<condition>_<regime>_seed<N>with--slug-prefixdefaulting tomatrix_; a run launched under another prefix, an earlier default included, keeps it (the first part of its directory names underartifacts/checkpoints/matrix/), so pass it with--slug-prefixto resume or evaluate it. Continuation cells (continue_*) load their source from the same prefix unless--continue-source-prefixnames another run. The analyzers (analyze_matrix_results.py,analyze_matrix_eval.py,inspect_route_diversity.py) andrun_matrix_eval.pyread cells under any prefix, keep the prefix in every row and comparison group, warn when one root holds several runs, and take--slug-prefixto read one.scripts/run_carla_checkpoint_eval.pywrites toartifacts/eval_runs/by default. - Schematic traffic view: headless runs publish lane geometry and
live vehicle poses (throttled, atomic JSON next to the metrics store;
CARLA poses come from the step's existing world snapshot — no extra
server round trips), and the monitor draws a SUMO-GUI-style live view —
lanes as lines, RL agents as oriented triangles — replacing the need
for CARLA rendering when watching a run. The geometry is written before
the first state and retried until it succeeds;
role: agentmarks a live vehicle of the learning population (background vehicles aretraffic);outputs.traffic_viewaccepts a bare bool. - Coordinate frames come from the map:
netOffsetis read from the SUMO network header instead of three hardcoded constants, and every SUMO-side position goes through oneCoordinateBridge.runtime.coordinate_landmarks(or a<net>_landmarks.jsonbeside the network) decides the convention from junction landmarks, which matters for CARLA town exports (they mirror CARLA's y axis); a map that ships no landmarks keeps the default. This fixes a real defect — SUMO's distance-to-junction subtracted a CARLA-frame centre from a SUMO-frame position, leaving the whole offset (~140 m on the bundled map) in the result. Junction centres are per active junction, and crossing order is reported per junction. - Maps on both backends:
scripts/convert_opendrive_maps.pyderives SUMO networks from OpenDRIVE (opencda_marl/assets/maps/), keeping CARLA's junction ids as SUMO node ids; a scenario pointed at an.xodrbuilds a building-free CARLA world on the same roads. SUMO takes its network from the.sumocfgatruntime.sumo_cfg, not fromscenario.map, and reads a derived town network in CARLA's frame only with the junction landmarks of the entry above; no.sumocfgnames the derived town networks inopencda_marl/assets/maps/, and no landmarks ship for any town (see Run Experiments). The converter records repository- or$CARLA_ROOT-relative paths in its outputs. - The step trace runs on either backend, keys on the environment's own
counters, and records
population/learningper vehicle, so offline metrics can separate the learner's vehicles from background traffic. CARLA collision events carry the other actor's id and type, the event frame and the impulse inmetadata. An agent may expose a safety-filter state throughshield_state(); its fields (applied_speed,shield_active,shield_stage,lane_hold,merge_priority_*) reach the trace. The hook is read on the CARLA vehicle path only, so on SUMO these fields are null: SUMO builds no driver agent. The bundledRuleBasedAgentimplements it on CARLA:lane_hold,merge_priority_releaseandmerge_priority_overriderecord each step's lane hold and merge order,merge_priority_enabledthe configuredlane_hold_geometry_priorityswitch, andapplied_speedthe target its rules gave the controller;shield_activeandshield_stagestay null, as it has no safety filter. The other bundled agents (marl,vanilla,behavior) do not implement it, so their vehicles, the learner'smarlvehicles included, record null in all seven fields. - PettingZoo
ParallelEnvport of the scenarios for external RL stacks.from_scenarioresolves a reference exactly as the command line does (aconfigs/root under the working directory,OPENCDA_MARL_CONFIG_ROOTS, then the bundled configs) and composesinclude:;MARL.external.featurescan come from the profile or throughoverrides, and the observation width follows those features, not the profile'sMARL.state_dim. Loop on the newenv.episode_over, not onenv.agents, which is empty before the first spawn. An agent you stop sending actions for is handed back to SUMO's car-following model; one never commanded keeps its departure speed. - The wheel declares what it imports, and is built clean. The base
dependencies carry everything the SUMO backend needs:
eclipse-sumobundles the simulator and itstraci/sumolibclients, which the engine imports from the package's ownsumo/tools(its SUMO resolver puts that first onsys.path).libsumois an optional accelerator; without it the engine falls back totraciwith one warning, which names the matchingpip install libsumo==<SUMO version>, and a SUMO step runs about 13× slower. The vendored OpenCDA core ships in the wheel, withnetworkx, so every engine module imports.[carla]adds the libraries for CARLA scenarios run from a repository checkout (python opencda.py -t <ref> --marl, which reads the maps from the working tree); the wheel's own CLI and env port drive SUMO only, andcarlaitself comes with your simulator.[ports]adds PettingZoo / Gymnasium.pixi run wheelbuilds from a cleanbuild/and refuses a wheel with a member that has no source file;test_wheel_contents.pywalks the engine's imports and fails on any that is declared nowhere. pixi run verifyruns the verification battery. It now checks thattraci/sumolibimport from theeclipse-sumopackage alone (withSUMO_HOMEunset and the separate distributions hidden). Neither smoke run starts a detached monitor: the SUMO run turns it off, and the CARLA run uses the rule-based baseline from its own profile, which writes under the verify root.- pixi:
tensorboard/tbserve--logdir=artifacts. On win-64, activation setsSUMO_HOMEto the environment's owneclipse-sumopackage, andCARLA_ROOT/CARLA_HOME/CARLA_BINdefault to the exampleD:\Applications\CARLA_0.9.16location; activation overrides shell values, so edit those lines inpixi.tomlwhen CARLA lives elsewhere. The alternate manifests independencies/pixi/dropmarl-quick-test-gui,marl-eval-agentsandpyside6; theirmarl-quick-testrunsmarl/intersection/rule_based. The Python 3.10 backup manifest no longer setsSUMO_HOME; set it yourself there. - Linux: solvable
linux-64lock, CARLA servers survive the pixi activation environment, measured multi-GPU throughput documented.
Mixed autonomy and path decisions¶
- Per-vehicle population (
agents.population: {mix, seed}): each spawned vehicle draws its agent type from a seeded pattern, so the shares are exact over every window ofresolution(default 100) vehicles and reproducible per seed; a shorter prefix only approximates them, and live flows draw flow by flow. The learner commands — speed and path actions alike, at spawn and at the approach decision zone — and learns from its own population (agents.agent_type) only; background vehicles keep their demand routes and stay visible in the traffic context. A mix must name that learning population: on CARLA a mix without it, or with a positive share for a name that is not a registered agent type (matched exactly, case-sensitive), is rejected at construction, and a zero learner share warns. Every built-in agent type reads its own section overvehicle.behavior, so a mix namingrule_basedorvanillaworks on a profile without anagents.rule_based/agents.vanillasection. On SUMO only the learner's population gets the RL runtime mode and a commanded speed; every other name is background traffic that keeps SUMO's default safeguards and car-following model, with any per-vehicle driver parameters from the recording (metadata.driver) applied. - The
agentkind is resolved on CARLA only, one agent per spawned vehicle; on SUMO a population key is a label and a registered agent factory never runs.BASELINE_AGENT_TYPES(behavior,vanilla,rule_based) is closed: a registered agent type is not treated as a no-learner baseline. - What one path action means is a registered provider
(
MARL.action_space.path.provider); the built-in semantic action (left / straight / right / keep) is the example, and a provider declares its own alternatives, mask and head width throughopencda_marl.api.path_action. A registered provider runs on both backends: the pipeline wires the provider the configured mode names (rebuilt through the registry on a CARLA world reload), and no kernel apply or log site reaches into the built-in decision type. - A decision zone (
MARL.action_space.path.decision_zone: {trigger: spawn | approach, distance_m}) takes the one-shot path decision at spawn or on approaching the junction. - A committed-exit-lane observation channel
(
MARL.<algo>.features.committed_exit_lanes: <width>, off by default) gives each agent, per exit lane, the number of other vehicles at the same junction that have already committed to it; the state width changes with it. - Spawn metadata reaches the agent factory as
config["spawn_metadata"], so a plugin agent can be per-vehicle and reproducible from a recording. An agent may also expose a safety-filter state throughshield_state()(the speed it applied, whether the filter intervened); on CARLA the vehicle adapter merges those fields into the vehicle's observation and the trace records them. The hook is read on the CARLA vehicle path only; the bundledRuleBasedAgentimplements it for its lane hold and merge order. - The safety API hands agents the step's TTC and kinematics.
constant_velocity_ttcis the world snapshot's quadratic as a public function,min_ttc_to_othersfolds it over the kinematics of every other vehicle, andpublished_kinematicsreturns what the agent manager published for the current step, so an agent that acts on time to collision uses the number the learner and the metrics see without a second round of simulator queries. - Competition metrics follow the objective, not a built-in profile name. Any run with an objective spec (or a reward profile whose name says competitive) records the per-agent competition metadata and the terminal records behind the route-intent and preference summaries, so an objective registered through the plugin door keeps them.
Performance¶
- CARLA: one world snapshot per step replaces O(N²) server round trips; the MARL collision sensor is installed at construction; the duplicate safety IMU, the disabled-V2X neighbour search and several per-step costs with no consumer are gone.
- Shared extractor caches, once-per-spec reward metadata, once-per-tick vehicle updates.
- State extraction reads no config at step time and runs once per
observation dict. Profiled on one SUMO episode (2,400 steps, ~14
vehicles, libsumo): the simulator was 6% of the step, while the feature
extractor walked the configured mapping -- an OmegaConf
DictConfig-- for every feature of every agent (31% of the step in the config resolver) and ran three times per step on identical inputs, because the manager's identity memo keyed on a learning view that a mixed population rebuilt on every call and the SUMO step handed the learner a copy of the read it had just extracted from. The feature list is now resolved once into a plan over plain values, the learning view is stable across the calls of a step, and the SUMO step reuses its post-step read (outside readers still get a copy); neighbour settings and exit-lane rows are built once per step rather than per vehicle. The same episode reproduces bit for bit at 27 ms/step under the profiler instead of 62. - Neighbour blocks in one pass per snapshot. The per-ego neighbour search, sort and TTC solves were the next 10% of a SUMO step (34k calls per episode). The world snapshot now computes every vehicle's k-slot block and minimum TTC at once -- same selection (k nearest within the radius, ascending distance, ties by row), same normalisation, same constant-velocity TTC -- and caches it per parameter set, so the per-ego calls of both backends index into it; the scalar implementation stays as the reference the batched pass is tested against on random scenes. Row lookup by actor id is a dict, not an array scan. The same episode reproduces bit for bit at 17 ms/step under the profiler instead of 27.
- A SUMO vehicle's population is resolved once per episode. The observation builder re-derived it on every step for every vehicle -- a config read through the resolver plus a spawn-event lookup, 34k calls and 1.9 s per 2,400-step episode -- for an answer the spawn event fixes for the vehicle's whole life. The episode state now remembers it (0.1 s), a new episode forgets it, and a vehicle seen before its spawn event is registered is asked again rather than remembered with the default.
- The learner crosses the device boundary less often. Its per-agent work was already batched (one context sort, one LSTM encode, one actor and one critic pass per step); what remained at the boundary were two host-to-device copies of the same table rows, three or four device-to-host syncs for the step's outputs, and a rollout re-stacked and re-uploaded for every PPO epoch. The ego rows are now read from the sequence tensor already on the device, the outputs come back in one transfer, and the rollout's device tensors are built once per fill. Numerics are unchanged (the same episode reproduces bit for bit); the gain is modest -- transfers halved, the PPO update about 20% faster -- because at a dozen agents the GPU is launch-bound: a full acting step measures 0.74 ms on CPU against 1.34 ms on CUDA with the real network shapes. Acting on a CPU replica is the remaining lever there, and a design decision rather than a fix.
- Learners act on a CPU replica and train on the GPU. With
acting_device: auto(the default) a learner training on CUDA acts on CPU copies of its actor, critic and context encoder, refreshed lazily after every optimiser step or checkpoint load through one pinned flat copy, with the value normalisers mirrored; PCMAPPO, MAPPO, PCMASAC and TD3 opt in. The replica caps torch's intra-op pool (acting_threads, default at most 8) because a tiny op pays to wake every thread. Back-to-back on the same machine state, one SUMO PCMAPPO episode under cProfile: 47–49 s acting on the GPU, 41 s on the replica, the acting path itself 11.5 s → 6.6 s. Runs are no longer bit-identical to GPU acting;acting_device: trainrestores it.
Fixed¶
- Platooning scenarios ran the plain behavior agent.
VehicleManagertested for'platoon'while the platooning scenarios pass['platooning'], so no platoon member or joining vehicle got the platooning agent. It tests'platooning'again, as stock OpenCDA does. - The bundled DQN baseline drove every vehicle at the top speed. The
manager read DQN's returned speed as an index into DQN's own
speed_actionstable, so 15, 30, 45 and 60 km/h all clamped to the last entry. The manager now follows the policy'sreturns_action_indexdeclaration (see Learners). The single-agent speed clamp also accepts aspeed_actionstable held as a numpy array, which failed every step before. - DQN and tabular Q-learning train again. The shared training strategy
indexed their per-agent states as multi-agent dicts and called a
six-argument
store_transition; single-agent learners, and any registered policy withmulti_agent=False, now get their own flow. MARL.pluginsreached too few runs. It loaded only when a learner was configured, and on CARLA only after the scenario had resolved its planner and intent provider, so a component registered only there was an unknown key. It now loads at initialize for every run (see Lifecycle), and loading the same plugin again is a no-op instead of reporting a working.pyplugin as failed.- SUMO ignored
MARL.components.reward_calculator. It read a top-levelcomponentsblock and silently ran the default calculator; it now selects from the MARL block, as CARLA does. - The SUMO evaluation miscounted its vehicles. A finished vehicle
still counted as active, and a vehicle still in SUMO's insertion queue at
the end of an episode dropped out of the evaluation's total. A queued
vehicle is now an unfinished route (a timeout) in
Eval/*andepisodes_summary.csv, matching the training summary and CARLA's never-entered spawns.SumoMARLEnv.pending_spawn_count()counts only spawn events not yet due (CARLA'spending_spawns), andSumoMARLEnv.queued_insertions()lists the queued vehicles. - A SUMO learner that stopped receiving commands kept its last speed.
TraCI's
setSpeedpersists, so a vehicle the policy stopped commanding (a warmup or no-action step, an external loop that sent nothing for it) held its last commanded speed. It is now handed back to SUMO's car-following model on the commanded → un-commanded transition; a vehicle never commanded keeps the departure speed the spawner set. - SUMO spawn-time path decisions mixed coordinate frames. The spawning ego is built in the CARLA frame, but its neighbours were in SUMO's, so their distance and TTC features carried the network offset (and the mirror on a y-flipped map). Every neighbour now goes through the coordinate bridge, position and velocity heading alike.
- A SUMO vehicle queued for insertion survived the episode reset. SUMO
holds a vehicle whose departure lane has no room in an insertion queue
that
traci.vehicle.getIDListdoes not list, so the reset left it there and it entered the next episode with the old route, blocking its own id ("vehicle ... to add already exists"). A short approach edge admits one vehicle at a time, so under demand this carried a sizeable share of an episode's spawns into the next. The reset now reads the queue (SUMO's pending list, completed from the spawner's registry), counts those vehicles as unfinished routes of the episode that spawned them, and removes them. - A SUMO trip ended at the end of an edge, not at its destination. The
waypoint shim converted a stored position back with the bundled
intersection's constant offset (not the published bridge) and clamped
next()at the lane's end, so on a calibrated map the planner's walk past a junction exit stopped at the first edge end and a route's destination sat wherever that edge ended. The spawner ranked destination edges by distance to their vertices, so a point halfway along a long edge lost to any short edge nearby; and SUMO's own arrival, at the end of the target edge, decided success, so a trip's length depended on how the network splits its edges.next()now measures progress through the bridge and continues onto the linked lane,previous()mirrors it,get_left_lane()/get_right_lane()return the adjacent lane at the same fraction of its length, candidate edges rank by distance to the lane, and the environment ends a trip withinworld.arrival_radius_m(5 m) of its destination point, as CARLA does, with SUMO's arrival as the fallback. The SUMO twin's route table reproduces the CARLA planner's spawn and destination points, and a replayed event keeps the recording's destination when a route decision keeps its demand route. WorldSnapshot.captureskips an unreadable actor instead of ending the run. Its error path referred to a logger the module never imported, so a CARLA server that stopped answering for one call raisedNameErrorinstead of costing one actor for one step.- The abreast guard told no neighbours apart. It read
.id, which the perception's obstacle vehicles do not have, so every neighbour was id 0: the tie-break always yielded and all neighbours shared one closing memory. It readscarla_idnow. - The branch choice at forks never saw a turning obstacle. Perception
obstacles carry no angular velocity, so the yaw rate read as zero and
every vehicle was predicted straight on. The rate is now estimated from
each obstacle's heading change between steps over the world's fixed
delta; an unmatched detection (
carla_id-1), or an id two objects carry in one step, gets no rate and is predicted straight on. - A population mix naming
rule_basedorvanillacrashed at the first such spawn on any profile without that agent's own section; both now read their section overvehicle.behavior. - A resumed external policy restarted its monitor counters. A
non-
BaseAlgorithmpolicy's metrics store now continues its episode and update-step counters when a resumed run reopens the same store. - Store discovery missed some run directories. Read-only opens work for
run directories containing
#,%or spaces, for stores under a mapped network drive (no longer resolved to UNC) and for UNC roots; such runs were missing from discovery and the monitor. An unreadable store is logged once at WARNING. - The monitor page missed tags that appear mid-run.
Loss/*after the warmup andEval/*now show up without reselecting the run, and the default selection extends to them; tags are re-listed at most every 10 s, and only when a selected run wrote scalars. A failed refresh showsrefresh failed: ...in the status line and polling continues. outputs.monitor: falsestarted the monitor anyway andoutputs.monitor: truestopped initialize; both sections now take a bare bool.- A learner run appeared twice in the monitor. The evaluation manager
opened a store of its own next to the learner's, with no mode in its
metadata (
?in the run list). A learner's evaluation numbers now go into the learner's store (see Evaluation is store-first under Breaking), and a separate evaluation run is labelled with the learner key or the agent type andtrainoreval. - MASAC and PCMASAC runs shared one
sac/directory. The metrics store directory, the run'salgorithmlabel and the TensorBoard event directory came from the learner class, which the two keys share. Both writers now name the run by the registered algorithm key, for every learner:<metrics log_dir>/<key>/<run>/and<tensorboard log_dir>/<key>/<run>/(the TensorBoard names that change from 1.x are listed in Migrating from 1.x). Only a learner built withoutMARLManagerfalls back to the class-derived name. A baretensorboard: true|falseormetrics_store: true|false, inoutputsor in an algorithm block, is theenabledswitch and keeps the other settings (the TensorBoard one wrote to./runsbefore), and an empty block reads as absent instead of failing the learner. - Built-in learners logged no timeouts to the store. Their episode
summaries carried the success and collision rates but not
timeout_rate,successes,collisionsortimeouts, which external policies logged, so the monitor's columns depended on the learner's base class. Every learner logs all of them now. - The episode table cut its most useful columns. It kept the first 12
extra columns in alphabetical order, which
intent_*diagnostics could fill. The outcome, throughput, TTC and near-miss columns now come first, then thereward_component_*columns, then the rest alphabetically, up to 24 extra columns. - Counts in the episode table printed as decimals. A count of 0
printed
0.00, and a count from 1 to 999 kept three decimals (2.000). A column whose shown values are all whole numbers (counts, episode and step numbers) now prints integers. Any other column keeps the page's usual precision on every row, so a reward of 3 shown next to 2.537 reads3.000. The table makes this choice again on every refresh, over the rows it shows. A missing or non-finite value prints a dash. - A finished run's traffic view looked live. The last frame stayed on
screen with only an age label. A frame with no newer one for
max(3 s, 3 × interval)is now dimmed and labelledstale, and a frame from a run whose store was closed after it is labelledrun ended.traffic_state.jsongains the publishinterval, and a store's metadata gainsclosed_atwhen its writer closes it (removed when a resumed run reopens the store). - MASAC logged preference tags that were always zero. The
SAC/pref_shift_*tags are logged only under an internal preference mode (internalpref,internalpref_sep) andSAC/pref_sep_*only underinternalpref_sep; elsewhere (plain MASAC,pref_mode: direct) they were constant zeros. - CARLA baseline agents ignored
max_episodes. Abehavior,vanillaorrule_basedrun was forced to one episode. It now honoursscenario.simulation.max_episodes, as SUMO did (see Breaking for a baseline profile that sets no count). - SUMO runs carried no scenario label. The
-tscenario reference now labels SUMO runs in the store metadata (scenario), as it did CARLA runs. - Another SUMO install could still supply the bindings. With the
environment's
eclipse-sumopresent, the SUMO resolver removed only$SUMO_HOME/toolsfromsys.path, so aPYTHONPATHentry for a system SUMO'stoolsstayed behind the package's. The resolver now removes every other SUMOtoolsdirectory:$SUMO_HOME/tools, and any directory namedtoolsthat ships bothtraci/andsumolib/. A project's owntools/without those bindings is kept. - A CARLA world build that timed out ended the run. On a loaded
machine, generating an OpenDRIVE world or loading a town could outlast
the client timeout. The build is now retried once with twice
world.timeout, and the client timeout is restored afterwards. A second time-out stops the run with a message that says to check the server or raiseworld.timeout; any other error is raised at once, as before. A town load that timed out is no longer reported as a missing map. - The evaluation summary printed
±as mojibake on a console that is not UTF-8; it prints+/-. - The RAL evaluation profiles load the bundled kit. The
marl/intersectionevaluation profiles nameload_checkpoint: ral/<run>, matchingcheckpoints/ral/manifest.json;mappo_h128,td3_43dandtd3_lstm256load their own entries instead of copies of other runs' paths, andtd3_44dis a training profile (no bundled weights). sumo-marl/intersection/base.yamlis headless (world.sumo_gui: false);sac.yamlstill opens the SUMO window.- A section left empty in a profile erased the family's.
outputs:with nothing under it parses as null, and the merge replaced the lower layer's whole block with it, at any depth and ininclude:fragments too; the run then crashed on its first read of the section. An empty key now keeps the lower layer's mapping. Any other scalar, a baretrue/falseincluded, still replaces it; a mapping still merges into it, and a list over it is still an error; null over a scalar or a list still sets it. A top-level section (scenario,world,runtime,agents,outputs,MARL) that is still null after the merge, because no layer defines it as a mapping, reads as an empty mapping: the coordinator, theopencda-marlCLI and the PettingZoo port'sfrom_scenarionormalise it before anything reads it. The matrix launcher's check for whether a cell needs the perception model (--apply_ml) now merges the layers the same way, a cell'sinclude:fragments included, so neither an empty section nor a fragment hides whether perception is on. - Checkpoints carried the run configuration of the machine that trained
them. Every built-in learner pickled its whole config into each
checkpoint: the
tensorboardandmetrics_storeblocks (log directories underoutputs.root_dir, the run name) and, because a nested OmegaConf node keeps a reference to the configuration it came from, the entire run configuration, every section withoutputs.root_diramong them. DQN also stored its speed list as such a node. The built-in learners now savecheckpoint_config(), which everyBaseAlgorithminherits fromTrainingLogger: plain values, without the run-output blocks, and with any value that is an absolute path replaced by<absolute path removed>. Interpolations inside nested blocks are kept as written. The top-level values of the learner's section reach it already resolved, so one that resolves to an absolute path is replaced, but a path inside a longer string (run@${outputs.root_dir}) is saved as it resolved. Hyperparameters are kept, and DQN saves its speed list as a plain list. Nothing reads the saved config back, so checkpoints written earlier, which still carry all of it, load and resume unchanged. The bundled baseline checkpoints are unchanged. A learner of your own should save the same (Add an Algorithm). - Matrix cells launched with a capitalised map name were invisible
downstream. The launchers accepted
--maps Custom(validation lowercased it) but wrotematrix_Custom_*cells, whichrun_matrix_eval.pyand the analyzers never matched.launch_matrix.py,launch_eval_matrix.pyandlaunch_distributed_matrix.pynow normalise--maps, and--continue-source-map/--checkpoint-mapwhere they take one, to the map profile key (matrix_common.canonical_map/canonical_maps), and plan each map once however it is spelt or repeated. An unknown--continue-source-mapor--checkpoint-mapnow stops the launcher up front. Cells an earlier run already wrote asmatrix_Custom_*keep that name: rename the directories named after their slugs tomatrix_custom_*(or re-run the cells) to bring them intorun_matrix_eval.pyand the analyzers and, on a case-sensitive filesystem, back into the evaluation launcher, continuation and--resume.
Known issues¶
- SUMO spawn-time path decisions see the ego heading +x. A spawn
transform generated on SUMO carries yaw 0, so the ego of a spawn-time
path decision moves along +x whatever its lane, and its TTC and closing
features against its neighbours are wrong. This covers SUMO runs with
scenario.traffic.mode: liveorrecord, and replays of recordings made on SUMO — includinglite_2min_SUMO.json, which the bundledsumo-marlprofiles replay — because a recording carries no backend tag. Decisions taken at the approach zone read the live vehicle, and replays of recordings made on CARLA carry each vehicle's own yaw; both are unaffected.make_spawn_event_proxy(..., heading_rad=)andspawn_lane_heading_radinopencda_marl/_kernel/traffic/sumo_route_intent.pyare the seam for the fix. SUMO path-head checkpoints trained in live or record mode are not comparable with ones trained after it. OpenCDAMARLParallelEnv.from_scenariodrives the SUMO backend only.- A long CARLA evaluation can die at an automatic world reset. The world
manager reloads the world when a step gets 1.5x slower than the episode's
baseline, and the reload uses the fixed
world.timeoutwithout a retry. On a windowed server a reload takes 10 to 35 s, so a run of a few tens of episodes can end with a time-out at the reload, followed by a time-out in the nextworld.tick()or a nativeAssertion failed: _episode != nullptrfrom the CARLA client. A failed reload does not stop the run cleanly. Raiseworld.timeoutin the profile, or run a headless server (-RenderOffScreen), until this is fixed. Smoke runs andpixi run verify --fullare too short to reach it. - The first build of the bundled intersection world can exceed the
default time-out.
generate_opendrive_worldtook 23 to 37 s on a windowed server; the defaults are 20 s (MARL profiles) and 10 s (OpenCDA scenarios) with one retry, and the retry waits behind the still-running first request, so it fails whenever one build takes more than about 30 s. Setworld: {timeout: 90.0}in the profile. - Sensors are not destroyed at an episode reset or a world reload. The
CARLA client logs
sensor object went out of the scope but the sensor is still alive in the simulationfor the GNSS, IMU and collision sensors, and they accumulate on the server during long runs. --apply_mlfails on Windows in the standard OpenCDA path. The ML manager importstorchaftercv2,open3dormatplotlibhave been loaded, andimport torchthen fails withWinError 127(shm.dll). Importtorchfirst, for examplepython -c "import torch, sys, runpy; sys.argv = ['opencda.py', '-t', '<scenario>', '--apply_ml']; runpy.run_path('opencda.py', run_name='__main__')".platoon_joining_2lanefree_carladoes not exercise the joining branches. The platoon members and the merging vehicle now run the platooning agent (the fix under Fixed), but the merging vehicle reaches the main road 80 to 125 m behind the platoon and about 8 km/h slower, so it never comes within the 35 m communication range and the frontal, cut-in and back joining code is not reached. The scenario file is the one the upstream repository ships.- Console noise from the OpenCDA scenarios. On
single_2lanefree_carlathe safety manager printsSafety Warning from the safety manager: {... 'offroad': True ...}at every tick of the whole route, and OpenCV or Open3D windows can printqt.qpa.screen: Unable to open monitor interfaceandQThreadStorage: entry N destroyed before end of threadon a multi-monitor Windows setup. The run itself completes.
Docs¶
- The portal is Getting Started (Installation, Quick Start, CARLA Version Support), Guide (Architecture, Run Experiments, Training Throughput, Customize — one worked page per replaceable kind) and Reference (PCMA, the OpenCDA patch ledger, Contributing, this changelog). The API reference was retired; the code and its tests are the reference.
- The Agent Workflow page and the Code Index are what coding agents read before acting; the PR template is the review contract.