-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate interval tree from cgranges to superintervals #42
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -67,14 +67,13 @@ | |||||||||||||||||||||||||||||
from typing import List | ||||||||||||||||||||||||||||||
from typing import Optional | ||||||||||||||||||||||||||||||
from typing import Protocol | ||||||||||||||||||||||||||||||
from typing import Set | ||||||||||||||||||||||||||||||
from typing import Type | ||||||||||||||||||||||||||||||
from typing import TypeVar | ||||||||||||||||||||||||||||||
from typing import Union | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
import attr | ||||||||||||||||||||||||||||||
from superintervals import IntervalSet | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
import cgranges as cr | ||||||||||||||||||||||||||||||
from pybedlite.bed_record import BedRecord | ||||||||||||||||||||||||||||||
from pybedlite.bed_record import BedStrand | ||||||||||||||||||||||||||||||
from pybedlite.bed_source import BedSource | ||||||||||||||||||||||||||||||
|
@@ -269,7 +268,7 @@ class OverlapDetector(Generic[SpanType], Iterable[SpanType]): | |||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
def __init__(self, intervals: Optional[Iterable[SpanType]] = None) -> None: | ||||||||||||||||||||||||||||||
# A mapping from the contig/chromosome name to the associated interval tree | ||||||||||||||||||||||||||||||
self._refname_to_tree: Dict[str, cr.cgranges] = {} # type: ignore | ||||||||||||||||||||||||||||||
self._refname_to_tree: Dict[str, IntervalSet] = {} | ||||||||||||||||||||||||||||||
self._refname_to_indexed: Dict[str, bool] = {} | ||||||||||||||||||||||||||||||
self._refname_to_intervals: Dict[str, List[SpanType]] = {} | ||||||||||||||||||||||||||||||
if intervals is not None: | ||||||||||||||||||||||||||||||
|
@@ -286,7 +285,7 @@ def add(self, interval: SpanType) -> None: | |||||||||||||||||||||||||||||
interval: the interval to add to this detector | ||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||
if interval.refname not in self._refname_to_tree: | ||||||||||||||||||||||||||||||
self._refname_to_tree[interval.refname] = cr.cgranges() # type: ignore | ||||||||||||||||||||||||||||||
self._refname_to_tree[interval.refname] = IntervalSet() | ||||||||||||||||||||||||||||||
self._refname_to_indexed[interval.refname] = False | ||||||||||||||||||||||||||||||
self._refname_to_intervals[interval.refname] = [] | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
|
@@ -295,9 +294,10 @@ def add(self, interval: SpanType) -> None: | |||||||||||||||||||||||||||||
interval_idx: int = len(self._refname_to_intervals[interval.refname]) | ||||||||||||||||||||||||||||||
self._refname_to_intervals[interval.refname].append(interval) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
# Add the interval to the tree | ||||||||||||||||||||||||||||||
# Add the interval to the tree. Note that IntervalSet uses closed intervals whereas we are | ||||||||||||||||||||||||||||||
# using half-open intervals, so add 1 to start | ||||||||||||||||||||||||||||||
tree = self._refname_to_tree[interval.refname] | ||||||||||||||||||||||||||||||
tree.add(interval.refname, interval.start, interval.end, interval_idx) | ||||||||||||||||||||||||||||||
tree.add(interval.start + 1, interval.end, interval_idx) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
# Flag this tree as needing to be indexed after adding a new interval, but defer | ||||||||||||||||||||||||||||||
# indexing | ||||||||||||||||||||||||||||||
|
@@ -322,18 +322,38 @@ def overlaps_any(self, interval: Span) -> bool: | |||||||||||||||||||||||||||||
True if and only if the given interval overlaps with any interval in this | ||||||||||||||||||||||||||||||
detector. | ||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||
tree = self._refname_to_tree.get(interval.refname) | ||||||||||||||||||||||||||||||
tree = self._refname_to_tree.get(interval.refname, None) | ||||||||||||||||||||||||||||||
if tree is None: | ||||||||||||||||||||||||||||||
return False | ||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||
if not self._refname_to_indexed[interval.refname]: | ||||||||||||||||||||||||||||||
tree.index() | ||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||
next(iter(tree.overlap(interval.refname, interval.start, interval.end))) | ||||||||||||||||||||||||||||||
except StopIteration: | ||||||||||||||||||||||||||||||
return False | ||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||
return True | ||||||||||||||||||||||||||||||
self._refname_to_indexed[interval.refname] = True | ||||||||||||||||||||||||||||||
# IntervalSet uses closed intervals whereas we are using half-open intervals, so add 1 | ||||||||||||||||||||||||||||||
# to start | ||||||||||||||||||||||||||||||
return tree.any_overlaps(interval.start + 1, interval.end) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
def iter_overlaps(self, interval: Span) -> Iterator[SpanType]: | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This new method isn't much different in behavior than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Responding to @clintval and @msto in the same place since you made similar comments:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd usually favor including those as options in e.g. def get_overlaps(
self,
interval: Span,
sort_overlaps: bool = True,
include_duplicates: bool = False,
) -> list[SpanType]: I agree with Clint that returning an
msto marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||
"""Yields any intervals in this detector that overlap the given interval | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||||||||
interval: the interval to check | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
Yields: | ||||||||||||||||||||||||||||||
Intervals in this detector that overlap the given interval, in insertion order. | ||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||
tree = self._refname_to_tree.get(interval.refname, None) | ||||||||||||||||||||||||||||||
if tree is not None: | ||||||||||||||||||||||||||||||
if not self._refname_to_indexed[interval.refname]: | ||||||||||||||||||||||||||||||
tree.index() | ||||||||||||||||||||||||||||||
self._refname_to_indexed[interval.refname] = True | ||||||||||||||||||||||||||||||
ref_intervals: List[SpanType] = self._refname_to_intervals[interval.refname] | ||||||||||||||||||||||||||||||
# IntervalSet uses closed intervals whereas we are using half-open intervals, so add 1 | ||||||||||||||||||||||||||||||
# to start. | ||||||||||||||||||||||||||||||
# Also IntervalSet yields indices in reverse insertion order, so yield intervals in | ||||||||||||||||||||||||||||||
# reverse of indices list. | ||||||||||||||||||||||||||||||
for index in reversed(tree.find_overlaps(interval.start + 1, interval.end)): | ||||||||||||||||||||||||||||||
yield ref_intervals[index] | ||||||||||||||||||||||||||||||
Comment on lines
+355
to
+356
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The call to Are you doing this to preserve original order? IMHO order wouldn't matter to me. Or if it did, I'd want the same order as is forced in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I put the call to |
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
def get_overlaps(self, interval: Span) -> List[SpanType]: | ||||||||||||||||||||||||||||||
"""Returns any intervals in this detector that overlap the given interval. | ||||||||||||||||||||||||||||||
|
@@ -351,27 +371,15 @@ def get_overlaps(self, interval: Span) -> List[SpanType]: | |||||||||||||||||||||||||||||
* The interval's strand, positive or negative (assumed to be positive if undefined) | ||||||||||||||||||||||||||||||
* The interval's reference sequence name (lexicographically) | ||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||
tree = self._refname_to_tree.get(interval.refname) | ||||||||||||||||||||||||||||||
if tree is None: | ||||||||||||||||||||||||||||||
return [] | ||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||
if not self._refname_to_indexed[interval.refname]: | ||||||||||||||||||||||||||||||
tree.index() | ||||||||||||||||||||||||||||||
ref_intervals: List[SpanType] = self._refname_to_intervals[interval.refname] | ||||||||||||||||||||||||||||||
# NB: only return unique instances of intervals | ||||||||||||||||||||||||||||||
intervals: Set[SpanType] = { | ||||||||||||||||||||||||||||||
ref_intervals[index] | ||||||||||||||||||||||||||||||
for _, _, index in tree.overlap(interval.refname, interval.start, interval.end) | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
return sorted( | ||||||||||||||||||||||||||||||
intervals, | ||||||||||||||||||||||||||||||
key=lambda intv: ( | ||||||||||||||||||||||||||||||
intv.start, | ||||||||||||||||||||||||||||||
intv.end, | ||||||||||||||||||||||||||||||
self._negative(intv), | ||||||||||||||||||||||||||||||
intv.refname, | ||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
return sorted( | ||||||||||||||||||||||||||||||
set(self.iter_overlaps(interval)), | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious about the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will depend on the hash of the object you place in the If you send in a dataclass with Both Mypy should disallow any custom interval-like object that does not have a pybedlite/pybedlite/overlap_detector.py Line 82 in a63c492
I don't think a call to pybedlite/pybedlite/overlap_detector.py Lines 362 to 374 in 9c4990e
|
||||||||||||||||||||||||||||||
key=lambda intv: ( | ||||||||||||||||||||||||||||||
intv.start, | ||||||||||||||||||||||||||||||
intv.end, | ||||||||||||||||||||||||||||||
self._negative(intv), | ||||||||||||||||||||||||||||||
intv.refname, | ||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@staticmethod | ||||||||||||||||||||||||||||||
def _negative(interval: Span) -> bool: | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't
.get()
already returnNone
as the default empty value?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I tend to prefer explicit
None
s in this kind of use case and changed it without thinking much. I can revert if this is an issue.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries, I don't really have an opinion. Explicit is nice.