# How to Configure and Switch Between Different Trajectory Planners in Peng

> Configure and switch trajectory planners in Peng by editing quad.yaml for timed transitions or use PlannerManager::set_planner for immediate Rust code changes.

- Repository: [Yang Zhou/peng](https://github.com/makeecat/peng)
- Tags: how-to-guide
- Published: 2026-03-06

---

**You configure trajectory planners in Peng by editing the `planner_schedule` array in [`config/quad.yaml`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/config/quad.yaml). Each entry is a **PlannerStep** object that specifies when to activate and how to parameterize a specific planner.

```yaml
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`](https://github.com/makeecat/peng/blob/main/src/lib.rs) is:

```rust
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.

```rust
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`](https://github.com/makeecat/peng/blob/main/main.rs) demonstrates how the YAML configuration, scheduler, and manager interact during the simulation loop:

```rust
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`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/config/quad.yaml) using its new `planner_type` string.

## Summary

- **Edit `planner_schedule`** in [`config/quad.yaml`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/src/lib.rs) (lines 723–730, 1212–1246, 2327–2334, 2425–2500) and [`config/quad.yaml`](https://github.com/makeecat/peng/blob/main/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`](https://github.com/makeecat/peng/blob/main/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.