1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq)]
455pub struct CollisionBody1D {
456 pub mass: f64,
457 pub velocity: f64,
458}
459
460impl CollisionBody1D {
461 #[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 #[must_use]
484 pub fn kinetic_energy(&self) -> Option<f64> {
485 kinetic_energy(self.mass, self.velocity)
486 }
487
488 #[must_use]
490 pub fn momentum(&self) -> Option<f64> {
491 momentum_from_mass_velocity(self.mass, self.velocity)
492 }
493}
494
495#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}