Parametrize¶
Every controller function takes physical parameters (gains, mass, mixing matrix, PWM bounds) as keyword-only arguments, and the exact values differ per drone. Rather than passing them at every call site, parametrize loads them for a named drone and binds them upfront, so call sites only need to provide state and command.
The parameters stay individually accessible after binding. Because they are plain keyword-argument defaults on a functools.partial, any of them can be overridden at call time, or batched across a set of environments, without re-parametrizing the function. This makes it straightforward to randomize physical properties across a simulated batch.
import numpy as np
from drone_controllers import parametrize
from drone_controllers.mellinger import state2attitude
ctrl = parametrize(state2attitude, "cf2x_L250")
# Inspect what was bound
list(ctrl.keywords.keys())
# ['mass', 'kp', 'kd', 'ki', 'gravity_vec', 'mass_thrust',
# 'int_err_max', 'thrust_max', 'pwm_max']
Overriding parameters at call time¶
Because parametrize returns a functools.partial, the bound parameters are just keyword-argument defaults. Pass a different value at call time to override for that call only; ctrl.keywords is not modified:
import numpy as np
from drone_controllers import parametrize
from drone_controllers.mellinger import state2attitude
ctrl = parametrize(state2attitude, "cf2x_L250")
pos = np.zeros(3)
quat = np.array([0., 0., 0., 1.])
vel = np.zeros(3)
cmd = np.zeros(13)
# Simulate with a heavier drone for this call only.
rpyt, _ = ctrl(pos, quat, vel, cmd, mass=0.035)
To make a change persist across all future calls, mutate ctrl.keywords directly:
import numpy as np
from drone_controllers import parametrize
from drone_controllers.mellinger import state2attitude
ctrl = parametrize(state2attitude, "cf2x_L250")
ctrl.keywords["mass"] = np.float64(0.035)
Warning
ctrl.keywords is a mutable dict shared across all references to the same partial. Call parametrize again for an independent copy.
Available drone configurations¶
The following configurations ship with pre-fitted parameters:
drone_model |
Platform |
|---|---|
"cf2x_L250" |
Crazyflie 2.x, L250 props |
"cf2x_P250" |
Crazyflie 2.x, P250 props |
"cf2x_T350" |
Crazyflie 2.x, T350 props |
"cf21B_500" |
Crazyflie 2.1 Brushless, 500 props |
Pass the model name as a plain string:
import numpy as np
from drone_controllers import parametrize
from drone_controllers.mellinger import state2attitude
ctrl = parametrize(state2attitude, "cf2x_L250")
pos = np.zeros(3)
quat = np.array([0., 0., 0., 1.])
vel = np.zeros(3)
cmd = np.zeros(13)
rpyt, _ = ctrl(pos, quat, vel, cmd)
Loading raw parameters¶
Use load_params to inspect or override the values that parametrize would bind:
from drone_controllers.core import load_params
from drone_controllers.mellinger import state2attitude
params = load_params(state2attitude, "cf2x_L250")
float(params["mass"]) # 0.029
load_core_params returns the shared [core] section for an entire controller module without filtering to a specific function's signature. This is useful when you need parameters like rpm2thrust that are not accepted by a particular stage:
from drone_controllers import mellinger
from drone_controllers.core import load_core_params
core = load_core_params(mellinger, "cf2x_L250")
core["L"] # arm length [m]
Switching array backends¶
By default parameters are stored as NumPy arrays. Pass xp to convert them upfront, which avoids per-call conversion overhead in frameworks like PyTorch or JAX:
import torch
from drone_controllers import parametrize
from drone_controllers.mellinger import state2attitude
ctrl = parametrize(state2attitude, "cf2x_L250", xp=torch)
You can also specify a compute device:
import jax
import jax.numpy as jnp
from drone_controllers import parametrize
from drone_controllers.mellinger import state2attitude
ctrl = parametrize(
state2attitude, "cf2x_L250",
xp=jnp, device=jax.devices("cpu")[0],
)
The output backend is always inferred from the arrays you pass at call time, regardless of where the parameters live.