Skip to content

Commit 8bda9ba

Browse files
committed
Fix #186: Locus.intersect returns the overlap region or None
Mirrors bedtools intersect for a single pair: returns a new Locus covering the inclusive-inclusive overlap between this locus and another, or None when they're on different contigs, opposite strands (unless ignore_strand=True), or disjoint. The result is reported on self's strand. Bump version to 2.9.3.
1 parent 6582355 commit 8bda9ba

3 files changed

Lines changed: 41 additions & 1 deletion

File tree

pyensembl/locus.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,25 @@ def overlap_length(self, other, ignore_strand=False):
240240
return 0
241241
return max(0, min(self.end, other.end) - max(self.start, other.start) + 1)
242242

243+
def intersect(self, other, ignore_strand=False):
244+
"""
245+
Return a new :class:`Locus` covering the inclusive-inclusive
246+
overlap between this locus and ``other``, or ``None`` if they do
247+
not overlap. Mirrors bedtools' ``intersect`` for a single pair.
248+
249+
Different contigs always return ``None``. Different strands return
250+
``None`` unless ``ignore_strand=True`` is passed, in which case the
251+
result is reported on this locus's strand.
252+
"""
253+
strand = None if ignore_strand else other.strand
254+
if not self.can_overlap(other.contig, strand):
255+
return None
256+
start = max(self.start, other.start)
257+
end = min(self.end, other.end)
258+
if start > end:
259+
return None
260+
return Locus(contig=self.contig, start=start, end=end, strand=self.strand)
261+
243262
def contains(self, contig, start, end, strand=None):
244263
return (
245264
self.can_overlap(contig, strand) and start >= self.start and end <= self.end

pyensembl/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "2.9.2"
1+
__version__ = "2.9.3"
22

33
def print_version():
44
print(f"v{__version__}")

tests/test_locus.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,24 @@ def test_locus_overlap_length():
189189
other_neg = Locus("1", 10, 20, "-")
190190
assert locus.overlap_length(other_neg) == 0
191191
assert locus.overlap_length(other_neg, ignore_strand=True) == 11
192+
193+
194+
def test_locus_intersect():
195+
a = Locus("1", 10, 20, "+")
196+
# full overlap (other contained in a)
197+
eq_result = a.intersect(Locus("1", 12, 15, "+"))
198+
assert eq_result == Locus("1", 12, 15, "+")
199+
# partial overlap (right tail of a)
200+
assert a.intersect(Locus("1", 15, 30, "+")) == Locus("1", 15, 20, "+")
201+
# partial overlap (left tail of a)
202+
assert a.intersect(Locus("1", 1, 15, "+")) == Locus("1", 10, 15, "+")
203+
# single-base boundary overlap
204+
assert a.intersect(Locus("1", 20, 30, "+")) == Locus("1", 20, 20, "+")
205+
# adjacent, non-overlapping
206+
assert a.intersect(Locus("1", 21, 30, "+")) is None
207+
# different contig
208+
assert a.intersect(Locus("2", 10, 20, "+")) is None
209+
# opposite strand: None by default, accepts when ignore_strand=True
210+
other_neg = Locus("1", 12, 18, "-")
211+
assert a.intersect(other_neg) is None
212+
assert a.intersect(other_neg, ignore_strand=True) == Locus("1", 12, 18, "+")

0 commit comments

Comments
 (0)