How to Switch Between Trajectory Planners at Runtime in Peng
You can switch between trajectory planners at runtime in Peng by defining a schedule in the YAML configuration file or by calling PlannerManager::set_planner() directly in your Rust code.
Peng is a Rust-based quadrotor simulation framework that provides a flexible trajectory-planning subsystem. The ability to switch between trajectory planners at runtime enables complex flight missions where the vehicle transitions from hovering to line tracking, Lissajous curves, or obstacle avoidance without restarting the simulation.
Understanding the Trajectory Planning Architecture
The runtime switching mechanism relies on four core components defined in src/lib.rs:
PlannerStepConfig(lines 2327-2334) – Represents a single scheduled switch, holding the step number, planner type name, and parameters.PlannerManager(lines 1212-1246) – Maintains the active planner and providesset_planner()for immediate switches.create_planner()(lines 2425-2500) – Factory function that maps string identifiers like"MinimumJerkLine"to concrete Rust structs.update_planner()(lines 723-730) – Scheduler called each simulation tick to check if a switch should occur.
Configuring Planner Switches in YAML
The planner_schedule field in config/quad.yaml defines when and how to switch between trajectory planners at runtime.
Structure of the planner_schedule
Each entry in the schedule requires three fields:
step– Simulation time in milliseconds when the planner becomes active.planner_type– String identifier that must match a variant increate_planner().params– Planner-specific configuration object.
Supported Planner Types and Parameters
Peng supports multiple trajectory primitives that you can sequence in the YAML schedule:
planner_schedule:
# Hover to Minimum-Jerk line at 1 second
- step: 1000
planner_type: MinimumJerkLine
params:
end_position: [0.0, 0.0, 1.0]
end_yaw: 0.0
duration: 2.5
# Switch to Lissajous curve at 5 seconds
- step: 5000
planner_type: Lissajous
params:
center: [0.5, 0.5, 1.0]
amplitude: [0.5, 0.5, 0.2]
frequency: [1.0, 2.0, 3.0]
phase: [0.0, 1.570796, 0.0]
duration: 20.0
end_yaw: 6.283185307179586
ramp_time: 5.0
# Circular trajectory at 27 seconds
- step: 27000
planner_type: Circle
params:
center: [0.5, 0.5, 1.0]
radius: 0.5
angular_velocity: 1.0
duration: 5.0
ramp_time: 2.0
Available planner types include: Hover, MinimumJerkLine, Lissajous, Circle, Landing, ObstacleAvoidance, MinimumSnapWaypoint, and QPpolyTraj.
How Runtime Switching Works
The automatic switching mechanism relies on the simulation loop calling update_planner() each tick.
The update_planner Function
Located at lines 723-730 in src/lib.rs, this function checks if the current simulation step matches any entry in the schedule:
update_planner(
&mut planner_manager,
i, // Current simulation step count
time, // Current simulation time in seconds
config.simulation.simulation_frequency,
&quad, // Current quadrotor state
&maze.obstacles, // Obstacles for avoidance planners
&planner_config, // Vec<PlannerStepConfig> from YAML
)?;
The function converts the YAML step (milliseconds) to simulation ticks by comparing step * simulation_frequency against current_step * 1000. When a match occurs, it logs the switch and calls create_planner() to instantiate the new trajectory generator.
The PlannerManager API
The PlannerManager struct maintains the active planner state. When update_planner() detects a schedule match, it invokes:
planner_manager.set_planner(new_planner);
This immediately replaces the active planner. The next call to planner_manager.update() (typically in the control loop) queries the new planner for desired position, velocity, and yaw.
Manual Planner Switching in Code
For reactive behaviors that cannot be predefined in YAML, use the PlannerManager API directly:
use peng_quad::{PlannerManager, PlannerType, CirclePlanner};
use nalgebra::Vector3;
// Create a new circular trajectory planner
let circle_planner = CirclePlanner {
center: Vector3::new(1.0, 1.0, 1.0),
radius: 0.75,
angular_velocity: 1.5,
start_position: quad.position,
start_time: current_time,
duration: 8.0,
start_yaw: quad.orientation.euler_angles().2,
end_yaw: quad.orientation.euler_angles().2,
ramp_time: 1.0,
};
// Immediately switch to the new planner
planner_manager.set_planner(PlannerType::Circle(circle_planner));
The PlannerType enum wraps all concrete planner implementations. Calling set_planner() bypasses the YAML schedule and immediately activates the specified trajectory generator.
Adding Custom Planners
To extend Peng with a new trajectory planner:
- Implement the
Plannertrait for your struct (seeHoverPlannerin the source for a minimal example). - Add a match arm in
create_planner()(lines 2425-2500 insrc/lib.rs) that maps a string identifier to your struct. - Document YAML parameters and use helper functions like
parse_vector3()orparse_f32()to extract configuration values.
Once added, you can reference the new planner via planner_type: MyNewPlanner in the YAML schedule.
Summary
- Configuration-driven switching is defined in
config/quad.yamlusing theplanner_schedulearray withstep(ms),planner_type, andparams. - Automatic runtime switching occurs via
update_planner()insrc/lib.rs, which checks simulation ticks against the schedule and callsPlannerManager::set_planner(). - Manual switching is available through the
PlannerManagerAPI by callingset_planner()with aPlannerTypevariant. - Supported planners include Hover, MinimumJerkLine, Lissajous, Circle, Landing, ObstacleAvoidance, MinimumSnapWaypoint, and QPpolyTraj.
- Extensibility is achieved by implementing the
Plannertrait and adding a case tocreate_planner()insrc/lib.rs.
Frequently Asked Questions
Can I switch planners mid-flight without restarting the simulation?
Yes. Peng is designed specifically for this use case. You can either define multiple entries in the planner_schedule YAML array with different step values, or call planner_manager.set_planner() programmatically at any point during the simulation loop. The switch takes effect immediately on the next control cycle.
What happens if two planner schedule entries have the same step value?
If multiple entries share the same step value in the YAML configuration, update_planner() will process them in the order they appear in the array. Since each call to set_planner() immediately replaces the active planner, the last entry with that step value will ultimately determine which planner is active. To avoid ambiguity, ensure each step value is unique or order entries carefully.
How do I implement a custom trajectory planner in Peng?
To add a custom planner, implement the Planner trait for your new struct, which requires methods for updating state and querying desired position, velocity, and yaw. Then add a match arm in the create_planner() function (around line 2425 in src/lib.rs) that maps a string identifier to your planner constructor. Finally, document the YAML parameters so users can reference your planner in the planner_schedule configuration.
Is there a performance penalty for switching planners at runtime?
The overhead is negligible. Switching planners involves only updating a pointer to the trait object inside PlannerManager and instantiating the new planner struct with its parameters. The update_planner() check runs once per simulation tick and performs simple integer comparisons to determine if a switch is required. The actual trajectory computation happens inside the active planner's update() method, so performance depends on the complexity of the specific planner, not the switching mechanism itself.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →