1use std::f64::consts::PI;
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct MassSpringSystem {
19 pub mass_kg: f64,
21 pub stiffness_nm: f64,
23 pub damping_ns_m: f64,
25}
26
27impl MassSpringSystem {
28 pub fn new(mass_kg: f64, stiffness_nm: f64) -> Self {
30 Self {
31 mass_kg,
32 stiffness_nm,
33 damping_ns_m: 0.0,
34 }
35 }
36
37 pub fn with_damping(self, damping_ns_m: f64) -> Self {
39 Self {
40 damping_ns_m,
41 ..self
42 }
43 }
44
45 pub fn is_underdamped(&self) -> bool {
47 VibrationAnalyzer::damping_ratio(self) < 1.0
48 }
49
50 pub fn is_overdamped(&self) -> bool {
52 VibrationAnalyzer::damping_ratio(self) > 1.0
53 }
54
55 pub fn is_critically_damped(&self) -> bool {
57 (VibrationAnalyzer::damping_ratio(self) - 1.0).abs() < 1e-9
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct VibrationMode {
64 pub frequency_hz: f64,
66 pub damping_ratio: f64,
68 pub mode_shape: Vec<f64>,
70}
71
72#[derive(Debug, Clone)]
74pub struct FrequencyResponse {
75 pub frequencies_hz: Vec<f64>,
77 pub amplitudes: Vec<f64>,
79 pub phase_rad: Vec<f64>,
81}
82
83impl FrequencyResponse {
84 pub fn len(&self) -> usize {
86 self.frequencies_hz.len()
87 }
88
89 pub fn is_empty(&self) -> bool {
91 self.frequencies_hz.is_empty()
92 }
93}
94
95pub struct VibrationAnalyzer;
97
98impl VibrationAnalyzer {
99 pub fn natural_frequency(system: &MassSpringSystem) -> f64 {
103 (1.0 / (2.0 * PI)) * (system.stiffness_nm / system.mass_kg).sqrt()
104 }
105
106 pub fn damping_ratio(system: &MassSpringSystem) -> f64 {
110 let critical = Self::critical_damping(system);
111 if critical == 0.0 {
112 return 0.0;
113 }
114 system.damping_ns_m / critical
115 }
116
117 pub fn damped_frequency(system: &MassSpringSystem) -> f64 {
121 let zeta = Self::damping_ratio(system);
122 let fn_hz = Self::natural_frequency(system);
123 if zeta >= 1.0 {
124 return 0.0; }
126 fn_hz * (1.0 - zeta * zeta).sqrt()
127 }
128
129 pub fn free_vibration(system: &MassSpringSystem, amplitude: f64, t: f64) -> f64 {
135 let zeta = Self::damping_ratio(system);
136 let omega_n = 2.0 * PI * Self::natural_frequency(system);
137 let omega_d = 2.0 * PI * Self::damped_frequency(system);
138
139 if zeta >= 1.0 {
140 amplitude * (-zeta * omega_n * t).exp()
142 } else {
143 amplitude * (-zeta * omega_n * t).exp() * (omega_d * t).cos()
144 }
145 }
146
147 pub fn harmonic_response_amplitude(
152 system: &MassSpringSystem,
153 force_n: f64,
154 forcing_freq_hz: f64,
155 ) -> f64 {
156 let omega_n_hz = Self::natural_frequency(system);
157 let zeta = Self::damping_ratio(system);
158 let r = forcing_freq_hz / omega_n_hz;
159
160 let static_deflection = force_n / system.stiffness_nm;
161 let denom = ((1.0 - r * r) * (1.0 - r * r) + (2.0 * zeta * r) * (2.0 * zeta * r)).sqrt();
162
163 if denom == 0.0 {
164 return f64::INFINITY;
165 }
166 static_deflection / denom
167 }
168
169 pub fn harmonic_response_phase(system: &MassSpringSystem, forcing_freq_hz: f64) -> f64 {
173 let omega_n_hz = Self::natural_frequency(system);
174 let zeta = Self::damping_ratio(system);
175 let r = forcing_freq_hz / omega_n_hz;
176 (2.0 * zeta * r).atan2(1.0 - r * r)
177 }
178
179 pub fn is_resonant(system: &MassSpringSystem, forcing_freq_hz: f64, tolerance: f64) -> bool {
183 let fn_hz = Self::natural_frequency(system);
184 if fn_hz == 0.0 {
185 return false;
186 }
187 ((forcing_freq_hz - fn_hz) / fn_hz).abs() < tolerance
188 }
189
190 pub fn log_decrement(damping_ratio: f64) -> f64 {
194 if damping_ratio >= 1.0 {
195 return f64::INFINITY;
196 }
197 2.0 * PI * damping_ratio / (1.0 - damping_ratio * damping_ratio).sqrt()
198 }
199
200 pub fn critical_damping(system: &MassSpringSystem) -> f64 {
204 2.0 * (system.stiffness_nm * system.mass_kg).sqrt()
205 }
206
207 pub fn frequency_response(
211 system: &MassSpringSystem,
212 force_n: f64,
213 freq_min: f64,
214 freq_max: f64,
215 n_points: usize,
216 ) -> FrequencyResponse {
217 if n_points == 0 {
218 return FrequencyResponse {
219 frequencies_hz: vec![],
220 amplitudes: vec![],
221 phase_rad: vec![],
222 };
223 }
224
225 let mut frequencies_hz = Vec::with_capacity(n_points);
226 let mut amplitudes = Vec::with_capacity(n_points);
227 let mut phase_rad = Vec::with_capacity(n_points);
228
229 let log_min = freq_min.ln();
230 let log_max = freq_max.ln();
231
232 for i in 0..n_points {
233 let freq = if n_points == 1 {
234 freq_min
235 } else {
236 (log_min + (log_max - log_min) * i as f64 / (n_points - 1) as f64).exp()
237 };
238
239 let amp = Self::harmonic_response_amplitude(system, force_n, freq);
240 let phase = Self::harmonic_response_phase(system, freq);
241
242 frequencies_hz.push(freq);
243 amplitudes.push(amp);
244 phase_rad.push(phase);
245 }
246
247 FrequencyResponse {
248 frequencies_hz,
249 amplitudes,
250 phase_rad,
251 }
252 }
253
254 pub fn damping_ratio_from_log_decrement(log_dec: f64) -> f64 {
258 log_dec / (4.0 * PI * PI + log_dec * log_dec).sqrt()
259 }
260
261 pub fn dynamic_magnification_factor(r: f64, zeta: f64) -> f64 {
263 let denom = ((1.0 - r * r) * (1.0 - r * r) + (2.0 * zeta * r) * (2.0 * zeta * r)).sqrt();
264 if denom == 0.0 {
265 return f64::INFINITY;
266 }
267 1.0 / denom
268 }
269
270 pub fn peak_frequency_ratio(zeta: f64) -> Option<f64> {
274 let arg = 1.0 - 2.0 * zeta * zeta;
275 if arg <= 0.0 {
276 None
277 } else {
278 Some(arg.sqrt())
279 }
280 }
281
282 pub fn half_power_bandwidth(system: &MassSpringSystem) -> f64 {
286 let zeta = Self::damping_ratio(system);
287 let fn_hz = Self::natural_frequency(system);
288 2.0 * zeta * fn_hz
289 }
290
291 pub fn quality_factor(zeta: f64) -> f64 {
293 if zeta == 0.0 {
294 return f64::INFINITY;
295 }
296 1.0 / (2.0 * zeta)
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 fn assert_approx(actual: f64, expected: f64, tol: f64, msg: &str) {
307 let err = (actual - expected).abs();
308 assert!(
309 err < tol,
310 "{msg}: got {actual}, expected {expected}, err {err}"
311 );
312 }
313
314 fn simple_system() -> MassSpringSystem {
316 MassSpringSystem::new(1.0, 100.0)
317 }
318
319 fn damped_system() -> MassSpringSystem {
321 MassSpringSystem::new(1.0, 100.0).with_damping(4.0)
322 }
323
324 #[test]
327 fn test_natural_frequency_formula() {
328 let sys = simple_system();
329 let expected = (1.0 / (2.0 * PI)) * (100.0_f64).sqrt();
330 assert_approx(
331 VibrationAnalyzer::natural_frequency(&sys),
332 expected,
333 1e-10,
334 "natural frequency",
335 );
336 }
337
338 #[test]
339 fn test_natural_frequency_unit_mass_unit_stiffness() {
340 let sys = MassSpringSystem::new(1.0, 1.0);
341 let expected = 1.0 / (2.0 * PI);
342 assert_approx(
343 VibrationAnalyzer::natural_frequency(&sys),
344 expected,
345 1e-10,
346 "f_n for k=1, m=1",
347 );
348 }
349
350 #[test]
351 fn test_natural_frequency_scales_with_stiffness() {
352 let sys1 = MassSpringSystem::new(1.0, 100.0);
353 let sys2 = MassSpringSystem::new(1.0, 400.0);
354 let ratio = VibrationAnalyzer::natural_frequency(&sys2)
356 / VibrationAnalyzer::natural_frequency(&sys1);
357 assert_approx(ratio, 2.0, 1e-10, "frequency ratio sqrt(4)=2");
358 }
359
360 #[test]
361 fn test_natural_frequency_scales_inverse_with_mass() {
362 let sys1 = MassSpringSystem::new(1.0, 100.0);
363 let sys2 = MassSpringSystem::new(4.0, 100.0);
364 let ratio = VibrationAnalyzer::natural_frequency(&sys1)
365 / VibrationAnalyzer::natural_frequency(&sys2);
366 assert_approx(ratio, 2.0, 1e-10, "frequency ratio 1/sqrt(m)");
367 }
368
369 #[test]
372 fn test_damping_ratio_undamped() {
373 let sys = simple_system();
374 assert_approx(
375 VibrationAnalyzer::damping_ratio(&sys),
376 0.0,
377 1e-15,
378 "undamped",
379 );
380 }
381
382 #[test]
383 fn test_damping_ratio_twenty_percent() {
384 let sys = damped_system(); assert_approx(
386 VibrationAnalyzer::damping_ratio(&sys),
387 0.2,
388 1e-10,
389 "20% damping",
390 );
391 }
392
393 #[test]
394 fn test_damping_ratio_critical() {
395 let sys = MassSpringSystem::new(1.0, 100.0).with_damping(20.0);
397 assert_approx(
398 VibrationAnalyzer::damping_ratio(&sys),
399 1.0,
400 1e-10,
401 "critical damping ratio",
402 );
403 }
404
405 #[test]
406 fn test_damping_ratio_formula_manual() {
407 let sys = MassSpringSystem::new(2.0, 200.0).with_damping(5.0);
408 assert_approx(
411 VibrationAnalyzer::damping_ratio(&sys),
412 0.125,
413 1e-10,
414 "manual formula check",
415 );
416 }
417
418 #[test]
421 fn test_damped_frequency_less_than_natural() {
422 let sys = damped_system();
423 let fn_hz = VibrationAnalyzer::natural_frequency(&sys);
424 let fd_hz = VibrationAnalyzer::damped_frequency(&sys);
425 assert!(
426 fd_hz < fn_hz,
427 "damped freq {fd_hz} must be < natural {fn_hz}"
428 );
429 }
430
431 #[test]
432 fn test_damped_frequency_undamped_equals_natural() {
433 let sys = simple_system();
434 let fn_hz = VibrationAnalyzer::natural_frequency(&sys);
435 let fd_hz = VibrationAnalyzer::damped_frequency(&sys);
436 assert_approx(fd_hz, fn_hz, 1e-10, "undamped: f_d == f_n");
437 }
438
439 #[test]
440 fn test_damped_frequency_formula() {
441 let sys = damped_system(); let fn_hz = VibrationAnalyzer::natural_frequency(&sys);
443 let expected = fn_hz * (1.0 - 0.04_f64).sqrt();
444 assert_approx(
445 VibrationAnalyzer::damped_frequency(&sys),
446 expected,
447 1e-10,
448 "f_d formula",
449 );
450 }
451
452 #[test]
453 fn test_damped_frequency_overdamped_is_zero() {
454 let sys = MassSpringSystem::new(1.0, 100.0).with_damping(30.0); assert_approx(
456 VibrationAnalyzer::damped_frequency(&sys),
457 0.0,
458 1e-15,
459 "overdamped f_d = 0",
460 );
461 }
462
463 #[test]
466 fn test_free_vibration_at_t0_equals_amplitude() {
467 let sys = damped_system();
468 let amp = 0.05; let x0 = VibrationAnalyzer::free_vibration(&sys, amp, 0.0);
470 assert_approx(x0, amp, 1e-10, "x(0) = amplitude");
471 }
472
473 #[test]
474 fn test_free_vibration_decays() {
475 let sys = damped_system();
476 let amp = 1.0;
477 let x0 = VibrationAnalyzer::free_vibration(&sys, amp, 0.0).abs();
478 let x1 = VibrationAnalyzer::free_vibration(&sys, amp, 0.5).abs();
479 let x2 = VibrationAnalyzer::free_vibration(&sys, amp, 1.0).abs();
480 assert!(x2 < x0, "vibration should decay: x(1) {x2} < x(0) {x0}");
483 let _ = x1; }
485
486 #[test]
487 fn test_free_vibration_undamped_at_t0() {
488 let sys = simple_system();
489 let x0 = VibrationAnalyzer::free_vibration(&sys, 2.0, 0.0);
490 assert_approx(x0, 2.0, 1e-10, "undamped x(0)=2");
491 }
492
493 #[test]
494 fn test_free_vibration_negative_amplitude() {
495 let sys = damped_system();
496 let x0 = VibrationAnalyzer::free_vibration(&sys, -1.0, 0.0);
497 assert_approx(x0, -1.0, 1e-10, "negative amplitude at t=0");
498 }
499
500 #[test]
503 fn test_harmonic_response_static_limit() {
504 let sys = damped_system();
506 let force = 10.0;
507 let static_def = force / sys.stiffness_nm;
508 let amp = VibrationAnalyzer::harmonic_response_amplitude(&sys, force, 0.001);
509 assert_approx(amp, static_def, 1e-4, "static limit");
510 }
511
512 #[test]
513 fn test_harmonic_response_at_resonance_large() {
514 let sys = damped_system(); let force = 10.0;
517 let fn_hz = VibrationAnalyzer::natural_frequency(&sys);
518 let amp_at_resonance = VibrationAnalyzer::harmonic_response_amplitude(&sys, force, fn_hz);
519 let static_def = force / sys.stiffness_nm;
520 let expected = static_def * 2.5;
522 assert_approx(amp_at_resonance, expected, 1e-10, "resonance amplitude");
523 }
524
525 #[test]
526 fn test_harmonic_response_amplitude_positive() {
527 let sys = damped_system();
528 let amp = VibrationAnalyzer::harmonic_response_amplitude(&sys, 5.0, 2.0);
529 assert!(amp > 0.0, "amplitude must be positive");
530 }
531
532 #[test]
533 fn test_harmonic_response_high_frequency_decreases() {
534 let sys = damped_system();
536 let amp_low = VibrationAnalyzer::harmonic_response_amplitude(&sys, 1.0, 0.01);
537 let amp_high = VibrationAnalyzer::harmonic_response_amplitude(&sys, 1.0, 1000.0);
538 assert!(
539 amp_high < amp_low,
540 "high-freq amplitude {amp_high} < low-freq {amp_low}"
541 );
542 }
543
544 #[test]
547 fn test_is_resonant_true_at_natural_freq() {
548 let sys = simple_system();
549 let fn_hz = VibrationAnalyzer::natural_frequency(&sys);
550 assert!(VibrationAnalyzer::is_resonant(&sys, fn_hz, 0.01));
551 }
552
553 #[test]
554 fn test_is_resonant_false_far_from_natural() {
555 let sys = simple_system();
556 let fn_hz = VibrationAnalyzer::natural_frequency(&sys);
557 assert!(!VibrationAnalyzer::is_resonant(&sys, fn_hz * 2.0, 0.01));
558 }
559
560 #[test]
561 fn test_is_resonant_within_tolerance() {
562 let sys = simple_system();
563 let fn_hz = VibrationAnalyzer::natural_frequency(&sys);
564 let forcing = fn_hz * 1.04;
566 assert!(VibrationAnalyzer::is_resonant(&sys, forcing, 0.05));
567 }
568
569 #[test]
570 fn test_is_resonant_outside_tolerance() {
571 let sys = simple_system();
572 let fn_hz = VibrationAnalyzer::natural_frequency(&sys);
573 let forcing = fn_hz * 1.2; assert!(!VibrationAnalyzer::is_resonant(&sys, forcing, 0.05));
575 }
576
577 #[test]
580 fn test_log_decrement_undamped_is_zero() {
581 assert_approx(VibrationAnalyzer::log_decrement(0.0), 0.0, 1e-15, "δ(0)=0");
582 }
583
584 #[test]
585 fn test_log_decrement_formula() {
586 let zeta = 0.1;
587 let expected = 2.0 * PI * zeta / (1.0 - zeta * zeta).sqrt();
588 assert_approx(
589 VibrationAnalyzer::log_decrement(zeta),
590 expected,
591 1e-10,
592 "log decrement formula",
593 );
594 }
595
596 #[test]
597 fn test_log_decrement_increases_with_damping() {
598 let d1 = VibrationAnalyzer::log_decrement(0.1);
599 let d2 = VibrationAnalyzer::log_decrement(0.3);
600 assert!(d2 > d1, "higher damping → larger log decrement");
601 }
602
603 #[test]
604 fn test_log_decrement_round_trip() {
605 let zeta = 0.15;
606 let delta = VibrationAnalyzer::log_decrement(zeta);
607 let zeta_back = VibrationAnalyzer::damping_ratio_from_log_decrement(delta);
608 assert_approx(zeta_back, zeta, 1e-10, "round trip zeta→δ→zeta");
609 }
610
611 #[test]
614 fn test_critical_damping_formula() {
615 let sys = simple_system(); let expected = 2.0 * (100.0_f64).sqrt(); assert_approx(
618 VibrationAnalyzer::critical_damping(&sys),
619 expected,
620 1e-10,
621 "c_crit formula",
622 );
623 }
624
625 #[test]
626 fn test_critical_damping_equals_20_for_simple_system() {
627 let sys = simple_system();
628 assert_approx(
629 VibrationAnalyzer::critical_damping(&sys),
630 20.0,
631 1e-10,
632 "c_crit = 20",
633 );
634 }
635
636 #[test]
637 fn test_critical_damping_scales_with_stiffness() {
638 let sys1 = MassSpringSystem::new(1.0, 100.0);
639 let sys2 = MassSpringSystem::new(1.0, 400.0);
640 let ratio =
641 VibrationAnalyzer::critical_damping(&sys2) / VibrationAnalyzer::critical_damping(&sys1);
642 assert_approx(ratio, 2.0, 1e-10, "c_crit scales sqrt(k)");
643 }
644
645 #[test]
648 fn test_frequency_response_n_points_length() {
649 let sys = damped_system();
650 let frf = VibrationAnalyzer::frequency_response(&sys, 1.0, 0.1, 100.0, 50);
651 assert_eq!(frf.len(), 50, "FRF must have 50 points");
652 assert_eq!(frf.frequencies_hz.len(), 50);
653 assert_eq!(frf.amplitudes.len(), 50);
654 assert_eq!(frf.phase_rad.len(), 50);
655 }
656
657 #[test]
658 fn test_frequency_response_zero_points() {
659 let sys = damped_system();
660 let frf = VibrationAnalyzer::frequency_response(&sys, 1.0, 0.1, 100.0, 0);
661 assert!(frf.is_empty());
662 }
663
664 #[test]
665 fn test_frequency_response_single_point() {
666 let sys = damped_system();
667 let frf = VibrationAnalyzer::frequency_response(&sys, 1.0, 1.0, 1.0, 1);
668 assert_eq!(frf.len(), 1);
669 assert_approx(frf.frequencies_hz[0], 1.0, 1e-10, "single point freq");
670 }
671
672 #[test]
673 fn test_frequency_response_amplitudes_positive() {
674 let sys = damped_system();
675 let frf = VibrationAnalyzer::frequency_response(&sys, 10.0, 0.1, 10.0, 20);
676 for amp in &frf.amplitudes {
677 assert!(*amp > 0.0, "amplitude must be positive");
678 }
679 }
680
681 #[test]
682 fn test_frequency_response_covers_range() {
683 let sys = damped_system();
684 let frf = VibrationAnalyzer::frequency_response(&sys, 1.0, 1.0, 100.0, 10);
685 assert_approx(frf.frequencies_hz[0], 1.0, 1e-6, "first frequency");
686 assert_approx(frf.frequencies_hz[9], 100.0, 1e-4, "last frequency");
687 }
688
689 #[test]
692 fn test_is_underdamped() {
693 let sys = damped_system(); assert!(sys.is_underdamped());
695 assert!(!sys.is_overdamped());
696 assert!(!sys.is_critically_damped());
697 }
698
699 #[test]
700 fn test_is_overdamped() {
701 let sys = MassSpringSystem::new(1.0, 100.0).with_damping(30.0); assert!(sys.is_overdamped());
703 assert!(!sys.is_underdamped());
704 assert!(!sys.is_critically_damped());
705 }
706
707 #[test]
708 fn test_is_critically_damped() {
709 let sys = MassSpringSystem::new(1.0, 100.0).with_damping(20.0); assert!(sys.is_critically_damped());
711 assert!(!sys.is_underdamped());
712 assert!(!sys.is_overdamped());
713 }
714
715 #[test]
716 fn test_undamped_is_underdamped() {
717 let sys = simple_system(); assert!(sys.is_underdamped());
719 }
720
721 #[test]
724 fn test_dynamic_magnification_factor_at_resonance() {
725 let zeta = 0.25;
727 let dmf = VibrationAnalyzer::dynamic_magnification_factor(1.0, zeta);
728 assert_approx(dmf, 1.0 / (2.0 * zeta), 1e-10, "DMF at r=1");
729 }
730
731 #[test]
732 fn test_quality_factor_inverse_damping() {
733 let zeta = 0.1;
734 let q = VibrationAnalyzer::quality_factor(zeta);
735 assert_approx(q, 5.0, 1e-10, "Q = 1/(2*0.1) = 5");
736 }
737
738 #[test]
739 fn test_half_power_bandwidth() {
740 let sys = damped_system(); let bw = VibrationAnalyzer::half_power_bandwidth(&sys);
742 let expected = 2.0 * 0.2 * VibrationAnalyzer::natural_frequency(&sys);
743 assert_approx(bw, expected, 1e-10, "half-power bandwidth");
744 }
745
746 #[test]
747 fn test_peak_frequency_ratio_low_damping() {
748 let zeta = 0.1;
749 let r_peak = VibrationAnalyzer::peak_frequency_ratio(zeta)
750 .expect("should have peak for low damping");
751 assert_approx(r_peak, (1.0 - 2.0 * 0.01_f64).sqrt(), 1e-10, "r_peak");
752 }
753
754 #[test]
755 fn test_peak_frequency_ratio_high_damping_none() {
756 let result = VibrationAnalyzer::peak_frequency_ratio(0.8);
758 assert!(result.is_none(), "no peak for overdamped");
759 }
760
761 #[test]
762 fn test_mass_spring_system_builder() {
763 let sys = MassSpringSystem::new(5.0, 200.0).with_damping(12.0);
764 assert_eq!(sys.mass_kg, 5.0);
765 assert_eq!(sys.stiffness_nm, 200.0);
766 assert_eq!(sys.damping_ns_m, 12.0);
767 }
768
769 #[test]
770 fn test_frequency_response_is_not_empty() {
771 let sys = damped_system();
772 let frf = VibrationAnalyzer::frequency_response(&sys, 1.0, 0.5, 10.0, 100);
773 assert!(!frf.is_empty());
774 }
775
776 #[test]
777 fn test_harmonic_response_phase_at_low_freq() {
778 let sys = damped_system();
780 let phase = VibrationAnalyzer::harmonic_response_phase(&sys, 0.001);
781 assert!(phase.abs() < 0.01, "phase near zero at low frequency");
782 }
783
784 #[test]
785 fn test_damping_ratio_from_log_decrement_roundtrip_2() {
786 let zeta_in = 0.05;
787 let delta = VibrationAnalyzer::log_decrement(zeta_in);
788 let zeta_out = VibrationAnalyzer::damping_ratio_from_log_decrement(delta);
789 assert_approx(zeta_out, zeta_in, 1e-10, "log-dec round trip 2");
790 }
791}