Repository: cogent3/cogent3
Pull request #1846 (2024-05-02 01:34:51)

Address code review comments


Patch:
diff --git a/src/cogent3/align/traceback.py b/src/cogent3/align/traceback.py
index 4a0ca3dba..44142eeec 100644
--- a/src/cogent3/align/traceback.py
+++ b/src/cogent3/align/traceback.py
@@ -1,10 +1,16 @@
 """Conversion of dynamic program results ("arrays of arrows") into gap vectors,
 gapped sequences or Cogent Alignment objects"""
+import typing
 
 from cogent3.core.alignment import Aligned, Alignment
 from cogent3.core.location import IndelMap
 
 
+IntOrNone = typing.Union[int, None]
+IntListType = list[int]
+CoordsListType = list[list[typing.Sequence[int]]]
+
+
 def seq_traceback(s1, s2, aligned_positions, gap_value):
     """gapped sequences from state matrix and ending point
     gaps are signified by 'gap_value' inserted in the sequences.
@@ -29,7 +35,9 @@ def seq_traceback(s1, s2, aligned_positions, gap_value):
     return alignments
 
 
-def gap_traceback(aligned_positions):
+def gap_traceback(
+    aligned_positions: list[list[IntOrNone, IntOrNone]]
+) -> tuple[IntListType, IntListType, CoordsListType, int]:
     """gap Vectors from state matrix and ending point"""
     consuming = [False, False]
     starts = [None, None]
@@ -54,7 +62,9 @@ def gap_traceback(aligned_positions):
     return starts, ends, gap_vectors, a
 
 
-def map_traceback(aligned_positions):
+def map_traceback(
+    aligned_positions: list[list[IntOrNone, IntOrNone]]
+) -> tuple[IntListType, IntListType, list[IndelMap]]:
     # using IndelMap's to keep track of gaps for indel alignment
     starts, ends, gap_vectors, alignment_len = gap_traceback(aligned_positions)
     maps = [
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index e34cf8b79..cb97be425 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -2250,11 +2250,7 @@ class Aligned:
     def _(self, span: FeatureMap):
         # we assume the feature map is in align coordinates
         start, end = span.start, span.end
-        if span.useful and start > end:
-            empty = numpy.array([], dtype=self.map.gap_pos.dtype)
-            im = IndelMap(gap_pos=empty, cum_gap_lengths=empty, parent_length=0)
-            data = self.data[:0]
-        elif span.useful and len(list(span.spans)) == 1:
+        if span.useful and len(list(span.spans)) == 1:
             im = self.map[start:end]
             seq_start = self.map.get_seq_index(start)
             seq_end = self.map.get_seq_index(end)
@@ -2266,7 +2262,7 @@ class Aligned:
             # multiple spans
             align_coords = span.get_coordinates()
             im = self.map.joined_segments(align_coords)
-            seq_map = self.map.make_seq_feature_map(span, include_gaps=False)
+            seq_map = self.map.make_seq_feature_map(span)
             data = self.data.gapped_by_map(seq_map)
 
         return Aligned(im, data)
diff --git a/src/cogent3/core/location.py b/src/cogent3/core/location.py
index 808af9fe3..1578615e6 100644
--- a/src/cogent3/core/location.py
+++ b/src/cogent3/core/location.py
@@ -1109,7 +1109,7 @@ class MapABC(ABC):
         ...
 
     @abstractmethod
-    def __add__(self, other):
+    def __add__(self, other: "MapABC") -> "MapABC":
         ...
 
     @abstractmethod
@@ -1121,7 +1121,7 @@ class MapABC(ABC):
         ...
 
     @abstractmethod
-    def nucleic_reversed(self):
+    def nucleic_reversed(self) -> "MapABC":
         ...
 
     @abstractmethod
@@ -1129,7 +1129,9 @@ class MapABC(ABC):
         ...
 
     @classmethod
-    def from_locations(cls, locations: SeqCoordTypes, parent_length: int, **kwargs):
+    def from_locations(
+        cls, locations: SeqCoordTypes, parent_length: int, **kwargs
+    ) -> "MapABC":
         if len(locations):
             spans = _spans_from_locations(locations, parent_length=parent_length)
         else:
@@ -1139,7 +1141,7 @@ class MapABC(ABC):
 
     @classmethod
     @abstractmethod
-    def from_spans(cls, spans: SeqSpanTypes, parent_length: int, **kwargs):
+    def from_spans(cls, spans: SeqSpanTypes, parent_length: int, **kwargs) -> "MapABC":
         ...
 
 
@@ -1209,7 +1211,7 @@ def _gap_spans(
     """returns 1D arrays in alignment coordinates of
     gap start, gap stop"""
     if not len(gap_pos):
-        r = numpy.array([], dtype=gap_pos.dtype)
+        r = numpy.array([], dtype=_DEFAULT_GAP_DTYPE)
         return r, r
 
     ends = gap_pos + cum_gap_lengths
@@ -1305,7 +1307,7 @@ class IndelMap(MapABC):
             and locations[0][0] == 0
             and locations[0][-1] == aligned_length
         ):
-            empty = numpy.array([], dtype=int)
+            empty = numpy.array([], dtype=_DEFAULT_GAP_DTYPE)
             return cls(
                 gap_pos=empty,
                 cum_gap_lengths=empty.copy(),
@@ -1319,7 +1321,7 @@ class IndelMap(MapABC):
             # ends with a gap
             locations += [(aligned_length, aligned_length)]
 
-        locations = numpy.array(locations, dtype=numpy.int32).flatten()[1:-1]
+        locations = numpy.array(locations, dtype=_DEFAULT_GAP_DTYPE).flatten()[1:-1]
         gap_coords = locations.reshape((locations.shape[0] // 2, 2))
         gap_ends, gap_starts = gap_coords[:, ::-1].T
         gap_lengths = gap_ends - gap_starts
@@ -1353,15 +1355,15 @@ class IndelMap(MapABC):
             raise NotImplementedError(
                 f"{type(self).__name__!r} does not yet support strides"
             )
-        zero_array = numpy.array([], dtype=self.gap_pos.dtype)
+        zero_array = numpy.array([], dtype=_DEFAULT_GAP_DTYPE)
         start = item.start or 0
-        stop = item.stop or len(self)
+        stop = item.stop if item.stop is not None else len(self)
 
         # convert negative indices
         start = start if start >= 0 else len(self) + start
         stop = stop if stop >= 0 else len(self) + stop
         if min((start, stop)) < 0:
-            raise IndexError(f"one of adjusted {start, stop} is < 0")
+            raise IndexError("item.start or item.stop is out of range")
 
         if start >= stop:
             # standard slice behaviour without negative step
@@ -1394,7 +1396,7 @@ class IndelMap(MapABC):
         if gap_starts[l] <= start < gap_ends[l] and stop <= gap_ends[l]:
             # entire span is within a single gap
             # pos now 0
-            gap_pos = numpy.array([0], dtype=self.gap_pos.dtype)
+            gap_pos = numpy.array([0], dtype=_DEFAULT_GAP_DTYPE)
             cum_lengths = cum_lengths[l : l + 1]
             cum_lengths[0] = stop - start
             return self.__class__(
@@ -1411,7 +1413,7 @@ class IndelMap(MapABC):
             # so the absolute gap_pos value remains unchanged, but we shorten
             # the gap length
             begin = l
-            begin_diff = start - gap_starts[l]  # if l else self.gap_pos[l]
+            begin_diff = start - gap_starts[l]
             lengths[l] -= begin_diff
             shift = (start - cum_lengths[l - 1] - begin_diff) if l else gap_pos[0]
         elif start == gap_ends[l]:
@@ -1460,6 +1462,8 @@ class IndelMap(MapABC):
             In that case, and if seq_index is in gap_pos then it returns
             the first alignment index of the gap run.
         """
+        cum_lengths = self.cum_gap_lengths
+        gap_pos = self.gap_pos
         # NOTE I explicitly cast all returned values to python int's due to
         # need for json serialisation, which does not support numpy int classes
         if seq_index < 0:
@@ -1468,32 +1472,29 @@ class IndelMap(MapABC):
         if seq_index < 0:
             raise IndexError(f"{seq_index} negative seq_index beyond limit ")
 
-        if not self.num_gaps or seq_index < self.gap_pos[0]:
+        if not self.num_gaps or seq_index < gap_pos[0]:
             return int(seq_index)
 
         # if stop_index, check if the seq_index corresponds to a gap position
-        if slice_stop and (match := seq_index == self.gap_pos).any():
+        if slice_stop and (match := seq_index == gap_pos).any():
             # if so, we return the alignment coord for the first gap position
             (idx,) = numpy.where(match)[0]
             if idx:
-                gap_len = self.cum_gap_lengths[idx] - self.cum_gap_lengths[idx - 1]
+                gap_len = cum_lengths[idx] - cum_lengths[idx - 1]
             else:
-                gap_len = self.cum_gap_lengths[idx]
-            gap_end = self.gap_pos[idx] + self.cum_gap_lengths[idx]
+                gap_len = cum_lengths[idx]
+            gap_end = gap_pos[idx] + cum_lengths[idx]
             return int(gap_end - gap_len)
 
-        cum_gap_lengths = self.cum_gap_lengths
-        gap_pos = self.gap_pos
-
         if seq_index >= gap_pos[-1]:
-            return int(seq_index + cum_gap_lengths[-1])
+            return int(seq_index + cum_lengths[-1])
 
         # find gap position before seq_index
         index = numpy.searchsorted(gap_pos, seq_index, side="left")
         if seq_index < gap_pos[index]:
-            gap_lengths = cum_gap_lengths[index - 1] if index else 0
+            gap_lengths = cum_lengths[index - 1] if index else 0
         else:
-            gap_lengths = cum_gap_lengths[index]
+            gap_lengths = cum_lengths[index]
 
         return int(seq_index + gap_lengths)
 
@@ -1533,21 +1534,17 @@ class IndelMap(MapABC):
         length_gaps = self.cum_gap_lengths[-1] if self.num_gaps else 0
         return int(self.parent_length + length_gaps)
 
-    def __add__(self, other) -> "IndelMap":
-        # what was the purpose of this method? The code seems designed for
-        # combining Maps from the same parent sequence, which is a union rather
-        # than addition
-        # I'm revising this to be suitable for the concatenation of two aligned
-        # sequences
+    def __add__(self, other: "IndelMap") -> "IndelMap":
+        """designed to support concatenation of two aligned sequences"""
         gap_pos = self.gap_pos.tolist() + (self.parent_length + other.gap_pos).tolist()
-        gap_pos = numpy.array(gap_pos, dtype=self.gap_pos.dtype)
+        gap_pos = numpy.array(gap_pos, dtype=_DEFAULT_GAP_DTYPE)
 
         cum_length = self.cum_gap_lengths[-1] if self.num_gaps else 0
         cum_gap_lengths = (
             self.cum_gap_lengths.tolist()
             + (cum_length + other.cum_gap_lengths).tolist()
         )
-        cum_gap_lengths = numpy.array(cum_gap_lengths, dtype=self.cum_gap_lengths.dtype)
+        cum_gap_lengths = numpy.array(cum_gap_lengths, dtype=_DEFAULT_GAP_DTYPE)
 
         return self.__class__(
             gap_pos=gap_pos,
@@ -1556,7 +1553,7 @@ class IndelMap(MapABC):
         )
 
     def __mul__(self, scale: int) -> "IndelMap":
-        # could be used for going from amino-acid alignment to codon alignment
+        """used for going from amino-acid alignment to codon alignment"""
         gap_pos = self.gap_pos * scale
         cum_gap_lengths = self.cum_gap_lengths * scale
         return self.__class__(
@@ -1574,14 +1571,6 @@ class IndelMap(MapABC):
         lengths[1:] = numpy.diff(lengths)
         return lengths
 
-    @property
-    def offsets(self) -> IntArrayTypes:
-        # offsets are the aligned indices for every starting point of a segment
-        # when we encounter a gap, we include that position and the end of that gap
-
-        starts, ends = _gap_spans(self.gap_pos, self.cum_gap_lengths)
-        return numpy.array([starts, ends]).T.flatten()[:-1].tolist()
-
     def nongap(self) -> Iterator[SpanTypes]:
         """ungappeed segments in this map in aligned coordinates"""
         # we want to know the coordinates of the ungapped segments on
@@ -1602,7 +1591,7 @@ class IndelMap(MapABC):
             prev_pos = pos
 
         if self.num_gaps and self.gap_pos[-1] + self.cum_gap_lengths[-1] < len(self):
-            yield Span(prev_pos, len(self))
+            yield Span(self.gap_pos[-1] + self.cum_gap_lengths[-1], len(self))
 
     @property
     def spans(self) -> Iterator[SpanTypes]:
@@ -1651,29 +1640,32 @@ class IndelMap(MapABC):
         -------
         [(start, end), ...]
         """
-        coords = []
-        last = 0
-        for pos, cum_length in zip(self.gap_pos, self.cum_gap_lengths):
-            pos, cum_length = int(pos), int(cum_length)
-
-            if not pos:
-                continue
-            coords.append((last, pos))
-            last = pos
-
-        if (
-            self.num_gaps
-            and self.gap_pos[-1] + self.cum_gap_lengths[-1] < self.parent_length
-        ):
-            coords.append((last, self.parent_length))
-        return coords
+        if not self.num_gaps or (self.num_gaps == 1 and not self.gap_pos[0]):
+            return [(0, int(self.parent_length))]
+        elif self.num_gaps == 1:
+            # does not start with a gap
+            starts = [0, int(self.gap_pos[0])]
+            ends = [int(self.gap_pos[0]), self.parent_length]
+            return list(zip(starts, ends))
+
+        starts = self.gap_pos[:-1].tolist()
+        ends = self.gap_pos[1:].tolist()
+        if self.gap_pos[0]:
+            # does not start with a gap
+            ends = starts[:1] + ends
+            starts = [0] + starts
+
+        if self.gap_pos[-1] + self.cum_gap_lengths[-1] < self.parent_length:
+            # does end with a gap
+            starts.append(ends[-1])
+            ends.append(self.parent_length)
+
+        return list(zip(starts, ends))
 
     def get_gap_coordinates(self) -> SeqCoordTypes:
         """returns [(gap pos, gap length), ...]"""
-        cum_lengths = self.cum_gap_lengths.copy()
-        diffs = numpy.diff(cum_lengths)
-        cum_lengths[1:] = diffs
-        return numpy.array([self.gap_pos, cum_lengths]).T.tolist()
+        lengths = self.get_gap_lengths()
+        return numpy.array([self.gap_pos, lengths]).T.tolist()
 
     def get_gap_align_coordinates(self) -> SeqCoordTypes:
         """returns [(gap start, gap end), ...] in alignment indices
@@ -1700,7 +1692,7 @@ class IndelMap(MapABC):
             overrides property
         """
         unique_pos = numpy.union1d(self.gap_pos, other.gap_pos)
-        gap_lengths = numpy.zeros(unique_pos.shape, dtype=self.cum_gap_lengths.dtype)
+        gap_lengths = numpy.zeros(unique_pos.shape, dtype=_DEFAULT_GAP_DTYPE)
         self_lengths = self.get_gap_lengths()
         other_lengths = other.get_gap_lengths()
         _update_lengths(unique_pos, gap_lengths, self.gap_pos, self_lengths)
@@ -1719,29 +1711,22 @@ class IndelMap(MapABC):
         ----------
         coords
             sequence insert gap coordinates [(gap start, gap end), ...]
-        Returns
-        -------
-
         """
-        coords = list(sorted(coords))
+        coords = sorted(coords)
         # using a dict here because joining can produce a gap merge
         gaps = {}
         cum_length = 0
         cum_parent_length = 0
-        dtype = self.gap_pos.dtype
         for start, end in coords:
             im = self[start:end]
             for i in range(im.num_gaps):
                 pos = im.gap_pos[i] + cum_parent_length
-                if pos in gaps:
-                    gaps[pos] += im.cum_gap_lengths[i]
-                else:
-                    gaps[pos] = im.cum_gap_lengths[i] + cum_length
+                gaps[pos] = gaps.get(pos, cum_length) + im.cum_gap_lengths[i]
             cum_parent_length += im.parent_length
             if im.num_gaps:
                 cum_length += im.cum_gap_lengths[-1]
-        gap_pos = numpy.empty(len(gaps), dtype=dtype)
-        cum_lengths = numpy.empty(len(gaps), dtype=dtype)
+        gap_pos = numpy.empty(len(gaps), dtype=_DEFAULT_GAP_DTYPE)
+        cum_lengths = numpy.empty(len(gaps), dtype=_DEFAULT_GAP_DTYPE)
         for i, (pos, length) in enumerate(sorted(gaps.items())):
             gap_pos[i] = pos
             cum_lengths[i] = length
@@ -1811,37 +1796,28 @@ class IndelMap(MapABC):
         """returns a Map type, suited to Features"""
         return FeatureMap(spans=list(self.spans), parent_length=self.parent_length)
 
-    def make_seq_feature_map(
-        self, align_feature_map: "FeatureMap", include_gaps: bool = True
-    ) -> "FeatureMap":
+    def make_seq_feature_map(self, align_feature_map: "FeatureMap") -> "FeatureMap":
         """converts align_feature_map to a FeatureMap with sequence coordinates
 
         Parameters
         ----------
         align_feature_map
             with alignment coordinates
-        include_gaps
-            whether to include gaps from self as LostSpan's
+
+        Notes
+        -----
+        LostSpans in align_feature_map are skipped
         """
-        gap_spans = {}
-        if include_gaps and self.num_gaps:
-            last = 0
-            for pos, cum_length in zip(self.gap_pos, self.cum_gap_lengths):
-                gap_spans[pos] = LostSpan(cum_length - last)
-                last = cum_length
         spans = []
         for span in align_feature_map.spans:
             if span.lost:
-                spans.append(span)
                 continue
 
             start = self.get_seq_index(span.start)
             end = self.get_seq_index(span.end)
-            if lost := gap_spans.pop(span.start, None):
-                spans.append(lost)
             spans.append(Span(start, end))
 
-        return FeatureMap(spans=spans, parent_length=align_feature_map.parent_length)
+        return FeatureMap(spans=spans, parent_length=self.parent_length)
 
 
 @dataclasses.dataclass
@@ -1855,6 +1831,8 @@ class FeatureMap(MapABC):
     complete: bool = dataclasses.field(init=False, repr=False, default=True)
     _serialisable: dict = dataclasses.field(init=False, repr=False)
     _spans: SeqSpanTypes = dataclasses.field(default=(), init=False)
+    _start: Optional[int] = dataclasses.field(default=None, init=False)
+    _end: Optional[int] = dataclasses.field(default=None, init=False)
 
     def __post_init__(self, spans: SeqSpanTypes):
         assert self.parent_length is not None
@@ -1879,10 +1857,10 @@ class FeatureMap(MapABC):
                 self.complete = False
             elif not self.useful:
                 self.useful = True
-                self.start, self.end = span.start, span.end
+                self._start, self._end = span.start, span.end
             else:
-                self.start = min(self.start, span.start)
-                self.end = max(self.end, span.end)
+                self._start = min(self._start, span.start)
+                self._end = max(self._end, span.end)
 
         self._spans = tuple(spans)
         self.length = posn
@@ -2143,8 +2121,8 @@ class FeatureMap(MapABC):
             span.end -= shift
             new_end = max(new_end, span.end)
 
-        zeroed.start = 0
-        zeroed.end = new_end
+        zeroed._start = 0
+        zeroed._end = new_end
 
         return zeroed
 
@@ -2156,16 +2134,14 @@ class FeatureMap(MapABC):
         raises ValueError if rel_pos < 0
         """
         check = (
-            numpy.array([rel_pos], dtype=int) if isinstance(rel_pos, int) else rel_pos
+            numpy.array([rel_pos], dtype=_DEFAULT_GAP_DTYPE)
+            if isinstance(rel_pos, int)
+            else rel_pos
         )
         if check.min() < 0:
             raise ValueError(f"must positive, not {rel_pos=}")
 
-        if len(self) == self.parent_length:
-            # handle case of reversed here?
-            return rel_pos
-
-        return self.start + rel_pos
+        return rel_pos if len(self) == self.parent_length else self.start + rel_pos
 
     def relative_position(self, abs_pos: IntTypes) -> IntTypes:
         """converts abs_pos into an relative position
@@ -2175,12 +2151,22 @@ class FeatureMap(MapABC):
         raises ValueError if abs_pos < 0
         """
         check = (
-            numpy.array([abs_pos], dtype=int) if isinstance(abs_pos, int) else abs_pos
+            numpy.array([abs_pos], dtype=_DEFAULT_GAP_DTYPE)
+            if isinstance(abs_pos, int)
+            else abs_pos
         )
         if check.min() < 0:
             raise ValueError(f"must positive, not {abs_pos=}")
         return abs_pos - self.start
 
+    @property
+    def start(self):
+        return self._start or 0
+
+    @property
+    def end(self):
+        return self._end or 0
+
 
 def gap_coords_to_map(
     gaps_lengths: dict[IntTypes, IntTypes], seq_length: int
@@ -2199,12 +2185,12 @@ def gap_coords_to_map(
     """
 
     if not gaps_lengths:
-        gap_pos = numpy.array([], dtype=int)
+        gap_pos = numpy.array([], dtype=_DEFAULT_GAP_DTYPE)
         lengths = gap_pos.copy()
     else:
         gap_pos, lengths = list(zip(*sorted(gaps_lengths.items())))
-        gap_pos = numpy.array(gap_pos, dtype=int)
-        lengths = numpy.array(lengths, dtype=int)
+        gap_pos = numpy.array(gap_pos, dtype=_DEFAULT_GAP_DTYPE)
+        lengths = numpy.array(lengths, dtype=_DEFAULT_GAP_DTYPE)
 
     return IndelMap(gap_pos=gap_pos, gap_lengths=lengths, parent_length=seq_length)
 


Test patch:
diff --git a/tests/test_core/test_alignment.py b/tests/test_core/test_alignment.py
index 2cb416735..19559f4f9 100644
--- a/tests/test_core/test_alignment.py
+++ b/tests/test_core/test_alignment.py
@@ -2612,7 +2612,7 @@ def test_get_gap_array_equivalence():
     assert_allclose(array_aln.get_gap_array(), aln.get_gap_array())
 
 
-@pytest.mark.parametrize("reverse", (False, True)[1:])
+@pytest.mark.parametrize("reverse", (False, True))
 def test_aligned_rich_dict(reverse):
     map_, s = make_seq(
         "TTGAAGAATATGT------GAAAGAG", name="s1", moltype="dna"
@@ -3667,3 +3667,31 @@ def test_slice_aligned(raw):
     al = Aligned(imap, seq)
     sliced = al[:-3]
     assert str(sliced) == raw[:-3]
+
+
+def test_slice_aligned_featuremap_allgap():
+    from cogent3.core.location import FeatureMap, LostSpan
+
+    imap, seq = DNA.make_seq("AAAGGGGGAACCCT", name="x").parse_out_gaps()
+    al = Aligned(imap, seq)
+    fmap = FeatureMap(spans=[LostSpan(4)], parent_length=0)
+    sliced = al[fmap]
+    assert not sliced
+
+
+def test_slice_aligned_featuremap_multi_spans():
+    from cogent3.core.location import FeatureMap
+
+    #                    1111111
+    #          01234567890123456
+    #           ***   **    ***
+    raw_seq = "AAAGG--GGG-AACCCT"
+    #          01234  567 890123
+    #                       1111
+    imap, seq = DNA.make_seq(raw_seq, name="x").parse_out_gaps()
+    al = Aligned(imap, seq)
+    fmap = FeatureMap.from_locations(
+        locations=[(1, 4), (7, 9), (13, 16)], parent_length=len(raw_seq)
+    )
+    sliced = al[fmap]
+    assert str(sliced) == "AAGGGCCC"
diff --git a/tests/test_core/test_features.py b/tests/test_core/test_features.py
index 40290d0e6..594ac9e22 100755
--- a/tests/test_core/test_features.py
+++ b/tests/test_core/test_features.py
@@ -433,7 +433,7 @@ def test_feature_from_alignment():
 
     # But these will be returned as **alignment**
     # features with locations in alignment coordinates.
-    assert aln_exons.get_slice().to_dict() == {"x": "AAAAA", "y": "--TTT"}
+    assert aln[aln_exons].to_dict() == {"x": "AAAAA", "y": "--TTT"}
 
     # Similarly alignment features can be projected onto the aligned sequences,
     # where they may end up falling across gaps:
@@ -451,7 +451,7 @@ def test_nested_get_slice():
     s.add_feature(biotype="exon", name="trev", spans=[(30, 40)])
     s.add_feature(biotype="repeat", name="bob", spans=[(12, 17)], parent_id="fred")
     f = list(ex.get_children())[0]
-    assert str(f.get_slice()) == str(s[12:17])
+    assert str(s[f]) == str(s[12:17])
 
 
 def test_roundtrip_annotated_seq():
diff --git a/tests/test_core/test_location.py b/tests/test_core/test_location.py
index 493e52451..b6aa01c6e 100644
--- a/tests/test_core/test_location.py
+++ b/tests/test_core/test_location.py
@@ -370,6 +370,30 @@ def test_nongap(cls):
     assert got == [(0, 2), (5, 6), (7, 10)]
 
 
+@pytest.mark.parametrize("cls", (IndelMap, FeatureMap))
+def test_nongap_startswith(cls):
+    # returns spans corresponding to position on "aligned" seq of nongaps
+    #                   012345678
+    seq = DNA.make_seq("--G-TAA--")
+    m, _ = seq.parse_out_gaps()
+    m = cls.from_spans(spans=tuple(m.spans), parent_length=m.parent_length)
+
+    got = [(s.start, s.end) for s in m.nongap()]
+    assert got == [(2, 3), (4, 7)]
+
+
+@pytest.mark.parametrize("cls", (IndelMap, FeatureMap))
+def test_nongap_not_endswith(cls):
+    # returns spans corresponding to position on "aligned" seq of nongaps
+    #                   0123456
+    seq = DNA.make_seq("--G-TAA")
+    m, _ = seq.parse_out_gaps()
+    m = cls.from_spans(spans=tuple(m.spans), parent_length=m.parent_length)
+
+    got = [(s.start, s.end) for s in m.nongap()]
+    assert got == [(2, 3), (4, 7)]
+
+
 def test_spans_gen():
     # returns spans corresponding to position on "aligned" seq of nongaps
     #                   000000000011
@@ -556,17 +580,6 @@ def test_indelmap_merge_parent_length():
     assert ov.parent_length == 20
 
 
-@pytest.mark.parametrize("cls", (FeatureMap, IndelMap))
-def test_map_offsets(cls):
-    # offsets are absolute starts of spans
-    #                              1
-    #                   01 3  678  1
-    seq = DNA.make_seq("-AC---G-TAA--")
-    m, _ = seq.parse_out_gaps()
-    got = m.offsets
-    assert got == [0, 1, 3, 6, 7, 8, 11]
-
-
 def test_map_indexed():
     m = FeatureMap.from_locations(locations=[(0, 2), (4, 6)], parent_length=6).inverse()
     indexed = m[2]
@@ -670,6 +683,17 @@ def test_indelmap_slice_terminating():
     assert got.cum_gap_lengths.tolist() == expect.cum_gap_lengths.tolist()
 
 
+def test_indelmap_slice_zero():
+    raw = "-CB-A--"
+    start, end = 0, 0
+    expect, s = DNA.make_seq(raw[start:end]).parse_out_gaps()
+    imap, _ = DNA.make_seq(raw).parse_out_gaps()
+    got = imap[start:end]
+    assert got.parent_length == len(s)
+    assert got.gap_pos.tolist() == expect.gap_pos.tolist()
+    assert got.cum_gap_lengths.tolist() == expect.cum_gap_lengths.tolist()
+
+
 def test_indelmap_invalid_slice_range():
     imap = IndelMap(
         gap_pos=numpy.array([10], dtype=int),
@@ -1146,3 +1170,28 @@ def test_featuremap_div():
     fm_1 = fm_3 / 3
     assert list(fm_1.spans) == [sp / 3 for sp in spans]
     assert fm_1.parent_length == 6 / 3
+
+
+def test_indelmap_make_seq_feature_map():
+    #           1
+    # 01234567890
+    # AC--GTA-TAA
+    # 01    234 567
+    im = IndelMap(
+        gap_pos=numpy.array([2, 5], dtype=int),
+        gap_lengths=numpy.array([2, 1], dtype=int),
+        parent_length=8,
+    )
+    orig_spans = [Span(1, 5)]
+    align_map = FeatureMap(spans=orig_spans, parent_length=11)
+    spans = [Span(1, 3)]
+    expect = FeatureMap(spans=spans, parent_length=8)
+    got = im.make_seq_feature_map(align_map)
+    assert got.get_coordinates() == expect.get_coordinates()
+    assert got.parent_length == expect.parent_length
+
+    # ignoring lost spans
+    align_map = FeatureMap(spans=orig_spans + [LostSpan(4)], parent_length=11)
+    got = im.make_seq_feature_map(align_map)
+    assert got.get_coordinates() == expect.get_coordinates()
+    assert got.parent_length == expect.parent_length