Skip to content

Commit faaf4a9

Browse files
Merge pull request #818 from ncsa/fix/497_bnd_warn
Stop warning that a breakend has no END/SVLEN (#497)
2 parents 21429e4 + 5a827e3 commit faaf4a9

2 files changed

Lines changed: 96 additions & 2 deletions

File tree

‎eidolon/src/gen_reads/utils/runner.rs‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3678,8 +3678,8 @@ fn alt_haplotype_window(
36783678
/// Builds a sorted, contiguous list of `(start, end, multiplier)` coverage
36793679
/// segments spanning `[0, block_end)`. Default multiplier is `1.0`; each
36803680
/// symbolic SV multiplies the multiplier in its span (overlapping SVs compose
3681-
/// multiplicatively). SVs without a usable span (no END / SVLEN) or with a
3682-
/// multiplier of `1.0` are skipped silently.
3681+
/// multiplicatively). Breakends and SVs with a multiplier of `1.0` are skipped; an
3682+
/// SV without a usable span (no END / SVLEN) is skipped with a warning.
36833683
fn build_coverage_multipliers(
36843684
sv_variants: &[Variant],
36853685
ploidy: usize,
@@ -3699,6 +3699,13 @@ fn build_coverage_multipliers(
36993699
// Convert the 0-based-stored location back to the VCF's 1-based POS
37003700
// so SvData::span() (which expects 1-based) returns the right count.
37013701
let pos_1based = v.location.saturating_add(1);
3702+
if sv.sv_type == SvType::Bnd {
3703+
// A breakend is a point (VCF 4.2 §5.4) with no copy-number change, so it has
3704+
// no span to modulate. Missing END/SVLEN is expected here, not a defect in the
3705+
// input, and the warning below is for SVs that should have one (#497).
3706+
debug!("Breakend at 1-based POS {pos_1based}: a point event, no coverage modulation");
3707+
continue;
3708+
}
37023709
let span_bases = match sv.span(pos_1based) {
37033710
Some(n) if n > 0 => n,
37043711
_ => {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//! A well-formed breakend has no END or SVLEN (VCF 4.2 §5.4: it is a point), so the
2+
//! coverage pass must not warn that one is missing. A symbolic SV that does need a span and
3+
//! lacks it must still warn (#497).
4+
//!
5+
//! The breakend case asserts the DEBUG line as well as the missing WARN: without it, a
6+
//! breakend that never reached the coverage pass would pass the test for the wrong reason.
7+
//! Both tests read the written log: the terminal logger sends WARN to stdout, not stderr.
8+
9+
mod common;
10+
use common::{GenReadsConfig, eidolon, fresh_workdir, h1n1_reference};
11+
use std::io::Write as _;
12+
13+
const MISSING_SPAN_WARN: &str = "has no END/SVLEN";
14+
const BND_DEBUG: &str = "Breakend at 1-based POS 600: a point event";
15+
16+
fn write_vcf(path: &std::path::Path, records: &[&str]) {
17+
let mut f = std::fs::File::create(path).unwrap();
18+
writeln!(f, "##fileformat=VCFv4.2").unwrap();
19+
writeln!(
20+
f,
21+
"##FORMAT=<ID=GT,Number=1,Type=String,Description=\"Genotype\">"
22+
)
23+
.unwrap();
24+
writeln!(
25+
f,
26+
"#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tS"
27+
)
28+
.unwrap();
29+
for r in records {
30+
writeln!(f, "{r}").unwrap();
31+
}
32+
}
33+
34+
/// Runs gen-reads on `records` at debug log level and returns the written log.
35+
fn run(records: &[&str]) -> String {
36+
let (_dir, work) = fresh_workdir();
37+
let vcf = work.join("in.vcf");
38+
write_vcf(&vcf, records);
39+
let mut config = GenReadsConfig::new(h1n1_reference(), work.clone(), "run");
40+
config.coverage = 5;
41+
config.produce_fastq = true;
42+
config.input_vcf = Some(vcf);
43+
config.sv_rate_scale = Some(0.0); // only the supplied records, no de novo SVs
44+
let yaml = config.write_yaml();
45+
let log = work.join("run.log");
46+
let out = eidolon()
47+
.args(["--log-level", "debug", "--log-dest"])
48+
.arg(&log)
49+
.args(["gen-reads", "-c"])
50+
.arg(yaml.path())
51+
.output()
52+
.unwrap();
53+
assert!(
54+
out.status.success(),
55+
"gen-reads failed: {}",
56+
String::from_utf8_lossy(&out.stderr)
57+
);
58+
std::fs::read_to_string(&log).unwrap()
59+
}
60+
61+
#[test]
62+
fn a_breakend_without_end_or_svlen_does_not_warn() {
63+
// REF bases are the actual reference at those positions.
64+
let log = run(&[
65+
"H1N1_HA\t600\t.\tT\tT[H1N1_PB2:900[\t60\tPASS\tSVTYPE=BND\tGT\t1/1",
66+
"H1N1_PB2\t900\t.\tG\t]H1N1_HA:600]G\t60\tPASS\tSVTYPE=BND\tGT\t1/1",
67+
]);
68+
assert!(
69+
log.contains(BND_DEBUG),
70+
"the breakend must reach the coverage pass and be skipped there:\n{log}"
71+
);
72+
assert!(
73+
!log.contains(MISSING_SPAN_WARN),
74+
"a breakend is not missing a span:\n{log}"
75+
);
76+
}
77+
78+
#[test]
79+
fn a_deletion_without_end_or_svlen_still_warns() {
80+
let log = run(&["H1N1_HA\t600\t.\tT\t<DEL>\t60\tPASS\tSVTYPE=DEL\tGT\t1/1"]);
81+
assert!(
82+
log.contains(&format!(
83+
"Symbolic SV at 1-based POS 600 {MISSING_SPAN_WARN}"
84+
)),
85+
"a <DEL> with no span is under-specified and must say so:\n{log}"
86+
);
87+
}

0 commit comments

Comments
 (0)