Skip to content

Commit 9fe6749

Browse files
ratheronamacati
andauthored
Move parameters and update load_params (#119)
--------- Co-authored-by: Martin Schuck <57562633+amacati@users.noreply.github.com>
1 parent 5e53832 commit 9fe6749

33 files changed

Lines changed: 559 additions & 331 deletions

‎SKILL.md‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Settings are in `pyproject.toml`, layout in the tree, API reference in `docs/`.
1616
`array_namespace(...)`, do all math through it, coerce bound parameters with `to_xp`, and never
1717
import `numpy` into a computation path.
1818
- `crazyflow/control/mellinger/params.toml` deliberately holds values that differ from the true
19-
physical constants in `crazyflow/drones/params.toml`, reproducing the real firmware. Do not
19+
physical constants in `crazyflow/dynamics/*/params.toml`, reproducing the real firmware. Do not
2020
unify them.
2121

2222
## Testing
@@ -36,8 +36,8 @@ Use pixi, not uv. `pixi run <cmd>` resolves task names only, so arbitrary comman
3636
checked goes in a `pycon` fence with `>>>` prompts, which doctest picks up.
3737
- `tests/integration/test_examples.py` runs every script under `examples/`, so a new example is a
3838
new test.
39-
- `tests/conftest.py` forces JAX's persistent cache on at `/tmp/jax_cache`, shared across branches.
40-
Delete it when failures make no sense.
39+
- `tests/conftest.py` forces JAX's persistent cache on at `/tmp/jax_cache-<uid>`, shared across
40+
branches. Delete it when failures make no sense.
4141
- Request the `device` fixture rather than a GPU marker. It falls back to CPU silently, so
4242
`gpu-tests` asserts nothing about placement on a machine without CUDA.
4343

@@ -46,10 +46,12 @@ Use pixi, not uv. `pixi run <cmd>` resolves task names only, so arbitrary comman
4646
Grepping the name of an existing model or drone finds every registration site, except when matching
4747
against all models in the simulation's `build_control_fns`.
4848

49-
Define the function in `dynamics.py` and never in the package `__init__.py`, because `load_params`
50-
derives the model name from `fn.__module__.split(".")[-2]`. `parametrize` binds exactly the
51-
keyword-only parameters after the bare `*`, so anything before it is never bound. Every drone in
52-
`available_drones` needs a section in every `crazyflow/dynamics/*/params.toml`, even an empty one.
49+
Define the function in `dynamics.py` and never in the package `__init__.py`, because
50+
`load_fn_params` derives the model name from `fn.__module__.split(".")[-2]`. `parametrize`
51+
binds exactly the keyword-only parameters after the bare `*`, so anything before it is never bound.
52+
Every drone in `available_drones` needs a complete section in every model's
53+
`crazyflow/dynamics/*/params.toml`. The commented example at the top of each file lists the keys.
54+
Only `gravity_vec` is global, in `crazyflow/dynamics/params.toml`.
5355

5456
Registration alone produces roughly 40 parametrized tests. These do not include derivatives tests.
5557

@@ -60,9 +62,9 @@ Pure, batched, array-API functions with no dependency on `Sim`.
6062
- Import crazyflow before scipy. `crazyflow/__init__.py` sets `SCIPY_ARRAY_API=1` and imports scipy
6163
immediately, and scipy cannot be reconfigured once loaded. Transitive imports through acados or
6264
sklearn trigger this too.
63-
- Three different `load_params` exist. The two in `.core` filter to the target signature and
64-
silently drop the rest, so hardware constants like `thrust_max` need
65-
`crazyflow.drones.load_params`.
65+
- `dynamics` and `control` each have `load_params(name, drone)`, returning everything defined for
66+
the drone, and `load_fn_params(fn, drone)`, which filters to the signature of `fn` and
67+
drops the rest. Additional platform data is stored as a comment in the drone MJCF.
6668
- `parametrize` returns a `functools.partial` whose `keywords` dict is shared by every reference to
6769
it. Call `parametrize` again for an independent copy.
6870
- Leading batch dimensions, trailing feature axis. `quat` is scalar-last xyzw, `force` is `(..., 1)`

‎crazyflow/control/__init__.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
__all__ = []
1616

17-
from crazyflow.control.core import Control, load_params, parametrize
17+
from crazyflow.control.core import Control, load_fn_params, load_params, parametrize
1818
from crazyflow.control.mellinger import attitude2force_torque as mellinger_attitude2force_torque
1919
from crazyflow.control.mellinger import body_rate2force_torque as mellinger_body_rate2force_torque
2020
from crazyflow.control.mellinger import state2attitude as mellinger_state2attitude
@@ -25,4 +25,4 @@
2525
"mellinger_body_rate2force_torque": mellinger_body_rate2force_torque,
2626
}
2727

28-
__all__ = ["Control", "load_params", "parametrize"]
28+
__all__ = ["Control", "load_params", "load_fn_params", "parametrize"]

‎crazyflow/control/core.py‎

Lines changed: 68 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,46 @@
2222
R = TypeVar("R")
2323

2424

25+
class Control(StrEnum):
26+
"""Control type of the simulated onboard controller."""
27+
28+
state = "state"
29+
"""State control takes [x, y, z, vx, vy, vz, ax, ay, az, qx, qy, qz, qw, wx, wy, wz].
30+
31+
Note:
32+
Recommended frequency is >=20 Hz.
33+
34+
Warning:
35+
Only the yaw of the attitude quaternion is used, as in the firmware. The so_rpy family
36+
ignores the body rate setpoint.
37+
"""
38+
attitude = "attitude"
39+
"""Attitude control takes [roll, pitch, yaw, collective thrust].
40+
41+
Note:
42+
Recommended frequency is >=100 Hz.
43+
"""
44+
body_rate = "body_rate"
45+
"""Body rate control takes [wx, wy, wz, collective thrust].
46+
47+
Note:
48+
Recommended frequency is >=200 Hz.
49+
"""
50+
force_torque = "force_torque"
51+
"""Force and torque control takes [fc, tx, ty, tz].
52+
53+
Note:
54+
Recommended frequency is >=500 Hz.
55+
"""
56+
rotor_vel = "rotor_vel"
57+
"""Rotor velocity control takes [w1, w2, w3, w4] in RPMs.
58+
59+
Note:
60+
Recommended frequency is >=500 Hz.
61+
"""
62+
default = attitude
63+
64+
2565
def parametrize(
2666
fn: Callable[P, R], drone: str, xp: ModuleType | None = None, device: str | None = None
2767
) -> Callable[P, R]:
@@ -50,77 +90,57 @@ def parametrize(
5090
Returns:
5191
The parametrized controller function with all keyword argument only parameters filled in.
5292
"""
53-
return _parametrize(fn, drone, load_params, xp=xp, device=device)
93+
return _parametrize(fn, drone, load_fn_params, xp=xp, device=device)
5494

5595

5696
def load_params(
57-
fn: Callable, drone: str, xp: ModuleType | None = None, device: str | None = None
58-
) -> dict[str, Array]:
59-
"""Load the parameters a specific controller function accepts.
97+
controller: str, drone: str, xp: ModuleType | None = None, device: str | None = None
98+
) -> dict[str, dict[str, Array]]:
99+
"""Load all parameters of a drone for a controller.
60100
61-
Merges the ``"core"`` section with the function's ``[drone.<fn_name>]`` section (function values
62-
take precedence), then keeps only the parameters in ``fn``'s signature.
101+
Returns the drone's table of ``crazyflow/control/<controller>/params.toml``: the ``core``
102+
section shared by all functions of the controller, and one section per controller function.
63103
64104
Args:
65-
fn: The controller function for which to load parameters.
105+
controller: Name of the controller package, e.g. ``"mellinger"``.
66106
drone: Name of the drone configuration, e.g. ``"cf2x_L250"``.
67107
xp: The array API module to use. If not provided, numpy is used.
68108
device: The device to use. If None, the device is inferred from the xp module.
69109
70-
Returns:
71-
A flat dict mapping parameter names to arrays in the requested array namespace.
110+
Raises:
111+
KeyError: If ``controller`` has no ``params.toml`` or ``drone`` has no section in it.
72112
"""
73-
assert isinstance(fn, Callable), f"Expected a function, got {type(fn)}"
74-
controller = fn.__module__.split(".")[-2]
75113
params_path = Path(__file__).parent / f"{controller}/params.toml"
76114
if not params_path.exists():
77-
raise KeyError(f"`{controller}` not found. Available controllers: {tuple(Control)}")
115+
raise KeyError(f"Controller `{controller}` not found")
78116
with open(params_path, "rb") as f:
79117
params = tomllib.load(f)
80118
if drone not in params:
81119
raise KeyError(f"Drone `{drone}` not found in {controller}/params.toml")
82-
merged = params[drone].get("core", {}) | params[drone].get(fn.__name__, {})
83-
return to_xp(filter_to_signature(merged, fn), xp=xp, device=device)
84-
85-
86-
class Control(StrEnum):
87-
"""Control type of the simulated onboard controller."""
88-
89-
state = "state"
90-
"""State control takes [x, y, z, vx, vy, vz, ax, ay, az, qx, qy, qz, qw, wx, wy, wz].
91-
92-
Note:
93-
Recommended frequency is >=20 Hz.
120+
return to_xp(params[drone], xp=xp, device=device)
94121

95-
Warning:
96-
Only the yaw of the attitude quaternion is used, as in the firmware. The so_rpy family
97-
ignores the body rate setpoint.
98-
"""
99-
attitude = "attitude"
100-
"""Attitude control takes [roll, pitch, yaw, collective thrust].
101122

102-
Note:
103-
Recommended frequency is >=100 Hz.
104-
"""
105-
body_rate = "body_rate"
106-
"""Body rate control takes [wx, wy, wz, collective thrust].
123+
def load_fn_params(
124+
fn: Callable, drone: str, xp: ModuleType | None = None, device: str | None = None
125+
) -> dict[str, Array]:
126+
"""Load the parameters a controller function accepts.
107127
108-
Note:
109-
Recommended frequency is >=200 Hz.
110-
"""
111-
force_torque = "force_torque"
112-
"""Force and torque control takes [fc, tx, ty, tz].
128+
The controller is derived from the function's package. Merges the ``core`` section with the
129+
function's section (function values take precedence), then keeps only the parameters in
130+
``fn``'s signature.
113131
114-
Note:
115-
Recommended frequency is >=500 Hz.
116-
"""
117-
rotor_vel = "rotor_vel"
118-
"""Rotor velocity control takes [w1, w2, w3, w4] in RPMs.
132+
Args:
133+
fn: The controller function for which to load parameters.
134+
drone: Name of the drone configuration, e.g. ``"cf2x_L250"``.
135+
xp: The array API module to use. If not provided, numpy is used.
136+
device: The device to use. If None, the device is inferred from the xp module.
119137
120-
Note:
121-
Recommended frequency is >=500 Hz.
138+
Returns:
139+
A flat dict mapping parameter names to arrays in the requested array namespace.
122140
"""
123-
default = attitude
141+
assert callable(fn), f"Expected a function, got {type(fn)}"
142+
params = load_params(fn.__module__.split(".")[-2], drone, xp=xp, device=device)
143+
return filter_to_signature(params.get("core", {}) | params.get(fn.__name__, {}), fn)
124144

125145

126146
@jax.jit

‎crazyflow/control/mellinger/control.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from flax.struct import dataclass, field
2121
from scipy.spatial.transform import Rotation as R
2222

23-
from crazyflow.control.core import controllable, load_params
23+
from crazyflow.control.core import controllable, load_fn_params
2424
from crazyflow.control.transform import force2pwm, motor_force2rotor_vel, pwm2force
2525
from crazyflow.utils import CORE_NDIM_KEY, leaf_replace
2626

@@ -513,7 +513,7 @@ def create(
513513
zeros_3d = jnp.zeros((n_worlds, n_drones, 3), device=device)
514514
cmd = jnp.zeros((n_worlds, n_drones, 16), device=device).at[..., 12].set(1.0)
515515
steps = -jnp.ones((n_worlds, 1), dtype=jnp.int32, device=device)
516-
params = load_params(state2attitude, drone, xp=jnp, device=device)
516+
params = load_fn_params(state2attitude, drone, xp=jnp, device=device)
517517
return MellingerStateData(
518518
cmd=cmd,
519519
staged_cmd=cmd.copy(),
@@ -553,7 +553,7 @@ def create(
553553
zeros_3d = jnp.zeros((n_worlds, n_drones, 3), device=device)
554554
zeros_4d = jnp.zeros((n_worlds, n_drones, 4), device=device)
555555
steps = -jnp.ones((n_worlds, 1), dtype=jnp.int32, device=device)
556-
params = load_params(attitude2force_torque, drone, xp=jnp, device=device)
556+
params = load_fn_params(attitude2force_torque, drone, xp=jnp, device=device)
557557
return MellingerAttitudeData(
558558
cmd=zeros_4d.copy(),
559559
staged_cmd=zeros_4d.copy(),
@@ -595,7 +595,7 @@ def create(
595595
zeros_3d = jnp.zeros((n_worlds, n_drones, 3), device=device)
596596
zeros_4d = jnp.zeros((n_worlds, n_drones, 4), device=device)
597597
steps = -jnp.ones((n_worlds, 1), dtype=jnp.int32, device=device)
598-
params = load_params(body_rate2force_torque, drone, xp=jnp, device=device)
598+
params = load_fn_params(body_rate2force_torque, drone, xp=jnp, device=device)
599599
return MellingerBodyRateData(
600600
cmd=zeros_4d.copy(),
601601
staged_cmd=zeros_4d.copy(),
@@ -629,7 +629,7 @@ def create(
629629
) -> MellingerForceTorqueData:
630630
zeros_4d = jnp.zeros((n_worlds, n_drones, 4), device=device)
631631
steps = -jnp.ones((n_worlds, 1), dtype=jnp.int32, device=device)
632-
params = load_params(force_torque2rotor_vel, drone, xp=jnp, device=device)
632+
params = load_fn_params(force_torque2rotor_vel, drone, xp=jnp, device=device)
633633
return MellingerForceTorqueData(
634634
cmd=zeros_4d.copy(), staged_cmd=zeros_4d.copy(), steps=steps, freq=freq, params=params
635635
)

‎crazyflow/drones/__init__.py‎

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,16 @@
11
"""Hardware descriptions for the supported drone platforms.
22
3-
This package bundles the physical assets that define each drone configuration: the MuJoCo MJCF scene
4-
files, their referenced meshes (``assets/``), and the physical parameters shared across all dynamics
5-
(``params.toml`` with mass, inertia, thrust and torque curves, …). These describe the *hardware* and
6-
are independent of the dynamics formulation used to simulate it (see [crazyflow.dynamics][]).
3+
This package bundles the MuJoCo MJCF scene files that define each drone configuration and their
4+
referenced meshes (``assets/``). For the physical params, see [crazyflow.dynamics.load_params][].
75
8-
Use ``available_drones`` to enumerate the supported configurations, and ``load_params`` to read all
9-
physical parameters of a drone.
6+
Use ``available_drones`` to enumerate the supported configurations.
107
"""
118

12-
import tomllib
13-
from pathlib import Path
14-
159
# Currently supported platforms:
1610
# * **cf2x_L250** — Crazyflie 2.x
1711
# * **cf2x_P250** — Crazyflie 2.x with plus propellers
1812
# * **cf2x_T350** — Crazyflie 2.x with thrust upgrade kit
1913
# * **cf21B_500** — Crazyflie 2.1 Brushless with 500 mAh battery
2014
available_drones: tuple[str, ...] = ("cf2x_L250", "cf2x_P250", "cf2x_T350", "cf21B_500")
2115

22-
__all__ = ["available_drones", "load_params"]
23-
24-
25-
def load_params(drone: str) -> dict:
26-
"""Load all physical parameters of a drone from ``params.toml``.
27-
28-
Returns the raw values (lists/scalars) for the whole drone.
29-
30-
Args:
31-
drone: Name of the drone configuration, e.g. ``"cf2x_L250"``.
32-
"""
33-
with open(Path(__file__).parent / "params.toml", "rb") as f:
34-
params = tomllib.load(f)
35-
if drone not in params or drone not in available_drones:
36-
raise KeyError(f"Drone `{drone}` not found in drones/params.toml")
37-
return params[drone]
16+
__all__ = ["available_drones"]

‎crazyflow/drones/cf21B_500.xml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
<mujoco model="cf21B">
2+
<!--
3+
Platform data not used by the simulation, kept for estimators, firmware and tooling:
4+
rotor_dyn_coef_simple = 15.416891997523813
5+
thrust_dyn_coef = 15.09965949800411
6+
vmotor2thrust = [-0.014058926705279723, 0.04265273261724981, 0.0018327760144017432, 0.0020576974784587178] # Index is order
7+
vmotor2torque = [-0.00016088354909542246, 0.0003960426420309137, -4.6274122414327404e-5, 1.8490661674309596e-5] # TODO, Index is order
8+
vmotor2rpm = [2938.3995608848436, 6001.834195381014] # Index is order
9+
prop_radius = 27.5e-3 # TODO check
10+
-->
211
<compiler inertiafromgeom="false" meshdir="assets" autolimits="true" />
312

413
<asset>

‎crazyflow/drones/cf2x_L250.xml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
<mujoco model="cf2x">
2+
<!--
3+
Platform data not used by the simulation, kept for estimators, firmware and tooling:
4+
rotor_dyn_coef_simple = 6.886705423469015
5+
thrust_dyn_coef = 6.8932095506763345
6+
vmotor2thrust = [-0.014830744918356092, 0.04724465241828281, -0.01847364358025878, 0.005960923942142] # Index is order
7+
vmotor2torque = [-3.3261514624778425e-6, 8.109636684075977e-5, 5.751459172588052e-5, -1.898633582060136e-7] # Index is order
8+
vmotor2rpm = [2968.1791506049194, 6647.948592402306] # Index is order
9+
prop_radius = 23.55e-3 # TODO check
10+
-->
211
<compiler inertiafromgeom="false" meshdir="assets" autolimits="true" />
312

413
<asset>

‎crazyflow/drones/cf2x_P250.xml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
<mujoco model="cf2x">
2+
<!--
3+
Platform data not used by the simulation, kept for estimators, firmware and tooling:
4+
rotor_dyn_coef_simple = 7.709730027690284
5+
thrust_dyn_coef = 7.9435775497736785
6+
vmotor2thrust = [-0.02476537915958403, 0.06523793527519485, -0.026792504967750107, 0.006776789303971145] # Index is order
7+
vmotor2torque = [-2.8633106919309745e-5, 0.00011679386117520097, 5.105754520419129e-5, 0.0] # Index is order
8+
vmotor2rpm = [4657.542534331524, 7536.161830990926] # Index is order
9+
prop_radius = 23.4e-3 # TODO check
10+
-->
211
<compiler inertiafromgeom="false" meshdir="assets" autolimits="true" />
312

413
<asset>

‎crazyflow/drones/cf2x_T350.xml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
<mujoco model="cf2x">
2+
<!--
3+
Platform data not used by the simulation, kept for estimators, firmware and tooling:
4+
rotor_dyn_coef_simple = 11.352970450445243
5+
thrust_dyn_coef = 11.12424272978587
6+
vmotor2thrust = [0.006728127583707208, 0.01011557616217668, 0.010263198062061085, 0.0028358638322392503] # Index is order
7+
vmotor2torque = [-1.2906047901738756e-5, 0.0001436101487030899, 2.794753913624656e-5, 1.3104535533494383e-5] # Index is order
8+
vmotor2rpm = [2977.884883031915, 8101.0293594093055] # Index is order
9+
prop_radius = 25.4e-3 # TODO check
10+
-->
211
<compiler inertiafromgeom="false" meshdir="assets" autolimits="true" />
312

413
<asset>

0 commit comments

Comments
 (0)