Architecture¶
Overview¶
AVLite follows a layered architecture with clear separation between interfaces and implementations. Every layer is extendable: apps, executers, perception/planning/control strategies, and world bridges are all auto-registered strategies, and any of them can be added or overridden by a plugin.
flowchart TB
subgraph ENTRY["Apps (AppStrategy)"]
direction LR
VIZ["Visualizer (default)\nReal-time Tkinter GUI"]
SET["Settings GUI"]
PLG["Plugin Manager"]
HL["Headless\nTerminal dashboard"]
SCLI["Setting CLI"]
VIZ ~~~ SET ~~~ PLG ~~~ HL ~~~ SCLI
end
EXEC["Execution (executer strategy)\nSync/async executer and factory"]
subgraph COMPONENTS["Stack modules (Strategy plugins)"]
direction LR
PERC["Perception (optional)\nLocalization · Mapping\nDetection · Tracking · Prediction"]
PLAN["Planning\nGlobal · Local · Lattice"]
CTRL["Control\nStanley · PID · Pure Pursuit · FTG"]
WB["World Bridge\nBasicSim · Carla · Gazebo · ROS2"]
PERC ~~~ PLAN ~~~ CTRL ~~~ WB
end
COMMON["Common\nSettings · Capabilities · Trajectories · Collision checking"]
ENTRY --> EXEC
EXEC --> COMPONENTS
COMPONENTS --> COMMON
Every box above the Common layer is a pluggable strategy: apps register via AppStrategy, and the stack modules (perception, planning, control, world bridge) plus the executer itself register via their own strategy base classes. See Strategy Pattern with Auto-Registration.
Design Patterns¶
Strategy Pattern with Auto-Registration¶
All major components use abstract base classes with automatic registration:
class PerceptionStrategy(ABC):
registry = {}
def __init_subclass__(cls, abstract=False, **kwargs):
super().__init_subclass__(**kwargs)
if not abstract:
PerceptionStrategy.registry[cls.__name__] = cls
When you create a subclass, it automatically registers itself and appears in the UI dropdowns. No manual registration needed.
App Strategy (CLI/GUI entry points)¶
Every entry point — the Tkinter visualizer, the settings GUI, the plugin manager, the headless runner, the setting CLI — is just an app: an AppStrategy subclass that auto-registers exactly like PerceptionStrategy, keyed by cli_name (None marks the default app that runs when no subcommand is given).
class MyToolApp(AppStrategy):
cli_name = "my-tool" # None = default app
help = "Short description for avlite --help"
def run(self, args, unknown):
...
return 0 # optional; None = exit 0
The Tk visualizer and headless dashboard carry no special status — they are two of several built-in apps, and new apps drop in the same way (subclass, set cli_name, implement run(), optionally configure_parser() for flags). At startup __main__.py drives the dispatch:
flowchart LR
boot["bootstrap_apps()\nimport p60_* app modules"]
reg["register_parsers(sub)\none subcommand per app"]
run["run_app(command, ...)\nregistry[command] or default"]
boot --> reg --> run
Built-in apps ship as p60_* plugins (p60_visualizer_tk, p60_headless_mode, p60_setting_cli), so the app layer is fully extendable — community plugins can register their own apps. See Plugin Development → Apps.
Capability System¶
Components declare what they require and provide along a clean 2x2 grid keyed on capability space (world/sensor vs stack) and direction (requires vs provides):
| requires | provides | |
|---|---|---|
World layer (WorldCapability) |
world_requirements |
world_capabilities (world bridge only) |
Stack layer (StackCapability) |
stack_requirements |
stack_capabilities |
- A strategy declares
world_requirements,stack_requirements, andstack_capabilities(what it produces). Strategy ABCs keep these as abstract@property; leaf concretes satisfy them with publicfrozensetclass attributes (readable without constructing an instance). Pipelines keep instance@propertyaggregators over their stages. - A world bridge declares
world_capabilities(sensors it exposes) andstack_capabilities(ground truth it provides).
Requirement entries may be plain capabilities or wrappers:
- Plain caps (AND) — every listed capability must be present (
all · A & Bin the visualizer popup). AnyOf(A, B, …)— hard OR: at least one member must be present (any · A | B).MayUse(A, B, …)— soft: never blocks assembly; the module uses these when available (may · A | B).
Runtime payloads (who passes what): capability enums declare need; they are not a message bus. Key strategy methods also take optional perception_model + sensors (supplied by the executer, pipeline, or UI) in addition to any method-specific args:
# Shared optional pair on every key method
perception_model=None, # stack world-state (PerceptionModel)
sensors=None, # world SensorFrame
# Control keeps its prior args as well:
control(ego, plan=None, control_dt=None, perception_model=None, sensors=None)
# Detect keeps optional unpacked sensor convenience args as well:
detect(perception_model=None, sensors=None, rgb_img=None, depth_img=None, lidar_data=None)
Modules read fields they need (e.g. sensors.lidar, or ego / plan on control). Global start/goal stay via set_start_goal before plan(...).
class MyPerception(PerceptionStrategy):
world_requirements = frozenset({WorldCapability.CAMERA_RGB})
stack_requirements = frozenset()
stack_capabilities = frozenset({StackCapability.DETECTION})
class MyLocalPlanner(LocalPlanningStrategy):
world_requirements = frozenset()
stack_requirements = frozenset({
StackCapability.GLOBAL_PLAN,
StackCapability.LOCALIZATION,
MayUse(StackCapability.DETECTION, StackCapability.PREDICTION),
})
stack_capabilities = frozenset({StackCapability.LOCAL_PLAN})
World Capabilities (sensors / actuation the bridge provides):
CAMERA_RGB- RGB camera imagesCAMERA_DEPTH- Depth camera imagesLIDAR_3D- 3D LiDAR point cloud dataLIDAR_2D- 2D LiDAR scanner dataRADAR- Radar sensor dataWHEEL_ENCODER- Wheel encoder for odometryIMU- Inertial measurement unitGNSS- GNSS / GPS receiverAGENT_SPAWN- Bridge can spawn NPC agentsAGENT_CONTROL- Bridge can actuate spawned NPC agents viacontrol_agent(opt-in; separate fromAGENT_SPAWN)
Stack Capabilities (StackCapability) — what a stack module produces, used both as a module's stack_capabilities and as another module's stack_requirements:
DETECTION- Object detectionTRACKING- Object trackingPREDICTION- Motion predictionLOCAL_PLAN- Local plan (produced by the local planner)GLOBAL_PLAN- Global plan (produced by the global planner)CONTROL- Control commands (produced by the controller)LOCALIZATION- Ego localizationMAP_HD- HD / OpenDRIVE map (from a mapping module such asMapReader)MAP_RACE_TRACK- Race-track corridor map (from a mapping module such asMapReader)SLAM- Simultaneous localization and mapping
Ground truth via the world bridge: a WorldBridge may advertise stack_capabilities (a set[StackCapability], default empty) to satisfy downstream stack_requirements without a real module. For example, BasicSim provides {DETECTION, TRACKING, LOCALIZATION} as ground truth and declares stack_requirements = {CONTROL}. Optional WorldBridge.map is simulation-only (e.g. LiDAR geometry) and does not advertise MAP_HD / MAP_RACE_TRACK. Typed stack map caps come from a mapping module such as MapReader (holds a pre-loaded Map; advertises MAP_HD or MAP_RACE_TRACK from the concrete type; format sniff/load stays on Map.open / Map.from_path). Global planners require the matching typed cap (HDMapGlobalPlanner → MAP_HD; race planners → MAP_RACE_TRACK). The executer’s available_stack_capabilities() unions every present module’s stack_capabilities with filtered world.stack_capabilities. At stack build it raises when a module’s hard stack_requirements are unmet (MayUse never fails that check), and warns when the world’s hard requirements are unmet or when the same capability is provided by more than one source.
In the visualizer, the ⓘ button (or right-click) on a stack Combobox opens a contract popup: world requirements, stack requirements (colored against available_stack_capabilities, including world GT), and provided stack capabilities. The Bridge Setting Combobox has the same ⓘ for the selected WorldBridge: world capabilities, stack requirements, and stack capabilities. Requirement rows are labeled all · / any · / may ·. Provided caps: green = consumed by another module’s hard or soft (MayUse) requirements or by the world bridge’s stack_requirements; orange = also provided by another top-level module or by world GT when that capability is checked under Bridge Setting’s stack column (c41_world_stack_capabilities); gray = unused. Parent PerceptionPipeline advertising does not orange its own detect/track/predict stages. Velocity and lattice local planners soft-use DETECTION and PREDICTION (agents + motion sweeps), not TRACKING. Bridge Setting’s world column (c41_world_capabilities) gates which sensors are fed into SensorFrame.
Factory Pattern¶
The executor factory assembles components based on configuration:
executer = executor_factory(
bridge="BasicSim",
perception_strategy_name="MultiObjectPredictor",
localization_strategy_name="MyLocalization",
mapping_strategy_name="MapReader",
local_planner_strategy_name="GreedyLatticePlanner",
controller_strategy_name="StanleyController"
)
It loads plugins, opens ExecutionSettings.c40_map once via Map.open (shared by MapReader, global planners, and WorldBridge), instantiates strategies from registries, and wires everything together. Perception, localization, and mapping strategy names are optional — pass an empty string or omit them to run without that component.
Before calling executor_factory(), load YAML profiles with load_stack_settings(profile, load_plugins) in c62_factory.py. Each setting reads its section from the single configs/<profile>.yaml: it loads the c10–c40 layer sections, AppSettings (the c69_apps section), and built-in plugin settings (the plugins section); the GUI loads the Tk VisualizationSettings binder separately.
Layer import rules¶
Stack core (c10–c40, c50_common) may import c61_app_strategy, c64_settings_schema, c68_paths, and c69_settings only. Profile export/import operates on a single per-profile YAML file (c65_setting_utils.export_profile / import_profile), with checkboxes to include the c69_apps and plugins sections. Tk binder VisualizationSettings lives in plugin settings.py; c69_settings is schema-only (plugin bootstrap fields use c62_*, consumed by c62_factory).
Agent model¶
Agents are represented as a small class hierarchy in c11:
EGO_AGENT_ID = 0— reserved for the ego vehicle (perception_model.ego_vehicle).- NPC ids
1, 2, 3, …— assigned byPerceptionModel.add_agent_vehicle. AgentType— platform metadata on each agent (Ackermann, diff-drive, aerial, pedestrian, …).- Default state — pose (
x,y,z,theta) plus scalarvelocity(car-centric; used by planning, collision, and viz). - Future — specialized subclasses (e.g.
DroneAgentState) when kinematics need body velocity or 3D integration; see Multi-robot agents and control.
Control actuation is a separate layer: ControlCommandBase subclasses and default AgentType → command mapping in c31. The car stack still uses the ControlCommand alias for AckermannControlCommand.
Layers¶
Perception¶
Optional monolithic or pipelined detect/track/predict strategies, plus localization and mapping interfaces. Built-in algorithms and plugin implementations register automatically and appear in UI dropdowns. Static map types (Map, RaceMap, HDMap) live in c11; OpenDRIVE parsing is in c18. See Plugin Development for monolithic vs pipeline extension paths.
Planning¶
Global route planning and reactive local planning (lattice-based). Produces trajectories for the controller. See Algorithms for lattice planner details.
Control¶
Vehicle control strategies (Stanley, PID, Pure Pursuit, Follow the Gap) output actuation commands. Commands use a ControlCommandBase hierarchy (AckermannControlCommand, DiffDriveControlCommand, BodyVelocityControlCommand in c31); the built-in car stack still returns ControlCommand (Ackermann alias). Per-agent command type defaults are mapped from AgentType in c31. See Plugin Development → Multi-robot agents and control. Pure Pursuit and Follow the Gap are documented in Algorithms.
Execution¶
World bridge (simulator/ROS interface), executer orchestration loop, sync/async scheduling, and the factory that wires the stack from YAML configuration. The built-in BasicSim bridge ships with the core stack; CARLA, Gazebo, and ROS2 are supported through optional world-bridge plugins. Alternative executers (for example a multiprocess ROS deployment) are selected via c40_executer_type and provided as optional plugins.
Apps¶
CLI and GUI entry points, each an AppStrategy (see App Strategy). Built-in apps ship as p60_* plugins and the layer is extendable:
- Visualizer (default,
avlite) — Tkinter GUI: real-time plots, profile/config management, schema tooltips, thread-safe log filtering (Core / Plugins / per-layer toggles), and plugin settings. - Settings GUI (
avlite setting) — full stack settings editor window. - Plugin Manager (
avlite plugins) — browse, install, and register community plugins. - Headless (
avlite headless) — runs the same YAML profile with a terminal dashboard, no GUI. - Setting CLI (
avlite setting-cli) — validate/describe/import/export profiles from the terminal.
Common¶
YAML profile load/save, hot reload, plugin discovery (c63_plugins), path resolution (c68_paths), capability enums, canonical sensor layouts (rgb, depth, lidar, imu, gnss between bridge and perception), collision checking, and settings validation (c64_settings_schema).
Data Flow¶
World Bridge
│
├─► Sensor Data ──► Localization ──► Ego Pose (updated in-place)
│ │
├─► Sensor Data ──► Perception ───► Agents │
│ ▼
│ Local Planner
│ │
│ ▼
│ Trajectory
│ │
│ ▼
│ Controller
│ │
└─────────────── Control Command ◄─────────┘
│
(future: control_agent for NPC fleet)
- World Bridge provides sensor data (IMU, LiDAR, camera, ground truth)
- Localization (optional) estimates the ego pose from sensor data, updating
PerceptionModel.ego_vehiclein-place - Perception (optional) detects/tracks/predicts surrounding agents
- Local Planner generates trajectory avoiding obstacles
- Controller computes steering and throttle (Ackermann today; other command types reserved for multi-robot plugins)
- World Bridge executes control command via
control_ego_state(ego path unchanged;control_agentandstep()hooks exist for future multi-agent and sub-stepping)
Plugin System¶
avlite/
└── plugins/ # Built-in (core team)
├── p60_visualizer_tk/ # visualizer + config + plugins Tk apps
├── p60_setting_cli/
└── p60_headless_mode/
~/.local/share/avlite/plugins/ # Community (installed)
└── my_plugin/
├── __init__.py
├── settings.py
└── ...
~/.config/avlite/plugin_my_plugin.yaml # Community plugin settings (user config, not in install dir)
Plugins are loaded at startup. Classes inheriting from base strategies auto-register. The built-in p60_* packages are the apps themselves (AppStrategy entry points); bootstrap_apps() in c61_app_strategy imports them at startup so their apps register before CLI parsing.
See Plugin Development for creating community plugins, pNx naming, and log filtering.