1616from drone_models import available_models , model_features
1717from drone_models .core import parametrize
1818from 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
2024if 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
5663def 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 ())
6597def 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