How Peng Simulates IMU Noise and Bias Drift in Quadrotor Simulations

Peng models IMU sensors by combining zero-mean Gaussian noise with a random-walk bias drift that evolves each simulation step, producing both short-term measurement jitter and realistic long-term sensor drift.

The makeecat/peng quadrotor simulator implements a high-fidelity Inertial Measurement Unit (IMU) model that captures the two dominant error sources found in real MEMS sensors: additive measurement noise and slowly varying bias drift. This approach allows researchers to test state estimation algorithms under realistic sensor degradation without hardware-in-the-loop. The implementation spans configuration loading, stochastic process modeling, and measurement synthesis across three core source files.

Configuration and Initialization

Loading IMU Parameters from YAML

Simulation parameters are centralized in a configuration structure defined in src/config.rs. The ImuConfig struct (lines 102-112) stores the standard deviations for both noise and drift processes, allowing users to characterize different sensor grades without recompiling.

// Excerpt from src/config.rs
pub struct ImuConfig {
    pub accel_noise_std: f64,
    pub gyro_noise_std: f64,
    pub accel_bias_std: f64,
    pub gyro_bias_std: f64,
}

These values are deserialized from the simulation YAML file and passed directly to the IMU constructor.

Instantiating the IMU State

The Imu struct in src/lib.rs (lines 83-95) maintains the internal sensor state, including current bias vectors and pre-allocated probability distributions. During initialization in Imu::new (lines 24-40), the constructor creates four independent normal distributions with zero mean:

  • accel_noise — accelerometer measurement noise
  • gyro_noise — gyroscope measurement noise
  • accel_bias_drift — accelerometer bias random walk
  • gyro_bias_drift — gyroscope bias random walk

The bias vectors are initialized to zero, while the random number generator is seeded for reproducibility.

Bias Drift Modeling via Random Walk

Real IMU biases wander over time due to temperature fluctuations and sensor aging. Peng models this as a discrete-time random walk integrated in the Imu::update method (lines 55-61).

At each simulation step with timestep dt, the drift update follows the equation:

// From src/lib.rs, Imu::update
let accel_drift_sample = self.accel_bias_distr.sample(&mut self.rng) * dt.sqrt();
let gyro_drift_sample = self.gyro_bias_distr.sample(&mut self.rng) * dt.sqrt();

self.accel_bias += accel_drift_sample;
self.gyro_bias += gyro_drift_sample;

The √dt scaling is critical for proper stochastic integration, ensuring the variance grows linearly with time rather than with the square of time. This produces slowly evolving biases that exhibit the characteristic "wandering" behavior of physical IMUs without instantaneous jumps.

Noisy Measurement Generation

When the simulation requests a sensor reading, the Imu::read method (lines 84-95) synthesizes realistic measurements by superimposing the current drift state and fresh noise samples onto the ground-truth values from the physics engine:

// From src/lib.rs, Imu::read
let meas_acc = true_acc + self.accel_bias + self.accel_noise_distr.sample(&mut self.rng);
let meas_gyro = true_gyro + self.gyro_bias + self.gyro_noise_distr.sample(&mut self.rng);

This additive error model follows the standard IMU equation: measured value = true value + bias + white noise. The bias term incorporates all accumulated drift from previous updates, while the noise term represents the instantaneous sensor uncertainty.

Practical Implementation Example

Integrating the IMU into the main simulation loop requires coordinating the physics timestep with the stochastic update rate. The following pattern mirrors the implementation in src/main.rs:

// Load configuration containing IMU noise parameters
let cfg = Config::from_yaml("config/quad.yaml")?;
let imu_cfg = cfg.imu;

// Initialize IMU with configured standard deviations
let mut imu = Imu::new(
    imu_cfg.accel_noise_std,
    imu_cfg.gyro_noise_std,
    imu_cfg.accel_bias_std,
    imu_cfg.gyro_bias_std,
)?;

// Inside the simulation loop
let dt = 0.001; // 1 kHz simulation rate
imu.update(dt)?; // Evolve bias drift by √dt

// Obtain true dynamics from quadrotor physics
let (true_acc, true_gyro) = quad.read_imu()?;

// Generate corrupted measurements for estimator testing
let (meas_acc, meas_gyro) = imu.read(true_acc, true_gyro)?;

This workflow ensures that the bias drift accumulates continuously while each individual reading receives independent noise samples.

Summary

  • Configuration-driven modeling: Noise and drift characteristics are loaded via ImuConfig in src/config.rs, enabling rapid parameter sweeps without code changes.
  • Physically accurate drift: The Imu::update method implements random walk integration with √dt scaling to prevent variance explosion and match continuous-time stochastic processes.
  • Additive error synthesis: The Imu::read method combines ground-truth physics, accumulated bias states, and fresh Gaussian noise to replicate real sensor outputs.
  • Modular architecture: The separation of drift evolution (update) and measurement generation (read) allows users to decouple the bias simulation rate from the sensor polling rate if desired.

Frequently Asked Questions

How does Peng prevent IMU bias from growing unbounded during long simulations?

Peng treats bias drift as a random walk (integrated white noise) rather than a random constant. While the variance grows with time, the implementation relies on the √dt scaling factor in Imu::update to ensure physically realistic drift rates. For extremely long simulations, users should configure the accel_bias_std and gyro_bias_std parameters in the YAML file to match the specific Allan variance characteristics of their target hardware, as real MEMS sensors exhibit bias instability that flattens out over time.

Can I simulate different IMU grades (consumer vs. tactical) in the same simulation?

Yes. Because the noise characteristics are passed as parameters to Imu::new rather than hardcoded, you can instantiate multiple Imu instances with different standard deviations within the same simulation. For example, instantiate one IMU with accel_noise_std: 0.1 to model a low-cost MEMS unit and another with accel_noise_std: 0.001 for a tactical-grade sensor, then compare estimator performance against both datasets simultaneously.

Why does the bias update use dt.sqrt() instead of just dt?

The square-root scaling preserves the variance properties of continuous-time white noise when discretized. Continuous white noise has infinite variance but finite integrated variance over time; when sampled at interval dt, the discrete variance must scale with dt (not dt²). Since standard deviation is the square root of variance, the code multiplies by √dt. Without this scaling, the bias would drift artificially fast at high simulation rates or too slowly at low rates, violating the physical units of the noise power spectral density.

Where are the true accelerometer and gyroscope values generated before corruption?

The ground-truth inertial values are computed by the quadrotor physics model and exposed through quad.read_imu() as shown in the usage example. The Imu struct itself is purely a corruption layer; it does not simulate dynamics. This separation of concerns allows the IMU model to remain agnostic of the underlying vehicle physics while ensuring that the noise and drift are applied to physically consistent specific force and angular velocity vectors.

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 →