1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::f64::consts::TAU;
7
8pub mod prelude;
9
10fn finite(value: f64) -> Option<f64> {
11 value.is_finite().then_some(value)
12}
13
14fn all_finite(values: &[f64]) -> bool {
15 values.iter().all(|value| value.is_finite())
16}
17
18fn nonnegative_finite(value: f64) -> bool {
19 value.is_finite() && value >= 0.0
20}
21
22fn positive_finite(value: f64) -> bool {
23 value.is_finite() && value > 0.0
24}
25
26fn harmonic_phase(amplitude: f64, angular_frequency: f64, time: f64, phase: f64) -> Option<f64> {
27 if !all_finite(&[amplitude, angular_frequency, time, phase])
28 || amplitude < 0.0
29 || angular_frequency < 0.0
30 || time < 0.0
31 {
32 return None;
33 }
34
35 finite(angular_frequency.mul_add(time, phase))
36}
37
38#[must_use]
52pub fn period_from_frequency(frequency: f64) -> Option<f64> {
53 if !positive_finite(frequency) {
54 return None;
55 }
56
57 finite(1.0 / frequency)
58}
59
60#[must_use]
65pub fn frequency_from_period(period: f64) -> Option<f64> {
66 if !positive_finite(period) {
67 return None;
68 }
69
70 finite(1.0 / period)
71}
72
73#[must_use]
75pub fn angular_frequency_from_frequency(frequency: f64) -> Option<f64> {
76 if !nonnegative_finite(frequency) {
77 return None;
78 }
79
80 finite(TAU * frequency)
81}
82
83#[must_use]
85pub fn frequency_from_angular_frequency(angular_frequency: f64) -> Option<f64> {
86 if !nonnegative_finite(angular_frequency) {
87 return None;
88 }
89
90 finite(angular_frequency / TAU)
91}
92
93#[must_use]
106pub fn angular_frequency_from_period(period: f64) -> Option<f64> {
107 if !positive_finite(period) {
108 return None;
109 }
110
111 finite(TAU / period)
112}
113
114#[must_use]
116pub fn period_from_angular_frequency(angular_frequency: f64) -> Option<f64> {
117 if !positive_finite(angular_frequency) {
118 return None;
119 }
120
121 finite(TAU / angular_frequency)
122}
123
124#[must_use]
136pub fn displacement(amplitude: f64, angular_frequency: f64, time: f64, phase: f64) -> Option<f64> {
137 let angle = harmonic_phase(amplitude, angular_frequency, time, phase)?;
138
139 finite(amplitude * angle.cos())
140}
141
142#[must_use]
144pub fn velocity(amplitude: f64, angular_frequency: f64, time: f64, phase: f64) -> Option<f64> {
145 let angle = harmonic_phase(amplitude, angular_frequency, time, phase)?;
146
147 finite(-amplitude * angular_frequency * angle.sin())
148}
149
150#[must_use]
152pub fn acceleration(amplitude: f64, angular_frequency: f64, time: f64, phase: f64) -> Option<f64> {
153 let angle = harmonic_phase(amplitude, angular_frequency, time, phase)?;
154
155 finite(-amplitude * angular_frequency.powi(2) * angle.cos())
156}
157
158#[must_use]
160pub fn acceleration_from_displacement(displacement: f64, angular_frequency: f64) -> Option<f64> {
161 if !displacement.is_finite() || !nonnegative_finite(angular_frequency) {
162 return None;
163 }
164
165 finite(-angular_frequency.powi(2) * displacement)
166}
167
168#[must_use]
170pub fn max_speed(amplitude: f64, angular_frequency: f64) -> Option<f64> {
171 if !nonnegative_finite(amplitude) || !nonnegative_finite(angular_frequency) {
172 return None;
173 }
174
175 finite(amplitude * angular_frequency)
176}
177
178#[must_use]
180pub fn max_acceleration(amplitude: f64, angular_frequency: f64) -> Option<f64> {
181 if !nonnegative_finite(amplitude) || !nonnegative_finite(angular_frequency) {
182 return None;
183 }
184
185 finite(amplitude * angular_frequency.powi(2))
186}
187
188#[must_use]
190pub fn spring_angular_frequency(spring_constant: f64, mass: f64) -> Option<f64> {
191 if !nonnegative_finite(spring_constant) || !positive_finite(mass) {
192 return None;
193 }
194
195 finite((spring_constant / mass).sqrt())
196}
197
198#[must_use]
211pub fn spring_period(spring_constant: f64, mass: f64) -> Option<f64> {
212 if !positive_finite(spring_constant) || !positive_finite(mass) {
213 return None;
214 }
215
216 finite(TAU * (mass / spring_constant).sqrt())
217}
218
219#[must_use]
221pub fn spring_frequency(spring_constant: f64, mass: f64) -> Option<f64> {
222 let period = spring_period(spring_constant, mass)?;
223
224 frequency_from_period(period)
225}
226
227#[must_use]
229pub fn spring_constant_from_period(mass: f64, period: f64) -> Option<f64> {
230 if !nonnegative_finite(mass) || !positive_finite(period) {
231 return None;
232 }
233
234 finite(TAU.powi(2) * mass / period.powi(2))
235}
236
237#[must_use]
239pub fn mass_from_spring_period(spring_constant: f64, period: f64) -> Option<f64> {
240 if !nonnegative_finite(spring_constant) || !positive_finite(period) {
241 return None;
242 }
243
244 finite(spring_constant * period.powi(2) / TAU.powi(2))
245}
246
247#[must_use]
260pub fn simple_pendulum_period(length: f64, gravitational_acceleration: f64) -> Option<f64> {
261 if !length.is_finite() || length < 0.0 || !positive_finite(gravitational_acceleration) {
262 return None;
263 }
264
265 finite(TAU * (length / gravitational_acceleration).sqrt())
266}
267
268#[must_use]
270pub fn simple_pendulum_frequency(length: f64, gravitational_acceleration: f64) -> Option<f64> {
271 let period = simple_pendulum_period(length, gravitational_acceleration)?;
272
273 frequency_from_period(period)
274}
275
276#[must_use]
278pub fn simple_pendulum_angular_frequency(
279 length: f64,
280 gravitational_acceleration: f64,
281) -> Option<f64> {
282 if !positive_finite(length) || !positive_finite(gravitational_acceleration) {
283 return None;
284 }
285
286 finite((gravitational_acceleration / length).sqrt())
287}
288
289#[must_use]
291pub fn pendulum_length_from_period(period: f64, gravitational_acceleration: f64) -> Option<f64> {
292 if !positive_finite(period) || !nonnegative_finite(gravitational_acceleration) {
293 return None;
294 }
295
296 finite(gravitational_acceleration * (period / TAU).powi(2))
297}
298
299#[must_use]
301pub fn spring_potential_energy(spring_constant: f64, displacement: f64) -> Option<f64> {
302 if !spring_constant.is_finite() || spring_constant < 0.0 || !displacement.is_finite() {
303 return None;
304 }
305
306 finite(0.5 * spring_constant * displacement.powi(2))
307}
308
309#[must_use]
311pub fn oscillator_total_energy(spring_constant: f64, amplitude: f64) -> Option<f64> {
312 if !nonnegative_finite(spring_constant) || !nonnegative_finite(amplitude) {
313 return None;
314 }
315
316 finite(0.5 * spring_constant * amplitude.powi(2))
317}
318
319#[must_use]
321pub fn kinetic_energy_from_total_and_potential(
322 total_energy: f64,
323 potential_energy: f64,
324) -> Option<f64> {
325 if !nonnegative_finite(total_energy) || !nonnegative_finite(potential_energy) {
326 return None;
327 }
328
329 let kinetic_energy = finite(total_energy - potential_energy)?;
330 if kinetic_energy < 0.0 {
331 return None;
332 }
333
334 Some(kinetic_energy)
335}
336
337#[must_use]
347pub fn damping_ratio(damping_coefficient: f64, mass: f64, spring_constant: f64) -> Option<f64> {
348 if !nonnegative_finite(damping_coefficient)
349 || !positive_finite(mass)
350 || !positive_finite(spring_constant)
351 {
352 return None;
353 }
354
355 let denominator = finite(2.0 * (mass * spring_constant).sqrt())?;
356
357 finite(damping_coefficient / denominator)
358}
359
360#[must_use]
362pub fn critical_damping_coefficient(mass: f64, spring_constant: f64) -> Option<f64> {
363 if !nonnegative_finite(mass) || !nonnegative_finite(spring_constant) {
364 return None;
365 }
366
367 finite(2.0 * (mass * spring_constant).sqrt())
368}
369
370#[must_use]
372pub fn damped_angular_frequency(
373 undamped_angular_frequency: f64,
374 damping_ratio: f64,
375) -> Option<f64> {
376 if !nonnegative_finite(undamped_angular_frequency)
377 || !nonnegative_finite(damping_ratio)
378 || damping_ratio >= 1.0
379 {
380 return None;
381 }
382
383 let damping_term = damping_ratio.mul_add(-damping_ratio, 1.0);
384
385 finite(undamped_angular_frequency * damping_term.sqrt())
386}
387
388#[must_use]
390pub fn is_underdamped(damping_ratio: f64) -> bool {
391 damping_ratio.is_finite() && (0.0..1.0).contains(&damping_ratio)
392}
393
394#[must_use]
396pub fn is_critically_damped(damping_ratio: f64, tolerance: f64) -> Option<bool> {
397 if !damping_ratio.is_finite() || !nonnegative_finite(tolerance) {
398 return None;
399 }
400
401 Some((damping_ratio - 1.0).abs() <= tolerance)
402}
403
404#[must_use]
406pub fn is_overdamped(damping_ratio: f64) -> bool {
407 damping_ratio.is_finite() && damping_ratio > 1.0
408}
409
410#[must_use]
420pub fn quality_factor_from_damping_ratio(damping_ratio: f64) -> Option<f64> {
421 if !positive_finite(damping_ratio) {
422 return None;
423 }
424
425 finite(1.0 / (2.0 * damping_ratio))
426}
427
428#[must_use]
430pub fn damping_ratio_from_quality_factor(quality_factor: f64) -> Option<f64> {
431 if !positive_finite(quality_factor) {
432 return None;
433 }
434
435 finite(1.0 / (2.0 * quality_factor))
436}
437
438#[must_use]
440pub fn resonance_angular_frequency_natural(spring_constant: f64, mass: f64) -> Option<f64> {
441 spring_angular_frequency(spring_constant, mass)
442}
443
444#[derive(Debug, Clone, Copy, PartialEq)]
446pub struct SimpleHarmonicOscillator {
447 pub amplitude: f64,
448 pub angular_frequency: f64,
449 pub phase: f64,
450}
451
452impl SimpleHarmonicOscillator {
453 #[must_use]
455 pub fn new(amplitude: f64, angular_frequency: f64, phase: f64) -> Option<Self> {
456 if !nonnegative_finite(amplitude)
457 || !nonnegative_finite(angular_frequency)
458 || !phase.is_finite()
459 {
460 return None;
461 }
462
463 Some(Self {
464 amplitude,
465 angular_frequency,
466 phase,
467 })
468 }
469
470 #[must_use]
482 pub fn displacement(&self, time: f64) -> Option<f64> {
483 displacement(self.amplitude, self.angular_frequency, time, self.phase)
484 }
485
486 #[must_use]
488 pub fn velocity(&self, time: f64) -> Option<f64> {
489 velocity(self.amplitude, self.angular_frequency, time, self.phase)
490 }
491
492 #[must_use]
494 pub fn acceleration(&self, time: f64) -> Option<f64> {
495 acceleration(self.amplitude, self.angular_frequency, time, self.phase)
496 }
497
498 #[must_use]
500 pub fn period(&self) -> Option<f64> {
501 period_from_angular_frequency(self.angular_frequency)
502 }
503
504 #[must_use]
506 pub fn frequency(&self) -> Option<f64> {
507 frequency_from_angular_frequency(self.angular_frequency)
508 }
509
510 #[must_use]
512 pub fn max_speed(&self) -> Option<f64> {
513 max_speed(self.amplitude, self.angular_frequency)
514 }
515
516 #[must_use]
518 pub fn max_acceleration(&self) -> Option<f64> {
519 max_acceleration(self.amplitude, self.angular_frequency)
520 }
521}
522
523#[derive(Debug, Clone, Copy, PartialEq)]
525pub struct SpringOscillator {
526 pub spring_constant: f64,
527 pub mass: f64,
528}
529
530impl SpringOscillator {
531 #[must_use]
533 pub fn new(spring_constant: f64, mass: f64) -> Option<Self> {
534 if !nonnegative_finite(spring_constant) || !positive_finite(mass) {
535 return None;
536 }
537
538 Some(Self {
539 spring_constant,
540 mass,
541 })
542 }
543
544 #[must_use]
546 pub fn angular_frequency(&self) -> Option<f64> {
547 spring_angular_frequency(self.spring_constant, self.mass)
548 }
549
550 #[must_use]
563 pub fn period(&self) -> Option<f64> {
564 spring_period(self.spring_constant, self.mass)
565 }
566
567 #[must_use]
569 pub fn frequency(&self) -> Option<f64> {
570 spring_frequency(self.spring_constant, self.mass)
571 }
572
573 #[must_use]
575 pub fn total_energy(&self, amplitude: f64) -> Option<f64> {
576 oscillator_total_energy(self.spring_constant, amplitude)
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use core::f64::consts::{PI, TAU};
583
584 use super::{
585 SimpleHarmonicOscillator, SpringOscillator, acceleration, acceleration_from_displacement,
586 angular_frequency_from_frequency, angular_frequency_from_period,
587 critical_damping_coefficient, damped_angular_frequency, damping_ratio,
588 damping_ratio_from_quality_factor, displacement, frequency_from_angular_frequency,
589 frequency_from_period, is_critically_damped, is_overdamped, is_underdamped,
590 kinetic_energy_from_total_and_potential, mass_from_spring_period, max_acceleration,
591 max_speed, oscillator_total_energy, pendulum_length_from_period,
592 period_from_angular_frequency, period_from_frequency, quality_factor_from_damping_ratio,
593 resonance_angular_frequency_natural, simple_pendulum_angular_frequency,
594 simple_pendulum_frequency, simple_pendulum_period, spring_angular_frequency,
595 spring_constant_from_period, spring_frequency, spring_period, spring_potential_energy,
596 velocity,
597 };
598
599 fn assert_approx_eq(left: f64, right: f64) {
600 let delta = (left - right).abs();
601
602 assert!(
603 delta <= 1.0e-12,
604 "left={left} right={right} delta={delta} tolerance=1e-12"
605 );
606 }
607
608 #[test]
609 fn period_and_frequency_helpers_cover_basic_cases() {
610 assert_eq!(period_from_frequency(2.0), Some(0.5));
611 assert_eq!(period_from_frequency(0.0), None);
612
613 assert_eq!(frequency_from_period(0.5), Some(2.0));
614 assert_eq!(frequency_from_period(0.0), None);
615
616 assert_approx_eq(angular_frequency_from_frequency(1.0).unwrap(), TAU);
617 assert_approx_eq(frequency_from_angular_frequency(TAU).unwrap(), 1.0);
618
619 assert_approx_eq(angular_frequency_from_period(1.0).unwrap(), TAU);
620 assert_approx_eq(period_from_angular_frequency(TAU).unwrap(), 1.0);
621 }
622
623 #[test]
624 fn simple_harmonic_motion_helpers_cover_basic_cases() {
625 assert_approx_eq(displacement(2.0, 1.0, 0.0, 0.0).unwrap(), 2.0);
626 assert_approx_eq(velocity(2.0, 1.0, 0.0, 0.0).unwrap(), 0.0);
627 assert_approx_eq(acceleration(2.0, 1.0, 0.0, 0.0).unwrap(), -2.0);
628
629 assert_eq!(acceleration_from_displacement(2.0, 3.0), Some(-18.0));
630 assert_eq!(max_speed(2.0, 3.0), Some(6.0));
631 assert_eq!(max_acceleration(2.0, 3.0), Some(18.0));
632 }
633
634 #[test]
635 fn spring_oscillator_helpers_cover_basic_cases() {
636 assert_eq!(spring_angular_frequency(8.0, 2.0), Some(2.0));
637 assert_eq!(spring_angular_frequency(8.0, 0.0), None);
638
639 assert_approx_eq(spring_period(8.0, 2.0).unwrap(), PI);
640 assert_eq!(spring_period(0.0, 2.0), None);
641
642 assert_approx_eq(spring_frequency(8.0, 2.0).unwrap(), 1.0 / PI);
643 assert_approx_eq(spring_constant_from_period(2.0, PI).unwrap(), 8.0);
644 assert_approx_eq(mass_from_spring_period(8.0, PI).unwrap(), 2.0);
645 }
646
647 #[test]
648 fn pendulum_helpers_cover_basic_cases() {
649 assert_approx_eq(simple_pendulum_period(9.806_65, 9.806_65).unwrap(), TAU);
650 assert_eq!(simple_pendulum_period(-1.0, 9.806_65), None);
651
652 assert_approx_eq(
653 simple_pendulum_frequency(9.806_65, 9.806_65).unwrap(),
654 1.0 / TAU,
655 );
656 assert_approx_eq(
657 simple_pendulum_angular_frequency(9.806_65, 9.806_65).unwrap(),
658 1.0,
659 );
660 assert_approx_eq(
661 pendulum_length_from_period(TAU, 9.806_65).unwrap(),
662 9.806_65,
663 );
664 }
665
666 #[test]
667 fn energy_helpers_cover_basic_cases() {
668 assert_eq!(spring_potential_energy(100.0, 0.5), Some(12.5));
669 assert_eq!(spring_potential_energy(-100.0, 0.5), None);
670
671 assert_eq!(oscillator_total_energy(100.0, 0.5), Some(12.5));
672 assert_eq!(oscillator_total_energy(100.0, -0.5), None);
673
674 assert_eq!(
675 kinetic_energy_from_total_and_potential(12.5, 2.5),
676 Some(10.0)
677 );
678 assert_eq!(kinetic_energy_from_total_and_potential(2.5, 12.5), None);
679 }
680
681 #[test]
682 fn damping_helpers_cover_basic_cases() {
683 assert_eq!(critical_damping_coefficient(2.0, 8.0), Some(8.0));
684 assert_eq!(critical_damping_coefficient(-2.0, 8.0), None);
685
686 assert_eq!(damping_ratio(4.0, 2.0, 8.0), Some(0.5));
687 assert_eq!(damping_ratio(-4.0, 2.0, 8.0), None);
688
689 assert_approx_eq(damped_angular_frequency(10.0, 0.6).unwrap(), 8.0);
690 assert_eq!(damped_angular_frequency(10.0, 1.0), None);
691
692 assert!(is_underdamped(0.5));
693 assert!(!is_underdamped(1.0));
694 assert!(is_overdamped(2.0));
695 assert!(!is_overdamped(1.0));
696
697 assert_eq!(is_critically_damped(1.0, 0.0), Some(true));
698 assert_eq!(is_critically_damped(1.01, 0.02), Some(true));
699 assert_eq!(is_critically_damped(1.1, 0.02), Some(false));
700 assert_eq!(is_critically_damped(1.0, -1.0), None);
701 }
702
703 #[test]
704 fn resonance_helpers_cover_basic_cases() {
705 assert_eq!(quality_factor_from_damping_ratio(0.25), Some(2.0));
706 assert_eq!(quality_factor_from_damping_ratio(0.0), None);
707
708 assert_eq!(damping_ratio_from_quality_factor(2.0), Some(0.25));
709 assert_eq!(damping_ratio_from_quality_factor(0.0), None);
710
711 assert_eq!(resonance_angular_frequency_natural(8.0, 2.0), Some(2.0));
712 }
713
714 #[test]
715 fn simple_harmonic_oscillator_type_delegates_to_helpers() {
716 let oscillator = SimpleHarmonicOscillator::new(2.0, 1.0, 0.0).unwrap();
717
718 assert_approx_eq(oscillator.displacement(0.0).unwrap(), 2.0);
719 assert_approx_eq(oscillator.velocity(0.0).unwrap(), 0.0);
720 assert_approx_eq(oscillator.acceleration(0.0).unwrap(), -2.0);
721 assert_approx_eq(
722 SimpleHarmonicOscillator::new(2.0, TAU, 0.0)
723 .unwrap()
724 .period()
725 .unwrap(),
726 1.0,
727 );
728 assert_eq!(SimpleHarmonicOscillator::new(-2.0, 1.0, 0.0), None);
729 }
730
731 #[test]
732 fn spring_oscillator_type_delegates_to_helpers() {
733 let oscillator = SpringOscillator::new(8.0, 2.0).unwrap();
734
735 assert_eq!(oscillator.angular_frequency(), Some(2.0));
736 assert_approx_eq(oscillator.period().unwrap(), PI);
737 assert_eq!(oscillator.total_energy(0.5), Some(1.0));
738 assert_eq!(SpringOscillator::new(8.0, 0.0), None);
739 }
740
741 #[test]
742 fn helpers_reject_non_finite_inputs() {
743 assert_eq!(period_from_frequency(f64::NAN), None);
744 assert_eq!(displacement(1.0, 1.0, f64::INFINITY, 0.0), None);
745 assert_eq!(spring_period(8.0, f64::NAN), None);
746 assert_eq!(damping_ratio(4.0, 2.0, f64::INFINITY), None);
747 assert_eq!(SimpleHarmonicOscillator::new(1.0, 1.0, f64::NAN), None);
748 assert_eq!(SpringOscillator::new(f64::INFINITY, 1.0), None);
749 }
750}