System identification¶
If your drone is not among the supported configurations, or if you want to refine the existing parameters with your own hardware, the system identification pipeline fits the dynamics coefficients from recorded flight data. It handles data preprocessing, derivative estimation, and least-squares parameter fitting for both translational and rotational dynamics.
The pipeline is part of crazyflow.dynamics.utils. Plotting the fit (plot=True) additionally requires matplotlib.
Required data format¶
The pipeline expects a Python dict of NumPy arrays assembled from your flight log. The keys below are required by preprocessing:
| Key | Shape | Units | Description |
|---|---|---|---|
"time" |
(N,) |
s | Timestamps (need not be evenly spaced) |
"pos" |
(N, 3) |
m | Position in world frame |
"quat" |
(N, 4) |
— | Orientation quaternion (xyzw) |
"cmd_rpy" |
(N, 3) |
rad | Commanded roll/pitch/yaw |
"cmd_f" |
(N,) |
N | Commanded collective thrust |
After preprocessing + derivatives_svf, the dict is augmented with filtered signals and numerical derivatives. The identification functions read SVF_vel, SVF_acc, SVF_quat, SVF_cmd_f (translation) and SVF_rpy, SVF_cmd_rpy (rotation).
Full pipeline¶
from crazyflow.dynamics.utils.data_utils import preprocessing, derivatives_svf
from crazyflow.dynamics.utils.identification import sys_id_translation, sys_id_rotation
# Step 1 — assemble raw data dict from your flight log
data = {
"time": time_array, # (N,) seconds
"pos": pos_array, # (N, 3) metres
"quat": quat_array, # (N, 4) xyzw
"cmd_rpy": cmd_rpy_array, # (N, 3) radians
"cmd_f": cmd_f_array, # (N,) Newtons
}
# Step 2 — outlier removal, quaternion normalisation, RPY calculation
data = preprocessing(data)
# Step 3 — low-pass filter and compute time derivatives via State Variable Filter
data = derivatives_svf(data)
# Step 4 — fit translational parameters
trans_params = sys_id_translation(
dynamics="so_rpy_rotor_drag",
mass=0.0319, # drone mass in kg — measure this directly
data=data,
verbose=0, # 0 = silent, 1 = progress, 2 = full optimizer output
plot=True, # show fit vs. measured plots
)
# Returns: {'cmd_f_coef': ..., 'thrust_time_coef': ...,
# 'drag_xy_coef': ..., 'drag_z_coef': ...}
# Step 5 — fit rotational parameters
rot_params = sys_id_rotation(data=data, verbose=0, plot=True)
# Returns: {'rpy_coef': (3,), 'rpy_rates_coef': (3,), 'cmd_rpy_coef': (3,)}
See sys_id_translation and sys_id_rotation in the API reference for the full argument list.
Validation¶
To check that the identified parameters generalise to unseen flight regimes, collect a second dataset of different trajectories and pass it as data_validation. RMSE and R² are then reported on both the training data and the validation data.
# Preprocess the validation dataset independently — it must come from
# different trajectories, not a split of the same recording.
data_valid = preprocessing(validation_raw_data)
data_valid = derivatives_svf(data_valid)
trans_params = sys_id_translation(
dynamics="so_rpy_rotor_drag",
mass=0.0319,
data=data,
data_validation=data_valid,
plot=True,
)
Using identified parameters¶
Once you have the identified coefficients, add them to the relevant params.toml file under a new drone name. Each dynamics sub-package ships its own params.toml — for example crazyflow/dynamics/so_rpy_rotor_drag/params.toml — and load_params reads from it when you call parametrize. Add a new section using the TOML table syntax:
[my_drone]
cmd_f_coef = 0.983 # from trans_params["cmd_f_coef"]
thrust_time_coef = 0.121 # from trans_params["thrust_time_coef"]
drag_matrix = [[-0.0147, 0.0, 0.0],
[0.0, -0.0147, 0.0],
[0.0, 0.0, -0.0128]] # diag([drag_xy, drag_xy, drag_z])
rpy_coef = [-245.67, -245.67, -227.78] # from rot_params["rpy_coef"]
rpy_rates_coef = [-17.32, -17.32, -25.63] # from rot_params["rpy_rates_coef"]
cmd_rpy_coef = [196.18, 196.18, 390.27] # from rot_params["cmd_rpy_coef"]
Note
sys_id_translation returns drag_xy_coef and drag_z_coef as scalars. Assemble the diagonal drag_matrix manually: [drag_xy, drag_xy, drag_z] on the diagonal.
Once the entry is in the TOML file, load the dynamics as usual:
from crazyflow.dynamics import parametrize
from crazyflow.dynamics.so_rpy_rotor_drag import dynamics
dynamics = parametrize(dynamics, drone="my_drone")
Support for new drones can be added to the shared parameter files via a pull request on GitHub.
Which dynamics to identify¶
Choose based on which physical effects you need to capture:
so_rpy— identifies onlycmd_f_coef; no motor dynamics, no drag. Fastest to calibrate, good for slow flight.so_rpy_rotor— addsthrust_time_coefto model motor spin-up delay. Better for agile maneuvers.so_rpy_rotor_drag— addsdrag_xy_coefanddrag_z_coef. Best accuracy at higher speeds where aerodynamic drag is significant.