ballistics_engine/
cluster_bc.rs

1//! Cluster-based BC degradation for improved accuracy
2//!
3//! This module implements empirical BC corrections based on bullet clustering.
4//! Bullets are classified into 4 clusters based on their physical characteristics,
5//! and each cluster has unique velocity-dependent BC degradation curves derived
6//! from real-world ballistic data.
7
8#[derive(Debug, Clone, Copy)]
9pub struct ClusterBCDegradation {
10    /// Pre-calculated cluster centroids
11    centroids: [(f64, f64, f64); 4],
12}
13
14impl ClusterBCDegradation {
15    pub fn new() -> Self {
16        Self {
17            // Cluster centroids: (caliber_normalized, weight_normalized, bc_normalized)
18            centroids: [
19                (0.605, 0.415, 0.613), // Cluster 0: Standard Long-Range
20                (0.516, 0.324, 0.643), // Cluster 1: Low-Drag Specialty
21                (0.307, 0.088, 0.336), // Cluster 2: Light Varmint
22                (0.750, 0.805, 0.505), // Cluster 3: Heavy Magnums
23            ],
24        }
25    }
26
27    /// Predict which cluster a bullet belongs to
28    pub fn predict_cluster(&self, caliber: f64, weight_gr: f64, bc_g1: f64) -> usize {
29        // Normalize features to [0, 1] range
30        // Bounds derived from training data and centroid values:
31        // - Caliber: 0.172 (.17 HMR) to 0.750 (.50 BMG) - matches cluster 3 centroid
32        // - Weight: 15gr (light varmint) to 750gr (heavy magnum)
33        // - BC G1: 0.05 (low drag) to 1.2 (high BC match bullets)
34        let caliber_norm = (caliber - 0.172) / (0.750 - 0.172);
35        let weight_norm = (weight_gr - 15.0) / (750.0 - 15.0);
36        let bc_norm = (bc_g1 - 0.05) / (1.2 - 0.05);
37
38        // Find nearest centroid
39        let mut min_distance = f64::INFINITY;
40        let mut best_cluster = 0;
41
42        for (i, &(c_cal, c_wt, c_bc)) in self.centroids.iter().enumerate() {
43            let distance = ((caliber_norm - c_cal).powi(2)
44                + (weight_norm - c_wt).powi(2)
45                + (bc_norm - c_bc).powi(2))
46            .sqrt();
47
48            if distance < min_distance {
49                min_distance = distance;
50                best_cluster = i;
51            }
52        }
53
54        best_cluster
55    }
56
57    /// Get BC multiplier for a given velocity and cluster
58    pub fn get_bc_multiplier(&self, velocity_fps: f64, cluster_id: usize) -> f64 {
59        match cluster_id {
60            0 => {
61                // Standard Long-Range: gradual degradation
62                if velocity_fps > 2800.0 {
63                    1.0
64                } else if velocity_fps > 2400.0 {
65                    0.98 - 0.03 * (2800.0 - velocity_fps) / 400.0
66                } else if velocity_fps > 1800.0 {
67                    0.95 - 0.05 * (2400.0 - velocity_fps) / 600.0
68                } else if velocity_fps > 1200.0 {
69                    0.90 - 0.10 * (1800.0 - velocity_fps) / 600.0
70                } else {
71                    0.80 - 0.05 * (1200.0 - velocity_fps) / 1200.0
72                }
73            }
74            1 => {
75                // Low-Drag Specialty: minimal degradation
76                if velocity_fps > 3000.0 {
77                    1.0
78                } else if velocity_fps > 2500.0 {
79                    0.99 - 0.02 * (3000.0 - velocity_fps) / 500.0
80                } else if velocity_fps > 2000.0 {
81                    0.97 - 0.03 * (2500.0 - velocity_fps) / 500.0
82                } else if velocity_fps > 1500.0 {
83                    0.94 - 0.06 * (2000.0 - velocity_fps) / 500.0
84                } else {
85                    0.88 - 0.08 * (1500.0 - velocity_fps) / 1500.0
86                }
87            }
88            2 => {
89                // Light Varmint/Target: significant degradation
90                if velocity_fps > 3500.0 {
91                    1.0
92                } else if velocity_fps > 3000.0 {
93                    0.96 - 0.04 * (3500.0 - velocity_fps) / 500.0
94                } else if velocity_fps > 2200.0 {
95                    0.92 - 0.08 * (3000.0 - velocity_fps) / 800.0
96                } else if velocity_fps > 1600.0 {
97                    0.84 - 0.14 * (2200.0 - velocity_fps) / 600.0
98                } else {
99                    0.70 - 0.15 * (1600.0 - velocity_fps) / 1600.0
100                }
101            }
102            3 => {
103                // Heavy Magnums: moderate degradation with steep initial drop
104                if velocity_fps > 2600.0 {
105                    1.0
106                } else if velocity_fps > 2200.0 {
107                    0.96 - 0.06 * (2600.0 - velocity_fps) / 400.0
108                } else if velocity_fps > 1700.0 {
109                    0.90 - 0.10 * (2200.0 - velocity_fps) / 500.0
110                } else if velocity_fps > 1200.0 {
111                    0.80 - 0.15 * (1700.0 - velocity_fps) / 500.0
112                } else {
113                    0.65 - 0.10 * (1200.0 - velocity_fps) / 1200.0
114                }
115            }
116            _ => 1.0, // Default: no adjustment
117        }
118    }
119
120    /// Get cluster name for display
121    pub fn get_cluster_name(&self, cluster_id: usize) -> &'static str {
122        match cluster_id {
123            0 => "Standard Long-Range",
124            1 => "Low-Drag Specialty",
125            2 => "Light Varmint/Target",
126            3 => "Heavy Magnum",
127            _ => "Unknown",
128        }
129    }
130
131    /// Apply cluster-based BC correction
132    pub fn apply_correction(
133        &self,
134        bc: f64,
135        caliber: f64,
136        weight_gr: f64,
137        velocity_fps: f64,
138    ) -> f64 {
139        let cluster_id = self.predict_cluster(caliber, weight_gr, bc);
140        let multiplier = self.get_bc_multiplier(velocity_fps, cluster_id);
141        bc * multiplier
142    }
143}
144
145impl Default for ClusterBCDegradation {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_cluster_prediction() {
157        let cluster_bc = ClusterBCDegradation::new();
158
159        // Test standard long-range bullet (308 Win 168gr)
160        let cluster = cluster_bc.predict_cluster(0.308, 168.0, 0.475);
161        assert!(
162            cluster <= 3,
163            "Standard long-range should be in a valid cluster"
164        );
165
166        // Test light varmint bullet (223 Rem 55gr)
167        let cluster = cluster_bc.predict_cluster(0.224, 55.0, 0.250);
168        assert_eq!(cluster, 2);
169
170        // Test heavy magnum (458 Win Mag 500gr)
171        let cluster = cluster_bc.predict_cluster(0.458, 500.0, 0.295);
172        assert_eq!(cluster, 3);
173    }
174
175    #[test]
176    fn test_bc_multiplier() {
177        let cluster_bc = ClusterBCDegradation::new();
178
179        // Test high velocity (minimal degradation)
180        let mult = cluster_bc.get_bc_multiplier(3000.0, 0);
181        assert!(mult > 0.95 && mult <= 1.0);
182
183        // Test low velocity (significant degradation)
184        let mult = cluster_bc.get_bc_multiplier(1000.0, 0);
185        assert!(mult < 0.85);
186
187        // Test that multiplier decreases with velocity
188        let mult_high = cluster_bc.get_bc_multiplier(2500.0, 1);
189        let mult_low = cluster_bc.get_bc_multiplier(1500.0, 1);
190        assert!(mult_high > mult_low);
191    }
192
193    #[test]
194    fn test_apply_correction() {
195        let cluster_bc = ClusterBCDegradation::new();
196
197        // Test that correction reduces BC at low velocity
198        let bc_original = 0.475;
199        let bc_corrected = cluster_bc.apply_correction(bc_original, 0.308, 168.0, 1500.0);
200        assert!(bc_corrected < bc_original);
201
202        // Test that correction is minimal at high velocity
203        let bc_corrected_high = cluster_bc.apply_correction(bc_original, 0.308, 168.0, 2800.0);
204        assert!(
205            bc_corrected_high >= bc_original * 0.90,
206            "High velocity should have minimal BC reduction"
207        );
208    }
209}