Skip to content

Commit b9f22de

Browse files
committed
Rewrite tests. Fix bug in so_rpy_rotor_drag
1 parent ecca817 commit b9f22de

2 files changed

Lines changed: 188 additions & 88 deletions

File tree

drone_models/so_rpy_rotor_drag/model.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,10 @@ def dynamics(
125125
if dist_t is not None:
126126
# adding torque disturbances to the state
127127
# angular acceleration can be converted to total torque given the inertia matrix
128-
torque = ang_vel_dot @ J.mT + xp.linalg.cross(ang_vel, ang_vel @ J.mT)
129-
# adding torque
128+
torque = (J @ ang_vel_dot[..., None])[..., 0]
129+
torque = torque + xp.linalg.cross(ang_vel, (J @ ang_vel[..., None])[..., 0])
130+
# adding torque. TODO: This should be a linear transformation. Can't we just transform the
131+
# disturbance torque to an ang_vel_dot summand directly?
130132
torque = torque + rot.apply(dist_t, inverse=True)
131133
# back to angular acceleration
132134
torque = torque - xp.linalg.cross(ang_vel, (J @ ang_vel[..., None])[..., 0])

tests/unit/test_models.py

Lines changed: 184 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
from drone_models import available_models, model_features
1717
from drone_models.core import parametrize
1818
from drone_models.drones import available_drones
19+
from drone_models.first_principles import dynamics as first_principle_dynamics
20+
from drone_models.so_rpy import dynamics as so_rpy_dynamics
21+
from drone_models.so_rpy_rotor import dynamics as so_rpy_rotor_dynamics
22+
from drone_models.so_rpy_rotor_drag import dynamics as so_rpy_rotor_drag_dynamics
1923

2024
if TYPE_CHECKING:
2125
from array_api_typing import Array
@@ -41,16 +45,19 @@ def create_rnd_commands(shape: tuple[int, ...] = (), dim: int = 4) -> Array:
4145
return xp.abs(xp.asarray(np.random.randn(*shape, dim))) # Motor forces must be positive
4246

4347

44-
def assert_array_meta(x: Array | None, y: Array | None):
48+
def assert_array_meta(x: Array | None, y: Array | None, name: str | None = None):
4549
"""Assert the output is on the correct device, has the correct type and shape."""
4650
if x is None and y is None:
4751
return
48-
assert isinstance(x, type(y)), f"Output type {type(x)} does not match expected {type(y)}"
52+
prefix = "" if name is None else f"{name}: "
53+
assert isinstance(x, type(y)), (
54+
f"{prefix}Output type {type(x)} does not match expected {type(y)}"
55+
)
4956
assert xp_device(x) == xp_device(y), (
50-
f"Output device {xp_device(x)} does not match expected {xp_device(y)}"
57+
f"{prefix}Output device {xp_device(x)} does not match expected {xp_device(y)}"
5158
)
52-
assert x.shape == y.shape, f"Output shape {x.shape} does not match expected {y.shape}"
53-
assert np.all(np.isnan(x) == np.isnan(y)), "Derivative of non-nan values are NaN"
59+
assert x.shape == y.shape, f"{prefix}Output shape {x.shape} does not match expected {y.shape}"
60+
assert np.all(np.isnan(x) == np.isnan(y)), f"{prefix}Derivative of non-nan values are NaN"
5461

5562

5663
def skip_models_without_features(model: Callable, features: list[str]):
@@ -60,6 +67,31 @@ def skip_models_without_features(model: Callable, features: list[str]):
6067
pytest.skip(f"Model {model.__name__} does not have the feature '{feature}'.")
6168

6269

70+
def assert_dynamics_shapes(
71+
dynamics: Callable,
72+
batch: int[...] = (),
73+
rotor_vel_input: bool = False,
74+
ext_force_torque: bool = False,
75+
):
76+
pos, quat, vel, ang_vel, rotor_vel, f_ext, t_ext = create_rnd_states(shape=batch)
77+
cmd = create_rnd_commands(shape=batch, dim=4)
78+
if not rotor_vel_input:
79+
rotor_vel = None
80+
if not ext_force_torque:
81+
f_ext, t_ext = None, None
82+
dpos, dquat, dvel, dang_vel, drotor_vel = dynamics(
83+
pos, quat, vel, ang_vel, cmd, rotor_vel, f_ext, t_ext
84+
)
85+
# Check if the output is on the correct device, has the correct type and shape
86+
for name, dx, x in zip(
87+
["dpos", "dquat", "dvel", "dang_vel", "drotor_vel"],
88+
[dpos, dquat, dvel, dang_vel, drotor_vel],
89+
[pos, quat, vel, ang_vel, rotor_vel],
90+
strict=True,
91+
):
92+
assert_array_meta(dx, x, name=name)
93+
94+
6395
@pytest.mark.unit
6496
@pytest.mark.parametrize("model_name, model", available_models.items())
6597
def test_model_features(model_name: str, model: Callable):
@@ -77,113 +109,179 @@ def test_model_features(model_name: str, model: Callable):
77109

78110

79111
@pytest.mark.unit
80-
@pytest.mark.parametrize("model_name, model", available_models.items())
81112
@pytest.mark.parametrize("drone_type", available_drones)
82-
def test_model_single_no_rotor_dynamics(model_name: str, model: Callable, drone_type: str):
83-
pos, quat, vel, ang_vel, _, _, _ = create_rnd_states()
84-
model = parametrize(model, drone_type)
85-
cmd = create_rnd_commands(dim=4) # TODO make dependent on model
86-
if model_features(model)["rotor_dynamics"]:
87-
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
88-
dpos, dquat, dvel, dang_vel, drotor_vel = model(pos, quat, vel, ang_vel, cmd)
89-
else:
90-
dpos, dquat, dvel, dang_vel, drotor_vel = model(pos, quat, vel, ang_vel, cmd)
91-
assert drotor_vel is None, "Model should not return rotor velocities without rotor_vel input"
92-
# Check if the output is on the correct device, has the correct type and shape
93-
for dx, x in zip([dpos, dquat, dvel, dang_vel], [pos, quat, vel, ang_vel], strict=True):
94-
assert_array_meta(dx, x)
113+
def test_first_principle_dynamics(drone_type: str):
114+
dynamics = parametrize(first_principle_dynamics, drone_type)
115+
assert model_features(dynamics)["rotor_dynamics"], "Model should support rotor dynamics"
116+
117+
assert_dynamics_shapes(dynamics, rotor_vel_input=True)
118+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
119+
assert_dynamics_shapes(dynamics, rotor_vel_input=False)
120+
121+
# External force-torque
122+
assert_dynamics_shapes(dynamics, rotor_vel_input=True, ext_force_torque=True)
123+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
124+
assert_dynamics_shapes(dynamics, rotor_vel_input=False, ext_force_torque=True)
95125

96126

97127
@pytest.mark.unit
98-
@pytest.mark.parametrize("model_name, model", available_models.items())
99128
@pytest.mark.parametrize("drone_type", available_drones)
100-
def test_model_single_rotor_dynamics(model_name: str, model: Callable, drone_type: str):
101-
skip_models_without_features(model, ["rotor_dynamics"])
102-
model = parametrize(model, drone_type)
129+
def test_first_principle_dynamics_batched(drone_type: str):
130+
dynamics = parametrize(first_principle_dynamics, drone_type, xp=xp)
131+
assert model_features(dynamics)["rotor_dynamics"], "Model should support rotor dynamics"
132+
133+
shape = (10, 5)
134+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=True)
135+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
136+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False)
137+
138+
# External force-torque
139+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=True, ext_force_torque=True)
140+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
141+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False, ext_force_torque=True)
142+
143+
# Batch params
144+
dynamics.keywords["J"] = xp.tile(dynamics.keywords["J"][None, None, ...], shape + (1, 1))
145+
dynamics.keywords["J_inv"] = xp.tile(
146+
dynamics.keywords["J_inv"][None, None, ...], shape + (1, 1)
147+
)
148+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=True)
149+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
150+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False)
103151

104-
pos, quat, vel, ang_vel, rotor_vel, _, _ = create_rnd_states()
105-
cmd = create_rnd_commands(dim=4)
106-
dpos, dquat, dvel, dang_vel, drotor_vel = model(pos, quat, vel, ang_vel, cmd, rotor_vel)
107-
# Check if the output is on the correct device, has the correct type and shape
108-
for dx, x in zip(
109-
[dpos, dquat, dvel, dang_vel, drotor_vel], [pos, quat, vel, ang_vel, rotor_vel], strict=True
110-
):
111-
assert_array_meta(dx, x)
152+
# External force-torque
153+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=True, ext_force_torque=True)
154+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
155+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False, ext_force_torque=True)
112156

113157

114158
@pytest.mark.unit
115-
@pytest.mark.parametrize("model_name, model", available_models.items())
116159
@pytest.mark.parametrize("drone_type", available_drones)
117-
def test_model_single_external_wrench(model_name: str, model: Callable, drone_type: str):
118-
model = parametrize(model, drone_type)
119-
pos, quat, vel, ang_vel, rotor_vel, dist_f, dist_t = create_rnd_states()
120-
if not model_features(model)["rotor_dynamics"]:
121-
rotor_vel = None
122-
cmd = create_rnd_commands(dim=4) # TODO make dependent on model
123-
dpos, dquat, dvel, dang_vel, drotor_vel = model(
124-
pos, quat, vel, ang_vel, cmd, rotor_vel, dist_f, dist_t
160+
def test_so_rpy(drone_type: str):
161+
dynamics = parametrize(so_rpy_dynamics, drone_type)
162+
assert not model_features(dynamics)["rotor_dynamics"], "Model should not support rotor dynamics"
163+
164+
assert_dynamics_shapes(dynamics, rotor_vel_input=False)
165+
166+
# External force-torque
167+
assert_dynamics_shapes(dynamics, rotor_vel_input=False, ext_force_torque=True)
168+
169+
170+
@pytest.mark.unit
171+
@pytest.mark.parametrize("drone_type", available_drones)
172+
def test_so_rpy_batched(drone_type: str):
173+
dynamics = parametrize(so_rpy_dynamics, drone_type, xp=xp)
174+
assert not model_features(dynamics)["rotor_dynamics"], "Model should not support rotor dynamics"
175+
176+
shape = (10, 5)
177+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False)
178+
179+
# External force-torque
180+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False, ext_force_torque=True)
181+
182+
# Batch params
183+
dynamics.keywords["J"] = xp.tile(dynamics.keywords["J"][None, None, ...], shape + (1, 1))
184+
dynamics.keywords["J_inv"] = xp.tile(
185+
dynamics.keywords["J_inv"][None, None, ...], shape + (1, 1)
125186
)
126-
# Check if the output is on the correct device, has the correct type and shape
127-
for dx, x in zip([dpos, dquat, dvel, dang_vel], [pos, quat, vel, ang_vel], strict=True):
128-
assert_array_meta(dx, x)
187+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False)
188+
189+
# External force-torque
190+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False, ext_force_torque=True)
129191

130192

131193
@pytest.mark.unit
132-
@pytest.mark.parametrize("model_name, model", available_models.items())
133194
@pytest.mark.parametrize("drone_type", available_drones)
134-
def test_model_batched_no_rotor_dynamics(model_name: str, model: Callable, drone_type: str):
135-
model = parametrize(model, drone_type)
136-
batch_shape = (10, 5)
137-
pos, quat, vel, ang_vel, _, _, _ = create_rnd_states(batch_shape)
138-
cmd = create_rnd_commands(batch_shape, dim=4) # TODO make dependent on model
139-
if model_features(model)["rotor_dynamics"]:
140-
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
141-
dpos, dquat, dvel, dang_vel, drotor_vel = model(pos, quat, vel, ang_vel, cmd, None)
142-
else:
143-
dpos, dquat, dvel, dang_vel, drotor_vel = model(pos, quat, vel, ang_vel, cmd, None)
144-
assert drotor_vel is None, "Model should not return rotor velocities without rotor_vel input"
145-
# Check if the output is on the correct device, has the correct type and shape
146-
for dx, x in zip([dpos, dquat, dvel, dang_vel], [pos, quat, vel, ang_vel], strict=True):
147-
assert_array_meta(dx, x)
195+
def test_so_rpy_rotor(drone_type: str):
196+
dynamics = parametrize(so_rpy_rotor_dynamics, drone_type)
197+
assert model_features(dynamics)["rotor_dynamics"], "Model should support rotor dynamics"
198+
199+
assert_dynamics_shapes(dynamics, rotor_vel_input=True)
200+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
201+
assert_dynamics_shapes(dynamics, rotor_vel_input=False)
202+
203+
# External force-torque
204+
assert_dynamics_shapes(dynamics, rotor_vel_input=True, ext_force_torque=True)
205+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
206+
assert_dynamics_shapes(dynamics, rotor_vel_input=False, ext_force_torque=True)
148207

149208

150209
@pytest.mark.unit
151-
@pytest.mark.parametrize("model_name, model", available_models.items())
152210
@pytest.mark.parametrize("drone_type", available_drones)
153-
def test_model_batched_rotor_dynamics(model_name: str, model: Callable, drone_type: str):
154-
skip_models_without_features(model, ["rotor_dynamics"])
155-
model = parametrize(model, drone_type)
211+
def test_so_rpy_rotor_batched(drone_type: str):
212+
dynamics = parametrize(so_rpy_rotor_dynamics, drone_type, xp=xp)
213+
assert model_features(dynamics)["rotor_dynamics"], "Model should support rotor dynamics"
214+
215+
shape = (10, 5)
216+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=True)
217+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
218+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False)
219+
220+
# External force-torque
221+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=True, ext_force_torque=True)
222+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
223+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False, ext_force_torque=True)
224+
225+
# Batch params
226+
dynamics.keywords["J"] = xp.tile(dynamics.keywords["J"][None, None, ...], shape + (1, 1))
227+
dynamics.keywords["J_inv"] = xp.tile(
228+
dynamics.keywords["J_inv"][None, None, ...], shape + (1, 1)
229+
)
230+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=True)
231+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
232+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False)
156233

157-
batch_shape = (10, 5)
158-
pos, quat, vel, ang_vel, rotor_vel, _, _ = create_rnd_states(batch_shape)
159-
cmd = create_rnd_commands(batch_shape, dim=4)
160-
dpos, dquat, dvel, dang_vel, drotor_vel = model(pos, quat, vel, ang_vel, cmd, rotor_vel)
161-
# Check if the output is on the correct device, has the correct type and shape
162-
for dx, x in zip(
163-
[dpos, dquat, dvel, dang_vel, drotor_vel], [pos, quat, vel, ang_vel, rotor_vel], strict=True
164-
):
165-
assert_array_meta(dx, x)
234+
# External force-torque
235+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=True, ext_force_torque=True)
236+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
237+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False, ext_force_torque=True)
166238

167239

168240
@pytest.mark.unit
169-
@pytest.mark.parametrize("model_name, model", available_models.items())
170241
@pytest.mark.parametrize("drone_type", available_drones)
171-
def test_model_batched_external_wrench(model_name: str, model: Callable, drone_type: str):
172-
model = parametrize(model, drone_type)
242+
def test_so_rpy_rotor_drag(drone_type: str):
243+
dynamics = parametrize(so_rpy_rotor_drag_dynamics, drone_type)
244+
assert model_features(dynamics)["rotor_dynamics"], "Model should support rotor dynamics"
173245

174-
batch_shape = (10, 5)
175-
pos, quat, vel, ang_vel, rotor_vel, dist_f, dist_t = create_rnd_states(batch_shape)
176-
if not model_features(model)["rotor_dynamics"]:
177-
rotor_vel = None
178-
cmd = create_rnd_commands(batch_shape, dim=4) # TODO make dependent on model
179-
dpos, dquat, dvel, dang_vel, drotor_vel = model(
180-
pos, quat, vel, ang_vel, cmd, rotor_vel, dist_f=dist_f, dist_t=dist_t
246+
assert_dynamics_shapes(dynamics, rotor_vel_input=True)
247+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
248+
assert_dynamics_shapes(dynamics, rotor_vel_input=False)
249+
250+
# External force-torque
251+
assert_dynamics_shapes(dynamics, rotor_vel_input=True, ext_force_torque=True)
252+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
253+
assert_dynamics_shapes(dynamics, rotor_vel_input=False, ext_force_torque=True)
254+
255+
256+
@pytest.mark.unit
257+
@pytest.mark.parametrize("drone_type", available_drones)
258+
def test_so_rpy_rotor_drag_batched(drone_type: str):
259+
dynamics = parametrize(so_rpy_rotor_drag_dynamics, drone_type, xp=xp)
260+
assert model_features(dynamics)["rotor_dynamics"], "Model should support rotor dynamics"
261+
262+
shape = (10, 5)
263+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=True)
264+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
265+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False)
266+
267+
# External force-torque
268+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=True, ext_force_torque=True)
269+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
270+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False, ext_force_torque=True)
271+
272+
# Batch params
273+
dynamics.keywords["J"] = xp.tile(dynamics.keywords["J"][None, None, ...], shape + (1, 1))
274+
dynamics.keywords["J_inv"] = xp.tile(
275+
dynamics.keywords["J_inv"][None, None, ...], shape + (1, 1)
181276
)
182-
# Check if the output is on the correct device, has the correct type and shape
183-
for dx, x in zip(
184-
[dpos, dquat, dvel, dang_vel, drotor_vel], [pos, quat, vel, ang_vel, rotor_vel], strict=True
185-
):
186-
assert_array_meta(dx, x)
277+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=True)
278+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
279+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False)
280+
281+
# External force-torque
282+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=True, ext_force_torque=True)
283+
with pytest.warns(UserWarning, match="Rotor velocity not provided"):
284+
assert_dynamics_shapes(dynamics, batch=shape, rotor_vel_input=False, ext_force_torque=True)
187285

188286

189287
@pytest.mark.unit

0 commit comments

Comments
 (0)