Parametrize¶
Each dynamics function has a large number of keyword-only parameters — mass, inertia matrix, thrust curves, drag coefficients, and so on. Passing all of them at every call would be impractical. parametrize solves this by loading the parameters for a named drone configuration and returning a functools.partial with those parameters pre-filled. You then call the returned object with just the state and command arrays.
from crazyflow.dynamics import parametrize
from crazyflow.dynamics.first_principles import dynamics
dynamics = parametrize(dynamics, drone="cf2x_L250")
# Inspect what was pre-filled
list(dynamics.keywords.keys())
# ['mass', 'L', 'prop_inertia', 'gravity_vec', 'J', 'J_inv',
# 'rpm2thrust', 'rpm2torque', 'mixing_matrix', 'rotor_dyn_coef', 'drag_matrix']
Available drone configurations¶
The following configurations ship with pre-fitted parameters. They cover both the brushed Crazyflie 2.x series and the brushless Crazyflie 2.1:
from crazyflow.drones import available_drones
available_drones # ('cf2x_L250', 'cf2x_P250', 'cf2x_T350', 'cf21B_500')
drone |
Platform |
|---|---|
"cf2x_L250" |
Crazyflie 2.x |
"cf2x_P250" |
Crazyflie 2.x, plus propellers |
"cf2x_T350" |
Crazyflie 2.x, thrust upgrade kit |
"cf21B_500" |
Crazyflie 2.1 Brushless |
If your drone is not listed, you can identify the parameters from flight data using the system identification pipeline and inject them into any dynamics.
Switching array backends¶
By default, parametrize stores parameters as NumPy arrays. For frameworks that would otherwise need to convert those arrays on every call — such as PyTorch, where NumPy arrays must become tensors — passing xp converts the parameters upfront. The backend of the outputs is always inferred from whatever arrays you pass in at call time.
import torch
import jax.numpy as jnp
from crazyflow.dynamics import parametrize
from crazyflow.dynamics.first_principles import dynamics
dynamics_torch = parametrize(dynamics, drone="cf2x_L250", xp=torch)
dynamics_jax = parametrize(dynamics, drone="cf2x_L250", xp=jnp)
You can also specify a compute device — for example, to move JAX parameters to GPU at construction time:
import jax
import jax.numpy as jnp
dynamics_gpu = parametrize(
dynamics, drone="cf2x_L250", xp=jnp, device=jax.devices("gpu")[0]
)
Overriding parameters at call time¶
Because parametrize returns a functools.partial, the stored parameters are just keyword argument defaults. You can override any of them for a single call by passing a new value as a keyword argument — the call-time value takes precedence and the stored defaults are unchanged.
import numpy as np
from crazyflow.dynamics import parametrize
from crazyflow.dynamics.first_principles import dynamics
dynamics = parametrize(dynamics, drone="cf2x_L250")
pos = np.zeros(3)
quat = np.array([0.0, 0.0, 0.0, 1.0])
vel = np.zeros(3)
ang_vel = np.zeros(3)
rotor_vel = np.zeros(4)
cmd = np.zeros(4)
# Simulate with a 10 g payload for this call only — dynamics.keywords is not modified.
pos_dot, *_ = dynamics(pos, quat, vel, ang_vel, cmd, rotor_vel, mass=0.0419)
This becomes particularly useful for domain randomization: instead of baking randomized parameters into the partial, you can pass a batch of them as call-time arguments and keep the step function JIT-compiled across parameter changes. See Batching & domain randomization for the full pattern.
Mutating stored parameters¶
You can also modify dynamics.keywords directly for changes that should persist across all future calls:
import numpy as np
from crazyflow.dynamics import parametrize
from crazyflow.dynamics.first_principles import dynamics
dynamics = parametrize(dynamics, drone="cf2x_L250")
dynamics.keywords["mass"] = np.float64(0.040) # heavier drone — applies to every call
Warning
dynamics.keywords is a mutable dict shared across all references to the same partial. Modifying it affects every call. Call parametrize again for an independent copy.
Selecting dynamics programmatically¶
available_dynamics is a dict mapping dynamics names to their unparametrized functions. This is useful when selecting a dynamics by name.
from crazyflow.dynamics import available_dynamics, parametrize
list(available_dynamics) # ['first_principles', 'so_rpy', 'so_rpy_rotor', 'so_rpy_rotor_drag']
dynamics = available_dynamics["so_rpy_rotor_drag"]
parametrized_dynamics = parametrize(dynamics, drone="cf2x_T350")
Loading raw parameters¶
If you need the parameter values directly, for example, to pass them to symbolic_dynamics, use load_params:
from crazyflow.dynamics.core import load_params
params = load_params(dynamics, "cf2x_L250")
params["mass"] # 0.0319
params["J_inv"] # array([...])
With a parametrized dynamics in hand, you can evaluate a single state. The next page covers running many drones simultaneously by adding batch dimensions — and how to randomize physical parameters across that batch for domain randomization.