Skip to main content

use_collision/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Scalar helpers for one-dimensional collisions.
5
6pub mod prelude;
7
8fn finite_result(value: f64) -> Option<f64> {
9    value.is_finite().then_some(value)
10}
11
12fn is_nonnegative_finite(value: f64) -> bool {
13    value.is_finite() && value >= 0.0
14}
15
16fn is_positive_finite(value: f64) -> bool {
17    value.is_finite() && value > 0.0
18}
19
20fn normalized_nonnegative(value: f64) -> Option<f64> {
21    if !value.is_finite() || value < 0.0 {
22        return None;
23    }
24
25    Some(if value == 0.0 { 0.0 } else { value })
26}
27
28fn combined_mass(mass_a: f64, mass_b: f64) -> Option<f64> {
29    if !is_nonnegative_finite(mass_a) || !is_nonnegative_finite(mass_b) {
30        return None;
31    }
32
33    let total_mass = mass_a + mass_b;
34    is_positive_finite(total_mass).then_some(total_mass)
35}
36
37fn momentum_from_mass_velocity(mass: f64, velocity: f64) -> Option<f64> {
38    if !is_nonnegative_finite(mass) || !velocity.is_finite() {
39        return None;
40    }
41
42    finite_result(mass * velocity)
43}
44
45fn total_momentum_1d(mass_a: f64, velocity_a: f64, mass_b: f64, velocity_b: f64) -> Option<f64> {
46    let momentum_a = momentum_from_mass_velocity(mass_a, velocity_a)?;
47    let momentum_b = momentum_from_mass_velocity(mass_b, velocity_b)?;
48
49    finite_result(momentum_a + momentum_b)
50}
51
52/// Computes the signed relative velocity between two one-dimensional bodies.
53///
54/// Formula: `v_rel = v_a - v_b`
55#[must_use]
56pub fn relative_velocity(velocity_a: f64, velocity_b: f64) -> Option<f64> {
57    if !velocity_a.is_finite() || !velocity_b.is_finite() {
58        return None;
59    }
60
61    finite_result(velocity_a - velocity_b)
62}
63
64/// Computes the relative speed between two one-dimensional bodies.
65///
66/// Formula: `speed_rel = |v_a - v_b|`
67#[must_use]
68pub fn relative_speed(velocity_a: f64, velocity_b: f64) -> Option<f64> {
69    let relative = relative_velocity(velocity_a, velocity_b)?;
70
71    normalized_nonnegative(relative.abs())
72}
73
74/// Computes the coefficient of restitution from approach and separation speeds.
75///
76/// Formula: `e = separation_speed / approach_speed`
77///
78/// Returns `None` when `approach_speed` is less than or equal to zero, when
79/// `separation_speed` is negative, when any input is not finite, when the computed value is not
80/// finite, or when the result is greater than `1.0`.
81///
82/// # Examples
83///
84/// ```rust
85/// use use_collision::coefficient_of_restitution;
86///
87/// assert_eq!(coefficient_of_restitution(10.0, 8.0), Some(0.8));
88/// assert_eq!(coefficient_of_restitution(10.0, 0.0), Some(0.0));
89/// ```
90#[must_use]
91pub fn coefficient_of_restitution(approach_speed: f64, separation_speed: f64) -> Option<f64> {
92    if !is_positive_finite(approach_speed) || !is_nonnegative_finite(separation_speed) {
93        return None;
94    }
95
96    let coefficient = separation_speed / approach_speed;
97    if !coefficient.is_finite() || coefficient > 1.0 {
98        return None;
99    }
100
101    normalized_nonnegative(coefficient)
102}
103
104/// Computes separation speed from an approach speed and restitution coefficient.
105///
106/// Formula: `separation_speed = e * approach_speed`
107#[must_use]
108pub fn separation_speed_from_restitution(
109    approach_speed: f64,
110    coefficient_of_restitution: f64,
111) -> Option<f64> {
112    if !is_nonnegative_finite(approach_speed) || !is_valid_restitution(coefficient_of_restitution) {
113        return None;
114    }
115
116    normalized_nonnegative(coefficient_of_restitution * approach_speed)
117}
118
119/// Returns `true` when a restitution coefficient is finite and within `[0.0, 1.0]`.
120#[must_use]
121pub fn is_valid_restitution(coefficient_of_restitution: f64) -> bool {
122    coefficient_of_restitution.is_finite() && (0.0..=1.0).contains(&coefficient_of_restitution)
123}
124
125/// Returns whether a valid restitution coefficient is effectively perfectly elastic.
126///
127/// This returns `Some(true)` when `abs(e - 1.0) <= tolerance`.
128#[must_use]
129pub fn is_perfectly_elastic(coefficient_of_restitution: f64, tolerance: f64) -> Option<bool> {
130    if !is_valid_restitution(coefficient_of_restitution) || !is_nonnegative_finite(tolerance) {
131        return None;
132    }
133
134    Some((coefficient_of_restitution - 1.0).abs() <= tolerance)
135}
136
137/// Returns whether a valid restitution coefficient is effectively perfectly inelastic.
138///
139/// This returns `Some(true)` when `abs(e) <= tolerance`.
140#[must_use]
141pub fn is_perfectly_inelastic(coefficient_of_restitution: f64, tolerance: f64) -> Option<bool> {
142    if !is_valid_restitution(coefficient_of_restitution) || !is_nonnegative_finite(tolerance) {
143        return None;
144    }
145
146    Some(coefficient_of_restitution.abs() <= tolerance)
147}
148
149/// Computes kinetic energy from mass and one-dimensional velocity.
150///
151/// Formula: `KE = 0.5 * m * v²`
152#[must_use]
153pub fn kinetic_energy(mass: f64, velocity: f64) -> Option<f64> {
154    if !is_nonnegative_finite(mass) || !velocity.is_finite() {
155        return None;
156    }
157
158    normalized_nonnegative(0.5 * mass * velocity * velocity)
159}
160
161/// Computes the total kinetic energy of two one-dimensional bodies.
162#[must_use]
163pub fn total_kinetic_energy_1d(
164    mass_a: f64,
165    velocity_a: f64,
166    mass_b: f64,
167    velocity_b: f64,
168) -> Option<f64> {
169    let energy_a = kinetic_energy(mass_a, velocity_a)?;
170    let energy_b = kinetic_energy(mass_b, velocity_b)?;
171
172    normalized_nonnegative(energy_a + energy_b)
173}
174
175/// Computes the kinetic energy lost between an initial and final state.
176///
177/// Formula: `loss = KE_initial - KE_final`
178#[must_use]
179pub fn kinetic_energy_loss(initial_kinetic_energy: f64, final_kinetic_energy: f64) -> Option<f64> {
180    if !is_nonnegative_finite(initial_kinetic_energy)
181        || !is_nonnegative_finite(final_kinetic_energy)
182        || final_kinetic_energy > initial_kinetic_energy
183    {
184        return None;
185    }
186
187    normalized_nonnegative(initial_kinetic_energy - final_kinetic_energy)
188}
189
190/// Computes the fraction of kinetic energy lost between two states.
191///
192/// Formula: `loss_fraction = (KE_initial - KE_final) / KE_initial`
193#[must_use]
194pub fn kinetic_energy_loss_fraction(
195    initial_kinetic_energy: f64,
196    final_kinetic_energy: f64,
197) -> Option<f64> {
198    let invalid_inputs =
199        !is_positive_finite(initial_kinetic_energy) || !is_nonnegative_finite(final_kinetic_energy);
200
201    if invalid_inputs || final_kinetic_energy > initial_kinetic_energy {
202        return None;
203    }
204
205    normalized_nonnegative((initial_kinetic_energy - final_kinetic_energy) / initial_kinetic_energy)
206}
207
208/// Computes the final velocities of a one-dimensional collision from masses, initial velocities,
209/// and a coefficient of restitution.
210///
211/// Formulas:
212///
213/// - `v_a' = (m_a*v_a + m_b*v_b - m_b*e*(v_a - v_b)) / (m_a + m_b)`
214/// - `v_b' = (m_a*v_a + m_b*v_b + m_a*e*(v_a - v_b)) / (m_a + m_b)`
215///
216/// # Examples
217///
218/// ```rust
219/// use use_collision::collision_final_velocities_1d;
220///
221/// let (final_a, final_b) = collision_final_velocities_1d(1.0, 1.0, 1.0, -1.0, 1.0).unwrap();
222///
223/// assert!((final_a + 1.0).abs() < 1.0e-12);
224/// assert!((final_b - 1.0).abs() < 1.0e-12);
225/// ```
226#[must_use]
227pub fn collision_final_velocities_1d(
228    mass_a: f64,
229    velocity_a: f64,
230    mass_b: f64,
231    velocity_b: f64,
232    coefficient_of_restitution: f64,
233) -> Option<(f64, f64)> {
234    if !velocity_a.is_finite()
235        || !velocity_b.is_finite()
236        || !is_valid_restitution(coefficient_of_restitution)
237    {
238        return None;
239    }
240
241    let total_mass = combined_mass(mass_a, mass_b)?;
242    let momentum_sum = total_momentum_1d(mass_a, velocity_a, mass_b, velocity_b)?;
243    let relative = relative_velocity(velocity_a, velocity_b)?;
244    let restitution_term_a = finite_result(mass_b * coefficient_of_restitution * relative)?;
245    let restitution_term_b = finite_result(mass_a * coefficient_of_restitution * relative)?;
246    let final_velocity_a = finite_result((momentum_sum - restitution_term_a) / total_mass)?;
247    let final_velocity_b = finite_result((momentum_sum + restitution_term_b) / total_mass)?;
248
249    Some((final_velocity_a, final_velocity_b))
250}
251
252/// Computes the final velocities of a perfectly elastic one-dimensional collision.
253///
254/// This delegates to [`collision_final_velocities_1d`] with `e = 1.0`.
255///
256/// # Examples
257///
258/// ```rust
259/// use use_collision::elastic_collision_final_velocities_1d;
260///
261/// let (final_a, final_b) = elastic_collision_final_velocities_1d(1.0, 1.0, 1.0, -1.0).unwrap();
262///
263/// assert!((final_a + 1.0).abs() < 1.0e-12);
264/// assert!((final_b - 1.0).abs() < 1.0e-12);
265/// ```
266#[must_use]
267pub fn elastic_collision_final_velocities_1d(
268    mass_a: f64,
269    velocity_a: f64,
270    mass_b: f64,
271    velocity_b: f64,
272) -> Option<(f64, f64)> {
273    collision_final_velocities_1d(mass_a, velocity_a, mass_b, velocity_b, 1.0)
274}
275
276/// Computes the shared final velocity of a perfectly inelastic one-dimensional collision.
277///
278/// Formula: `v_final = (m_a*v_a + m_b*v_b) / (m_a + m_b)`
279///
280/// # Examples
281///
282/// ```rust
283/// use use_collision::perfectly_inelastic_collision_velocity_1d;
284///
285/// let final_velocity = perfectly_inelastic_collision_velocity_1d(2.0, 3.0, 4.0, -1.0).unwrap();
286///
287/// assert!((final_velocity - 0.333_333_333_333_333_3).abs() < 1.0e-12);
288/// ```
289#[must_use]
290pub fn perfectly_inelastic_collision_velocity_1d(
291    mass_a: f64,
292    velocity_a: f64,
293    mass_b: f64,
294    velocity_b: f64,
295) -> Option<f64> {
296    if !velocity_a.is_finite() || !velocity_b.is_finite() {
297        return None;
298    }
299
300    let total_mass = combined_mass(mass_a, mass_b)?;
301    let total_momentum = total_momentum_1d(mass_a, velocity_a, mass_b, velocity_b)?;
302
303    finite_result(total_momentum / total_mass)
304}
305
306/// Computes the final velocities of a perfectly inelastic one-dimensional collision.
307///
308/// This delegates to [`perfectly_inelastic_collision_velocity_1d`] and returns the same velocity
309/// for both bodies.
310#[must_use]
311pub fn perfectly_inelastic_collision_final_velocities_1d(
312    mass_a: f64,
313    velocity_a: f64,
314    mass_b: f64,
315    velocity_b: f64,
316) -> Option<(f64, f64)> {
317    let final_velocity =
318        perfectly_inelastic_collision_velocity_1d(mass_a, velocity_a, mass_b, velocity_b)?;
319
320    Some((final_velocity, final_velocity))
321}
322
323/// Computes the collision impulse applied to body A.
324///
325/// Formula: `J_a = m_a * (v_a_final - v_a_initial)`
326#[must_use]
327pub fn collision_impulse_on_a(
328    mass_a: f64,
329    initial_velocity_a: f64,
330    final_velocity_a: f64,
331) -> Option<f64> {
332    if !is_nonnegative_finite(mass_a)
333        || !initial_velocity_a.is_finite()
334        || !final_velocity_a.is_finite()
335    {
336        return None;
337    }
338
339    finite_result(mass_a * (final_velocity_a - initial_velocity_a))
340}
341
342/// Computes the collision impulse applied to body B.
343///
344/// Formula: `J_b = m_b * (v_b_final - v_b_initial)`
345#[must_use]
346pub fn collision_impulse_on_b(
347    mass_b: f64,
348    initial_velocity_b: f64,
349    final_velocity_b: f64,
350) -> Option<f64> {
351    if !is_nonnegative_finite(mass_b)
352        || !initial_velocity_b.is_finite()
353        || !final_velocity_b.is_finite()
354    {
355        return None;
356    }
357
358    finite_result(mass_b * (final_velocity_b - initial_velocity_b))
359}
360
361/// Computes the impulses on both bodies for a one-dimensional collision.
362///
363/// This computes the final velocities with [`collision_final_velocities_1d`] and then returns the
364/// impulse on A and the impulse on B.
365///
366/// # Examples
367///
368/// ```rust
369/// use use_collision::collision_impulses_1d;
370///
371/// let (impulse_a, impulse_b) = collision_impulses_1d(1.0, 1.0, 1.0, -1.0, 1.0).unwrap();
372///
373/// assert!((impulse_a + 2.0).abs() < 1.0e-12);
374/// assert!((impulse_b - 2.0).abs() < 1.0e-12);
375/// ```
376#[must_use]
377pub fn collision_impulses_1d(
378    mass_a: f64,
379    velocity_a: f64,
380    mass_b: f64,
381    velocity_b: f64,
382    coefficient_of_restitution: f64,
383) -> Option<(f64, f64)> {
384    let (final_velocity_a, final_velocity_b) = collision_final_velocities_1d(
385        mass_a,
386        velocity_a,
387        mass_b,
388        velocity_b,
389        coefficient_of_restitution,
390    )?;
391    let impulse_a = collision_impulse_on_a(mass_a, velocity_a, final_velocity_a)?;
392    let impulse_b = collision_impulse_on_b(mass_b, velocity_b, final_velocity_b)?;
393
394    Some((impulse_a, impulse_b))
395}
396
397/// Computes the total kinetic energy lost in a one-dimensional collision.
398///
399/// This computes the initial and final total kinetic energy and returns the non-negative loss.
400///
401/// # Examples
402///
403/// ```rust
404/// use use_collision::collision_energy_loss_1d;
405///
406/// let loss = collision_energy_loss_1d(1.0, 1.0, 1.0, -1.0, 0.0).unwrap();
407///
408/// assert!((loss - 1.0).abs() < 1.0e-12);
409/// ```
410#[must_use]
411pub fn collision_energy_loss_1d(
412    mass_a: f64,
413    velocity_a: f64,
414    mass_b: f64,
415    velocity_b: f64,
416    coefficient_of_restitution: f64,
417) -> Option<f64> {
418    let initial_energy = total_kinetic_energy_1d(mass_a, velocity_a, mass_b, velocity_b)?;
419    let (final_velocity_a, final_velocity_b) = collision_final_velocities_1d(
420        mass_a,
421        velocity_a,
422        mass_b,
423        velocity_b,
424        coefficient_of_restitution,
425    )?;
426    let final_energy = total_kinetic_energy_1d(mass_a, final_velocity_a, mass_b, final_velocity_b)?;
427
428    kinetic_energy_loss(initial_energy, final_energy)
429}
430
431/// Computes the fraction of kinetic energy lost in a one-dimensional collision.
432#[must_use]
433pub fn collision_energy_loss_fraction_1d(
434    mass_a: f64,
435    velocity_a: f64,
436    mass_b: f64,
437    velocity_b: f64,
438    coefficient_of_restitution: f64,
439) -> Option<f64> {
440    let initial_energy = total_kinetic_energy_1d(mass_a, velocity_a, mass_b, velocity_b)?;
441    let (final_velocity_a, final_velocity_b) = collision_final_velocities_1d(
442        mass_a,
443        velocity_a,
444        mass_b,
445        velocity_b,
446        coefficient_of_restitution,
447    )?;
448    let final_energy = total_kinetic_energy_1d(mass_a, final_velocity_a, mass_b, final_velocity_b)?;
449
450    kinetic_energy_loss_fraction(initial_energy, final_energy)
451}
452
453/// A one-dimensional body with scalar mass and velocity.
454#[derive(Debug, Clone, Copy, PartialEq)]
455pub struct CollisionBody1D {
456    pub mass: f64,
457    pub velocity: f64,
458}
459
460impl CollisionBody1D {
461    /// Creates a one-dimensional collision body when `mass` is non-negative and both values are
462    /// finite.
463    #[must_use]
464    pub fn new(mass: f64, velocity: f64) -> Option<Self> {
465        if !is_nonnegative_finite(mass) || !velocity.is_finite() {
466            return None;
467        }
468
469        Some(Self { mass, velocity })
470    }
471
472    /// Computes kinetic energy for this body.
473    ///
474    /// # Examples
475    ///
476    /// ```rust
477    /// use use_collision::CollisionBody1D;
478    ///
479    /// let body = CollisionBody1D::new(2.0, 3.0).unwrap();
480    ///
481    /// assert_eq!(body.kinetic_energy(), Some(9.0));
482    /// ```
483    #[must_use]
484    pub fn kinetic_energy(&self) -> Option<f64> {
485        kinetic_energy(self.mass, self.velocity)
486    }
487
488    /// Computes scalar momentum for this body using `p = m * v`.
489    #[must_use]
490    pub fn momentum(&self) -> Option<f64> {
491        momentum_from_mass_velocity(self.mass, self.velocity)
492    }
493}
494
495/// A one-dimensional collision configuration with two bodies and a restitution coefficient.
496#[derive(Debug, Clone, Copy, PartialEq)]
497pub struct Collision1D {
498    pub body_a: CollisionBody1D,
499    pub body_b: CollisionBody1D,
500    pub coefficient_of_restitution: f64,
501}
502
503impl Collision1D {
504    /// Creates a one-dimensional collision when the restitution coefficient is valid.
505    #[must_use]
506    pub fn new(
507        body_a: CollisionBody1D,
508        body_b: CollisionBody1D,
509        coefficient_of_restitution: f64,
510    ) -> Option<Self> {
511        if !is_valid_restitution(coefficient_of_restitution) {
512            return None;
513        }
514
515        Some(Self {
516            body_a,
517            body_b,
518            coefficient_of_restitution,
519        })
520    }
521
522    /// Computes the final velocities of both bodies.
523    ///
524    /// # Examples
525    ///
526    /// ```rust
527    /// use use_collision::{Collision1D, CollisionBody1D};
528    ///
529    /// let body_a = CollisionBody1D::new(1.0, 1.0).unwrap();
530    /// let body_b = CollisionBody1D::new(1.0, -1.0).unwrap();
531    /// let collision = Collision1D::new(body_a, body_b, 1.0).unwrap();
532    ///
533    /// let (final_a, final_b) = collision.final_velocities().unwrap();
534    ///
535    /// assert!((final_a + 1.0).abs() < 1.0e-12);
536    /// assert!((final_b - 1.0).abs() < 1.0e-12);
537    /// ```
538    #[must_use]
539    pub fn final_velocities(&self) -> Option<(f64, f64)> {
540        collision_final_velocities_1d(
541            self.body_a.mass,
542            self.body_a.velocity,
543            self.body_b.mass,
544            self.body_b.velocity,
545            self.coefficient_of_restitution,
546        )
547    }
548
549    /// Computes the final body states after the collision.
550    #[must_use]
551    pub fn final_bodies(&self) -> Option<(CollisionBody1D, CollisionBody1D)> {
552        let (final_velocity_a, final_velocity_b) = self.final_velocities()?;
553        let body_a = CollisionBody1D::new(self.body_a.mass, final_velocity_a)?;
554        let body_b = CollisionBody1D::new(self.body_b.mass, final_velocity_b)?;
555
556        Some((body_a, body_b))
557    }
558
559    /// Computes the initial total kinetic energy.
560    #[must_use]
561    pub fn initial_kinetic_energy(&self) -> Option<f64> {
562        total_kinetic_energy_1d(
563            self.body_a.mass,
564            self.body_a.velocity,
565            self.body_b.mass,
566            self.body_b.velocity,
567        )
568    }
569
570    /// Computes the final total kinetic energy.
571    #[must_use]
572    pub fn final_kinetic_energy(&self) -> Option<f64> {
573        let (final_velocity_a, final_velocity_b) = self.final_velocities()?;
574
575        total_kinetic_energy_1d(
576            self.body_a.mass,
577            final_velocity_a,
578            self.body_b.mass,
579            final_velocity_b,
580        )
581    }
582
583    /// Computes the total kinetic energy lost in the collision.
584    #[must_use]
585    pub fn kinetic_energy_loss(&self) -> Option<f64> {
586        collision_energy_loss_1d(
587            self.body_a.mass,
588            self.body_a.velocity,
589            self.body_b.mass,
590            self.body_b.velocity,
591            self.coefficient_of_restitution,
592        )
593    }
594
595    /// Computes the fraction of kinetic energy lost in the collision.
596    #[must_use]
597    pub fn kinetic_energy_loss_fraction(&self) -> Option<f64> {
598        collision_energy_loss_fraction_1d(
599            self.body_a.mass,
600            self.body_a.velocity,
601            self.body_b.mass,
602            self.body_b.velocity,
603            self.coefficient_of_restitution,
604        )
605    }
606
607    /// Computes the impulses applied to both bodies.
608    #[must_use]
609    pub fn impulses(&self) -> Option<(f64, f64)> {
610        collision_impulses_1d(
611            self.body_a.mass,
612            self.body_a.velocity,
613            self.body_b.mass,
614            self.body_b.velocity,
615            self.coefficient_of_restitution,
616        )
617    }
618}
619
620#[cfg(test)]
621#[allow(clippy::float_cmp)]
622mod tests {
623    use super::{
624        Collision1D, CollisionBody1D, coefficient_of_restitution, collision_energy_loss_1d,
625        collision_energy_loss_fraction_1d, collision_final_velocities_1d, collision_impulse_on_a,
626        collision_impulse_on_b, collision_impulses_1d, elastic_collision_final_velocities_1d,
627        is_perfectly_elastic, is_perfectly_inelastic, is_valid_restitution, kinetic_energy,
628        kinetic_energy_loss, kinetic_energy_loss_fraction,
629        perfectly_inelastic_collision_final_velocities_1d,
630        perfectly_inelastic_collision_velocity_1d, relative_speed, relative_velocity,
631        separation_speed_from_restitution, total_kinetic_energy_1d,
632    };
633
634    const EPSILON: f64 = 1.0e-12;
635
636    fn assert_approx_eq(actual: f64, expected: f64) {
637        assert!(
638            (actual - expected).abs() <= EPSILON,
639            "expected {expected}, got {actual}"
640        );
641    }
642
643    fn assert_option_approx_eq(actual: Option<f64>, expected: f64) {
644        match actual {
645            Some(value) => assert_approx_eq(value, expected),
646            None => panic!("expected Some({expected}), got None"),
647        }
648    }
649
650    fn assert_option_pair_approx_eq(actual: Option<(f64, f64)>, expected: (f64, f64)) {
651        match actual {
652            Some((value_a, value_b)) => {
653                assert_approx_eq(value_a, expected.0);
654                assert_approx_eq(value_b, expected.1);
655            },
656            None => panic!("expected Some(({},{}) ), got None", expected.0, expected.1),
657        }
658    }
659
660    #[test]
661    fn relative_velocity_and_speed_cover_signed_inputs() {
662        assert_eq!(relative_velocity(5.0, 2.0), Some(3.0));
663        assert_eq!(relative_velocity(2.0, 5.0), Some(-3.0));
664        assert_eq!(relative_speed(2.0, 5.0), Some(3.0));
665    }
666
667    #[test]
668    fn restitution_helpers_validate_common_cases() {
669        assert_eq!(coefficient_of_restitution(10.0, 8.0), Some(0.8));
670        assert_eq!(coefficient_of_restitution(10.0, 0.0), Some(0.0));
671        assert_eq!(coefficient_of_restitution(0.0, 1.0), None);
672        assert_eq!(coefficient_of_restitution(10.0, -1.0), None);
673        assert_eq!(coefficient_of_restitution(10.0, 11.0), None);
674
675        assert_eq!(separation_speed_from_restitution(10.0, 0.8), Some(8.0));
676        assert_eq!(separation_speed_from_restitution(10.0, 1.2), None);
677
678        assert!(is_valid_restitution(0.0));
679        assert!(is_valid_restitution(1.0));
680        assert!(!is_valid_restitution(-0.1));
681        assert!(!is_valid_restitution(1.1));
682
683        assert_eq!(is_perfectly_elastic(1.0, 0.0), Some(true));
684        assert_eq!(is_perfectly_elastic(0.99, 0.02), Some(true));
685        assert_eq!(is_perfectly_elastic(0.9, 0.02), Some(false));
686
687        assert_eq!(is_perfectly_inelastic(0.0, 0.0), Some(true));
688        assert_eq!(is_perfectly_inelastic(0.01, 0.02), Some(true));
689        assert_eq!(is_perfectly_inelastic(0.1, 0.02), Some(false));
690    }
691
692    #[test]
693    fn kinetic_energy_helpers_cover_common_cases() {
694        assert_eq!(kinetic_energy(2.0, 3.0), Some(9.0));
695        assert_eq!(kinetic_energy(2.0, -3.0), Some(9.0));
696        assert_eq!(kinetic_energy(-2.0, 3.0), None);
697
698        assert_eq!(total_kinetic_energy_1d(2.0, 3.0, 4.0, 1.0), Some(11.0));
699
700        assert_eq!(kinetic_energy_loss(10.0, 6.0), Some(4.0));
701        assert_eq!(kinetic_energy_loss(6.0, 10.0), None);
702
703        assert_eq!(kinetic_energy_loss_fraction(10.0, 6.0), Some(0.4));
704        assert_eq!(kinetic_energy_loss_fraction(0.0, 0.0), None);
705    }
706
707    #[test]
708    fn collision_velocity_helpers_cover_elastic_and_inelastic_cases() {
709        assert_option_pair_approx_eq(
710            elastic_collision_final_velocities_1d(1.0, 1.0, 1.0, -1.0),
711            (-1.0, 1.0),
712        );
713
714        assert_option_pair_approx_eq(
715            collision_final_velocities_1d(1.0, 1.0, 1.0, -1.0, 1.0),
716            (-1.0, 1.0),
717        );
718        assert_option_pair_approx_eq(
719            collision_final_velocities_1d(1.0, 1.0, 1.0, -1.0, 0.0),
720            (0.0, 0.0),
721        );
722        assert_eq!(
723            collision_final_velocities_1d(1.0, 1.0, 1.0, -1.0, 1.2),
724            None
725        );
726        assert_eq!(
727            collision_final_velocities_1d(-1.0, 1.0, 1.0, -1.0, 1.0),
728            None
729        );
730
731        assert_eq!(
732            perfectly_inelastic_collision_velocity_1d(1.0, 1.0, 1.0, -1.0),
733            Some(0.0)
734        );
735        assert_option_approx_eq(
736            perfectly_inelastic_collision_velocity_1d(2.0, 3.0, 4.0, -1.0),
737            0.333_333_333_333_333_3,
738        );
739
740        assert_eq!(
741            perfectly_inelastic_collision_final_velocities_1d(1.0, 1.0, 1.0, -1.0),
742            Some((0.0, 0.0))
743        );
744    }
745
746    #[test]
747    fn impulse_and_energy_summary_helpers_cover_common_cases() {
748        assert_eq!(collision_impulse_on_a(2.0, 3.0, 1.0), Some(-4.0));
749        assert_eq!(collision_impulse_on_b(2.0, 1.0, 3.0), Some(4.0));
750
751        assert_option_pair_approx_eq(collision_impulses_1d(1.0, 1.0, 1.0, -1.0, 1.0), (-2.0, 2.0));
752
753        assert_option_approx_eq(collision_energy_loss_1d(1.0, 1.0, 1.0, -1.0, 1.0), 0.0);
754        assert_option_approx_eq(collision_energy_loss_1d(1.0, 1.0, 1.0, -1.0, 0.0), 1.0);
755
756        assert_option_approx_eq(
757            collision_energy_loss_fraction_1d(1.0, 1.0, 1.0, -1.0, 0.0),
758            1.0,
759        );
760    }
761
762    #[test]
763    fn simple_types_delegate_to_public_helpers() {
764        let body = CollisionBody1D::new(2.0, 3.0).unwrap();
765        assert_eq!(body.kinetic_energy(), Some(9.0));
766        assert_eq!(body.momentum(), Some(6.0));
767        assert_eq!(CollisionBody1D::new(-2.0, 3.0), None);
768
769        let body_a = CollisionBody1D::new(1.0, 1.0).unwrap();
770        let body_b = CollisionBody1D::new(1.0, -1.0).unwrap();
771        let collision = Collision1D::new(body_a, body_b, 1.0).unwrap();
772
773        assert_option_pair_approx_eq(collision.final_velocities(), (-1.0, 1.0));
774        assert_option_approx_eq(collision.initial_kinetic_energy(), 1.0);
775        assert_option_approx_eq(collision.final_kinetic_energy(), 1.0);
776        assert_option_approx_eq(collision.kinetic_energy_loss(), 0.0);
777        assert_option_pair_approx_eq(collision.impulses(), (-2.0, 2.0));
778        assert_eq!(Collision1D::new(body_a, body_b, 1.2), None);
779    }
780}