Skip to content

Commit aed9f5a

Browse files
committed
fix: handle non-canonicalizable breakpoint when both breakends at same position
When both breakends are at the same genomic position with leftPositive=false and rightPositive=true, calling reversed() produces an identical breakpoint (it is a fixed-point of the reversal operation). The previous isCanonical check returned false for this case, causing an unnecessary and ineffective reversal. Fix isCanonical to accept any same-position breakpoint where at least one strand is positive, since these cannot be further canonicalized. Only (false, false) is non-canonical, which correctly reverses to (true, true).
1 parent 512ef28 commit aed9f5a

File tree

2 files changed

+19
-3
lines changed

2 files changed

+19
-3
lines changed

src/main/scala/com/fulcrumgenomics/sv/Breakpoint.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,12 @@ case class Breakpoint(leftRefIndex: Int,
9696
)
9797

9898
/** Returns true if the representation of the breakpoint is canonical, with the left hand side of the break
99-
* earlier on the genome than the right hand side. */
99+
* earlier on the genome than the right hand side. When both breakends are at the same position, the breakpoint
100+
* is canonical if at least one strand is positive. This handles the fixed-point cases where `reversed` produces
101+
* an identical breakpoint (e.g. same position with leftPositive=false, rightPositive=true). */
100102
def isCanonical: Boolean = (leftRefIndex < rightRefIndex) ||
101103
(leftRefIndex == rightRefIndex && leftPos < rightPos) ||
102-
(leftRefIndex == rightRefIndex && leftPos == rightPos && leftPositive)
104+
(leftRefIndex == rightRefIndex && leftPos == rightPos && (leftPositive || rightPositive))
103105

104106
/** Returns an [[Interval]] for the left side of this breakpoint */
105107
def leftInterval(dict: SequenceDictionary): Interval = new Interval(

src/test/scala/com/fulcrumgenomics/sv/BreakpointTest.scala

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,21 @@ class BreakpointTest extends UnitSpec {
7979
(rbp.rightPos, rbp.rightPositive) shouldBe (1000, true)
8080
}
8181

82-
it should "represent a tandem duplication sanely" in {
82+
"Breakpoint.isCanonical" should "treat all strand combinations at the same position as canonical except (F, F)" in {
83+
val base = Breakpoint(leftRefIndex=0, leftPos=100, leftPositive=true, rightRefIndex=0, rightPos=100, rightPositive=true)
84+
85+
// (T, T) at same position: canonical
86+
base.copy(leftPositive=true, rightPositive=true).isCanonical shouldBe true
87+
// (T, F) at same position: canonical (fixed-point of reversed)
88+
base.copy(leftPositive=true, rightPositive=false).isCanonical shouldBe true
89+
// (F, T) at same position: canonical (fixed-point of reversed)
90+
base.copy(leftPositive=false, rightPositive=true).isCanonical shouldBe true
91+
// (F, F) at same position: NOT canonical, reversed gives (T, T)
92+
base.copy(leftPositive=false, rightPositive=false).isCanonical shouldBe false
93+
base.copy(leftPositive=false, rightPositive=false).reversed.isCanonical shouldBe true
94+
}
95+
96+
"Breakpoint.apply(segment, segment)" should "represent a tandem duplication sanely" in {
8397
// We want the same representation regardless of which piece of the duplication was sequenced more so
8498
// we test where the second segment is shorter, the same length as, and longer than the first
8599
Seq(-1, 0, 1).foreach { addend =>

0 commit comments

Comments
 (0)