Skip to content

Quick Start

This page shows how to use the estimators directly from Python. To run them on a real system with motion capture data, see ROS 2.

All estimators share the same interface: set_state initializes the estimate with a first measurement, predict advances the estimate in time, and correct fuses a new pose measurement. Both predict and correct return the current estimate.

Variable Description Shape Convention
pos Measured position in world frame (3,) m
quat Measured orientation as unit quaternion (4,) scalar-last xyzw
dt Time since the last prediction () s

Unscented Kalman Filter

The KalmanFilter uses a dynamics model from crazyflow to predict the state between measurements. Since the dynamics depend on what the drone is commanded to do, predict takes the latest command. The command has to match the input of the model, e.g., roll, pitch, yaw (rad) and collective thrust (N) for the so_rpy models, or the four motor velocities (RPM) for first_principles.

import numpy as np

from drone_estimators.estimator_kalman import KalmanFilter

kf = KalmanFilter(
    dt=1 / 200,  # Nominal time step [s]
    model="so_rpy_rotor_drag",  # Any crazyflow dynamics model
    config="cf21B_500",  # Any drone the model has parameters for
    estimate_rotor_vel=True,
    estimate_dist_f=True,
    jit=True,
)
kf.set_state(np.array([0.0, 0.0, 1.0]), np.array([0.0, 0.0, 0.0, 1.0]))  # pos, quat (xyzw)

cmd = np.array([0.0, 0.0, 0.0, 0.42])  # roll, pitch, yaw [rad], collective thrust [N]
for k in range(200):
    pos = np.array([0.0, 0.0, 1.0 + 0.1 * k / 200])  # e.g., from a motion capture system
    quat = np.array([0.0, 0.0, 0.0, 1.0])
    kf.predict(1 / 200, cmd)
    estimate = kf.correct(pos, quat)

print(estimate.pos, estimate.vel, estimate.dist_f)

The estimate is an EstimatorData with the fields pos, quat, vel, ang_vel (body frame), and, if enabled, rotor_vel, dist_f, and dist_t. Fields that are not estimated are None.

Rotor velocities

estimate_rotor_vel=True requires a model with rotor dynamics, i.e., first_principles, so_rpy_rotor, or so_rpy_rotor_drag.

first_principles

The ROS node does not support first_principles, since it only receives attitude and thrust commands, not motor velocities. The noise parameters of the UKF are tuned for the so_rpy models and need to be adjusted for first_principles, see #15.

Differentiation-based filter

The DiffFilter needs neither a model nor commands. It low pass filters the measurements and their numerical derivatives with five time constants for the position, velocity, acceleration, orientation, and angular velocity.

import jax
import numpy as np

jax.config.update("jax_enable_x64", True)

from drone_estimators.estimator_diff import DiffFilter

estimator = DiffFilter((0.0001, 0.007, 0.09, 0.005, 0.07))  # Filter time constants
estimator.set_state(np.zeros(3), np.array([0.0, 0.0, 0.0, 1.0]))
for k in range(1, 200):
    pos = np.array([0.1 * k / 200, 0.0, 0.0])
    quat = np.array([0.0, 0.0, 0.0, 1.0])
    estimator.predict(1 / 200)
    estimate = estimator.correct(pos, quat)

print(estimate.pos, estimate.vel, estimate.ang_vel)

JIT compilation

With jit=True, the filter functions are compiled with jax.jit on construction, so the first measurement is not delayed by the compilation. The UKF defaults to jit=False, the differentiation-based filter to jit=True.

64 bit precision

The jitted differentiation-based filter needs 64 bit precision, since its numerical derivatives amplify rounding errors. Call jax.config.update("jax_enable_x64", True) before creating it, otherwise it raises a RuntimeError. The Kalman filters run in 32 bit as well.

Batching

All estimators support leading batch dimensions, e.g., one estimator for a whole swarm. Pass the batch_shape on construction and arrays with a leading batch dimension afterwards. Drones without a new measurement, e.g., because they are occluded, are excluded from the correction with a mask. The time step of predict can also differ per drone. It can be combined with JIT compilation for faster estimation loops.

import numpy as np

from drone_estimators.estimator_kalman import KalmanFilter

n_drones = 3
kf = KalmanFilter(1 / 200, "so_rpy", "cf2x_L250", batch_shape=(n_drones,))
kf.set_state(np.zeros((n_drones, 3)), np.tile([0.0, 0.0, 0.0, 1.0], (n_drones, 1)))

pos = np.zeros((n_drones, 3))
quat = np.tile([0.0, 0.0, 0.0, 1.0], (n_drones, 1))
has_measurement = np.array([True, False, True])  # e.g., the second drone is occluded
kf.predict(1 / 200)
estimate = kf.correct(pos, quat, mask=has_measurement)
print(estimate.pos.shape)  # (3, 3)