#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExonOffsetCorrection {
Delta(i64),
InsideGenomeOnlyGap,
}
const MAX_REALIGN_LEN: usize = 4096;
pub fn correct_genome_offset(
genome: &[u8],
tx: &[u8],
genome_offset: usize,
) -> Option<ExonOffsetCorrection> {
if genome == tx {
return None;
}
if genome.is_empty() || tx.is_empty() {
return None;
}
if genome.len() > MAX_REALIGN_LEN || tx.len() > MAX_REALIGN_LEN {
return None;
}
if genome_offset >= genome.len() {
return None;
}
let alignment = align(genome, tx);
correction_at(&alignment, genome_offset)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TxOffsetCorrection {
Delta(i64),
InsideTxOnlyGap,
}
pub fn correct_tx_offset(genome: &[u8], tx: &[u8], tx_offset: usize) -> Option<TxOffsetCorrection> {
if genome == tx {
return None;
}
if genome.is_empty() || tx.is_empty() {
return None;
}
if genome.len() > MAX_REALIGN_LEN || tx.len() > MAX_REALIGN_LEN {
return None;
}
if tx_offset >= tx.len() {
return None;
}
let alignment = align(genome, tx);
tx_correction_at(&alignment, tx_offset)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Col {
Match,
GenomeOnly,
TxOnly,
}
fn align(genome: &[u8], tx: &[u8]) -> Vec<Col> {
let n = genome.len();
let m = tx.len();
let width = m + 1;
let mut cost = vec![0u32; (n + 1) * width];
for i in 0..=n {
cost[i * width] = i as u32;
}
#[allow(clippy::needless_range_loop)]
for j in 0..=m {
cost[j] = j as u32;
}
for i in 1..=n {
for j in 1..=m {
let sub =
cost[(i - 1) * width + (j - 1)] + if genome[i - 1] == tx[j - 1] { 0 } else { 1 };
let del = cost[(i - 1) * width + j] + 1; let ins = cost[i * width + (j - 1)] + 1; cost[i * width + j] = sub.min(del).min(ins);
}
}
let mut path = Vec::with_capacity(n.max(m));
let (mut i, mut j) = (n, m);
while i > 0 || j > 0 {
if i > 0 && j > 0 {
let diag = cost[(i - 1) * width + (j - 1)];
let here = cost[i * width + j];
let matched = genome[i - 1] == tx[j - 1];
if (matched && diag == here) || (!matched && diag + 1 == here) {
path.push(Col::Match);
i -= 1;
j -= 1;
continue;
}
}
if i > 0 && cost[(i - 1) * width + j] + 1 == cost[i * width + j] {
path.push(Col::GenomeOnly);
i -= 1;
} else {
path.push(Col::TxOnly);
j -= 1;
}
}
path.reverse();
path
}
fn correction_at(path: &[Col], genome_offset: usize) -> Option<ExonOffsetCorrection> {
let mut g = 0usize; let mut t = 0usize; for &col in path {
match col {
Col::Match => {
if g == genome_offset {
return Some(ExonOffsetCorrection::Delta(t as i64 - g as i64));
}
g += 1;
t += 1;
}
Col::GenomeOnly => {
if g == genome_offset {
return Some(ExonOffsetCorrection::InsideGenomeOnlyGap);
}
g += 1;
}
Col::TxOnly => {
t += 1;
}
}
}
None
}
fn tx_correction_at(path: &[Col], tx_offset: usize) -> Option<TxOffsetCorrection> {
let mut g = 0usize;
let mut t = 0usize;
for &col in path {
match col {
Col::Match => {
if t == tx_offset {
return Some(TxOffsetCorrection::Delta(g as i64 - t as i64));
}
g += 1;
t += 1;
}
Col::TxOnly => {
if t == tx_offset {
return Some(TxOffsetCorrection::InsideTxOnlyGap);
}
t += 1;
}
Col::GenomeOnly => {
g += 1;
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn delta(genome: &str, tx: &str, off: usize) -> Option<ExonOffsetCorrection> {
correct_genome_offset(genome.as_bytes(), tx.as_bytes(), off)
}
#[test]
fn identical_sequences_need_no_correction() {
assert_eq!(delta("ACGTACGT", "ACGTACGT", 3), None);
}
#[test]
fn genome_extra_prefix_shifts_following_positions() {
let common = "GGTGCGCCGGTAGGGG";
let genome = format!("CA{common}");
let tx = common.to_string();
assert_eq!(
delta(&genome, &tx, 0),
Some(ExonOffsetCorrection::InsideGenomeOnlyGap)
);
assert_eq!(
delta(&genome, &tx, 1),
Some(ExonOffsetCorrection::InsideGenomeOnlyGap)
);
assert_eq!(
delta(&genome, &tx, 2),
Some(ExonOffsetCorrection::Delta(-2))
);
assert_eq!(
delta(&genome, &tx, 9),
Some(ExonOffsetCorrection::Delta(-2))
);
}
#[test]
fn tx_extra_suffix_shifts_positions_after_it() {
let genome = "ACGTACGT";
let tx = "ACGTXACGT"; assert_eq!(delta(genome, tx, 0), Some(ExonOffsetCorrection::Delta(0)));
assert_eq!(delta(genome, tx, 3), Some(ExonOffsetCorrection::Delta(0)));
assert_eq!(delta(genome, tx, 4), Some(ExonOffsetCorrection::Delta(1)));
assert_eq!(delta(genome, tx, 7), Some(ExonOffsetCorrection::Delta(1)));
}
#[test]
fn two_indels_reproducer_shape() {
let common = "GGTGCGCCGGTAGGGGACGCGCCGGCACAGCAA";
let genome = format!("CA{common}");
let tx = format!("{common}A");
let off = 2 + 10; assert_eq!(
delta(&genome, &tx, off),
Some(ExonOffsetCorrection::Delta(-2))
);
assert_eq!(
delta(&genome, &tx, 0),
Some(ExonOffsetCorrection::InsideGenomeOnlyGap)
);
}
#[test]
fn empty_slice_declines() {
assert_eq!(delta("", "ACGT", 0), None);
assert_eq!(delta("ACGT", "", 0), None);
}
#[test]
fn out_of_range_offset_declines() {
assert_eq!(delta("CAACGT", "ACGT", 99), None);
}
fn tx_delta(genome: &str, tx: &str, off: usize) -> Option<TxOffsetCorrection> {
correct_tx_offset(genome.as_bytes(), tx.as_bytes(), off)
}
#[test]
fn tx_correction_mirrors_genome_correction() {
let common = "GGTGCGCCGGTAGGGG";
let genome = format!("CA{common}");
let tx = common.to_string();
assert_eq!(
tx_delta(&genome, &tx, 0),
Some(TxOffsetCorrection::Delta(2))
);
assert_eq!(
tx_delta(&genome, &tx, 7),
Some(TxOffsetCorrection::Delta(2))
);
}
#[test]
fn tx_only_insertion_declines() {
let genome = "ACGTACGT";
let tx = "ACGTXACGT"; assert_eq!(
tx_delta(genome, tx, 4),
Some(TxOffsetCorrection::InsideTxOnlyGap)
);
assert_eq!(tx_delta(genome, tx, 3), Some(TxOffsetCorrection::Delta(0)));
assert_eq!(tx_delta(genome, tx, 5), Some(TxOffsetCorrection::Delta(-1)));
}
#[test]
fn forward_and_inverse_round_trip() {
let genome = "CAGGTGCGCCGGTAGGGGA";
let tx = "GGTGCGCCGGTAGGGGAT";
for g_off in 0..genome.len() {
if let Some(ExonOffsetCorrection::Delta(fwd)) =
correct_genome_offset(genome.as_bytes(), tx.as_bytes(), g_off)
{
let t_off = (g_off as i64 + fwd) as usize;
let inv = correct_tx_offset(genome.as_bytes(), tx.as_bytes(), t_off);
assert_eq!(
inv,
Some(TxOffsetCorrection::Delta(-fwd)),
"genome offset {g_off}: forward {fwd}, inverse mismatch"
);
}
}
}
#[test]
fn equal_length_single_substitution_needs_no_shift() {
let genome = "ACGTACGT";
let tx = "ACGAACGT"; assert_eq!(delta(genome, tx, 0), Some(ExonOffsetCorrection::Delta(0)));
assert_eq!(delta(genome, tx, 3), Some(ExonOffsetCorrection::Delta(0)));
assert_eq!(delta(genome, tx, 7), Some(ExonOffsetCorrection::Delta(0)));
}
}