How to Configure and Switch Between Different Trajectory Planners in Peng
You configure trajectory planners in Peng by editing the planner_schedule array in config/quad.yaml to define timed switches, while the runtime automatically handles transitions via update_planner(); for immediate changes, call PlannerManager::set_planner() directly from your Rust code.
Peng is an open-source quadrotor flight simulator written in Rust. The repository at makeecat/peng provides a flexible trajectory-planning subsystem that lets you declare complex flight sequences and switch between planners without recompiling the code. This guide explains how to leverage the configuration-driven architecture to orchestrate multi-phase trajectories.
Understanding the Trajectory Planning Architecture
Peng's planning system relies on four core components that work together to manage trajectory generation:
- PlannerStepConfig – Defined in
src/lib.rs(lines 2327–2334), this struct holds a single scheduled transition, including the target step, planner type string, and parameter map. - PlannerManager – Located in
src/lib.rs(lines 1212–1246), this struct maintains the active planner and exposesset_planner()for immediate switches. - create_planner() – A factory function in
src/lib.rs(lines 2425–2500) that maps string identifiers like"MinimumJerkLine"or"Lissajous"to concrete Rust structs. - update_planner() – The scheduler implemented in
src/lib.rs(lines 723–730) that checks the simulation timeline and triggers planner instantiation.
These components allow you to declare an entire flight plan—hover, line trajectory, Lissajous curve, circular orbit—inside a YAML file, and Peng executes the sequence automatically as the simulation advances.
Configuring Planner Switches in YAML
All trajectory schedules are defined in the planner_schedule field of config/quad.yaml. Each entry is a PlannerStep object that specifies when to activate and how to parameterize a specific planner.
planner_schedule:
- step: 1000
planner_type: MinimumJerkLine
params:
end_position: [0.0, 0.0, 1.0]
end_yaw: 0.0
duration: 2.5
- 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
- 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
The configuration schema uses three critical fields:
step– The simulation time in milliseconds when the planner becomes active. Peng converts this to internal tick counts using the simulation frequency.planner_type– The exact string identifier thatcreate_planner()matches. Valid options includeHover,MinimumJerkLine,Lissajous,Circle,Landing,ObstacleAvoidance,MinimumSnapWaypoint, andQPpolyTraj.params– A free-form YAML object containing planner-specific arguments. The factory function uses helpers likeparse_vector3andparse_f32to extract these values when constructing the planner instance.
You can chain unlimited entries to build complex missions. The schedule is zero-indexed and evaluated sequentially, though each entry's step value determines its absolute activation time, not its order in the array.
How Runtime Planner Switching Works
Each simulation tick, the main loop invokes update_planner() to evaluate the schedule against the current simulation state. The function signature in src/lib.rs is:
pub fn update_planner(
planner_manager: &mut PlannerManager,
current_step: usize,
time: f32,
simulation_frequency: f32,
quad: &Quadrotor,
obstacles: &[Obstacle],
planner_config: &[PlannerStepConfig],
) -> Result<(), SimulationError>
The scheduler performs the following logic every tick:
- Time conversion – It calculates whether
planner_step.step * simulation_frequency == current_step * 1000. This converts your YAML millisecond values into discrete simulation steps. - Activation logging – When a match occurs, it emits
log::info!("Time: {:.2} s,\tSwitch {}", time, planner_step.planner_type). - Factory instantiation – It calls
create_planner()with the matchedPlannerStepConfigand current quadrotor state to generate the new trajectory generator. - Atomic replacement – It invokes
planner_manager.set_planner()to swap the active planner immediately.
If no schedule entry matches the current step, the existing planner continues executing uninterrupted. This design ensures deterministic, repeatable flight plans that are decoupled from the core simulation logic.
Manual Planner Control in Code
For reactive behaviors—such as aborting a trajectory when a sensor detects an obstacle—you can override the schedule by calling the PlannerManager API directly. This bypasses the YAML configuration and activates a new planner instantly.
use peng_quad::{PlannerManager, PlannerType, CirclePlanner};
use nalgebra::Vector3;
// Instantiate a circular trajectory manually
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,
};
// Immediate switch without waiting for the schedule
planner_manager.set_planner(PlannerType::Circle(circle_planner));
The PlannerType enum wraps every concrete planner implementation. When you call set_plrapper(), the manager replaces its internal Box<dyn Planner> trait object. The next call to planner_manager.update() will query the new trajectory for position, velocity, and yaw references.
Complete Integration Example
The following snippet from main.rs demonstrates how the YAML configuration, scheduler, and manager interact during the simulation loop:
use nalgebra::Vector3;
use peng_quad::*;
fn main() -> Result<(), SimulationError> {
// Load configuration including the planner schedule
let cfg = config::Config::from_yaml("config/quad.yaml")?;
// Initialize quadrotor dynamics
let mut quad = Quadrotor::new(
1.0 / cfg.simulation.simulation_frequency as f32,
cfg.quadrotor.mass,
cfg.quadrotor.gravity,
cfg.quadrotor.drag_coefficient,
cfg.quadrotor.inertia_matrix,
)?;
// Convert YAML PlannerStep entries into PlannerStepConfig structs
let planner_config: Vec<PlannerStepConfig> = cfg.planner_schedule
.into_iter()
.map(|step| PlannerStepConfig {
step: step.step,
planner_type: step.planner_type,
params: step.params,
})
.collect();
// Initialize manager with a hover planner at origin
let mut planner_mgr = PlannerManager::new(Vector3::zeros(), 0.0);
// Main simulation loop
for i in 0..100_000 {
let time = quad.time_step * i as f32;
// Automatic scheduler check
update_planner(
&mut planner_mgr,
i,
time,
cfg.simulation.simulation_frequency,
&quad,
&maze.obstacles,
&planner_config,
)?;
// Query active planner for reference states
let (position, velocity, yaw) = planner_mgr.update(
quad.position,
quad.orientation,
quad.velocity,
time,
&[],
)?;
// Proceed to control and physics integration...
}
Ok(())
}
This pattern separates declaration (the YAML schedule) from execution (the Rust runtime), enabling rapid prototyping of multi-phase flight plans without recompilation.
Extending Peng with Custom Planners
To add a planner not included in the default distribution:
- Implement the
Plannertrait for your new struct, following the pattern established byHoverPlannerin the source. - Extend the factory in
src/lib.rsby adding a match arm tocreate_planner()that maps a new string identifier (e.g.,"SpiralDescent") to your struct constructor. - Document parameters and use the provided parsing utilities (
parse_vector3,parse_f32) to extract configuration from the YAMLparamsmap.
Once registered, you can reference your custom planner immediately in config/quad.yaml using its new planner_type string.
Summary
- Edit
planner_scheduleinconfig/quad.yamlto declare timed planner switches using millisecondstepvalues and specificplanner_typestrings. - Rely on
update_planner()to automatically instantiate and activate planners when the simulation reaches the configured timestep. - Use
PlannerManager::set_planner()for imperative, code-level planner switches outside the YAML schedule. - Reference source files
src/lib.rs(lines 723–730, 1212–1246, 2327–2334, 2425–2500) andconfig/quad.yamlfor implementation details and configuration examples.
Frequently Asked Questions
How does Peng convert YAML step values to simulation ticks?
Peng multiplies the millisecond step value by the simulation_frequency to align the schedule with internal tick counts. The scheduler checks if planner_step.step * simulation_frequency == current_step * 1000 to determine activation timing, ensuring precise synchronization regardless of simulation speed.
Can I switch planners mid-flight without using the YAML schedule?
Yes. Call planner_manager.set_planner(PlannerType::YourPlanner(new_instance)) from any Rust code with access to the manager. This immediately replaces the active planner, overriding any pending YAML-scheduled switches until the next scheduled activation time is reached.
What happens if two planner schedule entries have the same step value?
The scheduler evaluates the planner_config slice sequentially. If multiple entries match the current step condition, the last matching entry in the array will overwrite previous matches during the same tick, resulting in the final entry becoming the active planner.
Which source file contains the list of available planner types?
The factory function create_planner() in src/lib.rs (lines 2425–2500) contains the exhaustive match statement mapping string identifiers to concrete planner structs. This serves as the authoritative reference for supported planner_type values and their required YAML parameters.
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 →