1818from typing import TYPE_CHECKING
1919
2020import casadi as cs
21- import jax
2221import jax .numpy as jnp
2322from array_api_compat import array_namespace
2423from array_api_compat import device as xp_device
@@ -49,8 +48,8 @@ def dynamics(
4948 dist_t : Array | None = None ,
5049 * ,
5150 mass : float ,
52- L : float ,
53- prop_inertia : float ,
51+ L : float | Array ,
52+ prop_inertia : float | Array ,
5453 gravity_vec : Array ,
5554 J : Array ,
5655 J_inv : Array ,
@@ -80,18 +79,25 @@ def dynamics(
8079 dist_t: Disturbance torque (Nm) in the world frame acting on the CoM.
8180
8281 mass: Mass of the drone (kg).
83- L: Distance from the CoM to the motor (m).
84- prop_inertia: Inertia of one propeller in z direction (kg m^2).
82+ L: Distance from the CoM to the motors (m). Shared (1,) or one value per motor (4,).
83+ prop_inertia: Inertia of the propellers in z direction (kg m^2). Shared (1,) or one value
84+ per motor (4,).
8585 gravity_vec: Gravity vector (m/s^2). We assume the gravity vector points downwards, e.g.
8686 [0, 0, -9.81].
8787 J: Inertia matrix (kg m^2).
8888 J_inv: Inverse inertia matrix (1/kg m^2).
89- rpm2thrust: Propeller force constant (N min^2).
90- rpm2torque: Propeller torque constant (Nm min^2).
89+ rpm2thrust: Propeller force constants (N min^2). Shared (1, 3) or one curve per motor
90+ (4, 3).
91+ rpm2torque: Propeller torque constants (Nm min^2). Shared (1, 3) or one curve per motor
92+ (4, 3).
9193 mixing_matrix: Mixing matrix denoting the turn direction of the motors (4x3).
9294 drag_matrix: Drag matrix containing the linear drag coefficients (3x3).
93- rotor_dyn_coef: Rotor dynamics coefficients.
95+ rotor_dyn_coef: Rotor dynamics coefficients. Shared (1, 4) or one set per motor (4, 4).
9496
97+ Note:
98+ All array parameters accept leading batch axes (N, M) to vary per world and per drone.
99+ Per-motor parameters carry a motor axis of size 1 when shared, so that the per-world layout
100+ is e.g. (N, M, 1, 3) for a shared and (N, M, 4, 3) for a per-motor thrust curve.
95101
96102 Warning:
97103 Do not use quat_dot directly for integration! Only usage of ang_vel is mathematically
@@ -112,24 +118,28 @@ def dynamics(
112118 warnings .warn ("Rotor velocity not provided, using commanded rotor velocity." )
113119 rotor_vel , rotor_vel_dot = cmd , None
114120 else :
121+ acc1 , acc2 = rotor_dyn_coef [..., 0 ], rotor_dyn_coef [..., 1 ]
122+ dec1 , dec2 = rotor_dyn_coef [..., 2 ], rotor_dyn_coef [..., 3 ]
115123 rotor_vel_dot = xp .where (
116124 cmd > rotor_vel ,
117- rotor_dyn_coef [ 0 ] * (cmd - rotor_vel ) + rotor_dyn_coef [ 1 ] * (cmd ** 2 - rotor_vel ** 2 ),
118- rotor_dyn_coef [ 2 ] * (cmd - rotor_vel ) + rotor_dyn_coef [ 3 ] * (cmd ** 2 - rotor_vel ** 2 ),
125+ acc1 * (cmd - rotor_vel ) + acc2 * (cmd ** 2 - rotor_vel ** 2 ),
126+ dec1 * (cmd - rotor_vel ) + dec2 * (cmd ** 2 - rotor_vel ** 2 ),
119127 )
120128 # Creating force and torque vector
121- forces_motor = rpm2thrust [0 ] + rpm2thrust [1 ] * rotor_vel + rpm2thrust [2 ] * rotor_vel ** 2
129+ k0 , k1 , k2 = rpm2thrust [..., 0 ], rpm2thrust [..., 1 ], rpm2thrust [..., 2 ]
130+ forces_motor = k0 + k1 * rotor_vel + k2 * rotor_vel ** 2
122131 forces_motor_tot = xp .sum (forces_motor , axis = - 1 )
123132 zeros = xp .zeros_like (forces_motor_tot )
124133 forces_motor_vec = xp .stack ((zeros , zeros , forces_motor_tot ), axis = - 1 )
125134 forces_motor_vec_world = rot .apply (forces_motor_vec )
126135 force_gravity = gravity_vec * mass
127136 force_drag = (rot_mat .mT @ (drag_matrix @ (rot_mat @ vel [..., None ])))[..., 0 ]
128137
129- torques_motor = rpm2torque [0 ] + rpm2torque [1 ] * rotor_vel + rpm2torque [2 ] * rotor_vel ** 2
130- torque_thrust = (mixing_matrix @ (forces_motor )[..., None ])[..., 0 ] * xp .stack (
131- [L , L , xp .asarray (0.0 )]
132- )
138+ c0 , c1 , c2 = rpm2torque [..., 0 ], rpm2torque [..., 1 ], rpm2torque [..., 2 ]
139+ torques_motor = c0 + c1 * rotor_vel + c2 * rotor_vel ** 2
140+ # Weight each motor force by its arm length before mixing to support per-motor arm lengths
141+ lever = xp .asarray ([1.0 , 1.0 , 0.0 ], dtype = forces_motor .dtype , device = device )
142+ torque_thrust = (mixing_matrix @ (forces_motor * L )[..., None ])[..., 0 ] * lever
133143 torque_drag = (mixing_matrix @ (torques_motor )[..., None ])[..., 0 ] * xp .stack (
134144 [xp .asarray (0.0 ), xp .asarray (0.0 ), xp .asarray (1.0 )]
135145 )
@@ -139,12 +149,12 @@ def dynamics(
139149 rotor_vel_dot_rads = (
140150 rotor_vel_dot * rpm_to_rad if rotor_vel_dot is not None else xp .zeros_like (rotor_vel )
141151 )
142- torque_inertia = prop_inertia * xp . stack (
143- [
144- ang_vel [..., 1 ] * xp .sum (mixing_matrix [..., - 1 , :] * rotor_vel_rads , axis = - 1 ),
145- - ang_vel [..., 0 ] * xp .sum (mixing_matrix [..., - 1 , :] * rotor_vel_rads , axis = - 1 ),
146- xp .sum ( mixing_matrix [..., - 1 , :] * rotor_vel_dot_rads , axis = - 1 ),
147- ],
152+ # Angular momentum of the propellers along the body z-axis, weighted per motor by its inertia
153+ spin = mixing_matrix [..., - 1 , :] * prop_inertia
154+ rotor_momentum = xp .sum (spin * rotor_vel_rads , axis = - 1 )
155+ rotor_momentum_dot = xp .sum (spin * rotor_vel_dot_rads , axis = - 1 )
156+ torque_inertia = xp .stack (
157+ [ ang_vel [..., 1 ] * rotor_momentum , - ang_vel [..., 0 ] * rotor_momentum , rotor_momentum_dot ],
148158 axis = - 1 ,
149159 )
150160 torque_vec = torque_thrust + torque_drag + torque_inertia
@@ -172,8 +182,8 @@ def symbolic_dynamics(
172182 model_dist_t : bool = False ,
173183 * ,
174184 mass : float ,
175- L : float ,
176- prop_inertia : float ,
185+ L : float | Array ,
186+ prop_inertia : float | Array ,
177187 gravity_vec : Array ,
178188 J : Array ,
179189 J_inv : Array ,
@@ -195,18 +205,21 @@ def symbolic_dynamics(
195205 model_dist_f: If ``True``, a 3-D force disturbance is appended to ``X``.
196206 model_dist_t: If ``True``, a 3-D torque disturbance is appended to ``X``.
197207 mass: Drone mass in kg.
198- L: Distance from centre of mass to motor in metres.
199- prop_inertia: Moment of inertia of one propeller about its spin axis in kg m².
208+ L: Distance from centre of mass to the motors in meters, shared ``(1,)`` or one value per
209+ motor ``(4,)``.
210+ prop_inertia: Moment of inertia of the propellers about their spin axis in kg m², shared
211+ ``(1,)`` or one value per motor ``(4,)``.
200212 gravity_vec: Gravity vector, shape ``(3,)``.
201213 J: Inertia matrix, shape ``(3, 3)``.
202214 J_inv: Inverse inertia matrix, shape ``(3, 3)``.
203215 rpm2thrust: Polynomial coefficients ``[a, b, c]`` for the thrust curve
204- ``f = a + b * rpm + c * rpm²``.
216+ ``f = a + b * rpm + c * rpm²``, shared ``(1, 3)`` or one curve per motor ``(4, 3)`` .
205217 rpm2torque: Polynomial coefficients ``[a, b, c]`` for the drag-torque curve
206- ``τ = a + b * rpm + c * rpm²``.
218+ ``τ = a + b * rpm + c * rpm²``, shared ``(1, 3)`` or one curve per motor ``(4, 3)`` .
207219 mixing_matrix: Matrix of shape ``(3, 4)`` mapping per-motor forces to body torques.
208220 rotor_dyn_coef: Four rotor dynamics coefficients ``[k_acc1, k_acc2, k_dec1, k_dec2]`` used
209- in the piecewise-linear spin-up/down model.
221+ in the piecewise-linear spin-up/down model, shared ``(1, 4)`` or one set per motor
222+ ``(4, 4)``.
210223 drag_matrix: Diagonal ``(3, 3)`` matrix of linear drag coefficients.
211224
212225 Returns:
@@ -234,36 +247,41 @@ def symbolic_dynamics(
234247 # Rotor dynamics
235248 rotor_vel_dot = cs .if_else (
236249 U > symbols .rotor_vel ,
237- rotor_dyn_coef [0 ] * (U - symbols .rotor_vel )
238- + rotor_dyn_coef [1 ] * (U ** 2 - symbols .rotor_vel ** 2 ),
239- rotor_dyn_coef [2 ] * (U - symbols .rotor_vel )
240- + rotor_dyn_coef [3 ] * (U ** 2 - symbols .rotor_vel ** 2 ),
250+ rotor_dyn_coef [..., 0 ] * (U - symbols .rotor_vel )
251+ + rotor_dyn_coef [..., 1 ] * (U ** 2 - symbols .rotor_vel ** 2 ),
252+ rotor_dyn_coef [..., 2 ] * (U - symbols .rotor_vel )
253+ + rotor_dyn_coef [..., 3 ] * (U ** 2 - symbols .rotor_vel ** 2 ),
241254 )
242255 else :
243256 _saved_rotor_vel = symbols .rotor_vel
244257 symbols .rotor_vel = U
245258 # Creating force and torque vector
246259 forces_motor = (
247- rpm2thrust [0 ] + rpm2thrust [1 ] * symbols .rotor_vel + rpm2thrust [2 ] * symbols .rotor_vel ** 2
260+ rpm2thrust [..., 0 ]
261+ + rpm2thrust [..., 1 ] * symbols .rotor_vel
262+ + rpm2thrust [..., 2 ] * symbols .rotor_vel ** 2
248263 )
249264 forces_motor_vec = cs .vertcat (0.0 , 0.0 , cs .sum1 (forces_motor ))
250265 forces_motor_vec_world = symbols .rot @ forces_motor_vec
251266 force_gravity = gravity_vec * mass
252267 force_drag = symbols .rot @ (drag_matrix @ (symbols .rot .T @ symbols .vel ))
253268
254269 torques_motor = (
255- rpm2torque [0 ] + rpm2torque [1 ] * symbols .rotor_vel + rpm2torque [2 ] * symbols .rotor_vel ** 2
270+ rpm2torque [..., 0 ]
271+ + rpm2torque [..., 1 ] * symbols .rotor_vel
272+ + rpm2torque [..., 2 ] * symbols .rotor_vel ** 2
256273 )
257- torques_thrust = mixing_matrix @ forces_motor * cs .vertcat (L , L , 0.0 )
274+ torques_thrust = mixing_matrix @ ( forces_motor * L ) * cs .vertcat (1.0 , 1.0 , 0.0 )
258275 torques_drag = mixing_matrix @ torques_motor * cs .vertcat (0.0 , 0.0 , 1.0 )
259276 # convert rotor speed from RPM to rad/s for physical calculations
260277 rpm_to_rad = 2 * cs .pi / 60
261278 rotor_vel_rads = symbols .rotor_vel * rpm_to_rad
262279 rotor_vel_dot_rads = rotor_vel_dot * rpm_to_rad if model_rotor_vel else symbols .rotor_vel * 0.0
263- torque_inertia = prop_inertia * cs .vertcat (
264- symbols .ang_vel [1 ] * cs .sum (mixing_matrix [- 1 , :] * rotor_vel_rads ),
265- - symbols .ang_vel [0 ] * cs .sum (mixing_matrix [- 1 , :] * rotor_vel_rads ),
266- cs .sum (mixing_matrix [- 1 , :] * rotor_vel_dot_rads ),
280+ spin = mixing_matrix [- 1 , :] * prop_inertia
281+ torque_inertia = cs .vertcat (
282+ symbols .ang_vel [1 ] * cs .sum (spin * rotor_vel_rads ),
283+ - symbols .ang_vel [0 ] * cs .sum (spin * rotor_vel_rads ),
284+ cs .sum (spin * rotor_vel_dot_rads ),
267285 )
268286 torques_motor_vec = torques_thrust + torques_drag + torque_inertia
269287
@@ -298,46 +316,46 @@ def symbolic_dynamics(
298316
299317@dataclass
300318class Params :
301- mass : Array = field (metadata = {CORE_NDIM_KEY : 1 }) # (N, M, 1 )
319+ mass : Array = field (metadata = {CORE_NDIM_KEY : 1 }) # (1, )
302320 """Mass of the drone."""
303- L : Array = field (metadata = {CORE_NDIM_KEY : 0 }) # ()
304- """Arm length of the drone."""
305- prop_inertia : Array = field (metadata = {CORE_NDIM_KEY : 0 }) # ()
306- """Inertia of the propeller ."""
321+ L : Array = field (metadata = {CORE_NDIM_KEY : 1 }) # (1, )
322+ """Arm length of the drone. One shared value, or one value per motor with shape (4,). """
323+ prop_inertia : Array = field (metadata = {CORE_NDIM_KEY : 1 }) # (1, )
324+ """Inertia of the propellers. One shared value, or one value per motor with shape (4,) ."""
307325 gravity_vec : Array = field (metadata = {CORE_NDIM_KEY : 1 }) # (3,)
308326 """Gravity vector of the drone."""
309- J : Array = field (metadata = {CORE_NDIM_KEY : 2 }) # (N, M, 3, 3)
327+ J : Array = field (metadata = {CORE_NDIM_KEY : 2 }) # (3, 3)
310328 """Inertia matrix of the drone."""
311- J_inv : Array = field (metadata = {CORE_NDIM_KEY : 2 }) # (N, M, 3, 3)
329+ J_inv : Array = field (metadata = {CORE_NDIM_KEY : 2 }) # (3, 3)
312330 """Inverse of the inertia matrix of the drone."""
313- rpm2thrust : Array = field (metadata = {CORE_NDIM_KEY : 1 }) # (3, )
314- """Force constant of the drone."""
315- rpm2torque : Array = field (metadata = {CORE_NDIM_KEY : 1 }) # (3, )
316- """Torque constant of the drone."""
331+ rpm2thrust : Array = field (metadata = {CORE_NDIM_KEY : 2 }) # (1, 3 )
332+ """Force constants of the drone. One shared curve, or one curve per motor with shape (4, 3) ."""
333+ rpm2torque : Array = field (metadata = {CORE_NDIM_KEY : 2 }) # (1, 3 )
334+ """Torque constants of the drone. One shared curve, or one curve per motor with shape (4, 3) ."""
317335 mixing_matrix : Array = field (metadata = {CORE_NDIM_KEY : 2 }) # (3, 4)
318336 """Mixing matrix of the drone."""
319337 drag_matrix : Array = field (metadata = {CORE_NDIM_KEY : 2 }) # (3, 3)
320338 """Drag matrix of the drone."""
321- rotor_dyn_coef : Array = field (metadata = {CORE_NDIM_KEY : 1 }) # (4, )
322- """Rotor speed dynamics time constant of the drone."""
339+ rotor_dyn_coef : Array = field (metadata = {CORE_NDIM_KEY : 2 }) # (1, 4 )
340+ """Rotor speed dynamics coefficients of the drone. One shared set, or one per motor (4, 4) ."""
323341
324342 @staticmethod
325- def create (n_worlds : int , n_drones : int , drone : str , device : Device ) -> Params :
326- """Create a default set of parameters for the simulation."""
343+ def create (drone : str , device : Device ) -> Params :
344+ """Create the default parameters for the simulation."""
327345 p = load_params (dynamics , drone )
328- J = jax . device_put ( jnp .tile (p ["J" ][ None , None , :, :], ( n_worlds , n_drones , 1 , 1 )), device )
346+ J = jnp .asarray (p ["J" ], device = device )
329347 return Params (
330- mass = jnp .full (( n_worlds , n_drones , 1 ), p ["mass" ], device = device ),
331- L = jnp .asarray (p ["L" ], device = device ),
332- prop_inertia = jnp .asarray (p ["prop_inertia" ], device = device ),
348+ mass = jnp .asarray ([ p ["mass" ] ], device = device ),
349+ L = jnp .asarray ([ p ["L" ] ], device = device ),
350+ prop_inertia = jnp .asarray ([ p ["prop_inertia" ] ], device = device ),
333351 gravity_vec = jnp .asarray (p ["gravity_vec" ], device = device ),
334352 J = J ,
335353 J_inv = jnp .linalg .inv (J ),
336- rpm2thrust = jnp .asarray (p ["rpm2thrust" ], device = device ),
337- rpm2torque = jnp .asarray (p ["rpm2torque" ], device = device ),
354+ rpm2thrust = jnp .asarray ([ p ["rpm2thrust" ] ], device = device ),
355+ rpm2torque = jnp .asarray ([ p ["rpm2torque" ] ], device = device ),
338356 mixing_matrix = jnp .asarray (p ["mixing_matrix" ], device = device ),
339357 drag_matrix = jnp .asarray (p ["drag_matrix" ], device = device ),
340- rotor_dyn_coef = jnp .asarray (p ["rotor_dyn_coef" ], device = device ),
358+ rotor_dyn_coef = jnp .asarray ([ p ["rotor_dyn_coef" ] ], device = device ),
341359 )
342360
343361
0 commit comments