Skip to main content

use_oscillation/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Oscillation-specific scalar helpers.
5
6use 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/// Computes period from frequency using `T = 1 / f`.
39///
40/// Returns `None` when `frequency` is less than or equal to zero, when the input is not finite,
41/// or when the computed result is not finite.
42///
43/// # Examples
44///
45/// ```rust
46/// use use_oscillation::period_from_frequency;
47///
48/// assert_eq!(period_from_frequency(2.0), Some(0.5));
49/// assert_eq!(period_from_frequency(0.0), None);
50/// ```
51#[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/// Computes frequency from period using `f = 1 / T`.
61///
62/// Returns `None` when `period` is less than or equal to zero, when the input is not finite, or
63/// when the computed result is not finite.
64#[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/// Computes angular frequency from frequency using `ω = 2πf`.
74#[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/// Computes frequency from angular frequency using `f = ω / 2π`.
84#[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/// Computes angular frequency from period using `ω = 2π / T`.
94///
95/// # Examples
96///
97/// ```rust
98/// use core::f64::consts::TAU;
99/// use use_oscillation::angular_frequency_from_period;
100///
101/// let angular_frequency = angular_frequency_from_period(1.0).unwrap();
102///
103/// assert!((angular_frequency - TAU).abs() < 1.0e-12);
104/// ```
105#[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/// Computes period from angular frequency using `T = 2π / ω`.
115#[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/// Computes displacement for simple harmonic motion using `x(t) = A * cos(ωt + φ)`.
125///
126/// # Examples
127///
128/// ```rust
129/// use use_oscillation::displacement;
130///
131/// let value = displacement(2.0, 1.0, 0.0, 0.0).unwrap();
132///
133/// assert!((value - 2.0).abs() < 1.0e-12);
134/// ```
135#[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/// Computes velocity for simple harmonic motion using `v(t) = -Aω * sin(ωt + φ)`.
143#[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/// Computes acceleration for simple harmonic motion using `a(t) = -Aω² * cos(ωt + φ)`.
151#[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/// Computes simple-harmonic acceleration from displacement using `a = -ω²x`.
159#[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/// Computes the maximum speed using `v_max = Aω`.
169#[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/// Computes the maximum acceleration using `a_max = Aω²`.
179#[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/// Computes spring angular frequency using `ω = sqrt(k / m)`.
189#[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/// Computes spring period using `T = 2π * sqrt(m / k)`.
199///
200/// # Examples
201///
202/// ```rust
203/// use core::f64::consts::PI;
204/// use use_oscillation::spring_period;
205///
206/// let period = spring_period(8.0, 2.0).unwrap();
207///
208/// assert!((period - PI).abs() < 1.0e-12);
209/// ```
210#[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/// Computes spring frequency in cycles per second.
220#[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/// Computes spring constant from mass and period using `k = 4π²m / T²`.
228#[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/// Computes mass from spring constant and period using `m = kT² / 4π²`.
238#[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/// Computes the small-angle simple pendulum period using `T = 2π * sqrt(L / g)`.
248///
249/// # Examples
250///
251/// ```rust
252/// use core::f64::consts::TAU;
253/// use use_oscillation::simple_pendulum_period;
254///
255/// let period = simple_pendulum_period(9.806_65, 9.806_65).unwrap();
256///
257/// assert!((period - TAU).abs() < 1.0e-12);
258/// ```
259#[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/// Computes small-angle simple pendulum frequency in cycles per second.
269#[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/// Computes small-angle simple pendulum angular frequency using `ω = sqrt(g / L)`.
277#[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/// Computes pendulum length from period using `L = g * (T / 2π)²`.
290#[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/// Computes spring potential energy using `U = 0.5 * k * x²`.
300#[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/// Computes oscillator total energy using `E = 0.5 * k * A²`.
310#[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/// Computes kinetic energy from total energy and potential energy using `KE = E_total - U`.
320#[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/// Computes damping ratio using `ζ = c / (2 * sqrt(mk))`.
338///
339/// # Examples
340///
341/// ```rust
342/// use use_oscillation::damping_ratio;
343///
344/// assert_eq!(damping_ratio(4.0, 2.0, 8.0), Some(0.5));
345/// ```
346#[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/// Computes the critical damping coefficient using `c_critical = 2 * sqrt(mk)`.
361#[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/// Computes the damped angular frequency for an underdamped oscillator.
371#[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/// Returns `true` when the damping ratio represents an underdamped system.
389#[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/// Returns `true` when the damping ratio is within `tolerance` of critical damping.
395#[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/// Returns `true` when the damping ratio represents an overdamped system.
405#[must_use]
406pub fn is_overdamped(damping_ratio: f64) -> bool {
407    damping_ratio.is_finite() && damping_ratio > 1.0
408}
409
410/// Computes quality factor from damping ratio using `Q = 1 / (2ζ)`.
411///
412/// # Examples
413///
414/// ```rust
415/// use use_oscillation::quality_factor_from_damping_ratio;
416///
417/// assert_eq!(quality_factor_from_damping_ratio(0.25), Some(2.0));
418/// ```
419#[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/// Computes damping ratio from quality factor using `ζ = 1 / (2Q)`.
429#[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/// Computes the natural resonance angular frequency of a spring-mass oscillator.
439#[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/// A simple scalar harmonic oscillator state.
445#[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    /// Creates a simple harmonic oscillator from amplitude, angular frequency, and phase.
454    #[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    /// Computes displacement at `time`.
471    ///
472    /// # Examples
473    ///
474    /// ```rust
475    /// use use_oscillation::SimpleHarmonicOscillator;
476    ///
477    /// let oscillator = SimpleHarmonicOscillator::new(2.0, 1.0, 0.0).unwrap();
478    ///
479    /// assert!((oscillator.displacement(0.0).unwrap() - 2.0).abs() < 1.0e-12);
480    /// ```
481    #[must_use]
482    pub fn displacement(&self, time: f64) -> Option<f64> {
483        displacement(self.amplitude, self.angular_frequency, time, self.phase)
484    }
485
486    /// Computes velocity at `time`.
487    #[must_use]
488    pub fn velocity(&self, time: f64) -> Option<f64> {
489        velocity(self.amplitude, self.angular_frequency, time, self.phase)
490    }
491
492    /// Computes acceleration at `time`.
493    #[must_use]
494    pub fn acceleration(&self, time: f64) -> Option<f64> {
495        acceleration(self.amplitude, self.angular_frequency, time, self.phase)
496    }
497
498    /// Computes the oscillation period.
499    #[must_use]
500    pub fn period(&self) -> Option<f64> {
501        period_from_angular_frequency(self.angular_frequency)
502    }
503
504    /// Computes the oscillation frequency.
505    #[must_use]
506    pub fn frequency(&self) -> Option<f64> {
507        frequency_from_angular_frequency(self.angular_frequency)
508    }
509
510    /// Computes maximum speed.
511    #[must_use]
512    pub fn max_speed(&self) -> Option<f64> {
513        max_speed(self.amplitude, self.angular_frequency)
514    }
515
516    /// Computes maximum acceleration.
517    #[must_use]
518    pub fn max_acceleration(&self) -> Option<f64> {
519        max_acceleration(self.amplitude, self.angular_frequency)
520    }
521}
522
523/// A spring-mass oscillator state.
524#[derive(Debug, Clone, Copy, PartialEq)]
525pub struct SpringOscillator {
526    pub spring_constant: f64,
527    pub mass: f64,
528}
529
530impl SpringOscillator {
531    /// Creates a spring oscillator from spring constant and mass.
532    #[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    /// Computes angular frequency.
545    #[must_use]
546    pub fn angular_frequency(&self) -> Option<f64> {
547        spring_angular_frequency(self.spring_constant, self.mass)
548    }
549
550    /// Computes period.
551    ///
552    /// # Examples
553    ///
554    /// ```rust
555    /// use core::f64::consts::PI;
556    /// use use_oscillation::SpringOscillator;
557    ///
558    /// let oscillator = SpringOscillator::new(8.0, 2.0).unwrap();
559    ///
560    /// assert!((oscillator.period().unwrap() - PI).abs() < 1.0e-12);
561    /// ```
562    #[must_use]
563    pub fn period(&self) -> Option<f64> {
564        spring_period(self.spring_constant, self.mass)
565    }
566
567    /// Computes frequency.
568    #[must_use]
569    pub fn frequency(&self) -> Option<f64> {
570        spring_frequency(self.spring_constant, self.mass)
571    }
572
573    /// Computes total oscillator energy for a given amplitude.
574    #[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}