use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;
#[derive(Debug)]
pub struct BcCorrectionTable {
data: Vec<f32>,
bc_bins: Vec<f32>,
mass_bins: Vec<f32>,
length_bins: Vec<f32>,
velocity_bins: Vec<f32>,
num_types: usize,
version: u32,
}
#[derive(Debug)]
pub enum BcTableError {
IoError(std::io::Error),
InvalidMagic,
UnsupportedVersion(u32),
ChecksumMismatch,
InvalidDimensions,
}
impl std::fmt::Display for BcTableError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BcTableError::IoError(e) => write!(f, "IO error: {}", e),
BcTableError::InvalidMagic => write!(f, "Invalid file magic (expected 'BCCR')"),
BcTableError::UnsupportedVersion(v) => write!(f, "Unsupported table version: {}", v),
BcTableError::ChecksumMismatch => write!(f, "Data checksum mismatch"),
BcTableError::InvalidDimensions => write!(f, "Invalid table dimensions"),
}
}
}
impl std::error::Error for BcTableError {}
impl From<std::io::Error> for BcTableError {
fn from(e: std::io::Error) -> Self {
BcTableError::IoError(e)
}
}
impl BcCorrectionTable {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, BcTableError> {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
if &magic != b"BCCR" {
return Err(BcTableError::InvalidMagic);
}
let version = read_u32(&mut reader)?;
if version != 1 {
return Err(BcTableError::UnsupportedVersion(version));
}
let _flags = read_u32(&mut reader)?;
let num_bc = read_u32(&mut reader)? as usize;
let num_mass = read_u32(&mut reader)? as usize;
let num_length = read_u32(&mut reader)? as usize;
let num_velocity = read_u32(&mut reader)? as usize;
let num_types = read_u32(&mut reader)? as usize;
let _timestamp = read_u64(&mut reader)?;
let _checksum = read_u32(&mut reader)?;
let mut reserved = [0u8; 16];
reader.read_exact(&mut reserved)?;
if num_bc == 0 || num_mass == 0 || num_length == 0 || num_velocity == 0 || num_types == 0 {
return Err(BcTableError::InvalidDimensions);
}
let bc_bins = read_f32_array(&mut reader, num_bc)?;
let mass_bins = read_f32_array(&mut reader, num_mass)?;
let length_bins = read_f32_array(&mut reader, num_length)?;
let velocity_bins = read_f32_array(&mut reader, num_velocity)?;
let total_cells = num_types * num_bc * num_mass * num_length * num_velocity;
let data = read_f32_array(&mut reader, total_cells)?;
Ok(BcCorrectionTable {
data,
bc_bins,
mass_bins,
length_bins,
velocity_bins,
num_types,
version,
})
}
pub fn lookup(&self, bc: f64, bc_type: &str, mass: f64, length: f64, velocity: f64) -> f64 {
let type_idx = if bc_type.to_uppercase() == "G1" { 0 } else { 1 };
let (bc_idx, bc_weight) = self.interp_idx(bc as f32, &self.bc_bins);
let (mass_idx, mass_weight) = self.interp_idx(mass as f32, &self.mass_bins);
let (length_idx, length_weight) = self.interp_idx(length as f32, &self.length_bins);
let (vel_idx, vel_weight) = self.interp_idx(velocity as f32, &self.velocity_bins);
let mut result = 0.0f64;
for di in 0..2 {
for dj in 0..2 {
for dk in 0..2 {
for dl in 0..2 {
let w = (if di == 0 { 1.0 - bc_weight } else { bc_weight })
* (if dj == 0 { 1.0 - mass_weight } else { mass_weight })
* (if dk == 0 { 1.0 - length_weight } else { length_weight })
* (if dl == 0 { 1.0 - vel_weight } else { vel_weight });
let i = (bc_idx + di).min(self.bc_bins.len() - 1);
let j = (mass_idx + dj).min(self.mass_bins.len() - 1);
let k = (length_idx + dk).min(self.length_bins.len() - 1);
let l = (vel_idx + dl).min(self.velocity_bins.len() - 1);
let idx = self.flat_index(type_idx, i, j, k, l);
result += w * self.data[idx] as f64;
}
}
}
}
result
}
pub fn lookup_with_caliber(
&self,
bc: f64,
bc_type: &str,
mass_grains: f64,
caliber_inches: f64,
velocity_fps: f64,
) -> f64 {
let estimated_length = caliber_inches * 3.5;
self.lookup(bc, bc_type, mass_grains, estimated_length, velocity_fps)
}
fn interp_idx(&self, value: f32, bins: &[f32]) -> (usize, f64) {
if bins.is_empty() {
return (0, 0.0);
}
if value <= bins[0] {
return (0, 0.0);
}
if value >= bins[bins.len() - 1] {
return (bins.len().saturating_sub(2), 1.0);
}
let idx = match bins.binary_search_by(|probe| probe.partial_cmp(&value).unwrap()) {
Ok(i) => i.saturating_sub(1).min(bins.len() - 2),
Err(i) => i.saturating_sub(1).min(bins.len() - 2),
};
let low = bins[idx];
let high = bins[idx + 1];
let weight = if high > low {
((value - low) / (high - low)) as f64
} else {
0.0
};
(idx, weight)
}
fn flat_index(&self, type_idx: usize, bc_idx: usize, mass_idx: usize, length_idx: usize, vel_idx: usize) -> usize {
let n_bc = self.bc_bins.len();
let n_mass = self.mass_bins.len();
let n_length = self.length_bins.len();
let n_velocity = self.velocity_bins.len();
type_idx * (n_bc * n_mass * n_length * n_velocity)
+ bc_idx * (n_mass * n_length * n_velocity)
+ mass_idx * (n_length * n_velocity)
+ length_idx * n_velocity
+ vel_idx
}
pub fn version(&self) -> u32 {
self.version
}
pub fn total_cells(&self) -> usize {
self.data.len()
}
pub fn dimensions_str(&self) -> String {
format!(
"{}x{}x{}x{}x{}",
self.bc_bins.len(),
self.mass_bins.len(),
self.length_bins.len(),
self.velocity_bins.len(),
self.num_types
)
}
}
fn read_u32<R: Read>(reader: &mut R) -> Result<u32, std::io::Error> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
fn read_u64<R: Read>(reader: &mut R) -> Result<u64, std::io::Error> {
let mut buf = [0u8; 8];
reader.read_exact(&mut buf)?;
Ok(u64::from_le_bytes(buf))
}
fn read_f32_array<R: Read>(reader: &mut R, count: usize) -> Result<Vec<f32>, std::io::Error> {
let mut data = vec![0f32; count];
let mut buf = vec![0u8; count * 4];
reader.read_exact(&mut buf)?;
for (i, chunk) in buf.chunks_exact(4).enumerate() {
data[i] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
Ok(data)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interp_idx_in_range() {
let table = BcCorrectionTable {
data: vec![1.0; 100],
bc_bins: vec![0.1, 0.2, 0.3, 0.4, 0.5],
mass_bins: vec![100.0, 150.0, 200.0],
length_bins: vec![1.0, 1.2, 1.4],
velocity_bins: vec![3000.0, 2500.0, 2000.0],
num_types: 2,
version: 1,
};
let (idx, weight) = table.interp_idx(0.25, &table.bc_bins);
assert_eq!(idx, 1);
assert!((weight - 0.5).abs() < 0.01);
let (idx, weight) = table.interp_idx(0.2, &table.bc_bins);
assert_eq!(idx, 0);
assert!((weight - 1.0).abs() < 0.01);
}
#[test]
fn test_interp_idx_out_of_range() {
let table = BcCorrectionTable {
data: vec![1.0; 100],
bc_bins: vec![0.1, 0.2, 0.3, 0.4, 0.5],
mass_bins: vec![100.0, 150.0, 200.0],
length_bins: vec![1.0, 1.2, 1.4],
velocity_bins: vec![3000.0, 2500.0, 2000.0],
num_types: 2,
version: 1,
};
let (idx, weight) = table.interp_idx(0.05, &table.bc_bins);
assert_eq!(idx, 0);
assert_eq!(weight, 0.0);
let (idx, weight) = table.interp_idx(0.6, &table.bc_bins);
assert_eq!(idx, 3); assert_eq!(weight, 1.0);
}
}