Source code for iranges.sew_handler
from typing import Optional, Tuple, Union
import numpy as np
from .utils import handle_negative_coords, normalize_array
__author__ = "Jayaram Kancherla"
__copyright__ = "jkanche"
__license__ = "MIT"
# reference: https://github.com/Bioconductor/IRanges/blob/devel/R/IRanges-constructor.R#L201
[docs]
class SEWWrangler:
"""Handler to resolve start/end/width parameters."""
[docs]
def __init__(
self,
ref_widths: np.ndarray,
start: Optional[Union[int, np.ndarray]] = None,
end: Optional[Union[int, np.ndarray]] = None,
width: Optional[Union[int, np.ndarray]] = None,
translate_negative: bool = True,
allow_nonnarrowing: bool = False,
):
"""Initialize SEW parameters.
Args:
ref_widths:
Reference widths array.
start:
Start positions.
end:
End positions.
width:
Widths.
translate_negative:
Whether to translate negative coordinates.
allow_nonnarrowing:
Whether to allow ranges wider than reference.
"""
self.ref_widths = np.asarray(ref_widths, dtype=np.int32)
self.length = len(ref_widths)
self.allow_nonnarrowing = allow_nonnarrowing
self.start = normalize_array(start, self.length)
self.end = normalize_array(end, self.length)
self.width = normalize_array(width, self.length)
if translate_negative:
self.start = handle_negative_coords(self.start, self.ref_widths)
self.end = handle_negative_coords(self.end, self.ref_widths)
if not allow_nonnarrowing:
# validate supplied ends
if not self.end.mask.all():
too_wide = (~self.end.mask) & (self.end > self.ref_widths)
if np.any(too_wide):
idx = np.where(too_wide)[0][0]
raise Exception(
f"solving row {idx + 1}: 'allow.nonnarrowing' is FALSE and "
f"the supplied end ({int(self.end[idx])}) is > refwidth"
)
def _validate_narrowing(self, starts: np.ndarray, widths: np.ndarray) -> None:
"""Validate that ranges don't exceed reference width."""
if not self.allow_nonnarrowing:
ends = starts + widths - 1
too_wide = ends > self.ref_widths
if np.any(too_wide):
idx = np.where(too_wide)[0][0]
raise Exception(
f"solving row {idx + 1}: 'allow.nonnarrowing' is FALSE and "
f"the solved end ({int(ends[idx])}) is > refwidth"
)
[docs]
def solve(self) -> Tuple[np.ndarray, np.ndarray]:
"""Resolve Start/End/Width parameters to concrete ranges.
Returns:
Tuple of resolved (starts, widths) ranges.
"""
out_starts = np.ones(self.length, dtype=np.int32)
out_widths = self.ref_widths.copy()
if not self.width.mask.all():
if np.any((~self.width.mask) & (self.width < 0)):
raise Exception("negative values are not allowed in 'width'")
if not self.end.mask.all():
# Width and end specified
# mask = (~self.width.mask) & (~self.end.mask)
out_starts = self.end - self.width + 1
out_widths = self.width
# Validate after computing
self._validate_narrowing(out_starts, out_widths)
elif not self.start.mask.all():
# Width and start specified
# mask = (~self.width.mask) & (~self.start.mask)
out_starts = self.start
out_widths = self.width
# Validate after computing
self._validate_narrowing(out_starts, out_widths)
else:
# Only width specified
out_widths = self.width
# Handle start/end specification
elif not self.start.mask.all() and not self.end.mask.all():
out_starts = self.start
out_widths = self.end - self.start + 1
if np.any(out_widths < 0):
raise Exception("ranges contain negative width")
# Validate after computing
self._validate_narrowing(out_starts, out_widths)
# Handle only start
elif not self.start.mask.all():
out_starts = self.start
out_widths = self.ref_widths - (self.start - 1)
# Validate after computing
self._validate_narrowing(out_starts, out_widths)
# Handle only end
elif not self.end.mask.all():
out_widths = self.end
# Validate after computing
self._validate_narrowing(out_starts, out_widths)
return out_starts, out_widths