ballistics_engine/
cluster_bc.rs1#[derive(Debug, Clone, Copy)]
9pub struct ClusterBCDegradation {
10 centroids: [(f64, f64, f64); 4],
12}
13
14impl ClusterBCDegradation {
15 pub fn new() -> Self {
16 Self {
17 centroids: [
19 (0.605, 0.415, 0.613), (0.516, 0.324, 0.643), (0.307, 0.088, 0.336), (0.750, 0.805, 0.505), ],
24 }
25 }
26
27 pub fn predict_cluster(&self, caliber: f64, weight_gr: f64, bc_g1: f64) -> usize {
29 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 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 pub fn get_bc_multiplier(&self, velocity_fps: f64, cluster_id: usize) -> f64 {
63 match cluster_id {
64 0 => {
65 if velocity_fps > 3500.0 {
68 1.0
69 } else if velocity_fps > 3000.0 {
70 1.0 - 0.01 * (3500.0 - velocity_fps) / 500.0 } else if velocity_fps > 2500.0 {
72 0.99 - 0.01 * (3000.0 - velocity_fps) / 500.0 } else if velocity_fps > 2000.0 {
74 0.98 } else if velocity_fps > 1500.0 {
76 0.98 - 0.02 * (2000.0 - velocity_fps) / 500.0 } else if velocity_fps > 1200.0 {
78 0.96 - 0.10 * (1500.0 - velocity_fps) / 300.0 } else if velocity_fps > 1000.0 {
80 0.86 + 0.11 * (1200.0 - velocity_fps) / 200.0 } else {
82 0.97 - 0.04 * (1000.0 - velocity_fps) / 1000.0 }
84 }
85 1 => {
86 if velocity_fps > 3500.0 {
88 1.0
89 } else if velocity_fps > 3000.0 {
90 1.0 - 0.01 * (3500.0 - velocity_fps) / 500.0 } else if velocity_fps > 2500.0 {
92 0.99 - 0.01 * (3000.0 - velocity_fps) / 500.0 } else if velocity_fps > 2000.0 {
94 0.98 - 0.01 * (2500.0 - velocity_fps) / 500.0 } else if velocity_fps > 1500.0 {
96 0.97 - 0.02 * (2000.0 - velocity_fps) / 500.0 } else if velocity_fps > 1200.0 {
98 0.95 - 0.05 * (1500.0 - velocity_fps) / 300.0 } else if velocity_fps > 1000.0 {
100 0.90 + 0.06 * (1200.0 - velocity_fps) / 200.0 } else {
102 0.96 - 0.02 * (1000.0 - velocity_fps) / 1000.0 }
104 }
105 2 => {
106 if velocity_fps > 3500.0 {
108 1.0
109 } else if velocity_fps > 3000.0 {
110 1.0 - 0.02 * (3500.0 - velocity_fps) / 500.0 } else if velocity_fps > 2500.0 {
112 0.98 - 0.02 * (3000.0 - velocity_fps) / 500.0 } else if velocity_fps > 2000.0 {
114 0.96 - 0.02 * (2500.0 - velocity_fps) / 500.0 } else if velocity_fps > 1500.0 {
116 0.94 - 0.04 * (2000.0 - velocity_fps) / 500.0 } else if velocity_fps > 1200.0 {
118 0.90 - 0.08 * (1500.0 - velocity_fps) / 300.0 } else if velocity_fps > 1000.0 {
120 0.82 + 0.06 * (1200.0 - velocity_fps) / 200.0 } else {
122 0.88 - 0.03 * (1000.0 - velocity_fps) / 1000.0 }
124 }
125 3 => {
126 if velocity_fps > 3500.0 {
128 1.0
129 } else if velocity_fps > 3000.0 {
130 1.0 - 0.01 * (3500.0 - velocity_fps) / 500.0 } else if velocity_fps > 2500.0 {
132 0.99 - 0.01 * (3000.0 - velocity_fps) / 500.0 } else if velocity_fps > 2000.0 {
134 0.98 - 0.01 * (2500.0 - velocity_fps) / 500.0 } else if velocity_fps > 1500.0 {
136 0.97 - 0.01 * (2000.0 - velocity_fps) / 500.0 } else if velocity_fps > 1200.0 {
138 0.96 - 0.04 * (1500.0 - velocity_fps) / 300.0 } else if velocity_fps > 1000.0 {
140 0.92 + 0.05 * (1200.0 - velocity_fps) / 200.0 } else {
142 0.97 - 0.02 * (1000.0 - velocity_fps) / 1000.0 }
144 }
145 _ => 1.0, }
147 }
148
149 pub fn get_cluster_name(&self, cluster_id: usize) -> &'static str {
151 match cluster_id {
152 0 => "Standard Long-Range",
153 1 => "Low-Drag Specialty",
154 2 => "Light Varmint/Target",
155 3 => "Heavy Magnum",
156 _ => "Unknown",
157 }
158 }
159
160 pub fn apply_correction(
162 &self,
163 bc: f64,
164 caliber: f64,
165 weight_gr: f64,
166 velocity_fps: f64,
167 ) -> f64 {
168 let cluster_id = self.predict_cluster(caliber, weight_gr, bc);
169 let multiplier = self.get_bc_multiplier(velocity_fps, cluster_id);
170 bc * multiplier
171 }
172}
173
174impl Default for ClusterBCDegradation {
175 fn default() -> Self {
176 Self::new()
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_cluster_prediction() {
186 let cluster_bc = ClusterBCDegradation::new();
187
188 let cluster = cluster_bc.predict_cluster(0.308, 168.0, 0.475);
190 assert!(
191 cluster <= 3,
192 "Standard long-range should be in a valid cluster"
193 );
194
195 let cluster = cluster_bc.predict_cluster(0.224, 55.0, 0.250);
197 assert_eq!(cluster, 2);
198
199 let cluster = cluster_bc.predict_cluster(0.458, 500.0, 0.295);
201 assert_eq!(cluster, 3);
202 }
203
204 #[test]
205 fn test_bc_multiplier() {
206 let cluster_bc = ClusterBCDegradation::new();
207
208 let mult = cluster_bc.get_bc_multiplier(3000.0, 0);
210 assert!(mult > 0.95 && mult <= 1.0);
211
212 let mult = cluster_bc.get_bc_multiplier(1000.0, 0);
214 assert!(
215 mult > 0.90 && mult < 1.0,
216 "Subsonic should retain ~93% BC, got {}",
217 mult
218 );
219
220 let mult_transonic = cluster_bc.get_bc_multiplier(1300.0, 0);
222 let mult_subsonic = cluster_bc.get_bc_multiplier(900.0, 0);
223 assert!(
224 mult_transonic < mult_subsonic,
225 "Transonic ({}) should be lower than subsonic ({})",
226 mult_transonic,
227 mult_subsonic
228 );
229 }
230
231 #[test]
232 fn test_apply_correction() {
233 let cluster_bc = ClusterBCDegradation::new();
234
235 let bc_original = 0.475;
237 let bc_corrected = cluster_bc.apply_correction(bc_original, 0.308, 168.0, 1300.0);
238 assert!(bc_corrected < bc_original);
239
240 let bc_corrected_high = cluster_bc.apply_correction(bc_original, 0.308, 168.0, 2800.0);
242 assert!(
243 bc_corrected_high >= bc_original * 0.95,
244 "High velocity should have minimal BC reduction (>95%), got {}",
245 bc_corrected_high / bc_original
246 );
247
248 let bc_subsonic = cluster_bc.apply_correction(bc_original, 0.308, 168.0, 800.0);
252 assert!(
253 bc_subsonic >= bc_original * 0.80,
254 "Subsonic should retain >80% BC, got {}",
255 bc_subsonic / bc_original
256 );
257
258 let mult_subsonic_c0 = cluster_bc.get_bc_multiplier(800.0, 0);
260 assert!(
261 mult_subsonic_c0 >= 0.90,
262 "Cluster 0 subsonic should retain >90% BC, got {}",
263 mult_subsonic_c0
264 );
265 }
266}