pub mod illumina;
use rand::Rng;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReadEnd {
Read1,
Read2,
}
pub trait ErrorModel {
fn error_probability(&self, cycle: usize, read_end: ReadEnd) -> f64;
fn base_quality_phred33(&self, cycle: usize, read_end: ReadEnd) -> u8 {
let p_err = self.error_probability(cycle, read_end);
#[expect(clippy::cast_possible_truncation, reason = "clamped to [2, 41]")]
#[expect(clippy::cast_sign_loss, reason = "clamped to [2, 41]")]
if p_err > 0.0 {
(-10.0 * p_err.log10()).round().clamp(2.0, 41.0) as u8 + 33
} else {
41 + 33
}
}
}
pub fn apply_errors(
model: &impl ErrorModel,
bases: &mut [u8],
read_end: ReadEnd,
rng: &mut impl Rng,
) -> (usize, Vec<u8>) {
let mut qualities = Vec::with_capacity(bases.len());
let mut n_errors = 0;
for (cycle, base) in bases.iter_mut().enumerate() {
let p_err = model.error_probability(cycle, read_end);
if rng.random::<f64>() < p_err {
*base = random_different_base(*base, rng);
n_errors += 1;
}
let base_qual = model.base_quality_phred33(cycle, read_end);
let noise: f64 = rng.random_range(-2.0..2.0);
#[expect(clippy::cast_possible_truncation, reason = "clamped to [35, 74]")]
#[expect(clippy::cast_sign_loss, reason = "clamped to [35, 74]")]
let q_noisy = (f64::from(base_qual) + noise).round().clamp(35.0, 74.0) as u8;
qualities.push(q_noisy);
}
(n_errors, qualities)
}
fn random_different_base(base: u8, rng: &mut impl Rng) -> u8 {
const ALTS: [u8; 12] = [
b'C', b'G', b'T', b'A', b'G', b'T', b'A', b'C', b'T', b'A', b'C', b'G', ];
let row = match base {
b'A' => 0,
b'C' => 3,
b'G' => 6,
_ => 9, };
ALTS[row + rng.random_range(0..3usize)]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_random_different_base_never_same() {
let mut rng = rand::rng();
for _ in 0..1000 {
let original = b'A';
let result = random_different_base(original, &mut rng);
assert_ne!(result, original);
assert!(matches!(result, b'C' | b'G' | b'T'));
}
}
#[test]
fn test_random_different_base_all_bases() {
let mut rng = rand::rng();
for base in [b'A', b'C', b'G', b'T'] {
let result = random_different_base(base, &mut rng);
assert_ne!(result, base);
}
}
}