Skip to main content

oxirs_physics/
kinematics.rs

1//! Kinematic equations for linear and rotational motion.
2//!
3//! Implements constant-acceleration kinematics for both linear
4//! (translational) and rotational motion, plus projectile motion helpers.
5
6/// Kinematic state for linear motion under constant acceleration.
7#[derive(Debug, Clone, PartialEq)]
8pub struct LinearState {
9    /// Position in meters.
10    pub position: f64,
11    /// Velocity in m/s.
12    pub velocity: f64,
13    /// Constant acceleration in m/s².
14    pub acceleration: f64,
15}
16
17/// Kinematic state for rotational motion under constant angular acceleration.
18#[derive(Debug, Clone, PartialEq)]
19pub struct RotationalState {
20    /// Angular position in radians.
21    pub angle: f64,
22    /// Angular velocity in rad/s.
23    pub omega: f64,
24    /// Constant angular acceleration in rad/s².
25    pub alpha: f64,
26}
27
28/// Solver for classical kinematic equations (constant acceleration).
29pub struct Kinematics;
30
31impl Default for Kinematics {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl Kinematics {
38    /// Create a new `Kinematics` solver.
39    pub fn new() -> Self {
40        Kinematics
41    }
42
43    // -----------------------------------------------------------------------
44    // Linear kinematics
45    // -----------------------------------------------------------------------
46
47    /// Velocity at time `t`: v = v₀ + a·t
48    pub fn velocity_at(&self, state: &LinearState, t: f64) -> f64 {
49        state.velocity + state.acceleration * t
50    }
51
52    /// Position at time `t`: x = x₀ + v₀·t + ½·a·t²
53    pub fn position_at(&self, state: &LinearState, t: f64) -> f64 {
54        state.position + state.velocity * t + 0.5 * state.acceleration * t * t
55    }
56
57    /// Velocity after displacement `dx` using v² = v₀² + 2·a·Δx.
58    ///
59    /// Returns `Some(v)` with the non-negative root when the discriminant is
60    /// non-negative (taking the sign that matches the direction of motion when
61    /// possible), or `None` when the discriminant is negative.
62    pub fn velocity_from_displacement(&self, state: &LinearState, dx: f64) -> Option<f64> {
63        let discriminant = state.velocity * state.velocity + 2.0 * state.acceleration * dx;
64        if discriminant < 0.0 {
65            return None;
66        }
67        Some(discriminant.sqrt())
68    }
69
70    /// Time to reach velocity `v` from `state.velocity` under constant `a`.
71    ///
72    /// Returns `None` when `a = 0` and `v ≠ state.velocity` (unreachable).
73    /// Returns `Some(0.0)` when `v == state.velocity`.
74    pub fn time_to_velocity(&self, state: &LinearState, v: f64) -> Option<f64> {
75        let delta_v = v - state.velocity;
76        if state.acceleration.abs() < f64::EPSILON {
77            if delta_v.abs() < f64::EPSILON {
78                return Some(0.0);
79            }
80            return None;
81        }
82        Some(delta_v / state.acceleration)
83    }
84
85    /// Propagate the linear state forward by time `t`.
86    pub fn propagate_linear(&self, state: &LinearState, t: f64) -> LinearState {
87        LinearState {
88            position: self.position_at(state, t),
89            velocity: self.velocity_at(state, t),
90            acceleration: state.acceleration,
91        }
92    }
93
94    // -----------------------------------------------------------------------
95    // Projectile motion
96    // -----------------------------------------------------------------------
97
98    /// Horizontal range of a projectile: R = v₀² · sin(2θ) / g
99    pub fn projectile_range(&self, v0: f64, angle_rad: f64, g: f64) -> f64 {
100        v0 * v0 * (2.0 * angle_rad).sin() / g
101    }
102
103    /// Maximum height of a projectile: H = (v₀ · sin θ)² / (2g)
104    pub fn projectile_max_height(&self, v0: f64, angle_rad: f64, g: f64) -> f64 {
105        let vy = v0 * angle_rad.sin();
106        vy * vy / (2.0 * g)
107    }
108
109    /// Total time of flight: T = 2 · v₀ · sin θ / g
110    pub fn projectile_time_of_flight(&self, v0: f64, angle_rad: f64, g: f64) -> f64 {
111        2.0 * v0 * angle_rad.sin() / g
112    }
113
114    // -----------------------------------------------------------------------
115    // Rotational kinematics
116    // -----------------------------------------------------------------------
117
118    /// Angular velocity at time `t`: ω = ω₀ + α·t
119    pub fn angular_velocity_at(&self, state: &RotationalState, t: f64) -> f64 {
120        state.omega + state.alpha * t
121    }
122
123    /// Angle at time `t`: θ = θ₀ + ω₀·t + ½·α·t²
124    pub fn angle_at(&self, state: &RotationalState, t: f64) -> f64 {
125        state.angle + state.omega * t + 0.5 * state.alpha * t * t
126    }
127
128    /// Propagate the rotational state forward by time `t`.
129    pub fn propagate_rotational(&self, state: &RotationalState, t: f64) -> RotationalState {
130        RotationalState {
131            angle: self.angle_at(state, t),
132            omega: self.angular_velocity_at(state, t),
133            alpha: state.alpha,
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use std::f64::consts::{FRAC_PI_4, FRAC_PI_6, PI};
142
143    const EPS: f64 = 1e-10;
144    const G: f64 = 9.81;
145
146    fn kin() -> Kinematics {
147        Kinematics::new()
148    }
149
150    fn linear(pos: f64, vel: f64, acc: f64) -> LinearState {
151        LinearState {
152            position: pos,
153            velocity: vel,
154            acceleration: acc,
155        }
156    }
157
158    fn rotational(angle: f64, omega: f64, alpha: f64) -> RotationalState {
159        RotationalState {
160            angle,
161            omega,
162            alpha,
163        }
164    }
165
166    // -----------------------------------------------------------------------
167    // velocity_at
168    // -----------------------------------------------------------------------
169
170    #[test]
171    fn test_velocity_at_zero_time() {
172        let k = kin();
173        let s = linear(0.0, 5.0, 2.0);
174        assert!((k.velocity_at(&s, 0.0) - 5.0).abs() < EPS);
175    }
176
177    #[test]
178    fn test_velocity_at_positive_time() {
179        let k = kin();
180        let s = linear(0.0, 0.0, 10.0);
181        // v = 0 + 10 * 3 = 30
182        assert!((k.velocity_at(&s, 3.0) - 30.0).abs() < EPS);
183    }
184
185    #[test]
186    fn test_velocity_at_deceleration() {
187        let k = kin();
188        let s = linear(0.0, 20.0, -5.0);
189        // v = 20 - 5*2 = 10
190        assert!((k.velocity_at(&s, 2.0) - 10.0).abs() < EPS);
191    }
192
193    #[test]
194    fn test_velocity_at_zero_acceleration() {
195        let k = kin();
196        let s = linear(100.0, 15.0, 0.0);
197        assert!((k.velocity_at(&s, 7.0) - 15.0).abs() < EPS);
198    }
199
200    // -----------------------------------------------------------------------
201    // position_at
202    // -----------------------------------------------------------------------
203
204    #[test]
205    fn test_position_at_zero_time() {
206        let k = kin();
207        let s = linear(50.0, 3.0, 2.0);
208        assert!((k.position_at(&s, 0.0) - 50.0).abs() < EPS);
209    }
210
211    #[test]
212    fn test_position_at_constant_velocity() {
213        let k = kin();
214        let s = linear(0.0, 10.0, 0.0);
215        // x = 10 * 5 = 50
216        assert!((k.position_at(&s, 5.0) - 50.0).abs() < EPS);
217    }
218
219    #[test]
220    fn test_position_at_with_acceleration() {
221        let k = kin();
222        let s = linear(0.0, 0.0, 2.0);
223        // x = 0.5 * 2 * 4² = 16
224        assert!((k.position_at(&s, 4.0) - 16.0).abs() < EPS);
225    }
226
227    #[test]
228    fn test_position_at_full_equation() {
229        let k = kin();
230        let s = linear(10.0, 5.0, 2.0);
231        // x = 10 + 5*3 + 0.5*2*9 = 10 + 15 + 9 = 34
232        assert!((k.position_at(&s, 3.0) - 34.0).abs() < EPS);
233    }
234
235    // -----------------------------------------------------------------------
236    // propagate_linear
237    // -----------------------------------------------------------------------
238
239    #[test]
240    fn test_propagate_linear_preserves_acceleration() {
241        let k = kin();
242        let s = linear(0.0, 5.0, 3.0);
243        let next = k.propagate_linear(&s, 2.0);
244        assert!((next.acceleration - 3.0).abs() < EPS);
245    }
246
247    #[test]
248    fn test_propagate_linear_position() {
249        let k = kin();
250        let s = linear(0.0, 0.0, 2.0);
251        let next = k.propagate_linear(&s, 3.0);
252        // x = 0.5 * 2 * 9 = 9
253        assert!((next.position - 9.0).abs() < EPS);
254    }
255
256    #[test]
257    fn test_propagate_linear_velocity() {
258        let k = kin();
259        let s = linear(0.0, 5.0, 4.0);
260        let next = k.propagate_linear(&s, 2.0);
261        assert!((next.velocity - 13.0).abs() < EPS);
262    }
263
264    #[test]
265    fn test_propagate_linear_zero_time_identity() {
266        let k = kin();
267        let s = linear(7.0, 3.0, -1.5);
268        let next = k.propagate_linear(&s, 0.0);
269        assert!((next.position - s.position).abs() < EPS);
270        assert!((next.velocity - s.velocity).abs() < EPS);
271    }
272
273    // -----------------------------------------------------------------------
274    // velocity_from_displacement
275    // -----------------------------------------------------------------------
276
277    #[test]
278    fn test_velocity_from_displacement_basic() {
279        let k = kin();
280        let s = linear(0.0, 0.0, 10.0);
281        // v² = 0 + 2*10*5 = 100  → v = 10
282        let v = k.velocity_from_displacement(&s, 5.0).expect("ok");
283        assert!((v - 10.0).abs() < EPS);
284    }
285
286    #[test]
287    fn test_velocity_from_displacement_negative_discriminant() {
288        let k = kin();
289        // v₀ = 1, a = -10, dx = 100 → v² = 1 - 2000 < 0
290        let s = linear(0.0, 1.0, -10.0);
291        assert!(k.velocity_from_displacement(&s, 100.0).is_none());
292    }
293
294    #[test]
295    fn test_velocity_from_displacement_zero() {
296        let k = kin();
297        let s = linear(0.0, 3.0, 0.0);
298        // v² = 9 → v = 3
299        let v = k.velocity_from_displacement(&s, 0.0).expect("ok");
300        assert!((v - 3.0).abs() < EPS);
301    }
302
303    #[test]
304    fn test_velocity_from_displacement_returns_positive() {
305        let k = kin();
306        let s = linear(0.0, 4.0, 0.0);
307        let v = k.velocity_from_displacement(&s, 0.0).expect("ok");
308        assert!(v >= 0.0);
309    }
310
311    // -----------------------------------------------------------------------
312    // time_to_velocity
313    // -----------------------------------------------------------------------
314
315    #[test]
316    fn test_time_to_velocity_basic() {
317        let k = kin();
318        let s = linear(0.0, 0.0, 5.0);
319        // t = (25 - 0) / 5 = 5
320        let t = k.time_to_velocity(&s, 25.0).expect("ok");
321        assert!((t - 5.0).abs() < EPS);
322    }
323
324    #[test]
325    fn test_time_to_velocity_already_there() {
326        let k = kin();
327        let s = linear(0.0, 10.0, 3.0);
328        let t = k.time_to_velocity(&s, 10.0).expect("ok");
329        assert!((t).abs() < EPS);
330    }
331
332    #[test]
333    fn test_time_to_velocity_zero_accel_same_vel() {
334        let k = kin();
335        let s = linear(0.0, 5.0, 0.0);
336        let t = k.time_to_velocity(&s, 5.0);
337        assert_eq!(t, Some(0.0));
338    }
339
340    #[test]
341    fn test_time_to_velocity_zero_accel_different_vel() {
342        let k = kin();
343        let s = linear(0.0, 5.0, 0.0);
344        // Cannot reach v=10 with zero acceleration
345        assert!(k.time_to_velocity(&s, 10.0).is_none());
346    }
347
348    #[test]
349    fn test_time_to_velocity_deceleration() {
350        let k = kin();
351        let s = linear(0.0, 20.0, -4.0);
352        // t = (0 - 20) / (-4) = 5
353        let t = k.time_to_velocity(&s, 0.0).expect("ok");
354        assert!((t - 5.0).abs() < EPS);
355    }
356
357    // -----------------------------------------------------------------------
358    // Projectile motion – 45° launch angle
359    // -----------------------------------------------------------------------
360
361    #[test]
362    fn test_projectile_range_45_degrees() {
363        let k = kin();
364        let v0 = 20.0;
365        let range = k.projectile_range(v0, FRAC_PI_4, G);
366        // R = v₀² / g  at 45°
367        let expected = v0 * v0 / G;
368        assert!((range - expected).abs() < 1e-9);
369    }
370
371    #[test]
372    fn test_projectile_max_height_45_degrees() {
373        let k = kin();
374        let v0 = 10.0;
375        let h = k.projectile_max_height(v0, FRAC_PI_4, G);
376        // H = (v₀/√2)² / (2g) = v₀²/(4g)
377        let expected = v0 * v0 / (4.0 * G);
378        assert!((h - expected).abs() < 1e-9);
379    }
380
381    #[test]
382    fn test_projectile_time_of_flight_45_degrees() {
383        let k = kin();
384        let v0 = 10.0;
385        let tof = k.projectile_time_of_flight(v0, FRAC_PI_4, G);
386        let expected = 2.0 * v0 * FRAC_PI_4.sin() / G;
387        assert!((tof - expected).abs() < 1e-9);
388    }
389
390    #[test]
391    fn test_projectile_range_90_degrees_zero() {
392        // At 90° sin(2*90°) = sin(180°) = 0 → range = 0
393        let k = kin();
394        let range = k.projectile_range(10.0, PI / 2.0, G);
395        assert!(range.abs() < 1e-9);
396    }
397
398    #[test]
399    fn test_projectile_range_30_degrees() {
400        let k = kin();
401        let v0 = 30.0;
402        let range = k.projectile_range(v0, FRAC_PI_6, G);
403        // R = 900 * sin(60°) / 9.81
404        let expected = 900.0 * (PI / 3.0).sin() / G;
405        assert!((range - expected).abs() < 1e-6);
406    }
407
408    #[test]
409    fn test_projectile_max_height_vertical() {
410        let k = kin();
411        let v0 = 10.0;
412        // At 90°: H = v₀²/(2g)
413        let h = k.projectile_max_height(v0, PI / 2.0, G);
414        let expected = v0 * v0 / (2.0 * G);
415        assert!((h - expected).abs() < 1e-9);
416    }
417
418    #[test]
419    fn test_projectile_time_of_flight_positive() {
420        let k = kin();
421        let tof = k.projectile_time_of_flight(20.0, FRAC_PI_4, G);
422        assert!(tof > 0.0);
423    }
424
425    // -----------------------------------------------------------------------
426    // Rotational kinematics
427    // -----------------------------------------------------------------------
428
429    #[test]
430    fn test_angular_velocity_at_zero_time() {
431        let k = kin();
432        let s = rotational(0.0, 3.0, 1.0);
433        assert!((k.angular_velocity_at(&s, 0.0) - 3.0).abs() < EPS);
434    }
435
436    #[test]
437    fn test_angular_velocity_at_positive_time() {
438        let k = kin();
439        let s = rotational(0.0, 2.0, 4.0);
440        // ω = 2 + 4*3 = 14
441        assert!((k.angular_velocity_at(&s, 3.0) - 14.0).abs() < EPS);
442    }
443
444    #[test]
445    fn test_angle_at_zero_time() {
446        let k = kin();
447        let s = rotational(1.5, 2.0, 0.5);
448        assert!((k.angle_at(&s, 0.0) - 1.5).abs() < EPS);
449    }
450
451    #[test]
452    fn test_angle_at_positive_time() {
453        let k = kin();
454        let s = rotational(0.0, 0.0, 2.0);
455        // θ = 0.5 * 2 * 4² = 0.5 * 2 * 16 = 16
456        assert!((k.angle_at(&s, 4.0) - 16.0).abs() < EPS);
457    }
458
459    #[test]
460    fn test_angle_at_full_equation() {
461        let k = kin();
462        let s = rotational(1.0, 3.0, 2.0);
463        // θ = 1 + 3*2 + 0.5*2*4 = 1 + 6 + 4 = 11
464        assert!((k.angle_at(&s, 2.0) - 11.0).abs() < EPS);
465    }
466
467    #[test]
468    fn test_propagate_rotational_preserves_alpha() {
469        let k = kin();
470        let s = rotational(0.0, 1.0, 2.0);
471        let next = k.propagate_rotational(&s, 3.0);
472        assert!((next.alpha - 2.0).abs() < EPS);
473    }
474
475    #[test]
476    fn test_propagate_rotational_angle() {
477        let k = kin();
478        let s = rotational(0.0, 0.0, 2.0);
479        let next = k.propagate_rotational(&s, 3.0);
480        // θ = 0.5 * 2 * 9 = 9
481        assert!((next.angle - 9.0).abs() < EPS);
482    }
483
484    #[test]
485    fn test_propagate_rotational_omega() {
486        let k = kin();
487        let s = rotational(0.0, 1.0, 3.0);
488        let next = k.propagate_rotational(&s, 4.0);
489        // ω = 1 + 3*4 = 13
490        assert!((next.omega - 13.0).abs() < EPS);
491    }
492
493    #[test]
494    fn test_propagate_rotational_zero_time_identity() {
495        let k = kin();
496        let s = rotational(2.5, 1.0, -0.5);
497        let next = k.propagate_rotational(&s, 0.0);
498        assert!((next.angle - s.angle).abs() < EPS);
499        assert!((next.omega - s.omega).abs() < EPS);
500    }
501
502    // -----------------------------------------------------------------------
503    // Consistency between linear and rotational (mirror)
504    // -----------------------------------------------------------------------
505
506    #[test]
507    fn test_linear_rotational_analogy() {
508        let k = kin();
509        // Linear with same parameters should mirror rotational
510        let lin = linear(1.0, 3.0, 0.5);
511        let rot = rotational(1.0, 3.0, 0.5);
512        assert!((k.position_at(&lin, 2.0) - k.angle_at(&rot, 2.0)).abs() < EPS);
513        assert!((k.velocity_at(&lin, 2.0) - k.angular_velocity_at(&rot, 2.0)).abs() < EPS);
514    }
515
516    // -----------------------------------------------------------------------
517    // Default
518    // -----------------------------------------------------------------------
519
520    #[test]
521    fn test_default() {
522        let k = Kinematics;
523        let s = linear(0.0, 5.0, 0.0);
524        assert!((k.velocity_at(&s, 1.0) - 5.0).abs() < EPS);
525    }
526
527    #[test]
528    fn test_linear_state_clone() {
529        let s = linear(1.0, 2.0, 3.0);
530        let c = s.clone();
531        assert_eq!(s, c);
532    }
533
534    #[test]
535    fn test_rotational_state_clone() {
536        let s = rotational(0.1, 0.2, 0.3);
537        let c = s.clone();
538        assert_eq!(s, c);
539    }
540}