How to Implement Error Handling for the Quadrotor Simulation in Peng
Peng uses a centralized SimulationError enum in src/lib.rs combined with Rust's Result type and the ? operator to propagate failures from matrix operations, OSQP solvers, and logging initialization through every layer of the quadrotor simulation.
Peng is a minimal quadrotor autonomy framework written in Rust that prioritizes safety and debuggability through rigorous error handling. Implementing robust error handling for the quadrotor simulation in Peng requires understanding its centralized SimulationError enum, the Result type propagation pattern, and idiomatic Rust conversion traits.
Understanding the SimulationError Enum
All error conditions in Peng are represented by a single enum SimulationError defined in src/lib.rs. This enum derives thiserror::Error and Debug, automatically implementing std::error::Error with human-readable messages.
The enum variants cover every failure point in the quadrotor simulation:
| Variant | Description | Source Location |
|---|---|---|
RerunError |
Failure creating or using rerun.io recording streams | src/lib.rs lines 64-66 |
RerunSpawnError |
Process spawn problems for rerun tools | src/lib.rs lines 68-70 |
SetLoggerError |
Logger initialization failure | src/lib.rs lines 71-73 |
NalgebraError |
Linear algebra operations (matrix inversion) fail | src/lib.rs lines 74-76 |
OSQPError |
OSQP optimization problem setup or solve failure | src/lib.rs lines 77-79 |
NormalError |
Random distribution creation fails | src/lib.rs lines 80-82 |
OtherError |
Miscellaneous errors not fitting above categories | src/lib.rs lines 83-84 |
Propagating Errors with Result Types
Every public entry point that can fail returns Result<T, SimulationError>, allowing the ? operator to propagate errors concisely.
Quadrotor Construction
The Quadrotor::new method in src/lib.rs returns Result<Self, SimulationError> because it must invert the inertia matrix during initialization:
// src/lib.rs lines 41-55
pub fn new(
time_step: f32,
mass: f32,
gravity: f32,
arm_length: f32,
inertia: [f32; 9],
) -> Result<Self, SimulationError> {
// ... validation and matrix inversion that may fail
}
IMU and Sensor Initialization
The Imu struct constructor may return SimulationError::NormalError when initializing Gaussian noise distributions using rand_distr:
// src/lib.rs lines 23-28
pub fn new(
accelerometer_noise_std: f32,
gyroscope_noise_std: f32,
) -> Result<Self, SimulationError> {
// ... distribution creation that may fail
}
Configuration Loading
The Config::from_yaml method in src/config.rs returns a generic Box<dyn Error> that callers convert to SimulationError:
// src/config.rs lines 62-66
pub fn from_yaml(path: &str) -> Result<Self, Box<dyn Error>> {
let contents = std::fs::read_to_string(path)?;
let config: Config = serde_yaml::from_str(&contents)?;
Ok(config)
}
Main Simulation Loop
The entry point in src/main.rs returns Result<(), SimulationError>, allowing any error in the simulation loop to abort execution with a clear message:
// src/main.rs lines 4-5
fn main() -> Result<(), SimulationError> {
// ... simulation setup and loop using ? operator
}
Implementing Custom Error Handling Patterns
When extending Peng with new error-prone functionality, follow this five-step pattern to maintain consistency:
- Identify the failure point (file reads, math operations, third-party calls)
- Map the failure to an existing
SimulationErrorvariant or create a new variant with#[error("...")] - Use the
?operator to convertResult<T, E>intoResult<T, SimulationError>by implementingFrom<E>forSimulationError - Wrap external calls in helpers that add context when the error type doesn't map cleanly
- Log at the call site before bubbling if immediate visibility is needed
Implementing From Traits for External Errors
Convert standard library or crate errors into SimulationError using the From trait:
impl From<std::io::Error> for SimulationError {
fn from(err: std::io::Error) -> Self {
SimulationError::OtherError(err.to_string())
}
}
Wrapping External Calls with Context
When external libraries don't return types compatible with SimulationError, wrap them in helpers:
fn load_mesh(path: &str) -> Result<Mesh, SimulationError> {
std::fs::read_to_string(path)
.map_err(|e| SimulationError::OtherError(format!("Failed to read mesh: {}", e)))?
.parse()
.map_err(|e| SimulationError::OtherError(format!("Mesh parse error: {}", e)))
}
Logging Before Propagation
For critical simulation steps, log errors before returning them:
if let Err(e) = quad.update_dynamics_with_controls_euler(thrust, &torque) {
log::error!("Dynamics update failed: {}", e);
return Err(e);
}
Practical Code Examples for Error Handling in Peng
Loading Custom YAML Configurations
When loading simulation parameters from external files, convert generic errors to SimulationError:
use peng_quad::Config;
use peng_quad::SimulationError;
fn load_custom_cfg(path: &str) -> Result<Config, SimulationError> {
Config::from_yaml(path).map_err(|e| SimulationError::OtherError(e.to_string()))
}
Adding New Error Variants for External Libraries
Extend SimulationError when integrating new hardware or physics libraries:
// In src/lib.rs
#[derive(thiserror::Error, Debug)]
pub enum SimulationError {
// … existing variants …
#[error("External library error: {0}")]
ExternalLibError(String),
}
// Convert the external library's error type
impl From<external_lib::Error> for SimulationError {
fn from(err: external_lib::Error) -> Self {
SimulationError::ExternalLibError(err.to_string())
}
}
Matrix Inversion with Error Propagation
Handle singular matrix errors during dynamics calculations:
use nalgebra::Matrix3;
use peng_quad::SimulationError;
fn invert_matrix(m: Matrix3<f32>) -> Result<Matrix3<f32>, SimulationError> {
m.try_inverse()
.ok_or_else(|| SimulationError::NalgebraError("Matrix not invertible".into()))
}
Simulation Loop Error Handling
Propagate planner and controller errors through the main execution flow:
// Inside the main loop
let (desired_position, desired_velocity, desired_yaw) = planner_manager.update(
quad.position,
quad.orientation,
quad.velocity,
time,
&maze.obstacles,
)?;
If any planner returns a SimulationError, the ? operator propagates it to main(), which then aborts with a clear message.
Summary
- Peng centralizes all errors in the
SimulationErrorenum defined insrc/lib.rs, covering everything from matrix operations to OSQP solver failures. - Every fallible function returns
Result<T, SimulationError>, enabling the?operator to propagate errors concisely through the quadrotor construction, IMU initialization, and simulation loop. - The
thiserrorcrate automates boilerplate, implementingstd::error::ErrorandDisplayvia derive macros while allowing custom error messages for each variant. - Extending error handling follows a consistent pattern: implement
From<ExternalError>forSimulationError, use?for propagation, and wrap external calls with context when needed. - Testing error paths is straightforward because
SimulationErrorderivesDebugandPartialEq, allowingassert!(matches!(...))in unit tests.
Frequently Asked Questions
What is SimulationError in Peng?
SimulationError is a centralized error enum defined in src/lib.rs that represents every possible failure condition in the quadrotor simulation. It includes variants for linear algebra failures (NalgebraError), OSQP optimization errors (OSQPError), logging initialization issues (SetLoggerError), and miscellaneous external failures (OtherError). The enum derives thiserror::Error, which automatically implements the std::error::Error trait and provides formatted error messages.
How do I add custom error handling for new hardware drivers in Peng?
To add error handling for new hardware or external libraries, first add a variant to the SimulationError enum in src/lib.rs with a descriptive #[error("...")] attribute. Then implement From<ExternalDriverError> for SimulationError to enable automatic conversion. Finally, use the ? operator in your driver methods to propagate errors. If the external library uses complex error types that don't map cleanly, wrap the calls in helper functions that convert errors using map_err().
Why does Peng use Result types instead of panicking?
Peng uses Result<T, SimulationError> instead of panicking because the quadrotor simulation must handle recoverable failures gracefully, such as singular inertia matrices during vehicle construction or OSQP solver failures during trajectory optimization. Returning Result forces callers to explicitly handle error cases, prevents undefined states in the physics simulation, and allows the main loop in src/main.rs to abort cleanly with a descriptive error message rather than crashing the process.
How can I test error conditions in the Peng simulation?
You can test error conditions by leveraging the fact that SimulationError derives Debug and PartialEq via the thiserror crate. In unit tests, use assert!(matches!(result, Err(SimulationError::SpecificVariant))) to verify that functions return the expected error variants. For example, test that Quadrotor::new returns NalgebraError when passed a singular inertia matrix, or that Imu::new returns NormalError when given invalid noise 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 →