type VelocityBand = (f64, f64);
const VELOCITY_BANDS: [[VelocityBand; 8]; 4] = [
[
(3500.0, 1.00),
(3000.0, 1.01),
(2500.0, 1.01),
(2000.0, 0.99),
(1500.0, 0.99),
(1200.0, 1.03),
(1000.0, 1.05),
(0.0, 0.94),
],
[
(3500.0, 0.90),
(3000.0, 0.91),
(2500.0, 0.94),
(2000.0, 1.02),
(1500.0, 1.06),
(1200.0, 1.17),
(1000.0, 1.40),
(0.0, 1.14),
],
[
(3500.0, 1.00),
(3000.0, 1.00),
(2500.0, 1.01),
(2000.0, 0.99),
(1500.0, 0.985),
(1200.0, 1.01),
(1000.0, 1.03),
(0.0, 0.90),
],
[
(3500.0, 1.00),
(3000.0, 1.01),
(2500.0, 1.00),
(2000.0, 0.99),
(1500.0, 0.99),
(1200.0, 1.02),
(1000.0, 1.04),
(0.0, 0.94),
],
];
#[derive(Debug, Clone, Copy, Default)]
pub struct ClusterBCDegradation;
impl ClusterBCDegradation {
pub fn new() -> Self {
Self
}
pub fn predict_cluster(&self, caliber: f64, weight_gr: f64, bc_g1: f64) -> usize {
let sd = if caliber > 0.0 {
weight_gr / (7000.0 * caliber * caliber)
} else {
0.0
};
let form_factor = if sd > 0.0 { bc_g1 / sd } else { 1.5 };
if weight_gr > 400.0 && caliber > 0.35 {
return 3;
}
if form_factor < 1.1 && bc_g1 < 0.2 {
return 1;
}
if weight_gr < 100.0 && caliber < 0.3 {
return 2;
}
0
}
pub fn get_bc_multiplier(&self, velocity_fps: f64, cluster_id: usize) -> f64 {
let cid = if cluster_id < VELOCITY_BANDS.len() {
cluster_id
} else {
0
};
let bands = &VELOCITY_BANDS[cid];
for i in 0..bands.len() {
let (v_threshold, multiplier) = bands[i];
if velocity_fps >= v_threshold {
if i == 0 {
return multiplier;
}
let (v_high, mult_high) = bands[i - 1];
let (v_low, mult_low) = bands[i];
let t = (velocity_fps - v_low) / (v_high - v_low);
return mult_low + t * (mult_high - mult_low);
}
}
bands[bands.len() - 1].1
}
pub fn get_cluster_name(&self, cluster_id: usize) -> &'static str {
match cluster_id {
0 => "Standard Long-Range",
1 => "Low-Drag Specialty",
2 => "Light Varmint/Target",
3 => "Heavy Magnum",
_ => "Unknown",
}
}
pub fn apply_correction(
&self,
bc: f64,
caliber: f64,
weight_gr: f64,
velocity_fps: f64,
) -> f64 {
let cluster_id = self.predict_cluster(caliber, weight_gr, bc);
let multiplier = self.get_bc_multiplier(velocity_fps, cluster_id);
bc * multiplier
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cluster_prediction() {
let cluster_bc = ClusterBCDegradation::new();
assert_eq!(cluster_bc.predict_cluster(0.308, 168.0, 0.475), 0);
assert_eq!(cluster_bc.predict_cluster(0.224, 55.0, 0.250), 2);
assert_eq!(cluster_bc.predict_cluster(0.458, 500.0, 0.295), 3);
}
#[test]
fn test_caliber_must_be_inches_not_meters() {
let cluster_bc = ClusterBCDegradation::new();
let inches = cluster_bc.predict_cluster(0.458, 500.0, 0.295);
let meters = cluster_bc.predict_cluster(0.458 * 0.0254, 500.0, 0.295);
assert_eq!(inches, 3, ".458/500gr in inches -> Heavy Magnum cluster");
assert_ne!(
meters, inches,
"caliber in meters must misclassify (caller must pass inches)"
);
}
#[test]
fn test_bc_multiplier_g7_semantics() {
let cluster_bc = ClusterBCDegradation::new();
let mult_hi = cluster_bc.get_bc_multiplier(3000.0, 0);
assert!(
(mult_hi - 1.01).abs() < 1e-9,
"cluster 0 @3000fps should be 1.01, got {mult_hi}"
);
let mult_transonic = cluster_bc.get_bc_multiplier(1100.0, 0);
let mult_mid = cluster_bc.get_bc_multiplier(1750.0, 0);
assert!(
mult_transonic > mult_mid,
"G7 transonic ({mult_transonic}) should now rise above mid-supersonic ({mult_mid})"
);
}
#[test]
fn test_apply_correction_g7() {
let cluster_bc = ClusterBCDegradation::new();
let bc = 0.475;
let hi = cluster_bc.apply_correction(bc, 0.308, 168.0, 2800.0);
assert!(hi >= bc * 0.95, "high-v should keep >=95% BC, got {}", hi / bc);
let transonic = cluster_bc.apply_correction(bc, 0.308, 168.0, 1100.0);
assert!(
transonic > bc,
"cluster-0 transonic G7 BC should rise above stated, got {}",
transonic / bc
);
}
#[test]
fn test_parity_with_python_reference() {
let m = ClusterBCDegradation::new();
let mult_goldens: &[(usize, f64, f64)] = &[
(0, 3600.0, 1.000000), (0, 3500.0, 1.000000), (0, 3250.0, 1.005000),
(0, 3000.0, 1.010000), (0, 2800.0, 1.010000), (0, 2500.0, 1.010000),
(0, 2250.0, 1.000000), (0, 2000.0, 0.990000), (0, 1750.0, 0.990000),
(0, 1500.0, 0.990000), (0, 1350.0, 1.010000), (0, 1200.0, 1.030000),
(0, 1100.0, 1.040000), (0, 1000.0, 1.050000), (0, 900.0, 1.039000),
(0, 800.0, 1.028000), (0, 500.0, 0.995000), (0, 0.0, 0.940000),
(1, 3600.0, 0.900000), (1, 3500.0, 0.900000), (1, 3250.0, 0.905000),
(1, 3000.0, 0.910000), (1, 2800.0, 0.922000), (1, 2500.0, 0.940000),
(1, 2250.0, 0.980000), (1, 2000.0, 1.020000), (1, 1750.0, 1.040000),
(1, 1500.0, 1.060000), (1, 1350.0, 1.115000), (1, 1200.0, 1.170000),
(1, 1100.0, 1.285000), (1, 1000.0, 1.400000), (1, 900.0, 1.374000),
(1, 800.0, 1.348000), (1, 500.0, 1.270000), (1, 0.0, 1.140000),
(2, 3600.0, 1.000000), (2, 3500.0, 1.000000), (2, 3250.0, 1.000000),
(2, 3000.0, 1.000000), (2, 2800.0, 1.004000), (2, 2500.0, 1.010000),
(2, 2250.0, 1.000000), (2, 2000.0, 0.990000), (2, 1750.0, 0.987500),
(2, 1500.0, 0.985000), (2, 1350.0, 0.997500), (2, 1200.0, 1.010000),
(2, 1100.0, 1.020000), (2, 1000.0, 1.030000), (2, 900.0, 1.017000),
(2, 800.0, 1.004000), (2, 500.0, 0.965000), (2, 0.0, 0.900000),
(3, 3600.0, 1.000000), (3, 3500.0, 1.000000), (3, 3250.0, 1.005000),
(3, 3000.0, 1.010000), (3, 2800.0, 1.006000), (3, 2500.0, 1.000000),
(3, 2250.0, 0.995000), (3, 2000.0, 0.990000), (3, 1750.0, 0.990000),
(3, 1500.0, 0.990000), (3, 1350.0, 1.005000), (3, 1200.0, 1.020000),
(3, 1100.0, 1.030000), (3, 1000.0, 1.040000), (3, 900.0, 1.030000),
(3, 800.0, 1.020000), (3, 500.0, 0.990000), (3, 0.0, 0.940000),
];
for &(c, v, expected) in mult_goldens {
let got = m.get_bc_multiplier(v, c);
assert!(
(got - expected).abs() < 1e-6,
"multiplier parity: cluster {c} @{v}fps expected {expected}, got {got}"
);
}
let cluster_goldens: &[(f64, f64, f64, usize)] = &[
(0.308, 168.0, 0.475, 0), (0.308, 168.0, 0.223, 0),
(0.224, 55.0, 0.250, 2), (0.224, 77.0, 0.372, 2),
(0.458, 500.0, 0.295, 3), (0.375, 350.0, 0.310, 0),
(0.264, 140.0, 0.315, 0), (0.284, 180.0, 0.360, 0),
(0.338, 300.0, 0.400, 0), (0.243, 105.0, 0.278, 0),
(0.20, 40.0, 0.15, 1), (0.172, 20.0, 0.10, 1),
(0.510, 750.0, 0.500, 3), (0.30, 150.0, 0.19, 1),
(0.36, 410.0, 0.40, 3),
];
for &(cal, wt, bc, expected) in cluster_goldens {
let got = m.predict_cluster(cal, wt, bc);
assert_eq!(
got, expected,
"cluster parity: ({cal}, {wt}, {bc}) expected {expected}, got {got}"
);
}
}
}