Skip to content

Commit 2682219

Browse files
committed
Fix all numeric models
1 parent acd9dbf commit 2682219

18 files changed

Lines changed: 223 additions & 893 deletions

File tree

drone_models/core.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import warnings
56
from functools import partial, wraps
67
from typing import TYPE_CHECKING, Any, Callable, ParamSpec, Protocol, TypeVar, runtime_checkable
78

@@ -33,6 +34,8 @@ def wrapper(
3334
) -> tuple[Array, Array, Array, Array, Array | None]:
3435
if not rotor_dynamics and rotor_vel is not None:
3536
raise ValueError("Rotor dynamics not supported, but rotor_vel is provided.")
37+
if rotor_dynamics and rotor_vel is None:
38+
warnings.warn("Rotor velocity not provided, using commanded rotor velocity.")
3639
return fn(pos, quat, vel, ang_vel, cmd, rotor_vel, *args, **kwargs)
3740

3841
wrapper.__drone_model_features__ = {"rotor_dynamics": rotor_dynamics}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
"""First principles model.
2+
3+
TODO: Add description.
4+
"""
5+
16
from drone_models.first_principles.model import dynamics, dynamics_symbolic
27

38
__all__ = ["dynamics", "dynamics_symbolic"]

drone_models/first_principles/model.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from __future__ import annotations
44

5-
import warnings
65
from typing import TYPE_CHECKING
76

87
import casadi as cs
@@ -30,15 +29,15 @@ def dynamics(
3029
dist_f: Array | None = None,
3130
dist_t: Array | None = None,
3231
*,
33-
thrust_tau: float,
32+
mass: float,
33+
gravity_vec: Array,
34+
J: Array,
35+
J_inv: Array,
3436
KF: float,
3537
KM: float,
3638
L: float,
3739
mixing_matrix: Array,
38-
gravity_vec: Array,
39-
mass: float,
40-
J: Array,
41-
J_inv: Array,
40+
thrust_tau: float,
4241
) -> tuple[Array, Array, Array, Array, Array | None]:
4342
r"""First principles model for a quatrotor.
4443
@@ -52,12 +51,22 @@ def dynamics(
5251
vel: Velocity of the drone (m/s).
5352
ang_vel: Angular velocity of the drone (rad/s).
5453
cmd: Motor speeds (rad/s).
55-
constants: Containing the constants of the drone.
5654
rotor_vel: Angular velocity of the 4 motors (rad/s). If None, the commanded thrust is
5755
directly applied. If value is given, thrust dynamics are calculated.
5856
dist_f: Disturbance force acting on the CoM (N).
5957
dist_t: Disturbance torque acting on the CoM (Nm).
6058
59+
mass: Mass of the drone (kg).
60+
gravity_vec: Gravity vector (m/s^2). We assume the gravity vector points downwards, e.g.
61+
[0, 0, -9.81].
62+
J: Inertia matrix (kg m^2).
63+
J_inv: Inverse inertia matrix (1/kg m^2).
64+
KF: Motor force constant (N/rad^2).
65+
KM: Motor torque constant (Nm/rad^2).
66+
L: Distance from the CoM to the motor (m).
67+
mixing_matrix: Mixing matrix denoting the turn direction of the motors (4x3).
68+
thrust_tau: Thrust time constant (s).
69+
6170
.. math::
6271
\sum_{i=1}^{\\infty} x_{i} TODO
6372
@@ -67,15 +76,13 @@ def dynamics(
6776
More information https://ahrs.readthedocs.io/en/latest/filters/angular.html
6877
"""
6978
xp = array_namespace(pos)
70-
mass, gravity_vec, KF, KM, L, mixing_matrix, J, J_inv = to_xp(
71-
mass, gravity_vec, KF, KM, L, mixing_matrix, J, J_inv, xp=xp, device=device(pos)
79+
mass, gravity_vec, J, J_inv, KF, KM, L, mixing_matrix, thrust_tau = to_xp(
80+
mass, gravity_vec, J, J_inv, KF, KM, L, mixing_matrix, thrust_tau, xp=xp, device=device(pos)
7281
)
7382
rot = R.from_quat(quat)
7483
# Rotor dynamics
7584
if rotor_vel is None:
76-
rotor_vel_dot = None
77-
rotor_vel = cmd
78-
warnings.warn("Rotor velocity is not provided, using commanded rotor velocity directly.")
85+
rotor_vel, rotor_vel_dot = cmd, None
7986
else:
8087
rotor_vel_dot = 1 / thrust_tau * (cmd - rotor_vel) - 1 / KM * rotor_vel**2
8188
# Creating force and torque vector
@@ -87,7 +94,7 @@ def dynamics(
8794
# Because there currently is no way to identify the z torque in relation to the thrust,
8895
# we rely on a old identified value that can compute rpm to torque.
8996
# force = kf * rpm², torque = km * rpm² => torque = km/kf*force TODO
90-
torques_motor_vec = forces_motor @ mixing_matrix * xp.stack([L, L, KM / KF])
97+
torque = forces_motor @ mixing_matrix * xp.stack([L, L, KM / KF])
9198

9299
# Linear equation of motion
93100
forces_motor_vec_world = rot.apply(forces_motor_vec)
@@ -99,12 +106,11 @@ def dynamics(
99106
vel_dot = forces_sum / mass
100107

101108
# Rotational equation of motion
102-
torques_sum = torques_motor_vec
103109
if dist_t is not None:
104-
torques_sum = torques_sum + rot.apply(dist_t, inverse=True)
110+
torque = torque + rot.apply(dist_t, inverse=True)
105111
quat_dot = rotation.ang_vel2quat_dot(quat, ang_vel)
106-
ang_vel_dot = J_inv @ (torques_sum - xp.linalg.cross(ang_vel, J @ ang_vel))
107-
112+
torque = torque - xp.linalg.cross(ang_vel, (J @ ang_vel[..., None])[..., 0])
113+
ang_vel_dot = (J_inv @ torque[..., None])[..., 0]
108114
return pos_dot, quat_dot, vel_dot, ang_vel_dot, rotor_vel_dot
109115

110116

drone_models/first_principles/params.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""First principles model parameters."""
2+
13
from __future__ import annotations
24

35
import tomllib
@@ -11,6 +13,8 @@
1113

1214

1315
class FirstPrinciplesParams(NamedTuple):
16+
"""Parameters for the FirstPrinciples model."""
17+
1418
thrust_tau: float
1519
KF: float
1620
KM: float
@@ -23,6 +27,7 @@ class FirstPrinciplesParams(NamedTuple):
2327

2428
@staticmethod
2529
def load(drone_model: str) -> FirstPrinciplesParams:
30+
"""Load the parameters for the drone model from the params.toml file."""
2631
with open(Path(__file__).parent / "params.toml", "rb") as f:
2732
params = tomllib.load(f)
2833
if drone_model not in params:

drone_models/so_rpy/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
"""SoRpy model.
2+
3+
TODO: Add description.
4+
"""
5+
16
from drone_models.so_rpy.model import dynamics, dynamics_symbolic
27

38
__all__ = ["dynamics", "dynamics_symbolic"]

drone_models/so_rpy/model.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ def dynamics(
3333
*,
3434
mass: float,
3535
gravity_vec: Array,
36+
J: Array,
37+
J_inv: Array,
3638
acc_coef: Array,
3739
cmd_f_coef: Array,
3840
rpy_coef: Array,
3941
rpy_rates_coef: Array,
4042
cmd_rpy_coef: Array,
41-
J: Array,
42-
J_inv: Array,
4343
) -> tuple[Array, Array, Array, Array, Array | None]:
4444
"""The fitted double integrator (DI) model with optional motor delay (D).
4545
@@ -49,12 +49,21 @@ def dynamics(
4949
vel: Velocity of the drone (m/s).
5050
ang_vel: Angular velocity of the drone (rad/s).
5151
cmd: Roll pitch yaw (rad) and collective thrust (N) command.
52-
constants: Containing the constants of the drone.
53-
rotor_vel: Speed of the 4 motors (rad/s). If None, the commanded thrust is directly
54-
applied. If a value is given, the function raises an error.
52+
rotor_vel: Speed of the 4 motors (rad/s). Kept for compatibility with the model signature.
5553
dist_f: Disturbance force (N) acting on the CoM.
5654
dist_t: Disturbance torque (Nm) acting on the CoM.
5755
56+
mass: Mass of the drone (kg).
57+
gravity_vec: Gravity vector (m/s^2). We assume the gravity vector points downwards, e.g.
58+
[0, 0, -9.81].
59+
J: Inertia matrix (kg m^2).
60+
J_inv: Inverse inertia matrix (1/kg m^2).
61+
acc_coef: Coefficient for the acceleration (1/s^2).
62+
cmd_f_coef: Coefficient for the collective thrust (N/rad^2).
63+
rpy_coef: Coefficient for the roll pitch yaw dynamics (1/s).
64+
rpy_rates_coef: Coefficient for the roll pitch yaw rates dynamics (1/s^2).
65+
cmd_rpy_coef: Coefficient for the roll pitch yaw command dynamics (1/s).
66+
5867
Returns:
5968
The derivatives of all state variables.
6069
"""
@@ -93,7 +102,8 @@ def dynamics(
93102
# adding torque
94103
torque = torque + rot.apply(dist_t, inverse=True)
95104
# back to angular acceleration
96-
ang_vel_dot = J_inv @ (torque - xp.linalg.cross(ang_vel, J @ ang_vel))
105+
torque = torque - xp.linalg.cross(ang_vel, (J @ ang_vel[..., None])[..., 0])
106+
ang_vel_dot = (J_inv @ torque[..., None])[..., 0]
97107

98108
return pos_dot, quat_dot, vel_dot, ang_vel_dot, rotor_vel_dot
99109

drone_models/so_rpy/params.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414

1515
class SoRpyParams(NamedTuple):
16-
"""TODO."""
16+
"""Parameters for the SoRpy model."""
1717

1818
mass: float
1919
gravity_vec: Array
@@ -27,6 +27,7 @@ class SoRpyParams(NamedTuple):
2727

2828
@staticmethod
2929
def load(drone_model: str) -> SoRpyParams:
30+
"""Load the parameters for the drone model from the params.toml file."""
3031
with open(Path(__file__).parent / "params.toml", "rb") as f:
3132
params = tomllib.load(f)
3233
if drone_model not in params:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
"""SoRpyRotor model.
2+
3+
TODO: Add description.
4+
"""
5+
16
from drone_models.so_rpy_rotor.model import dynamics, dynamics_symbolic
27

38
__all__ = ["dynamics", "dynamics_symbolic"]

drone_models/so_rpy_rotor/model.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22

33
from __future__ import annotations
44

5-
import warnings
65
from typing import TYPE_CHECKING
76

87
import casadi as cs
98
from array_api_compat import array_namespace
9+
from array_api_compat import device as xp_device
1010
from scipy.spatial.transform import Rotation as R
1111

1212
import drone_models.symbols as symbols
1313
from drone_models.core import register_model_parameters, supports
1414
from drone_models.so_rpy_rotor.params import SoRpyRotorParams
1515
from drone_models.transform import motor_force2rotor_vel
16-
from drone_models.utils import rotation
16+
from drone_models.utils import rotation, to_xp
1717

1818
if TYPE_CHECKING:
1919
from array_api_typing import Array
@@ -54,31 +54,52 @@ def dynamics(
5454
vel: Velocity of the drone (m/s).
5555
ang_vel: Angular velocity of the drone (rad/s).
5656
cmd: Roll pitch yaw (rad) and collective thrust (N) command.
57-
constants: Containing the constants of the drone.
5857
rotor_vel: Speed of the 4 motors (rad/s). If None, the commanded thrust is directly
5958
applied (not recommended). If value is given, rotor dynamics are calculated.
6059
dist_f: Disturbance force acting on the CoM (N).
6160
dist_t: Disturbance torque acting on the CoM (Nm).
6261
62+
mass: Mass of the drone (kg).
63+
gravity_vec: Gravity vector (m/s^2). We assume the gravity vector points downwards, e.g.
64+
[0, 0, -9.81].
65+
KF: Motor force constant (N/rad^2).
66+
KM: Motor torque constant (Nm/rad^2).
67+
J: Inertia matrix (kg m^2).
68+
J_inv: Inverse inertia matrix (1/kg m^2).
69+
rotor_coef: Coefficient for the rotor dynamics (1/s).
70+
acc_coef: Coefficient for the acceleration (1/s^2).
71+
cmd_f_coef: Coefficient for the collective thrust (N/rad^2).
72+
rpy_coef: Coefficient for the roll pitch yaw dynamics (1/s).
73+
rpy_rates_coef: Coefficient for the roll pitch yaw rates dynamics (1/s^2).
74+
cmd_rpy_coef: Coefficient for the roll pitch yaw command dynamics (1/s).
75+
6376
Returns:
6477
tuple[Array, Array, Array, Array, Array | None]: _description_
6578
"""
6679
xp = array_namespace(pos)
80+
# Convert constants to the correct framework and device
81+
device = xp_device(pos)
82+
mass, gravity_vec, KF, KM, J, J_inv = to_xp(
83+
mass, gravity_vec, KF, KM, J, J_inv, xp=xp, device=device
84+
)
85+
rotor_coef, acc_coef, cmd_f_coef = to_xp(rotor_coef, acc_coef, cmd_f_coef, xp=xp, device=device)
86+
rpy_coef, rpy_rates_coef, cmd_rpy_coef = to_xp(
87+
rpy_coef, rpy_rates_coef, cmd_rpy_coef, xp=xp, device=device
88+
)
89+
6790
cmd_f = cmd[..., -1]
6891
cmd_rotor_vel = motor_force2rotor_vel(cmd_f / 4, KF)
6992
cmd_rpy = cmd[..., 0:3]
7093
rot = R.from_quat(quat)
7194
euler_angles = rot.as_euler("xyz")
7295

7396
if rotor_vel is None:
74-
rotor_vel_dot = None
75-
rotor_vel = cmd_rotor_vel
76-
warnings.warn("Rotor velocity is not provided, using commanded rotor velocity directly.")
97+
rotor_vel, rotor_vel_dot = cmd_rotor_vel[..., None], None
7798
else:
7899
rotor_vel_dot = 1 / rotor_coef * (cmd_rotor_vel[..., None] - rotor_vel) - KM * rotor_vel**2
79-
forces_motor = xp.sum(KF * rotor_vel**2, axis=-1)
80-
forces_sum = xp.sum(forces_motor, axis=-1)
81-
thrust = acc_coef + cmd_f_coef * forces_sum
100+
101+
forces_motor = KF * xp.sum(rotor_vel**2, axis=-1)
102+
thrust = acc_coef + cmd_f_coef * forces_motor
82103

83104
drone_z_axis = rot.as_matrix()[..., -1]
84105

@@ -98,11 +119,11 @@ def dynamics(
98119
# adding torque disturbances to the state
99120
# angular acceleration can be converted to total torque given the inertia matrix
100121
torque = ang_vel_dot @ J.mT + xp.linalg.cross(ang_vel, ang_vel @ J.mT)
101-
102122
# adding torque
103123
torque = torque + rot.apply(dist_t, inverse=True) # TODO rotation into body frame
104124
# back to angular acceleration
105-
ang_vel_dot = J_inv @ (torque - xp.linalg.cross(ang_vel, J @ ang_vel))
125+
torque = torque - xp.linalg.cross(ang_vel, (J @ ang_vel[..., None])[..., 0])
126+
ang_vel_dot = (J_inv @ torque[..., None])[..., 0]
106127

107128
return pos_dot, quat_dot, vel_dot, ang_vel_dot, rotor_vel_dot
108129

drone_models/so_rpy_rotor/params.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414

1515
class SoRpyRotorParams(NamedTuple):
16-
"""TODO."""
16+
"""Parameters for the SoRpyRotor model."""
1717

1818
mass: float
1919
gravity_vec: Array
@@ -30,6 +30,7 @@ class SoRpyRotorParams(NamedTuple):
3030

3131
@staticmethod
3232
def load(drone_model: str) -> SoRpyRotorParams:
33+
"""Load the parameters for the drone model from the params.toml file."""
3334
with open(Path(__file__).parent / "params.toml", "rb") as f:
3435
params = tomllib.load(f)
3536
if drone_model not in params:

0 commit comments

Comments
 (0)