Architecture and Roadmap
Status: Proposal (open for discussion)
This document describes the architecture this project is moving toward, and the order in which we plan to get there. It is meant for contributors and for anyone who wants to understand how the simulations are put together.
Goal
Learners should be able to study the whole navigation of an autonomous vehicle through simulation: sensing, localization, perception, mapping, planning and control working together. They should be able to swap algorithms and change parameters at any stage (for example EKF → particle filter, Pure Pursuit → MPC, kinematic → dynamic vehicle model) and compare the results on the same scenario.
Today the project covers localization, sensing, global mapping, global path planning and a kinematic vehicle model, mostly as separate demos. Next, we want to add object tracking, local path planning, local mapping, obstacle avoidance, a dynamic vehicle model and lane changes.
Design principles
Readable first. This is learning material. The flow “sense → estimate → perceive → plan → control → move” must be readable top to bottom in one short function, with no hidden framework.
Small, explicit interfaces. Each stage has a minimal interface (a few methods, documented with
typing.Protocol). Any algorithm that implements it can be plugged in.Truth and estimate are separate. The simulator owns the true state of the world. The autonomy stack only sees sensor measurements and its own estimates. This is what makes estimation errors visible and measurable.
Simulation, autonomy and visualization are separate. The simulator advances time, the stack decides, and the visualizer only observes. Simulations can then run without drawing (fast tests and comparisons).
Units and frames are explicit. Names carry units (
_m,_rad,_mps), and data states which frame it is in (global, vehicle or sensor).The system grows one stage at a time. A learner starts from a vehicle that only follows given commands and adds one stage at a time until it is a full autonomous driving system. Adding a stage doesn’t require changing the code written for the earlier ones.
Algorithms work outside the simulator. The core of each algorithm depends only on NumPy, SciPy and the standard library. It takes and returns plain data (arrays and numbers) and doesn’t know about the simulator,
Stateor drawing. This keeps the code close to the equations in the book, and lets any other Python project (an application, research code, a ROS 2 node, …) import and call it.
Current architecture and its limits
Today, FourWheelsVehicle holds both the physical vehicle and the whole processing chain, and the animation advances time:
flowchart TB
Vis["GlobalXYVisualizer (animation advances time)"] --> Veh["FourWheelsVehicle.update()"]
Veh --> Sen["Sensors"]
Sen --> Det["Detector (output only drawn)"]
Det --> Map["Mapper"]
Map --> Ctrl["Controller (accel, yaw rate)"]
Ctrl --> Loc["Localizer"]
Loc --> St["State (true state overwritten by estimate)"]
Limit |
Today |
Blocks |
|---|---|---|
Truth and estimate are mixed |
With a localizer, |
Evaluating estimation, tracking, obstacle avoidance |
The vehicle model is built into |
|
Dynamic vehicle model, steering-based control |
Planning runs once, outside the loop |
Planners write |
Local planning, obstacle avoidance, lane change |
No shared data between modules |
Detector output is only drawn; the order of modules is fixed inside |
Tracking, local mapping, avoidance |
Drawing drives time |
Time advances inside the animation; about 96% of runtime is spent recreating plot objects every frame |
Fast runs, comparisons, sensor rates |
No road or traffic model |
A course is a single spline; obstacles move with constant acceleration and yaw rate |
Lane change, tracking scenarios |
Target architecture
The simulator owns the truth (top row). The navigation stack works only with measurements and estimates, and its command moves the ego vehicle in the next step, which closes the loop:
flowchart TB
Ego["Ego vehicle (truth)"] --> Sen["Sensors"]
Agents["Other agents (truth)"] --> Sen
Road["Road and lanes"] --> Sen
Sen -->|measurements| Loc["Localization"]
Loc -->|ego estimate| Per["Perception"]
Loc -->|ego estimate| Map["Mapping"]
Per -->|tracks| Plan["Planning"]
Map -->|maps| Plan
Plan -->|trajectory| Ctrl["Control"]
Ctrl -->|command: accel, steer| Next["Ego vehicle at next step"]
For readability the diagram shows only the main flow. Perception and mapping also use sensor data directly, and the ego estimate is available to every later stage. The visualizer and metrics observe both the world and the stack without changing them.
Components
Component |
Responsibility |
Interface (sketch) |
|---|---|---|
|
Advances time with a fixed |
|
|
True state of everything: ego vehicle, other agents, road, static obstacles |
|
|
Vehicle motion, swappable (kinematic bicycle, dynamic bicycle, …) |
|
|
Actuator input from control |
|
|
Generates measurements from the true world, with noise and its own rate |
|
|
Estimates the ego pose from odometry and GNSS |
|
|
Finds objects in a point cloud |
|
|
Associates detections over time and estimates object motion |
|
|
Builds a global or local (ego-centered, rolling) map |
|
|
Plans a route on a map (A*, RRT*, …) |
|
|
Decides what to do (keep lane, change lane, stop) |
|
|
Plans a short collision-free trajectory (VFH, DWA, Frenet, …) |
|
|
Tracks a trajectory |
|
|
Observe the world and the stack outputs; draw or record |
|
Key decisions:
NavigationStack.step()is one short function that calls the stages in order and stores their outputs in a per-cycleFrame(a plain dataclass: measurements, ego estimate, detections, tracks, maps, trajectory, command). Learners read this one function to see the whole navigation flow; each stage can be replaced with one line.Trajectorykeeps the same methods as today’sCubicSplineCourse(nearest point, errors, curvature, target speed). Existing controllers can then follow a trajectory planned during the simulation without changes.Stages are optional. A demo of one algorithm (e.g. only NDT mapping) uses only the stages it needs, so today’s single-algorithm demos keep working as “lessons”.
One cycle runs as follows. The simulator, not the animation, advances time:
sequenceDiagram
participant Sim as Simulator
participant W as World
participant Sen as Sensors
participant Nav as Stack
participant Obs as Observers
Sim->>W: step(command)
Sim->>Sen: measure(world)
Sen-->>Sim: measurements
Sim->>Nav: step(measurements)
Note over Nav: localize ... control
Nav-->>Sim: frame
Sim->>Obs: on_step(frame)
The world moves with the command from the previous cycle, the sensors measure the new truth, the stack fills a Frame from localization to control, and the observers draw or record it. This repeats every dt.
Truth vs estimate in drawings: the vehicle is drawn at its true pose, estimates are drawn on top (e.g. covariance ellipses, estimated pose).
How a learner uses it
world = scenarios.two_lane_road_with_traffic()
stack = NavigationStack(
localizer=ExtendedKalmanFilterLocalizer(...), # try ParticleFilterLocalizer
detector=LShapeFittingDetector(...),
tracker=KalmanFilterTracker(...),
behavior_planner=LaneChangeBehaviorPlanner(...),
local_planner=FrenetPathPlanner(...), # try a VFH-based planner
controller=PurePursuitController(...), # try MpcController
)
Simulator(world, stack, dt_s=0.1).run(span_sec=30, observers=[GlobalXYVisualizer(...)])
A comparison runner runs the same scenario with different stacks without drawing and reports metrics such as localization RMSE, lateral tracking error, minimum distance to obstacles, travel time and lateral acceleration.
How the system grows
The book (issue #165) builds the stack in this order. Each step adds one stage to the previous step’s simulation, usually as one more argument to NavigationStack:
Step |
Stage added |
What the learner sees |
|---|---|---|
0 |
none: the vehicle follows a fixed list of commands |
The vehicle model and the simulator loop |
1 |
|
The vehicle follows a course |
2 |
|
Measurements with noise, next to the truth |
3 |
|
The controller now uses the estimate; the estimation error becomes visible |
4 |
|
A map built while driving |
5 |
|
The course comes from a planned route instead of being given |
6 |
|
Other objects found and followed |
7 |
|
Obstacles avoided in the loop |
8 |
|
Lane changes in traffic |
# Step 1: follow a given course
stack = NavigationStack(controller=PurePursuitController(course))
# Step 3: the same stack with a localizer added (one line)
stack = NavigationStack(
localizer=ExtendedKalmanFilterLocalizer(...),
controller=PurePursuitController(course),
)
For this to work, a missing stage has a defined, visible default:
No localizer: the stack passes the true pose as the ego estimate, and the drawing labels it as truth
No planner: the controller follows the course given to the stack
No detector, tracker or mapper: the later stages get empty detections, tracks or maps
Decision: package structure
Scripts currently load modules with sys.path.append(...) (282 calls in 41 simulations). As modules start to depend on each other through shared interfaces, this gets harder to follow and risks silent name collisions. In Phase 1 we move to an installable package (pip install -e ., then from <package>.control.pure_pursuit import PurePursuitController). Until then, new code keeps following the current sys.path.append(...) convention.
The package is also meant to be imported by other projects, not only by the simulations in this repository. For this, each algorithm is split into three parts:
Part |
Depends on |
Example (EKF localization) |
|---|---|---|
Core |
NumPy, SciPy, standard library |
Predict and update steps on the state vector and covariance matrix |
Stage adapter |
Stage interfaces, |
|
Drawing |
matplotlib |
Draws the covariance ellipse |
Drawing dependencies (matplotlib, Pillow) and MPC dependencies (CasADi, do-mpc) are optional extras (pip install <package>[viz], [mpc]). Each component is split when its stage is ported in Phase 2, not in one big rewrite, so open pull requests are affected as little as possible. Until Phase 2 is done the API can change between versions (0.x). A stable release on PyPI follows after it. The library is for learning, research and prototyping. It is pure Python and not meant for real-time control of real vehicles.
The public API of the core has type hints for every argument and return value, the package ships a py.typed marker, and CI type-checks the core modules with pyright.
Roadmap
Each phase keeps existing simulations working. Phases are split into issues tracked in the roadmap issue. Phase 6 only needs the vehicle model interface from Phase 1, so it can run in parallel with Phases 2-5. Phase 7 continues alongside all phases.
flowchart TB
P0["Phase 0: Foundation"] --> P1["Phase 1: Core separation"]
P1 --> P2["Phase 2: Navigation stack"]
P1 --> P6["Phase 6: Vehicle dynamics"]
P2 --> P3["Phase 3: Perception"]
P2 --> P4["Phase 4: Planning in the loop"]
P3 --> P5["Phase 5: Behavior"]
P4 --> P5
Phase |
Content |
Enables |
|---|---|---|
0. Foundation (in progress) |
Fix test/CI issues, faster drawing (persistent plot objects), separate showing and saving GIFs, deterministic tests, README and learning path |
Fast feedback for the next phases |
1. Core separation |
Separate truth and estimate; |
Measurable estimation, vehicle model swapping, headless runs |
2. Navigation stack |
|
Swapping and comparing algorithms in one scenario; using the algorithms from other projects |
3. Perception |
Object tracking (Kalman filter + data association), local rolling occupancy map, scenarios with moving agents |
Seeing and predicting other objects |
4. Planning in the loop |
|
Local path planning, obstacle avoidance |
5. Behavior |
|
Lane change while driving |
6. Vehicle dynamics (can start after Phase 1) |
Dynamic bicycle model with a tire model; comparison with the kinematic model at higher speeds; controllers that use dynamics |
Dynamic model |
7. Learning experience (ongoing) |
The book (#165), with one chapter per stage in the order of “How the system grows”; playground to select components and parameters |
Learning by building up and recombining |
Ongoing contributions
New single-algorithm demos are welcome at any time. They will be connected to the stack interfaces when the corresponding phase arrives.
The VFH series (issue #52) fits Phase 4 as a local planner. Its steps are merged as standalone demos first, and adapted to the
LocalPlannerinterface in Phase 4.