owb_core/utils/math/
kinematics.rs

1//! Kinematics utilities for 3-wheeled omni-directional robots.
2//!
3//! The `EmbodiedKinematics` struct computes wheel velocity mappings based on
4//! desired translational and rotational motion and inverts wheel measurements back
5//! to body velocities.
6//!
7//! # Example
8//! ```rust
9//! use owb_core::utils::math::kinematics::EmbodiedKinematics;
10//! let kin = EmbodiedKinematics::new(0.148, 0.195);
11//! let wheel_speeds = kin.compute_wheel_velocities(1.0, 90.0, 0.0, 0.0);
12//! ```
13//!
14use core::f32::consts::PI;
15use libm;
16
17/// Represents the kinematics of a three-wheeled omni-wheel robot.
18pub struct EmbodiedKinematics {
19    /// Radius of each wheel (m)
20    wheel_radius: f32,
21    /// Robot center-to-wheel distance (m)
22    robot_radius: f32,
23    /// Precomputed wheel mounting angles (rad)
24    wheel_angles: [f32; 3],
25}
26
27impl EmbodiedKinematics {
28    /// Instantiate with a given wheel and robot radii.
29    pub fn new(
30        wheel_radius: f32,
31        robot_radius: f32,
32    ) -> Self {
33        let wheel_angles = [PI / 3.0, PI, 5.0 * PI / 3.0];
34        Self {
35            wheel_radius,
36            robot_radius,
37            wheel_angles,
38        }
39    }
40
41    /// Transform a global motion command into body-frame velocities.
42    ///
43    /// `speed` is the translational magnitude, `angle` and `orientation` are in degrees
44    /// (0° = +X, increasing CCW). Returns `(vx, vy)` in the robot's body frame.
45    pub fn convert_to_body_frame(
46        speed: f32,
47        angle: f32,
48        orientation: f32,
49    ) -> (f32, f32) {
50        let a = angle * (PI / 180.0);
51        let o = orientation * (PI / 180.0);
52        let vx = speed * libm::cosf(a - o);
53        let vy = speed * libm::sinf(a - o);
54        (-vy, vx)
55    }
56
57    /// Build Jacobian J such that ω_wheels = J * [vx, vy, ω_body]
58    pub fn construct_jacobian(&self) -> [[f32; 3]; 3] {
59        let r = self.wheel_radius;
60        let l = self.robot_radius;
61        let mut j = [[0.0; 3]; 3];
62        for (i, &t) in self.wheel_angles.iter().enumerate() {
63            j[i][0] = libm::cosf(t) / r;
64            j[i][1] = libm::sinf(t) / r;
65            j[i][2] = l / r;
66        }
67        j
68    }
69
70    /// Recover body velocities from measured wheel speeds.
71    ///
72    /// # Returns
73    ///
74    /// `(vx, vy, ω)` where `vx`/`vy` are linear body-frame velocities and `ω` is angular velocity.
75    pub fn compute_body_velocity(
76        &self,
77        wheel_velocity: [f32; 3],
78    ) -> (f32, f32, f32) {
79        let j = self.construct_jacobian();
80        let inv = invert_3x3(j);
81        let vx = inv[0][0] * wheel_velocity[0]
82            + inv[0][1] * wheel_velocity[1]
83            + inv[0][2] * wheel_velocity[2];
84        let vy = inv[1][0] * wheel_velocity[0]
85            + inv[1][1] * wheel_velocity[1]
86            + inv[1][2] * wheel_velocity[2];
87        let w = inv[2][0] * wheel_velocity[0]
88            + inv[2][1] * wheel_velocity[1]
89            + inv[2][2] * wheel_velocity[2];
90        (vx, vy, w)
91    }
92
93    /// Compute wheel angular velocities to achieve the desired motion.
94    ///
95    /// `speed` is forward translational speed, `angle` and `orientation` are in degrees,
96    /// and `omega` is rotational speed (deg/sec). Returns an array of wheel speeds.
97    pub fn compute_wheel_velocities(
98        &self,
99        speed: f32,
100        angle: f32,
101        orientation: f32,
102        omega: f32,
103    ) -> [f32; 3] {
104        let (vx, vy) = Self::convert_to_body_frame(speed, angle, orientation);
105        let v = [vx, vy, omega];
106        let j = self.construct_jacobian();
107        let mut out = [0.0; 3];
108        fn clamp_small(
109            v: f32,
110            eps: f32,
111        ) -> f32 {
112            if v.abs() < eps {
113                0.0
114            } else {
115                v
116            }
117        }
118        for i in 0..3 {
119            out[i] = clamp_small(j[i][0] * v[0] + j[i][1] * v[1] + j[i][2] * v[2], 1e-6);
120        }
121        out
122    }
123}
124
125/// Invert a 3×3 matrix using cofactor expansion.
126///
127/// # Panics
128///
129/// Panics if the matrix is singular (determinant is zero).
130fn invert_3x3(m: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
131    let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
132        - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
133        + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
134    let inv_det = 1.0 / det;
135    [
136        [
137            (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
138            -(m[0][1] * m[2][2] - m[0][2] * m[2][1]) * inv_det,
139            (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
140        ],
141        [
142            -(m[1][0] * m[2][2] - m[1][2] * m[2][0]) * inv_det,
143            (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
144            -(m[0][0] * m[1][2] - m[0][2] * m[1][0]) * inv_det,
145        ],
146        [
147            (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
148            -(m[0][0] * m[2][1] - m[0][1] * m[2][0]) * inv_det,
149            (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
150        ],
151    ]
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_convert_to_body_frame() {
160        // Forward at 0°, no orientation offset => body vx=0, vy=1
161        let (vx, vy) = EmbodiedKinematics::convert_to_body_frame(1.0, 0.0, 0.0);
162        assert!((vx - 0.0).abs() < 1e-6);
163        assert!((vy - 1.0).abs() < 1e-6);
164    }
165
166    #[test]
167    fn test_invert_3x3_identity() {
168        let id = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
169        let inv = invert_3x3(id);
170        for i in 0..3 {
171            for j in 0..3 {
172                assert!(
173                    (inv[i][j] - id[i][j]).abs() < 1e-6,
174                    "inv != id at {}:{}",
175                    i,
176                    j
177                );
178            }
179        }
180    }
181
182    #[test]
183    fn test_compute_wheel_velocities_zero() {
184        let kin = EmbodiedKinematics::new(0.1, 0.2);
185        let wheels = kin.compute_wheel_velocities(0.0, 0.0, 0.0, 0.0);
186        assert_eq!(wheels, [0.0, 0.0, 0.0]);
187    }
188
189    #[test]
190    fn test_round_trip_body_velocity() {
191        let kin = EmbodiedKinematics::new(0.1, 0.2);
192        // Some arbitrary motion command
193        let speed = 1.23;
194        let angle = 45.0;
195        let orientation = 10.0;
196        let omega = 0.5;
197        // Compute wheel speeds, then invert back
198        let wheel_speeds = kin.compute_wheel_velocities(speed, angle, orientation, omega);
199        let (vx, vy, w) = kin.compute_body_velocity(wheel_speeds);
200        // vx, vy, w should approximate body-frame motion
201        let (exp_vx, exp_vy) = EmbodiedKinematics::convert_to_body_frame(speed, angle, orientation);
202        assert!((vx - exp_vx).abs() < 1e-3);
203        assert!((vy - exp_vy).abs() < 1e-3);
204        assert!((w - omega).abs() < 1e-3);
205    }
206}