pub fn extract_flanked_region<'a>(
read: &'a [u8],
left_flank: &[u8],
right_flank: &[u8],
) -> Option<&'a [u8]> {
let seg_start = find_flank_end(read, left_flank)?;
let seg_end = find_right_flank_start(read, right_flank)?;
if seg_start >= seg_end {
return None;
}
Some(&read[seg_start..seg_end])
}
const GAP: i32 = -2;
fn find_flank_end(read: &[u8], flank: &[u8]) -> Option<usize> {
if flank.is_empty() {
return Some(0);
}
if read.len() < flank.len() {
return None;
}
let m = flank.len();
let n = read.len();
let cols = n + 1;
let mut dp = vec![0i32; (m + 1) * cols];
for i in 1..=m {
dp[i * cols] = GAP * i as i32;
}
for i in 1..=m {
for j in 1..=n {
let sub = if flank[i - 1] == read[j - 1] {
2i32
} else {
-1i32
};
let mat = dp[(i - 1) * cols + (j - 1)] + sub;
let del = dp[(i - 1) * cols + j] + GAP; let ins = dp[i * cols + (j - 1)] + GAP; dp[i * cols + j] = mat.max(del).max(ins);
}
}
let mut best_j = 1;
let mut best_score = dp[m * cols + 1];
for j in 2..=n {
let s = dp[m * cols + j];
if s > best_score {
best_score = s;
best_j = j;
}
}
if best_score < flank.len() as i32 {
return None;
}
Some(best_j)
}
fn find_right_flank_start(read: &[u8], right_flank: &[u8]) -> Option<usize> {
if right_flank.is_empty() {
return Some(read.len());
}
let rev_read: Vec<u8> = read.iter().rev().copied().collect();
let rev_flank: Vec<u8> = right_flank.iter().rev().copied().collect();
let end_in_rev = find_flank_end(&rev_read, &rev_flank)?;
Some(read.len() - end_in_rev)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_flanks() {
let read = b"GGGGGCATCATCATCATAAAAA";
let seg =
extract_flanked_region(read, b"GGGGG", b"AAAAA").expect("should find both flanks");
assert_eq!(seg, b"CATCATCATCAT");
}
#[test]
fn single_base_flanks() {
let read = b"GCATCATCATA";
let seg = extract_flanked_region(read, b"G", b"A").expect("single-base flanks");
assert_eq!(seg, b"CATCATCAT");
}
#[test]
fn no_left_flank_returns_none() {
let read = b"GGGGGCATCATCATCATAAAAA";
assert!(extract_flanked_region(read, b"TTTTT", b"AAAAA").is_none());
}
#[test]
fn no_right_flank_returns_none() {
let read = b"GGGGGCATCATCATCATAAAAA";
assert!(extract_flanked_region(read, b"GGGGG", b"TTTTT").is_none());
}
#[test]
fn read_shorter_than_flank_returns_none() {
assert!(extract_flanked_region(b"ACG", b"GGGGG", b"AAAAA").is_none());
}
#[test]
fn noisy_flanks() {
let read = b"GGGAGCATCATCATCATAAATA";
let seg = extract_flanked_region(read, b"GGGGG", b"AAAAA")
.expect("noisy flanks should still align");
assert_eq!(seg, b"CATCATCATCAT");
}
#[test]
fn overlapping_flanks_returns_none() {
let read = b"GGGGGAAAAA";
assert!(extract_flanked_region(read, b"GGGGGAAAAA", b"GGGGGAAAAA").is_none());
}
#[test]
fn empty_repeat_segment_returns_none() {
let read = b"GGGGGAAAAA";
assert!(extract_flanked_region(read, b"GGGGG", b"AAAAA").is_none());
}
#[test]
fn longer_read_with_flanked_str() {
let read = b"ATCGATCGATCATCATCATCATCATTAGCTAGCTA";
let seg = extract_flanked_region(read, b"ATCGATCGAT", b"TAGCTAGCTA")
.expect("10-bp flanks + 15-bp repeat");
assert_eq!(seg, b"CATCATCATCATCAT");
}
#[test]
fn short_5bp_flank_fails_at_two_mismatches() {
let read = b"GAGAGCATCATCATCATAAAAA"; assert!(
extract_flanked_region(read, b"GGGGG", b"AAAAA").is_none(),
"5 bp flank with 2 mismatches should fail the score threshold"
);
}
#[test]
fn medium_10bp_flank_survives_two_mismatches() {
let read = b"ATCTATCGAACATCATCATCATCATTAGCTAGCTA";
let seg = extract_flanked_region(read, b"ATCGATCGAT", b"TAGCTAGCTA")
.expect("10 bp flank should survive 2 mismatches");
assert_eq!(seg, b"CATCATCATCATCAT");
}
#[test]
fn medium_10bp_flank_fails_at_four_mismatches() {
let read = b"TGTAAAACGTCAGCAGCAGCAGCAGCCTTAAGGCC";
assert!(
extract_flanked_region(read, b"GGTTAACCGG", b"CCTTAAGGCC").is_none(),
"10 bp flank with 4 mismatches should fail"
);
}
#[test]
fn long_20bp_flank_survives_six_mismatches() {
let lf = b"AATTCCGGAATTCCGGAATT";
let rf = b"TTAAGGCCTTAAGGCCTTAA";
let repeat = b"CAGCAGCAG";
let mut lf_noisy = *lf;
lf_noisy[0] ^= 1; lf_noisy[3] ^= 1;
lf_noisy[7] ^= 1;
lf_noisy[11] ^= 1;
lf_noisy[14] ^= 1;
lf_noisy[18] ^= 1;
let mut read2: Vec<u8> = Vec::new();
read2.extend_from_slice(&lf_noisy);
read2.extend_from_slice(repeat);
read2.extend_from_slice(rf);
let mismatches = lf_noisy
.iter()
.zip(lf.iter())
.filter(|(a, b)| a != b)
.count();
assert_eq!(
mismatches, 6,
"test setup: expected 6 mismatches in noisy flank"
);
let seg = extract_flanked_region(&read2, lf, rf)
.expect("20 bp flank should survive 6 mismatches (30% error)");
assert_eq!(seg, repeat.as_ref());
}
#[test]
fn long_20bp_flank_fails_at_seven_mismatches() {
let lf = b"AATTCCGGAATTCCGGAATT";
let rf = b"TTAAGGCCTTAAGGCCTTAA";
let mut lf_noisy = *lf;
for pos in [0, 3, 7, 11, 14, 17, 19] {
lf_noisy[pos] ^= 1;
}
let mut read: Vec<u8> = Vec::new();
read.extend_from_slice(&lf_noisy);
read.extend_from_slice(b"CAGCAGCAG");
read.extend_from_slice(rf);
assert!(
extract_flanked_region(&read, lf, rf).is_none(),
"20 bp flank with 7 mismatches should fail"
);
}
#[test]
fn partial_read_5bp_captured_passes_5bp_query() {
let read = b"GGGGGCATCATCATCATAAAAA";
let seg = extract_flanked_region(read, b"GGGGG", b"AAAAA")
.expect("5 bp query matches 5 bp captured flank");
assert_eq!(seg, b"CATCATCATCAT");
}
#[test]
fn partial_read_5bp_captured_fails_20bp_query() {
let read = b"GGGGGCATCATCATCATAAAAA";
assert!(
extract_flanked_region(read, b"AAAAAAAAAAAAAAAAGGGGG", b"AAAAA").is_none(),
"20 bp query must fail when read only has 5 bp of captured flank"
);
}
#[test]
fn partial_read_10bp_captured_passes_10bp_query_with_one_error() {
let read_1err = b"ATCGTTCGATCATCATCATCATCATTAGCTAGCTA"; let seg = extract_flanked_region(read_1err, b"ATCGATCGAT", b"TAGCTAGCTA")
.expect("10 bp query should survive 1 mismatch");
assert_eq!(seg, b"CATCATCATCATCAT");
}
#[test]
fn flank_ending_in_repeat_unit_anchors_correctly_with_unique_prefix() {
let read = b"GGGGGCATCATCATCATGGGGG"; let seg = extract_flanked_region(read, b"GGGGGCAT", b"GGGGG")
.expect("unique prefix keeps anchor at correct boundary");
assert_eq!(seg, b"CATCATCAT");
}
#[test]
fn flank_query_identical_to_repeat_unit_anchors_at_first_occurrence() {
let read = b"CATCATCATCATGGG";
let seg = extract_flanked_region(read, b"CAT", b"GGG")
.expect("degenerate flank returns something");
assert_eq!(
seg.len() % 3,
0,
"result is still a whole number of CAT units"
);
assert!(
seg.len() < 12,
"but it is shorter than the full repeat (known anchor drift)"
);
}
#[test]
fn cag_repeat_10bp_flanks_one_error_each() {
let left_true = b"ATCGATCGAT"; let right_true = b"TAGCTAGCTA"; let repeat = b"CAGCAGCAGCAGCAGCAGCAG"; let left_noisy = b"ATCGATCGTT"; let right_noisy = b"TAGCTAGCCA"; let read: Vec<u8> = [left_noisy.as_ref(), repeat, right_noisy].concat();
let seg = extract_flanked_region(&read, left_true, right_true)
.expect("10 bp flanks + 1 error each should pass for HiFi");
assert_eq!(seg, repeat.as_ref());
}
#[test]
fn gaa_repeat_frda_like_20bp_flanks_with_noise() {
let left_true = b"TTCCTGCAGTTCCTGCAGAA"; let right_true = b"TTCTTGCAGTTCTTGCAGTT"; let repeat = b"GAAGAAGAAGAAGAAGAAGAA"; let mut left_noisy = *left_true;
left_noisy[2] ^= 2; left_noisy[9] ^= 2;
let read: Vec<u8> = [left_noisy.as_ref(), repeat, right_true.as_ref()].concat();
let seg = extract_flanked_region(&read, left_true, right_true)
.expect("20 bp flank ending in repeat unit + 2 errors should pass");
assert_eq!(seg, repeat.as_ref());
}
#[test]
fn ont_r10_worst_case_20bp_flank_passes() {
let lf = b"AATTCCGGAATTCCGGAATT";
let rf = b"TTAAGGCCTTAAGGCCTTAA";
let mut lf_noisy = *lf;
for pos in [1, 4, 8, 12, 15, 18] {
lf_noisy[pos] ^= 1;
}
let mismatches = lf_noisy
.iter()
.zip(lf.iter())
.filter(|(a, b)| a != b)
.count();
assert_eq!(mismatches, 6);
let mut read: Vec<u8> = Vec::new();
read.extend_from_slice(&lf_noisy);
read.extend_from_slice(b"CAGCAGCAGCAGCAG");
read.extend_from_slice(rf);
let seg = extract_flanked_region(&read, lf, rf)
.expect("20 bp flank survives 6 mismatches (30% = ONT worst-case)");
assert_eq!(seg, b"CAGCAGCAGCAGCAG");
}
}