Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Compound State Planning: Rust

This example demonstrates how to use CompoundStateSpace to combine multiple state spaces (in this case, RealVectorStateSpace for position and SO2StateSpace for orientation) into a single planning problem.

use std::{f64::consts::PI, sync::Arc, time::Duration};

use oxmpl::base::{
    error::StateSamplingError,
    goal::{Goal, GoalRegion, GoalSampleableRegion},
    planner::{Planner, PlannerConfig},
    problem_definition::ProblemDefinition,
    space::{CompoundStateSpace, RealVectorStateSpace, SO2StateSpace, StateSpace},
    state::{CompoundState, RealVectorState, SO2State},
    validity::StateValidityChecker,
};
use oxmpl::geometric::RRT;

use rand::Rng;

/// A StateValidityChecker that checks for collision with a box obstacle.
/// The robot is considered a point with orientation, but we check if the point
/// is inside the box.
struct BoxObstacleChecker {
    x_min: f64,
    x_max: f64,
    y_min: f64,
    y_max: f64,
}

impl StateValidityChecker<CompoundState> for BoxObstacleChecker {
    fn is_valid(&self, state: &CompoundState) -> bool {
        // Extract the RealVector component (index 0)
        if let Some(rv_state) = state.components[0]
            .as_any()
            .downcast_ref::<RealVectorState>()
        {
            let x = rv_state.values[0];
            let y = rv_state.values[1];

            // Check if inside the box
            let inside_box =
                x >= self.x_min && x <= self.x_max && y >= self.y_min && y <= self.y_max;

            !inside_box
        } else {
            // Should not happen if constructed correctly
            false
        }
    }
}

/// A Goal definition for a CompoundState (Position + Orientation).
/// Success is being within a radius of the target position.
struct CompoundGoalRegion {
    target: CompoundState,
    radius: f64,
    space: Arc<CompoundStateSpace>,
}

impl Goal<CompoundState> for CompoundGoalRegion {
    fn is_satisfied(&self, state: &CompoundState) -> bool {
        self.space.distance(state, &self.target) <= self.radius
    }
}

impl GoalRegion<CompoundState> for CompoundGoalRegion {
    fn distance_goal(&self, state: &CompoundState) -> f64 {
        let dist = self.space.distance(state, &self.target);
        (dist - self.radius).max(0.0)
    }
}

impl GoalSampleableRegion<CompoundState> for CompoundGoalRegion {
    fn sample_goal(&self, rng: &mut impl Rng) -> Result<CompoundState, StateSamplingError> {
        // Get target components
        let target_rv = self.target.components[0]
            .as_any()
            .downcast_ref::<RealVectorState>()
            .unwrap();

        // Sample a position around the target within radius
        let angle = rng.random_range(0.0..2.0 * PI);
        let radius = self.radius * rng.random::<f64>().sqrt();

        let x = target_rv.values[0] + radius * angle.cos();
        let y = target_rv.values[1] + radius * angle.sin();

        // Sample a random orientation for the goal (or keep it fixed if desired)
        // Here we sample a random orientation effectively ignoring orientation for the goal region
        let theta = rng.random_range(0.0..2.0 * PI);

        Ok(CompoundState {
            components: vec![
                Box::new(RealVectorState { values: vec![x, y] }),
                Box::new(SO2State::new(theta)),
            ],
        })
    }
}

#[allow(clippy::arc_with_non_send_sync)]
fn main() {
    // 1. Define the state space: R^2 x SO(2)
    // R^2 for position (x, y)
    let r2 = RealVectorStateSpace::new(2, Some(vec![(-5.0, 5.0), (-5.0, 5.0)]))
        .expect("Failed to create R2 space");

    // SO(2) for orientation (theta)
    let so2 = SO2StateSpace::new(None).expect("Failed to create SO2 space");

    // Combine them into a CompoundStateSpace
    // Weights: 1.0 for position, 0.5 for orientation (orientation matters less for distance)
    let space = Arc::new(CompoundStateSpace::new(
        vec![Box::new(r2), Box::new(so2)],
        vec![1.0, 0.5],
    ));

    // 2. Define Start and Goal states
    let start_state = CompoundState {
        components: vec![
            Box::new(RealVectorState::new(vec![-2.0, 0.0])),
            Box::new(SO2State::new(0.0)),
        ],
    };

    let target_state = CompoundState {
        components: vec![
            Box::new(RealVectorState::new(vec![2.0, 0.0])),
            Box::new(SO2State::new(PI)),
        ],
    };

    // 3. Define Goal Region
    let goal_definition = Arc::new(CompoundGoalRegion {
        target: target_state.clone(),
        radius: 0.5,
        space: space.clone(),
    });

    // 4. Define Problem
    let problem_definition = Arc::new(ProblemDefinition {
        space: space.clone(),
        start_states: vec![start_state.clone()],
        goal: goal_definition.clone(),
    });

    // 5. Define Validity Checker (Box Obstacle)
    let validity_checker = Arc::new(BoxObstacleChecker {
        x_min: -0.5,
        x_max: 0.5,
        y_min: -2.0,
        y_max: 2.0,
    });

    // 6. Setup Planner (RRT)
    let mut planner = RRT::new(0.5, 0.1, &PlannerConfig { seed: Some(123) });
    planner.setup(problem_definition, validity_checker.clone());

    // 7. Solve
    println!("Solving Compound State planning problem...");
    let timeout = Duration::from_secs(5);
    match planner.solve(timeout) {
        Ok(path) => {
            println!("Solution found with {} states.", path.0.len());

            // Print first and last state to verify
            if let Some(first) = path.0.first() {
                let rv = first.components[0]
                    .as_any()
                    .downcast_ref::<RealVectorState>()
                    .unwrap();
                let so2 = first.components[1]
                    .as_any()
                    .downcast_ref::<SO2State>()
                    .unwrap();
                println!(
                    "Start: ({:.2}, {:.2}, {:.2})",
                    rv.values[0], rv.values[1], so2.value
                );
            }
            if let Some(last) = path.0.last() {
                let rv = last.components[0]
                    .as_any()
                    .downcast_ref::<RealVectorState>()
                    .unwrap();
                let so2 = last.components[1]
                    .as_any()
                    .downcast_ref::<SO2State>()
                    .unwrap();
                println!(
                    "End:   ({:.2}, {:.2}, {:.2})",
                    rv.values[0], rv.values[1], so2.value
                );
            }
        }
        Err(e) => println!("Planner failed: {:?}", e),
    }
}