Skip to content

Commit 499e33a

Browse files
ratheronamacati
andauthored
Enable randomizations (#105)
--------- Co-authored-by: Martin Schuck <martin.schuck@tum.de>
1 parent 3980e2b commit 499e33a

30 files changed

Lines changed: 529 additions & 193 deletions

File tree

‎crazyflow/control/transform.py‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from typing import TYPE_CHECKING
99

1010
from array_api_compat import array_namespace
11+
from array_api_compat import device as xp_device
12+
13+
from crazyflow.utils import to_xp
1114

1215
if TYPE_CHECKING:
1316
from crazyflow._typing import Array # To be changed to array_api_typing later
@@ -18,16 +21,17 @@ def motor_force2rotor_vel(motor_forces: Array, rpm2thrust: Array) -> Array:
1821
1922
Args:
2023
motor_forces: Motor forces in SI units with shape (..., N).
21-
rpm2thrust: RPM to thrust conversion factors.
24+
rpm2thrust: RPM to thrust conversion factors with shape (3,), shared (1, 3) or one curve per
25+
motor (N, 3), optionally with leading batch axes.
2226
2327
Returns:
2428
Array of rotor velocities in RPMs with shape (..., N).
2529
"""
2630
xp = array_namespace(motor_forces)
27-
return (
28-
-rpm2thrust[1]
29-
+ xp.sqrt(rpm2thrust[1] ** 2 - 4 * rpm2thrust[2] * (rpm2thrust[0] - motor_forces))
30-
) / (2 * rpm2thrust[2])
31+
rpm2thrust = to_xp(rpm2thrust, xp=xp, device=xp_device(motor_forces))
32+
# shared (1, 3) and per-motor (..., N, 3) coefficients both broadcast against motor_forces.
33+
c, b, a = rpm2thrust[..., 0], rpm2thrust[..., 1], rpm2thrust[..., 2]
34+
return (-b + xp.sqrt(b**2 - 4 * a * (c - motor_forces))) / (2 * a)
3135

3236

3337
def force2pwm(thrust: Array | float, thrust_max: Array | float, pwm_max: Array | float) -> Array:

‎crazyflow/dynamics/first_principles/dynamics.py‎

Lines changed: 79 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from typing import TYPE_CHECKING
1919

2020
import casadi as cs
21-
import jax
2221
import jax.numpy as jnp
2322
from array_api_compat import array_namespace
2423
from 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
300318
class 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

‎crazyflow/dynamics/so_rpy/dynamics.py‎

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from typing import TYPE_CHECKING
1818

1919
import casadi as cs
20-
import jax
2120
import jax.numpy as jnp
2221
from array_api_compat import array_namespace
2322
from array_api_compat import device as xp_device
@@ -150,9 +149,9 @@ def dynamics_euler(
150149
cmd_f = cmd[..., -1]
151150
cmd_rpy = cmd[..., 0:3]
152151
drone_z_axis = R.from_euler("xyz", rpy).as_matrix()[..., -1]
153-
thrust = acc_coef + cmd_f_coef * cmd_f
152+
thrust = acc_coef + cmd_f_coef * cmd_f[..., None] # (..., 1)
154153
pos_dot = vel
155-
vel_dot = 1.0 / mass * thrust[..., None] * drone_z_axis + gravity_vec
154+
vel_dot = 1.0 / mass * thrust * drone_z_axis + gravity_vec
156155
rpy_rates_dot = rpy_coef * rpy + rpy_rates_coef * rpy_rates + cmd_rpy_coef * cmd_rpy
157156
return pos_dot, rpy_rates, vel_dot, rpy_rates_dot
158157

@@ -316,22 +315,22 @@ def symbolic_dynamics_euler(
316315

317316
@dataclass
318317
class Params:
319-
mass: Array = field(metadata={CORE_NDIM_KEY: 1}) # (N, M, 1)
318+
mass: Array = field(metadata={CORE_NDIM_KEY: 1}) # (1,)
320319
"""Mass of the drone."""
321320

322321
gravity_vec: Array = field(metadata={CORE_NDIM_KEY: 1}) # (3,)
323322
"""Gravity vector of the drone."""
324323

325-
J: Array = field(metadata={CORE_NDIM_KEY: 2}) # (N, M, 3, 3)
324+
J: Array = field(metadata={CORE_NDIM_KEY: 2}) # (3, 3)
326325
"""Inertia matrix of the drone."""
327326

328-
J_inv: Array = field(metadata={CORE_NDIM_KEY: 2}) # (N, M, 3, 3)
327+
J_inv: Array = field(metadata={CORE_NDIM_KEY: 2}) # (3, 3)
329328
"""Inverse of the inertia matrix of the drone."""
330329

331-
acc_coef: Array = field(metadata={CORE_NDIM_KEY: 0}) # ()
330+
acc_coef: Array = field(metadata={CORE_NDIM_KEY: 1}) # (1,)
332331
"""Coefficient for the acceleration."""
333332

334-
cmd_f_coef: Array = field(metadata={CORE_NDIM_KEY: 0}) # ()
333+
cmd_f_coef: Array = field(metadata={CORE_NDIM_KEY: 1}) # (1,)
335334
"""Coefficient for the collective thrust."""
336335

337336
rpy_coef: Array = field(metadata={CORE_NDIM_KEY: 1}) # (3,)
@@ -344,17 +343,17 @@ class Params:
344343
"""Coefficient for the roll pitch yaw command dynamics."""
345344

346345
@staticmethod
347-
def create(n_worlds: int, n_drones: int, drone: str, device: Device) -> Params:
348-
"""Create a default set of parameters for the simulation."""
346+
def create(drone: str, device: Device) -> Params:
347+
"""Create the default parameters for the simulation."""
349348
p = load_params(dynamics, drone)
350-
J = jax.device_put(jnp.tile(p["J"][None, None, :, :], (n_worlds, n_drones, 1, 1)), device)
349+
J = jnp.asarray(p["J"], device=device)
351350
return Params(
352-
mass=jnp.full((n_worlds, n_drones, 1), p["mass"], device=device),
351+
mass=jnp.asarray([p["mass"]], device=device),
353352
gravity_vec=jnp.asarray(p["gravity_vec"], device=device),
354353
J=J,
355354
J_inv=jnp.linalg.inv(J),
356-
acc_coef=jnp.asarray(p["acc_coef"], device=device),
357-
cmd_f_coef=jnp.asarray(p["cmd_f_coef"], device=device),
355+
acc_coef=jnp.asarray([p["acc_coef"]], device=device),
356+
cmd_f_coef=jnp.asarray([p["cmd_f_coef"]], device=device),
358357
rpy_coef=jnp.asarray(p["rpy_coef"], device=device),
359358
rpy_rates_coef=jnp.asarray(p["rpy_rates_coef"], device=device),
360359
cmd_rpy_coef=jnp.asarray(p["cmd_rpy_coef"], device=device),

0 commit comments

Comments
 (0)