Repository: cogent3/cogent3
Pull request #2003 (2024-08-27 06:20:35)

ENH: support slicing of Alignment/Aligned
In this PR: 

- implementing changes to the design of aligned/alignment as discussed on 30/07
  - stop tracking slice history directly in `AlignedDataView`, now via a slice record object 
  - generally trying to mirror the `SeqsData` design more closely in `AlignedSeqsData` - especially with the assignment of `make_seq` and `make_aligned` methods
- Slicing an `Alignment` propagates to `Aligned` instances!


Patch:
diff --git a/src/cogent3/core/location.py b/src/cogent3/core/location.py
index 44bc5fae9..e6c148dce 100644
--- a/src/cogent3/core/location.py
+++ b/src/cogent3/core/location.py
@@ -1206,10 +1206,9 @@ class IndelMap(MapABC):
         adj_gaps = []
         lengths = self.get_gap_lengths()
 
-        # work out the first gap.
+        # determine the beginning of the slice
         if gap_starts[l] <= start < gap_ends[l]:
             # start is within a gap
-            # we adjust the gap length to account for the start position
             adj = start - gap_starts[l]
             # we want "ceiling division" to determine how many steps are
             # included which we can achieve with upside down "floor division"
@@ -1218,8 +1217,8 @@ class IndelMap(MapABC):
             adj_gaps.append([0, adj_gap_len])
         else:
             # start is within a ungapped segment
-            if stop - ((stop - start) % step) <= gap_starts[l]:
-                # the slice ends within the same ungapped segment
+            if stop <= gap_starts[l]:
+                # the stop is within the same ungapped segment
                 return no_gaps
 
             # determine the preceding seq length
@@ -1250,7 +1249,7 @@ class IndelMap(MapABC):
             # calculate the adjustment to the gap
             adj = _step_adjustment(gap_starts[j], start, step)
             if adj < lengths[j]:
-                # if overstep > length, then the gap is "stepped over" by the stride
+                # if adj > length, then the gap is "stepped over" by the stride
                 adj_gap_len = -((lengths[j] - adj) // -step)
 
                 if adj_gaps and adj_gaps[-1][0] == cum_seq_length:
@@ -1258,30 +1257,42 @@ class IndelMap(MapABC):
                     adj_gaps[-1][1] += adj_gap_len
                 else:
                     adj_gaps.append([cum_seq_length, adj_gap_len])
-        # now we determine the end of the slice
+
+        # determine the end of the slice
         if l == r:
             # the stop was inside the first gap
             adj = _step_adjustment(gap_starts[r], start, step)
-            adj_gaps[-1][1] = _step_adjusted_length(gap_starts[r], stop, adj, step)
+
+            if adj_gaps:
+                # start was in the gap
+                adj_gaps[-1][1] = _step_adjusted_length(gap_starts[r], stop, adj, step)
+            else:
+                # start was in the seq before the gap
+                adj_gaps.append(
+                    [
+                        cum_seq_length,
+                        _step_adjusted_length(gap_starts[r], stop, adj, step),
+                    ]
+                )
+
         elif stop >= gap_ends[-1] or stop < gap_starts[r]:
             # stop is within an ungapped segment
-            # either the last segment or the one before the last included gap
             adj = _step_adjustment(gap_ends[r - 1], start, step)
             cum_seq_length += _step_adjusted_length(gap_ends[r - 1], stop, adj, step)
         else:
             # stop is within a gap
-            # work out the previous ungapped segment
+            # determine length of previous ungapped segment
             adj = _step_adjustment(gap_ends[r - 1], start, step)
             cum_seq_length += _step_adjusted_length(
                 gap_ends[r - 1], gap_starts[r], adj, step
             )
 
-            # work out the length of the gap when considering the stop
+            # determine length of the gap when considering the stop
             adj = _step_adjustment(gap_starts[r], start, step)
             adj_gap_len = _step_adjusted_length(gap_starts[r], stop, adj, step)
 
+            # check that we do not step over the gap entirely
             if adj_gap_len > 0:
-                # check that we do not step over the gap entirely
                 if adj_gaps and adj_gaps[-1][0] == cum_seq_length:
                     # the previous gap is contiguous with this one
                     adj_gaps[-1][1] += adj_gap_len
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index 9a10cf074..cdffdd95d 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -22,12 +22,7 @@ from cogent3.core.annotation_db import (
 )
 from cogent3.core.info import Info as InfoClass
 from cogent3.core.location import IndelMap
-from cogent3.core.profile import (
-    PSSM,
-    MotifCountsArray,
-    MotifFreqsArray,
-    load_pssm,
-)
+from cogent3.core.profile import PSSM, MotifCountsArray, MotifFreqsArray, load_pssm
 from cogent3.format.alignment import save_to_filename
 from cogent3.format.fasta import seqs_to_fasta
 from cogent3.format.phylip import alignment_to_phylip
@@ -64,6 +59,14 @@ class MakeSeqCallable(typing.Protocol):
     ) -> new_sequence.Sequence: ...
 
 
+class MakeAlignedCallable(typing.Protocol):
+    def __call__(
+        self,
+        aligned_data_view: AlignedDataView,
+        moltype: new_moltype.MolType,
+    ) -> Aligned: ...
+
+
 def assign_sequential_names(num_seqs: int, base_name: str = "seq", start_at: int = 0):
     """Returns list of sequential, unique names, e.g., ['seq_0' ... 'seq_n'] for
      num_seqs = n+1
@@ -180,8 +183,6 @@ class SeqDataView(new_sequence.SeqViewABC, new_sequence.SliceRecordABC):
         return self.str_value
 
     def __array__(self, dtype=None, copy=None) -> numpy.ndarray:
-        if copy is False:
-            raise ValueError("`copy=False` isn't supported. A copy is always created.")
         arr = self.array_value
         if dtype:
             arr = arr.astype(dtype)
@@ -561,6 +562,8 @@ class SequenceCollection:
         self.source = source
         # todo: kath - move _repr_policy to method so it can be reimplemented in Aligment class
         self._repr_policy = dict(num_seqs=10, num_pos=60, ref_name="longest", wrap=60)
+        # refactor:
+        # think on the assignment of annotation dbs, could we change it to a method where there is an optional argument of the remapping of names?
         self._annotation_db = annotation_db or DEFAULT_ANNOTATION_DB()
 
     @property
@@ -596,9 +599,6 @@ class SequenceCollection:
 
         self._annotation_db = value
 
-        for seq in self.seqs:
-            seq.replace_annotation_db(value, check=False)
-
     @property
     @c3warn.deprecated_callable(
         version="2025.5", reason=".seqs can now be indexed by name", new=".seqs"
@@ -1137,9 +1137,6 @@ class SequenceCollection:
         -------
         Feature
         """
-        if not self.annotation_db:
-            self.annotation_db = DEFAULT_ANNOTATION_DB()
-
         if seqid and seqid not in self.names:
             raise ValueError(f"unknown {seqid=}")
 
@@ -1191,11 +1188,7 @@ class SequenceCollection:
         if not self.annotation_db:
             return None
 
-        # create a map between original seqid and current seqname (if renamed)
-        seqid_to_seqname = {seq.parent_coordinates()[0]: seq.name for seq in self.seqs}
-        if seqid and (
-            seqid not in seqid_to_seqname and seqid not in seqid_to_seqname.values()
-        ):
+        if seqid and (seqid not in self.names):
             raise ValueError(f"unknown {seqid=}")
 
         for feature in self.annotation_db.get_features_matching(
@@ -1208,7 +1201,7 @@ class SequenceCollection:
             allow_partial=allow_partial,
             **kwargs,
         ):
-            seqname = seqid_to_seqname[feature["seqid"]]
+            seqname = feature["seqid"]
             seq = self.seqs[seqname]
             if offset := seq.annotation_offset:
                 feature["spans"] = (numpy.array(feature["spans"]) - offset).tolist()
@@ -2403,8 +2396,7 @@ def seq_to_gap_coords(
     seq: StrORBytesORArray,
     *,
     alphabet: new_alphabet.AlphabetABC,
-    make_seq: MakeSeqCallable,
-) -> tuple[StrORBytesORArray, IndelMap]:
+) -> tuple[numpy.ndarray, numpy.ndarray]:
     """
     Takes a sequence with (or without) gaps and returns an ungapped sequence
     and a map of the position and length of gaps in the original parent sequence
@@ -2412,27 +2404,11 @@ def seq_to_gap_coords(
     raise NotImplementedError(f"seq_to_gap_coords not implemented for type {type(seq)}")
 
 
-@seq_to_gap_coords.register
-def _(
-    seq: str, *, alphabet: new_alphabet.AlphabetABC, make_seq: MakeSeqCallable
-) -> tuple[str, numpy.ndarray]:
-    seq = make_seq(seq=seq)
-    indel_map, ungapped_seq = seq.parse_out_gaps()
-
-    if indel_map.num_gaps == 0:
-        return str(ungapped_seq), numpy.array([], dtype=int)
-
-    return str(ungapped_seq), numpy.array(
-        [indel_map.gap_pos, indel_map.cum_gap_lengths]
-    ).T
-
-
 @seq_to_gap_coords.register
 def _(
     seq: numpy.ndarray,
     *,
     alphabet: new_alphabet.AlphabetABC,
-    make_seq: MakeSeqCallable,
 ) -> tuple[numpy.ndarray, numpy.ndarray]:
     gaps_bool = seq == alphabet.gap_index
     ungapped = seq[~gaps_bool]
@@ -2466,27 +2442,38 @@ def _(
     return ungapped, numpy.array([gap_pos, cum_lengths]).T
 
 
+@seq_to_gap_coords.register
+def _(seq: str, *, alphabet: new_alphabet.AlphabetABC) -> tuple[str, numpy.ndarray]:
+    if not alphabet.is_valid(seq):
+        raise ValueError(f"Sequence is invalid for alphabet {alphabet}")
+
+    return seq_to_gap_coords(alphabet.to_indices(seq), alphabet=alphabet)
+
+
 @seq_to_gap_coords.register
 def _(
-    seq: bytes, *, alphabet: new_alphabet.AlphabetABC, make_seq: MakeSeqCallable
-) -> tuple[bytes, numpy.ndarray]:
-    seq, map_array = seq_to_gap_coords(
-        seq.decode("utf-8"), alphabet=alphabet, make_seq=make_seq
-    )
-    return seq.encode("utf-8"), map_array
+    seq: bytes, *, alphabet: new_alphabet.AlphabetABC
+) -> tuple[numpy.ndarray, numpy.ndarray]:
+    return seq_to_gap_coords(seq.decode("utf-8"), alphabet=alphabet)
 
 
 class SliceRecord(new_sequence.SliceRecordABC):
     __slots__ = "_parent_len"
 
     def __init__(
-        self, start: OptInt, stop: OptInt, step: OptInt, offset: int, seq_len: int
+        self,
+        parent_len: int,
+        start: OptInt = None,
+        stop: OptInt = None,
+        step: OptInt = None,
+        offset: int = None,
     ):
-        self.start = start
-        self.stop = stop
-        self.step = step
-        self._offset = offset
-        self._seq_len = seq_len
+        parent_len = int(parent_len)
+        self._parent_len = parent_len
+        self.start = start if start is not None else 0
+        self.stop = stop if stop is not None else parent_len
+        self.step = step or 1
+        self._offset = offset or 0
 
     @property
     def parent_len(self) -> int:
@@ -2499,15 +2486,27 @@ class SliceRecord(new_sequence.SliceRecordABC):
         # todo: kath
         return self
 
+    def __repr__(self):
+        # refactor: design
+        # what should this look like?
+        return (
+            f"[{self.start}:{self.stop}:{self.step} of parent length {self.parent_len}]"
+        )
+
     @property
-    def _zero_slice(self): ...
+    def _zero_slice(self):
+        return self.__class__(start=0, stop=0, step=1, parent_len=self.parent_len)
 
 
 class Aligned:
     """A single sequence in an alignment."""
 
-    def __init__(self, data: AlignedDataView):
+    def __init__(self, data: AlignedDataView, moltype: new_moltype.MolType):
         self._data = data
+        self._moltype = moltype
+
+    def __len__(self) -> int:
+        return len(self.map)
 
     @property
     def data(self) -> AlignedDataView:
@@ -2520,21 +2519,27 @@ class Aligned:
     @property
     def seq(self) -> new_sequence.Sequence:
         """Returns Sequence object, excluding gaps."""
+        # todo: kath
+        # check for reverse and complement when we support negative steps
         return self.data.parent.make_seq(seq=self.data.str_value, name=self.data.seqid)
 
     @property
     def gapped_seq(self) -> new_sequence.Sequence:
         """Returns Sequence object, including gaps."""
+        # todo: kath
+        # check for reverse and complement when we support negative steps
         return self.data.parent.make_seq(
             seq=self.data.gapped_str_value, name=self.data.seqid
         )
 
+    @property
+    def moltype(self) -> new_moltype.MolType:
+        return self._moltype
+
     def __str__(self) -> str:
         return str(self.gapped_seq)
 
     def __array__(self, dtype=None, copy=None) -> numpy.ndarray:
-        if copy is False:
-            raise ValueError("`copy=False` isn't supported. A copy is always created.")
         return numpy.array(self.gapped_seq, dtype=dtype)
 
     def __bytes__(self) -> bytes:
@@ -2542,7 +2547,7 @@ class Aligned:
 
     def __iter__(self):
         """Iterates over sequence one motif (e.g. char) at a time, incl. gaps"""
-        yield self.gapped_seq
+        yield from self.gapped_seq
 
     @singledispatchmethod
     def __getitem__(self, span: Union[int, slice]):
@@ -2554,15 +2559,13 @@ class Aligned:
 
     @__getitem__.register
     def _(self, span: slice):
-        # todo: kath
-        # implement for reverse complement
-        if span.step and span.step < 0:
-            raise NotImplementedError("Cannot reverse an Aligned")
-
-        return self.__class__(data=self.data[span.start : span.stop])
+        self.data.slice_record = self.data.slice_record[span]
+        return self.__class__(data=self.data, moltype=self.moltype)
 
 
 class AlignedSeqsDataABC(SeqsDataABC):
+    __slots__ = ()
+
     @classmethod
     @abstractmethod
     def from_aligned_seqs(
@@ -2570,7 +2573,7 @@ class AlignedSeqsDataABC(SeqsDataABC):
         *,
         data: dict[str, StrORArray],
         alphabet: new_alphabet.AlphabetABC,
-        make_seq: Optional[MakeSeqCallable] = None,
+        make_seq: Optional[MakeSeqCallable],
     ): ...
 
     @property
@@ -2584,6 +2587,7 @@ class AlignedSeqsDataABC(SeqsDataABC):
         seqid: str,
         start: OptInt = None,
         stop: OptInt = None,
+        step: OptInt = None,
     ) -> numpy.ndarray: ...
 
     @abstractmethod
@@ -2593,6 +2597,7 @@ class AlignedSeqsDataABC(SeqsDataABC):
         seqid: str,
         start: OptInt = None,
         stop: OptInt = None,
+        step: OptInt = None,
     ) -> str: ...
 
     @abstractmethod
@@ -2602,11 +2607,31 @@ class AlignedSeqsDataABC(SeqsDataABC):
         seqid: str,
         start: OptInt = None,
         stop: OptInt = None,
+        step: OptInt = None,
     ) -> bytes: ...
 
 
+def _gapped_seq_len(seq: numpy.ndarray, gap_map: numpy.ndarray) -> int:
+    """calculate the gapped sequence length from a ungapped sequence and gap map
+
+    Parameters
+    ----------
+    seq
+        numpy array of sequence indices
+    gap_map
+        numpy array of [gap index, cumulative gap length] pairs
+    """
+    try:
+        gap_len = gap_map[-1][1]
+    except IndexError:  # no gaps
+        return len(seq)
+
+    return len(seq) + gap_len
+
+
 class AlignedSeqsData(AlignedSeqsDataABC):
     # refactor: docstring
+    # needs to record parentid, strand, offset
 
     __slots__ = (
         "_seqs",
@@ -2614,6 +2639,7 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         "_alphabet",
         "_make_seq",
         "_align_len",
+        "_make_aligned",
     )
 
     def __init__(
@@ -2622,8 +2648,8 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         seqs: Optional[dict[str, StrORBytesORArray]],
         gaps: Optional[dict[str, numpy.ndarray]],
         alphabet: new_alphabet.AlphabetABC,
+        make_seq: Optional[MakeSeqCallable],
         align_len: OptInt = None,
-        make_seq: Optional[MakeSeqCallable] = None,
         check: bool = True,
     ):
         self._alphabet = alphabet
@@ -2633,12 +2659,6 @@ class AlignedSeqsData(AlignedSeqsDataABC):
                 raise ValueError("Both seqs and gaps must be provided.")
             if set(seqs.keys()) != set(gaps.keys()):
                 raise ValueError("Keys in seqs and gaps must be identical.")
-            seq_lengths = {
-                len(v) + l[-1][1] for v, l in zip(seqs.values(), gaps.values())
-            }
-            if len(seq_lengths) != 1:
-                raise ValueError("All sequence lengths must be the same.")
-            align_len = seq_lengths.pop()
         self._seqs = {}
         for k, v in seqs.items():
             seq = self._alphabet.to_indices(v)
@@ -2648,7 +2668,12 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         for k, v in gaps.items():
             self._gaps[k] = v
             v.flags.writeable = False
-        self._align_len = align_len or len(next(iter(self._seqs.values())))
+        gapped_seq_lengths = {
+            _gapped_seq_len(v, l) for v, l in zip(seqs.values(), gaps.values())
+        }
+        if len(gapped_seq_lengths) != 1:
+            raise ValueError("All sequence lengths must be the same.")
+        self._align_len = align_len or gapped_seq_lengths.pop()
 
     @classmethod
     def from_aligned_seqs(
@@ -2677,7 +2702,7 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         seqs = {}
         gaps = {}
         for name, seq in data.items():
-            seq, gap_map = seq_to_gap_coords(seq, alphabet=alphabet, make_seq=make_seq)
+            seq, gap_map = seq_to_gap_coords(seq, alphabet=alphabet)
             seq = alphabet.to_indices(seq)
             seq.flags.writeable = False
             gap_map.flags.writeable = False
@@ -2692,22 +2717,6 @@ class AlignedSeqsData(AlignedSeqsDataABC):
             check=False,
         )
 
-    def __len__(self):
-        return self.align_len
-
-    @singledispatchmethod
-    def __getitem__(self, index: Union[str, int]) -> Aligned:
-        raise NotImplementedError(f"__getitem__ not implemented for {type(index)}")
-
-    @__getitem__.register
-    def _(self, index: str) -> Aligned:
-        adv = self.get_view(seqid=index)
-        return Aligned(data=adv)
-
-    @__getitem__.register
-    def _(self, index: int) -> Aligned:
-        return self[self.names[index]]
-
     @property
     def names(self) -> tuple[str]:
         return list(self._seqs.keys())
@@ -2732,49 +2741,128 @@ class AlignedSeqsData(AlignedSeqsDataABC):
     def make_seq(self, make_seq: MakeSeqCallable) -> None:
         self._make_seq = make_seq
 
+    @property
+    def make_aligned(self) -> MakeAlignedCallable:
+        return self._make_aligned
+
+    @make_aligned.setter
+    def make_aligned(self, make_aligned: MakeAlignedCallable) -> None:
+        self._make_aligned = make_aligned
+
     @property
     def align_len(self) -> int:
         return self._align_len
 
+    def __len__(self):
+        return self.align_len
+
+    @singledispatchmethod
+    def __getitem__(self, index: Union[str, int]) -> Aligned:
+        raise NotImplementedError(f"__getitem__ not implemented for {type(index)}")
+
+    @__getitem__.register
+    def _(self, index: str) -> Aligned:
+        adv = self.get_view(seqid=index)
+        return self.make_aligned(data=adv)
+
+    @__getitem__.register
+    def _(self, index: int) -> Aligned:
+        return self[self.names[index]]
+
     def seq_lengths(self) -> dict[str, int]:
         """Returns lengths of ungapped sequences as dict of {name: length, ... }."""
         return {name: len(seq) for name, seq in self._seqs.items()}
 
     def get_view(self, seqid: str) -> AlignedDataView:
-        return AlignedDataView(parent=self, seqid=seqid, parent_len=self.align_len)
+        return AlignedDataView(parent=self, seqid=seqid)
 
     def get_gaps(self, seqid: str) -> numpy.ndarray:
         return self._gaps[seqid]
 
-    def get_seq_array(
+    def _get_seq_array(
         self,
         *,
         seqid: str,
+        gapped: bool,
         start: OptInt = None,
         stop: OptInt = None,
+        step: OptInt = None,
     ) -> numpy.ndarray:
-        """Return ungapped sequence corresponding to seqid as an array of indices.
-        start/stop are in alignment coordinates and the alignment coordinates
-        are converted to sequence coordinates. Excludes gaps.
+        """helper function to return sequence data corresponding to seqid as an
+        array of indices. start/stop are in alignment coordinates.
+
+        Interweaves gaps into the sequence if gapped is True. Returns the ungapped
+        sequence if gapped is False.
         """
         seq = self._seqs[seqid]
+        start = start if start is not None else 0
+        stop = stop if stop is not None else self.align_len
+        step = step or 1
+
         # if there's no gaps, just slice the sequence
         if len(seq) == self.align_len:
-            return seq[start:stop]
+            return seq[start:stop:step]
 
         gaps = self._gaps[seqid]
         indel_map = IndelMap(
             gap_pos=gaps[:, 0], cum_gap_lengths=gaps[:, 1], parent_length=self.align_len
         )
 
-        new_map = (
-            indel_map[start:stop]
-            if start is not None or stop is not None
-            else indel_map
+        unknown = self.alphabet.gap_index
+
+        # there is only gaps
+        if len(seq) == 0:
+            if gapped:
+                return numpy.full(
+                    int(-((stop - start) // -step)), unknown, dtype=numpy.uint8
+                )
+            else:
+                return numpy.array([], dtype=numpy.uint8)
+
+        # convert from alignment coordinates to sequences coordinates
+        # selecting only the indices that are in the frame of the stride
+        indices = []
+        for span in indel_map.nongap():
+            indices += [
+                indel_map.get_seq_index(x)
+                for x in range(span.start, span.end)
+                if start <= x < stop and (x - start) % step == 0
+            ]
+
+        seq_sliced = seq[indices]
+        map_sliced = indel_map[start:stop:step]
+        concat = numpy.array([], dtype=numpy.uint8)
+
+        # iterate through spans
+        for span in map_sliced.spans:
+            if span.lost:
+                if gapped:
+                    seg = numpy.full(span.length, unknown, dtype=numpy.uint8)
+                else:
+                    # if not gapped, we skip over this segment
+                    continue
+            else:
+                seg = seq_sliced[span.start : span.end]
+
+            concat = numpy.concatenate((concat, seg))
+
+        return concat
+
+    def get_seq_array(
+        self,
+        *,
+        seqid: str,
+        start: OptInt = None,
+        stop: OptInt = None,
+        step: OptInt = None,
+    ) -> numpy.ndarray:
+        """Return ungapped sequence corresponding to seqid as an array of indices.
+        start/stop are in alignment coordinates and the alignment coordinates
+        are converted to sequence coordinates. Excludes gaps.
+        """
+        return self._get_seq_array(
+            seqid=seqid, gapped=False, start=start, stop=stop, step=step
         )
-        seq_start = indel_map.get_seq_index(start or 0)
-        seq_end = indel_map.get_seq_index(stop or self.align_len)
-        return seq[seq_start:seq_end] if new_map.useful else seq[:0]
 
     def get_gapped_seq_array(
         self,
@@ -2782,48 +2870,29 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         seqid: str,
         start: OptInt = None,
         stop: OptInt = None,
+        step: OptInt = None,
     ) -> numpy.ndarray:
         """Return sequence data corresponding to seqid as an array of indices.
         start/stop are in alignment coordinates. Includes gaps.
         """
-        seq = self._seqs[seqid]
-
-        # if there's no gaps, just slice the sequence
-        if len(seq) == self.align_len:
-            return seq[start:stop]
-
-        # refactor: design
-        # direct numpy array approach would be faster
-        gaps = self._gaps[seqid]
-        indel_map = IndelMap(
-            gap_pos=gaps[:, 0], cum_gap_lengths=gaps[:, 1], parent_length=self.align_len
+        return self._get_seq_array(
+            seqid=seqid, gapped=True, start=start, stop=stop, step=step
         )
 
-        gapped = numpy.array([], dtype=numpy.uint8)
-        for span in indel_map.spans:
-            if span.lost:
-                unknown = self.alphabet.gap_index
-                seg = numpy.full(span.length, unknown, dtype=numpy.uint8)
-            else:
-                seg = seq[span.start : span.end]
-
-            gapped = numpy.concatenate((gapped, seg))
-
-        return gapped[start:stop]
-
     def get_seq_str(
         self,
         *,
         seqid: str,
         start: OptInt = None,
         stop: OptInt = None,
+        step: OptInt = None,
     ) -> str:
         """Return ungapped sequence corresponding to seqid as a string.
         start/stop are in alignment coordinates and the alignment coordinates
         are converted to sequence coordinates. Excludes gaps."""
 
         return self.alphabet.from_indices(
-            self.get_seq_array(seqid=seqid, start=start, stop=stop)
+            self.get_seq_array(seqid=seqid, start=start, stop=stop, step=step)
         )
 
     def get_gapped_seq_str(
@@ -2832,12 +2901,13 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         seqid: str,
         start: OptInt = None,
         stop: OptInt = None,
+        step: OptInt = None,
     ) -> str:
         """Return sequence corresponding to seqid as a string.
         start/stop are in alignment coordinates. Includes gaps."""
 
         return self.alphabet.from_indices(
-            self.get_gapped_seq_array(seqid=seqid, start=start, stop=stop)
+            self.get_gapped_seq_array(seqid=seqid, start=start, stop=stop, step=step)
         )
 
     def get_seq_bytes(
@@ -2846,11 +2916,14 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         seqid: str,
         start: OptInt = None,
         stop: OptInt = None,
+        step: OptInt = None,
     ) -> bytes:
         """Return ungapped sequence corresponding to seqid as a bytes string.
         start/stop are in alignment coordinates and the alignment coordinates
         are converted to sequence coordinates. Excludes gaps."""
-        return self.get_seq_str(seqid=seqid, start=start, stop=stop).encode("utf8")
+        return self.get_seq_str(seqid=seqid, start=start, stop=stop, step=step).encode(
+            "utf8"
+        )
 
     def get_gapped_seq_bytes(
         self,
@@ -2858,12 +2931,13 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         seqid: str,
         start: OptInt = None,
         stop: OptInt = None,
+        step: OptInt = None,
     ) -> bytes:
         """Return sequence corresponding to seqid as a bytes string.
         start/stop are in alignment coordinates. Includes gaps."""
-        return self.get_gapped_seq_str(seqid=seqid, start=start, stop=stop).encode(
-            "utf8"
-        )
+        return self.get_gapped_seq_str(
+            seqid=seqid, start=start, stop=stop, step=step
+        ).encode("utf8")
 
     def add_seqs(
         self, seqs: dict[str, StrORArray], force_unique_keys=True
@@ -2905,35 +2979,31 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         ...
 
 
-class AlignedDataView(new_sequence.SeqViewABC, new_sequence.SliceRecordABC):
+class AlignedDataView(new_sequence.SeqViewABC):
     # refactor: docstring
+
+    __slots__ = ("parent", "_seqid", "_offset", "_parent_len", "_slice_record")
+
     def __init__(
         self,
         *,
         parent: AlignedSeqsDataABC,
         seqid: str,
-        parent_len: int,
-        start: OptInt = None,
-        stop: OptInt = None,
-        step: OptInt = None,
         offset: int = 0,
     ):
-        if step and step < 1:
-            raise ValueError(f"step cannot be {step}")
-        step = 1 if step is None else step
         self.parent = parent
-        self._parent_len = self._checked_seq_len(parent_len)
-        func = (
-            new_sequence._input_vals_pos_step
-            if step > 0
-            else new_sequence._input_vals_neg_step
-        )
-        start, stop, step = func(self._parent_len, start, stop, step)
-        self.start = start
-        self.stop = stop
-        self.step = step
-        self._offset = offset
         self._seqid = seqid
+        self._offset = offset
+        self._parent_len = parent.align_len
+        self._slice_record = SliceRecord(parent_len=self.parent_len)
+
+    @property
+    def slice_record(self):
+        return self._slice_record
+
+    @slice_record.setter
+    def slice_record(self, value: new_sequence.SliceRecordABC):
+        self._slice_record = value
 
     @property
     def seqid(self) -> str:
@@ -2945,99 +3015,122 @@ class AlignedDataView(new_sequence.SeqViewABC, new_sequence.SliceRecordABC):
 
     @property
     def map(self) -> IndelMap:
-        gap_pos_gap_length = self.parent.get_gaps(self.seqid)[self.start : self.stop]
-        return IndelMap(
-            gap_pos=gap_pos_gap_length[:, 0],
-            cum_gap_lengths=gap_pos_gap_length[:, 1],
-            parent_length=self.parent_len,
+        gap_pos_gap_length = self.parent.get_gaps(self.seqid)
+        if gap_pos_gap_length.size > 0:
+            gap_pos = (gap_pos_gap_length[:, 0],)
+            cum_gap_lengths = (gap_pos_gap_length[:, 1],)
+        else:
+            gap_pos, cum_gap_lengths = (
+                numpy.array([], dtype=int),
+                numpy.array([], dtype=int),
+            )
+
+        im = IndelMap(
+            gap_pos=gap_pos,
+            cum_gap_lengths=cum_gap_lengths,
+            parent_length=len(self.parent),
         )
+        return im[
+            self.slice_record.start : self.slice_record.stop : self.slice_record.step
+        ]
 
     @property
     def str_value(self) -> str:
         return self.parent.get_seq_str(
-            seqid=self.seqid, start=self.start, stop=self.stop
+            seqid=self.seqid,
+            start=self.slice_record.start,
+            stop=self.slice_record.stop,
+            step=self.slice_record.step,
         )
 
     @property
     def gapped_str_value(self) -> str:
         return self.parent.get_gapped_seq_str(
-            seqid=self.seqid, start=self.start, stop=self.stop
+            seqid=self.seqid,
+            start=self.slice_record.start,
+            stop=self.slice_record.stop,
+            step=self.slice_record.step,
         )
 
     @property
     def array_value(self) -> numpy.ndarray:
         return self.parent.get_seq_array(
-            seqid=self.seqid, start=self.start, stop=self.stop
+            seqid=self.seqid,
+            start=self.slice_record.start,
+            stop=self.slice_record.stop,
+            step=self.slice_record.step,
         )
 
     @property
     def gapped_array_value(self) -> numpy.ndarray:
         return self.parent.get_gapped_seq_array(
-            seqid=self.seqid, start=self.start, stop=self.stop
+            seqid=self.seqid,
+            start=self.slice_record.start,
+            stop=self.slice_record.stop,
+            step=self.slice_record.step,
         )
 
     @property
     def bytes_value(self) -> bytes:
         return self.parent.get_seq_bytes(
-            seqid=self.seqid, start=self.start, stop=self.stop
+            seqid=self.seqid,
+            start=self.slice_record.start,
+            stop=self.slice_record.stop,
+            step=self.slice_record.step,
         )
 
     @property
     def gapped_bytes_value(self) -> bytes:
         return self.parent.get_gapped_seq_bytes(
-            seqid=self.seqid, start=self.start, stop=self.stop
+            seqid=self.seqid,
+            start=self.slice_record.start,
+            stop=self.slice_record.stop,
+            step=self.slice_record.step,
         )
 
     def __str__(self) -> str:
         # refactor: design
         # should this return gapped or ungapped?
-        return self.str_value
+        return self.gapped_str_value
 
     def __array__(self, dtype=None, copy=None) -> numpy.ndarray:
-        if copy is False:
-            raise ValueError("`copy=False` isn't supported. A copy is always created.")
-        arr = self.array_value
+        arr = self.gapped_array_value
         if dtype:
             arr = arr.astype(dtype)
         return arr
 
     def __bytes__(self) -> bytes:
-        return self.bytes_value
+        return self.gapped_bytes_value
 
     def copy(self, sliced: bool = False):
         return self
 
-    def to_rich_dict(self) -> dict:
-        # todo: kath
-        ...
-
-    def _get_init_kwargs(self) -> dict:
-        return {"parent": self.parent, "seqid": self.seqid}
-
-    def _checked_seq_len(self, seq_len: int) -> int:
-        assert seq_len == self.parent.align_len
-        return seq_len
-
-    @property
-    def _zero_slice(self):
-        return self.__class__(
-            parent=self.parent,
-            seqid=self.seqid,
-            parent_len=self._parent_len,
-            start=0,
-            stop=0,
-        )
+    def to_rich_dict(self) -> dict: ...
 
 
 class Alignment(SequenceCollection):
     def __init__(
         self,
+        slice_record: new_sequence.SliceRecordABC = None,
         **kwargs,
     ):
         super().__init__(**kwargs)
+        self._seqs_data.make_aligned = self._make_aligned
+        self._seqs_data.make_seq = self.moltype.make_seq
+        self._slice_record = (
+            slice_record
+            if slice_record is not None
+            else SliceRecord(parent_len=self._seqs_data.align_len)
+        )
+
+    def _make_aligned(self, data: AlignedDataView) -> Aligned:
+        data.slice_record = self._slice_record
+        return Aligned(data=data, moltype=self.moltype)
 
     @property
     def seqs(self) -> AlignedSeqsDataABC:
+        # want to construct the ASD here (i think)
+        # gets passed the slice record
         return self._seqs_data
 
     def get_seq(
@@ -3083,6 +3176,281 @@ class Alignment(SequenceCollection):
         for pos in pos_order:
             yield [str(get(seq)[pos]) for seq in seq_order]
 
+    def __len__(self):
+        return len(self.seqs)
+
+    def __getitem__(self, index):
+        new_slice = self._slice_record[index]
+        result = self.__class__(
+            seqs_data=self.seqs,
+            slice_record=new_slice,
+            moltype=self.moltype,
+            info=self.info,
+        )
+        # we cannot support connection to the original annotation db if the slice is not 1 or -1
+        if abs(new_slice.step) > 1:
+            return result
+        result.annotation_db = self.annotation_db
+        return result
+
+    def reverse_complement(self):
+        # needs to be tracked in the ASD mirroring SD
+        ...
+
+    def iupac_consensus(self, allow_gap: bool = True):
+        """Returns string containing IUPAC consensus sequence of the alignment."""
+        exclude = set() if allow_gap else set(self.moltype.gaps)
+        consensus = []
+        degen = self.moltype.degenerate_from_seq
+        for col in self.iter_positions():
+            col = set(col) - exclude
+            consensus.append(degen("".join(col)))
+        return "".join(consensus)
+
+    def _get_raw_pretty(self, name_order):
+        """returns dict {name: seq, ...} for pretty print"""
+        if name_order is not None:
+            assert set(name_order) <= set(self.names), "names don't match"
+
+        output = defaultdict(list)
+        names = name_order or self.names
+        num_seqs = len(names)
+
+        seqs = [str(self.seqs[name]) for name in names]
+        positions = list(zip(*seqs))
+
+        for position in positions:
+            ref = position[0]
+            output[names[0]].append(ref)
+            for seq_num in range(1, num_seqs):
+                val = "." if position[seq_num] == ref else position[seq_num]
+                output[names[seq_num]].append(val)
+
+        return names, output
+
+    def _repr_html_(self) -> str:
+        settings = self._repr_policy.copy()
+        env_vals = get_setting_from_environ(
+            "COGENT3_ALIGNMENT_REPR_POLICY",
+            dict(num_seqs=int, num_pos=int, wrap=int, ref_name=str),
+        )
+        settings.update(env_vals)
+        return self.to_html(
+            name_order=self.names[: settings["num_seqs"]],
+            ref_name=settings["ref_name"],
+            limit=settings["num_pos"],
+            wrap=settings["wrap"],
+        )
+
+    def to_html(
+        self,
+        name_order: Optional[typing.Sequence[str]] = None,
+        wrap: int = 60,
+        limit: Optional[int] = None,
+        ref_name: str = "longest",
+        colors: Optional[Mapping[str, str]] = None,
+        font_size: int = 12,
+        font_family: str = "Lucida Console",
+    ) -> str:
+        """returns html with embedded styles for sequence colouring
+
+        Parameters
+        ----------
+        name_order
+            order of names for display.
+        wrap
+            number of alignment columns per row
+        limit
+            truncate alignment to this length
+        ref_name
+            Name of an existing sequence or 'longest'. If the latter, the
+            longest sequence (excluding gaps and ambiguities) is selected as the
+            reference.
+        colors
+            {character
+            moltype.
+        font_size
+            in points. Affects labels and sequence and line spacing
+            (proportional to value)
+        font_family
+            string denoting font family
+
+        Examples
+        ---------
+
+        In a jupyter notebook, this code is used to provide the representation.
+
+        .. code-block:: python
+
+            aln  # is rendered by jupyter
+
+        You can directly use the result for display in a notebook as
+
+        .. code-block:: python
+
+            from IPython.core.display import HTML
+
+            HTML(aln.to_html())
+        """
+        css, styles = self.moltype.get_css_style(
+            colors=colors, font_size=font_size, font_family=font_family
+        )
+        if name_order:
+            selected = self.take_seqs(name_order)
+            name_order = list(name_order)
+        else:
+            name_order = list(self.names)
+            ref_name = ref_name or "longest"
+            selected = self
+
+        if ref_name == "longest":
+            lengths = selected.get_lengths(include_ambiguity=False, allow_gap=False)
+
+            length_names = defaultdict(list)
+            for n, l in lengths.items():
+                length_names[l].append(n)
+
+            longest = max(length_names)
+            ref = sorted(length_names[longest])[0]
+
+        elif ref_name:
+            if ref_name not in selected.names:
+                raise ValueError(f"Unknown sequence name {ref_name}")
+            ref = ref_name
+
+        name_order.remove(ref)
+        name_order.insert(0, ref)
+
+        if limit is None:
+            names, output = selected._get_raw_pretty(name_order)
+        else:
+            names, output = selected[:limit]._get_raw_pretty(name_order)
+
+        gaps = "".join(selected.moltype.gaps)
+        refname = names[0]
+        refseq = output[refname]
+        seqlen = len(refseq)
+        start_gap = re.search(f"^[{gaps}]+", "".join(refseq))
+        end_gap = re.search(f"[{gaps}]+$", "".join(refseq))
+        start = 0 if start_gap is None else start_gap.end()
+        end = len(refseq) if end_gap is None else end_gap.start()
+        seq_style = []
+        template = '<span class="%s">%%s</span>'
+        styled_seqs = defaultdict(list)
+        for i in range(seqlen):
+            char = refseq[i]
+            if i < start or i >= end:
+                style = f"terminal_ambig_{selected.moltype.label}"
+            else:
+                style = styles[char]
+
+            seq_style.append(template % style)
+            styled_seqs[refname].append(seq_style[-1] % char)
+
+        for name in names:
+            if name == refname:
+                continue
+
+            seq = []
+            for i, c in enumerate(output[name]):
+                if c == ".":
+                    s = seq_style[i] % c
+                else:
+                    s = template % (styles[c])
+                    s = s % c
+                seq.append(s)
+
+            styled_seqs[name] = seq
+
+        # make a html table
+        seqs = numpy.array([styled_seqs[n] for n in names], dtype="O")
+        table = ["<table>"]
+        seq_ = "<td>%s</td>"
+        label_ = '<td class="label">%s</td>'
+        num_row_ = '<tr class="num_row"><td></td><td><b>{:,d}</b></td></tr>'
+        for i in range(0, seqlen, wrap):
+            table.append(num_row_.format(i))
+            seqblock = seqs[:, i : i + wrap].tolist()
+            for n, s in zip(names, seqblock):
+                s = "".join(s)
+                row = "".join([label_ % n, seq_ % s])
+                table.append(f"<tr>{row}</tr>")
+        table.append("</table>")
+        if (
+            limit
+            and limit < len(selected)
+            or name_order
+            and len(name_order) < len(selected.names)
+        ):
+            summary = (
+                f"{self.num_seqs} x {len(self)} (truncated to "
+                f"{len(name_order) if name_order else len(selected.names)} x "
+                f"{limit or len(selected)}) {selected.moltype.label} alignment"
+            )
+        else:
+            summary = (
+                f"{self.num_seqs} x {len(self)} {selected.moltype.label} alignment"
+            )
+
+        text = [
+            "<style>",
+            ".c3align table {margin: 10px 0;}",
+            ".c3align td { border: none !important; text-align: left !important; }",
+            ".c3align tr:not(.num_row) td span {margin: 0 2px;}",
+            ".c3align tr:nth-child(even) {background: #f7f7f7;}",
+            ".c3align .num_row {background-color:rgba(161, 195, 209, 0.5) !important; border-top: solid 1px black; }",
+            ".c3align .label { font-size: %dpt ; text-align: right !important; "
+            "color: black !important; padding: 0 4px; display: table-cell !important; "
+            "font-weight: normal !important; }" % font_size,
+            "\n".join([f".c3align {style}" for style in css]),
+            "</style>",
+            '<div class="c3align">',
+            "\n".join(table),
+            f"<p><i>{summary}</i></p>",
+            "</div>",
+        ]
+        return "\n".join(text)
+
+    def to_pretty(self, name_order=None, wrap=None):
+        """returns a string representation of the alignment in pretty print format
+
+        Parameters
+        ----------
+        name_order
+            order of names for display.
+        wrap
+            maximum number of printed bases
+        """
+        names, output = self._get_raw_pretty(name_order=name_order)
+        label_width = max(list(map(len, names)))
+        name_template = "{:>%d}" % label_width
+        display_names = dict([(n, name_template.format(n)) for n in names])
+
+        def make_line(label, seq):
+            return f"{label}    {seq}"
+
+        if wrap is None:
+            result = [make_line(display_names[n], "".join(output[n])) for n in names]
+            return "\n".join(result)
+
+        align_length = len(self)
+        result = []
+        for start in range(0, align_length, wrap):
+            for n in names:
+                result.append(
+                    make_line(
+                        display_names[n],
+                        "".join(output[n][start : start + wrap]),
+                    )
+                )
+
+            result.append("")
+
+        if not result[-1]:
+            del result[-1]
+
+        return "\n".join(result)
+
 
 @singledispatch
 def make_aligned_seqs(
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index e9fde527b..24fabdaf2 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -2007,6 +2007,9 @@ class SliceRecordABC(ABC):
         length of the underlying data (not including offset)
     """
 
+    # todo: kath
+    # slice record should throw an error if the step is 0
+
     __slots__ = ("start", "stop", "step", "_offset")
 
     @property


Test patch:
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index 5eb248697..38452ae60 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -1319,11 +1319,13 @@ def test_sequence_collection_get_similar(transform):
     assert got.names == ["d"]
 
 
-def test_sequence_collection_init_annotated_seqs():
+# todo: kath: add test for Alignment when it can be constructed with Sequence objects
+@pytest.mark.parametrize("coll_maker", [new_alignment.make_unaligned_seqs])
+def test_sequence_collection_init_annotated_seqs(coll_maker):
     """correctly construct from list with annotated seq"""
     seq = new_moltype.DNA.make_seq(seq="GCCAGGGGGGAAAG-GGAGAA", name="seq1")
     seq.add_feature(biotype="exon", name="name", spans=[(4, 10)])
-    coll = new_alignment.make_unaligned_seqs([seq], moltype="dna")
+    coll = coll_maker({"seq1": seq}, moltype="dna")
     features = list(coll.get_features(biotype="exon"))
     assert len(features) == 1
 
@@ -1470,10 +1472,11 @@ def test_sequence_collection_annotation_db_assign_same(gff_db):
     assert seq_coll.annotation_db is gff_db
 
 
-def test_sequence_collection_get_annotations_from_any_seq():
+@pytest.mark.parametrize("coll_maker", [new_alignment.make_unaligned_seqs])
+def test_sequence_collection_get_annotations_from_any_seq(coll_maker):
     """get_annotations_from_any_seq returns correct annotations"""
     data = {"seq1": "ACGTACGTA", "seq2": "ACCGAA---", "seq3": "ACGTACGTT"}
-    seqs = new_alignment.make_unaligned_seqs(data, moltype="dna")
+    seqs = coll_maker(data, moltype="dna")
     db = GffAnnotationDb()
     db.add_feature(seqid="seq1", biotype="exon", name="annotation1", spans=[(3, 8)])
     db.add_feature(seqid="seq2", biotype="exon", name="annotation2", spans=[(1, 2)])
@@ -2216,6 +2219,19 @@ def aligned_dict():
     return dict(seq1="ACG--T", seq2="-CGAAT", seq3="------", seq4="--GA--")
 
 
+@pytest.fixture
+def gapped_seqs_dict():
+    return dict(
+        seq1="AA--CC",
+        seq2="--AA--",
+        seq3="-A-A-A",
+        seq4="AAA---",
+        seq5="---AAA",
+        seq6="------",
+        seq7="AAAAAA",
+    )
+
+
 @pytest.fixture
 def aligned_array_dict():
     return dict(
@@ -2242,25 +2258,92 @@ def gap_seqs():
     ]
 
 
-@pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
-def test_aligned_initialisation(aligned_dict, seqid, dna_alphabet, dna_make_seq):
-    """indexing a AlignedSeqsData object should return an Aligned object"""
-    ad = new_alignment.AlignedSeqsData.from_aligned_seqs(
-        data=aligned_dict, alphabet=dna_alphabet, make_seq=dna_make_seq
+@pytest.mark.parametrize("i", range(3))
+def test_seq_to_gap_coords_str(gap_seqs, i, dna_alphabet):
+    seq, gap_coords = gap_seqs[i]
+    got_ungapped, got_map = new_alignment.seq_to_gap_coords(seq, alphabet=dna_alphabet)
+    expect = dna_alphabet.to_indices(seq.replace("-", ""))
+    assert numpy.array_equal(got_ungapped, expect)
+    assert numpy.array_equal(got_map, gap_coords)
+
+
+def test_seq_to_gap_coords_str_all_gaps(dna_alphabet):
+    parent_seq = "-----"
+    expect_gaplen = numpy.array([len(parent_seq)])
+    got_ungap, got_map = new_alignment.seq_to_gap_coords(
+        parent_seq, alphabet=dna_alphabet
+    )
+    expect = numpy.array([])
+    assert numpy.array_equal(got_ungap, expect)
+    assert got_map[:, 1] == expect_gaplen
+
+
+def test_seq_to_gap_coords_str_no_gaps(dna_alphabet):
+    parent_seq = "ACTGC"
+    got_ungap, got_map = new_alignment.seq_to_gap_coords(
+        parent_seq, alphabet=dna_alphabet
+    )
+    expect = dna_alphabet.to_indices(parent_seq)
+    assert numpy.array_equal(got_ungap, expect)
+    assert got_map.size == 0
+
+
+def test_seq_to_gap_coords_arr_all_gaps(dna_alphabet):
+    parent_seq = dna_alphabet.to_indices("-----")
+    got_ungap, got_map = new_alignment.seq_to_gap_coords(
+        parent_seq, alphabet=dna_alphabet
+    )
+    assert got_ungap.size == 0
+    assert numpy.array_equal(got_map, numpy.array([[0, 5]]))
+
+
+def test_seq_to_gap_coords_arr_no_gaps(dna_alphabet):
+    parent_seq = dna_alphabet.to_indices("ACTGC")
+    got_ungap, got_empty_arr = new_alignment.seq_to_gap_coords(
+        parent_seq, alphabet=dna_alphabet
+    )
+    assert numpy.array_equal(got_ungap, parent_seq)
+    assert got_empty_arr.size == 0
+
+
+@pytest.mark.parametrize("i", range(3))
+def test_seq_to_gap_coords_arr(gap_seqs, i, dna_alphabet):
+    seq, gap_coords = gap_seqs[i]
+    seq = dna_alphabet.to_indices(seq)
+    got_ungapped, got_map = new_alignment.seq_to_gap_coords(seq, alphabet=dna_alphabet)
+    expect = seq[seq != 4]  # gap_char = 4
+    assert numpy.array_equal(got_ungapped, expect)
+    assert numpy.array_equal(got_map, gap_coords)
+
+
+@pytest.mark.parametrize("i", range(3))
+def test_seq_to_gap_coords_arr_dispatch_equal(gap_seqs, i, dna_alphabet):
+    """seq_to_gap_coords should return the same gap coords when input is a string/array/bytes"""
+    seq_str, _ = gap_seqs[i]
+    seq_array = dna_alphabet.to_indices(seq_str)
+    seq_bytes = dna_alphabet.array_to_bytes(seq_array)
+    seq_from_str, gaps_from_str = new_alignment.seq_to_gap_coords(
+        seq_str, alphabet=dna_alphabet
+    )
+    seq_from_array, gaps_from_arr = new_alignment.seq_to_gap_coords(
+        seq_array, alphabet=dna_alphabet
     )
-    got = ad[seqid]
-    assert isinstance(got, new_alignment.Aligned)
-    assert got.data.parent is ad
+    seq_from_bytes, gaps_from_bytes = new_alignment.seq_to_gap_coords(
+        seq_bytes, alphabet=dna_alphabet
+    )
+    assert numpy.array_equal(gaps_from_str, gaps_from_arr)
+    assert numpy.array_equal(gaps_from_arr, gaps_from_bytes)
+    assert numpy.array_equal(seq_from_str, seq_from_array)
+    assert numpy.array_equal(seq_from_array, seq_from_bytes)
 
 
-@pytest.mark.parametrize("start, stop", [(0, 6), (0, 3), (3, 6), (1, 2)])
 @pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
-def test_aligned_str(aligned_dict, seqid, start, stop, dna_moltype):
+def test_aligned_str(aligned_dict, seqid, dna_moltype):
     """str() of an Aligned instance should return gapped sequence cast to a string"""
     aln = new_alignment.make_aligned_seqs(aligned_dict, moltype=dna_moltype)
-    aligned = aln.seqs[seqid][start:stop]
+    aligned = aln.seqs[seqid]
     got = str(aligned)
-    expect = aligned_dict[seqid][start:stop]
+    expect = aligned_dict[seqid]
     assert got == expect
 
 
@@ -2285,145 +2368,159 @@ def test_aligned_bytes(aligned_dict, seqid, dna_moltype):
     assert got == expect
 
 
+@pytest.mark.parametrize("start", range(6))
+@pytest.mark.parametrize("stop", range(6))
+@pytest.mark.parametrize("step", range(1, 4))
 @pytest.mark.parametrize(
-    "slicer",
-    (
-        slice(0, 3),
-        slice(1, 4),
-        slice(3, 6),
-        slice(None, -3),
-        slice(-10, -3),
-        slice(-10, None),
-    ),
+    "seqid", ("seq1", "seq2", "seq3", "seq4", "seq5", "seq6", "seq7")
 )
-@pytest.mark.parametrize("seqid", ("seq1", "seq2"))
-def test_slice_aligned(seqid, slicer, dna_alphabet, dna_make_seq):
-    seqs = dict(seq1="-AAGGGGGAACCCT", seq2="AAAGGGGGAACCCT")
-    aligned_seqs_data = new_alignment.AlignedSeqsData.from_aligned_seqs(
-        data=seqs, alphabet=dna_alphabet, make_seq=dna_make_seq
-    )
-    al = aligned_seqs_data[seqid]
-    sliced = al[slicer]
-    assert str(sliced) == seqs[seqid][slicer]
+def test_aligned_sliced(gapped_seqs_dict, seqid, start, stop, step):
+    aln = new_alignment.make_aligned_seqs(gapped_seqs_dict, moltype="dna")
+    al = aln.seqs[seqid]
+    got = al[start:stop:step]
+    expect = gapped_seqs_dict[seqid][start:stop:step]
+    assert str(got) == expect
 
+    got1 = got.gapped_seq
+    assert got1 == expect
 
-@pytest.mark.parametrize("seqid", ("seq1", "seq2"))
-@pytest.mark.parametrize("i", (*range(6), slice(0, 2), slice(1, 4), slice(3, 5)))
-def test_aligned_seq_returns_sequence(seqid, i, dna_alphabet, dna_make_seq):
-    """Aligned.seq should return the sequence without gaps, if the index corresponds
-    to a gap position, it will return an empty Sequence"""
-    aligned_dict = dict(seq1="ACG--T", seq2="-CGAAT")
-    ad = new_alignment.AlignedSeqsData.from_aligned_seqs(
-        data=aligned_dict, alphabet=dna_alphabet, make_seq=dna_make_seq
-    )
-    aligned = ad[seqid]
-    got = aligned[i].seq
-    expect = aligned_dict[seqid][i].replace("-", "")
-    assert got == expect
+    # Aligned.seq should return the sequence without gaps
+    got2 = got.seq
+    expect = expect.replace("-", "")
+    assert got2 == expect
 
 
-@pytest.mark.parametrize("i", range(3))
-def test_seq_to_gap_coords_str(gap_seqs, i, dna_alphabet, dna_make_seq):
-    seq, gap_coords = gap_seqs[i]
-    got_ungapped, got_map = new_alignment.seq_to_gap_coords(
-        seq, alphabet=dna_alphabet, make_seq=dna_make_seq
-    )
-    assert got_ungapped == seq.replace("-", "")
-    assert numpy.array_equal(got_map, gap_coords)
+@pytest.mark.parametrize(
+    "seqid", ("seq1", "seq2", "seq3", "seq4", "seq5", "seq6", "seq7")
+)
+def test_aligned_iter(seqid, gapped_seqs_dict):
+    aln = new_alignment.make_aligned_seqs(gapped_seqs_dict, moltype="dna")
+    aligned = aln.seqs[seqid]
+    for i, got in enumerate(aligned):
+        expect = gapped_seqs_dict[seqid][i]  # directly index the sequence
+        assert got == expect
 
 
-def test_seq_to_gap_coords_str_all_gaps(dna_alphabet, dna_make_seq):
-    parent_seq = "-----"
-    expect_gaplen = numpy.array([len(parent_seq)])
-    got_ungap, got_map = new_alignment.seq_to_gap_coords(
-        parent_seq, alphabet=dna_alphabet, make_seq=dna_make_seq
+@pytest.mark.parametrize("seqid", ("seq0", "seq1", "seq2"))
+def test_aligned_seqs_data_init(seqid, gap_seqs, dna_alphabet, dna_make_seq):
+    """test initialisation of AlignedSeqsData object with dictionary of ungapped
+    sequences and dictionary of gap coordinates and cumulated gap lengths"""
+    seqs = {f"seq{i}": seq.replace("-", "") for i, (seq, _) in enumerate(gap_seqs)}
+    gaps = {f"seq{i}": numpy.array(gaps) for i, (_, gaps) in enumerate(gap_seqs)}
+    ad = new_alignment.AlignedSeqsData(
+        seqs=seqs, gaps=gaps, alphabet=dna_alphabet, make_seq=dna_make_seq
     )
-    assert got_ungap == ""
-    assert got_map[:, 1] == expect_gaplen
+    assert ad.get_seq_str(seqid=seqid) == seqs[seqid]
+    assert numpy.array_equal(ad.get_gaps(seqid=seqid), gaps[seqid])
 
 
-def test_seq_to_gap_coords_str_no_gaps(dna_alphabet, dna_make_seq):
-    parent_seq = "ACTGC"
-    got_ungap, got_empty_arr = new_alignment.seq_to_gap_coords(
-        parent_seq, alphabet=dna_alphabet, make_seq=dna_make_seq
-    )
-    assert got_ungap == parent_seq
-    assert got_empty_arr.size == 0
+@pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4", "seq5"))
+@pytest.mark.parametrize("data_type", (str, numpy.array, bytes))
+def test_aligned_seqs_data_init_gapped(
+    gapped_seqs_dict, seqid, data_type, dna_alphabet, dna_moltype, dna_make_seq
+):
+    """AlignedSeqsData should handle data from gapped sequences correctly,
+    including edge cases (all gaps, no gaps, etc)"""
 
+    typed_data = {
+        name: make_typed(seq, data_type=data_type, moltype=dna_moltype)
+        for name, seq in gapped_seqs_dict.items()
+    }
 
-def test_seq_to_gap_coords_arr_all_gaps(dna_alphabet, dna_make_seq):
-    parent_seq = dna_alphabet.to_indices("-----")
-    got_ungap, got_map = new_alignment.seq_to_gap_coords(
-        parent_seq, alphabet=dna_alphabet, make_seq=dna_make_seq
+    seq_data = {
+        name: new_alignment.seq_to_gap_coords(seq, alphabet=dna_alphabet)[0]
+        for name, seq in typed_data.items()
+    }
+    gap_data = {
+        name: new_alignment.seq_to_gap_coords(seq, alphabet=dna_alphabet)[1]
+        for name, seq in typed_data.items()
+    }
+    asd = new_alignment.AlignedSeqsData(
+        seqs=seq_data, gaps=gap_data, alphabet=dna_alphabet, make_seq=dna_make_seq
     )
-    assert got_ungap.size == 0
-    assert numpy.array_equal(got_map, numpy.array([[0, 5]]))
+    assert asd.align_len == 6
+    assert asd.get_gapped_seq_str(seqid=seqid) == gapped_seqs_dict[seqid]
 
 
-def test_seq_to_gap_coords_arr_no_gaps(dna_alphabet, dna_make_seq):
-    parent_seq = dna_alphabet.to_indices("ACTGC")
-    got_ungap, got_empty_arr = new_alignment.seq_to_gap_coords(
-        parent_seq, alphabet=dna_alphabet, make_seq=dna_make_seq
+@pytest.mark.parametrize("data_type", (str, numpy.array, bytes))
+def test_aligned_seqs_data_unequal_seqlens_raises(
+    data_type, dna_alphabet, dna_moltype, dna_make_seq
+):
+    """from_aligned_seqs should raise an error if sequences are of unequal length"""
+    data = dict(
+        seq1=make_typed("A-A", data_type=data_type, moltype=dna_moltype),
+        seq2=make_typed("AAAAAAA--", data_type=data_type, moltype=dna_moltype),
     )
-    assert numpy.array_equal(got_ungap, parent_seq)
-    assert got_empty_arr.size == 0
-
+    with pytest.raises(ValueError):
+        _ = new_alignment.AlignedSeqsData.from_aligned_seqs(
+            data=data, alphabet=dna_alphabet, make_seq=dna_make_seq
+        )
+    # directly creating an AlignedSeqsData object should also raise an error
+    seq_data = {
+        name: new_alignment.seq_to_gap_coords(seq, alphabet=dna_alphabet)[0]
+        for name, seq in data.items()
+    }
+    gap_data = {
+        name: new_alignment.seq_to_gap_coords(seq, alphabet=dna_alphabet)[1]
+        for name, seq in data.items()
+    }
+    with pytest.raises(ValueError):
+        _ = new_alignment.AlignedSeqsData(
+            seqs=seq_data, gaps=gap_data, alphabet=dna_alphabet, make_seq=dna_make_seq
+        )
 
-@pytest.mark.parametrize("i", range(3))
-def test_seq_to_gap_coords_arr(gap_seqs, i, dna_alphabet, dna_make_seq):
-    seq, gap_coords = gap_seqs[i]
-    seq = dna_alphabet.to_indices(seq)  # convert to array repr
-    got_ungapped, got_map = new_alignment.seq_to_gap_coords(
-        seq, alphabet=dna_alphabet, make_seq=dna_make_seq
-    )
-    assert numpy.array_equal(got_ungapped, seq[seq != 4])  # gap_char = 4
-    assert numpy.array_equal(got_map, gap_coords)
 
+def test_aligned_seqs_data_diff_keys_raises(dna_alphabet, dna_make_seq):
+    """AlignedSeqsData expect identical keys in seqs and gaps"""
+    seqs = dict(seq1=numpy.array([2, 1]), seq2=numpy.array([2, 0, 3, 1]))
+    gaps = dict(seq1=numpy.array([[1, 3]]), seq3=numpy.array([[0, 1]]))
 
-@pytest.mark.parametrize("i", range(3))
-def test_seq_to_gap_coords_arr_dispatch_equal(gap_seqs, i, dna_alphabet, dna_make_seq):
-    """seq_to_gap_coords should return the same gap coords when input is a string/array/bytes"""
-    seq_str, _ = gap_seqs[i]
-    seq_array = dna_alphabet.to_indices(seq_str)
-    seq_bytes = dna_alphabet.array_to_bytes(seq_array)
-    _, got_from_str = new_alignment.seq_to_gap_coords(
-        seq_str, alphabet=dna_alphabet, make_seq=dna_make_seq
-    )
-    _, got_from_arr = new_alignment.seq_to_gap_coords(
-        seq_array, alphabet=dna_alphabet, make_seq=dna_make_seq
-    )
-    _, got_from_bytes = new_alignment.seq_to_gap_coords(
-        seq_bytes, alphabet=dna_alphabet, make_seq=dna_make_seq
+    with pytest.raises(ValueError):
+        _ = new_alignment.AlignedSeqsData(
+            seqs=seqs, gaps=gaps, alphabet=dna_alphabet, make_seq=dna_make_seq
+        )
+    # assert that it would work if we indeed had the same keys
+    gaps["seq2"] = gaps.pop("seq3")
+    asd = new_alignment.AlignedSeqsData(
+        seqs=seqs, gaps=gaps, alphabet=dna_alphabet, make_seq=dna_make_seq
     )
-    assert numpy.array_equal(got_from_str, got_from_arr)
-    assert numpy.array_equal(got_from_str, got_from_bytes)
+    assert asd.get_seq_str(seqid="seq1") == "AC"
 
 
-def test_aligned_seqs_data_from_string_unequal_seqlens(dna_alphabet, dna_make_seq):
-    data = dict(seq1="A-A", seq2="AAAAAAA--")
+def test_aligned_seqs_data_omit_seqs_gaps_raises(dna_alphabet, dna_make_seq):
     with pytest.raises(ValueError):
-        _ = new_alignment.AlignedSeqsData.from_aligned_seqs(
-            data=data, alphabet=dna_alphabet, make_seq=dna_make_seq
+        _ = new_alignment.AlignedSeqsData(
+            seqs={}, gaps={}, alphabet=dna_alphabet, make_seq=dna_make_seq
         )
 
 
-def test_aligned_seqs_data_from_string_returns_self(
-    aligned_dict, dna_alphabet, dna_make_seq
-):
+def test_aligned_seqs_data_names(aligned_dict, dna_alphabet, dna_make_seq):
     got = new_alignment.AlignedSeqsData.from_aligned_seqs(
         data=aligned_dict, alphabet=dna_alphabet, make_seq=dna_make_seq
     )
     assert isinstance(got, new_alignment.AlignedSeqsData)
     assert got.names == ["seq1", "seq2", "seq3", "seq4"]
-    assert got.align_len == 6
 
 
-def test_aligned_seqs_data_init_check(gap_seqs, dna_alphabet):
-    seqs = {f"seq_{i}": seq.replace("-", "") for i, (seq, _) in enumerate(gap_seqs)}
-    gaps = {f"seq_{i}": numpy.array(gaps) for i, (_, gaps) in enumerate(gap_seqs)}
-    ad = new_alignment.AlignedSeqsData(seqs=seqs, gaps=gaps, alphabet=dna_alphabet)
-    assert ad.get_seq_str(seqid="seq_0") == seqs["seq_0"]
-    assert numpy.array_equal(ad.get_gaps(seqid="seq_0"), gaps["seq_0"])
+def test_aligned_seqs_data_len(aligned_dict, dna_alphabet, dna_make_seq):
+    got = new_alignment.AlignedSeqsData.from_aligned_seqs(
+        data=aligned_dict, alphabet=dna_alphabet, make_seq=dna_make_seq
+    )
+    assert len(got) == len(aligned_dict["seq1"])
+    assert got.align_len == len(aligned_dict["seq1"])
+
+
+@pytest.mark.parametrize(
+    "seqid, index", [("seq1", 0), ("seq2", 1), ("seq3", 2), ("seq4", 3)]
+)
+def test_aligned_seqs_data_getitem(seqid, index):
+    data = dict(seq1="ACG--T", seq2="-CGAAT", seq3="------", seq4="--GA--")
+    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
+    got_with_seqid = aln.seqs[seqid]
+    got_with_index = aln.seqs[index]
+
+    assert got_with_index.seq == got_with_seqid.seq == data[seqid].replace("-", "")
+    assert got_with_index.gapped_seq == got_with_seqid.gapped_seq == data[seqid]
 
 
 @pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
@@ -2436,7 +2533,7 @@ def test_aligned_seqs_data_get_seq_array(aligned_array_dict, seqid, moltype, req
         make_seq=moltype.make_seq,
     )
     got = ad.get_seq_array(seqid=seqid)
-    expect = aligned_array_dict[seqid]
+    expect = aligned_array_dict[seqid]  # get original data
     expect = expect[expect != 4]  # remove gaps
     assert numpy.array_equal(got, expect)
 
@@ -2452,7 +2549,7 @@ def test_aligned_seqs_data_get_gapped_seq_array(
         alphabet=moltype.degen_gapped_alphabet,
         make_seq=moltype.make_seq,
     )
-    got = ad.get_gapped_seq_array(seqid=seqid)
+    got = ad.get_gapped_seq_array(seqid=seqid)  # get original data
     expect = aligned_array_dict[seqid]
     assert numpy.array_equal(got, expect)
 
@@ -2523,6 +2620,7 @@ def test_aligned_seqs_data_get_gapped_seq_bytes(
 
 @pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
 def test_aligned_seqs_data_seq_lengths(seqid, aligned_dict, dna_alphabet, dna_make_seq):
+    """AlignedSeqsData.seq_lengths should return the length of the ungapped sequences"""
     ad = new_alignment.AlignedSeqsData.from_aligned_seqs(
         data=aligned_dict, alphabet=dna_alphabet, make_seq=dna_make_seq
     )
@@ -2564,7 +2662,7 @@ def test_aligned_seqs_data_add_seqs_diff_moltype_raises(dna_alphabet, dna_make_s
         data=data, alphabet=dna_alphabet, make_seq=dna_make_seq
     )
     new_data = {"seq3": "ACGU-", "seq4": "ACG-U"}  # RNA
-    with pytest.raises(AssertionError):
+    with pytest.raises(ValueError):
         _ = ad.add_seqs(new_data)
 
 
@@ -2594,8 +2692,8 @@ def test_aligned_seqs_data_get_aligned_view(
     got = ad.get_view(seqid)
     assert isinstance(got, new_alignment.AlignedDataView)
     assert got.parent == ad
-    assert got.stop == ad.align_len
     assert got.parent_len == ad.align_len
+    assert str(got) == aligned_dict[seqid]
 
 
 @pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
@@ -2605,7 +2703,7 @@ def test_aligned_data_view_array(aligned_array_dict, dna_alphabet, dna_make_seq,
     )
     view = ad.get_view(seqid)
     got = numpy.array(view)
-    expect = aligned_array_dict[seqid][aligned_array_dict[seqid] != 4]  # remove gaps
+    expect = aligned_array_dict[seqid]
     assert numpy.array_equal(got, expect)
 
 
@@ -2616,27 +2714,11 @@ def test_aligned_data_view_bytes(aligned_array_dict, dna_alphabet, dna_make_seq,
     )
     view = ad.get_view(seqid)
     got = bytes(view)
-    expect = aligned_array_dict[seqid][aligned_array_dict[seqid] != 4]  # remove gaps
+    expect = aligned_array_dict[seqid]
     expect = dna_alphabet.array_to_bytes(expect)  # convert to bytes
     assert numpy.array_equal(got, expect)
 
 
-@pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
-def test_aligned_data_view_zero_slice(
-    aligned_array_dict, dna_alphabet, dna_make_seq, seqid
-):
-    # an invalid slice should return a empty view
-    ad = new_alignment.AlignedSeqsData.from_aligned_seqs(
-        data=aligned_array_dict, alphabet=dna_alphabet, make_seq=dna_make_seq
-    )
-    view = ad.get_view(seqid)
-    got = view[:0]
-    assert len(got) == 0
-    got = view[15:20]
-    print(got)
-    assert len(got) == 0
-
-
 def test_alignment_init(aligned_dict, dna_moltype, dna_alphabet, dna_make_seq):
     ad = new_alignment.AlignedSeqsData.from_aligned_seqs(
         data=aligned_dict, alphabet=dna_alphabet, make_seq=dna_make_seq
@@ -2692,7 +2774,7 @@ def test_alignment_get_gapped_seq():
     assert got == expect
 
 
-def test_iter_positions():
+def test_alignment_iter_positions():
     data = {"a": "AAAAAA", "b": "AAA---", "c": "AAAA--"}
     r = new_alignment.make_aligned_seqs({k: data[k] for k in "cb"}, moltype="dna")
     assert list(r.iter_positions(pos_order=[5, 1, 3])) == list(
@@ -2704,15 +2786,166 @@ def test_iter_positions():
     assert cols == list(map(list, ["AAA", "AAA", "AAA", "A-A", "A--", "A--"]))
 
 
-def test_alignment_to_dict():
-    data = {"a": "--A-BC-", "b": "-CB-A--", "c": "--D-EF-"}
-    aln = new_alignment.make_aligned_seqs(data, moltype="protein")
-    assert aln.to_dict() == data
+def test_alignment_to_dict(gapped_seqs_dict):
+    aln = new_alignment.make_aligned_seqs(gapped_seqs_dict, moltype="protein")
+    assert aln.to_dict() == gapped_seqs_dict
 
 
-def test_alignment_get_lengths():
-    data = {"a": "--C-CC-", "b": "--T-T--", "c": "--A-GG-"}
-    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
+def test_alignment_get_lengths(gapped_seqs_dict):
+    aln = new_alignment.make_aligned_seqs(gapped_seqs_dict, moltype="dna")
     got = aln.get_lengths()
-    expect = {name: len(seq.replace("-", "")) for name, seq in data.items()}
+    expect = {name: len(seq.replace("-", "")) for name, seq in gapped_seqs_dict.items()}
+    assert got == expect
+
+
+@pytest.mark.parametrize(
+    "moltype",
+    ["rna", "dna", "protein"],
+)
+def test_upac_consensus_allow_gaps(moltype):
+    aln = new_alignment.make_aligned_seqs(
+        {"s1": "ACGG", "s2": "ACGG", "s3": "-CGG"},
+        moltype=moltype,
+    )
+    # default behaviour
+    iupac = aln.iupac_consensus()
+    assert iupac == "?CGG"
+
+    # allow_gaps
+    iupac = aln.iupac_consensus(allow_gap=False)
+    assert iupac == "ACGG"
+
+
+def test_alignment_to_pretty():
+    """produce correct pretty print formatted text"""
+    seqs = {"seq1": "ACGAANGA", "seq2": "-CGAACGA", "seq3": "ATGAACGA"}
+    expect = ["seq1    ACGAANGA", "seq2    -....C..", "seq3    .T...C.."]
+
+    aln = new_alignment.make_aligned_seqs(seqs, moltype="dna")
+    got = aln.to_pretty(name_order=["seq1", "seq2", "seq3"])
+    assert got == "\n".join(expect)
+
+    got = aln.to_pretty(name_order=["seq1", "seq2", "seq3"], wrap=4)
+    expect = [
+        "seq1    ACGA",
+        "seq2    -...",
+        "seq3    .T..",
+        "",
+        "seq1    ANGA",
+        "seq2    .C..",
+        "seq3    .C..",
+    ]
+    assert got == "\n".join(expect)
+
+
+def test_alignment_to_html():
+    """produce correct html formatted text"""
+    seqs = {"seq1": "ACG", "seq2": "-CT"}
+
+    aln = new_alignment.make_aligned_seqs(seqs, moltype="dna")
+    got = aln.to_html(ref_name="longest")  # name_order=['seq1', 'seq2'])
+    # ensure balanced tags are in the txt
+    for tag in ["<style>", "</style>", "<div", "</div>", "<table>", "</table>"]:
+        assert tag in got
+
+    ref_row = (
+        '<tr><td class="label">seq1</td>'
+        '<td><span class="A_dna">A</span>'
+        '<span class="C_dna">C</span>'
+        '<span class="G_dna">G</span></td></tr>'
+    )
+    other_row = (
+        '<tr><td class="label">seq2</td>'
+        '<td><span class="ambig_dna">-</span>'
+        '<span class="C_dna">.</span>'
+        '<span class="T_dna">T</span></td></tr>'
+    )
+
+    assert ref_row in got
+    assert other_row in got
+    assert got.find(ref_row) < got.find(other_row)
+
+    # using different ref sequence
+    ref_row = (
+        '<tr><td class="label">seq2</td>'
+        '<td><span class="terminal_ambig_dna">-</span>'
+        '<span class="C_dna">C</span>'
+        '<span class="T_dna">T</span></td></tr>'
+    )
+    other_row = (
+        '<tr><td class="label">seq1</td>'
+        '<td><span class="A_dna">A</span>'
+        '<span class="C_dna">.</span>'
+        '<span class="G_dna">G</span></td></tr>'
+    )
+    got = aln.to_html(ref_name="seq2")
+    # order now changes
+    assert got.find(ref_row) < got.find(other_row)
+
+
+@pytest.mark.xfail(
+    reason="todo: Alignment repr currently using Seqcollection code, need to update"
+)
+def test_alignment_repr():
+    data = {
+        "ENSMUSG00000056468": "GCCAGGGGGAAAA",
+        "ENSMUSG00000039616": "GCCCTTCAAATTT",
+    }
+    seqs = new_alignment.make_aligned_seqs(data, moltype="dna")
+    assert (
+        repr(seqs)
+        == "2 x 13 dna alignment: ENSMUSG00000056468[GCCAGGGGGA...], ENSMUSG00000039616[GCCCTTCAAA...]"
+    )
+
+    data = {
+        "ENSMUSG00000039616": "GCCCTTCAAATTT",
+        "ENSMUSG00000056468": "GCCAGGGGGAAAA",
+    }
+    seqs = new_alignment.make_aligned_seqs(data, moltype="dna")
+
+    assert (
+        repr(seqs)
+        == "2 x 13 dna alignment: ENSMUSG00000039616[GCCCTTCAAA...], ENSMUSG00000056468[GCCAGGGGGA...]"
+    )
+
+    data = {
+        "a": "TCGAT",
+    }
+    seqs = new_alignment.make_aligned_seqs(data, moltype="dna")
+    assert repr(seqs) == "1 x 5 dna alignment: a[TCGAT]"
+
+    data = {
+        "a": "TCGAT" * 2,
+    }
+    seqs = new_alignment.make_aligned_seqs(data, moltype="dna")
+    assert repr(seqs) == "1 x 10 dna alignment: a[TCGATTCGAT]"
+
+    data = {
+        "a": "A" * 11,
+        "b": "B" * 11,
+        "c": "C" * 11,
+        "d": "D" * 11,
+        "e": "E" * 11,
+    }
+    seqs = new_alignment.make_aligned_seqs(data, moltype="dna")
+    assert (
+        repr(seqs)
+        == "5 x 11 text alignment: a[AAAAAAAAAA...], b[BBBBBBBBBB...], c[CCCCCCCCCC...], ..."
+    )
+
+
+@pytest.mark.parametrize("start", range(6))
+@pytest.mark.parametrize("stop", range(6))
+@pytest.mark.parametrize("step", range(1, 3))
+@pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
+def test_alignment_slice(aligned_dict, start, stop, step, seqid):
+    """slicing an alignment should propogate the slice to aligned instances"""
+    aln = new_alignment.make_aligned_seqs(aligned_dict, moltype="dna")
+    sliced_aln = aln[start:stop:step]
+    got = sliced_aln.seqs[seqid].gapped_seq
+    expect = aligned_dict[seqid][start:stop:step]
+    assert got == expect
+
+    got = sliced_aln.seqs[seqid].seq
+    expect = expect.replace("-", "")
     assert got == expect