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 exposes set_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:

  1. step – The simulation time in milliseconds when the planner becomes active. Peng converts this to internal tick counts using the simulation frequency.
  2. planner_type – The exact string identifier that create_planner() matches. Valid options include Hover, MinimumJerkLine, Lissajous, Circle, Landing, ObstacleAvoidance, MinimumSnapWaypoint, and QPpolyTraj.
  3. params – A free-form YAML object containing planner-specific arguments. The factory function uses helpers like parse_vector3 and parse_f32 to 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:

  1. Time conversion – It calculates whether planner_step.step * simulation_frequency == current_step * 1000. This converts your YAML millisecond values into discrete simulation steps.
  2. Activation logging – When a match occurs, it emits log::info!("Time: {:.2} s,\tSwitch {}", time, planner_step.planner_type).
  3. Factory instantiation – It calls create_planner() with the matched PlannerStepConfig and current quadrotor state to generate the new trajectory generator.
  4. 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:

  1. Implement the Planner trait for your new struct, following the pattern established by HoverPlanner in the source.
  2. Extend the factory in src/lib.rs by adding a match arm to create_planner() that maps a new string identifier (e.g., "SpiralDescent") to your struct constructor.
  3. Document parameters and use the provided parsing utilities (parse_vector3, parse_f32) to extract configuration from the YAML params map.

Once registered, you can reference your custom planner immediately in config/quad.yaml using its new planner_type string.

Summary

  • Edit planner_schedule in config/quad.yaml to declare timed planner switches using millisecond step values and specific planner_type strings.
  • 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) and config/quad.yaml for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →