Skip to main content

ballistics_engine/
constants.rs

1//! Physical constants used in ballistics calculations.
2
3/// Gravitational acceleration in m/s²
4pub const G_ACCEL_MPS2: f64 = 9.80665;
5
6/// Conversion factor: meters per second to feet per second
7pub const MPS_TO_FPS: f64 = 3.28084;
8
9/// Conversion factor: feet per second to meters per second
10pub const FPS_TO_MPS: f64 = 0.3048;
11
12/// Default powder reference temperature in Fahrenheit.
13pub const DEFAULT_POWDER_REFERENCE_TEMP_F: f64 = 70.0;
14
15/// Default powder reference temperature in Celsius.
16pub const DEFAULT_POWDER_REFERENCE_TEMP_C: f64 =
17    (DEFAULT_POWDER_REFERENCE_TEMP_F - 32.0) * 5.0 / 9.0;
18
19/// Standard air density at sea level (kg/m³)
20pub const STANDARD_AIR_DENSITY: f64 = 1.225;
21
22/// Standard sea-level air density under the ICAO Standard Atmosphere (lb/ft^3).
23///
24/// This is the density basis for [`CD_TO_RETARD`] below — see its doc comment. Most
25/// modern published ballistic coefficients (this engine's own retardation formula
26/// included) are referenced to this density.
27pub const ICAO_DENSITY_LB_FT3: f64 = 0.076474;
28
29/// Standard sea-level air density under the (older) Army Standard Metro atmosphere
30/// (lb/ft^3).
31///
32/// Many published Sierra/Hornady/Barnes ballistic coefficients are referenced to THIS
33/// density instead of [`ICAO_DENSITY_LB_FT3`] — quoting the same physical drag at a
34/// ~1.8% different reference air density. Not used by this engine's retardation math
35/// directly (which is calibrated to the ICAO figure via [`CD_TO_RETARD`]); it exists so
36/// [`ASM_TO_ICAO_BC`] can convert an ASM-referenced BC to the ICAO-referenced value the
37/// engine expects (`BallisticInputs::bc_reference_standard`, MBA-1365).
38pub const ASM_DENSITY_LB_FT3: f64 = 0.075126;
39
40/// Multiplier that converts an Army-Standard-Metro-referenced BC to the ICAO-referenced
41/// value this engine's retardation formula expects (MBA-1365):
42///
43/// `BC_icao = ASM_TO_ICAO_BC * BC_asm`
44///
45/// A denser reference atmosphere implies a numerically SMALLER published BC for the same
46/// physical bullet (the same real drag is attributed to a "worse" — smaller — BC when the
47/// assumed air is denser), so this ratio is `ASM_DENSITY_LB_FT3 / ICAO_DENSITY_LB_FT3`
48/// (< 1), not its reciprocal. Evaluates to 0.98237 to 5 decimal places (asserted by a
49/// unit test in `cli_api.rs`).
50pub const ASM_TO_ICAO_BC: f64 = ASM_DENSITY_LB_FT3 / ICAO_DENSITY_LB_FT3;
51
52/// Cd-to-retardation conversion for ICAO-referenced BCs.
53///
54/// Exact imperial retardation form for density normalized to ICAO sea-level air
55/// (1.225 kg/m^3 / [`ICAO_DENSITY_LB_FT3`]):
56///
57/// `a_ft/s^2 = Cd * v_fps^2 * (rho / 1.225) * CD_TO_RETARD / BC`
58///
59/// The older `0.000683 * 0.30` value is the Army Standard Metro constant and is
60/// only consistent with an [`ASM_DENSITY_LB_FT3`] density reference.
61pub const CD_TO_RETARD: f64 = 2.08551e-4;
62
63/// Conversion factor: grains to kilograms
64pub const GRAINS_TO_KG: f64 = 0.00006479891;
65
66/// Grams per grain — exact by definition.
67///
68/// The international avoirdupois pound is defined as exactly 0.45359237 kg
69/// (i.e. 453.59237 g), and one pound is exactly 7000 grains, so this value
70/// is exact (not a measured or rounded conversion):
71/// 453.59237 g / 7000 = 0.06479891 g/grain (equivalently,
72/// 0.45359237 kg / 7000 = 0.00006479891 kg/grain, then x1000 to get grams).
73///
74/// This is the single source of truth for the grain<->gram conversion; do
75/// not re-derive or re-round it elsewhere (see `tests/constants_guard.rs`).
76pub const GRAMS_PER_GRAIN: f64 = 0.06479891;
77
78/// Grains per gram — the exact reciprocal of [`GRAMS_PER_GRAIN`].
79pub const GRAINS_PER_GRAM: f64 = 1.0 / GRAMS_PER_GRAIN;
80
81/// Air density at sea level (kg/m³)
82pub const AIR_DENSITY_SEA_LEVEL: f64 = 1.225;
83
84/// Speed of sound at sea level, standard atmospheric conditions
85///
86/// Value: 340.29 m/s (1116.44 ft/s)
87/// Conditions: 15°C (59°F), 1013.25 hPa, dry air
88///
89/// Temperature dependence: c = 331.3 * sqrt(T_kelvin / 273.15)
90///
91/// Note: Some calculations use 343.0 m/s (20°C reference) - ensure consistency
92/// in Mach number calculations. This value follows ICAO Standard Atmosphere.
93///
94/// Source: International Standard Atmosphere (ISO 2533)
95pub const SPEED_OF_SOUND_MPS: f64 = 340.29;
96
97// Numerical stability constants
98/// General numerical tolerance for floating point comparisons
99pub const NUMERICAL_TOLERANCE: f64 = 1e-9;
100
101/// Minimum threshold for velocity magnitude to avoid division by zero
102pub const MIN_VELOCITY_THRESHOLD: f64 = 1e-6;
103
104/// Minimum threshold for preventing division by zero in general calculations
105pub const MIN_DIVISION_THRESHOLD: f64 = 1e-12;
106
107/// Tolerance for root finding algorithms
108pub const ROOT_FINDING_TOLERANCE: f64 = 1e-6;
109
110/// Minimum threshold for Mach number calculations near unity
111pub const MIN_MACH_THRESHOLD: f64 = 1e-3;
112
113// Ballistic Coefficient (BC) fallback constants
114//
115// These values are used when BC calculations fail or data is missing.
116// Derived from statistical analysis of 2,000+ projectile database.
117// Values represent conservative estimates (25th percentile) to avoid
118// over-predicting ballistic performance.
119
120/// Conservative overall BC fallback value
121///
122/// Value: 0.31 (25th percentile from comprehensive ballistics database)
123/// Usage: General fallback when no specific projectile data available
124/// Methodology: Statistical analysis of measured BC values across all categories
125pub const BC_FALLBACK_CONSERVATIVE: f64 = 0.31;
126
127// BC fallback values by projectile weight category (grains).
128// Values are based on statistical analysis of ballistic coefficient vs mass relationships.
129// Each constant represents the 25th-percentile BC for that weight category.
130
131/// Ultra-light projectiles (0-50 grains)
132/// Typical: .17 caliber varmint bullets, .22 caliber target bullets
133pub const BC_FALLBACK_ULTRA_LIGHT: f64 = 0.172;
134
135/// Light projectiles (50-100 grains)  
136/// Typical: .223 Remington, .243 Winchester hunting bullets
137pub const BC_FALLBACK_LIGHT: f64 = 0.242;
138
139/// Medium projectiles (100-150 grains)
140/// Typical: .270 Winchester, .30-06 hunting bullets
141pub const BC_FALLBACK_MEDIUM: f64 = 0.310;
142
143/// Heavy projectiles (150-200 grains)
144/// Typical: .308 Winchester match bullets, .300 Winchester Magnum
145pub const BC_FALLBACK_HEAVY: f64 = 0.393;
146
147/// Very heavy projectiles (200+ grains)
148/// Typical: .338 Lapua Magnum, .50 BMG bullets
149pub const BC_FALLBACK_VERY_HEAVY: f64 = 0.441;
150
151// BC fallback values by caliber category (inches).
152// Values account for diameter limitations on achievable ballistic coefficient.
153// Larger calibers generally allow higher BC but with diminishing returns.
154
155/// Small calibers (.224" and smaller)
156/// Examples: .17 Remington, .22-250, .223 Remington
157pub const BC_FALLBACK_SMALL_CALIBER: f64 = 0.215;
158
159/// Medium calibers (.243")
160/// Examples: .243 Winchester, 6mm Creedmoor
161pub const BC_FALLBACK_MEDIUM_CALIBER: f64 = 0.300;
162
163/// Large calibers (.264" to .284")
164/// Examples: .270 Winchester, .280 Remington, 7mm Remington Magnum
165pub const BC_FALLBACK_LARGE_CALIBER: f64 = 0.404;
166
167/// Extra large calibers (.308" and larger)
168/// Examples: .308 Winchester, .30-06, .300 Winchester Magnum
169/// Note: Lower than expected due to inclusion of older, less aerodynamic designs
170pub const BC_FALLBACK_XLARGE_CALIBER: f64 = 0.291;
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn powder_reference_temperature_defaults_are_equivalent() {
178        let converted = (DEFAULT_POWDER_REFERENCE_TEMP_F - 32.0) * 5.0 / 9.0;
179
180        assert_eq!(
181            DEFAULT_POWDER_REFERENCE_TEMP_C.to_bits(),
182            converted.to_bits()
183        );
184    }
185
186    /// Independent ground-truth check for the grain<->gram constants: this
187    /// test does *not* reference `GRAMS_PER_GRAIN`/`GRAINS_PER_GRAM` in its
188    /// expected values, so it fails if either constant is ever silently
189    /// corrupted (re-truncated, re-rounded, or swapped for a sibling value),
190    /// even if the corrupted value doesn't match one of the specific banned
191    /// literals `tests/constants_guard.rs` greps for.
192    #[test]
193    fn grams_per_grain_matches_independent_ground_truth() {
194        // 1 lb = exactly 0.45359237 kg = 453.59237 g (avoirdupois pound,
195        // international definition); 1 lb = exactly 7000 grains.
196        let independent_grams_per_grain: f64 = 453.59237 / 7000.0;
197        assert_eq!(
198            GRAMS_PER_GRAIN.to_bits(),
199            independent_grams_per_grain.to_bits()
200        );
201
202        // Cross-check against the pre-existing kilogram-scale constant:
203        // GRAMS_PER_GRAIN must equal GRAINS_TO_KG scaled from kg to g.
204        assert!((GRAMS_PER_GRAIN - GRAINS_TO_KG * 1000.0).abs() < 1e-15);
205
206        // GRAINS_PER_GRAM must be the exact reciprocal used in production.
207        assert_eq!(GRAINS_PER_GRAM.to_bits(), (1.0 / GRAMS_PER_GRAIN).to_bits());
208    }
209}