Repository: cogent3/cogent3
Pull request #1730 (2024-02-24 01:20:03)

Migrate develop branch changes to refactor-of-map branch


Patch:
diff --git a/pyproject.toml b/pyproject.toml
index db217f66e..8bbddd899 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,6 +36,7 @@ classifiers = [
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3.12",
     ]
 
 [project.urls]
diff --git a/src/cogent3/align/traceback.py b/src/cogent3/align/traceback.py
index 16a8d0ceb..626d30c09 100644
--- a/src/cogent3/align/traceback.py
+++ b/src/cogent3/align/traceback.py
@@ -3,7 +3,7 @@
 gapped sequences or Cogent Alignment objects"""
 
 from cogent3.core.alignment import Aligned, Alignment
-from cogent3.core.location import IndelMap
+from cogent3.core.annotation import Map
 
 
 def seq_traceback(s1, s2, aligned_positions, gap_value):
@@ -56,13 +56,10 @@ def gap_traceback(aligned_positions):
 
 
 def map_traceback(aligned_positions):
-    # using IndelMap's to keep track of gaps for indel alignment
+    # using Map's to keep track of gaps for indel alignment
     (starts, ends, gap_vectors, alignment_len) = gap_traceback(aligned_positions)
     # print 'gv', gap_vectors
-    maps = [
-        IndelMap(locations=gv, parent_length=alignment_len).inverse()
-        for gv in gap_vectors
-    ]
+    maps = [Map(gv, parent_length=alignment_len).inverse() for gv in gap_vectors]
     return (starts, ends, maps)
 
 
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index b8e982ba3..77a317b1a 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -56,7 +56,7 @@ from numpy.random import choice, permutation, randint
 import cogent3  # will use to get at cogent3.parse.fasta.MinimalFastaParser,
 
 from cogent3._version import __version__
-from cogent3.core.annotation import Feature
+from cogent3.core.annotation import Feature, Map
 from cogent3.core.annotation_db import (
     BasicAnnotationDb,
     FeatureDataType,
@@ -66,7 +66,6 @@ from cogent3.core.annotation_db import (
 )
 from cogent3.core.genetic_code import get_code
 from cogent3.core.info import Info as InfoClass
-from cogent3.core.location import FeatureMap, IndelMap
 from cogent3.core.profile import PSSM, MotifCountsArray
 from cogent3.core.sequence import ArraySequence, Sequence, frac_same
 # which is a circular import otherwise.
@@ -637,12 +636,11 @@ class _SequenceCollectionBase:
             annotations.
         """
         if isinstance(self, Alignment):
-            *_, strand = self.seqs[0].data.parent_coordinates()
-
+            reversed = self.seqs[0].map.reverse
         else:
-            *_, strand = self.seqs[0].parent_coordinates()
-
-        reversed = strand == "-"
+            # WARNING GAH this is tightly coupling to implementation of seq attribute!
+            # todo GAH make method on Sequence?
+            reversed = self.seqs[0]._seq.reverse
         new_seqs = dict()
         db = None if reversed and sliced else deepcopy(self.annotation_db)
         for seq in self.seqs:
@@ -2147,7 +2145,7 @@ class Aligned:
         # Unlike the normal map constructor, here we take a list of pairs of
         # alignment coordinates, NOT a list of pairs of sequence coordinates
         if isinstance(map, list):
-            map = IndelMap(locations=map, parent_length=length).inverse()
+            map = Map(map, parent_length=length).inverse()
         self.map = map
         self.data = data
         if hasattr(data, "info"):
@@ -2191,8 +2189,7 @@ class Aligned:
         new_seq = self.data.copy(exclude_annotations=exclude_annotations, sliced=sliced)
         if sliced:
             db = new_seq.annotation_db
-            *_, strand = self.data.parent_coordinates()
-            if strand == "-" or exclude_annotations:
+            if self.map.reverse or exclude_annotations:
                 new_seq.annotation_db = None
             else:
                 new_seq.annotation_offset = self.map.start
@@ -2244,23 +2241,20 @@ class Aligned:
 
     def __add__(self, other):
         if self.data is other.data:
-            map, seq = self.map + other.map, self.data
+            (map, seq) = (self.map + other.map, self.data)
         else:
             seq = self.get_gapped_seq() + other.get_gapped_seq()
-            map, seq = seq.parse_out_gaps()
+            (map, seq) = seq.parse_out_gaps()
         return Aligned(map, seq)
 
     def __getitem__(self, slice):
-        # todo we need to get the sequence coordinates that slice corresponds to
-        #  so we can update the self.data, plus we will need to zero new_map
         new_map = self.map[slice]
         data = (
-            self.data[new_map.start : new_map.end] if new_map.useful else self.data[:0]
+            self.data[new_map.start : new_map.end] if new_map.useful else self.data[0:0]
         )
-        if new_map.useful and new_map.start > new_map.end:
-            # For now, a reverse slice means we should have an empty sequence
-            # todo modify this clause if a negative step is ever allowed
-            new_map = new_map.__class__(locations=(), parent_length=len(self.data))
+        if new_map.reverse:
+            # A reverse slice means we should have an empty sequence
+            new_map = type(new_map)(locations=(), parent_length=len(self.data))
         elif new_map.useful:
             new_map = new_map.zeroed()
         return Aligned(new_map, data)
@@ -2303,7 +2297,7 @@ class Aligned:
             deserialise_seq,
         )
 
-        map_ = IndelMap.from_rich_dict(data["map_init"])
+        map_ = deserialise_map_spans(data["map_init"])
         seq = deserialise_seq(data["seq_init"])
         return cls(map_, seq)
 
@@ -2331,9 +2325,7 @@ class Aligned:
     def make_feature(self, feature: FeatureDataType, alignment: "Alignment") -> Feature:
         """returns a feature, not written into annotation_db"""
         annot = self.data.make_feature(feature)
-        inverted = self.map.inverse()
-        # todo should indicate whether tidy or not
-        return annot.remapped_to(alignment, inverted)
+        return annot.remapped_to(alignment, self.map.inverse())
 
     def gap_vector(self):
         """Returns gap_vector of GappedSeq, for omit_gap_pos."""
@@ -2834,7 +2826,7 @@ class AlignmentI(object):
         positions = [
             (loc * motif_length, (loc + 1) * motif_length) for loc in locations
         ]
-        sample = IndelMap(locations=positions, parent_length=len(self))
+        sample = Map(positions, parent_length=len(self))
         return self.gapped_by_map(sample, info=self.info)
 
     def sliding_windows(self, window, step, start=None, end=None):
@@ -4667,7 +4659,7 @@ class Alignment(AlignmentI, SequenceCollection):
                 raise ValueError(f"feature.parent {index.seqid!r} is not self")
             return index.get_slice()
 
-        if isinstance(index, (FeatureMap, IndelMap)):
+        if isinstance(index, Map):
             new = self._mapped(index)
 
         elif isinstance(index, (int, slice)):
@@ -4841,7 +4833,7 @@ class Alignment(AlignmentI, SequenceCollection):
 
         locations = [(gv[i], gv[i + 1]) for i in range(0, len(gv), 2)]
 
-        keep = IndelMap(locations=locations, parent_length=len(self))
+        keep = Map(locations, parent_length=len(self))
         return self.gapped_by_map(keep, info=self.info)
 
     def get_seq(self, seqname):
@@ -4849,7 +4841,8 @@ class Alignment(AlignmentI, SequenceCollection):
 
         Note: always returns Sequence object, not ArraySequence.
         """
-        return self.named_seqs[seqname].data
+        seq = self.named_seqs[seqname]
+        return seq.data[seq.map.without_gaps()]
 
     def get_gapped_seq(self, seq_name, recode_gaps=False):
         """Return a gapped Sequence object for the specified seqname.
@@ -5239,7 +5232,7 @@ class Alignment(AlignmentI, SequenceCollection):
         # in Sequence?
         revd = feature.pop("strand", None) == "-"
         feature["strand"] = "-" if revd else "+"
-        fmap = FeatureMap(parent_length=len(self), locations=feature.pop("spans"))
+        fmap = Map(parent_length=len(self), locations=feature.pop("spans"))
         if revd:
             fmap = fmap.nucleic_reversed()
         return Feature(parent=self, map=fmap, **feature)
diff --git a/src/cogent3/core/annotation.py b/src/cogent3/core/annotation.py
index 8ddc86a6e..5f8a4270b 100644
--- a/src/cogent3/core/annotation.py
+++ b/src/cogent3/core/annotation.py
@@ -2,7 +2,7 @@ from typing import Iterable, Optional
 
 from numpy import array
 
-from .location import FeatureMap
+from .location import Map
 
 
 # todo gah write docstrings!
@@ -23,14 +23,7 @@ class Feature:
 
     # todo gah implement a __new__ to trap args for serialisation purposes?
     def __init__(
-        self,
-        *,
-        parent,
-        seqid: str,
-        map: FeatureMap,
-        biotype: str,
-        name: str,
-        strand: str,
+        self, *, parent, seqid: str, map: Map, biotype: str, name: str, strand: str
     ):
         # _serialisable is used for creating derivative instances
         d = locals()
@@ -154,11 +147,6 @@ class Feature:
         return f"{name}({txt})"
 
     def remapped_to(self, grandparent, gmap):
-        # grandparent can be either a Sequence or an Alignment
-        if not isinstance(gmap, FeatureMap):
-            # due to separation of IndelMap and Map, change class
-            gmap = gmap.to_feature_map()
-
         seqid = grandparent.name or f"from {self.seqid!r}"
         kwargs = {
             **self._serialisable,
@@ -225,13 +213,13 @@ class Feature:
         -----
         Overlapping spans are merged
         """
-        combined = list(self.map.spans)
+        combined = self.map.spans[:]
         feat_names = [self.name] if self.name else set()
         biotypes = {self.biotype} if self.biotype else set()
         seqids = {self.seqid} if self.seqid else set()
         for feature in features:
             if feature.parent is not self.parent:
-                raise ValueError("cannot merge annotations from different objects")
+                raise ValueError(f"cannot merge annotations from different objects")
 
             combined.extend(feature.map.spans)
             if feature.name:
@@ -241,7 +229,7 @@ class Feature:
             if feature.biotype:
                 biotypes.add(feature.biotype)
         name = ", ".join(feat_names)
-        map = FeatureMap(spans=combined, parent_length=len(self.parent))
+        map = Map(spans=combined, parent_length=len(self.parent))
         map = map.covered()  # No overlaps
         # the covered method drops reversed status so we need to
         # resurrect that, but noting we've not checked consistency
diff --git a/src/cogent3/core/location.py b/src/cogent3/core/location.py
index 9803c7830..db6c9d67c 100644
--- a/src/cogent3/core/location.py
+++ b/src/cogent3/core/location.py
@@ -20,14 +20,11 @@ and d), etc. Similarly, Span(0,2) does _not_ overlap Span(2,3), since the
 former contains a and b while the latter contains c.
 """
 import copy
-import dataclasses
-import inspect
 
-from abc import ABC, abstractmethod
 from bisect import bisect_left, bisect_right
 from functools import total_ordering
 from itertools import chain
-from typing import Iterator, List, Optional, Sequence, Tuple, Union
+from typing import Union
 
 from numpy import array, ndarray
 
@@ -69,21 +66,25 @@ def _norm_slice(index, length):
         return (start, start + 1, 1)
 
 
-def as_map(slice, length, cls):
+def as_map(slice, length):
     """Take anything that might be used as a subscript: Integer, Slice,
-    or MapABC, and return cls."""
+    or Map, and return a Map."""
 
     if isinstance(slice, (list, tuple)):
         spans = []
         for i in slice:
-            spans.extend(as_map(i, length, cls).spans)
-        return cls(spans=spans, parent_length=length)
-    elif isinstance(slice, (FeatureMap, IndelMap)):
-        return slice
+            spans.extend(as_map(i, length).spans)
+        map = Map(spans=spans, parent_length=length)
+    elif isinstance(slice, Map):
+        map = slice
+        # TODO reasons for failure when the following is not commented out
+        # should be checked further
+        # assert map.parent_length == length, (map, length)
     else:
-        lo, hi, step = _norm_slice(slice, length)
+        (lo, hi, step) = _norm_slice(slice, length)
         assert (step or 1) == 1
-        return cls(locations=[(lo, hi)], parent_length=length)
+        map = Map([(lo, hi)], parent_length=length)
+    return map
 
 
 class SpanI(object):
@@ -322,13 +323,13 @@ class Span(SpanI):
         C is a span of a feature on B which itself is a feature on A, so to
         place C on A return that part of B (map) covered by C (self)"""
 
-        offsets, spans = map.offsets, list(map.spans)
+        (offsets, spans) = (map.offsets, map.spans)
         map_length = offsets[-1] + spans[-1].length
 
         # don't try to remap any non-corresponding end region(s)
         # this won't matter if all spans lie properly within their
         # parent maps, but that might not be true of Display slices.
-        zlo, zhi = max(0, self.start), min(map_length, self.end)
+        (zlo, zhi) = (max(0, self.start), min(map_length, self.end))
 
         # Find the right span(s) of the map
         first = bisect_right(offsets, zlo) - 1
@@ -449,7 +450,7 @@ class Span(SpanI):
             return type(self) == type(other)
 
 
-class _LostSpan:
+class _LostSpan(object):
     """A placeholder span which doesn't exist in the underlying sequence"""
 
     __slots__ = ["length", "value", "_serialisable"]
@@ -528,7 +529,7 @@ class TerminalPadding(_LostSpan):
         return f"?{self.length}?"
 
 
-class Map:  # pragma: no cover
+class Map:
     """A map holds a list of spans."""
 
     def __init__(
@@ -551,7 +552,7 @@ class Map:  # pragma: no cover
                 reverse = start > end
                 if max(start, end) < 0 or min(start, end) > parent_length:
                     raise RuntimeError(
-                        f"located outside sequence: {(start, end, parent_length)}"
+                        f"located outside sequence: {str((start, end, parent_length))}"
                     )
                 if max(start, end) > parent_length and min(start, end) < 0:
                     l_diff = min(start, end)
@@ -604,13 +605,12 @@ class Map:  # pragma: no cover
                     self.reverse = None
 
         if termini_unknown:
-            spans = list(spans)
             if spans[0].lost:
                 spans[0] = TerminalPadding(spans[0].length)
             if spans[-1].lost:
                 spans[-1] = TerminalPadding(spans[-1].length)
 
-        self.spans = tuple(spans)
+        self.spans = spans
         self.length = posn
         self.parent_length = parent_length
         self.__inverse = None
@@ -619,11 +619,11 @@ class Map:  # pragma: no cover
         return self.length
 
     def __repr__(self):
-        return repr(list(self.spans)) + f"/{self.parent_length}"
+        return repr(self.spans) + f"/{self.parent_length}"
 
     def __getitem__(self, slice):
         # A possible shorter map at the same level
-        slice = as_map(slice, len(self), self.__class__)
+        slice = as_map(slice, len(self))
         new_parts = []
         for span in slice.spans:
             new_parts.extend(span.remap_with(self))
@@ -1099,836 +1099,7 @@ def RangeFromString(string, delimiter=","):  # pragma: no cover
     return result
 
 
-class MapABC(ABC):
-    """base class for genomic map objects"""
-
-    def __new__(cls, *args, **kwargs):
-        obj = object.__new__(cls)
-        init_sig = inspect.signature(cls.__init__)
-        bargs = init_sig.bind_partial(cls, *args, **kwargs)
-        bargs.apply_defaults()
-        init_vals = bargs.arguments
-        init_vals.pop("self", None)
-
-        obj._serialisable = init_vals
-        return obj
-
-    @abstractmethod
-    def __len__(self):
-        ...
-
-    @abstractmethod
-    def __add__(self, other):
-        ...
-
-    @classmethod
-    @abstractmethod
-    def from_rich_dict(cls, data):
-        ...
-
-    @abstractmethod
-    def gaps(self):
-        ...
-
-    @abstractmethod
-    def nongap(self):
-        ...
-
-    @abstractmethod
-    def get_coordinates(self):
-        ...
-
-    @abstractmethod
-    def nucleic_reversed(self):
-        ...
-
-    @abstractmethod
-    def to_rich_dict(self):
-        ...
-
-    @abstractmethod
-    def inverse(self):
-        ...
-
-
-def _spans_from_locations(locations, parent_length) -> Tuple[Union[Span, _LostSpan]]:
-    spans = []
-    for start, end in locations:
-        reverse = start > end
-        if max(start, end) < 0 or min(start, end) > parent_length:
-            raise RuntimeError(
-                f"located outside sequence: {(start, end, parent_length)}"
-            )
-        if max(start, end) > parent_length and min(start, end) < 0:
-            l_diff = min(start, end)
-            r_diff = max(start, end) - parent_length
-            start, end = (0, parent_length) if start < end else (parent_length, 0)
-            spans += [
-                LostSpan(abs(l_diff)),
-                Span(start, end, reverse=reverse),
-                LostSpan(abs(r_diff)),
-            ]
-        elif min(start, end) < 0:
-            diff = min(start, end)
-            start = max(start, 0)
-            end = max(end, 0)
-            spans += [
-                LostSpan(abs(diff)),
-                Span(start, end, reverse=reverse),
-            ]
-        elif max(start, end) > parent_length:
-            diff = max(start, end) - parent_length
-            start = min(start, parent_length)
-            end = min(end, parent_length)
-            spans += [
-                Span(start, end, reverse=reverse),
-                LostSpan(abs(diff)),
-            ]
-        else:
-            spans += [Span(start, end, reverse=reverse)]
-    return tuple(spans)
-
-
-T = Union[List[int], Tuple[int]]
-
-
-@dataclasses.dataclass
-class IndelMap(MapABC):
-    """store locations of deletions in a Aligned sequence"""
-
-    # todo design notes
-    #  I think this object should never try and store "history", i.e. it
-    #  should directly relate to the current sequence only. Let that sequence,
-    #  which is represented by a SeqView, store history.
-    #  Following ths, storing reverse is also a bad idea for this object,
-    #  also done by the SeqView only.
-    # TODO reverse complement of Alignment -> Aligned -> SeqView, IndelMap
-    #  should just do nucleic reverse. I think this is the next task.
-
-    spans: dataclasses.InitVar[Optional[tuple]] = ()
-    locations: dataclasses.InitVar[Optional[Sequence[T]]] = None
-    termini_unknown: dataclasses.InitVar[bool] = dataclasses.field(default=False)
-    parent_length: int = 0
-    start: Optional[int] = dataclasses.field(init=False, default=0)
-    end: Optional[int] = dataclasses.field(init=False, default=0)
-    length: int = dataclasses.field(init=False, default=0)
-    _serialisable: dict = dataclasses.field(init=False, repr=False)
-    _spans: Tuple[Union[Span, _LostSpan, TerminalPadding]] = dataclasses.field(
-        default=(), init=False
-    )
-
-    def __post_init__(self, spans, locations, termini_unknown):
-        if locations:
-            spans = _spans_from_locations(locations, parent_length=self.parent_length)
-        elif isinstance(spans, property):
-            # This clause is due a known issue with dataclasses.
-            # As we have a spans property, the default spans value is
-            # ignored, so we have to check for its value being a property
-            # and then set the default value here
-            spans = ()
-
-        spans = tuple(spans)
-        last_not_lost = None
-        start, end = None, None
-        for i, span in enumerate(spans):
-            self.length += len(span)
-            if span.lost:
-                continue
-            elif start is None:
-                # this ugly logic because we're using spans!
-                start = span.end if span.reverse else span.start
-
-            last_not_lost = i
-
-        if last_not_lost is not None:
-            span = spans[last_not_lost]
-            end = span.start if span.reverse else span.end
-
-        if start is None:
-            start = 0
-
-        if end is None:
-            end = self.parent_length
-
-        self.start, self.end = start, end
-
-        if termini_unknown:
-            spans = list(spans)
-            if spans[0].lost:
-                spans[0] = TerminalPadding(spans[0].length)
-            if spans[-1].lost:
-                spans[-1] = TerminalPadding(spans[-1].length)
-
-        self._spans = tuple(spans)
-
-    def __getitem__(self, slice):
-        # A possible shorter map at the same level
-        new_map = as_map(slice, len(self), self.__class__)
-        new_parts = []
-        for span in new_map.spans:
-            # we reset tidy start / end to false to avoid changes to
-            # default behaviour of tidy on IndelMap (which ignores these
-            # anyway)
-            old_tidy = span.tidy_start, span.tidy_end
-            span.tidy_start, span.tidy_end = False, False
-            new_parts.extend(span.remap_with(self))
-            span.tidy_start, span.tidy_end = old_tidy
-        return self.__class__(spans=new_parts, parent_length=self.parent_length)
-
-    def __len__(self):
-        return self.length
-
-    def __add__(self, other):
-        if other.parent_length != self.parent_length:
-            raise ValueError("Those maps belong to different sequences")
-        return self.__class__(
-            spans=self._spans + tuple(other.spans), parent_length=self.parent_length
-        )
-
-    def __mul__(self, scale):
-        # For Protein -> DNA
-        new_parts = []
-        for span in self._spans:
-            new_parts.append(span * scale)
-        return self.__class__(spans=new_parts, parent_length=self.parent_length * scale)
-
-    def __repr__(self):
-        return repr(self._spans) + f"/{self.parent_length}"
-
-    @property
-    def offsets(self):
-        return [0] + array([s.length for s in self._spans[:-1]]).cumsum().tolist()
-
-    def gaps(self):
-        """The gaps (lost spans) in this map"""
-        locations = []
-        offset = 0
-        for s in self._spans:
-            if s.lost:
-                locations.append((offset, offset + s.length))
-            offset += s.length
-        return self.__class__(locations=locations, parent_length=len(self))
-
-    def nongap(self):
-        """ungappeed segments in this map"""
-        locations = []
-        offset = 0
-        for s in self._spans:
-            if not s.lost:
-                locations.append((offset, offset + s.length))
-            offset += s.length
-        return self.__class__(locations=locations, parent_length=len(self))
-
-    @property
-    def spans(self) -> Iterator[Span]:
-        """generator of spans"""
-        yield from self._spans
-
-    @property
-    def complete(self):
-        """whether any span represents a gap"""
-        return not any(span.lost for span in self._spans)
-
-    @property
-    def useful(self):
-        return not all(span.lost for span in self._spans)
-
-    def get_coordinates(self):
-        """returns span coordinates as [(start, end), ...]"""
-        return [(s.start, s.end) for s in self._spans if not s.lost]
-
-    def get_gap_coordinates(self):
-        """returns [(gap pos, gap length), ...]"""
-        gap_pos = []
-        for i, span in enumerate(self._spans):
-            if not span.lost:
-                continue
-
-            pos = self._spans[i - 1].end if i else 0
-            gap_pos.append((pos, len(span)))
-
-        return gap_pos
-
-    def nucleic_reversed(self):
-        """Same location on reversed parent"""
-        spans = [s.reversed_relative_to(self.parent_length) for s in self._spans]
-        return self.__class__(spans=spans, parent_length=self.parent_length)
-
-    def strict_nucleic_reversed(self):
-        """map for a sequence that has itself been reversed and complemented
-
-        Notes
-        -----
-        discards reverse attribute on both spans and self
-        """
-        spans = []
-        parent_length = self.parent_length
-        for s in self._spans:
-            if not s.lost:
-                start = parent_length - s.end
-                assert start >= 0
-                end = start + s.length
-                s = Span(start=start, end=end)
-            spans.append(s)
-
-        spans.reverse()
-        return self.__class__(spans=spans, parent_length=self.parent_length)
-
-    def to_rich_dict(self):
-        """returns dicts for contained spans [dict(), ..]"""
-        spans = [s.to_rich_dict() for s in self._spans]
-        # exclude spans from deep copy since being overwritten
-        data = copy.deepcopy(
-            {k: v for k, v in self._serialisable.items() if k != "spans"}
-        )
-        data.pop("locations")
-        data["spans"] = spans
-        data["type"] = get_object_provenance(self)
-        data["version"] = __version__
-        return data
-
-    @classmethod
-    def from_rich_dict(cls, map_element):
-        from cogent3.util.deserialise import _get_class
-
-        map_element.pop("version", None)
-        type_ = map_element.pop("type")
-        assert _get_class(type_) == cls
-        spans = []
-        for element in map_element["spans"]:
-            element.pop("version", None)
-            klass = _get_class(element.pop("type"))
-            instance = klass(**element)
-            spans.append(instance)
-
-        map_element["spans"] = spans
-        return cls(**map_element)
-
-    def with_termini_unknown(self):
-        """returns new instance with terminal gaps indicated as unknown"""
-        return self.__class__(
-            spans=self._spans[:],
-            parent_length=self.parent_length,
-            termini_unknown=True,
-        )
-
-    def inverse(self):
-        """returns instance with coordinates updated for aligned, unaligned"""
-        # is this only required for parse_out_gaps?
-        # NO also used in cogent3.align code
-
-        # can't work if there are overlaps in the map
-        # tidy ends don't survive inversion
-        if self.parent_length is None:
-            raise ValueError("Uninvertable. parent length not known")
-
-        cum_posn = 0
-        temp = []
-        for span in self._spans:
-            if not span.lost:
-                if span.reverse:
-                    temp.append(
-                        (span.start, span.end, cum_posn + span.length, cum_posn)
-                    )
-                else:
-                    temp.append(
-                        (span.start, span.end, cum_posn, cum_posn + span.length)
-                    )
-            cum_posn += span.length
-
-        temp.sort()
-        new_spans = []
-        last_start = 0
-        for start, end, cum_start, cum_end in temp:
-            if start > last_start:
-                new_spans.append(LostSpan(start - last_start))
-            elif start < last_start:
-                raise ValueError(f"Uninvertable. Overlap: {start} < {last_start}")
-
-            # we force tidy_<start/end> to be same as self, attribute has no meaning
-            # for IndelMap, but retained for compatability for now
-            new_spans.append(
-                Span(
-                    cum_start,
-                    cum_end,
-                    reverse=cum_start > cum_end,
-                )
-            )
-            last_start = end
-
-        if self.parent_length > last_start:
-            new_spans.append(LostSpan(self.parent_length - last_start))
-
-        return self.__class__(spans=new_spans, parent_length=len(self))
-
-    T = Union[ndarray, int]
-
-    def absolute_position(self, rel_pos: T) -> T:
-        """converts rel_pos into an absolute position
-
-        Raises
-        ------
-        raises ValueError if rel_pos < 0
-        """
-        check = array([rel_pos], dtype=int) 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
-
-    def relative_position(self, abs_pos: T) -> T:
-        """converts abs_pos into an relative position
-
-        Raises
-        ------
-        raises ValueError if abs_pos < 0
-        """
-        check = array([abs_pos], dtype=int) 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
-
-    def get_covering_span(self):
-        span = (self.end, self.start)
-        return self.__class__(locations=[span], parent_length=self.parent_length)
-
-    def zeroed(self):
-        """returns a new instance with the first span starting at 0
-
-        Note
-        ----
-
-        Useful when an annotated sequence is sliced, but the connection to
-        the original parent is being deliberately broken as in the
-        Sequence.deepcopy(sliced=True) case.
-        """
-        # todo is this really required, best ifn we can rely on SeqView to
-        #  store all relationship to underlying sequence
-        min_val = min(self.start, self.end)
-        spans = []
-        for span in self._spans:
-            if span.lost:
-                spans.append(span)
-                continue
-            kwargs = span.to_rich_dict()
-            del kwargs["version"]
-            del kwargs["type"]
-            kwargs["start"] = kwargs["start"] - min_val
-            kwargs["end"] = kwargs["end"] - min_val
-            spans.append(Span(**kwargs))
-
-        kwargs = self.to_rich_dict()
-        del kwargs["version"]
-        del kwargs["type"]
-        kwargs["spans"] = spans
-        kwargs["parent_length"] = abs(self.start - self.end)
-        return self.__class__(**kwargs)
-
-    def to_feature_map(self):
-        """returns a Map type, suited to Features"""
-        spans = []
-        for span in self._spans:
-            if span.lost:
-                spans.append(span)
-                continue
-            kwargs = span.to_rich_dict()
-            del kwargs["version"]
-            del kwargs["type"]
-            spans.append(Span(**kwargs))
-
-        kwargs = self.to_rich_dict()
-        del kwargs["version"]
-        del kwargs["type"]
-        kwargs["spans"] = spans
-        return FeatureMap(**kwargs)
-
-
-class FeatureMap:
-    """A map holds a list of spans."""
-
-    def __init__(
-        self,
-        locations=None,
-        spans=None,
-        tidy=False,
-        parent_length=None,
-        termini_unknown=False,
-    ):
-        assert parent_length is not None
-        d = locals()
-        exclude = ("self", "__class__", "__slots__")
-        self._serialisable = {k: v for k, v in d.items() if k not in exclude}
-
-        if spans is None:
-            spans = []
-            for start, end in locations:
-                diff = 0
-                reverse = start > end
-                if max(start, end) < 0 or min(start, end) > parent_length:
-                    raise RuntimeError(
-                        f"located outside sequence: {(start, end, parent_length)}"
-                    )
-                if max(start, end) > parent_length and min(start, end) < 0:
-                    l_diff = min(start, end)
-                    r_diff = max(start, end) - parent_length
-                    start, end = (
-                        (0, parent_length) if start < end else (parent_length, 0)
-                    )
-                    spans += [
-                        LostSpan(abs(l_diff)),
-                        Span(start, end, tidy, tidy, reverse=reverse),
-                        LostSpan(abs(r_diff)),
-                    ]
-                elif min(start, end) < 0:
-                    diff = min(start, end)
-                    start = 0 if start < 0 else start
-                    end = 0 if end < 0 else end
-                    spans += [
-                        LostSpan(abs(diff)),
-                        Span(start, end, tidy, tidy, reverse=reverse),
-                    ]
-                elif max(start, end) > parent_length:
-                    diff = max(start, end) - parent_length
-                    start = parent_length if start > parent_length else start
-                    end = parent_length if end > parent_length else end
-                    spans += [
-                        Span(start, end, tidy, tidy, reverse=reverse),
-                        LostSpan(abs(diff)),
-                    ]
-                else:
-                    spans += [Span(start, end, tidy, tidy, reverse=reverse)]
-
-        self.offsets = []
-        self.useful = False
-        self.complete = True
-        self.reverse = None
-        posn = 0
-        for span in spans:
-            self.offsets.append(posn)
-            posn += span.length
-            if span.lost:
-                self.complete = False
-            elif not self.useful:
-                self.useful = True
-                (self.start, self.end) = (span.start, span.end)
-                self.reverse = span.reverse
-            else:
-                self.start = min(self.start, span.start)
-                self.end = max(self.end, span.end)
-                if self.reverse is not None and (span.reverse != self.reverse):
-                    self.reverse = None
-
-        if termini_unknown:
-            spans = list(spans)
-            if spans[0].lost:
-                spans[0] = TerminalPadding(spans[0].length)
-            if spans[-1].lost:
-                spans[-1] = TerminalPadding(spans[-1].length)
-
-        self.spans = tuple(spans)
-        self.length = posn
-        self.parent_length = parent_length
-
-    def __len__(self):
-        return self.length
-
-    def __repr__(self):
-        return repr(list(self.spans)) + f"/{self.parent_length}"
-
-    def __getitem__(self, new_map):
-        # A possible shorter map at the same level
-        new_map = as_map(new_map, len(self), self.__class__)
-        new_parts = []
-        for span in new_map.spans:
-            new_parts.extend(span.remap_with(self))
-        return self.__class__(spans=new_parts, parent_length=self.parent_length)
-
-    def __mul__(self, scale):
-        new_parts = [span * scale for span in self.spans]
-        return self.__class__(spans=new_parts, parent_length=self.parent_length * scale)
-
-    def __div__(self, scale):
-        new_parts = [span / scale for span in self.spans]
-        return self.__class__(
-            spans=new_parts, parent_length=self.parent_length // scale
-        )
-
-    def __add__(self, other):
-        if other.parent_length != self.parent_length:
-            raise ValueError("Those maps belong to different sequences")
-        return self.__class__(
-            spans=self.spans + other.spans, parent_length=self.parent_length
-        )
-
-    def _with_termini_unknown(self):
-        return self.__class__(
-            self,
-            spans=self.spans[:],
-            parent_length=self.parent_length,
-            termini_unknown=True,
-        )
-
-    def get_covering_span(self):
-        if self.reverse:
-            span = (self.end, self.start)
-        else:
-            span = (self.start, self.end)
-        return self.__class__([span], parent_length=self.parent_length)
-
-    def covered(self):
-        """>>> Map([(10,20), (15, 25), (80, 90)]).covered().spans
-        [Span(10,25), Span(80, 90)]"""
-
-        delta = {}
-        for span in self.spans:
-            if span.lost:
-                continue
-            delta[span.start] = delta.get(span.start, 0) + 1
-            delta[span.end] = delta.get(span.end, 0) - 1
-        positions = sorted(delta.keys())
-        last_y = y = 0
-        last_x = start = None
-        result = []
-        for x in positions:
-            y += delta[x]
-            if x == last_x:
-                continue
-            if y and not last_y:
-                assert start is None
-                start = x
-            elif last_y and not y:
-                result.append((start, x))
-                start = None
-            last_x = x
-            last_y = y
-        assert y == 0
-        return self.__class__(locations=result, parent_length=self.parent_length)
-
-    def reversed(self):
-        """Reversed location on same parent"""
-        spans = [s.reversed() for s in self.spans]
-        spans.reverse()
-        return self.__class__(spans=spans, parent_length=self.parent_length)
-
-    def nucleic_reversed(self):
-        """Same location on reversed parent"""
-        spans = [s.reversed_relative_to(self.parent_length) for s in self.spans]
-        return self.__class__(spans=spans, parent_length=self.parent_length)
-
-    def get_gap_coordinates(self):
-        """returns [(gap pos, gap length), ...]"""
-        gap_pos = []
-        for i, span in enumerate(self.spans):
-            if not span.lost:
-                continue
-
-            pos = self.spans[i - 1].end if i else 0
-            gap_pos.append((pos, len(span)))
-
-        return gap_pos
-
-    def gaps(self):
-        """The gaps (lost spans) in this map"""
-        locations = []
-        offset = 0
-        for s in self.spans:
-            if s.lost:
-                locations.append((offset, offset + s.length))
-            offset += s.length
-        return self.__class__(locations, parent_length=len(self))
-
-    def shadow(self):
-        """The 'negative' map of the spans not included in this map"""
-        return self.inverse().gaps()
-
-    def nongap(self):
-        locations = []
-        offset = 0
-        for s in self.spans:
-            if not s.lost:
-                locations.append((offset, offset + s.length))
-            offset += s.length
-        return self.__class__(locations, parent_length=len(self))
-
-    def without_gaps(self):
-        return self.__class__(
-            spans=[s for s in self.spans if not s.lost],
-            parent_length=self.parent_length,
-        )
-
-    def inverse(self):
-        """returns instance with coordinates updated for aligned, unaligned"""
-        # is this only required for parse_out_gaps?
-        # NO also used in cogent3.align code
-
-        # can't work if there are overlaps in the map
-        # tidy ends don't survive inversion
-        if self.parent_length is None:
-            raise ValueError("Uninvertable. parent length not known")
-
-        cum_posn = 0
-        temp = []
-        for span in self.spans:
-            if not span.lost:
-                if span.reverse:
-                    temp.append(
-                        (span.start, span.end, cum_posn + span.length, cum_posn)
-                    )
-                else:
-                    temp.append(
-                        (span.start, span.end, cum_posn, cum_posn + span.length)
-                    )
-            cum_posn += span.length
-
-        temp.sort()
-        new_spans = []
-        last_start = 0
-        for start, end, cum_start, cum_end in temp:
-            if start > last_start:
-                new_spans.append(LostSpan(start - last_start))
-            elif start < last_start:
-                raise ValueError(f"Uninvertable. Overlap: {start} < {last_start}")
-
-            # we force tidy_<start/end> to be same as self, attribute has no meaning
-            # for IndelMap, but retained for compatability for now
-            new_spans.append(
-                Span(
-                    cum_start,
-                    cum_end,
-                    reverse=cum_start > cum_end,
-                )
-            )
-            last_start = end
-
-        if self.parent_length > last_start:
-            new_spans.append(LostSpan(self.parent_length - last_start))
-
-        return self.__class__(spans=new_spans, parent_length=len(self))
-
-    def _inverse(self):
-        # can't work if there are overlaps in the map
-        # tidy ends don't survive inversion
-        if self.parent_length is None:
-            raise ValueError("Uninvertable. parent length not known")
-        posn = 0
-        temp = []
-        for span in self.spans:
-            if not span.lost:
-                if span.reverse:
-                    temp.append((span.start, span.end, posn + span.length, posn))
-                else:
-                    temp.append((span.start, span.end, posn, posn + span.length))
-            posn += span.length
-
-        temp.sort()
-        new_spans = []
-        last_hi = 0
-        for lo, hi, start, end in temp:
-            if lo > last_hi:
-                new_spans.append(LostSpan(lo - last_hi))
-            elif lo < last_hi:
-                raise ValueError(f"Uninvertable. Overlap: {lo} < {last_hi}")
-            new_spans.append(Span(start, end, reverse=start > end))
-            last_hi = hi
-        if self.parent_length > last_hi:
-            new_spans.append(LostSpan(self.parent_length - last_hi))
-        return self.__class__(spans=new_spans, parent_length=len(self))
-
-    def get_coordinates(self):
-        """returns span coordinates as [(v1, v2), ...]
-
-        v1/v2 are (start, end) unless the map is reversed, in which case it will
-        be (end, start)"""
-
-        if self.reverse:
-            order_func = lambda x: (max(x), min(x))
-        else:
-            order_func = lambda x: x
-
-        coords = list(
-            map(order_func, [(s.start, s.end) for s in self.spans if not s.lost])
-        )
-
-        return coords
-
-    def to_rich_dict(self):
-        """returns dicts for contained spans [dict(), ..]"""
-        spans = [s.to_rich_dict() for s in self.spans]
-        data = copy.deepcopy(self._serialisable)
-        data.pop("locations")
-        data["spans"] = spans
-        data["type"] = get_object_provenance(self)
-        data["version"] = __version__
-        return data
-
-    def zeroed(self):
-        """returns a new instance with the first span starting at 0
-
-        Note
-        ----
-
-        Useful when an Annotatable object is sliced, but the connection to
-        the original parent is being deliberately broken as in the
-        Sequence.deepcopy(sliced=True) case.
-        """
-        # todo there's probably a more efficient way to do this
-        # create the new instance
-        from cogent3.util.deserialise import deserialise_map_spans
-
-        data = self.to_rich_dict()
-        zeroed = deserialise_map_spans(data)
-        zeroed.parent_length = len(zeroed.get_covering_span())
-        shift = min(zeroed.start, zeroed.end)
-        new_end = 0
-        for span in zeroed.spans:
-            if span.lost:
-                continue
-            span.start -= shift
-            span.end -= shift
-            new_end = max(new_end, span.end)
-
-        zeroed.start = 0
-        zeroed.end = new_end
-
-        return zeroed
-
-    T = Union[ndarray, int]
-
-    def absolute_position(self, rel_pos: T) -> T:
-        """converts rel_pos into an absolute position
-
-        Raises
-        ------
-        raises ValueError if rel_pos < 0
-        """
-        check = array([rel_pos], dtype=int) 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
-
-    def relative_position(self, abs_pos: T) -> T:
-        """converts abs_pos into an relative position
-
-        Raises
-        ------
-        raises ValueError if abs_pos < 0
-        """
-        check = array([abs_pos], dtype=int) 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
-
-
-def gap_coords_to_map(gaps_lengths: dict, seq_length: int) -> FeatureMap:
+def gap_coords_to_map(gaps_lengths: dict, seq_length: int) -> Map:
     """
     Parameters
     ----------
@@ -1943,7 +1114,7 @@ def gap_coords_to_map(gaps_lengths: dict, seq_length: int) -> FeatureMap:
     """
 
     if not gaps_lengths:
-        return FeatureMap([(0, seq_length)], parent_length=seq_length)
+        return Map([(0, seq_length)], parent_length=seq_length)
 
     spans = []
     last = pos = 0
@@ -1960,4 +1131,4 @@ def gap_coords_to_map(gaps_lengths: dict, seq_length: int) -> FeatureMap:
     if pos < seq_length:
         spans.append(Span(last, seq_length))
 
-    return FeatureMap(spans=spans, parent_length=seq_length)
+    return Map(spans=spans, parent_length=seq_length)
diff --git a/src/cogent3/core/sequence.py b/src/cogent3/core/sequence.py
index 0a5c6459b..14279b9dd 100644
--- a/src/cogent3/core/sequence.py
+++ b/src/cogent3/core/sequence.py
@@ -56,7 +56,7 @@ from cogent3.core.annotation_db import (
 )
 from cogent3.core.genetic_code import get_code
 from cogent3.core.info import Info as InfoClass
-from cogent3.core.location import FeatureMap, IndelMap, LostSpan
+from cogent3.core.location import LostSpan, Map
 from cogent3.format.fasta import alignment_to_fasta
 from cogent3.maths.stats.contingency import CategoryCounts
 from cogent3.maths.stats.number import CategoryCounter
@@ -1059,16 +1059,16 @@ class Sequence(SequenceI):
                 continue
             new_spans.append(new.tolist())
 
-        fmap = FeatureMap(locations=new_spans, parent_length=len(self))
+        fmap = Map(locations=new_spans, parent_length=len(self))
         if pre or post:
             # create a lost span to represent the segment missing from
             # the instance
-            spans = list(fmap.spans)
+            spans = fmap.spans
             if pre:
                 spans.insert(0, LostSpan(pre))
             if post:
                 spans.append(LostSpan(post))
-            fmap = FeatureMap(spans=spans, parent_length=len(self))
+            fmap = Map(spans=spans, parent_length=len(self))
 
         if revd and not seq_rced:
             # the sequence is on the plus strand, and the
@@ -1268,7 +1268,7 @@ class Sequence(SequenceI):
                 seqid=self.name,
                 name=None,
                 biotype=None,
-                map=FeatureMap(locations=[], parent_length=len(self)),
+                map=Map(locations=[], parent_length=len(self)),
                 strand="+",
             )
         else:
@@ -1344,7 +1344,7 @@ class Sequence(SequenceI):
         if hasattr(index, "map"):
             index = index.map
 
-        if isinstance(index, (FeatureMap, IndelMap)):
+        if isinstance(index, Map):
             new = self._mapped(index)
             preserve_offset = not index.reverse
 
@@ -1484,7 +1484,7 @@ class Sequence(SequenceI):
         for match in nongap.finditer(str(self)):
             segments.append(match.span())
             gapless.append(match.group())
-        map = IndelMap(locations=segments, parent_length=len(self)).inverse()
+        map = Map(segments, parent_length=len(self)).inverse()
         seq = self.__class__(
             "".join(gapless), name=self.get_name(), info=self.info, preserve_case=True
         )
diff --git a/src/cogent3/parse/cigar.py b/src/cogent3/parse/cigar.py
index 6c6dc28f6..77051e554 100644
--- a/src/cogent3/parse/cigar.py
+++ b/src/cogent3/parse/cigar.py
@@ -18,14 +18,14 @@ the aligned sequence will be:
 import re
 
 from cogent3 import DNA, make_aligned_seqs
-from cogent3.core.location import IndelMap, LostSpan, Span
+from cogent3.core.location import LostSpan, Map, Span
 
 
 _pattern = re.compile("([0-9]*)([DM])")
 
 
 def map_to_cigar(map):
-    """convert a IndelMap into a cigar string"""
+    """convert a Map into a cigar string"""
     cigar = ""
     for span in map.spans:
         if isinstance(span, Span):
@@ -39,7 +39,7 @@ def map_to_cigar(map):
 
 
 def cigar_to_map(cigar_text):
-    """convert cigar string into IndelMap"""
+    """convert cigar string into Map"""
     assert "I" not in cigar_text
     spans, posn = [], 0
     for n, c in _pattern.findall(cigar_text):
@@ -49,7 +49,7 @@ def cigar_to_map(cigar_text):
             posn += n
         else:
             spans.append(LostSpan(n))
-    return IndelMap(spans=spans, parent_length=posn)
+    return Map(spans=spans, parent_length=posn)
 
 
 def aligned_from_cigar(cigar_text, seq, moltype=DNA):
@@ -88,7 +88,7 @@ def _remap(map):
                 span.end = span.end - start
                 length = span.end
             spans.append(span)
-        new_map = IndelMap(spans=spans, parent_length=length)
+        new_map = Map(spans=spans, parent_length=length)
     return new_map
 
 
diff --git a/src/cogent3/parse/gbseq.py b/src/cogent3/parse/gbseq.py
index bc2712b70..cf0b501f5 100644
--- a/src/cogent3/parse/gbseq.py
+++ b/src/cogent3/parse/gbseq.py
@@ -5,7 +5,7 @@ DOCTYPE Bioseq-set PUBLIC "-//NCBI//NCBI Seqset/EN" "http://www.ncbi.nlm.nih.gov
 import io
 import xml.dom.minidom
 
-from cogent3.core import location, moltype
+from cogent3.core import annotation, moltype
 
 
 """
@@ -64,7 +64,7 @@ def GbSeqXmlParser(doc):
 
         seq = alphabet.make_seq(raw_string, name=name)
 
-        all = location.FeatureMap(locations=[(0, len(seq))], parent_length=len(seq))
+        all = annotation.Map([(0, len(seq))], parent_length=len(seq))
         seq.add_feature(
             biotype="source", name=name, spans=all.get_coordinates(), strand=all.reverse
         )
@@ -107,7 +107,7 @@ def GbSeqXmlParser(doc):
                         .nodeValue
                     )
                     spans.append((point - 1, point))
-            if not spans:
+            if spans == []:
                 spans = [(0, len(seq))]
             for qualifier in feature.getElementsByTagName("GBQualifier"):
                 qname = (


Test patch:
diff --git a/tests/test_core/test_alignment.py b/tests/test_core/test_alignment.py
index dd2be94b8..5dc949871 100644
--- a/tests/test_core/test_alignment.py
+++ b/tests/test_core/test_alignment.py
@@ -2827,7 +2827,7 @@ def test_aligned_rich_dict(reverse):
 
     rd = seq.to_rich_dict()
     got = Aligned.from_rich_dict(rd)
-    assert str(got) == str(seq)
+    assert str(seq) == str(got)
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment, ArrayAlignment))
@@ -3498,6 +3498,9 @@ def test_sliced_deepcopy(data, name, rev):
     if rev:
         aln = aln.rc()
 
+    # the map just mirrors the slice effect, it no longer has "memory" of rev complement
+    assert aln.named_seqs[name].map.reverse == False
+
     notsliced = aln.deepcopy(sliced=False)
     # the annotation offsets should match original object
     assert {s.data.annotation_offset for s in notsliced.seqs} == {
@@ -3522,6 +3525,10 @@ def test_sliced_deepcopy(data, name, rev):
         sliced.named_seqs[name].data._seq.seq is not orig.named_seqs[name].data._seq.seq
     )
 
+    # the map just mirrors the slice effect, it has no  "memory" of rev complement
+    assert notsliced.named_seqs[name].map.reverse == False
+    assert sliced.named_seqs[name].map.reverse == False
+
     assert sliced.named_seqs[name].map.parent_length == len(
         str(sliced_seq).replace("-", "")
     )
diff --git a/tests/test_core/test_annotation.py b/tests/test_core/test_annotation.py
index bddd156ff..e8f384cb8 100644
--- a/tests/test_core/test_annotation.py
+++ b/tests/test_core/test_annotation.py
@@ -4,7 +4,7 @@ import pytest
 
 from cogent3 import DNA, load_seq, make_aligned_seqs, make_unaligned_seqs
 from cogent3.core.alignment import Alignment, SequenceCollection
-from cogent3.core.location import FeatureMap, Span
+from cogent3.core.location import Map, Span
 
 
 def makeSampleSequence(name, with_gaps=False):
@@ -95,10 +95,10 @@ class TestMapSpans(unittest.TestCase):
         """reversing a map with multiple spans should preserve span relative
         order"""
         forward = [Span(20, 30), Span(40, 50)]
-        fmap = FeatureMap(spans=forward, parent_length=100)
+        fmap = Map(spans=forward, parent_length=100)
         fmap_reversed = fmap.nucleic_reversed()
         reverse = [Span(70, 80, reverse=True), Span(50, 60, reverse=True)]
-        rmap = FeatureMap(spans=reverse, parent_length=100)
+        rmap = Map(spans=reverse, parent_length=100)
         for i in range(2):
             self.assertEqual(fmap_reversed.spans[i], rmap.spans[i])
 
diff --git a/tests/test_core/test_annotation_db.py b/tests/test_core/test_annotation_db.py
index ac081d1a0..28e45da99 100644
--- a/tests/test_core/test_annotation_db.py
+++ b/tests/test_core/test_annotation_db.py
@@ -406,7 +406,7 @@ def test_feature_nucleic():
     from cogent3.core import location as loc
 
     seq = make_seq("AACCTTTGGGGAATTT", moltype="dna")
-    mmap = loc.FeatureMap(locations=[(4, 7), (11, 13)], parent_length=16)
+    mmap = loc.Map(locations=[(4, 7), (11, 13)], parent_length=16)
     expect = seq[mmap.reversed()]
 
     rcseq = seq.rc()
diff --git a/tests/test_core/test_location.py b/tests/test_core/test_location.py
index fc415a4c1..6c940da11 100644
--- a/tests/test_core/test_location.py
+++ b/tests/test_core/test_location.py
@@ -2,17 +2,8 @@
 """
 from unittest import TestCase
 
-import pytest
-
 from cogent3 import DNA
-from cogent3.core.location import (
-    FeatureMap,
-    IndelMap,
-    LostSpan,
-    Span,
-    TerminalPadding,
-    gap_coords_to_map,
-)
+from cogent3.core.location import Map, Span, gap_coords_to_map
 
 
 class SpanTests(TestCase):
@@ -266,13 +257,13 @@ class MapTests(TestCase):
     def test_get_coords(self):
         """get_coordinates should return raw coordinates matching input"""
         spans = [(0, 9), (20, 32)]
-        map = FeatureMap(spans, parent_length=100)
+        map = Map(spans, parent_length=100)
         coords = map.get_coordinates()
         self.assertEqual(coords, spans)
 
         # should work for reversed Maps too
         spans = [(32, 20), (9, 0)]
-        map = FeatureMap(spans, parent_length=100)
+        map = Map(spans, parent_length=100)
         coords = map.get_coordinates()
         self.assertEqual(coords, spans)
 
@@ -309,15 +300,14 @@ class MapTests(TestCase):
             got = gap_coords_to_map({20: 1}, len(seq))
 
 
-@pytest.mark.parametrize("cls", (IndelMap, FeatureMap))
-def test_map_plus_position(cls):
+def test_map_plus_position():
     # seq is 9 long
     # plus coords  012345678
     # +slice         **
     # plus seq     AAACCCTGG
 
     # orig = Aligned(*DNA.make_seq("AAACCCTGG", name="a").parse_out_gaps())
-    orig = cls(locations=[(0, 9)], parent_length=9)
+    orig = Map([(0, 9)], parent_length=9)
     assert orig.absolute_position(2) == 2
     assert orig.absolute_position(6) == 6
 
@@ -339,262 +329,3 @@ def test_map_plus_position(cls):
     rc = orig.nucleic_reversed()
     rcsubseq = rc[2:7]
     abs_coord = rcsubseq.absolute_position(0)
-
-
-def test_indel_map_useful_complete():
-    im = IndelMap(spans=[LostSpan(3)], parent_length=0)
-    assert not im.useful
-    assert not im.complete
-    assert len(im) == im.length == 3
-
-
-@pytest.mark.parametrize("cls,expect", ((FeatureMap, [(9, 0)]), (IndelMap, [(0, 9)])))
-def test_map_nucleic_reversed(cls, expect):
-    # seq is 9 long
-    # plus coords  012345678
-    # +slice         **
-    # plus seq     AAACCCTGG
-
-    # orig = Aligned(*DNA.make_seq("AAACCCTGG", name="a").parse_out_gaps())
-    orig = cls(locations=[(0, 9)], parent_length=9)
-    # minus coords 012345678
-    # rel coord      01234
-    # -slice         *****
-    # minus seq    CCAGGGTTT
-    # plus coords  876543210
-    rc = orig.nucleic_reversed()
-    coords = rc.get_coordinates()
-    assert coords == expect
-
-
-@pytest.mark.parametrize("cls", (FeatureMap, IndelMap))
-def test_coordinate(cls):
-    # coordinates are for ungapped segments in underlying sequence
-    #                   01   2 345
-    seq = DNA.make_seq("AC---G-TAA--")
-    m, _ = seq.parse_out_gaps()
-    m = cls(spans=tuple(m.spans), parent_length=m.parent_length)
-    got = m.get_coordinates()
-    assert got == [(0, 2), (2, 3), (3, 6)]
-
-
-@pytest.mark.parametrize("cls", (FeatureMap, IndelMap))
-def test_gap_coordinate(cls):
-    seq = DNA.make_seq("AC---G-TAA--")
-    m, _ = seq.parse_out_gaps()
-    m = cls(spans=tuple(m.spans), parent_length=m.parent_length)
-    got = m.get_gap_coordinates()
-    assert got == [(2, 3), (3, 1), (6, 2)]
-
-
-@pytest.mark.parametrize("cls", (FeatureMap, IndelMap))
-def test_gaps(cls):
-    # returns spans corresponding to position on "aligned" seq of gaps
-    #                   000000000011
-    #                   012345678901
-    seq = DNA.make_seq("AC---G-TAA--")
-    m, s = seq.parse_out_gaps()
-    m = cls(spans=tuple(m.spans), parent_length=m.parent_length)
-    got = [(g.start, g.end) for g in tuple(m.gaps().spans)]
-    assert got == [(2, 5), (6, 7), (10, 12)]
-
-
-@pytest.mark.parametrize("cls", (IndelMap, FeatureMap))
-def test_nongap(cls):
-    # returns spans corresponding to position on "aligned" seq of nongaps
-    #                   000000000011
-    #                   012345678901
-    seq = DNA.make_seq("AC---G-TAA--")
-    m, _ = seq.parse_out_gaps()
-    m = cls(spans=tuple(m.spans), parent_length=m.parent_length)
-
-    got = [(g.start, g.end) for g in m.nongap().spans]
-    assert got == [(0, 2), (5, 6), (7, 10)]
-
-
-def test_reversed():
-    seq = DNA.make_seq("AC---G-TAA--")
-    m, _ = seq.parse_out_gaps()
-    m = FeatureMap(spans=m.spans, parent_length=m.parent_length)
-    # reversed() reverses the order of spans, but keeps their coordinates
-    # differs from nucleic reversed, which computes a new relative position
-    rev = m.reversed()
-    got = [s.length if s.lost else (s.start, s.end) for s in rev.spans]
-    expect = [s.length if s.lost else (s.start, s.end) for s in m.spans]
-    expect.reverse()
-    assert got == expect
-
-
-def test_round_trip_rich_dict():
-    seq = DNA.make_seq("AC---G-TAA--")
-    m, s = seq.parse_out_gaps()
-    # reversed() reverses the order of spans, but keeps their coordinates
-    # differs from nucleic reversed, which computes a new relative position
-    im = IndelMap(spans=m.spans, parent_length=m.parent_length)
-    rd = im.to_rich_dict()
-    got = IndelMap.from_rich_dict(rd)
-    assert im is not got
-    assert got.to_rich_dict() == im.to_rich_dict()
-
-
-def test_serialisable_attr():
-    im = IndelMap(locations=[(0, 2)], parent_length=20)
-    set_vals = {"locations": [(0, 2)], "parent_length": 20}
-    got = {k: im._serialisable[k] for k in set_vals}
-    assert got == set_vals
-
-
-def test_terminal_unknown():
-    # span idx          01 2  345  6
-    seq = DNA.make_seq("-AC---G-TAA--")
-    m, _ = seq.parse_out_gaps()
-    # not unknown, by default
-    m_spans = tuple(m.spans)
-    assert m_spans[0].lost and not isinstance(m_spans[0], TerminalPadding)
-    # use the constructor arg
-    m = IndelMap(
-        spans=tuple(m.spans), parent_length=m.parent_length, termini_unknown=True
-    )
-    m_spans = tuple(m.spans)
-    assert isinstance(m_spans[0], TerminalPadding)
-    assert isinstance(m_spans[-1], TerminalPadding)
-    assert m_spans[2].lost and not isinstance(m_spans[1], TerminalPadding)
-    assert m_spans[4].lost and not isinstance(m_spans[2], TerminalPadding)
-
-    # use the method
-    m, _ = seq.parse_out_gaps()
-    m = IndelMap(
-        spans=tuple(m.spans), parent_length=m.parent_length
-    ).with_termini_unknown()
-    m_spans = tuple(m.spans)
-    assert isinstance(m_spans[0], TerminalPadding)
-    assert isinstance(m_spans[-1], TerminalPadding)
-    # middle gap is not terminal, so...
-    assert not isinstance(m_spans[2], TerminalPadding)
-
-    # no gaps, no effect
-    seq = DNA.make_seq("ACGTAA")
-    m, _ = seq.parse_out_gaps()
-    # use the constructor arg
-    m = IndelMap(
-        spans=tuple(m.spans), parent_length=m.parent_length, termini_unknown=True
-    )
-    m_spans = tuple(m.spans)
-    assert not isinstance(m_spans[0], TerminalPadding)
-    # use the method
-    m = IndelMap(
-        spans=tuple(m.spans), parent_length=m.parent_length
-    ).with_termini_unknown()
-    m_spans = tuple(m.spans)
-    assert not isinstance(m_spans[0], TerminalPadding)
-
-
-@pytest.mark.parametrize("cls", (FeatureMap, IndelMap))
-def test_map_inverse(cls):
-    m = cls(locations=[(0, 2), (4, 6)], parent_length=6)
-    assert len(m) == 4
-    mi = m.inverse()
-    assert len(mi) == 6
-    mi_spans = tuple(mi.spans)
-    assert mi_spans[1].lost and len(mi_spans[1]) == 2
-    # invert the inversion, should give us back the original
-    re_inv = mi.inverse()
-    expect = m.to_rich_dict()
-    got = re_inv.to_rich_dict()
-    assert got == expect
-
-
-@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, s = seq.parse_out_gaps()
-    got = m.offsets
-    assert got == [0, 1, 3, 6, 7, 8, 11]
-
-
-@pytest.mark.parametrize("cls", (FeatureMap, IndelMap))
-def test_map_indexed(cls):
-    m = cls(locations=[(0, 2), (4, 6)], parent_length=6).inverse()
-    indexed = m[2]
-    assert len(indexed) == 1
-
-
-def test_compare_map_indexed():
-    from cogent3.core.alignment import Aligned
-
-    seq = DNA.make_seq("--AC-GTAA--".replace("-", ""))
-    spans = [LostSpan(2), Span(0, 2), LostSpan(2), Span(2, 6), LostSpan(2)]
-    kwargs = dict(spans=spans, parent_length=len(seq))
-    mm = FeatureMap(**kwargs)
-    im = IndelMap(**kwargs)
-    ma = Aligned(mm, seq)
-    ia = Aligned(im, seq)
-    first = ia[0]
-    assert first == "-"
-    length = len(ma)
-    got = [str(ia[i]) for i in range(length)]
-    expect = [str(ma[i]) for i in range(length)]
-    assert got == expect
-
-
-@pytest.mark.parametrize("slice_it", (True, False))
-def test_indel_map_zeroed(slice_it):
-    spans = [LostSpan(2), Span(2, 4), LostSpan(2), Span(4, 8), LostSpan(2)]
-    kwargs = dict(spans=spans, parent_length=6)
-
-    mm = FeatureMap(**kwargs)
-    if slice_it:
-        mm = mm[:6]
-    mm_zeroed = mm.zeroed()
-
-    im = IndelMap(**kwargs)
-    if slice_it:
-        im = im[:6]
-
-    im_zeroed = im.zeroed()
-    assert im_zeroed.get_coordinates() == mm_zeroed.get_coordinates()
-    assert im_zeroed.parent_length == mm_zeroed.parent_length
-
-
-def test_indelmap_to_feature_map():
-    spans = [LostSpan(2), Span(2, 4), LostSpan(2), Span(4, 8), LostSpan(2)]
-    kwargs = dict(spans=spans, parent_length=6)
-    im = IndelMap(**kwargs)
-    mm = im.to_feature_map()
-    assert mm.get_coordinates() == im.get_coordinates()
-
-
-def test_indelmap_strict_nucleic_reversed():
-    spans = [LostSpan(2), Span(2, 4), LostSpan(2), Span(4, 8), LostSpan(2)]
-    kwargs = dict(spans=spans, parent_length=12)
-    orig = IndelMap(**kwargs)
-    rev = orig.strict_nucleic_reversed()
-    rev_spans = tuple(rev.spans)
-    assert rev_spans[1].reverse == rev_spans[3].reverse == False
-    old = orig.nucleic_reversed()
-    old_spans = tuple(old.spans)
-    assert old_spans[1].reverse == old_spans[3].reverse == True
-    assert rev.get_coordinates() == [
-        tuple(sorted(a)) for a in reversed(old.get_coordinates())
-    ]
-
-
-def test_indelmap_with_reverse_span():
-    spans = [
-        LostSpan(2),
-        Span(8, 4, reverse=True),
-        LostSpan(2),
-        Span(4, 2, reverse=True),
-        LostSpan(2),
-    ]
-    imap = IndelMap(spans=spans, parent_length=12)
-    assert (imap.start, imap.end) == (8, 2)
-
-
-def test_indelmap_no_gaps():
-    imap = IndelMap(locations=(), parent_length=6)
-    gaps = imap.gaps()
-    assert not gaps
diff --git a/tests/test_core/test_maps.py b/tests/test_core/test_maps.py
index 275876598..6ccb21b31 100644
--- a/tests/test_core/test_maps.py
+++ b/tests/test_core/test_maps.py
@@ -1,7 +1,7 @@
 import unittest
 
 from cogent3 import DNA, make_aligned_seqs
-from cogent3.core.location import FeatureMap, Span
+from cogent3.core.location import Map, Span
 
 
 class MapTest(unittest.TestCase):
@@ -9,7 +9,7 @@ class MapTest(unittest.TestCase):
 
     def test_spans(self):
         # a simple two part map of length 10
-        map = FeatureMap([(0, 5), (5, 10)], parent_length=10)
+        map = Map([(0, 5), (5, 10)], parent_length=10)
         # try different spans on the above map
         for (start, end), expected in [
             ((0, 4), "[0:4]"),