ballistics_engine/
pitch_damping.rs

1//! Pitch Damping Moment Physics for Ballistics Calculations
2//!
3//! This module implements pitch damping moments that affect:
4//! - Dynamic stability during flight
5//! - Precession and nutation damping  
6//! - Yaw of repose convergence
7//! - Transonic stability transitions
8
9use crate::constants::G_ACCEL_MPS2;
10use crate::spin_decay::calculate_moment_of_inertia;
11use std::f64::consts::PI;
12
13/// Aerodynamic damping coefficients for different flight regimes
14#[derive(Debug, Clone, Copy)]
15pub struct PitchDampingCoefficients {
16    pub subsonic: f64,       // Cmq + Cmα̇ for M < 0.8
17    pub transonic_low: f64,  // For 0.8 <= M < 1.0
18    pub transonic_high: f64, // For 1.0 <= M < 1.2 (can be destabilizing)
19    pub supersonic: f64,     // For M >= 1.2
20}
21
22impl Default for PitchDampingCoefficients {
23    fn default() -> Self {
24        // These values use the q * S * d * (pitch_rate * d / V) convention in
25        // `calculate_pitch_damping_moment`. Published small-arms coefficients are order 1-10;
26        // a representative 7.62 mm pitch-damping coefficient is about -4.7.
27        Self {
28            subsonic: -8.0,
29            transonic_low: -3.0,
30            transonic_high: 2.0,
31            supersonic: -5.0,
32        }
33    }
34}
35
36impl PitchDampingCoefficients {
37    /// Get typical coefficients for different bullet types
38    pub fn from_bullet_type(bullet_type: &str) -> Self {
39        match bullet_type.to_lowercase().as_str() {
40            "match_boat_tail" => Self {
41                subsonic: -9.0,
42                transonic_low: -4.0,
43                transonic_high: 1.0,
44                supersonic: -6.0,
45            },
46            "match_flat_base" => Self {
47                subsonic: -7.0,
48                transonic_low: -2.0,
49                transonic_high: 3.0,
50                supersonic: -4.0,
51            },
52            "vld" => Self {
53                // Very Low Drag - more stable
54                subsonic: -10.0,
55                transonic_low: -5.0,
56                transonic_high: -1.0,
57                supersonic: -7.0,
58            },
59            "hunting" => Self {
60                subsonic: -6.0,
61                transonic_low: -1.0,
62                transonic_high: 4.0,
63                supersonic: -3.0,
64            },
65            "fmj" => Self {
66                subsonic: -7.0,
67                transonic_low: -2.0,
68                transonic_high: 2.0,
69                supersonic: -5.0,
70            },
71            _ => Self::default(),
72        }
73    }
74}
75
76/// Calculate pitch damping coefficient based on Mach number
77pub fn calculate_pitch_damping_coefficient(mach: f64, coeffs: &PitchDampingCoefficients) -> f64 {
78    if mach < 0.8 {
79        // Subsonic - stable damping
80        coeffs.subsonic
81    } else if mach < 1.0 {
82        // Lower transonic - decreasing stability
83        // Linear interpolation
84        let t = (mach - 0.8) / 0.2;
85        coeffs.subsonic * (1.0 - t) + coeffs.transonic_low * t
86    } else if mach < 1.2 {
87        // Upper transonic - potentially destabilizing
88        // This is where "transonic jump" occurs
89        let t = (mach - 1.0) / 0.2;
90        coeffs.transonic_low * (1.0 - t) + coeffs.transonic_high * t
91    } else {
92        // Supersonic - returns to stable
93        // Asymptotic approach to supersonic value
94        let t = ((mach - 1.2) / 0.8).min(1.0);
95        coeffs.transonic_high * (1.0 - t) + coeffs.supersonic * t
96    }
97}
98
99/// Calculate the aerodynamic moment opposing pitch motion
100pub fn calculate_pitch_damping_moment(
101    pitch_rate_rad_s: f64,
102    velocity_mps: f64,
103    air_density_kg_m3: f64,
104    caliber_m: f64,
105    _length_m: f64,
106    mach: f64,
107    coeffs: &PitchDampingCoefficients,
108) -> f64 {
109    if velocity_mps == 0.0 || pitch_rate_rad_s == 0.0 {
110        return 0.0;
111    }
112
113    // Get damping coefficient for current Mach
114    let cmq = calculate_pitch_damping_coefficient(mach, coeffs);
115
116    // Dynamic pressure
117    let q = 0.5 * air_density_kg_m3 * velocity_mps.powi(2);
118
119    // Reference area (cross-sectional)
120    let s = PI * (caliber_m / 2.0).powi(2);
121
122    // Reference length (use diameter for missiles/projectiles)
123    let d = caliber_m;
124
125    // Non-dimensional pitch rate
126    let q_nondim = pitch_rate_rad_s * d / velocity_mps;
127
128    // Pitch damping moment
129    // Negative because it opposes motion
130    q * s * d * cmq * q_nondim
131}
132
133/// Calculate moment of inertia about transverse axis (pitch/yaw)
134pub fn calculate_transverse_moment_of_inertia(
135    mass_kg: f64,
136    caliber_m: f64,
137    length_m: f64,
138    shape: &str,
139) -> f64 {
140    let radius = caliber_m / 2.0;
141
142    match shape {
143        "cylinder" => {
144            // I_transverse = m * (3*r² + L²) / 12
145            mass_kg * (3.0 * radius.powi(2) + length_m.powi(2)) / 12.0
146        }
147        "ogive" => {
148            // Ogive has more mass toward the front
149            // Approximate as 85% of cylinder value
150            let cylinder_i = mass_kg * (3.0 * radius.powi(2) + length_m.powi(2)) / 12.0;
151            0.85 * cylinder_i
152        }
153        "boat_tail" => {
154            // Boat tail has less mass at rear
155            // Approximate as 80% of cylinder value
156            let cylinder_i = mass_kg * (3.0 * radius.powi(2) + length_m.powi(2)) / 12.0;
157            0.80 * cylinder_i
158        }
159        _ => {
160            // Default to cylinder
161            mass_kg * (3.0 * radius.powi(2) + length_m.powi(2)) / 12.0
162        }
163    }
164}
165
166/// Calculate angular acceleration from moment and inertia
167pub fn calculate_angular_acceleration(moment: f64, moment_of_inertia: f64) -> f64 {
168    if moment_of_inertia > 0.0 {
169        moment / moment_of_inertia
170    } else {
171        0.0
172    }
173}
174
175/// First-order yaw-of-repose magnitude for a flat-fire trajectory.
176///
177/// Following AMCP 706-238 section 4-11.2, the classical relations
178/// `Sg = (Ix * p)^2 / (4 * Iy * M)` and
179/// `yaw = Ix * p * g / (M * V)` eliminate the unavailable static-moment slope `M`, leaving
180/// `yaw = 4 * Iy * Sg * g / (Ix * |p| * V)`. The API has no flight-path angle, so this uses
181/// the flat-fire approximation `cos(theta) = 1`. Twist direction is applied downstream.
182pub(crate) fn calculate_gravity_yaw_of_repose(
183    stability_factor: f64,
184    velocity_mps: f64,
185    spin_rate_rad_s: f64,
186    mass_kg: f64,
187    caliber_m: f64,
188    length_m: f64,
189) -> f64 {
190    if !stability_factor.is_finite()
191        || stability_factor <= 1.0
192        || !velocity_mps.is_finite()
193        || velocity_mps <= 0.0
194        || !spin_rate_rad_s.is_finite()
195        || spin_rate_rad_s == 0.0
196        || !mass_kg.is_finite()
197        || mass_kg <= 0.0
198        || !caliber_m.is_finite()
199        || caliber_m <= 0.0
200        || !length_m.is_finite()
201        || length_m <= 0.0
202    {
203        return 0.0;
204    }
205
206    let axial_inertia = calculate_moment_of_inertia(mass_kg, caliber_m, length_m, "ogive");
207    let transverse_inertia =
208        calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "ogive");
209    if axial_inertia <= 0.0 || transverse_inertia <= 0.0 {
210        return 0.0;
211    }
212
213    4.0 * transverse_inertia * stability_factor * G_ACCEL_MPS2
214        / (axial_inertia * spin_rate_rad_s.abs() * velocity_mps)
215}
216
217/// Calculate yaw of repose with pitch damping effects.
218///
219/// Returns the equilibrium yaw and a signed convergence rate in `s^-1`: positive values
220/// converge toward equilibrium, while negative values identify a divergent pitch mode.
221#[allow(clippy::too_many_arguments)] // Public compatibility API; grouping would be breaking.
222pub fn calculate_damped_yaw_of_repose(
223    stability_factor: f64,
224    velocity_mps: f64,
225    spin_rate_rad_s: f64,
226    _wind_velocity_mps: f64,
227    pitch_rate_rad_s: f64,
228    air_density_kg_m3: f64,
229    caliber_inches: f64,
230    length_inches: f64,
231    mass_grains: f64,
232    mach: f64,
233    bullet_type: &str,
234) -> (f64, f64) {
235    if stability_factor <= 1.0 || spin_rate_rad_s == 0.0 {
236        return (0.0, 0.0);
237    }
238
239    // Convert units
240    let caliber_m = caliber_inches * 0.0254;
241    let length_m = length_inches * 0.0254;
242    let mass_kg = mass_grains * crate::constants::GRAINS_TO_KG;
243
244    // Crosswind creates an initial transient handled by aerodynamic-jump physics; it is not part
245    // of the persistent equilibrium yaw. Use the gravity/gyroscopic balance for repose instead.
246    let equilibrium_yaw_rad = calculate_gravity_yaw_of_repose(
247        stability_factor,
248        velocity_mps,
249        spin_rate_rad_s,
250        mass_kg,
251        caliber_m,
252        length_m,
253    );
254
255    // Get damping coefficients
256    let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
257
258    // Calculate pitch damping moment
259    let damping_moment = calculate_pitch_damping_moment(
260        pitch_rate_rad_s,
261        velocity_mps,
262        air_density_kg_m3,
263        caliber_m,
264        length_m,
265        mach,
266        &coeffs,
267    );
268
269    // Calculate transverse moment of inertia
270    let i_transverse =
271        calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "ogive");
272
273    // Angular acceleration from damping
274    let angular_accel = calculate_angular_acceleration(damping_moment, i_transverse);
275
276    // For q_dot = lambda*q, the convergence rate is -lambda: positive for damping and
277    // negative for a destabilizing moment. Preserve the legacy zero-signal fallback.
278    let convergence_rate = if angular_accel != 0.0 && pitch_rate_rad_s != 0.0 {
279        -angular_accel / pitch_rate_rad_s
280    } else {
281        0.1
282    };
283
284    (equilibrium_yaw_rad, convergence_rate)
285}
286
287/// Legacy alias for the slow-mode precession angular frequency in radians per second.
288///
289/// Pitch damping changes modal amplitude/convergence, not the slow-mode phase frequency. The
290/// removed yaw, velocity, and damping-moment arguments could not form a dimensionally valid
291/// correction. Use [`crate::precession_nutation::calculate_precession_frequency`] directly.
292#[deprecated(
293    since = "0.22.18",
294    note = "use precession_nutation::calculate_precession_frequency; damping changes modal amplitude, not phase frequency"
295)]
296pub fn calculate_precession_with_damping(
297    spin_rate_rad_s: f64,
298    spin_inertia: f64,
299    transverse_inertia: f64,
300    stability_factor: f64,
301) -> f64 {
302    crate::precession_nutation::calculate_precession_frequency(
303        spin_rate_rad_s,
304        spin_inertia,
305        transverse_inertia,
306        stability_factor,
307    )
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn test_pitch_damping_coefficient() {
316        let coeffs = PitchDampingCoefficients::default();
317
318        // Subsonic
319        assert_eq!(calculate_pitch_damping_coefficient(0.5, &coeffs), -8.0);
320
321        // Transonic
322        let transonic = calculate_pitch_damping_coefficient(0.9, &coeffs);
323        assert!(transonic > -8.0 && transonic < -3.0);
324
325        // Supersonic
326        let supersonic = calculate_pitch_damping_coefficient(2.0, &coeffs);
327        assert_eq!(supersonic, -5.0);
328    }
329
330    #[test]
331    fn test_pitch_damping_moment() {
332        let coeffs = PitchDampingCoefficients::default();
333        let moment = calculate_pitch_damping_moment(
334            0.1,     // pitch rate
335            300.0,   // velocity
336            1.225,   // air density
337            0.00782, // caliber (7.82mm)
338            0.033,   // length (33mm)
339            0.87,    // Mach
340            &coeffs,
341        );
342
343        // Should be negative (opposing motion)
344        assert!(moment < 0.0);
345    }
346
347    #[test]
348    fn default_308_pitch_rate_damps_on_small_arms_timescale() {
349        let pitch_rate = 0.1;
350        let velocity = 850.0;
351        let density = 1.225;
352        let caliber = 0.308 * 0.0254;
353        let length = 1.3 * 0.0254;
354        let mass = 175.0 * crate::constants::GRAINS_TO_KG;
355        let mach = velocity / 343.0;
356        let coeffs = PitchDampingCoefficients::default();
357
358        let moment = calculate_pitch_damping_moment(
359            pitch_rate, velocity, density, caliber, length, mach, &coeffs,
360        );
361        let inertia = calculate_transverse_moment_of_inertia(mass, caliber, length, "ogive");
362        let angular_accel = calculate_angular_acceleration(moment, inertia);
363        let time_constant = (pitch_rate / angular_accel).abs();
364
365        assert!(
366            (0.05..=0.25).contains(&time_constant),
367            ".308 pitch damping should settle on a small-arms timescale, got tau={time_constant}s"
368        );
369    }
370
371    #[test]
372    fn test_bullet_type_coefficients() {
373        let types = [
374            "match_boat_tail",
375            "match_flat_base",
376            "vld",
377            "hunting",
378            "fmj",
379            "unknown",
380        ];
381
382        for bullet_type in &types {
383            let coeffs = PitchDampingCoefficients::from_bullet_type(bullet_type);
384
385            // Check that subsonic is always stabilizing (negative)
386            assert!(coeffs.subsonic < 0.0);
387
388            // Check that supersonic eventually stabilizes
389            assert!(coeffs.supersonic < 0.0);
390
391            // Stable-regime presets must stay on the published small-arms coefficient scale.
392            assert!(coeffs.subsonic.abs() >= 3.0);
393            assert!(coeffs.supersonic.abs() >= 3.0);
394
395            // Check that VLD is most stable
396            if *bullet_type == "vld" {
397                let default_coeffs = PitchDampingCoefficients::default();
398                assert!(coeffs.subsonic < default_coeffs.subsonic);
399            }
400        }
401    }
402
403    #[test]
404    fn test_transonic_instability() {
405        let coeffs = PitchDampingCoefficients::from_bullet_type("hunting");
406
407        // Check that transonic high can be destabilizing (positive)
408        assert!(coeffs.transonic_high > 0.0);
409
410        // Check coefficient through transonic region
411        let mach_1_1 = calculate_pitch_damping_coefficient(1.1, &coeffs);
412
413        // Should be transitioning toward destabilizing
414        assert!(mach_1_1 > coeffs.transonic_low);
415    }
416
417    #[test]
418    fn test_transverse_moment_of_inertia() {
419        let mass_kg = 0.01134; // 175 grains
420        let caliber_m = 0.00782; // .308"
421        let length_m = 0.033; // 1.3"
422
423        let i_cylinder =
424            calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "cylinder");
425        let i_ogive = calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "ogive");
426        let i_boat_tail =
427            calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "boat_tail");
428        let i_unknown =
429            calculate_transverse_moment_of_inertia(mass_kg, caliber_m, length_m, "unknown");
430
431        // Check relative magnitudes
432        assert!(i_cylinder > i_ogive);
433        assert!(i_ogive > i_boat_tail);
434        assert_eq!(i_cylinder, i_unknown);
435
436        // Check absolute values are reasonable
437        assert!(i_cylinder > 0.0);
438        assert!(i_cylinder < 1.0); // Should be small for a bullet
439    }
440
441    #[test]
442    fn test_angular_acceleration() {
443        let moment = -0.001; // Small damping moment
444        let inertia = 0.0001; // Small inertia
445
446        let accel = calculate_angular_acceleration(moment, inertia);
447        assert_eq!(accel, moment / inertia);
448
449        // Test zero inertia
450        let accel_zero = calculate_angular_acceleration(moment, 0.0);
451        assert_eq!(accel_zero, 0.0);
452    }
453
454    #[test]
455    fn test_damped_yaw_of_repose() {
456        let (yaw, convergence) = calculate_damped_yaw_of_repose(
457            2.5,     // stability factor
458            800.0,   // velocity m/s
459            19000.0, // spin rate rad/s
460            10.0,    // wind velocity m/s
461            0.01,    // pitch rate rad/s
462            1.225,   // air density
463            0.308,   // caliber inches
464            1.3,     // length inches
465            175.0,   // mass grains
466            0.9,     // Mach
467            "match_boat_tail",
468        );
469
470        // Should have non-zero yaw and convergence
471        assert!(yaw > 0.0);
472        assert!(yaw < 0.1); // Should be small angle
473        assert!(convergence > 0.0);
474
475        // Test with no stability (Sg <= 1)
476        let (yaw_unstable, conv_unstable) = calculate_damped_yaw_of_repose(
477            0.9,
478            800.0,
479            19000.0,
480            10.0,
481            0.01,
482            1.225,
483            0.308,
484            1.3,
485            175.0,
486            0.9,
487            "match_boat_tail",
488        );
489        assert_eq!(yaw_unstable, 0.0);
490        assert_eq!(conv_unstable, 0.0);
491    }
492
493    #[test]
494    fn damped_yaw_convergence_rate_preserves_stability_sign() {
495        let rate = |mach, pitch_rate_rad_s| {
496            calculate_damped_yaw_of_repose(
497                2.5,
498                800.0,
499                19_000.0,
500                0.0,
501                pitch_rate_rad_s,
502                1.225,
503                0.308,
504                1.3,
505                175.0,
506                mach,
507                "fmj",
508            )
509            .1
510        };
511
512        // FMJ uses equal-and-opposite Cmq values at these regime boundaries.
513        let damped = rate(1.0, 0.01);
514        let divergent = rate(1.2, 0.01);
515
516        assert!(damped > 0.0);
517        assert!(divergent < 0.0);
518        assert_eq!(damped.to_bits(), (-divergent).to_bits());
519    }
520
521    #[test]
522    fn crosswind_is_not_persistent_equilibrium_yaw() {
523        let calculate = |wind_velocity_mps| {
524            calculate_damped_yaw_of_repose(
525                2.5,
526                300.0,
527                19_000.0,
528                wind_velocity_mps,
529                0.01,
530                1.225,
531                0.308,
532                1.3,
533                175.0,
534                0.875,
535                "match_boat_tail",
536            )
537        };
538
539        let (calm_yaw, calm_rate) = calculate(0.0);
540        let (windy_yaw, windy_rate) = calculate(10.0);
541
542        assert!(
543            (windy_yaw - calm_yaw).abs() < 1e-12,
544            "crosswind became persistent equilibrium yaw: calm={calm_yaw} windy={windy_yaw}"
545        );
546        assert_eq!(windy_rate.to_bits(), calm_rate.to_bits());
547        assert!(windy_yaw.abs() < 0.003);
548    }
549
550    #[test]
551    fn gravity_yaw_of_repose_matches_classical_stability_reduction() {
552        let stability_factor = 2.5;
553        let velocity_mps = 300.0;
554        let spin_rate_rad_s = 19_000.0;
555        let mass_kg = 175.0 * crate::constants::GRAINS_TO_KG;
556        let caliber_m = 0.308 * 0.0254;
557        let length_m = 1.3 * 0.0254;
558
559        let actual = calculate_gravity_yaw_of_repose(
560            stability_factor,
561            velocity_mps,
562            spin_rate_rad_s,
563            mass_kg,
564            caliber_m,
565            length_m,
566        );
567
568        // Independent expansion of the inertia approximations and the classical flat-fire
569        // reduction: yaw = 4 * Iy * Sg * g / (Ix * |p| * V).
570        let radius_m = caliber_m / 2.0;
571        let axial_inertia = 0.4 * mass_kg * radius_m.powi(2);
572        let transverse_inertia =
573            0.85 * mass_kg * (3.0 * radius_m.powi(2) + length_m.powi(2)) / 12.0;
574        let expected = 4.0 * transverse_inertia * stability_factor * 9.80665
575            / (axial_inertia * spin_rate_rad_s * velocity_mps);
576
577        assert!((actual - expected).abs() < 1e-15);
578        assert!((actual - 0.000226244442784).abs() < 1e-15);
579    }
580
581    #[test]
582    #[allow(deprecated)]
583    fn test_precession_with_damping() {
584        let precession = calculate_precession_with_damping(
585            19000.0, // spin rate rad/s
586            0.00005, // spin inertia
587            0.0001,  // transverse inertia
588            2.5,     // stability factor
589        );
590
591        assert!(precession > 0.0);
592
593        // Test zero spin
594        let precession_zero = calculate_precession_with_damping(0.0, 0.00005, 0.0001, 2.5);
595        assert_eq!(precession_zero, 0.0);
596
597        // Test a non-gyroscopically-stable projectile
598        let precession_unstable = calculate_precession_with_damping(19000.0, 0.00005, 0.0001, 1.0);
599        assert_eq!(precession_unstable, 0.0);
600    }
601
602    #[test]
603    #[allow(deprecated)]
604    fn precession_uses_slow_epicyclic_frequency() {
605        let spin_rate_rad_s = 17_522.0;
606        let spin_inertia = 6.94e-8;
607        let transverse_inertia = 9.13e-7;
608        let stability_factor = 2.0;
609        let expected = crate::precession_nutation::calculate_precession_frequency(
610            spin_rate_rad_s,
611            spin_inertia,
612            transverse_inertia,
613            stability_factor,
614        );
615
616        let actual = calculate_precession_with_damping(
617            spin_rate_rad_s,
618            spin_inertia,
619            transverse_inertia,
620            stability_factor,
621        );
622
623        assert!(
624            (actual - expected).abs() <= expected * 1e-12,
625            "precession did not use the slow epicyclic rate: actual={actual} expected={expected}"
626        );
627        assert!((150.0..250.0).contains(&actual));
628    }
629
630    #[test]
631    fn test_mach_interpolation() {
632        let coeffs = PitchDampingCoefficients::default();
633
634        // Test continuity at every piecewise interpolation boundary without assuming a scale.
635        let epsilon = 1e-9;
636        for boundary in [0.8, 1.0, 1.2, 2.0] {
637            let at_boundary = calculate_pitch_damping_coefficient(boundary, &coeffs);
638            let below = calculate_pitch_damping_coefficient(boundary - epsilon, &coeffs);
639            let above = calculate_pitch_damping_coefficient(boundary + epsilon, &coeffs);
640
641            assert!((at_boundary - below).abs() < 1e-6);
642            assert!((at_boundary - above).abs() < 1e-6);
643        }
644    }
645
646    #[test]
647    fn test_pitch_damping_edge_cases() {
648        let coeffs = PitchDampingCoefficients::default();
649
650        // Test zero pitch rate
651        let moment_zero_pitch =
652            calculate_pitch_damping_moment(0.0, 300.0, 1.225, 0.00782, 0.033, 0.87, &coeffs);
653        assert_eq!(moment_zero_pitch, 0.0);
654
655        // Test zero velocity
656        let moment_zero_vel =
657            calculate_pitch_damping_moment(0.1, 0.0, 1.225, 0.00782, 0.033, 0.87, &coeffs);
658        assert_eq!(moment_zero_vel, 0.0);
659    }
660
661    #[test]
662    fn test_default_implementation() {
663        let coeffs1 = PitchDampingCoefficients::default();
664        let coeffs2 = PitchDampingCoefficients::from_bullet_type("unknown");
665
666        assert_eq!(coeffs1.subsonic, coeffs2.subsonic);
667        assert_eq!(coeffs1.transonic_low, coeffs2.transonic_low);
668        assert_eq!(coeffs1.transonic_high, coeffs2.transonic_high);
669        assert_eq!(coeffs1.supersonic, coeffs2.supersonic);
670    }
671
672    #[test]
673    fn test_transonic_jump() {
674        let _coeffs = PitchDampingCoefficients::from_bullet_type("hunting");
675
676        // In transonic region, check for potential instability
677        let (yaw_subsonic, _) = calculate_damped_yaw_of_repose(
678            2.5, 250.0, 19000.0, 10.0, 0.01, 1.225, 0.308, 1.3, 175.0, 0.7, "hunting",
679        );
680
681        let (yaw_transonic, _) = calculate_damped_yaw_of_repose(
682            2.5, 343.0, 19000.0, 10.0, 0.01, 1.225, 0.308, 1.3, 175.0, 1.0, "hunting",
683        );
684
685        // Both should be valid but potentially different
686        assert!(yaw_subsonic > 0.0);
687        assert!(yaw_transonic > 0.0);
688    }
689}