extern crate statrs;
use rayon::prelude::*;
use rayon::ThreadPoolBuilder;
use std::collections::{BTreeMap, HashMap, HashSet};
use crate::chemistry::constants::{MASS_NEUTRON, MASS_PROTON};
use crate::chemistry::elements::{atoms_isotopic_weights, isotopic_abundance};
use crate::data::peptide::PeptideIon;
use crate::data::spectrum::MzSpectrum;
use crate::data::spectrum::ToResolution;
use statrs::distribution::{Continuous, Normal};
pub fn convolve(
dist_a: &Vec<(f64, f64)>,
dist_b: &Vec<(f64, f64)>,
mass_tolerance: f64,
abundance_threshold: f64,
max_results: usize,
) -> Vec<(f64, f64)> {
let mut result: Vec<(f64, f64)> = Vec::new();
for (mass_a, abundance_a) in dist_a {
for (mass_b, abundance_b) in dist_b {
let combined_mass = mass_a + mass_b;
let combined_abundance = abundance_a * abundance_b;
if combined_abundance < abundance_threshold {
continue;
}
if let Some(entry) = result
.iter_mut()
.find(|(m, _)| (*m - combined_mass).abs() < mass_tolerance)
{
entry.1 += combined_abundance;
} else {
result.push((combined_mass, combined_abundance));
}
}
}
result.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
if result.len() > max_results {
result.truncate(max_results);
}
result.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
result
}
pub fn convolve_pow(dist: &Vec<(f64, f64)>, n: i32) -> Vec<(f64, f64)> {
if n == 0 {
return vec![(0.0, 1.0)]; }
if n == 1 {
return dist.clone();
}
let mut result = dist.clone();
let mut power = 2;
while power <= n {
result = convolve(&result, &result, 1e-6, 1e-12, 200); power *= 2;
}
if power / 2 < n {
result = convolve(
&result,
&convolve_pow(dist, n - power / 2),
1e-6,
1e-12,
200,
);
}
result
}
pub fn generate_isotope_distribution(
atomic_composition: &HashMap<String, i32>,
mass_tolerance: f64,
abundance_threshold: f64,
max_result: i32,
) -> Vec<(f64, f64)> {
let mut cumulative_distribution: Option<Vec<(f64, f64)>> = None;
let atoms_isotopic_weights: HashMap<String, Vec<f64>> = atoms_isotopic_weights()
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
let atomic_isotope_abundance: HashMap<String, Vec<f64>> = isotopic_abundance()
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect();
for (element, &count) in atomic_composition.iter() {
let elemental_isotope_weights = atoms_isotopic_weights
.get(element)
.expect("Element not found in isotopic weights table")
.clone();
let elemental_isotope_abundance = atomic_isotope_abundance
.get(element)
.expect("Element not found in isotopic abundance table")
.clone();
let element_distribution: Vec<(f64, f64)> = elemental_isotope_weights
.iter()
.zip(elemental_isotope_abundance.iter())
.map(|(&mass, &abundance)| (mass, abundance))
.collect();
let element_power_distribution = if count > 1 {
convolve_pow(&element_distribution, count)
} else {
element_distribution
};
cumulative_distribution = match cumulative_distribution {
Some(cum_dist) => Some(convolve(
&cum_dist,
&element_power_distribution,
mass_tolerance,
abundance_threshold,
max_result as usize,
)),
None => Some(element_power_distribution),
};
}
let final_distribution = cumulative_distribution.expect("Peptide has no elements");
let total_abundance: f64 = final_distribution
.iter()
.map(|&(_, abundance)| abundance)
.sum();
let result: Vec<_> = final_distribution
.into_iter()
.map(|(mass, abundance)| (mass, abundance / total_abundance))
.collect();
let mut sort_map: BTreeMap<i64, f64> = BTreeMap::new();
let quantize = |mz: f64| -> i64 { (mz * 1_000_000.0).round() as i64 };
for (mz, intensity) in result {
let key = quantize(mz);
sort_map
.entry(key)
.and_modify(|e| *e += intensity)
.or_insert(intensity);
}
let mz: Vec<f64> = sort_map
.keys()
.map(|&key| key as f64 / 1_000_000.0)
.collect();
let intensity: Vec<f64> = sort_map.values().map(|&intensity| intensity).collect();
mz.iter()
.zip(intensity.iter())
.map(|(&mz, &intensity)| (mz, intensity))
.collect()
}
pub fn normal_pdf(x: f64, mean: f64, std_dev: f64) -> f64 {
let normal = Normal::new(mean, std_dev).unwrap();
normal.pdf(x)
}
pub fn factorial(n: i32) -> f64 {
(1..=n).fold(1.0, |acc, x| acc * x as f64)
}
pub fn weight(mass: f64, peak_nums: Vec<i32>, normalize: bool) -> Vec<f64> {
let lam_val = lam(mass, 0.000594, -0.03091);
let factorials: Vec<f64> = peak_nums.iter().map(|&k| factorial(k)).collect();
let mut weights: Vec<f64> = peak_nums
.iter()
.map(|&k| {
let pow = lam_val.powi(k);
let exp = (-lam_val).exp();
exp * pow / factorials[k as usize]
})
.collect();
if normalize {
let sum: f64 = weights.iter().sum();
weights = weights.iter().map(|&w| w / sum).collect();
}
weights
}
pub fn lam(mass: f64, slope: f64, intercept: f64) -> f64 {
slope * mass + intercept
}
pub fn iso(
x: &Vec<f64>,
mass: f64,
charge: f64,
sigma: f64,
amp: f64,
k: usize,
step_size: f64,
) -> Vec<f64> {
let k_range: Vec<usize> = (0..k).collect();
let means: Vec<f64> = k_range
.iter()
.map(|&k_val| (mass + MASS_NEUTRON * k_val as f64) / charge)
.collect();
let weights = weight(
mass,
k_range
.iter()
.map(|&k_val| k_val as i32)
.collect::<Vec<i32>>(),
true,
);
let mut intensities = vec![0.0; x.len()];
for (i, x_val) in x.iter().enumerate() {
for (j, &mean) in means.iter().enumerate() {
intensities[i] += weights[j] * normal_pdf(*x_val, mean, sigma);
}
intensities[i] *= step_size;
}
intensities
.iter()
.map(|&intensity| intensity * amp)
.collect()
}
pub fn generate_isotope_pattern(
lower_bound: f64,
upper_bound: f64,
mass: f64,
charge: f64,
amp: f64,
k: usize,
sigma: f64,
resolution: i32,
) -> (Vec<f64>, Vec<f64>) {
let step_size = f64::min(sigma / 10.0, 1.0 / 10f64.powi(resolution));
let size = ((upper_bound - lower_bound) / step_size).ceil() as usize;
let mzs: Vec<f64> = (0..size)
.map(|i| lower_bound + step_size * i as f64)
.collect();
let intensities = iso(&mzs, mass, charge, sigma, amp, k, step_size);
(
mzs.iter().map(|&mz| mz + MASS_PROTON).collect(),
intensities,
)
}
pub fn generate_averagine_spectrum(
mass: f64,
charge: i32,
min_intensity: i32,
k: i32,
resolution: i32,
centroid: bool,
amp: Option<f64>,
) -> MzSpectrum {
let amp = amp.unwrap_or(1e4);
let lb = mass / charge as f64 - 0.2;
let ub = mass / charge as f64 + k as f64 + 0.2;
let (mz, intensities) = generate_isotope_pattern(
lb,
ub,
mass,
charge as f64,
amp,
k as usize,
0.008492569002123142,
resolution,
);
let spectrum = MzSpectrum::new(mz, intensities)
.to_resolution(resolution)
.filter_ranged(lb, ub, min_intensity as f64, 1e9);
if centroid {
spectrum.to_centroid(
std::cmp::max(min_intensity, 1),
1.0 / 10f64.powi(resolution - 1),
true,
)
} else {
spectrum
}
}
pub fn generate_averagine_spectra(
masses: Vec<f64>,
charges: Vec<i32>,
min_intensity: i32,
k: i32,
resolution: i32,
centroid: bool,
num_threads: usize,
amp: Option<f64>,
) -> Vec<MzSpectrum> {
let amp = amp.unwrap_or(1e5);
let mut spectra: Vec<MzSpectrum> = Vec::new();
let thread_pool = ThreadPoolBuilder::new()
.num_threads(num_threads)
.build()
.unwrap();
thread_pool.install(|| {
spectra = masses
.par_iter()
.zip(charges.par_iter())
.map(|(&mass, &charge)| {
generate_averagine_spectrum(
mass,
charge,
min_intensity,
k,
resolution,
centroid,
Some(amp),
)
})
.collect();
});
spectra
}
pub fn generate_precursor_spectrum(
sequence: &str,
charge: i32,
peptide_id: Option<i32>,
) -> MzSpectrum {
let peptide_ion = PeptideIon::new(sequence.to_string(), charge, 1.0, peptide_id);
peptide_ion.calculate_isotopic_spectrum(1e-3, 1e-9, 200, 1e-6)
}
pub fn generate_precursor_spectra(
sequences: &Vec<&str>,
charges: &Vec<i32>,
num_threads: usize,
peptide_ids: Vec<Option<i32>>,
) -> Vec<MzSpectrum> {
let thread_pool = ThreadPoolBuilder::new()
.num_threads(num_threads)
.build()
.unwrap();
let result = thread_pool.install(|| {
sequences
.par_iter()
.zip(charges.par_iter())
.zip(peptide_ids.par_iter())
.map(|((&sequence, &charge), &peptide_id)| {
generate_precursor_spectrum(sequence, charge, peptide_id)
})
.collect()
});
result
}
#[derive(Debug, Clone)]
pub struct TransmissionDependentIsotopeDistribution {
pub distribution: Vec<(f64, f64)>,
pub transmission_factor: f64,
}
pub fn calculate_transmission_dependent_fragment_ion_isotope_distribution(
fragment_isotope_dist: &Vec<(f64, f64)>,
comp_fragment_isotope_dist: &Vec<(f64, f64)>,
precursor_isotopes: &HashSet<usize>,
max_isotope: usize,
) -> Vec<(f64, f64)> {
if fragment_isotope_dist.is_empty() || comp_fragment_isotope_dist.is_empty() {
return Vec::new();
}
let mut r_max = fragment_isotope_dist.len();
if max_isotope != 0 && r_max > max_isotope {
r_max = max_isotope;
}
let mut result = (0..r_max)
.map(|i| (fragment_isotope_dist[0].0 + i as f64, 0.0))
.collect::<Vec<(f64, f64)>>();
for (i, &(_mz, intensity)) in fragment_isotope_dist.iter().enumerate().take(r_max) {
for &precursor in precursor_isotopes {
if precursor >= i && (precursor - i) < comp_fragment_isotope_dist.len() {
let comp_intensity = comp_fragment_isotope_dist[precursor - i].1;
result[i].1 += comp_intensity;
}
}
result[i].1 *= intensity;
}
result
}
pub fn calculate_transmission_dependent_distribution_with_factor(
fragment_isotope_dist: &Vec<(f64, f64)>,
comp_fragment_isotope_dist: &Vec<(f64, f64)>,
precursor_isotopes: &HashSet<usize>,
max_isotope: usize,
) -> TransmissionDependentIsotopeDistribution {
if fragment_isotope_dist.is_empty() || comp_fragment_isotope_dist.is_empty() {
return TransmissionDependentIsotopeDistribution {
distribution: Vec::new(),
transmission_factor: 0.0,
};
}
let all_isotopes: HashSet<usize> = (0..fragment_isotope_dist.len().max(comp_fragment_isotope_dist.len())).collect();
let full_distribution = calculate_transmission_dependent_fragment_ion_isotope_distribution(
fragment_isotope_dist,
comp_fragment_isotope_dist,
&all_isotopes,
max_isotope,
);
let full_sum: f64 = full_distribution.iter().map(|(_, i)| i).sum();
let transmitted_distribution = calculate_transmission_dependent_fragment_ion_isotope_distribution(
fragment_isotope_dist,
comp_fragment_isotope_dist,
precursor_isotopes,
max_isotope,
);
let transmitted_sum: f64 = transmitted_distribution.iter().map(|(_, i)| i).sum();
let transmission_factor = if full_sum > 0.0 {
transmitted_sum / full_sum
} else {
0.0
};
TransmissionDependentIsotopeDistribution {
distribution: transmitted_distribution,
transmission_factor,
}
}
pub fn calculate_precursor_transmission_factor(
precursor_isotope_dist: &[(f64, f64)],
transmitted_indices: &HashSet<usize>,
) -> f64 {
if precursor_isotope_dist.is_empty() || transmitted_indices.is_empty() {
return 0.0;
}
let total_intensity: f64 = precursor_isotope_dist.iter().map(|(_, i)| i).sum();
if total_intensity <= 0.0 {
return 0.0;
}
let transmitted_intensity: f64 = precursor_isotope_dist
.iter()
.enumerate()
.filter(|(idx, _)| transmitted_indices.contains(idx))
.map(|(_, (_, i))| i)
.sum();
transmitted_intensity / total_intensity
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, epsilon: f64) -> bool {
(a - b).abs() < epsilon
}
#[test]
fn test_transmission_all_isotopes() {
let fragment_dist = vec![
(500.0, 0.6),
(501.0, 0.3),
(502.0, 0.1),
];
let comp_dist = vec![
(800.0, 0.7),
(801.0, 0.2),
(802.0, 0.1),
];
let all_isotopes: HashSet<usize> = (0..3).collect();
let result = calculate_transmission_dependent_distribution_with_factor(
&fragment_dist,
&comp_dist,
&all_isotopes,
0,
);
assert!(
approx_eq(result.transmission_factor, 1.0, 0.001),
"Transmission factor should be 1.0 when same isotopes as reference transmitted, got {}",
result.transmission_factor
);
assert!(!result.distribution.is_empty());
}
#[test]
fn test_transmission_extra_isotopes() {
let fragment_dist = vec![
(500.0, 0.6),
(501.0, 0.3),
(502.0, 0.1),
];
let comp_dist = vec![
(800.0, 0.7),
(801.0, 0.2),
(802.0, 0.1),
];
let extra_isotopes: HashSet<usize> = [0, 1, 2, 3, 4, 5].iter().cloned().collect();
let result = calculate_transmission_dependent_distribution_with_factor(
&fragment_dist,
&comp_dist,
&extra_isotopes,
0,
);
assert!(
result.transmission_factor >= 1.0,
"Extra isotopes should give transmission factor >= 1.0, got {}",
result.transmission_factor
);
}
#[test]
fn test_transmission_partial_isotopes() {
let fragment_dist = vec![
(500.0, 0.5),
(501.0, 0.3),
(502.0, 0.15),
(503.0, 0.05),
];
let comp_dist = vec![
(800.0, 0.6),
(801.0, 0.25),
(802.0, 0.1),
(803.0, 0.05),
];
let all_isotopes: HashSet<usize> = [0, 1, 2, 3].iter().cloned().collect();
let full_result = calculate_transmission_dependent_distribution_with_factor(
&fragment_dist,
&comp_dist,
&all_isotopes,
0,
);
let partial_isotopes: HashSet<usize> = [0, 1].iter().cloned().collect();
let partial_result = calculate_transmission_dependent_distribution_with_factor(
&fragment_dist,
&comp_dist,
&partial_isotopes,
0,
);
assert!(
partial_result.transmission_factor < full_result.transmission_factor,
"Partial transmission ({}) should be less than full transmission ({})",
partial_result.transmission_factor,
full_result.transmission_factor
);
assert!(
partial_result.transmission_factor > 0.0 && partial_result.transmission_factor < 1.0,
"Partial transmission factor should be between 0 and 1, got {}",
partial_result.transmission_factor
);
}
#[test]
fn test_transmission_m0_only() {
let fragment_dist = vec![
(500.0, 0.5),
(501.0, 0.3),
(502.0, 0.15),
(503.0, 0.05),
];
let comp_dist = vec![
(800.0, 0.6),
(801.0, 0.25),
(802.0, 0.1),
(803.0, 0.05),
];
let m0_only: HashSet<usize> = [0].iter().cloned().collect();
let m0_result = calculate_transmission_dependent_distribution_with_factor(
&fragment_dist,
&comp_dist,
&m0_only,
0,
);
let m0_m1: HashSet<usize> = [0, 1].iter().cloned().collect();
let m0_m1_result = calculate_transmission_dependent_distribution_with_factor(
&fragment_dist,
&comp_dist,
&m0_m1,
0,
);
assert!(
m0_result.transmission_factor < m0_m1_result.transmission_factor,
"M0 only ({}) should transmit less than M0+M1 ({})",
m0_result.transmission_factor,
m0_m1_result.transmission_factor
);
let m0_intensity = m0_result.distribution.get(0).map(|(_, i)| *i).unwrap_or(0.0);
let m1_intensity = m0_result.distribution.get(1).map(|(_, i)| *i).unwrap_or(0.0);
assert!(
m0_intensity > 0.0,
"M0 fragment should have intensity when M0 precursor transmitted"
);
assert!(
approx_eq(m1_intensity, 0.0, 1e-10),
"M1 fragment should have zero intensity when only M0 precursor transmitted, got {}",
m1_intensity
);
}
#[test]
fn test_transmission_empty_inputs() {
let empty: Vec<(f64, f64)> = vec![];
let non_empty = vec![(500.0, 0.5)];
let isotopes: HashSet<usize> = [0].iter().cloned().collect();
let result1 = calculate_transmission_dependent_distribution_with_factor(
&empty,
&non_empty,
&isotopes,
0,
);
assert!(result1.distribution.is_empty());
assert!(approx_eq(result1.transmission_factor, 0.0, 1e-10));
let result2 = calculate_transmission_dependent_distribution_with_factor(
&non_empty,
&empty,
&isotopes,
0,
);
assert!(result2.distribution.is_empty());
assert!(approx_eq(result2.transmission_factor, 0.0, 1e-10));
}
#[test]
fn test_isotope_pattern_shift() {
let fragment_dist = vec![
(500.0, 0.6),
(501.0, 0.3),
(502.0, 0.1),
];
let comp_dist = vec![
(800.0, 0.7),
(801.0, 0.2),
(802.0, 0.1),
];
let all_isotopes: HashSet<usize> = [0, 1, 2].iter().cloned().collect();
let full_result = calculate_transmission_dependent_distribution_with_factor(
&fragment_dist,
&comp_dist,
&all_isotopes,
0,
);
let m0_only: HashSet<usize> = [0].iter().cloned().collect();
let m0_result = calculate_transmission_dependent_distribution_with_factor(
&fragment_dist,
&comp_dist,
&m0_only,
0,
);
let full_sum: f64 = full_result.distribution.iter().map(|(_, i)| i).sum();
let m0_sum: f64 = m0_result.distribution.iter().map(|(_, i)| i).sum();
let full_m0_fraction = if full_sum > 0.0 {
full_result.distribution[0].1 / full_sum
} else {
0.0
};
let m0_m0_fraction = if m0_sum > 0.0 {
m0_result.distribution[0].1 / m0_sum
} else {
0.0
};
assert!(
m0_m0_fraction >= full_m0_fraction,
"M0 fraction with M0-only transmission ({}) should be >= full transmission ({})",
m0_m0_fraction,
full_m0_fraction
);
}
#[test]
fn test_max_isotope_limit() {
let fragment_dist = vec![
(500.0, 0.5),
(501.0, 0.3),
(502.0, 0.15),
(503.0, 0.05),
];
let comp_dist = vec![
(800.0, 0.6),
(801.0, 0.25),
(802.0, 0.1),
(803.0, 0.05),
];
let all_isotopes: HashSet<usize> = [0, 1, 2, 3].iter().cloned().collect();
let result = calculate_transmission_dependent_distribution_with_factor(
&fragment_dist,
&comp_dist,
&all_isotopes,
2,
);
assert_eq!(
result.distribution.len(),
2,
"Distribution should be limited to 2 isotopes, got {}",
result.distribution.len()
);
}
#[test]
fn test_frame_building_intensity_scaling() {
let fragment_dist = vec![
(500.25, 0.65),
(501.25, 0.25),
(502.25, 0.08),
(503.25, 0.02),
];
let comp_dist = vec![
(800.40, 0.55),
(801.40, 0.28),
(802.40, 0.12),
(803.40, 0.05),
];
let fraction_events: f64 = 1000.0;
let all_isotopes: HashSet<usize> = (0..4).collect();
let full_dist = calculate_transmission_dependent_fragment_ion_isotope_distribution(
&fragment_dist,
&comp_dist,
&all_isotopes,
0,
);
let full_intensity_sum: f64 = full_dist.iter().map(|(_, i)| i * fraction_events).sum();
let m0_only: HashSet<usize> = [0].iter().cloned().collect();
let m0_dist = calculate_transmission_dependent_fragment_ion_isotope_distribution(
&fragment_dist,
&comp_dist,
&m0_only,
0,
);
let m0_intensity_sum: f64 = m0_dist.iter().map(|(_, i)| i * fraction_events).sum();
let m0_m1: HashSet<usize> = [0, 1].iter().cloned().collect();
let m0_m1_dist = calculate_transmission_dependent_fragment_ion_isotope_distribution(
&fragment_dist,
&comp_dist,
&m0_m1,
0,
);
let m0_m1_intensity_sum: f64 = m0_m1_dist.iter().map(|(_, i)| i * fraction_events).sum();
assert!(
full_intensity_sum > m0_m1_intensity_sum,
"Full transmission intensity ({}) should be > M0+M1 ({})",
full_intensity_sum, m0_m1_intensity_sum
);
assert!(
m0_m1_intensity_sum > m0_intensity_sum,
"M0+M1 transmission intensity ({}) should be > M0 only ({})",
m0_m1_intensity_sum, m0_intensity_sum
);
let m0_m1_ratio = m0_m1_intensity_sum / full_intensity_sum;
assert!(
m0_m1_ratio > 0.5 && m0_m1_ratio < 1.0,
"M0+M1 ratio ({}) should be between 0.5 and 1.0",
m0_m1_ratio
);
let m0_ratio = m0_intensity_sum / full_intensity_sum;
assert!(
m0_ratio > 0.2 && m0_ratio < 0.8,
"M0 ratio ({}) should be between 0.2 and 0.8",
m0_ratio
);
let factor_result = calculate_transmission_dependent_distribution_with_factor(
&fragment_dist,
&comp_dist,
&m0_m1,
0,
);
let manual_factor = m0_m1_intensity_sum / full_intensity_sum;
assert!(
approx_eq(factor_result.transmission_factor, manual_factor, 0.001),
"Transmission factor ({}) should match manual calculation ({})",
factor_result.transmission_factor, manual_factor
);
println!("Frame building intensity test results:");
println!(" Full transmission intensity: {:.2}", full_intensity_sum);
println!(" M0+M1 transmission intensity: {:.2} (ratio: {:.3})", m0_m1_intensity_sum, m0_m1_ratio);
println!(" M0 only transmission intensity: {:.2} (ratio: {:.3})", m0_intensity_sum, m0_ratio);
println!(" Transmission factor (M0+M1): {:.4}", factor_result.transmission_factor);
}
#[test]
fn test_realistic_peptide_transmission() {
let b_ion_dist = vec![
(600.30, 0.58),
(601.30, 0.28),
(602.30, 0.10),
(603.30, 0.03),
(604.30, 0.01),
];
let comp_dist = vec![
(900.45, 0.48),
(901.45, 0.30),
(902.45, 0.14),
(903.45, 0.06),
(904.45, 0.02),
];
let wide_window: HashSet<usize> = [0, 1, 2].iter().cloned().collect();
let wide_result = calculate_transmission_dependent_distribution_with_factor(
&b_ion_dist,
&comp_dist,
&wide_window,
0,
);
let narrow_window: HashSet<usize> = [0, 1].iter().cloned().collect();
let narrow_result = calculate_transmission_dependent_distribution_with_factor(
&b_ion_dist,
&comp_dist,
&narrow_window,
0,
);
let very_narrow: HashSet<usize> = [0].iter().cloned().collect();
let very_narrow_result = calculate_transmission_dependent_distribution_with_factor(
&b_ion_dist,
&comp_dist,
&very_narrow,
0,
);
assert!(
wide_result.transmission_factor > narrow_result.transmission_factor,
"Wide window should have higher transmission"
);
assert!(
narrow_result.transmission_factor > very_narrow_result.transmission_factor,
"Narrow window should have higher transmission than very narrow"
);
assert!(wide_result.transmission_factor > 0.0 && wide_result.transmission_factor <= 1.0);
assert!(narrow_result.transmission_factor > 0.0 && narrow_result.transmission_factor <= 1.0);
assert!(very_narrow_result.transmission_factor > 0.0 && very_narrow_result.transmission_factor <= 1.0);
println!("Realistic peptide transmission test results:");
println!(" Wide window (M0-M2) transmission factor: {:.4}", wide_result.transmission_factor);
println!(" Narrow window (M0-M1) transmission factor: {:.4}", narrow_result.transmission_factor);
println!(" Very narrow (M0 only) transmission factor: {:.4}", very_narrow_result.transmission_factor);
}
}