Repository: cogent3/cogent3
Pull request #2036 (2024-10-08 05:05:25)

ENH: continued Alignment refactor 
Major design change in this PR:
- DEV: `Seq[Data]View` no longer inherits from `SliceRecord`
- ENH: added intermediate class `_IndexableSeqs` which handles the construction of `Aligned` instances
- DEV: generally following the paradigm that all methods that are from `SeqsDataABC` should work in sequence coordinates and all methods unique to `AlignedSeqsDataABC` should work in aligned coordinates

Note: the drop in coverage is from the addition of feature querying methods for which the tests are still xfail


Patch:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 8ec70de36..6ce4dc63d 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -2532,7 +2532,7 @@ class AlignmentI(object):
             f"{len(self.names)} x {self.seq_len} {self.moltype.label} alignment: {seqs}"
         )
 
-    def alignment_quality(self, app_name: str = "ic_score", **kwargs):
+    def alignment_quality(self, app_name: str = "ic_score", **kwargs):  # ported
         """
         Computes the alignment quality using the indicated app
 
@@ -2575,7 +2575,7 @@ class AlignmentI(object):
         for pos in pos_order:
             yield [get(seq)[pos] for seq in seq_order]
 
-    positions = property(iter_positions)
+    positions = property(iter_positions)  # ported
 
     def take_positions(self, cols, negate=False):
         """Returns new Alignment containing only specified positions.
@@ -2603,7 +2603,7 @@ class AlignmentI(object):
             result, names=self.names, info=self.info, moltype=self.moltype
         )
 
-    def get_position_indices(self, f, native=False, negate=False):
+    def get_position_indices(self, f, native=False, negate=False):  # ported
         """Returns list of column indices for which f(col) is True.
 
         f : callable
@@ -2631,7 +2631,7 @@ class AlignmentI(object):
         """Returns new Alignment containing cols where f(col) is True."""
         return self.take_positions(self.get_position_indices(f, negate=negate))
 
-    def iupac_consensus(self, alphabet=None, allow_gap=True):
+    def iupac_consensus(self, alphabet=None, allow_gap=True):  # ported
         """Returns string containing IUPAC consensus sequence of the alignment."""
         if alphabet is None:
             alphabet = self.moltype
@@ -2644,7 +2644,7 @@ class AlignmentI(object):
             consensus.append(degen(coerce_to_string(col)))
         return coerce_to_string(consensus)
 
-    def majority_consensus(self):
+    def majority_consensus(self):  # ported
         """Returns list containing most frequent item at each position.
 
         Optional parameter transform gives constructor for type to which result
@@ -2661,7 +2661,7 @@ class AlignmentI(object):
 
     def probs_per_pos(
         self, motif_length=1, include_ambiguity=False, allow_gap=False, warn=False
-    ):
+    ):  # ported
         """returns MotifFreqsArray per position"""
         counts = self.counts_per_pos(
             motif_length=motif_length,
@@ -2673,7 +2673,7 @@ class AlignmentI(object):
 
     def entropy_per_pos(
         self, motif_length=1, include_ambiguity=False, allow_gap=False, warn=False
-    ):
+    ):  # ported
         """returns shannon entropy per position"""
         probs = self.probs_per_pos(
             motif_length=motif_length,
@@ -2690,7 +2690,7 @@ class AlignmentI(object):
         allow_gap=False,
         exclude_unobserved=False,
         warn=False,
-    ):
+    ):  # ported
         """return MotifFreqsArray per sequence
 
         Parameters
@@ -2725,7 +2725,7 @@ class AlignmentI(object):
         allow_gap=False,
         exclude_unobserved=True,
         warn=False,
-    ):
+    ):  # ported
         """returns the Shannon entropy per sequence
 
         Parameters
@@ -2810,7 +2810,7 @@ class AlignmentI(object):
         )
         return self.filtered(gaps_ok, motif_length=motif_length)
 
-    def get_gap_array(self, include_ambiguity=True):
+    def get_gap_array(self, include_ambiguity=True):  # ported
         """returns bool array with gap state True, False otherwise
 
         Parameters
@@ -2831,7 +2831,7 @@ class AlignmentI(object):
 
         return gapped
 
-    def count_gaps_per_pos(self, include_ambiguity=True):
+    def count_gaps_per_pos(self, include_ambiguity=True):  # ported
         """return counts of gaps per position as a DictArray
 
         Parameters
@@ -2849,7 +2849,7 @@ class AlignmentI(object):
 
     def count_gaps_per_seq(
         self, induced_by=False, unique=False, include_ambiguity=True, drawable=False
-    ):
+    ):  # ported
         """return counts of gaps per sequence as a DictArray
 
         Parameters
@@ -2904,7 +2904,7 @@ class AlignmentI(object):
 
         return result
 
-    def omit_bad_seqs(self, quantile=None):
+    def omit_bad_seqs(self, quantile=None):  #  ported
         """Returns new alignment without sequences with a number of uniquely
         introduced gaps exceeding quantile
 
@@ -2924,7 +2924,7 @@ class AlignmentI(object):
         names = [name for name, count in gap_counts.items() if count <= cutoff]
         return self.take_seqs(names)
 
-    def matching_ref(self, ref_name, gap_fraction, gap_run):
+    def matching_ref(self, ref_name, gap_fraction, gap_run):  # ported
         """Returns new alignment with seqs well aligned with a reference.
 
         gap_fraction = fraction of positions that either have a gap in the
@@ -2988,7 +2988,7 @@ class AlignmentI(object):
 
         return self.__class__(new_seqs, info=self.info, moltype=self.moltype)
 
-    def sliding_windows(self, window, step, start=None, end=None):
+    def sliding_windows(self, window, step, start=None, end=None):  # ported
         """Generator yielding new alignments of given length and interval.
 
         Parameters
@@ -3010,7 +3010,7 @@ class AlignmentI(object):
             for pos in range(start, end, step):
                 yield self[pos : pos + window]
 
-    def _get_raw_pretty(self, name_order):
+    def _get_raw_pretty(self, name_order):  # ported
         """returns dict {name: seq, ...} for pretty print"""
         if name_order is not None:
             assert set(name_order) <= set(self.names), "names don't match"
@@ -3035,7 +3035,7 @@ class AlignmentI(object):
 
         return names, output
 
-    def _repr_html_(self) -> str:
+    def _repr_html_(self) -> str:  # ported
         settings = self._repr_policy.copy()
         env_vals = get_setting_from_environ(
             "COGENT3_ALIGNMENT_REPR_POLICY",
@@ -3058,7 +3058,7 @@ class AlignmentI(object):
         colors: Optional[Mapping[str, str]] = None,
         font_size: int = 12,
         font_family: str = "Lucida Console",
-    ) -> str:
+    ) -> str:  # ported
         """returns html with embedded styles for sequence colouring
 
         Parameters
@@ -3222,7 +3222,7 @@ class AlignmentI(object):
         ]
         return "\n".join(text)
 
-    def to_pretty(self, name_order=None, wrap=None):
+    def to_pretty(self, name_order=None, wrap=None):  # ported
         """returns a string representation of the alignment in pretty print format
 
         Parameters
@@ -3264,7 +3264,7 @@ class AlignmentI(object):
 
     def counts_per_pos(
         self, motif_length=1, include_ambiguity=False, allow_gap=False, warn=False
-    ):
+    ):  # ported
         """return DictArray of counts per position
 
         Parameters
@@ -3316,7 +3316,7 @@ class AlignmentI(object):
         allow_gap=False,
         exclude_unobserved=False,
         warn=False,
-    ):
+    ):  # ported
         """counts of non-overlapping motifs per sequence
 
         Parameters
@@ -4147,7 +4147,7 @@ class ArrayAlignment(AlignmentI, _SequenceCollectionBase):
             names=self.names,
         )
 
-    def get_gapped_seq(self, seq_name, recode_gaps=False):
+    def get_gapped_seq(self, seq_name, recode_gaps=False):  # ported
         """Return a gapped Sequence object for the specified seqname.
 
         Note: return type may depend on what data was loaded into the
@@ -4555,7 +4555,7 @@ class ArrayAlignment(AlignmentI, _SequenceCollectionBase):
         return gapped
 
 
-def make_gap_filter(template, gap_fraction, gap_run):
+def make_gap_filter(template, gap_fraction, gap_run):  # ported
     """Returns f(seq) -> True if no gap runs and acceptable gap fraction.
 
     Calculations relative to template.
diff --git a/src/cogent3/core/location.py b/src/cogent3/core/location.py
index e24d4045c..b27cb761a 100644
--- a/src/cogent3/core/location.py
+++ b/src/cogent3/core/location.py
@@ -1016,17 +1016,11 @@ def _step_adjusted_length(start: int, end: int, adj: int, step: int) -> int:
 def _input_vals_pos_step(
     seqlen: int, start: OptInt, stop: OptInt, step: int
 ) -> tuple[int, int, int]:
-    """returns standardised start, stop, step values for positive step slicing.
+    """returns standardised start, stop, step values for positive step slicing."""
+    # The input values for start and stop can be +ve, -ve or None. The returned
+    # start and stop are strictly +ve and within the sequence length, and if not
+    # provided default to 0 and seqlen, respectively.
 
-    Notes
-    -----
-    The input values for start and stop can be +ve, -ve or None. The returned
-    start and stop are strictly +ve and within the sequence length, and if not
-    provided default to 0 and seqlen respectively.
-
-    If the slice results in a zero-length slice, the returned start, stop, step
-    will be 0, 0, 1 respectively.
-    """
     start = start or 0
     if start > 0 and start >= seqlen:
         # start beyond seq is an empty slice
@@ -1054,23 +1048,17 @@ def _input_vals_pos_step(
 def _input_vals_neg_step(
     seqlen: int, start: OptInt, stop: OptInt, step: int
 ) -> tuple[int, int, int]:
-    """returns standardised start, stop, step values for negative step slicing.
-
-    Notes
-    -----
-    The input values for start and stop can be positive, negative or None. The
-    returned start and stop are strictly negative and correspond to indices
-    within the sequence length. If not provided, start defaults to -1 and stop
-    to -seqlen-1.
+    """returns standardised start, stop, step values for negative step slicing."""
+    # The input values for start and stop can be positive, negative or None. The
+    # returned start and stop are strictly negative and correspond to indices
+    # within the sequence length. If not provided, start defaults to -1 and stop
+    # to -seqlen-1.
 
-    If the slice results in a zero-length slice, the returned start, stop, step
-    will be 0, 0, 1 respectively.
-    """
     # Note how Python reverse slicing works
-    # we need to make sure the start and stop are both
-    # negative, for example "abcd"[-1:-5:-1] returns "dcba"
-    # returning the complete string in reverse order is only
-    # possible with negative indexing
+    # we need to make sure the start and stop are both negative, for example
+    # "abcd"[-1:-5:-1] returns "dcba". returning the complete string in reverse
+    # order is only possible with negative indexing
+
     if start is None or start >= seqlen:  # set default
         start = -1  # Done
     elif start >= 0:  # convert to -ve index
@@ -1367,12 +1355,10 @@ class IndelMap(MapABC):
 
         Returns
             True if no gaps are present in the slice
-
-        Notes
-        -----
-        this method assumes positive indexing and a positive step
-        (i.e. start, stop, step > 0)
         """
+        # note that this methods assumes positive indexing and a positive step
+        # (i.e. start, stop, step > 0)
+
         # when slicing an indel maps, we can have four easy cases:
         # 1 - invalid slice
         if start == stop:
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index 22b88cf87..b6da1adf4 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import json
+import os
 import re
 import typing
 import warnings
@@ -12,6 +13,7 @@ from typing import Any, Callable, Iterator, Mapping, Optional, Union
 
 import numpy
 
+from cogent3 import get_app
 from cogent3._version import __version__
 from cogent3.core import new_alphabet, new_moltype, new_sequence
 from cogent3.core.annotation import Feature
@@ -26,19 +28,23 @@ from cogent3.core.profile import PSSM, MotifCountsArray, MotifFreqsArray, load_p
 from cogent3.format.alignment import save_to_filename
 from cogent3.format.fasta import seqs_to_fasta
 from cogent3.format.phylip import alignment_to_phylip
+from cogent3.maths.stats.number import CategoryCounter
 from cogent3.util import progress_display as UI
 from cogent3.util import warning as c3warn
 from cogent3.util.deserialise import deserialise_object, register_deserialiser
+from cogent3.util.dict_array import DictArray, DictArrayTemplate
 from cogent3.util.io import atomic_write, get_format_suffixes
 from cogent3.util.misc import (
     get_object_provenance,
     get_setting_from_environ,
     negate_condition,
 )
+from cogent3.util.union_dict import UnionDict
 
 DEFAULT_ANNOTATION_DB = BasicAnnotationDb
 
 OptInt = Optional[int]
+OptFloat = Optional[float]
 OptStr = Optional[str]
 OptList = Optional[list]
 OptDict = Optional[dict]
@@ -83,7 +89,7 @@ def assign_sequential_names(num_seqs: int, base_name: str = "seq", start_at: int
     return [f"{base_name}_{i}" for i in range(start_at, start_at + num_seqs)]
 
 
-class SeqDataView(new_sequence.SeqViewABC, new_sequence.SliceRecordABC):
+class SeqDataView(new_sequence.SeqViewABC):
     """
     A view class for SeqsData, providing methods for different representations
     of a single sequence.
@@ -100,48 +106,25 @@ class SeqDataView(new_sequence.SeqViewABC, new_sequence.SliceRecordABC):
     # array([3, 1, 2, 0], dtype=int8)
     """
 
-    __slots__ = ("parent", "start", "stop", "step", "_offset", "_seqid", "_parent_len")
+    __slots__ = ("parent", "alphabet", "_seqid", "_parent_len", "_slice_record")
 
     def __init__(
         self,
         *,
         parent: SeqsData,
-        seqid: str,
+        alphabet: new_alphabet.CharAlphabet,
         parent_len: int,
-        start: OptInt = None,
-        stop: OptInt = None,
-        step: OptInt = None,
-        offset: int = 0,
+        seqid: str = None,
+        slice_record: new_sequence.SliceRecordABC = None,
     ):
-        if step == 0:
-            raise ValueError("step cannot be 0")
-        step = 1 if step is None else step
-        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.parent = parent
-        self.start = start
-        self.stop = stop
-        self.step = step
-        self._offset = offset
+        self.alphabet = alphabet
         self._seqid = seqid
-
-    def _checked_seq_len(self, seq_len: int) -> int:
-        assert seq_len is not None
-        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,
+        self._parent_len = parent_len
+        self._slice_record = (
+            slice_record
+            if slice_record is not None
+            else new_sequence.SliceRecord(parent_len=self._parent_len)
         )
 
     @property
@@ -152,32 +135,46 @@ class SeqDataView(new_sequence.SeqViewABC, new_sequence.SliceRecordABC):
     def parent_len(self) -> int:
         return self._parent_len
 
-    def _get_init_kwargs(self):
-        return {"parent": self.parent, "seqid": self.seqid}
+    @property
+    def slice_record(self) -> new_sequence.SliceRecordABC:
+        return self._slice_record
 
     @property
     def str_value(self) -> str:
         """returns the sequence as a string"""
+        # todo: kath, in ADV, the .get_seq_str method gets passed the step and
+        # the returned sequence is sliced by the step. In SDV, the step is not
+        # applied in the get_seq_str method, but is applied in this method here.
+        # can we make this consistent?
+
+        # also, keep using parent_start and parent_stop or to use start and stop?
+        # this is another inconsistency between SDV and ADV
         raw = self.parent.get_seq_str(
-            seqid=self.seqid, start=self.parent_start, stop=self.parent_stop
+            seqid=self.seqid,
+            start=self.slice_record.parent_start,
+            stop=self.slice_record.parent_stop,
         )
-        return raw if self.step == 1 else raw[:: self.step]
+        return raw if self.slice_record.step == 1 else raw[:: self.slice_record.step]
 
     @property
     def array_value(self) -> numpy.ndarray:
         """returns the sequence as a numpy array"""
         raw = self.parent.get_seq_array(
-            seqid=self.seqid, start=self.parent_start, stop=self.parent_stop
+            seqid=self.seqid,
+            start=self.slice_record.parent_start,
+            stop=self.slice_record.parent_stop,
         )
-        return raw if self.step == 1 else raw[:: self.step]
+        return raw if self.slice_record.step == 1 else raw[:: self.slice_record.step]
 
     @property
     def bytes_value(self) -> bytes:
         """returns the sequence as bytes"""
         raw = self.parent.get_seq_bytes(
-            seqid=self.seqid, start=self.parent_start, stop=self.parent_stop
+            seqid=self.seqid,
+            start=self.slice_record.parent_start,
+            stop=self.slice_record.parent_stop,
         )
-        return raw if self.step == 1 else raw[:: self.step]
+        return raw if self.slice_record.step == 1 else raw[:: self.slice_record.step]
 
     def __str__(self) -> str:
         return self.str_value
@@ -191,13 +188,20 @@ class SeqDataView(new_sequence.SeqViewABC, new_sequence.SliceRecordABC):
     def __bytes__(self) -> bytes:
         return self.bytes_value
 
+    def __getitem__(self, segment: typing.Union[int, slice]) -> new_sequence.SeqViewABC:
+        return self.__class__(
+            parent=self.parent,
+            seqid=self.seqid,
+            alphabet=self.alphabet,
+            parent_len=self.parent_len,
+            slice_record=self.slice_record[segment],
+        )
+
     def __repr__(self) -> str:
-        # todo: add alphabet to the repr
         seq = f"{self[:10]!s}...{self[-5:]}" if len(self) > 15 else str(self)
         return (
-            f"{self.__class__.__name__}(parent={seq}, start={self.start}, "
-            f"stop={self.stop}, step={self.step}, offset={self.offset}, "
-            f"seqid={self.seqid!r}, parent_len={self.parent_len})"
+            f"{self.__class__.__name__}(seqid={self.seqid!r}, parent={seq}, "
+            f"slice_record={self.slice_record!r})"
         )
 
     # refactor: design, do we support copy? do we support copy with sliced?
@@ -205,6 +209,9 @@ class SeqDataView(new_sequence.SeqViewABC, new_sequence.SliceRecordABC):
         """returns copy"""
         return self
 
+    def _get_init_kwargs(self):
+        return {"parent": self.parent, "seqid": self.seqid, "alphabet": self.alphabet}
+
     def to_rich_dict(self) -> dict[str, str | dict[str, str]]:
         """returns a json serialisable dict.
 
@@ -220,18 +227,21 @@ class SeqDataView(new_sequence.SeqViewABC, new_sequence.SliceRecordABC):
 
         data = {"type": get_object_provenance(self), "version": __version__}
         data["init_args"] = self._get_init_kwargs()
-        data["init_args"]["step"] = self.step
 
-        if self.is_reversed:
+        if self.slice_record.is_reversed:
             adj = self.parent_len + 1
-            start, stop = self.stop + adj, self.start + adj
+            start, stop = self.slice_record.stop + adj, self.slice_record.start + adj
         else:
-            start, stop = self.start, self.stop
+            start, stop = self.slice_record.start, self.slice_record.stop
 
         data["init_args"]["parent"] = self.str_value[start:stop]
-        data["init_args"]["offset"] = int(self.parent_start)
-        data["init_args"]["parent_len"] = len(self)
-
+        new_sr = new_sequence.SliceRecord(
+            parent_len=(stop - start),
+            step=self.slice_record.step,
+            offset=self.slice_record.parent_start,
+        )
+        data["init_args"]["slice_record"] = new_sr.to_rich_dict()
+        data["init_args"]["alphabet"] = self.alphabet.to_rich_dict()
         return data
 
 
@@ -266,7 +276,11 @@ class SeqsDataABC(ABC):
 
     @property
     @abstractmethod
-    def reversed(self) -> dict[str, bool]: ...
+    def strand(self) -> dict[str, str]: ...
+
+    @property
+    @abstractmethod
+    def offset(self) -> dict[str, int]: ...
 
     @abstractmethod
     def get_seq_array(
@@ -284,7 +298,7 @@ class SeqsDataABC(ABC):
     ) -> bytes: ...
 
     @abstractmethod
-    def get_view(self, seqid: str) -> new_sequence.SliceRecordABC: ...
+    def get_view(self, seqid: str) -> new_sequence.SeqViewABC: ...
 
     @abstractmethod
     def subset(self, names: Union[str, typing.Sequence[str]]) -> SeqsDataABC: ...
@@ -299,7 +313,7 @@ class SeqsDataABC(ABC):
     def to_rich_dict(self) -> dict: ...
 
     @abstractmethod
-    def __len__(self): ...
+    def __len__(self) -> int: ...
 
     @abstractmethod
     def __getitem__(
@@ -322,14 +336,11 @@ class SeqsData(SeqsDataABC):
         optional sequence constructor, takes 'seq' and 'name' as keyword arguments
         and returns a Sequence instance. If not set, __getitem__ will return a
         SeqDataView
-    reversed_seqs
-        a dictionary of {name: bool} pairs indicating if the sequence is reversed
+    strand
+        a dictionary of {name: strand} pairs indicating the sequence orientation
     """
 
-    __slots__ = ("_data", "_alphabet", "_make_seq", "_reversed_seqs")
-    # todo: kath
-    # refactor: design
-    # SeqsData needs new fields that record the offsets
+    __slots__ = ("_data", "_alphabet", "_make_seq", "_strand", "_offset", "_reversed")
 
     def __init__(
         self,
@@ -337,7 +348,10 @@ class SeqsData(SeqsDataABC):
         data: dict[str, StrORBytesORArray],
         alphabet: new_alphabet.AlphabetABC,
         make_seq: Optional[MakeSeqCallable] = None,
-        reversed_seqs: dict[str, bool] = None,
+        strand: dict[str, int] = None,
+        offset: dict[str, int] = None,
+        reversed: bool = False,
+        check: bool = True,
     ):
         self._alphabet = alphabet
         self._make_seq = make_seq
@@ -346,7 +360,21 @@ class SeqsData(SeqsDataABC):
             arr = self._alphabet.to_indices(seq)
             arr.flags.writeable = False
             self._data[str(name)] = arr
-        self._reversed_seqs = reversed_seqs or {}
+        self._strand = strand or {}
+        self._offset = offset or {}
+        if check:
+            assert set(self._offset.keys()) <= set(
+                self._data.keys()
+            ), "sequence name provided in offset not found in data"
+            assert set(self._strand.keys()) <= set(
+                self._data.keys()
+            ), "sequence name provided in strand not found in data"
+            assert set(self._strand.values()) <= {
+                1,
+                -1,
+            }, "strand must be one of '1' or '-1'"
+        self._reversed = reversed
+        # refactor: rename reversed argument to not clash with python built-in
 
     @property
     def names(self) -> list:
@@ -373,8 +401,50 @@ class SeqsData(SeqsDataABC):
         return self._alphabet
 
     @property
-    def reversed(self) -> dict[str, bool]:
-        return self._reversed_seqs
+    def strand(self) -> dict[str, int]:
+        """returns the strand orientation of the sequences. +1 indicates the
+        sequence is on the plus strand, -1 indicates the sequence is on the
+        minus strand.
+        """
+        # todo: kath
+        # handling of strand needs to be worked out
+
+        # we don't provide a setter for strand, as the strand information can only
+        # be provided at initialisation, we check to see if the SeqsData has been
+        # reversed and return the opposite strand orientation if it has
+        # todo: kath,
+        # the strand needs to be passed to seq (so .parent_coordinates knows about it)
+        return (
+            {name: -self._strand.get(name, 1) for name in self.names}
+            if self.is_reversed
+            else {name: self._strand.get(name, 1) for name in self.names}
+        )
+
+    @property
+    def offset(self) -> dict[str, int]:
+        return {name: self._offset.get(name, 0) for name in self.names}
+
+    @property
+    def is_reversed(self) -> bool:
+        return self._reversed
+
+    def reverse(self) -> SeqsData:
+        """Reverse the orientation of all sequences in the collection."""
+        # refactor: design, this can't be put on the SeqsDataABC unless we add it to the
+        # AlignedSeqsData class as well -- however, ASD does not have the responsibility
+        # of managing the orientation of the sequences, the lies with the
+        # Alignment.slice_record
+
+        # we dont touch the data, just toggle the reversed attribute
+        return self.__class__(
+            data=self._data,
+            alphabet=self.alphabet,
+            make_seq=self.make_seq,
+            strand=self._strand,
+            offset=self._offset,
+            reversed=not self._reversed,
+            check=False,
+        )
 
     def seq_lengths(self) -> dict[str, int]:
         """Returns lengths of sequences as dict of {name: length, ... }."""
@@ -399,53 +469,80 @@ class SeqsData(SeqsDataABC):
 
     def get_view(self, seqid: str) -> SeqDataView:
         seq_len = len(self._data[seqid])
-        step = -1 if self._reversed_seqs.get(seqid, False) else 1
-        return SeqDataView(parent=self, seqid=seqid, parent_len=seq_len, step=step)
+        offset = self._offset.get(seqid, 0)
+        slice_record = new_sequence.SliceRecord(
+            step=-1 if self.is_reversed else 1, parent_len=seq_len, offset=offset
+        )
+        return SeqDataView(
+            parent=self,
+            seqid=seqid,
+            parent_len=seq_len,
+            alphabet=self.alphabet,
+            slice_record=slice_record,
+        )
 
     def subset(self, names: Union[str, typing.Sequence[str]]) -> SeqsData:
         """Returns a new SeqsData object with only the specified names."""
         names = [names] if isinstance(names, str) else names
         if data := {name: self._data.get(name) for name in names if name in self.names}:
             return self.__class__(
-                data=data, alphabet=self.alphabet, make_seq=self.make_seq
+                data=data,
+                alphabet=self.alphabet,
+                make_seq=self.make_seq,
+                strand={
+                    name: strand
+                    for name, strand in self._strand.items()
+                    if name in names
+                },
+                offset={
+                    name: offset
+                    for name, offset in self._offset.items()
+                    if name in names
+                },
+                reversed=self._reversed,
+                check=False,
             )
         else:
             raise ValueError(f"provided {names=} not found in collection")
 
-    def reverse_seqs(self, seqids: Union[str, typing.Sequence[str]] = None) -> SeqsData:
-        """Returns a new SeqsData object with the specified sequences reversed.
-        If no seqids are provided, all sequences are reversed."""
-
-        if seqids is None:
-            seqids = self.names
-        elif isinstance(seqids, str):
-            seqids = [seqids]
-
-        reversed_seqs = {
-            seqid: not self._reversed_seqs.get(seqid, False) for seqid in seqids
-        }
-
-        return self.__class__(
-            data=self._data,
-            alphabet=self.alphabet,
-            make_seq=self.make_seq,
-            reversed_seqs=reversed_seqs,
-        )
-
     def rename_seqs(self, renamer: Callable[[str], str]) -> SeqsData:
-        new_data = {renamer(name): seq for name, seq in self._data.items()}
+        # todo: kath, create a map between original seqid and current seqname for feature querying?
+        renamed_data = {}
+        renamed_strand = {}
+        renamed_offset = {}
+
+        for name in self._data:
+            new_name = renamer(name)
+            renamed_data[new_name] = self._data[name]
+            if name in self._strand:
+                renamed_strand[new_name] = self._strand[name]
+            if name in self._offset:
+                renamed_offset[new_name] = self._offset[name]
 
         return self.__class__(
-            data=new_data,
+            data=renamed_data,
             alphabet=self.alphabet,
             make_seq=self.make_seq,
-            reversed_seqs=self.reversed,
+            strand=renamed_strand,
+            offset=renamed_offset,
+            reversed=self._reversed,
+            check=False,
         )
 
     def add_seqs(
-        self, seqs: dict[str, StrORBytesORArray], force_unique_keys=True
+        self,
+        seqs: dict[str, StrORBytesORArray],
+        force_unique_keys=True,
+        strand=None,
+        offset=None,
     ) -> SeqsData:
-        """Returns a new SeqsData object with added sequences."""
+        """Returns a new SeqsData object with added sequences. If force_unique_keys
+        is True, raises ValueError if any names already exist in the collection."""
+        # refactor: design
+        # if the collection has been reversed, we assume the same for
+        # the new data. If this is not a desirable behaviour, we could realise
+        # the reversal of the current collection, add the new data, and set
+        # reversed to False
         if force_unique_keys and any(name in self.names for name in seqs):
             raise ValueError("One or more sequence names already exist in collection")
         new_data = {
@@ -453,7 +550,12 @@ class SeqsData(SeqsDataABC):
             **{name: self.alphabet.to_indices(seq) for name, seq in seqs.items()},
         }
         return self.__class__(
-            data=new_data, alphabet=self.alphabet, make_seq=self.make_seq
+            data=new_data,
+            alphabet=self.alphabet,
+            make_seq=self.make_seq,
+            strand={**self._strand, **(strand or {})},
+            offset={**self._offset, **(offset or {})},
+            reversed=self._reversed,
         )
 
     def to_alphabet(
@@ -473,10 +575,6 @@ class SeqsData(SeqsDataABC):
         )
 
         for seqid in self.names:
-            # refactor: design
-            # this conversion needs to be a method on the alphabet
-            # so it can be used by Sequence.to_moltype
-
             seq_data = self.get_seq_array(seqid=seqid)
             as_new_alpha = convert_bytes_to_new(convert_old_to_bytes(seq_data))
 
@@ -487,7 +585,14 @@ class SeqsData(SeqsDataABC):
                 )
             new_data[seqid] = as_new_alpha
 
-        return self.__class__(data=new_data, alphabet=alphabet)
+        return self.__class__(
+            data=new_data,
+            alphabet=alphabet,
+            strand=self._strand,
+            offset=self._offset,
+            reversed=self._reversed,
+            check=False,
+        )
 
     def __len__(self):
         return len(self.names)
@@ -513,13 +618,13 @@ class SeqsData(SeqsDataABC):
 
     def to_rich_dict(self) -> dict[str, str | dict[str, str]]:
         """returns a json serialisable dict"""
-        # todo: kath
-        # this should also include offset when implemented
         return {
             "init_args": {
                 "data": {name: self.get_seq_str(seqid=name) for name in self.names},
                 "alphabet": self.alphabet.to_rich_dict(),
-                "reversed_seqs": self.reversed,
+                "strand": self._strand,
+                "offset": self._offset,
+                "reversed": self.is_reversed,
             },
             "type": get_object_provenance(self),
             "version": __version__,
@@ -532,7 +637,9 @@ class SeqsData(SeqsDataABC):
         return cls(
             data=data["init_args"]["data"],
             alphabet=alphabet,
-            reversed_seqs=data["init_args"]["reversed_seqs"],
+            strand=data["init_args"]["strand"],
+            offset=data["init_args"]["offset"],
+            reversed=data["init_args"]["reversed"],
         )
 
 
@@ -560,10 +667,7 @@ class SequenceCollection:
             info = InfoClass(info) if info else InfoClass()
         self.info = info
         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
@@ -571,7 +675,10 @@ class SequenceCollection:
         return self._names
 
     @names.setter
-    def names(self, names: list[str]):  # refactor: design
+    def names(self, names: list[str]):
+        # refactor: design, do we accept the names as an arg, or should we rely on seqs_data keys?
+        # if we are allowing the set of names to be a subset of the seqs_data keys, we should
+        # ensure that we are using the names and not the keys in the rest of the code!
         if names is None:
             self._names = self._seqs_data.names
         elif set(names) <= set(self._seqs_data.names):
@@ -664,12 +771,14 @@ class SequenceCollection:
         if not names:
             raise ValueError(f"{names=} and {negate=} resulted in no names")
 
-        seqs_data = self.seqs.subset(names)
+        seqs_data = self._seqs_data.subset(names)
 
         result = self.__class__(
             seqs_data=seqs_data,
             moltype=self.moltype,
+            names=names,
             info=self.info,
+            source=self.source,
             **kwargs,
         )
         if self.annotation_db:
@@ -686,11 +795,11 @@ class SequenceCollection:
         self, f: Callable[[new_sequence.Sequence], bool], negate: bool = False
     ):
         """Returns list of names of seqs where f(seq) is True."""
-        get = self.seqs.__getitem__
+        get = self.seqs
 
         new_f = negate_condition(f) if negate else f
 
-        return [name for name in self.names if new_f(get(name))]
+        return [name for name in self.names if new_f(get[name])]
 
     def take_seqs_if(
         self, f: Callable[[new_sequence.Sequence], bool], negate: bool = False, **kwargs
@@ -749,7 +858,7 @@ class SequenceCollection:
             sequences to add
         """
         data = coerce_to_seqs_data_dict(seqs, label_to_name=None)
-        seqs_data = self.seqs.add_seqs(data, **kwargs)
+        seqs_data = self._seqs_data.add_seqs(data, **kwargs)
         return self.__class__(
             seqs_data=seqs_data,
             moltype=self.moltype,
@@ -760,7 +869,7 @@ class SequenceCollection:
     def rename_seqs(self, renamer: Callable[[str], str]):
         """Returns new collection with renamed sequences."""
         result = self.__class__(
-            seqs_data=self.seqs.rename_seqs(renamer),
+            seqs_data=self._seqs_data.rename_seqs(renamer),
             moltype=self.moltype,
             info=self.info,
             source=self.source,
@@ -783,9 +892,8 @@ class SequenceCollection:
         as_array
             if True, sequences are returned as numpy arrays, otherwise as strings
         """
-        get = self.seqs.__getitem__
         return {
-            name: (numpy.array(get(name)) if as_array else str(get(name)))
+            name: (numpy.array(self.seqs[name]) if as_array else str(self.seqs[name]))
             for name in self.names
         }
 
@@ -818,7 +926,7 @@ class SequenceCollection:
             "type": get_object_provenance(self),
             "version": __version__,
         }
-        data["seqs_data"] = self.seqs.to_rich_dict()
+        data["seqs_data"] = self._seqs_data.to_rich_dict()
 
         return data
 
@@ -845,15 +953,19 @@ class SequenceCollection:
         -----
         The returned collection will not retain an annotation_db if present.
         """
+        # todo: kath, constructing the SeqsData object here violates loose coupling
+        # can we put this in the SeqsData class?
         seqs = {
-            name: self.moltype.degap(self.seqs.get_seq_array(seqid=name))
+            name: self.moltype.degap(self._seqs_data.get_seq_array(seqid=name))
             for name in self.names
         }
-        seqs_data = self.seqs.__class__(
+        seqs_data = self._seqs_data.__class__(
             data=coerce_to_seqs_data_dict(seqs),
-            alphabet=self.seqs.alphabet,
-            make_seq=self.seqs.make_seq,
-            reversed_seqs=self.seqs.reversed,
+            alphabet=self._seqs_data.alphabet,
+            make_seq=self._seqs_data.make_seq,
+            strand=self._seqs_data.strand,
+            offset=self._seqs_data.offset,
+            reversed=self._seqs_data.is_reversed,
         )
         return self.__class__(
             seqs_data=seqs_data,
@@ -881,7 +993,7 @@ class SequenceCollection:
 
         alpha = mtype.most_degen_alphabet()
         try:
-            new_seqs_data = self.seqs.to_alphabet(alpha)
+            new_seqs_data = self._seqs_data.to_alphabet(alpha)
         except ValueError as e:
             raise ValueError(
                 f"Failed to convert moltype from {self.moltype.label} to {moltype}"
@@ -891,6 +1003,7 @@ class SequenceCollection:
             seqs_data=new_seqs_data,
             moltype=mtype,
             info=self.info,
+            source=self.source,
             annotation_db=self.annotation_db,
         )
 
@@ -954,10 +1067,13 @@ class SequenceCollection:
         seqs_data = coerce_to_seqs_data_dict(translated, label_to_name=None)
         pep_moltype = pep.moltype
 
-        seqs_data = self.seqs.__class__(
+        seqs_data = self._seqs_data.__class__(
             data=seqs_data,
             alphabet=pep_moltype.most_degen_alphabet(),
             make_seq=pep_moltype.make_seq,
+            strand=self._seqs_data.strand,
+            offset=self._seqs_data.offset,
+            reversed=self._seqs_data.is_reversed,
         )
         return self.__class__(
             seqs_data=seqs_data,
@@ -975,10 +1091,9 @@ class SequenceCollection:
         -----
         Reverse complementing the collection will break the relationship to an
         annotation_db if present.
-
         """
         return self.__class__(
-            seqs_data=self.seqs.reverse_seqs(),
+            seqs_data=self._seqs_data.reverse(),
             names=self.names,
             info=self.info,
             moltype=self.moltype,
@@ -1185,6 +1300,7 @@ class SequenceCollection:
         strictly starting after start will be returned.
 
         """
+
         if not self.annotation_db:
             return None
 
@@ -1449,11 +1565,19 @@ class SequenceCollection:
             return self
 
         new_seqs = {s.name: s.trim_stop_codon(gc=gc, strict=strict) for s in self.seqs}
-        seqs_data = self.seqs.__class__(
+        # todo: kath
+        # should we be using the seqs_data object to do this?
+        # creating the new SeqsData object here violates the loose coupling
+        # principle which we have been trying to adhere to.
+        # or we warn of the potential change in the seqs_data object
+
+        seqs_data = self._seqs_data.__class__(
             data=coerce_to_seqs_data_dict(new_seqs),
-            alphabet=self.seqs.alphabet,
-            make_seq=self.seqs.make_seq,
-            reversed_seqs=self.seqs.reversed,
+            alphabet=self._seqs_data.alphabet,
+            make_seq=self._seqs_data.make_seq,
+            strand=self._seqs_data.strand,
+            offset=self._seqs_data.offset,
+            reversed=self._seqs_data.is_reversed,
         )
         result = self.__class__(
             seqs_data=seqs_data,
@@ -1723,7 +1847,7 @@ class SequenceCollection:
             length if pad_length is None or less than max length.
         """
 
-        max_len = max(self.seqs.seq_lengths().values())
+        max_len = max(self._seqs_data.seq_lengths().values())
 
         if pad_length is None:
             pad_length = max_len
@@ -1734,15 +1858,17 @@ class SequenceCollection:
 
         new_seqs = {}
         for seq_name in self.names:
-            seq = self.seqs.get_seq_str(seqid=seq_name)
+            seq = self._seqs_data.get_seq_str(seqid=seq_name)
             padded_seq = seq + "-" * (pad_length - len(seq))
             new_seqs[seq_name] = padded_seq
 
-        seqs_data = self.seqs.__class__(
+        seqs_data = self._seqs_data.__class__(
             data=new_seqs,
-            alphabet=self.seqs.alphabet,
-            make_seq=self.seqs.make_seq,
-            reversed_seqs=self.seqs.reversed,
+            alphabet=self._seqs_data.alphabet,
+            make_seq=self._seqs_data.make_seq,
+            strand=self._seqs_data.strand,
+            offset=self._seqs_data.offset,
+            reversed=self._seqs_data.is_reversed,
         )
         return self.__class__(
             seqs_data=seqs_data,
@@ -1757,7 +1883,7 @@ class SequenceCollection:
         return {s.name: s.strand_symmetry(motif_length=motif_length) for s in self.seqs}
 
     def is_ragged(self) -> bool:
-        return len(set(self.seqs.seq_lengths().values())) > 1
+        return len(set(self._seqs_data.seq_lengths().values())) > 1
 
     def has_terminal_stop(self, gc: Any = None, strict: bool = False) -> bool:
         """Returns True if any sequence has a terminal stop codon.
@@ -2020,7 +2146,7 @@ class SequenceCollection:
             colors=colors, font_size=font_size, font_family=font_family
         )
 
-        seq_lengths = numpy.array(list(self.seqs.seq_lengths().values()))
+        seq_lengths = numpy.array(list(self._seqs_data.seq_lengths().values()))
         min_val = seq_lengths.min()
         max_val = seq_lengths.max()
         med_val = numpy.median(seq_lengths)
@@ -2305,6 +2431,7 @@ def make_unaligned_seqs(
     info: dict = None,
     source: OptPathType = None,
     annotation_db: SupportsFeatures = None,
+    strand: dict[str, int] = None,
 ) -> SequenceCollection:
     """Initialise an unaligned collection of sequences.
 
@@ -2349,7 +2476,7 @@ def make_unaligned_seqs(
     moltype = new_moltype.get_moltype(moltype)
     alphabet = moltype.most_degen_alphabet()
 
-    seqs_data = SeqsData(data=seqs_data, alphabet=alphabet)
+    seqs_data = SeqsData(data=seqs_data, alphabet=alphabet, strand=strand)
     return make_unaligned_seqs(
         seqs_data,
         moltype=moltype,
@@ -2369,6 +2496,7 @@ def _(
     info: dict = None,
     source: OptPathType = None,
     annotation_db: SupportsFeatures = None,
+    strand: dict[str, int] = None,
 ) -> SequenceCollection:
     moltype = new_moltype.get_moltype(moltype)
     if not moltype.is_compatible_alphabet(data.alphabet):
@@ -2457,47 +2585,6 @@ def _(
     return seq_to_gap_coords(seq.decode("utf-8"), alphabet=alphabet)
 
 
-class SliceRecord(new_sequence.SliceRecordABC):
-    __slots__ = "_parent_len"
-
-    def __init__(
-        self,
-        parent_len: int,
-        start: OptInt = None,
-        stop: OptInt = None,
-        step: OptInt = None,
-        offset: int = None,
-    ):
-        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:
-        return self._parent_len
-
-    def _get_init_kwargs(self) -> dict:
-        return {}
-
-    def copy(self, sliced: bool = False):
-        # 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):
-        return self.__class__(start=0, stop=0, step=1, parent_len=self.parent_len)
-
-
 class Aligned:
     """A single sequence in an alignment."""
 
@@ -2519,23 +2606,47 @@ 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)
+        # if the slice record has abs(step) > 1, we cannot retain a connection to the underlying aligned
+        # seq data container because the gaps are not going to be modulo the step.
+        rev = False
+        if abs(self.data.slice_record.step) == 1:
+            seq = self.data.get_seq_view()
+        elif self.data.slice_record.step < -1:
+            # refactor: design
+            # gapped_str_value will apply the step to the underlying data, so
+            # we need to re-reverse the underlying data AND reverse the Sequence
+            # so that the seq knows to complement the data on output
+            # we should revisit this design
+            seq = self.moltype.degap(self.data.gapped_str_value)[::-1]
+            rev = True
+        else:
+            seq = self.moltype.degap(self.data.gapped_str_value)
+        return (
+            self.moltype.make_seq(seq=seq, name=self.data.seqid)[::-1]
+            if rev
+            else self.moltype.make_seq(seq=seq, 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
-        )
+        seq = self.data.gapped_str_value
+        if self.data.slice_record.step < 0:
+            seq = self.moltype.complement(seq)
+        return self.moltype.make_seq(seq=seq, name=self.data.seqid)
 
     @property
     def moltype(self) -> new_moltype.MolType:
         return self._moltype
 
+    @property
+    def name(self) -> str:
+        return self.data.seqid
+
+    def gap_vector(self) -> list[bool]:
+        """Returns gap_vector of GappedSeq, for omit_gap_pos."""
+        return self.gapped_seq.gap_vector()
+
     def __str__(self) -> str:
         return str(self.gapped_seq)
 
@@ -2559,11 +2670,42 @@ class Aligned:
 
     @__getitem__.register
     def _(self, span: slice):
-        self.data.slice_record = self.data.slice_record[span]
-        return self.__class__(data=self.data, moltype=self.moltype)
+        return self.__class__(data=self.data[span], moltype=self.moltype)
+
+    def parent_coordinates(self, seq_coords=False):
+        """returns seqid, start, stop, strand on the parent sequence
+
+        Parameters
+        ----------
+        seq_coords
+            if True, the coordinates for the unaligned sequence
+        """
+        strand = -1 if self.data.is_reversed else 1
+        seqid = self.data.seqid
+        if not seq_coords:
+            start = self.data.slice_record.parent_start
+            stop = self.data.slice_record.parent_stop
+        else:
+            # AlignedDataView.parent_seq_coords uses it's indelmap, etc..
+            # to return the necessary coordinates
+            seqid, start, stop, strand = self.data.parent_seq_coords()
+
+        return (
+            seqid,
+            start,
+            stop,
+            strand,
+        )
+
+    def __repr__(self) -> str:
+        # refactor: design
+        # todo: when design is finalised, add tests for this
+        return f"Aligned({self.data!r})"
 
 
 class AlignedSeqsDataABC(SeqsDataABC):
+    # all methods that are from SeqsDataABC should work in sequence coordinates
+    # all methods unique to AlignedSeqsDataABC should work in aligned coordinates
     __slots__ = ()
 
     @classmethod
@@ -2631,7 +2773,7 @@ def _gapped_seq_len(seq: numpy.ndarray, gap_map: numpy.ndarray) -> int:
 
 class AlignedSeqsData(AlignedSeqsDataABC):
     # refactor: docstring
-    # needs to record parentid, strand, offset
+    # refactor: design
 
     __slots__ = (
         "_seqs",
@@ -2639,7 +2781,8 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         "_alphabet",
         "_make_seq",
         "_align_len",
-        "_make_aligned",
+        "_strand",
+        "_offset",
     )
 
     def __init__(
@@ -2648,17 +2791,32 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         seqs: Optional[dict[str, StrORBytesORArray]],
         gaps: Optional[dict[str, numpy.ndarray]],
         alphabet: new_alphabet.AlphabetABC,
-        make_seq: Optional[MakeSeqCallable],
+        make_seq: Optional[MakeSeqCallable] = None,
+        strand: dict[str, int] = None,
+        offset: dict[str, int] = None,
         align_len: OptInt = None,
         check: bool = True,
     ):
         self._alphabet = alphabet
         self._make_seq = make_seq
-        if check:
+        if check or not align_len:
             if not seqs or not gaps:
                 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.")
+            if strand:
+                assert set(strand.keys()).issubset(seqs.keys())
+                assert set(strand.values()) <= {
+                    1,
+                    -1,
+                }, "strand must be one of '1' or '-1'"
+            if offset:
+                assert set(offset.keys()).issubset(seqs.keys())
+            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._seqs = {}
         for k, v in seqs.items():
             seq = self._alphabet.to_indices(v)
@@ -2668,12 +2826,9 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         for k, v in gaps.items():
             self._gaps[k] = v
             v.flags.writeable = False
-        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()
+        self._strand = strand or {}
+        self._offset = offset or {}
 
     @classmethod
     def from_aligned_seqs(
@@ -2682,6 +2837,7 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         data: dict[str, StrORArray],
         alphabet: new_alphabet.AlphabetABC,
         make_seq: Optional[MakeSeqCallable] = None,
+        **kwargs,
     ):
         """Construct an AlignedSeqsData object from a dict of aligned sequences
 
@@ -2690,9 +2846,11 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         data
             dict of gapped sequences {name: seq, ...}. sequences must all be
             the same length
-        moltype
-            the molecular type of the sequences e.g., 'dna', 'protein' or a
-            MolType instance
+        alphabet
+            alphabet object for the sequences
+        make_seq
+            callable that takes 'seq' and 'name' as keyword arguments and
+            constructs a Sequence object.
         """
         seq_lengths = {len(v) for v in data.values()}
         if len(seq_lengths) != 1:
@@ -2715,6 +2873,7 @@ class AlignedSeqsData(AlignedSeqsDataABC):
             make_seq=make_seq,
             align_len=align_len,
             check=False,
+            **kwargs,
         )
 
     @property
@@ -2742,58 +2901,85 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         self._make_seq = make_seq
 
     @property
-    def make_aligned(self) -> MakeAlignedCallable:
-        return self._make_aligned
+    def align_len(self) -> int:
+        return self._align_len
 
-    @make_aligned.setter
-    def make_aligned(self, make_aligned: MakeAlignedCallable) -> None:
-        self._make_aligned = make_aligned
+    @property
+    def strand(self) -> dict[str, str]:
+        """returns the strand orientation of each sequence in the Alignment"""
+        # todo: kath
+        # can we move to SeqsDataABC and inherit here and in SeqsData?
+        return {name: -self._strand.get(name, 1) for name in self.names}
 
     @property
-    def align_len(self) -> int:
-        return self._align_len
+    def offset(self) -> dict[str, int]:
+        """returns the offset of each sequence in the Alignment"""
+        return {name: self._offset.get(name, 0) for name in self.names}
 
-    def __len__(self):
+    def __len__(self) -> int:
         return self.align_len
 
     @singledispatchmethod
-    def __getitem__(self, index: Union[str, int]) -> Aligned:
+    def __getitem__(self, index: Union[str, int]):
         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)
+    def _(self, index: str):
+        return self.get_view(index)
 
     @__getitem__.register
-    def _(self, index: int) -> Aligned:
+    def _(self, index: int):
         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)
+    @singledispatchmethod
+    def get_view(self, seqid: str):
+        return AlignedDataView(
+            parent=self,
+            seqid=seqid,
+            alphabet=self.alphabet,
+        )
+
+    @get_view.register
+    def _(self, seqid: int):
+        return self.get_view(self.names[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:
-        """helper function to return sequence data corresponding to seqid as an
-        array of indices. start/stop are in alignment coordinates.
+        """Return ungapped sequence corresponding to seqid as an array of indices.
+        assumes start/stop are in sequence coordinates. Excludes gaps.
+        """
+        # refactor: design
+        seq = self._seqs[seqid]
+        return seq[start:stop:step]
 
-        Interweaves gaps into the sequence if gapped is True. Returns the ungapped
-        sequence if gapped is False.
+    def get_gapped_seq_array(
+        self,
+        *,
+        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.
         """
+
+        # refactor: design
+        # should this pass the data sliced data to the ADV which then applies the step???
+
         step = 1 if step is None else step
         func = _input_vals_pos_step if step > 0 else _input_vals_neg_step
         start, stop, step = func(self.align_len, start, stop, step)
@@ -2807,12 +2993,9 @@ class AlignedSeqsData(AlignedSeqsDataABC):
 
         # 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)
+            return numpy.full(
+                int(-((stop - start) // -step)), unknown, dtype=numpy.uint8
+            )
 
         gaps = self._gaps[seqid]
         indel_map = IndelMap(
@@ -2843,49 +3026,13 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         # 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
+                seg = numpy.full(span.length, unknown, dtype=numpy.uint8)
             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
-        )
-
-    def get_gapped_seq_array(
-        self,
-        *,
-        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.
-        """
-        return self._get_seq_array(
-            seqid=seqid, gapped=True, start=start, stop=stop, step=step
-        )
-
     def get_seq_str(
         self,
         *,
@@ -2897,7 +3044,6 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         """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, step=step)
         )
@@ -2947,14 +3093,25 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         ).encode("utf8")
 
     def add_seqs(
-        self, seqs: dict[str, StrORArray], force_unique_keys=True
+        self,
+        seqs: dict[str, StrORArray],
+        force_unique_keys=True,
+        strand: dict[str, int] = None,
+        offset: dict[str, int] = None,
     ) -> AlignedSeqsData:
-        """Returns a new AlignedSeqsData object with added sequences."""
+        """Returns a new AlignedSeqsData object with added sequences.
+
+        Parameters
+        ----------
+        seqs
+            dict of sequences to add {name: seq, ...}
+        force_unique_keys
+            if True, raises ValueError if any sequence names already exist in the collection
+        strand
+            the strand orientations {name: strand, ...} where strand is one of '1', '-1'.
+        """
         if force_unique_keys and any(name in self.names for name in seqs):
             raise ValueError("One or more sequence names already exist in collection")
-        # refactor: design
-        # instead of re-computing the gaps, can we construct the new gaps and append to
-        # the existing gaps?
 
         new_seq_lens = {len(seq) for seq in seqs.values()}
         if len(new_seq_lens) != 1 or new_seq_lens.pop() != self.align_len:
@@ -2962,20 +3119,52 @@ class AlignedSeqsData(AlignedSeqsDataABC):
                 "All sequences must be the same length as existing sequences"
             )
 
-        old_seqs = {name: self.get_gapped_seq_array(seqid=name) for name in self.names}
-        new_data = old_seqs | seqs
+        new_seqs = {}
+        new_gaps = {}
+        for name, seq in seqs.items():
+            seq, gap_map = seq_to_gap_coords(seq, alphabet=self.alphabet)
+            seq = self.alphabet.to_indices(seq)
+            seq.flags.writeable = False
+            gap_map.flags.writeable = False
+            new_seqs[name], new_gaps[name] = seq, gap_map
 
-        return self.__class__.from_aligned_seqs(
-            data=new_data, alphabet=self.alphabet, make_seq=self._make_seq
+        return self.__class__(
+            seqs={**self._seqs, **new_seqs},
+            gaps={**self._gaps, **new_gaps},
+            alphabet=self.alphabet,
+            make_seq=self._make_seq,
+            strand={**self._strand, **(strand or {})},
+            offset={**self._offset, **(offset or {})},
+            align_len=self.align_len,
         )
 
-    def reversed(self):
-        # todo: kath
-        ...
-
     def subset(self, names: Union[str, typing.Sequence[str]]):
-        # todo: kath
-        ...
+        """Returns a new AlignedSeqsData object with only the specified names."""
+        names = [names] if isinstance(names, str) else names
+        if seq_data := {
+            name: self._seqs.get(name) for name in names if name in self.names
+        }:
+            gap_data = {name: self._gaps.get(name) for name in seq_data}
+            return self.__class__(
+                seqs=seq_data,
+                gaps=gap_data,
+                alphabet=self.alphabet,
+                make_seq=self._make_seq,
+                strand={
+                    name: strand
+                    for name, strand in self._strand.items()
+                    if name in names
+                },
+                offset={
+                    name: offset
+                    for name, offset in self._offset.items()
+                    if name in names
+                },
+                align_len=self.align_len,
+                check=False,
+            )
+        else:
+            raise ValueError(f"provided {names=} not found in collection")
 
     def to_alphabet(self, alphabet: new_alphabet.AlphabetABC):
         # todo: kath
@@ -2986,23 +3175,56 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         ...
 
 
-class AlignedDataView(new_sequence.SeqViewABC):
-    # refactor: docstring
+class AlignedDataViewABC(new_sequence.SeqViewABC):
+    __slots__ = ()
 
-    __slots__ = ("parent", "_seqid", "_offset", "_parent_len", "_slice_record")
+    @abstractmethod
+    def get_seq_view(self) -> new_sequence.SeqViewABC: ...
 
-    def __init__(
-        self,
-        *,
-        parent: AlignedSeqsDataABC,
-        seqid: str,
-        offset: int = 0,
-    ):
-        self.parent = parent
-        self._seqid = seqid
-        self._offset = offset
+    @property
+    @abstractmethod
+    def gapped_str_value(self) -> str: ...
+
+    @property
+    @abstractmethod
+    def gapped_array_value(self) -> numpy.ndarray: ...
+
+    @property
+    @abstractmethod
+    def gapped_bytes_value(self) -> bytes: ...
+
+
+class AlignedDataView(new_sequence.SeqViewABC):
+    # refactor: docstring
+
+    __slots__ = (
+        "parent",
+        "_seqid",
+        "alphabet",
+        "_offset",
+        "_parent_len",
+        "_slice_record",
+    )
+
+    def __init__(
+        self,
+        *,
+        parent: AlignedSeqsDataABC,
+        seqid: str,
+        alphabet: new_alphabet.AlphabetABC,
+        slice_record: new_sequence.SliceRecord = None,
+        offset: int = 0,
+    ):
+        self.parent = parent
+        self._seqid = seqid
+        self.alphabet = alphabet
         self._parent_len = parent.align_len
-        self._slice_record = SliceRecord(parent_len=self.parent_len)
+        self._slice_record = (
+            slice_record
+            if slice_record is not None
+            else new_sequence.SliceRecord(parent_len=self._parent_len)
+        )
+        self._offset = offset
 
     @property
     def slice_record(self):
@@ -3024,22 +3246,19 @@ class AlignedDataView(new_sequence.SeqViewABC):
     def map(self) -> IndelMap:
         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],)
+            gap_pos = numpy.array(gap_pos_gap_length[:, 0], dtype=int)
+            cum_gap_lengths = numpy.array(gap_pos_gap_length[:, 1], dtype=int)
         else:
             gap_pos, cum_gap_lengths = (
                 numpy.array([], dtype=int),
                 numpy.array([], dtype=int),
             )
 
-        im = IndelMap(
+        return IndelMap(
             gap_pos=gap_pos,
             cum_gap_lengths=cum_gap_lengths,
-            parent_length=len(self.parent),
+            parent_length=self.parent.seq_lengths()[self.seqid],
         )
-        return im[
-            self.slice_record.start : self.slice_record.stop : self.slice_record.step
-        ]
 
     @property
     def str_value(self) -> str:
@@ -3096,174 +3315,888 @@ class AlignedDataView(new_sequence.SeqViewABC):
         )
 
     def __str__(self) -> str:
-        # refactor: design
-        # should this return gapped or ungapped?
-        return self.gapped_str_value
+        return self.str_value
 
     def __array__(self, dtype=None, copy=None) -> numpy.ndarray:
-        arr = self.gapped_array_value
+        arr = self.array_value
         if dtype:
             arr = arr.astype(dtype)
         return arr
 
     def __bytes__(self) -> bytes:
-        return self.gapped_bytes_value
+        return self.bytes_value
+
+    def __getitem__(self, segment) -> AlignedDataViewABC:
+        return self.__class__(
+            parent=self.parent,
+            seqid=self.seqid,
+            alphabet=self.alphabet,
+            slice_record=self.slice_record[segment],
+            offset=self._offset,
+        )
+
+    def __repr__(self) -> str:
+        # refactor: design
+        # todo: once design is settled, need to be tested
+        seq_preview = (
+            f"{self.parent.get_seq_array(seqid=self.seqid, start=0, stop=10)}..."
+            f"{self.parent.get_seq_array(seqid=self.seqid, start=self.parent_len-5)}"
+            if self.parent_len > 15
+            else self.parent.get_seq_array(seqid=self.seqid)
+        )
+        seq_preview = self.alphabet.from_indices(seq_preview)
+        return (
+            f"{self.__class__.__name__}(seqid={self.seqid!r}, map={self.map!r}, parent={seq_preview!r}, "
+            f"slice_record={self.slice_record.__repr__()})"
+        )
+
+    def parent_seq_coords(self) -> tuple[str, int, int, int]:
+        """returns seqid, start, stop, strand on the parent sequence"""
+        start = self.map.get_seq_index(self.slice_record.parent_start)
+        stop = self.map.get_seq_index(self.slice_record.parent_stop)
+        strand = -1 if self.slice_record.step < 0 else 1
+
+        return self.seqid, start, stop, strand
 
     def copy(self, sliced: bool = False):
         return self
 
-    def to_rich_dict(self) -> dict: ...
+    def to_rich_dict(self) -> dict:
+        ...
+        # todo: kath...
+
+    def get_seq_view(self) -> new_sequence.SeqViewABC:
+        # we want the parent coordinates in sequence coordinates
+        # parent_seq_coords does not account for the stride
+        seqid, start, stop, _ = self.parent_seq_coords()
+        parent_len = self.parent.seq_lengths()[seqid]
+        sr = new_sequence.SliceRecord(
+            start=start,
+            stop=stop,
+            parent_len=parent_len,
+        )[:: self.slice_record.step]
+
+        return SeqDataView(
+            parent=self.parent,
+            seqid=seqid,
+            alphabet=self.alphabet,
+            parent_len=parent_len,
+            slice_record=sr,
+        )
+
+
+def make_gap_filter(template, gap_fraction, gap_run):
+    """Returns f(seq) -> True if no gap runs and acceptable gap fraction.
+
+    Calculations relative to template.
+    gap_run = number of consecutive gaps allowed in either the template or seq
+    gap_fraction = fraction of positions that either have a gap in the template
+        but not in the seq or in the seq but not in the template
+    NOTE: template and seq must both be ArraySequence objects.
+    """
+    template_gaps = numpy.array(template.gap_vector())
+
+    def result(seq):
+        """Returns True if seq adhers to the gap threshold and gap fraction."""
+        seq_gaps = numpy.array(seq.gap_vector())
+        # check if gap amount bad
+        if sum(seq_gaps != template_gaps) / float(len(seq)) > gap_fraction:
+            return False
+        # check if gap runs bad
+        if (
+            b"\x01" * gap_run
+            in numpy.logical_and(seq_gaps, numpy.logical_not(template_gaps))
+            .astype(numpy.uint8)
+            .tobytes()
+        ):
+            return False
+        # check if insertion runs bad
+        elif (
+            b"\x01" * gap_run
+            in numpy.logical_and(template_gaps, numpy.logical_not(seq_gaps))
+            .astype(numpy.uint8)
+            .tobytes()
+        ):
+            return False
+        return True
+
+    return result
+
+
+class _IndexableSeqs:
+    def __init__(self, parent):
+        self.parent = parent
+
+    def __getitem__(self, key):
+        return self.parent._make_aligned(key)
 
 
 class Alignment(SequenceCollection):
     def __init__(
         self,
-        slice_record: new_sequence.SliceRecordABC = None,
+        slice_record: new_sequence.SliceRecord = 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)
+            else new_sequence.SliceRecord(parent_len=self._seqs_data.align_len)
+        )
+
+    @singledispatchmethod
+    def __getitem__(self, index):
+        raise NotImplementedError(f"__getitem__ not implemented for {type(index)}")
+
+    @__getitem__.register
+    def _(self, index: str):
+        return self._make_aligned(seqid=index)
+
+    @__getitem__.register
+    def _(self, index: int):
+        new_slice = self._slice_record[index]
+        return self.__class__(
+            seqs_data=self._seqs_data,
+            slice_record=new_slice,
+            moltype=self.moltype,
+            info=self.info,
+        )
+
+    @__getitem__.register
+    def _(self, index: slice):
+        new_slice = self._slice_record[index]
+        return self.__class__(
+            seqs_data=self._seqs_data,
+            slice_record=new_slice,
+            moltype=self.moltype,
+            info=self.info,
+        )
+
+    def __repr__(self):
+        seqs = []
+        limit = 10
+        delimiter = ""
+        for count, name in enumerate(self.names):
+            if count == 3:
+                seqs.append("...")
+                break
+            elts = list(str(self.seqs[name])[: limit + 1])
+            if len(elts) > limit:
+                elts[-1] = "..."
+            seqs.append(f"{name}[{delimiter.join(elts)}]")
+        seqs = ", ".join(seqs)
+
+        return f"{len(self.names)} x {len(self)} {self.moltype.label} alignment: {seqs}"
+
+    def __len__(self):
+        return len(self._seqs_data)
+
+    def _make_aligned(self, seqid: str) -> Aligned:
+        # refactor: design
+        # add slice_record as an argument to the get_view() method
+        data = self._seqs_data.get_view(seqid)
+        data.slice_record = self._slice_record
+        return Aligned(data=data, moltype=self.moltype)
+
+    @property
+    def seqs(self) -> AlignedSeqsDataABC:
+        # refactor: design
+        # make _seqs a private attribute and the indexable seqs class will be
+        # assigned to it in the constructor. The property will then just return
+        #  the _seqs attribute.
+        return _IndexableSeqs(self)
+
+    def get_seq(
+        self, seqname: str, copy_annotations: bool = False
+    ) -> new_sequence.Sequence:
+        """Return a Sequence object for the specified seqname.
+
+        Parameters
+        ----------
+        seqname
+            name of the sequence to return
+        copy_annotations
+            if True, only the annotations for the specified sequence are copied
+            to the annotation database of the Sequence object. If False, all
+            annotations are copied.
+        """
+        # todo
+        # more efficient to constuct the sequence object directly?
+        seq = self.seqs[seqname].seq
+        if copy_annotations:
+            seq.annotation_db = type(self.annotation_db)()
+            seq.annotation_db.update(annot_db=self.annotation_db, seqids=seqname)
+        else:
+            seq.annotation_db = self.annotation_db
+
+        return seq
+
+    @c3warn.deprecated_args(
+        version="2025.6", reason="naming consistency", old_new=[("seq_name", "seqname")]
+    )
+    def get_gapped_seq(self, seqname: str) -> new_sequence.Sequence:
+        """Return a gapped Sequence object for the specified seqname.
+
+        Notes
+        -----
+        This method breaks the connection to the annotation database.
+        """
+        return self.seqs[seqname].gapped_seq
+
+    def rc(self):
+        """Returns the reverse complement of all sequences in the alignment.
+        A synonym for reverse_complement.
+
+        Notes
+        -----
+        Reverse complementing the collection will break the relationship to an
+        annotation_db if present.
+        """
+        return self.__class__(
+            seqs_data=self._seqs_data,
+            slice_record=self._slice_record[::-1],
+            names=self.names,
+            info=self.info,
+            moltype=self.moltype,
+            source=self.source,
+        )
+
+    def alignment_quality(self, app_name: str = "ic_score", **kwargs):
+        """
+        Computes the alignment quality using the indicated app
+
+        Parameters
+        ----------
+        app_name
+            name of an alignment score calculating app, e.g. 'ic_score',
+            'cogent3_score', 'sp_score'
+
+        kwargs
+            keyword arguments to be passed to the app. Use
+            ``cogent3.app_help(app_name)`` to see the available options.
+
+        Returns
+        -------
+        float or a NotCompleted instance if the score could not be computed
+        """
+        app = get_app(app_name, **kwargs)
+        return app(self)
+
+    def iter_positions(
+        self, pos_order: list = None
+    ) -> typing.Iterator[list, list, list]:
+        """Iterates over positions in the alignment, in order.
+
+        Parameters
+        ----------
+        pos_order
+            list of indices specifying the column order. If None, the
+            positions are iterated in order.
+
+        Returns
+        -------
+        yields lists of elemenets for each position (column) in the alignment
+        """
+        # refactor: array
+        # this could also iter columns of indices as a numpy array - could be an optional arg
+        pos_order = pos_order or range(self._seqs_data.align_len)
+        for pos in pos_order:
+            yield [str(self[seq][pos]) for seq in self.names]
+
+    positions = property(iter_positions)
+
+    def get_position_indices(
+        self, f: Callable, native: bool = False, negate: bool = False
+    ) -> list[int]:
+        """Returns list of column indices for which f(col) is True.
+
+        f : callable
+          function that returns true/false given an alignment position
+        native : boolean
+          if True, and ArrayAlignment, f is provided with slice of array
+          otherwise the string is used
+        negate : boolean
+          if True, not f() is used
+        """
+        # refactor:
+        # type hint for f
+        # implement native
+        if negate:
+
+            def new_f(x):
+                return not f(x)
+
+        else:
+            new_f = f
+
+        result = [i for i, col in enumerate(self.positions) if new_f(col)]
+
+        return result
+
+    def get_gap_array(self, include_ambiguity: bool = True) -> numpy.ndarray:
+        """returns bool array with gap state True, False otherwise
+
+        Parameters
+        ----------
+        include_ambiguity
+            if True, ambiguity characters that include the gap state are
+            included
+        """
+        result = numpy.full((self.num_seqs, len(self)), False, dtype=bool)
+        for i, seqid in enumerate(self.names):
+            if include_ambiguity:
+                seq_array = self._seqs_data.get_gapped_seq_array(seqid=seqid)
+                result[i][seq_array >= self.moltype.most_degen_alphabet().gap_index] = (
+                    True
+                )
+            else:
+                # refactor: design
+                # this should be on IndelMap
+                gaps = self._seqs_data.get_gaps(seqid)
+                for gap_pos, cum_gap_length in gaps:
+                    result[i][list(range(gap_pos, gap_pos + cum_gap_length))] = True
+        return result
+
+    def iupac_consensus(self, allow_gap: bool = True) -> str:
+        """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 majority_consensus(self) -> new_sequence.Sequence:
+        """Returns consensus sequence containing most frequent item at each
+        position."""
+        states = []
+        data = zip(*map(str, self.seqs))
+        for pos in data:
+            pos = CategoryCounter(pos)
+            states.append(pos.mode)
+
+        return self.moltype.make_seq(seq="".join(states))
+
+    def counts_per_pos(
+        self,
+        motif_length: int = 1,
+        include_ambiguity: bool = False,
+        allow_gap: bool = False,
+        warn: bool = False,
+    ) -> DictArray:
+        """return DictArray of counts per position
+
+        Parameters
+        ----------
+
+        warn
+            warns if motif_length > 1 and alignment trimmed to produce
+            motif columns
+        """
+        length = (len(self) // motif_length) * motif_length
+        if warn and len(self) != length:
+            warnings.warn(f"trimmed {len(self) - length}", UserWarning)
+
+        data = list(self.to_dict().values())
+        alpha = self.moltype.alphabet.get_kmer_alphabet(motif_length)
+        all_motifs = set()
+        exclude_chars = set()
+        if not allow_gap:
+            exclude_chars.update(self.moltype.gap)
+
+        if not include_ambiguity:
+            ambigs = [c for c, v in self.moltype.ambiguities.items() if len(v) > 1]
+            exclude_chars.update(ambigs)
+
+        result = []
+        for i in range(0, len(self) - motif_length + 1, motif_length):
+            counts = CategoryCounter([s[i : i + motif_length] for s in data])
+            all_motifs.update(list(counts))
+            result.append(counts)
+
+        if all_motifs:
+            alpha += tuple(sorted(set(alpha) ^ all_motifs))
+
+        if exclude_chars:
+            # this additional clause is required for the bytes moltype
+            # That moltype includes '-' as a character
+            alpha = [m for m in alpha if not (set(m) & exclude_chars)]
+
+        for i, counts in enumerate(result):
+            result[i] = counts.tolist(alpha)
+
+        result = MotifCountsArray(result, alpha)
+        return result
+
+    def probs_per_pos(
+        self,
+        motif_length: int = 1,
+        include_ambiguity: bool = False,
+        allow_gap: bool = False,
+        warn: bool = False,
+    ) -> MotifFreqsArray:
+        """returns MotifFreqsArray per position"""
+        counts = self.counts_per_pos(
+            motif_length=motif_length,
+            include_ambiguity=include_ambiguity,
+            allow_gap=allow_gap,
+            warn=warn,
+        )
+        return counts.to_freq_array()
+
+    def entropy_per_pos(
+        self,
+        motif_length: int = 1,
+        include_ambiguity: bool = False,
+        allow_gap: bool = False,
+        warn: bool = False,
+    ) -> numpy.ndarray:
+        """returns shannon entropy per position"""
+        probs = self.probs_per_pos(
+            motif_length=motif_length,
+            include_ambiguity=include_ambiguity,
+            allow_gap=allow_gap,
+            warn=warn,
         )
+        return probs.entropy()
+
+    def counts_per_seq(
+        self,
+        motif_length: int = 1,
+        include_ambiguity: bool = False,
+        allow_gap: bool = False,
+        exclude_unobserved: bool = False,
+        warn: bool = False,
+    ) -> MotifCountsArray:
+        """counts of non-overlapping motifs per sequence
+
+        Parameters
+        ----------
+        motif_length
+            number of elements per character.
+        include_ambiguity
+            if True, motifs containing ambiguous characters
+            from the seq moltype are included. No expansion of those is attempted.
+        allow_gap
+            if True, motifs containing a gap character are included.
+        exclude_unobserved
+            if False, all canonical states included
+        warn
+            warns if motif_length > 1 and alignment trimmed to produce
+            motif columns
+        """
+        length = (len(self) // motif_length) * motif_length
+        if warn and len(self) != length:
+            warnings.warn(f"trimmed {len(self) - length}", UserWarning)
+
+        counts = []
+        motifs = set()
+        for name in self.names:
+            seq = self.get_gapped_seq(name)
+            c = seq.counts(
+                motif_length=motif_length,
+                include_ambiguity=include_ambiguity,
+                allow_gap=allow_gap,
+                exclude_unobserved=exclude_unobserved,
+            )
+            motifs.update(c.keys())
+            counts.append(c)
+
+        if not exclude_unobserved:
+            motifs.update(self.moltype.alphabet.get_kmer_alphabet(motif_length))
+
+        motifs = list(sorted(motifs))
+        if not motifs:
+            return None
+
+        for i, c in enumerate(counts):
+            counts[i] = c.tolist(motifs)
+        return MotifCountsArray(counts, motifs, row_indices=self.names)
+
+    def probs_per_seq(
+        self,
+        motif_length: int = 1,
+        include_ambiguity: bool = False,
+        allow_gap: bool = False,
+        exclude_unobserved: bool = False,
+        warn: bool = False,
+    ) -> MotifFreqsArray:
+        """return MotifFreqsArray per sequence
+
+        Parameters
+        ----------
+        motif_length
+            number of characters per tuple.
+        include_ambiguity
+            if True, motifs containing ambiguous characters
+            from the seq moltype are included. No expansion of those is attempted.
+        allow_gap
+            if True, motifs containing a gap character are included.
+        exclude_unobserved
+            if True, unobserved motif combinations are excluded.
+        warn
+            warns if motif_length > 1 and alignment trimmed to produce
+            motif columns
+        """
+
+        counts = self.counts_per_seq(
+            motif_length=motif_length,
+            include_ambiguity=include_ambiguity,
+            allow_gap=allow_gap,
+            exclude_unobserved=exclude_unobserved,
+            warn=warn,
+        )
+        return None if counts is None else counts.to_freq_array()
+
+    def entropy_per_seq(
+        self,
+        motif_length: int = 1,
+        include_ambiguity: bool = False,
+        allow_gap: bool = False,
+        exclude_unobserved: bool = True,
+        warn: bool = False,
+    ) -> numpy.ndarray:
+        """returns the Shannon entropy per sequence
+
+        Parameters
+        ----------
+        motif_length
+            number of characters per tuple.
+        include_ambiguity
+            if True, motifs containing ambiguous characters
+            from the seq moltype are included. No expansion of those is attempted.
+        allow_gap
+            if True, motifs containing a gap character are included.
+        exclude_unobserved
+            if True, unobserved motif combinations are excluded.
+        warn
+            warns if motif_length > 1 and alignment trimmed to produce
+            motif columns
+
+        Notes
+        -----
+        For motif_length > 1, it's advisable to specify exclude_unobserved=True,
+        this avoids unnecessary calculations.
+        """
+
+        probs = self.probs_per_seq(
+            motif_length=motif_length,
+            include_ambiguity=include_ambiguity,
+            allow_gap=allow_gap,
+            exclude_unobserved=exclude_unobserved,
+            warn=warn,
+        )
+        return None if probs is None else probs.entropy()
+
+    def count_gaps_per_pos(self, include_ambiguity: bool = True) -> DictArray:
+        """return counts of gaps per position as a DictArray
+
+        Parameters
+        ----------
+        include_ambiguity
+            if True, ambiguity characters that include the gap state are
+            included
+        """
+        gap_array = self.get_gap_array(include_ambiguity=include_ambiguity)
+        darr = DictArrayTemplate(range(len(self)))
+
+        result = gap_array.sum(axis=0)
+        result = darr.wrap(result)
+        return result
+
+    def count_gaps_per_seq(
+        self,
+        induced_by: bool = False,
+        unique: bool = False,
+        include_ambiguity: bool = True,
+        drawable: bool = False,
+    ):
+        """return counts of gaps per sequence as a DictArray
+
+        Parameters
+        ----------
+        induced_by
+            a gapped column is considered to be induced by a seq if the seq
+            has a non-gap character in that column.
+        unique
+            count is limited to gaps uniquely induced by each sequence
+        include_ambiguity
+            if True, ambiguity characters that include the gap state are
+            included
+        drawable
+            if True, resulting object is capable of plotting data via specified
+            plot type 'bar', 'box' or 'violin'
+        """
+        from cogent3.draw.drawable import Drawable
+
+        gap_array = self.get_gap_array(include_ambiguity=include_ambiguity)
+        darr = DictArrayTemplate(self.names)
+
+        if unique:
+            # we identify cols with a single non-gap character
+            gap_cols = gap_array.sum(axis=0) == self.num_seqs - 1
+            gap_array = gap_array[:, gap_cols] == False
+        elif induced_by:
+            # identify all columns with gap opposite
+            gap_cols = gap_array.sum(axis=0) > 0
+            gap_array = gap_array[:, gap_cols] == False
+        else:
+            gap_cols = gap_array.sum(axis=0) > 0
+            gap_array = gap_array[:, gap_cols]
+
+        result = gap_array.sum(axis=1)
+        result = darr.wrap(result)
+        if drawable:
+            drawable = drawable.lower()
+            trace_name = (
+                os.path.basename(self.info.source) if self.info.source else None
+            )
+            draw = Drawable("Gaps Per Sequence", showlegend=False)
+            draw.layout |= dict(yaxis=dict(title="Gap counts"))
+            if drawable == "bar":
+                trace = UnionDict(type="bar", y=result.array, x=self.names)
+            else:
+                trace = UnionDict(
+                    type=drawable, y=result.array, text=self.names, name=trace_name
+                )
+
+            draw.add_trace(trace)
+            result = draw.bound_to(result)
+
+        return result
+
+    def omit_bad_seqs(self, quantile: OptFloat = None):
+        """Returns new alignment without sequences with a number of uniquely
+        introduced gaps exceeding quantile
+
+        Uses count_gaps_per_seq(unique=True) to obtain the counts of gaps
+        uniquely introduced by a sequence. The cutoff is the quantile of
+        this distribution.
+
+        Parameters
+        ----------
+        quantile
+            sequences whose unique gap count is in a quantile larger than this
+            cutoff are excluded. The default quantile is (num_seqs - 1) / num_seqs
+        """
+        gap_counts = self.count_gaps_per_seq(unique=True)
+        quantile = quantile or (self.num_seqs - 1) / self.num_seqs
+        cutoff = numpy.quantile(gap_counts.array, quantile)
+        names = [name for name, count in gap_counts.items() if count <= cutoff]
+        return self.take_seqs(names)
+
+    def matching_ref(self, ref_name, gap_fraction, gap_run):
+        """Returns new alignment with seqs well aligned with a reference.
+
+        gap_fraction = fraction of positions that either have a gap in the
+            template but not in the seq or in the seq but not in the template
+        gap_run = number of consecutive gaps tolerated in query relative to
+            sequence or sequence relative to query
+        """
+        template = self.seqs[ref_name]
+        gap_filter = make_gap_filter(template, gap_fraction, gap_run)
+        return self.take_seqs_if(gap_filter)
+
+    def sliding_windows(
+        self, window: int, step: int, start: OptInt = None, end: OptInt = None
+    ):
+        """Generator yielding new alignments of given length and interval.
+
+        Parameters
+        ----------
+        window
+            The length of each returned alignment.
+        step
+            The interval between the start of the successive windows.
+        start
+            first window start position
+        end
+            last window start position
+        """
+        start = start or 0
+        end = [end, len(self) - window + 1][end is None]
+        end = min(len(self) - window + 1, end)
+        if start < end and len(self) - end >= window - 1:
+            for pos in range(start, end, step):
+                yield self[pos : pos + window]
+
+    def _get_seq_features(
+        self,
+        *,
+        seqid: Optional[str] = None,
+        biotype: Optional[str] = None,
+        name: Optional[str] = None,
+        allow_partial: bool = False,
+    ) -> Iterator[Feature]:
+        """yields Feature instances
 
-    def _make_aligned(self, data: AlignedDataView) -> Aligned:
-        data.slice_record = self._slice_record
-        return Aligned(data=data, moltype=self.moltype)
+        Parameters
+        ----------
+        seqid
+            limit search to features on this named sequence, defaults to search all
+        biotype
+            biotype of the feature, e.g. CDS, gene
+        name
+            name of the feature
+        allow_partial
+            allow features partially overlaping self
 
-    @property
-    def seqs(self) -> AlignedSeqsDataABC:
-        # want to construct the ASD here (i think)
-        # gets passed the slice record
-        return self._seqs_data
+        Notes
+        -----
+        When dealing with a nucleic acid moltype, the returned features will
+        yield a sequence segment that is consistently oriented irrespective
+        of strand of the current instance.
+        """
+        if self.annotation_db is None:
+            return None
 
-    def get_seq(
-        self, seqname: str, copy_annotations: bool = False
-    ) -> new_sequence.Sequence:
-        """Return a Sequence object for the specified seqname."""
-        seq = self.seqs[seqname].seq
-        if copy_annotations:
-            seq.annotation_db = type(self.annotation_db)()
-            seq.annotation_db.update(annot_db=self.annotation_db, seqids=seqname)
-        else:
-            seq.annotation_db = self.annotation_db
+        seqid_to_seqname = {seq.parent_coordinates()[0]: seq.name for seq in self.seqs}
 
-        return seq
+        seqids = [seqid] if isinstance(seqid, str) else seqid
+        if seqids is None:
+            seqids = tuple(seqid_to_seqname)
+        elif set(seqids) & set(self.names):
+            # we've been given seq names, convert to parent names
+            seqids = [self.seqs[seqid].parent_coordinates()[0] for seqid in seqids]
+        elif seqids and set(seqids) <= seqid_to_seqname.keys():
+            # already correct
+            pass
+        else:
+            raise ValueError(f"unknown {seqid=}")
 
-    @c3warn.deprecated_args(
-        version="2025.6", reason="naming consistency", old_new=[("seq_name", "seqname")]
-    )
-    def get_gapped_seq(self, seqname):
-        """Return a gapped Sequence object for the specified seqname."""
-        return self.seqs[seqname].gapped_seq
+        for seqid in seqids:
+            seqname = seqid_to_seqname[seqid]
+            seq = self.seqs[seqname].seq
+            # we use parent seqid
+            parent_id, start, stop, _ = seq.parent_coordinates()
+            offset = seq.annotation_offset
+
+            for feature in self.annotation_db.get_features_matching(
+                seqid=parent_id,
+                biotype=biotype,
+                name=name,
+                on_alignment=False,
+                allow_partial=allow_partial,
+                start=start,
+                stop=stop,
+            ):
+                if offset:
+                    feature["spans"] = (numpy.array(feature["spans"]) - offset).tolist()
+                # passing self only used when self is an Alignment
+                yield seq.make_feature(feature, self)
 
-    def iter_positions(
-        self, pos_order: list = None
-    ) -> typing.Iterator[list, list, list]:
-        """Iterates over positions in the alignment, in order.
+    def get_features(
+        self,
+        *,
+        seqid: Optional[str] = None,
+        biotype: Optional[str] = None,
+        name: Optional[str] = None,
+        on_alignment: Optional[bool] = None,
+        allow_partial: bool = False,
+    ) -> Iterator[Feature]:
+        """yields Feature instances
 
         Parameters
         ----------
-        pos_order
-            list of indices specifying the column order. If None, the
-            positions are iterated in order.
+        seqid
+            limit search to features on this named sequence, defaults to search all
+        biotype
+            biotype of the feature, e.g. CDS, gene
+        name
+            name of the feature
+        on_alignment
+            limit query to features on Alignment, ignores sequences. Ignored on
+            SequenceCollection instances.
+        allow_partial
+            allow features partially overlaping self
 
-        Returns
-        -------
-        yields lists of elemenets for each position (column) in the alignment
+        Notes
+        -----
+        When dealing with a nucleic acid moltype, the returned features will
+        yield a sequence segment that is consistently oriented irrespective
+        of strand of the current instance.
         """
-        # refactor: array
-        # this could also iter columns of indices as a numpy array - could be an optional arg
-        get = self.seqs.__getitem__
-        pos_order = pos_order or range(self.seqs.align_len)
-        seq_order = self.names
-        for pos in pos_order:
-            yield [str(get(seq)[pos]) for seq in seq_order]
-
-    def __len__(self):
-        return len(self.seqs)
+        # we only do on-alignment in here
+        if not on_alignment:
+            kwargs = {
+                k: v for k, v in locals().items() if k not in ("self", "__class__")
+            }
+            kwargs.pop("on_alignment")
+            yield from self._get_seq_features(**kwargs)
+
+        if on_alignment == False:
+            return
 
-    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
+        if self.annotation_db is None:
+            anno_db = merged_db_collection(self._seqs_data)
+            self.annotation_db = anno_db
 
-    def reverse_complement(self):
-        # needs to be tracked in the ASD mirroring SD
-        ...
+        if self.annotation_db is None:
+            return None
 
-    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)
+        seq_map = None
+        for feature in self.annotation_db.get_features_matching(
+            biotype=biotype,
+            name=name,
+            on_alignment=on_alignment,
+            allow_partial=allow_partial,
+        ):
+            if feature["seqid"]:
+                continue
+            on_al = feature.pop("on_alignment", on_alignment)
+            if feature["seqid"]:
+                raise RuntimeError(f"{on_alignment=} {feature=}")
+            if seq_map is None:
+                seq_map = self.seqs[0].map.to_feature_map()
+                *_, strand = self.seqs[0].data.parent_coordinates()
+
+            spans = numpy.array(feature["spans"])
+            spans = seq_map.relative_position(spans)
+            feature["spans"] = spans.tolist()
+            # and if i've been reversed...?
+            feature["strand"] = "-" if strand == -1 else "+"
+            yield self.make_feature(feature=feature, on_alignment=on_al)
 
-    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"
+    def to_pretty(self, name_order=None, wrap=None):
+        """returns a string representation of the alignment in pretty print format
 
-        output = defaultdict(list)
-        names = name_order or self.names
-        num_seqs = len(names)
+        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])
 
-        seqs = [str(self.seqs[name]) for name in names]
-        positions = list(zip(*seqs))
+        def make_line(label, seq):
+            return f"{label}    {seq}"
 
-        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)
+        if wrap is None:
+            result = [make_line(display_names[n], "".join(output[n])) for n in names]
+            return "\n".join(result)
 
-        return names, output
+        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]),
+                    )
+                )
 
-    def __repr__(self):
-        seqs = []
-        limit = 10
-        delimiter = ""
-        for count, name in enumerate(self.names):
-            if count == 3:
-                seqs.append("...")
-                break
-            elts = list(str(self.seqs[name])[: limit + 1])
-            if len(elts) > limit:
-                elts[-1] = "..."
-            seqs.append(f"{name}[{delimiter.join(elts)}]")
-        seqs = ", ".join(seqs)
+            result.append("")
 
-        return f"{len(self.names)} x {len(self)} {self.moltype.label} alignment: {seqs}"
+        if not result[-1]:
+            del result[-1]
 
-    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"],
-        )
+        return "\n".join(result)
 
     def to_html(
         self,
@@ -3434,45 +4367,40 @@ class Alignment(SequenceCollection):
         ]
         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}"
+    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"
 
-        if wrap is None:
-            result = [make_line(display_names[n], "".join(output[n])) for n in names]
-            return "\n".join(result)
+        output = defaultdict(list)
+        names = name_order or self.names
+        num_seqs = len(names)
 
-        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]),
-                    )
-                )
+        seqs = [str(self.seqs[name]) for name in names]
+        positions = list(zip(*seqs))
 
-            result.append("")
+        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)
 
-        if not result[-1]:
-            del result[-1]
+        return names, output
 
-        return "\n".join(result)
+    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"],
+        )
 
 
 @singledispatch
@@ -3481,23 +4409,49 @@ def make_aligned_seqs(
     *,
     moltype: str,
     info: dict = None,
+    strand: dict[str, int] = None,
+    offset: dict[str, int] = None,
+    annotation_db: SupportsFeatures = None,
 ) -> Alignment:
     raise NotImplementedError(f"make_aligned_seqs not implemented for {type(data)}")
 
 
 @make_aligned_seqs.register
-def _(data: dict, *, moltype: str, info: dict = None) -> Alignment:
+def _(
+    data: dict,
+    *,
+    moltype: str,
+    info: dict = None,
+    strand: dict[str, str] = None,
+    offset: dict[str, int] = None,
+    annotation_db: SupportsFeatures = None,
+) -> Alignment:
     # todo: implement a coerce to AlignedSeqsData dict function
     moltype = new_moltype.get_moltype(moltype)
     alphabet = moltype.most_degen_alphabet()
     aligned_data = AlignedSeqsData.from_aligned_seqs(
-        data=data, alphabet=alphabet, make_seq=moltype.make_seq
+        data=data,
+        alphabet=alphabet,
+        make_seq=moltype.make_seq,
+        strand=strand,
+        offset=offset,
+    )
+    annotation_db = annotation_db or merged_db_collection(data)
+    return make_aligned_seqs(
+        aligned_data, moltype=moltype, info=info, annotation_db=annotation_db
     )
-    return make_aligned_seqs(aligned_data, moltype=moltype, info=info)
 
 
 @make_aligned_seqs.register
-def _(data: AlignedSeqsDataABC, *, moltype: str, info: dict = None) -> Alignment:
+def _(
+    data: AlignedSeqsDataABC,
+    *,
+    moltype: str,
+    info: dict = None,
+    strand: dict[str, str] = None,
+    offset: dict[str, int] = None,
+    annotation_db: SupportsFeatures = None,
+) -> Alignment:
     moltype = new_moltype.get_moltype(moltype)
     if not moltype.is_compatible_alphabet(data.alphabet):
         raise ValueError(
@@ -3505,4 +4459,6 @@ def _(data: AlignedSeqsDataABC, *, moltype: str, info: dict = None) -> Alignment
             f" alphabet: {data.alphabet}",
         )
 
-    return Alignment(seqs_data=data, moltype=moltype, info=info)
+    return Alignment(
+        seqs_data=data, moltype=moltype, info=info, annotation_db=annotation_db
+    )
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index b0f35fc32..1b48c7bc7 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -91,12 +91,12 @@ def _moltype_seq_from_rich_dict(data):
 
     seqview_data = data.pop("seq")
     seq = _coerce_to_seqview(
-        seqview_data["init_args"]["seq"],
+        seqview_data["init_args"]["parent"],
         data["name"],
         moltype.most_degen_alphabet(),
         offset,
     )
-    seq = seq[:: seqview_data["init_args"]["step"]]
+    seq = seq[:: seqview_data["init_args"]["slice_record"]["init_args"]["step"]]
 
     return moltype, seq
 
@@ -245,7 +245,7 @@ class Sequence:
             version=__version__,
         )
         if hasattr(self, "annotation_offset"):
-            offset = int(self._seq.parent_start)
+            offset = int(self._seq.slice_record.parent_start)
             data |= dict(annotation_offset=offset)
 
         if (
@@ -862,7 +862,7 @@ class Sequence:
             int: The offset between annotation coordinates and sequence coordinates.
         """
 
-        return self._seq.parent_start
+        return self._seq.slice_record.parent_start
 
     @property
     def annotation_db(self):
@@ -954,10 +954,14 @@ class Sequence:
             *_,
             strand,
         ) = self.parent_coordinates()
-        query_start = self._seq.absolute_position(start, include_boundary=False)
+        query_start = self._seq.slice_record.absolute_position(
+            start, include_boundary=False
+        )
         # we set include_boundary=True because stop is exclusive indexing,
         # i,e., the stop can be equal to the length of the view
-        query_stop = self._seq.absolute_position(stop, include_boundary=True)
+        query_stop = self._seq.slice_record.absolute_position(
+            stop, include_boundary=True
+        )
 
         rev_strand = strand == -1
         if rev_strand:
@@ -994,7 +998,7 @@ class Sequence:
             # current view
             spans = array(feature.pop("spans"), dtype=int)
             for i, v in enumerate(spans.ravel()):
-                rel_pos = self._seq.relative_position(v)
+                rel_pos = self._seq.slice_record.relative_position(v)
                 spans.ravel()[i] = rel_pos
 
             if rev_strand:
@@ -1671,7 +1675,12 @@ class Sequence:
         -1 or 1.
         """
         strand = -1 if self._seq.is_reversed else 1
-        return self._seq.seqid, self._seq.parent_start, self._seq.parent_stop, strand
+        return (
+            self._seq.seqid,
+            self._seq.slice_record.parent_start,
+            self._seq.slice_record.parent_stop,
+            strand,
+        )
 
 
 @register_deserialiser(get_object_provenance(Sequence))
@@ -2007,9 +2016,6 @@ 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
@@ -2389,6 +2395,62 @@ class SliceRecordABC(ABC):
         )
 
 
+class SliceRecord(SliceRecordABC):
+    __slots__ = "_parent_len"
+
+    def __init__(
+        self,
+        *,
+        parent_len: int,
+        start: OptInt = None,
+        stop: OptInt = None,
+        step: OptInt = None,
+        offset: int = 0,
+    ):
+        if step == 0:
+            raise ValueError("step cannot be 0")
+        step = step if step is not None else 1
+        func = _input_vals_pos_step if step > 0 else _input_vals_neg_step
+        start, stop, step = func(parent_len, start, stop, step)
+        self.start = start
+        self.stop = stop
+        self.step = step
+        self._parent_len = parent_len
+        self._offset = offset or 0
+
+    @property
+    def parent_len(self) -> int:
+        return self._parent_len
+
+    def _get_init_kwargs(self) -> dict:
+        return {}
+
+    def copy(self, sliced: bool = False):
+        # todo: kath
+        return self
+
+    def __repr__(self):
+        return (
+            f"{self.__class__.__name__}(start={self.start}, stop={self.stop}, step={self.step}, "
+            f"parent_len={self.parent_len}, offset={self.offset})"
+        )
+
+    @property
+    def _zero_slice(self):
+        return self.__class__(start=0, stop=0, step=1, parent_len=self.parent_len)
+
+    def to_rich_dict(self) -> dict:
+        data = {"type": get_object_provenance(self), "version": __version__}
+        data["init_args"] = {
+            "parent_len": self.parent_len,
+            "start": self.start,
+            "stop": self.stop,
+            "step": self.step,
+            "offset": self.offset,
+        }
+        return data
+
+
 class SeqViewABC(ABC):
     """
     An abstract base class for providing a view of a sequence.
@@ -2405,6 +2467,22 @@ class SeqViewABC(ABC):
     @abstractmethod
     def seqid(self) -> OptStr: ...
 
+    @property
+    @abstractmethod
+    def parent_len(self) -> int: ...
+
+    @property
+    @abstractmethod
+    def slice_record(self) -> SliceRecordABC: ...
+
+    @property
+    def offset(self) -> int:
+        return self.slice_record.offset
+
+    @property
+    def is_reversed(self) -> bool:
+        return self.slice_record.is_reversed
+
     @property
     @abstractmethod
     def str_value(self) -> str: ...
@@ -2432,8 +2510,14 @@ class SeqViewABC(ABC):
     @abstractmethod
     def __bytes__(self): ...
 
+    @abstractmethod
+    def __getitem__(self, segment: typing.Union[int, slice]) -> SeqViewABC: ...
+
+    def __len__(self):
+        return len(self.slice_record)
+
 
-class SeqView(SeqViewABC, SliceRecordABC):
+class SeqView(SeqViewABC):
     """
     Provides a view of a sequence with support for slicing operations.
 
@@ -2446,14 +2530,6 @@ class SeqView(SeqViewABC, SliceRecordABC):
         the original sequence data
     alphabet
         the alphabet object defining valid characters for the sequence
-    start
-        the starting index of the slice. Defaults to the start of the sequence
-    stop
-        the stopping index of the slice. Defaults to the end of the sequence
-    step
-        the step size for the slice. Defaults to 1
-    offset
-        an offset used for adjusting the view's starting position. Defaults to 0
     seqid
         the name or identifier of the sequence
     parent_len
@@ -2461,15 +2537,15 @@ class SeqView(SeqViewABC, SliceRecordABC):
 
     """
 
+    # todo: kath,
+    # update the docstring to reflect the new design
+
     __slots__ = (
         "parent",
         "alphabet",
-        "start",
-        "stop",
-        "step",
-        "_offset",
         "_seqid",
         "_parent_len",
+        "_slice_record",
     )
 
     def __init__(
@@ -2477,48 +2553,54 @@ class SeqView(SeqViewABC, SliceRecordABC):
         *,
         parent: str,
         alphabet: new_alphabet.AlphabetABC,
-        start: OptInt = None,
-        stop: OptInt = None,
-        step: OptInt = None,
-        offset: int = 0,
         seqid: OptStr = None,
         parent_len: OptInt = None,
+        slice_record: SliceRecordABC = None,
+        offset: int = 0,
     ):
-        if step == 0:
-            raise ValueError("step cannot be 0")
-        step = 1 if step is None else step
         self.alphabet = alphabet
-        func = _input_vals_pos_step if step > 0 else _input_vals_neg_step
-        start, stop, step = func(len(parent), start, stop, step)
         self.parent = parent
-        self.start = start
-        self.stop = stop
-        self.step = step
-        self._offset = offset
         self._seqid = seqid
-
         if parent_len is not None and parent_len != len(parent):
             raise AssertionError(f"{parent_len} != {len(self.parent)})")
         self._parent_len = parent_len or len(self.parent)
-
-    @property
-    def _zero_slice(self):
-        return self.__class__(parent="", alphabet=self.alphabet)
+        self._slice_record = (
+            slice_record
+            if slice_record is not None
+            else SliceRecord(parent_len=self._parent_len)
+        )
+        if offset and self._slice_record.offset:
+            raise ValueError(
+                f"cannot set {offset=} on a SeqView with an offset {self._slice_record.offset=}"
+            )
+        elif offset:
+            self._slice_record.offset = offset
 
     @property
     def seqid(self) -> str:
         return self._seqid
 
+    @property
+    def slice_record(self) -> SliceRecordABC:
+        return self._slice_record
+
     @property
     def parent_len(self) -> int:
         return self._parent_len
 
     def _get_init_kwargs(self):
-        return {"parent": self.parent, "seqid": self.seqid, "alphabet": self.alphabet}
+        return {
+            "parent": self.parent,
+            "seqid": self.seqid,
+            "alphabet": self.alphabet,
+            "slice_record": self.slice_record,
+        }
 
     @property
     def str_value(self):
-        return self.parent[self.start : self.stop : self.step]
+        return self.parent[
+            self.slice_record.start : self.slice_record.stop : self.slice_record.step
+        ]
 
     @property
     def array_value(self):
@@ -2540,18 +2622,36 @@ class SeqView(SeqViewABC, SliceRecordABC):
     def __bytes__(self) -> bytes:
         return self.bytes_value
 
+    def __getitem__(self, segment: typing.Union[int, slice]) -> SeqViewABC:
+        return self.__class__(
+            parent=self.parent,
+            seqid=self.seqid,
+            alphabet=self.alphabet,
+            parent_len=self.parent_len,
+            slice_record=self.slice_record[segment],
+        )
+
     def __repr__(self) -> str:
-        seq = (
+        seq_preview = (
             f"{self.parent[:10]}...{self.parent[-5:]}"
             if self.parent_len > 15
             else self.parent
         )
         return (
-            f"{self.__class__.__name__}(parent={seq!r}, start={self.start}, "
-            f"stop={self.stop}, step={self.step}, offset={self.offset}, "
-            f"seqid={self.seqid!r}, parent_len={self.parent_len})"
+            f"{self.__class__.__name__}(seqid={self.seqid!r}, parent={seq_preview!r}, "
+            f"slice_record={self.slice_record.__repr__()})"
         )
 
+    def with_offset(self, offset: int):
+        if self._slice_record.offset:
+            raise ValueError(
+                f"cannot set {offset=} on a SeqView with an offset {self._slice_record.offset=}"
+            )
+
+        init_kwargs = self._get_init_kwargs()
+        init_kwargs["offset"] = offset
+        return self.__class__(**init_kwargs)
+
     def to_rich_dict(self) -> dict[str, str | dict[str, str]]:
         """returns a json serialisable dict
 
@@ -2568,14 +2668,19 @@ class SeqView(SeqViewABC, SliceRecordABC):
         data = {"type": get_object_provenance(self), "version": __version__}
         data["init_args"] = self._get_init_kwargs()
 
-        data["init_args"]["step"] = self.step
-        if self.is_reversed:
+        if self.slice_record.is_reversed:
             adj = self.parent_len + 1
-            start, stop = self.stop + adj, self.start + adj
+            start, stop = self.slice_record.stop + adj, self.slice_record.start + adj
+
         else:
-            start, stop = self.start, self.stop
+            start, stop = self.slice_record.start, self.slice_record.stop
 
-        data["init_args"]["seq"] = self.parent[start:stop]
+        data["init_args"]["parent"] = self.parent[start:stop]
+        new_sr = SliceRecord(
+            parent_len=(stop - start),
+            step=self.slice_record.step,
+        )
+        data["init_args"]["slice_record"] = new_sr.to_rich_dict()
         data["init_args"]["alphabet"] = self.alphabet.to_rich_dict()
         return data
 
@@ -2593,19 +2698,15 @@ class SeqView(SeqViewABC, SliceRecordABC):
                 parent=self.parent,
                 seqid=self.seqid,
                 alphabet=self.alphabet,
-                start=self.start,
-                stop=self.stop,
-                step=self.step,
-                offset=self.offset,
-                parent_len=self._parent_len,
+                slice_record=self.slice_record.copy(),
+                parent_len=self.parent_len,
             )
         data = self.to_rich_dict()
         return self.__class__(
-            parent=data["init_args"]["seq"],
+            parent=data["init_args"]["parent"],
             seqid=self.seqid,
             alphabet=self.alphabet,
-            step=self.step,
-            offset=self.offset,
+            slice_record=SliceRecord(**data["init_args"]["slice_record"]["init_args"]),
         )
 
 
@@ -2627,7 +2728,7 @@ def _(data: SeqViewABC, seqid, alphabet, offset) -> SeqViewABC:
             f"cannot set {offset=} on a SeqView with an offset {data.offset=}"
         )
     elif offset:
-        data.offset = offset
+        return data.with_offset(offset)
     return data
 
 
diff --git a/src/cogent3/evolve/fast_distance.py b/src/cogent3/evolve/fast_distance.py
index bb58b0d22..b49bb1e9e 100644
--- a/src/cogent3/evolve/fast_distance.py
+++ b/src/cogent3/evolve/fast_distance.py
@@ -7,6 +7,7 @@ from numpy import array, diag, dot, eye, float64, int32, log, sqrt, zeros
 from numpy.linalg import det, inv
 
 from cogent3._version import __version__
+from cogent3.core import new_moltype
 from cogent3.core.moltype import DNA, RNA, get_moltype
 from cogent3.util.dict_array import DictArray
 from cogent3.util.misc import get_object_provenance
@@ -341,7 +342,7 @@ class _PairwiseDistance:
 
     def _convert_seqs_to_indices(self, alignment):
         assert isinstance(
-            alignment.moltype, type(self.moltype)
+            alignment.moltype, (type(self.moltype), new_moltype.MolType)
         ), "Alignment does not have correct MolType"
 
         self._dists = {}


Test patch:
diff --git a/tests/test_core/test_alignment.py b/tests/test_core/test_alignment.py
index a7f7a0446..007eacf14 100644
--- a/tests/test_core/test_alignment.py
+++ b/tests/test_core/test_alignment.py
@@ -1037,7 +1037,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
         self.assertEqual(len(got3), len(got1))
         self.assertEqual(got3.to_dict(), got1.to_dict())
 
-    def test_omit_bad_seqs(self):
+    def test_omit_bad_seqs(self):  # ported
         """omit_bad_seqs should return alignment w/o seqs causing most gaps"""
         data = {
             "s1": "---ACC---TT-",
@@ -1061,7 +1061,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
         result = aln.omit_bad_seqs(0.5)
         self.assertEqual(result.to_dict(), expect)
 
-    def test_matching_ref(self):
+    def test_matching_ref(self):  # ported
         """Alignment.matching_ref returns new aln with well-aln to temp"""
         aln = self.omitSeqsTemplate_aln
         result = aln.matching_ref("s3", 0.9, 5)
@@ -1076,7 +1076,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
         result4 = aln.matching_ref("s3", 0.5, 13)
         self.assertEqual(result4, {"s3": "UUCCUUCUU-UUC", "s4": "UU-UUUU-UUUUC"})
 
-    def test_iupac_consensus_RNA(self):
+    def test_iupac_consensus_RNA(self):  # ported
         """SequenceCollection iupac_consensus should use RNA IUPAC symbols correctly"""
         alignmentUpper = self.Class(
             [
@@ -1095,7 +1095,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
             alignmentUpper.iupac_consensus(), "UYHBN?BSNN??KBVSN?NN??AGCWD?-"
         )
 
-    def test_iupac_consensus_DNA(self):
+    def test_iupac_consensus_DNA(self):  # ported
         """SequenceCollection iupac_consensus should use DNA IUPAC symbols correctly"""
         alignmentUpper = self.Class(
             [
@@ -1112,7 +1112,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
             alignmentUpper.iupac_consensus(DNA), "TYHBN?BSNN??KBVSN?NN??AGCWD?-"
         )
 
-    def test_iupac_consensus_Protein(self):
+    def test_iupac_consensus_Protein(self):  # ported
         """SequenceCollection iupac_consensus should use protein IUPAC symbols correctly"""
         alignmentUpper = self.Class(
             [
@@ -1133,7 +1133,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
         assert not self.identical.is_ragged()
         assert not self.gaps.is_ragged()
 
-    def test_probs_per_pos(self):
+    def test_probs_per_pos(self):  # ported
         """SequenceCollection.probs_per_pos should find Pr(symbol) in each
         column"""
         # make an alignment with 4 seqs (easy to calculate probabilities)
@@ -1149,12 +1149,12 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
             for char, prob in probs.items():
                 assert_allclose(got[pos, char], prob)
 
-    def test_majority_consensus(self):
+    def test_majority_consensus(self):  # ported
         """SequenceCollection.majority_consensus should return commonest symbol per column"""
         # Check the exact strings expected from string transform
         self.assertEqual(self.sequences.majority_consensus(), "UCAG")
 
-    def test_uncertainties(self):
+    def test_uncertainties(self):  # ported
         """SequenceCollection.uncertainties should match hand-calculated values"""
         aln = self.Class(["ABC", "AXC"])
         obs = aln.entropy_per_pos()
@@ -1375,7 +1375,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
         for k, v in expect.items():
             self.assertEqual(got[k], v)
 
-    def test_counts_per_pos(self):
+    def test_counts_per_pos(self):  # ported
         """correctly count motifs"""
         exp = array(
             [
@@ -1430,7 +1430,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
         got = coll.counts_per_seq(include_ambiguity=True, allow_gap=True)
         self.assertEqual(got.col_sum()["-"], 2)
 
-    def test_counts_per_pos_default_moltype(self):  # will not port
+    def test_counts_per_pos_default_moltype(self):
         """produce correct counts per pos with default moltypes"""
         data = {"a": "AAAA??????", "b": "CCCGGG--NN", "c": "CCGGTTCCAA"}
         coll = self.Class(data=data)
@@ -1462,7 +1462,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
         entropy = a.entropy_per_pos()
         assert_allclose(entropy, [numpy.nan, numpy.nan, numpy.nan])
 
-    def test_entropy_excluding_unobserved(self):
+    def test_entropy_excluding_unobserved(self):  # ported
         """omitting unobserved motifs should not affect entropy calculation"""
         a = self.Class(dict(a="ACAGGG", b="AGACCC", c="GGCCTA"), moltype=DNA)
         entropy_excluded = a.entropy_per_seq(exclude_unobserved=True)
@@ -1499,7 +1499,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
         aln = self.Class({"seq1": "--TTT?", "seq2": "GATC??"})
         self.assertEqual(str(aln.get_gapped_seq("seq1")), "--TTT?")
 
-    def test_count_gaps_per_seq(self):
+    def test_count_gaps_per_seq(self):  # ported
         """correctly compute the number of gaps"""
         data = {"a": "AAAA---GGT", "b": "CCC--GG?GT"}
         aln = self.Class(data=data, moltype=DNA)
@@ -1552,7 +1552,7 @@ class AlignmentBaseTests(SequenceCollectionBaseTests):
         seqs = load_aligned_seqs("data/brca1.fasta", array_align=array_align)
         self.assertEqual(seqs.info.source, "data/brca1.fasta")
 
-    def test_seq_entropy_just_gaps(self):
+    def test_seq_entropy_just_gaps(self):  # ported
         """get_seq_entropy should get entropy of each seq"""
         a = self.Class(dict(a="A---", b="----"), moltype=DNA)
         entropy = a.entropy_per_seq()
@@ -1643,7 +1643,7 @@ class ArrayAlignmentTests(AlignmentBaseTests, TestCase):
 class AlignmentTests(AlignmentBaseTests, TestCase):
     Class = Alignment
 
-    def test_sliding_windows(self):
+    def test_sliding_windows(self):  # ported
         """sliding_windows should return slices of alignments."""
         alignment = self.Class(
             {"seq1": "ACGTACGT", "seq2": "ACGTACGT", "seq3": "ACGTACGT"}
@@ -2640,7 +2640,7 @@ def test_get_translation_error(cls, seqs):  # ported for SequenceCollection
 
 @pytest.mark.parametrize("cls", (Alignment, ArrayAlignment))
 @pytest.mark.parametrize("method", ("ic_score", "cogent3_score", "sp_score"))
-def test_alignment_quality_methods(cls, method):
+def test_alignment_quality_methods(cls, method):  # ported
     data = {
         "DogFaced": "TG----AATATGT------GAAAGAG",
         "FreeTaile": "TTGAAGAATATGT------GAAAGAG",
@@ -2691,7 +2691,7 @@ def test_alignment_quality_methods_zero_length(method):
     assert_allclose(score, 0.0)
 
 
-def test_get_gap_array_equivalence():
+def test_get_gap_array_equivalence():  # will not port
     # make sure produced gap arrays are identical between the
     # two Alignment classes
     data = {
@@ -3019,7 +3019,7 @@ def test_add(cls):  # will not port for SequenceCollection
 
 
 @pytest.mark.parametrize("cls", (Alignment, ArrayAlignment))
-def test_count_gaps_per_pos(cls):
+def test_count_gaps_per_pos(cls):  # ported
     """correctly compute the number of gaps"""
     data = {"a": "AAAA---GGT", "b": "CCC--GG?GT"}
     aln = cls(data=data, moltype=DNA)
@@ -3524,7 +3524,7 @@ def test_to_json(cls):  # ported for SequenceCollection
 
 
 @pytest.mark.parametrize("cls", (Alignment, ArrayAlignment))
-def test_get_position_indices(cls):
+def test_get_position_indices(cls):  # ported
     """get_position_indices should return names of cols where f(col)"""
 
     def gap_1st(x):
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index b5cb9e1f4..f0218fd64 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -7,7 +7,7 @@ from warnings import catch_warnings, filterwarnings
 import numpy
 import pytest
 
-from cogent3 import load_unaligned_seqs, open_
+from cogent3 import get_app, load_unaligned_seqs, open_
 from cogent3._version import __version__
 from cogent3.core import new_alignment, new_alphabet, new_moltype, new_sequence
 from cogent3.core.annotation import Feature
@@ -84,10 +84,13 @@ def seqs() -> new_alignment.SequenceCollection:
 
 
 @pytest.fixture
-def ragged_padded():
-    return new_alignment.make_unaligned_seqs(
-        {"a": "AAAAAA", "b": "AAA---", "c": "AAAA--"}, moltype="dna"
-    )
+def ragged_padded_dict():
+    return {"a": "AAAAAA", "b": "AAA---", "c": "AAAA--"}
+
+
+@pytest.fixture
+def ragged_padded(ragged_padded_dict):
+    return new_alignment.make_unaligned_seqs(ragged_padded_dict, moltype="dna")
 
 
 @pytest.fixture
@@ -162,29 +165,26 @@ def test_seqs_data_default_attributes(dna_sd: new_alignment.SeqsData):
     assert isinstance(dna_sd.alphabet, new_alphabet.CharAlphabet)
 
 
-def test_seqs_data_view_zero_step_raises(dna_sd):
-    with pytest.raises(ValueError):
-        new_alignment.SeqDataView(parent=dna_sd, seqid=seq1, step=0, parent_len=4)
-
-
 @pytest.mark.parametrize("seqid", ["seq1", "seq2"])
 def test_seqs_data_view_repr_default(dna_sd: new_alignment.SeqsData, seqid: str):
     seq = dna_sd.get_seq_str(seqid=seqid)
-    seq_len = len(seq)
-    expect = f"SeqDataView(parent={seq}, start=0, stop={seq_len}, step=1, offset=0, seqid='{seqid}', parent_len={seq_len})"
     got = dna_sd.get_view(seqid)
+    expect = (
+        f"SeqDataView(seqid='{seqid}', parent={seq}, slice_record={got.slice_record!r})"
+    )
     assert repr(got) == expect
 
 
 def test_seqs_data_view_repr_default_long(dna_alphabet):
     longseq = "CGATCGTAGTACGTGTCAAGTCTGAC"
-    seq_len = len(longseq)
     trunc = f"{longseq[:10]}...{longseq[-5:]}"
-    expect = f"SeqDataView(parent={trunc}, start=0, stop={seq_len}, step=1, offset=0, seqid='long', parent_len={seq_len})"
 
     d = {"long": longseq}
     sd = new_alignment.SeqsData(data=d, alphabet=dna_alphabet)
     got = sd.get_view(seqid="long")
+    expect = (
+        f"SeqDataView(seqid='long', parent={trunc}, slice_record={got.slice_record!r})"
+    )
     assert repr(got) == expect
 
 
@@ -200,7 +200,6 @@ def test_seqs_data_view_copy(dna_alphabet, seqid, sliced, step):
     sd = new_alignment.SeqsData(data=data, alphabet=dna_alphabet)
     sdv = sd.get_view(seqid=seqid)
     sliced_sdv = sdv[start:stop:step]
-
     copied_sdv = sliced_sdv.copy(sliced=sliced)
 
     assert copied_sdv.str_value == data[seqid][start:stop:step]
@@ -214,7 +213,6 @@ def test_seqs_data_get_seq_view(str_seqs_dict, dna_alphabet, seqid):
     got = sd.get_view(seqid)
     assert isinstance(got, new_alignment.SeqDataView)
     assert got.parent == sd
-    assert got.stop == parent_len
     assert got.seqid == seqid
     assert got.parent_len == parent_len
 
@@ -382,7 +380,7 @@ def test_seqs_data_round_trip(reverse, dna_alphabet):
     seqs_data = new_alignment.SeqsData(
         data={"seq1": "ACGG", "seq2": "CGCA", "seq3": "CCG-"}, alphabet=dna_alphabet
     )
-    seqs_data = seqs_data.reverse_seqs() if reverse else seqs_data
+    seqs_data = seqs_data.reverse() if reverse else seqs_data
 
     rd = seqs_data.to_rich_dict()
     got = deserialise_object(rd)
@@ -403,8 +401,14 @@ def test_seqs_data_round_trip(reverse, dna_alphabet):
         slice(None),
     ],
 )
-def test_seq_data_view_slice_returns_self(seq1: str, index: slice):
-    sdv = new_alignment.SeqDataView(parent=seq1, seqid="seq1", parent_len=len(seq1))
+def test_seq_data_view_slice_returns_self(seq1: str, index: slice, dna_alphabet):
+    sdv = new_alignment.SeqDataView(
+        parent=seq1,
+        seqid="seq1",
+        alphabet=dna_alphabet,
+        parent_len=len(seq1),
+        slice_record=new_sequence.SliceRecord(parent_len=len(seq1)),
+    )
     got = sdv[index]
     assert isinstance(got, new_alignment.SeqDataView)
 
@@ -435,10 +439,9 @@ def test_seq_data_view_to_rich_dict(rev):
     expect = {
         "init_args": {
             "seqid": "seq1",
-            "step": sdv.step,
-            "offset": sdv.offset,
-            "parent_len": sdv.parent_len,
             "parent": sdv.str_value,
+            "alphabet": alpha.to_rich_dict(),
+            "slice_record": sdv.slice_record.to_rich_dict(),
         },
         "type": get_object_provenance(sdv),
         "version": __version__,
@@ -573,14 +576,20 @@ def test_sequence_collection_names_is_list(collection_maker):
     assert isinstance(seqs.names, list)
 
 
-def test_sequence_collection_names(seqs):
-    assert seqs.names == ["seq1", "seq2", "seq3"]
-    seqs.names = ["seq2", "seq3", "seq1"]
-    assert seqs.names == ["seq2", "seq3", "seq1"]
-    seqs.names = ["seq1", "seq2"]
-    assert seqs.names == ["seq1", "seq2"]
+@pytest.mark.parametrize(
+    "collection_maker",
+    (new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs),
+)
+def test_sequence_collection_names(collection_maker):
+    seqs = {"seq1": "AAAAAA", "seq2": "TTTT--", "seq3": "ATTCCC"}
+    seq_coll = collection_maker(seqs, moltype="dna")
+    assert seq_coll.names == ["seq1", "seq2", "seq3"]
+    seq_coll.names = ["seq2", "seq3", "seq1"]
+    assert seq_coll.names == ["seq2", "seq3", "seq1"]
+    seq_coll.names = ["seq1", "seq2"]
+    assert seq_coll.names == ["seq1", "seq2"]
     with pytest.raises(ValueError):
-        seqs.names = ["seq1", "seq2", "seq3", "seq4"]
+        seq_coll.names = ["seq1", "seq2", "seq3", "seq4"]
 
 
 def test_make_unaligned_seqs_list_str():
@@ -629,12 +638,16 @@ def test_sequence_collection_info_source():
     assert seqs.info.source == str(path)
 
 
-def test_sequence_collection_init_ambig():
-    """SequenceCollection should tolerate ambiguous chars"""
-    _ = new_alignment.make_unaligned_seqs(["AAA", "CCC"], moltype="dna")
-    _ = new_alignment.make_unaligned_seqs(["ANS", "CWC"], moltype="dna")
-    _ = new_alignment.make_unaligned_seqs(["A-A", "CC-"], moltype="dna")
-    _ = new_alignment.make_unaligned_seqs(["A?A", "CC-"], moltype="dna")
+@pytest.mark.parametrize(
+    "collection_maker",
+    (new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs),
+)
+def test_sequence_collection_init_ambig(collection_maker):
+    """SequenceCollection and Alignment should tolerate ambiguous chars"""
+    _ = collection_maker({"s0": "AAA", "s1": "CCC"}, moltype="dna")
+    _ = collection_maker({"s0": "ANS", "s1": "CWC"}, moltype="dna")
+    _ = collection_maker({"s0": "A-A", "s1": "CC-"}, moltype="dna")
+    _ = collection_maker({"s0": "A?A", "s1": "CC-"}, moltype="dna")
 
 
 def test_sequence_collection_iter_seqs_ragged_padded(ragged_padded):
@@ -771,38 +784,49 @@ def test_sequence_collection_set_repr_policy_valid_input(seqs):
     assert seqs._repr_policy == dict(num_seqs=5, num_pos=40, ref_name="seq1", wrap=10)
 
 
-def test_sequence_collection_take_seqs(ragged_padded):
-    """SequenceCollection take_seqs should return new SequenceCollection with selected seqs."""
-    a = ragged_padded.take_seqs(["b", "c"])
-    assert isinstance(a, new_alignment.SequenceCollection)
-    assert a.names == ["b", "c"]
-    assert str(a.seqs["b"]) == "AAA---"
-    assert str(a.seqs["c"]) == "AAAA--"
-    assert a.num_seqs == 2
+@pytest.mark.parametrize(
+    "collection_maker",
+    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
+)
+@pytest.mark.parametrize(
+    "sample", [["a"], ["b"], ["c"], ["a", "b"], ["a", "c"], ["b", "c"]]
+)
+def test_sequence_collection_take_seqs(ragged_padded_dict, collection_maker, sample):
+    """take_seqs should return new SequenceCollection/Alignment with selected seqs."""
+    orig = collection_maker(ragged_padded_dict, moltype="dna")
+    subset = orig.take_seqs(sample)
+    assert subset.names == sample
+    assert subset.num_seqs == len(sample)
     # should be able to negate
-    a = ragged_padded.take_seqs(list("bc"), negate=True)
-    assert a.names == ["a"]
-    assert str(a.seqs["a"]) == "AAAAAA"
+    neg_subset = orig.take_seqs(sample, negate=True)
+    assert neg_subset.names == [name for name in orig.names if name not in sample]
 
 
-def test_sequence_collection_take_seqs_str(ragged_padded):
+@pytest.mark.parametrize(
+    "collection_maker",
+    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
+)
+def test_sequence_collection_take_seqs_str(ragged_padded_dict, collection_maker):
     """string arg to SequenceCollection take_seqs should work."""
-    a = ragged_padded.take_seqs("a")
-    assert a.names == ["a"]
-    assert a.num_seqs == 1
+    orig = collection_maker(ragged_padded_dict, moltype="dna")
+    subset = orig.take_seqs("a")
+    assert subset.names == ["a"]
+    assert subset.num_seqs == 1
 
     # should be able to negate
-    a = ragged_padded.take_seqs("a", negate=True)
-    assert isinstance(a, new_alignment.SequenceCollection)
-    assert a.names == ["b", "c"]
-    assert a.num_seqs == 2
+    neg_subset = orig.take_seqs("a", negate=True)
+    assert neg_subset.names == ["b", "c"]
+    assert neg_subset.num_seqs == 2
 
 
-def test_sequence_collection_take_seqs_info():
+@pytest.mark.parametrize(
+    "collection_maker",
+    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
+)
+def test_sequence_collection_take_seqs_info(ragged_padded_dict, collection_maker):
     """take_seqs should preserve info attribute"""
-    data = {"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}
-    orig = new_alignment.make_unaligned_seqs(
-        data,
+    orig = collection_maker(
+        ragged_padded_dict,
         moltype="dna",
         info={"key": "value"},
     )
@@ -810,27 +834,37 @@ def test_sequence_collection_take_seqs_info():
     assert set(subset.info) == set(orig.info)
 
 
-def test_sequence_collection_take_seqs_moltype():
+@pytest.mark.parametrize(
+    "collection_maker",
+    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
+)
+def test_sequence_collection_take_seqs_moltype(ragged_padded_dict, collection_maker):
     """take_seqs should preserve the MolType"""
-    data = {"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}
-    orig = new_alignment.make_unaligned_seqs(data, moltype="dna")
+    orig = collection_maker(ragged_padded_dict, moltype="dna")
     subset = orig.take_seqs(list("ab"))
     assert set(subset.moltype) == set(orig.moltype)
 
 
-def test_sequence_collection_take_seqs_empty_names():
+@pytest.mark.parametrize(
+    "collection_maker",
+    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
+)
+def test_sequence_collection_take_seqs_empty_names(
+    ragged_padded_dict, collection_maker
+):
     """take_seqs should raise ValueError if no seqs are selected."""
-    data = {"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}
-    orig = new_alignment.make_unaligned_seqs(data, moltype="dna")
+    orig = new_alignment.make_unaligned_seqs(ragged_padded_dict, moltype="dna")
     with pytest.raises(ValueError):
         _ = orig.take_seqs([])
 
 
-def test_sequence_collection_take_seqs_copy_annotations(gff_db):
-    data = {"test_seq": "ACGT", "test_seq2": "CGTTTA"}
-    seq_coll = new_alignment.make_unaligned_seqs(
-        data, moltype="dna", annotation_db=gff_db
-    )
+@pytest.mark.parametrize(
+    "collection_maker",
+    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
+)
+def test_sequence_collection_take_seqs_copy_annotations(gff_db, collection_maker):
+    data = {"test_seq": "ACGT--", "test_seq2": "CGTTTA"}
+    seq_coll = collection_maker(data, moltype="dna", annotation_db=gff_db)
     seq_coll.annotation_db = gff_db
     # if copy annotations is true, only the annotations for the selected seqs should be copied
     just_seq2 = seq_coll.take_seqs(names="test_seq2", copy_annotations=True)
@@ -1357,12 +1391,14 @@ def test_sequence_collection_get_motif_probs():
     aln = new_alignment.make_unaligned_seqs(data, moltype="dna")
     got = aln.get_motif_probs(pseudocount=1)
     expect = {"A": 4 / 10, "C": 4 / 10, "G": 1 / 10, "T": 1 / 10}
+    assert got == expect
 
     # pseudocount and allow gap
     data = {"a": "AC-", "b": "AC", "c": "AC"}
     aln = new_alignment.make_unaligned_seqs(data, moltype="dna")
-    got = aln.get_motif_probs(pseudocount=1)
-    expect = {"A": 4 / 11, "C": 4 / 11, "G": 1 / 11, "T": 1 / 11, "-": 1 / 11}
+    got = aln.get_motif_probs(pseudocount=1, allow_gap=True)
+    expect = {"A": 4 / 12, "C": 4 / 12, "G": 1 / 12, "T": 1 / 12, "-": 2 / 12}
+    assert got == expect
 
 
 def test_sequence_collection_get_motif_probs_protein():
@@ -1412,9 +1448,12 @@ def test_sequence_collection_counts_per_seq():
         assert c[k] == expect[k]
 
 
-def test_sequence_collection_probs_per_seq():
+@pytest.mark.parametrize(
+    "coll_maker", (new_alignment.make_aligned_seqs, new_alignment.make_unaligned_seqs)
+)
+def test_sequence_collection_probs_per_seq(coll_maker):
     data = {"seq1": "AA??", "seq2": "CG-N", "seq3": "CGAA"}
-    coll = new_alignment.make_unaligned_seqs(data, moltype="dna")
+    coll = coll_maker(data, moltype="dna")
     got = coll.probs_per_seq()
     assert got["seq1", "A"] == 1.0
     assert got["seq2", "C"] == 0.5
@@ -1661,6 +1700,16 @@ def test_sequence_collection_copy_annotations_incompat_type_fails(seqcoll_db, se
         seqcoll_db.copy_annotations(seqs)
 
 
+@pytest.mark.parametrize("moltype", ("dna", "rna", "protein"))
+def test_sequence_collection_to_moltype(moltype):
+    data = dict(s1="ACGAA-", s2="ACCCAA")
+    seqs = new_alignment.make_unaligned_seqs(data, moltype="text")
+    mt_seqs = seqs.to_moltype(moltype=moltype)
+    got = mt_seqs.seqs["s1"]
+    assert got.moltype.label == moltype
+    assert isinstance(got, new_sequence.Sequence)
+
+
 @pytest.mark.parametrize("moltype", ("dna", "protein"))
 def test_sequence_collection_to_moltype_same_moltype(moltype):
     data = dict(s1="ACGTT", s2="ACCTT")
@@ -1804,21 +1853,25 @@ def test_sequence_collection_dotplot_annotated():
 )
 def test_sequence_collection_add_seqs(collection_maker):
     data = dict(
-        [("name1", "AAA"), ("name2", "AAA"), ("name3", "AAA"), ("name4", "AAA")]
+        [("name1", "AAA"), ("name2", "A--"), ("name3", "AAA"), ("name4", "AAA")]
     )
-    data2 = dict([("name5", "TTT"), ("name6", "CCC")])
+    data2 = dict([("name5", "TTT"), ("name6", "---")])
     aln = collection_maker(data, moltype="dna", info={"key": "foo"})
     out_aln = aln.add_seqs(data2)
     assert len(out_aln.names) == 6
 
 
-def test_sequence_collection_add_seqs_duplicate_raises():
+@pytest.mark.parametrize(
+    "collection_maker",
+    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
+)
+def test_sequence_collection_add_seqs_duplicate_raises(collection_maker):
     """add_seqs should raise an error if duplicate names"""
     data = dict(
         [("name1", "AAA"), ("name2", "AAA"), ("name3", "AAA"), ("name4", "AAA")]
     )
     data2 = dict([("name5", "BBB"), ("name1", "CCC")])
-    aln = new_alignment.make_unaligned_seqs(data, moltype="text", info={"key": "foo"})
+    aln = collection_maker(data, moltype="text", info={"key": "foo"})
     with pytest.raises(ValueError):
         _ = aln.add_seqs(data2)
 
@@ -1827,22 +1880,52 @@ def test_sequence_collection_add_seqs_duplicate_raises():
     assert len(out_aln.names) == 5
 
 
-def test_sequence_collection_add_seqs_info():
+@pytest.mark.parametrize(
+    "collection_maker",
+    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
+)
+def test_sequence_collection_add_seqs_info(collection_maker):
     """add_seqs should preserve info attribute"""
     data = dict(
         [("name1", "AAA"), ("name2", "AAA"), ("name3", "AAA"), ("name4", "AAA")]
     )
     data2 = dict([("name5", "BBB"), ("name6", "CCC")])
-    aln = new_alignment.make_unaligned_seqs(data, moltype="text", info={"key": "foo"})
-    aln2 = new_alignment.make_unaligned_seqs(data2, moltype="text", info={"key": "bar"})
+    aln = collection_maker(data, moltype="text", info={"key": "foo"})
+    aln2 = collection_maker(data2, moltype="text", info={"key": "bar"})
     out_aln = aln.add_seqs(aln2)
     assert out_aln.info["key"] == "foo"
 
 
-def test_sequence_collection_write(tmp_path):
+@pytest.mark.xfail(reason="design of strand attribute is not finalized")
+@pytest.mark.parametrize(
+    "collection_maker",
+    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
+)
+def test_sequence_collection_add_seqs_strand(collection_maker):
+    """add_seqs should preserve strand attribute, and accept new strand information
+    for the new sequences"""
+    data = dict(
+        [("name1", "AAA"), ("name2", "AAA"), ("name3", "AAA"), ("name4", "AAA")]
+    )
+    strand = {"name2": -1}
+    data2 = dict([("name5", "TTT"), ("name6", "CCC")])
+    strand2 = {
+        "name5": -1,
+    }
+    aln = collection_maker(data, moltype="dna", info={"key": "foo"}, strand=strand)
+    out_aln = aln.add_seqs(data2, strand=strand2)
+    assert out_aln.seqs.strand["name2"] == -1
+    assert out_aln.seqs.strand["name5"] == -1
+
+
+@pytest.mark.parametrize(
+    "collection_maker",
+    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
+)
+def test_sequence_collection_write(collection_maker, tmp_path):
     """SequenceCollection.write should write in correct format"""
     data = {"a": "AAAA", "b": "TTTT", "c": "CCCC"}
-    seqs = new_alignment.make_unaligned_seqs(data, moltype="dna")
+    seqs = collection_maker(data, moltype="dna")
     fn = tmp_path / "seqs.fasta"
     seqs.write(fn)
     with open(fn, newline=None) as infile:
@@ -1850,14 +1933,18 @@ def test_sequence_collection_write(tmp_path):
     assert result == ">a\nAAAA\n>b\nTTTT\n>c\nCCCC\n"
 
 
-def test_sequence_collection_write_gapped(tmp_path):
-    data = {"a": "AAA--", "b": "TTTT", "c": "CCCC"}
-    seqs = new_alignment.make_unaligned_seqs(data, moltype="dna")
+@pytest.mark.parametrize(
+    "collection_maker",
+    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
+)
+def test_sequence_collection_write_gapped(collection_maker, tmp_path):
+    data = {"a": "AAA--", "b": "TTTTT", "c": "CCCCC"}
+    seqs = collection_maker(data, moltype="dna")
     fn = tmp_path / "seqs.fasta"
     seqs.write(fn)
     with open(fn, newline=None) as infile:
         result = infile.read()
-    assert result == ">a\nAAA--\n>b\nTTTT\n>c\nCCCC\n"
+    assert result == ">a\nAAA--\n>b\nTTTTT\n>c\nCCCCC\n"
 
 
 def test_get_ambiguous_positions():
@@ -2051,7 +2138,9 @@ def test_sequence_collection_to_rich_dict():
         "init_args": {
             "data": {name: seqs.seqs.get_seq_str(seqid=name) for name in seqs.names},
             "alphabet": seqs.moltype.most_degen_alphabet().to_rich_dict(),
-            "reversed_seqs": seqs.seqs.reversed,
+            "strand": seqs.seqs._strand,
+            "offset": seqs.seqs._offset,
+            "reversed": seqs.seqs.is_reversed,
         },
         "type": get_object_provenance(seqs.seqs),
         "version": __version__,
@@ -2080,7 +2169,9 @@ def test_sequence_collection_to_rich_dict_annotation_db():
         "init_args": {
             "data": {name: seqs.seqs.get_seq_str(seqid=name) for name in seqs.names},
             "alphabet": seqs.moltype.most_degen_alphabet().to_rich_dict(),
-            "reversed_seqs": seqs.seqs.reversed,
+            "strand": seqs.seqs._strand,
+            "offset": seqs.seqs._offset,
+            "reversed": seqs.seqs.is_reversed,
         },
         "type": get_object_provenance(seqs.seqs),
         "version": __version__,
@@ -2101,7 +2192,7 @@ def test_sequence_collection_to_rich_dict_annotation_db():
 
 def test_sequence_collection_to_rich_dict_reversed_seqs():
     data = {"seq1": "ACGG", "seq2": "CGCA", "seq3": "CCG-"}
-    seqs = new_alignment.make_unaligned_seqs(data, moltype="dna")
+    seqs = new_alignment.make_unaligned_seqs(data, moltype="dna", strand={"seq1": -1})
     reversed_seqs = seqs.reverse_complement()
 
     got = reversed_seqs.to_rich_dict()
@@ -2109,7 +2200,9 @@ def test_sequence_collection_to_rich_dict_reversed_seqs():
         "init_args": {
             "data": {name: seqs.seqs.get_seq_str(seqid=name) for name in seqs.names},
             "alphabet": seqs.moltype.most_degen_alphabet().to_rich_dict(),
-            "reversed_seqs": {"seq1": True, "seq2": True, "seq3": True},
+            "strand": {"seq1": -1},
+            "offset": seqs.seqs._offset,
+            "reversed": True,
         },
         "type": get_object_provenance(seqs.seqs),
         "version": __version__,
@@ -2200,9 +2293,13 @@ def test_sequence_collection_distance_matrix_passes_correct_moltype(moltype):
     seqs.distance_matrix()
 
 
-def test_sequence_collection_reverse_complement():
+@pytest.mark.parametrize(
+    "collection_maker",
+    (new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs),
+)
+def test_sequence_collection_reverse_complement(collection_maker):
     data = {"s1": "AACC", "s2": "GGTT"}
-    seqs = new_alignment.make_unaligned_seqs(data, moltype="dna")
+    seqs = collection_maker(data, moltype="dna")
     got = seqs.reverse_complement()
     expect = {"s1": "GGTT", "s2": "AACC"}
     assert got.to_dict() == expect
@@ -2375,7 +2472,7 @@ def test_aligned_bytes(aligned_dict, seqid, dna_moltype):
 @pytest.mark.parametrize(
     "seqid", ("seq1", "seq2", "seq3", "seq4", "seq5", "seq6", "seq7")
 )
-def test_aligned_sliced(gapped_seqs_dict, seqid, start, stop, step):
+def test_aligned_getitem_slice(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]
@@ -2391,6 +2488,26 @@ def test_aligned_sliced(gapped_seqs_dict, seqid, start, stop, step):
     assert got2 == expect
 
 
+@pytest.mark.parametrize(
+    "seqid", ("seq1", "seq2", "seq3", "seq4", "seq5", "seq6", "seq7")
+)
+@pytest.mark.parametrize("i", range(6))
+def test_aligned_getitem_int(gapped_seqs_dict, seqid, i):
+    aln = new_alignment.make_aligned_seqs(gapped_seqs_dict, moltype="dna")
+    al = aln.seqs[seqid]
+    assert isinstance(al, new_alignment.Aligned)
+    got = al[i]
+    expect = gapped_seqs_dict[seqid][i]
+    assert str(got) == expect
+
+
+def test_aligned_getitem_raises(gapped_seqs_dict):
+    aln = new_alignment.make_aligned_seqs(gapped_seqs_dict, moltype="dna")
+    al = aln.seqs["seq1"]
+    with pytest.raises(NotImplementedError):
+        _ = al[(9.0, 20.0)]
+
+
 @pytest.mark.parametrize(
     "seqid", ("seq1", "seq2", "seq3", "seq4", "seq5", "seq6", "seq7")
 )
@@ -2514,14 +2631,21 @@ def test_aligned_seqs_data_len(aligned_dict, dna_alphabet, dna_make_seq):
 @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]
+@pytest.mark.parametrize("moltype", ("dna_moltype", "rna_moltype"))
+def test_aligned_seqs_data_getitem(seqid, index, aligned_array_dict, moltype, request):
+    moltype = request.getfixturevalue(moltype)
+    ad = new_alignment.AlignedSeqsData.from_aligned_seqs(
+        data=aligned_array_dict,
+        alphabet=moltype.degen_gapped_alphabet,
+        make_seq=moltype.make_seq,
+    )
+    got_with_seqid = numpy.array(ad[seqid])
+    got_with_index = numpy.array(ad[index])
+
+    expect = aligned_array_dict[seqid][aligned_array_dict[seqid] != 4]
 
-    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]
+    assert numpy.array_equal(got_with_seqid, expect)
+    assert numpy.array_equal(got_with_index, expect)
 
 
 @pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
@@ -2687,14 +2811,22 @@ def test_aligned_seqs_data_add_seqs_duplicate_keys_raises(dna_alphabet, dna_make
 def test_aligned_seqs_data_get_aligned_view(
     aligned_dict, seqid, dna_alphabet, dna_make_seq
 ):
+    # str on an ADV should return the ungapped sequence
     ad = new_alignment.AlignedSeqsData.from_aligned_seqs(
         data=aligned_dict, alphabet=dna_alphabet, make_seq=dna_make_seq
     )
     got = ad.get_view(seqid)
-    assert isinstance(got, new_alignment.AlignedDataView)
     assert got.parent == ad
     assert got.parent_len == ad.align_len
-    assert str(got) == aligned_dict[seqid]
+    assert str(got) == aligned_dict[seqid].replace("-", "")
+
+
+def test_aligned_seqs_data_subset_raises(aligned_dict, dna_alphabet, dna_make_seq):
+    ad = new_alignment.AlignedSeqsData.from_aligned_seqs(
+        data=aligned_dict, alphabet=dna_alphabet, make_seq=dna_make_seq
+    )
+    with pytest.raises(ValueError):
+        _ = ad.subset(["seq99"])
 
 
 @pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
@@ -2704,10 +2836,52 @@ 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
+    assert numpy.array_equal(got, expect)
+
+    # directly accessing .array_value property should return the same result
+    got = view.array_value
+    assert numpy.array_equal(got, expect)
+
+
+@pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
+def test_aligned_data_view_gapped_array(
+    aligned_array_dict, dna_alphabet, dna_make_seq, seqid
+):
+    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.gapped_array_value
     expect = aligned_array_dict[seqid]
     assert numpy.array_equal(got, expect)
 
 
+@pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
+def test_aligned_data_view_str(aligned_dict, dna_alphabet, dna_make_seq, seqid):
+    ad = new_alignment.AlignedSeqsData.from_aligned_seqs(
+        data=aligned_dict, alphabet=dna_alphabet, make_seq=dna_make_seq
+    )
+    view = ad.get_view(seqid)
+    got = str(view)
+    expect = aligned_dict[seqid].replace("-", "")
+    assert got == expect
+
+    # directly accessing .str_value property should return the same result
+    got = view.str_value
+    assert got == expect
+
+
+def test_aligned_data_view_gapped_str_value(aligned_dict, dna_alphabet, dna_make_seq):
+    ad = new_alignment.AlignedSeqsData.from_aligned_seqs(
+        data=aligned_dict, alphabet=dna_alphabet, make_seq=dna_make_seq
+    )
+    view = ad.get_view("seq1")
+    got = view.gapped_str_value
+    expect = aligned_dict["seq1"]
+    assert got == expect
+
+
 @pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
 def test_aligned_data_view_bytes(aligned_array_dict, dna_alphabet, dna_make_seq, seqid):
     ad = new_alignment.AlignedSeqsData.from_aligned_seqs(
@@ -2715,10 +2889,27 @@ 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]
+    expect = aligned_array_dict[seqid][aligned_array_dict[seqid] != 4]  # remove gaps
     expect = dna_alphabet.array_to_bytes(expect)  # convert to bytes
     assert numpy.array_equal(got, expect)
 
+    # directly accessing .bytes_value property should return the same result
+    got = view.bytes_value
+    assert numpy.array_equal(got, expect)
+
+
+@pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
+def test_aligned_data_view_gapped_bytes_value(
+    aligned_array_dict, dna_alphabet, dna_make_seq, seqid
+):
+    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.gapped_bytes_value
+    expect = dna_alphabet.array_to_bytes(aligned_array_dict[seqid])
+    assert numpy.array_equal(got, expect)
+
 
 def test_alignment_init(aligned_dict, dna_moltype, dna_alphabet, dna_make_seq):
     ad = new_alignment.AlignedSeqsData.from_aligned_seqs(
@@ -2932,6 +3123,34 @@ def test_alignment_repr():
     )
 
 
+@pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
+def test_alignment_getitem_slice(aligned_dict, seqid):
+    """slicing an alignment should propogate the slice to aligned instances"""
+    start = 1
+    stop = 5
+    step = 3
+    aln = new_alignment.make_aligned_seqs(aligned_dict, moltype="dna")
+    sliced_aln = aln[start:stop:step]
+    got = sliced_aln.get_seq(seqid)
+    expect = aligned_dict[seqid][start:stop:step].replace("-", "")
+    assert got == expect
+
+
+@pytest.mark.parametrize("index", (0, 1, 2, 3))
+@pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
+def test_alignment_getitem_int(aligned_dict, index, seqid):
+    aln = new_alignment.make_aligned_seqs(aligned_dict, moltype="dna")
+    got = aln[index].get_gapped_seq(seqid)
+    expect = aligned_dict[seqid][index]
+    assert got == expect
+
+
+def test_alignment_getitem_raises(aligned_dict):
+    aln = new_alignment.make_aligned_seqs(aligned_dict, moltype="dna")
+    with pytest.raises(NotImplementedError):
+        _ = aln[1.0]
+
+
 @pytest.mark.parametrize("start", range(6))
 @pytest.mark.parametrize("stop", range(6))
 @pytest.mark.parametrize("step", range(1, 3))
@@ -2966,7 +3185,7 @@ def test_alignment_slice_neg_step_gapped(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
+    got = sliced_aln.seqs[seqid].data.gapped_str_value
     expect = aligned_dict[seqid][start:stop:step]
     assert got == expect
 
@@ -2978,7 +3197,481 @@ def test_alignment_slice_neg_step_gapped(aligned_dict, start, stop, step, seqid)
 def test_alignment_slice_neg_step_ungapped(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")
+    dna = aln.moltype
     sliced_aln = aln[start:stop:step]
     got = sliced_aln.seqs[seqid].seq
-    expect = aligned_dict[seqid][start:stop:step].replace("-", "")
+    expect = dna.complement(aligned_dict[seqid][start:stop:step].replace("-", ""))
     assert got == expect
+
+
+@pytest.mark.xfail(reason="not implemented yet")
+@pytest.mark.parametrize(
+    "coll_maker", (new_alignment.make_aligned_seqs, new_alignment.make_unaligned_seqs)
+)
+def test_alignment_strand_invalid(aligned_dict, coll_maker):
+    with pytest.raises(ValueError):
+        _ = coll_maker(aligned_dict, moltype="dna", strand={"seq1": "invalid"})
+
+    with pytest.raises(ValueError):
+        _ = coll_maker(aligned_dict, moltype="dna", strand={"seq1": 3})
+
+
+@pytest.mark.xfail(reason="design of strand attribute is not finalized")
+@pytest.mark.parametrize(
+    "coll_maker", (new_alignment.make_aligned_seqs, new_alignment.make_unaligned_seqs)
+)
+def test_alignment_strand(aligned_dict, coll_maker):
+    aln = coll_maker(aligned_dict, moltype="dna", strand={"seq1": -1})
+    assert aln.seqs.strand["seq1"] == -1
+    # if we don't set the strand information, it should be set to 1
+    assert aln.seqs.strand["seq2"] == 1
+    # reverse complement should flip the strand orientation
+    rc_aln = aln.rc()
+    assert rc_aln.seqs.strand["seq1"] == 1
+    assert rc_aln.seqs.strand["seq2"] == -1
+
+
+@pytest.mark.xfail(reason="not implemented for new style alignments")
+@pytest.mark.parametrize("method", ("ic_score", "cogent3_score", "sp_score"))
+def test_alignment_quality_methods(method):
+    data = {
+        "DogFaced": "TG----AATATGT------GAAAGAG",
+        "FreeTaile": "TTGAAGAATATGT------GAAAGAG",
+        "LittleBro": "CTGAAGAACCTGTGAAAGTGAAAGAG",
+    }
+    expected_score = dict(
+        cogent3_score=-123.0, ic_score=32.93032499, sp_score=-25.25687109
+    )[method]
+    aln = new_alignment.make_aligned_seqs(
+        data,
+        moltype="dna",
+        info=dict(align_params=dict(lnL=-123.0)),
+    )
+    app = get_app(method)
+    score = app(aln)
+    assert numpy.allclose(score, expected_score)
+
+
+def test_alignment_counts_per_pos():
+    """correctly count motifs"""
+    exp = numpy.array(
+        [
+            [1, 1, 1, 0],
+            [0, 2, 0, 1],
+            [0, 0, 3, 0],
+            [1, 1, 0, 1],
+            [0, 0, 3, 0],
+            [1, 1, 0, 1],
+        ]
+    )
+
+    exp_gap = numpy.array(
+        [
+            [1, 1, 0, 1, 0],
+            [0, 2, 0, 0, 1],
+            [0, 0, 3, 0, 0],
+            [0, 2, 0, 1, 0],
+            [0, 1, 2, 0, 0],
+            [0, 2, 0, 1, 0],
+        ]
+    )
+
+    aln = new_alignment.make_aligned_seqs(
+        {"s1": "TCAGAG", "s2": "CCACAC", "s3": "AGATAT"}, moltype="dna"
+    )
+    obs = aln.counts_per_pos()
+    assert numpy.array_equal(obs.array, exp)
+    assert numpy.array_equal(obs.motifs, tuple(aln.moltype.alphabet))
+    obs = aln.counts_per_pos(motif_length=2)
+    assert numpy.array_equal(obs[0, "TC"], 1)
+    assert numpy.array_equal(obs[1, "AC"], 1)
+    assert numpy.array_equal(obs[2, "AC"], 1)
+    aln = new_alignment.make_aligned_seqs(
+        {"s1": "TCAGAG", "s2": "CCACAC", "s4": "G-ACCC"}, moltype="dna"
+    )
+    obs = aln.counts_per_pos(allow_gap=True)
+    assert numpy.array_equal(obs.array, exp_gap)
+    aln = new_alignment.make_aligned_seqs(
+        {"s1": "-RAT", "s2": "ACCT", "s3": "GTGT"}, moltype="dna"
+    )
+    c = aln.counts_per_pos(include_ambiguity=False, allow_gap=True)
+    assert numpy.array_equal(set(c.motifs), set("ACGT-"))
+
+
+def test_alignment_get_gap_array():
+    data = {
+        "DogFaced": "TG-",
+        "FreeTaile": "T-G",
+        "LittleBro": "---",
+        "BigBro": "GTT",
+        "MiddleBro": "-T-",
+    }
+    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
+    got = aln.get_gap_array()
+    expect = numpy.array(
+        [
+            [False, False, True],
+            [False, True, False],
+            [True, True, True],
+            [False, False, False],
+            [True, False, True],
+        ],
+        dtype=bool,
+    )
+    assert numpy.allclose(got, expect)
+
+
+def test_get_position_indices():
+    """get_position_indices should return names of cols where f(col)"""
+
+    def gap_1st(x):
+        return x[0] == "-"
+
+    def gap_2nd(x):
+        return x[1] == "-"
+
+    def gap_3rd(x):
+        return x[2] == "-"
+
+    def is_list(x):
+        return isinstance(x, list)
+
+    gaps = new_alignment.make_aligned_seqs(
+        {"a": "AAAAAAA", "b": "A--A-AA", "c": "AA-----"}, moltype="dna"
+    )
+
+    assert gaps.get_position_indices(gap_1st) == []
+    assert gaps.get_position_indices(gap_2nd) == [1, 2, 4]
+    assert gaps.get_position_indices(gap_3rd) == [2, 3, 4, 5, 6]
+    assert gaps.get_position_indices(is_list) == [0, 1, 2, 3, 4, 5, 6]
+    # should be able to negate
+    assert gaps.get_position_indices(gap_2nd, negate=True) == [0, 3, 5, 6]
+    assert gaps.get_position_indices(gap_1st, negate=True) == [0, 1, 2, 3, 4, 5, 6]
+    assert gaps.get_position_indices(is_list, negate=True) == []
+
+
+def test_iupac_consensus_rna():
+    """Alignment iupac_consensus should use RNA IUPAC symbols correctly"""
+    aln = new_alignment.make_aligned_seqs(
+        {
+            "seq1": "UCAGN-UCAGN-UCAGN-UCAGAGCAUN-",
+            "seq2": "UUCCAAGGNN--UUCCAAGGNNAGCAG--",
+            "seq3": "UUCCAAGGNN--UUCCAAGGNNAGCUA--",
+            "seq4": "UUUUCCCCAAAAGGGGNNNN--AGCUA--",
+            "seq5": "UUUUCCCCAAAAGGGGNNNN--AGCUA--",
+        },
+        moltype="rna",
+    )
+
+    # following IUPAC consensus calculated by hand
+    assert aln.iupac_consensus() == "UYHBN?BSNN??KBVSN?NN??AGCWD?-"
+
+
+def test_iupac_consensus_dna():
+    """Alignment iupac_consensus should use DNA IUPAC symbols correctly"""
+    aln = new_alignment.make_aligned_seqs(
+        {
+            "seq1": "TCAGN-TCAGN-TCAGN-TCAGAGCATN-",
+            "seq2": "TTCCAAGGNN--TTCCAAGGNNAGCAG--",
+            "seq3": "TTCCAAGGNN--TTCCAAGGNNAGCTA--",
+            "seq4": "TTTTCCCCAAAAGGGGNNNN--AGCTA--",
+            "seq5": "TTTTCCCCAAAAGGGGNNNN--AGCTA--",
+        },
+        moltype="dna",
+    )
+    # following IUPAC consensus calculated by hand
+    assert aln.iupac_consensus() == "TYHBN?BSNN??KBVSN?NN??AGCWD?-"
+
+
+def test_iupac_consensus_protein():
+    """Alignment iupac_consensus should use protein IUPAC symbols correctly"""
+    aln = new_alignment.make_aligned_seqs(
+        {
+            "seq1": "ACDEFGHIKLMNPQRSTUVWY-",
+            "seq2": "ACDEFGHIKLMNPQRSUUVWF-",
+            "seq3": "ACDEFGHIKLMNPERSKUVWC-",
+            "seq4": "ACNEFGHIKLMNPQRS-UVWP-",
+        },
+        moltype="protein",
+    )
+    # following IUPAC consensus calculated by hand
+    # Test all uppper
+    assert aln.iupac_consensus() == "ACBEFGHIKLMNPZRS?UVWX-"
+
+
+def test_majority_consensus():
+    """Alignment.majority_consensus should return commonest symbol per column"""
+    # Check the exact strings expected from string transform
+    aln = new_alignment.make_aligned_seqs(
+        {
+            "seq1": "ACG",
+            "seq2": "ACG",
+            "seq3": "TTT",
+        },
+        moltype="dna",
+    )
+    assert aln.majority_consensus() == "ACG"
+
+
+def test_probs_per_pos():
+    """Alignment.probs_per_pos should find Pr(symbol) in each
+    column"""
+    # 4 seqs (easy to calculate probabilities)
+    align = new_alignment.make_aligned_seqs(
+        {"seq1": "AAA", "seq2": "ACA", "seq3": "GGG", "seq4": "GUC"}, moltype="rna"
+    )
+    got = align.probs_per_pos()
+    # check that the column probs match the counts we expect
+    expect = [
+        {"A": 0.5, "G": 0.5},
+        {"A": 0.25, "C": 0.25, "G": 0.25, "U": 0.25},
+        {"A": 0.5, "G": 0.25, "C": 0.25},
+    ]
+    for pos, probs in enumerate(expect):
+        for char, prob in probs.items():
+            assert numpy.allclose(got[pos, char], prob)
+
+
+def test_entropy_per_pos():
+    """SequenceCollection.entropy_per_pos should match hand-calculated values"""
+    aln = new_alignment.make_aligned_seqs({"seq1": "ATA", "seq2": "AAA"}, moltype="dna")
+    obs = aln.entropy_per_pos()
+    assert numpy.allclose(obs, [0, 1, 0])
+    # check what happens with only one input sequence
+    aln = new_alignment.make_aligned_seqs({"seq1": "TGC"}, moltype="dna")
+    obs = aln.entropy_per_pos()
+    assert numpy.allclose(obs, [0, 0, 0])
+
+
+@pytest.mark.parametrize(
+    "coll_maker", (new_alignment.make_aligned_seqs, new_alignment.make_unaligned_seqs)
+)
+def test_entropy_excluding_unobserved(coll_maker):
+    """omitting unobserved motifs should not affect entropy calculation"""
+    a = coll_maker(dict(a="ACAGGG", b="AGACCC", c="GGCCTA"), moltype="dna")
+    entropy_excluded = a.entropy_per_seq(exclude_unobserved=True)
+    entropy_unexcluded = a.entropy_per_seq(exclude_unobserved=False)
+    assert numpy.allclose(entropy_excluded, entropy_unexcluded)
+
+
+def test_seq_entropy_just_gaps():
+    """get_seq_entropy should get entropy of each seq"""
+    aln = new_alignment.make_aligned_seqs(dict(a="A---", b="----"), moltype="dna")
+    got = aln.entropy_per_seq()
+    expect = numpy.array([0, numpy.nan], dtype=numpy.float64)
+
+    assert numpy.allclose(got, expect, equal_nan=True)
+
+    aln = new_alignment.make_aligned_seqs(dict(a="----", b="----"), moltype="dna")
+    entropy = aln.entropy_per_seq()
+    assert entropy is None
+
+
+def test_get_gap_array():
+    aln = new_alignment.make_aligned_seqs(
+        {"seq1": "A-GN", "seq2": "TG--", "seq3": "----"}, moltype="dna"
+    )
+    got = aln.get_gap_array()
+    expect = numpy.array(
+        [
+            [False, True, False, True],
+            [False, False, True, True],
+            [True, True, True, True],
+        ]
+    )
+    assert numpy.allclose(got, expect)
+
+    got = aln.get_gap_array(include_ambiguity=False)
+    expect = numpy.array(
+        [
+            [False, True, False, False],
+            [False, False, True, True],
+            [True, True, True, True],
+        ]
+    )
+
+
+def test_count_gaps_per_pos():
+    """correctly compute the number of gaps"""
+    data = {"a": "AAAA---GGT", "b": "CCC--GG?GT"}
+    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
+    # per position
+    got = aln.count_gaps_per_pos(include_ambiguity=False)
+    assert numpy.array_equal(got.array, [0, 0, 0, 1, 2, 1, 1, 0, 0, 0])
+    got = aln.count_gaps_per_pos(include_ambiguity=True)
+    assert numpy.array_equal(got.array, [0, 0, 0, 1, 2, 1, 1, 1, 0, 0])
+
+
+def test_count_gaps_per_seq():
+    """correctly compute the number of gaps"""
+    data = {"a": "AAAA---GGT", "b": "CCC--GG?GT"}
+    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
+    got = aln.count_gaps_per_seq(include_ambiguity=False)
+    assert numpy.array_equal(got.array, [3, 2])
+    assert numpy.array_equal(got["b"], 2)
+    got = aln.count_gaps_per_seq(include_ambiguity=True)
+    assert numpy.array_equal(got.array, [3, 3])
+    assert numpy.array_equal(got["b"], 3)
+    # per seq, unique
+    got = aln.count_gaps_per_seq(include_ambiguity=False, unique=True)
+    assert numpy.array_equal(got.array, [1, 2])
+    got = aln.count_gaps_per_seq(include_ambiguity=True, unique=True)
+    assert numpy.array_equal(got.array, [2, 2])
+
+    data = {"a": "AAAGGG", "b": "------", "c": "------"}
+    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
+    got = aln.count_gaps_per_seq(include_ambiguity=False, unique=True)
+    assert numpy.array_equal(got.array, [6, 0, 0])
+    assert numpy.array_equal(got["a"], 6)
+    assert numpy.array_equal(got["b"], 0)
+
+    # per_seq, induced_by
+    data = {"a": "--ACGT---GTAC", "b": "--ACGTA--GT--", "c": "--ACGTA-AGT--"}
+    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
+    got = aln.count_gaps_per_seq(unique=False, induced_by=True)
+    assert numpy.array_equal(got.array, [2, 1, 2])
+    assert numpy.array_equal(got["b"], 1)
+
+
+def test_omit_bad_seqs():
+    """omit_bad_seqs should return alignment w/o seqs causing most gaps"""
+    data = {
+        "s1": "---ACC---TT-",
+        "s2": "---ACC---TT-",
+        "s3": "---ACC---TT-",
+        "s4": "--AACCG-GTT-",
+        "s5": "--AACCGGGTTT",
+        "s6": "AGAACCGGGTT-",
+    }
+
+    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
+    # with defaults, excludes s6
+    expect = data.copy()
+    del expect["s6"]
+    result = aln.omit_bad_seqs()
+    assert result.to_dict() == expect
+    # with quantile 0.5, just s1, s2, s3
+    expect = data.copy()
+    for key in ("s6", "s5"):
+        del expect[key]
+    result = aln.omit_bad_seqs(0.5)
+    assert result.to_dict() == expect
+
+
+def test_matching_ref():
+    """Alignment.matching_ref returns new aln with well-aln to temp"""
+    data = {
+        "s1": "UC-----CU---C",
+        "s2": "UC------U---C",
+        "s3": "UUCCUUCUU-UUC",
+        "s4": "UU-UUUU-UUUUC",
+        "s5": "-------------",
+    }
+
+    aln = new_alignment.make_aligned_seqs(data, moltype="rna")
+    result = aln.matching_ref("s3", 0.9, 5)
+    assert result.to_dict() == {"s3": "UUCCUUCUU-UUC", "s4": "UU-UUUU-UUUUC"}
+    result2 = aln.matching_ref("s4", 0.9, 4)
+    assert result2.to_dict() == {"s3": "UUCCUUCUU-UUC", "s4": "UU-UUUU-UUUUC"}
+    result3 = aln.matching_ref("s1", 0.9, 4)
+    assert result3.to_dict() == {
+        "s2": "UC------U---C",
+        "s1": "UC-----CU---C",
+        "s5": "-------------",
+    }
+
+    result4 = aln.matching_ref("s3", 0.5, 13)
+    assert result4.to_dict() == {"s3": "UUCCUUCUU-UUC", "s4": "UU-UUUU-UUUUC"}
+
+
+def test_sliding_windows():
+    """sliding_windows should return slices of alignments."""
+    alignment = new_alignment.make_aligned_seqs(
+        {"seq1": "ACGTACGT", "seq2": "ACGTACGT", "seq3": "ACGTACGT"}, moltype="dna"
+    )
+    result = []
+
+    for bit in alignment.sliding_windows(5, 2):
+        result += [bit]
+    assert result[0].to_dict() == {"seq3": "ACGTA", "seq2": "ACGTA", "seq1": "ACGTA"}
+    assert result[1].to_dict() == {"seq3": "GTACG", "seq2": "GTACG", "seq1": "GTACG"}
+
+    result = []
+    for bit in alignment.sliding_windows(5, 1):
+        result += [bit]
+    assert result[0].to_dict() == {"seq3": "ACGTA", "seq2": "ACGTA", "seq1": "ACGTA"}
+    assert result[1].to_dict() == {"seq3": "CGTAC", "seq2": "CGTAC", "seq1": "CGTAC"}
+    assert result[2].to_dict() == {"seq3": "GTACG", "seq2": "GTACG", "seq1": "GTACG"}
+    assert result[3].to_dict() == {"seq3": "TACGT", "seq2": "TACGT", "seq1": "TACGT"}
+
+
+@pytest.mark.xfail(reason="todo: get_features not properly implemented so test hangs")
+def test_get_feature():
+    aln = new_alignment.make_aligned_seqs(
+        {"x": "-AAAAAAAAA", "y": "TTTT--CCCT"}, moltype="dna"
+    )
+
+    db = aln.annotation_db
+    db.add_feature(seqid="y", biotype="exon", name="A", spans=[(5, 8)])
+    feat = list(aln.get_features(seqid="y", biotype="exon", on_alignment=False))[0]
+    assert feat.get_slice().to_dict() == dict(x="AAA", y="CCT")
+
+
+@pytest.fixture
+def alignment():
+    data = {
+        "seq1": "A-GT",
+        #         012
+        "seq2": "-GGT",
+    }
+    return new_alignment.make_aligned_seqs(data, moltype="dna")
+
+
+def test_aligned_view_parent_coords(alignment):
+    seqid = "seq2"
+    a1 = alignment.seqs[seqid]
+    got = a1.parent_coordinates()
+    assert got == (seqid, 0, 4, 1)
+
+    a2 = alignment[2:]
+    a1_2 = a2.seqs[seqid]
+    assert a1_2.parent_coordinates() == (seqid, 2, 4, 1)
+
+    # now getting the sequence coordinates
+    s2 = a2.get_seq(seqid)
+    expect = s2.parent_coordinates()
+    got = a1_2.parent_coordinates(seq_coords=True)
+    assert got == expect
+
+
+def test_aligned_view_parent_coords_reversed(alignment):
+    seqid = "seq2"
+    a2 = alignment[2:].rc()
+    a1_2 = a2.seqs[seqid]
+    assert a1_2.parent_coordinates() == (seqid, 2, 4, -1)
+
+    s2 = a2.get_seq(seqid)
+    expect = s2.parent_coordinates()
+    got = a1_2.parent_coordinates(seq_coords=True)
+    assert got == expect
+
+
+@pytest.mark.parametrize("rced", [True, False])
+def test_get_seq_from_slice(alignment, rced):
+    seqid = "seq2"
+    raw = str(alignment.seqs[seqid])
+    dna = alignment.moltype
+    expect = dna.rc(raw[2:]) if rced else raw[2:]
+
+    a2 = alignment[2:].rc() if rced else alignment[2:]
+    seq = a2.get_seq(seqid)
+    got = str(seq)
+    assert got == expect
+
+
+@pytest.mark.parametrize("seqid", ["seq1", "seq2"])
+def test_alignment_indexing_string(alignment, seqid):
+    # when indexing with a string, should return an Aligned instance
+    got = alignment.seqs[seqid]
+    assert isinstance(got, new_alignment.Aligned)
+    assert str(got) == alignment.to_dict()[seqid]
diff --git a/tests/test_core/test_new_sequence.py b/tests/test_core/test_new_sequence.py
index ef3c56e02..5b977c07f 100644
--- a/tests/test_core/test_new_sequence.py
+++ b/tests/test_core/test_new_sequence.py
@@ -998,8 +998,11 @@ def test_to_phylip():
 def test_seqview_initialisation(start, stop, step, bytes_alphabet):
     """Initialising a SeqView should work with range of provided values"""
     seq_data = "0123456789"
+    slice_record = new_sequence.SliceRecord(
+        start=start, stop=stop, step=step, parent_len=len(seq_data)
+    )
     got = new_sequence.SeqView(
-        parent=seq_data, start=start, stop=stop, step=step, alphabet=bytes_alphabet
+        parent=seq_data, slice_record=slice_record, alphabet=bytes_alphabet
     )
     expected = seq_data[start:stop:step]
     assert got.str_value == expected
@@ -1029,7 +1032,10 @@ def test_seqview_step_0(bytes_alphabet):
     with pytest.raises(ValueError):
         _ = sv[::0]
     with pytest.raises(ValueError):
-        _ = new_sequence.SeqView(parent="0123456789", alphabet=bytes_alphabet, step=0)
+        sr = new_sequence.SliceRecord(step=0, parent_len=10)
+        _ = new_sequence.SeqView(
+            parent="0123456789", alphabet=bytes_alphabet, slice_record=sr
+        )
 
 
 @pytest.mark.parametrize("start", (0, 2, 4))
@@ -1040,7 +1046,8 @@ def test_seqview_invalid_index(start, bytes_alphabet):
     pos_boundary_index = length
     neg_boundary_index = -length - 1
 
-    sv = new_sequence.SeqView(parent=seq, start=start, alphabet=bytes_alphabet)
+    sr = new_sequence.SliceRecord(start=start, parent_len=len(seq))
+    sv = new_sequence.SeqView(parent=seq, slice_record=sr, alphabet=bytes_alphabet)
     with pytest.raises(IndexError):
         _ = sv[pos_boundary_index]
     with pytest.raises(IndexError):
@@ -1056,9 +1063,8 @@ def test_seqview_invalid_index_positive_step_gt_1(start, bytes_alphabet):
     neg_boundary_index = -length - 1
     pos_boundary_index = length
 
-    sv = new_sequence.SeqView(
-        parent=seq, start=start, step=step, alphabet=bytes_alphabet
-    )
+    sr = new_sequence.SliceRecord(start=start, step=step, parent_len=len(seq))
+    sv = new_sequence.SeqView(parent=seq, slice_record=sr, alphabet=bytes_alphabet)
     with pytest.raises(IndexError):
         _ = sv[pos_boundary_index]
     with pytest.raises(IndexError):
@@ -1075,9 +1081,10 @@ def test_seqview_invalid_index_reverse_step(stop, bytes_alphabet):
     neg_boundary_index = -length - 1
     pos_boundary_index = length
 
-    sv = new_sequence.SeqView(
-        parent=seq, start=start, stop=stop, step=step, alphabet=bytes_alphabet
+    sr = new_sequence.SliceRecord(
+        start=start, stop=stop, step=step, parent_len=len(seq)
     )
+    sv = new_sequence.SeqView(parent=seq, slice_record=sr, alphabet=bytes_alphabet)
     with pytest.raises(IndexError):
         _ = sv[pos_boundary_index]
     with pytest.raises(IndexError):
@@ -1094,9 +1101,10 @@ def test_seqview_invalid_index_reverse_step_gt_1(stop, bytes_alphabet):
     neg_boundary_index = -length - 1
     pos_boundary_index = length
 
-    sv = new_sequence.SeqView(
-        parent=seq, start=start, stop=stop, step=step, alphabet=bytes_alphabet
+    sr = new_sequence.SliceRecord(
+        start=start, stop=stop, step=step, parent_len=len(seq)
     )
+    sv = new_sequence.SeqView(parent=seq, slice_record=sr, alphabet=bytes_alphabet)
     with pytest.raises(IndexError):
         _ = sv[pos_boundary_index]
     with pytest.raises(IndexError):
@@ -1115,11 +1123,12 @@ def test_seqview_start_out_of_bounds(bytes_alphabet):
     seq = "0123456789"
     init_start, init_stop, init_step = 2, 10, 1
     boundary = abs((init_start - init_stop) // init_step)
+    sr = new_sequence.SliceRecord(
+        start=init_start, stop=init_stop, step=init_step, parent_len=len(seq)
+    )
     sv = new_sequence.SeqView(
         parent=seq,
-        start=init_start,
-        stop=init_stop,
-        step=init_step,
+        slice_record=sr,
         alphabet=bytes_alphabet,
     )
     got = sv[boundary::].str_value
@@ -1131,11 +1140,12 @@ def test_seqview_start_out_of_bounds_step_gt_1(bytes_alphabet):
     seq = "0123456789"
     init_start, init_stop, init_step = 2, 10, 2
     boundary = abs((init_start - init_stop) // init_step)
+    sr = new_sequence.SliceRecord(
+        start=init_start, stop=init_stop, step=init_step, parent_len=len(seq)
+    )
     sv = new_sequence.SeqView(
         parent=seq,
-        start=init_start,
-        stop=init_stop,
-        step=init_step,
+        slice_record=sr,
         alphabet=bytes_alphabet,
     )
     got = sv[boundary::].str_value
@@ -1149,11 +1159,12 @@ def test_seqview_start_out_of_bounds_reverse_step(bytes_alphabet):
     boundary_pos = abs((init_start - init_stop) // init_step)
     boundary_neg = -abs((init_start - init_stop) // init_step) - 1
 
+    sr = new_sequence.SliceRecord(
+        start=init_start, stop=init_stop, step=init_step, parent_len=len(seq)
+    )
     sv = new_sequence.SeqView(
         parent=seq,
-        start=init_start,
-        stop=init_stop,
-        step=init_step,
+        slice_record=sr,
         alphabet=bytes_alphabet,
     )
 
@@ -1205,7 +1216,8 @@ def test_seqview_sliced_index(index, simple_slices, bytes_alphabet):
 def test_seqview_reverse_slice(first_step, second_step, bytes_alphabet):
     """subsequent slices may reverse the previous slice"""
     seq = "0123456789"
-    sv = new_sequence.SeqView(parent=seq, step=first_step, alphabet=bytes_alphabet)
+    sr = new_sequence.SliceRecord(step=first_step, parent_len=len(seq))
+    sv = new_sequence.SeqView(parent=seq, slice_record=sr, alphabet=bytes_alphabet)
     got = sv[::second_step]
     expected = seq[::first_step][::second_step]
     assert got.str_value == expected
@@ -1223,16 +1235,20 @@ def test_seqview_rev_sliced_index(index, start, stop, step, seq, bytes_alphabet)
         expected = seq_data[start:stop:step][index]
     except IndexError:
         with pytest.raises(IndexError):
+            sr = new_sequence.SliceRecord(
+                start=start, stop=stop, step=step, parent_len=len(seq_data)
+            )
             _ = new_sequence.SeqView(
                 parent=seq_data,
-                start=start,
-                stop=stop,
-                step=step,
+                slice_record=sr,
                 alphabet=bytes_alphabet,
             )[index].str_value
     else:  # if no index error, SeqView should match python slicing
+        sr = new_sequence.SliceRecord(
+            start=start, stop=stop, step=step, parent_len=len(seq_data)
+        )
         got = new_sequence.SeqView(
-            parent=seq_data, start=start, stop=stop, step=step, alphabet=bytes_alphabet
+            parent=seq_data, slice_record=sr, alphabet=bytes_alphabet
         )[index].str_value
         assert got == expected
 
@@ -1243,9 +1259,10 @@ def test_seqview_rev_sliced_index(index, start, stop, step, seq, bytes_alphabet)
 @pytest.mark.parametrize("step", (1, 2, -1, -2))
 def test_seqview_init_with_negatives(seq, start, stop, step, bytes_alphabet):
     "SeqView initialisation should handle any combination of positive and negative slices"
-    got = new_sequence.SeqView(
-        parent=seq, start=start, stop=stop, step=step, alphabet=bytes_alphabet
+    sr = new_sequence.SliceRecord(
+        start=start, stop=stop, step=step, parent_len=len(seq)
     )
+    got = new_sequence.SeqView(parent=seq, slice_record=sr, alphabet=bytes_alphabet)
     expected = seq[start:stop:step]
     assert got.str_value == expected
 
@@ -1567,34 +1584,49 @@ def test_seqview_repr():
     # Short sequence, defaults
     seq = "ACGT"
     view = new_sequence.SeqView(parent=seq, alphabet=alpha)
-    expected = "SeqView(parent='ACGT', start=0, stop=4, step=1, offset=0, seqid=None, parent_len=4)"
+    expected = f"SeqView(seqid=None, parent='ACGT', slice_record={view.slice_record!r})"
     assert repr(view) == expected
 
     # Long sequence
     seq = "ACGT" * 10
     view = new_sequence.SeqView(parent=seq, alphabet=alpha)
-    expected = "SeqView(parent='ACGTACGTAC...TACGT', start=0, stop=40, step=1, offset=0, seqid=None, parent_len=40)"
+    expected = f"SeqView(seqid=None, parent='ACGTACGTAC...TACGT', slice_record={view.slice_record!r})"
     assert repr(view) == expected
 
-    # Non-zero start, stop, and step values
+    # Non-zero slice record
     seq = "ACGT" * 10
-    view = new_sequence.SeqView(parent=seq, start=5, stop=35, step=2, alphabet=alpha)
-    expected = "SeqView(parent='ACGTACGTAC...TACGT', start=5, stop=35, step=2, offset=0, seqid=None, parent_len=40)"
+    sr = new_sequence.SliceRecord(start=5, stop=35, step=2, parent_len=len(seq))
+    view = new_sequence.SeqView(parent=seq, alphabet=alpha, slice_record=sr)
+    expected = f"SeqView(seqid=None, parent='ACGTACGTAC...TACGT', slice_record={view.slice_record!r})"
     assert repr(view) == expected
 
-    # offset
+    # slice record with an offset
     seq = "ACGT"
-    view = new_sequence.SeqView(parent=seq, offset=5, alphabet=alpha)
-    expected = "SeqView(parent='ACGT', start=0, stop=4, step=1, offset=5, seqid=None, parent_len=4)"
+    sr = new_sequence.SliceRecord(offset=5, parent_len=len(seq))
+    view = new_sequence.SeqView(parent=seq, alphabet=alpha, slice_record=sr)
+    expected = f"SeqView(seqid=None, parent='ACGT', slice_record={view.slice_record!r})"
     assert repr(view) == expected
 
     # seqid
     seq = "ACGT"
     view = new_sequence.SeqView(parent=seq, seqid="seq1", alphabet=alpha)
-    expected = "SeqView(parent='ACGT', start=0, stop=4, step=1, offset=0, seqid='seq1', parent_len=4)"
+    expected = (
+        f"SeqView(seqid='seq1', parent='ACGT', slice_record={view.slice_record!r})"
+    )
     assert repr(view) == expected
 
 
+def test_slice_record_repr():
+    # defaults
+    sr = new_sequence.SliceRecord(parent_len=20)
+    expected = "SliceRecord(start=0, stop=20, step=1, parent_len=20, offset=0)"
+    assert repr(sr) == expected
+
+    sr = new_sequence.SliceRecord(start=1, stop=10, step=2, parent_len=20, offset=5)
+    expected = "SliceRecord(start=1, stop=10, step=2, parent_len=20, offset=5)"
+    assert repr(sr) == expected
+
+
 @pytest.mark.parametrize("k", range(1, 7))
 def test_iter_kmers_returns_generator(k):
     orig = "TCAGGA"
@@ -1899,44 +1931,44 @@ def test_annotation_from_slice_with_stride():
 
 def test_absolute_position_base_cases(one_seq):
     """with no offset or view, the absolute index should remain unchanged"""
-    got = one_seq._seq.absolute_position(5)
+    got = one_seq._seq.slice_record.absolute_position(5)
     assert got == 5
 
     # an index outside the range of the sequence should raise an IndexError
     with pytest.raises(IndexError):
-        one_seq._seq.absolute_position(20)
+        one_seq._seq.slice_record.absolute_position(20)
 
     with pytest.raises(IndexError):
-        one_seq._seq.absolute_position(-20)
+        one_seq._seq.slice_record.absolute_position(-20)
 
 
 def test_absolute_position_positive(one_seq):
     # with an offset, the abs index should be offset + index
-    one_seq._seq.offset = 2
-    got = one_seq._seq.absolute_position(2)
+    one_seq._seq = one_seq._seq.with_offset(2)
+    view = one_seq._seq
+    got = view.slice_record.absolute_position(2)
     assert got == 2 + 2
 
     # with an offset and start, the abs index should be offset + start + index
-    view = one_seq[2::]
-    view._seq.offset = 2  # todo: do we want the annotation_offset to be preserved when slicing? I think yes
-    got = view._seq.absolute_position(2)
+    seq = one_seq[2::]
+    view = seq._seq  # todo: do we want the annotation_offset to be preserved when slicing? I think yes
+    got = view.slice_record.absolute_position(2)
     assert got == 2 + 2 + 2
 
     # with an offset, start and step, the abs index should be offset + start + index * step
-    view = one_seq[2::2]
-    view._seq.offset = 2
-    got = view._seq.absolute_position(2)
+    seq = one_seq[2::2]
+    got = seq._seq.slice_record.absolute_position(2)
     assert got == 2 + 2 + 2 * 2
 
 
 def test_relative_position_base_cases(one_seq):
     """with no offset or view, the absolute index should remain unchanged"""
-    got = one_seq._seq.relative_position(5)
+    got = one_seq._seq.slice_record.relative_position(5)
     assert got == 5
 
     # a -ve index  should raise an IndexError
     with pytest.raises(IndexError):
-        one_seq._seq.relative_position(-5)
+        one_seq._seq.slice_record.relative_position(-5)
 
 
 def test_relative_position(integer_seq):
@@ -1945,11 +1977,11 @@ def test_relative_position(integer_seq):
 
     view = integer_seq[1:9:]
     # view = "12345678"
-    got = view.relative_position(0)
+    got = view.slice_record.relative_position(0)
     # precedes the view, so should return -1
     assert got == -1
     # exceeds the view, but still returns a value
-    got = view.relative_position(10)
+    got = view.slice_record.relative_position(10)
     assert got == 9
 
 
@@ -1961,13 +1993,13 @@ def test_relative_position_step_GT_one(integer_seq):
     # precedes the view, with step > 1
     view = integer_seq[2:7:2]
     # view = "246", precedes the view by 1 step
-    got = view.relative_position(0)
+    got = view.slice_record.relative_position(0)
     assert got == -1
     # precedes the view by 0.5 step, default behaviour is to round up to 0
-    got = view.relative_position(1)
+    got = view.slice_record.relative_position(1)
     assert got == 0
     # exceeds the view by two steps, len(view) + 2 = 4
-    got = view.relative_position(10)
+    got = view.slice_record.relative_position(10)
     assert got == 4
 
 
@@ -1995,12 +2027,12 @@ def test_relative_position_with_remainder(integer_seq):
     a position that is 'stepped over'"""
     view = integer_seq[1:9:2]
     # view = "1357"
-    got = view.relative_position(2)
+    got = view.slice_record.relative_position(2)
     # 2 is stepped over in the view, so we return the index of 3 (which is 1)
     assert got == 1
 
     # setting the arg stop=True will adjust to the largest number, smaller than the given abs value, that is in the view
-    got = view.relative_position(8, stop=True)
+    got = view.slice_record.relative_position(8, stop=True)
     # 8 is excluded from the view, so we return the index of 7 (which is 3)
     assert got == 3
 
@@ -2012,10 +2044,10 @@ def test_relative_position_with_remainder(integer_seq):
 @pytest.mark.parametrize("step", (None, 1, 2))
 def test_absolute_relative_roundtrip(one_seq, value, offset, start, stop, step):
     # a round trip from relative to absolute then from absolute to relative, should return the same value we began with
-    view = one_seq[start:stop:step]
-    view._seq.offset = offset or 0
-    abs_val = view._seq.absolute_position(value)
-    rel_val = view._seq.relative_position(abs_val)
+    seq = one_seq[start:stop:step]
+    seq._seq.with_offset(offset or 0)
+    abs_val = seq._seq.slice_record.absolute_position(value)
+    rel_val = seq._seq.slice_record.relative_position(abs_val)
     assert rel_val == value
 
 
@@ -2029,10 +2061,10 @@ def test_absolute_relative_roundtrip_reverse(
 ):
     # a round trip from relative to absolute then from absolute to relative, should return the same value we began with
     view = integer_seq[start:stop:step]
-    view.offset = offset or 0
-    abs_val = view.absolute_position(value)
-    rel_val = view.relative_position(abs_val)
-    assert view.offset == (offset or 0)
+    view.slice_record.offset = offset or 0
+    abs_val = view.slice_record.absolute_position(value)
+    rel_val = view.slice_record.relative_position(abs_val)
+    assert view.slice_record.offset == (offset or 0)
     assert (view[rel_val]).str_value == view[value].str_value
 
 
@@ -2148,8 +2180,11 @@ def test_seqview_to_rich_dict(coord, dna_alphabet):
     minus = sv[::-1].to_rich_dict()
     plus = plus.pop("init_args")
     minus = minus.pop("init_args")
-    assert plus.pop("seq") == minus.pop("seq")
-    assert plus["step"] == -minus["step"]
+    assert plus.pop("parent") == minus.pop("parent")
+    assert (
+        plus["slice_record"]["init_args"]["step"]
+        == -minus["slice_record"]["init_args"]["step"]
+    )
     assert coord not in plus
     assert coord not in minus
 
@@ -2161,7 +2196,7 @@ def test_sliced_seqview_rich_dict(reverse, dna_alphabet):
     sv = new_sequence.SeqView(parent=parent, alphabet=dna_alphabet)[sl]
     sv = sv[::-1] if reverse else sv
     rd = sv.to_rich_dict()
-    assert rd["init_args"]["seq"] == parent[sl]
+    assert rd["init_args"]["parent"] == parent[sl]
 
 
 @pytest.mark.parametrize(
@@ -2178,13 +2213,16 @@ def test_parent_start_stop(sl, offset, ascii_alphabet):
     data = "0123456789"
     # check our slice matches the expectation for rest of test
     expect = "234" if sl.step > 0 else "432"
-    sv = new_sequence.SeqView(parent=data, alphabet=ascii_alphabet)
-    sv.offset = offset
+    sv = new_sequence.SeqView(parent=data, alphabet=ascii_alphabet, offset=offset)
     sv = sv[sl]
+    assert sv.offset == offset
     assert sv.str_value == expect
     # now check that start / stop are always the same
     # irrespective of step sign
-    assert (sv.parent_start, sv.parent_stop) == (2 + offset, 5 + offset)
+    assert (sv.slice_record.parent_start, sv.slice_record.parent_stop) == (
+        2 + offset,
+        5 + offset,
+    )
 
 
 @pytest.mark.parametrize(
@@ -2203,7 +2241,7 @@ def test_parent_start_stop_limits(sl, ascii_alphabet):
     assert sv.str_value == expect
     # now check that start / stop are always the same
     # irrespective of step sign
-    assert (sv.parent_start, sv.parent_stop) == (0, 10)
+    assert (sv.slice_record.parent_start, sv.slice_record.parent_stop) == (0, 10)
 
 
 @pytest.mark.parametrize("rev", (False, True))
@@ -2216,7 +2254,7 @@ def test_parent_start_stop_empty(rev, ascii_alphabet):
     assert sv.str_value == expect
     # now check that start / stop are always the same
     # irrespective of step sign
-    assert (sv.parent_start, sv.parent_stop) == (0, 0)
+    assert (sv.slice_record.parent_start, sv.slice_record.parent_stop) == (0, 0)
 
 
 @pytest.mark.parametrize("rev", (False, True))
@@ -2232,7 +2270,10 @@ def test_parent_start_stop_singletons(index, rev, ascii_alphabet):
     assert sv.str_value == expect
     # now check that start / stop are always the same
     # irrespective of step sign
-    assert (sv.parent_start, sv.parent_stop) == (index, index + 1)
+    assert (sv.slice_record.parent_start, sv.slice_record.parent_stop) == (
+        index,
+        index + 1,
+    )
 
 
 def test_get_drawable(DATA_DIR):
@@ -2481,8 +2522,8 @@ def test_make_seq_assigns_to_seqview():
 
 def test_empty_seqview_translate_position(dna_alphabet):
     sv = new_sequence.SeqView(parent="", alphabet=dna_alphabet)
-    assert sv.absolute_position(0) == 0
-    assert sv.relative_position(0) == 0
+    assert sv.slice_record.absolute_position(0) == 0
+    assert sv.slice_record.relative_position(0) == 0
 
 
 @pytest.mark.parametrize("start", (None, 0, 1, 10, -1, -10))
@@ -2492,9 +2533,8 @@ def test_empty_seqview_translate_position(dna_alphabet):
 def test_seqview_seq_len_init(start, stop, step, length, dna_alphabet):
     # seq_len is length of seq when None
     seq_data = "A" * length
-    sv = new_sequence.SeqView(
-        parent=seq_data, start=start, stop=stop, step=step, alphabet=dna_alphabet
-    )
+    sr = new_sequence.SliceRecord(start=start, stop=stop, step=step, parent_len=length)
+    sv = new_sequence.SeqView(parent=seq_data, slice_record=sr, alphabet=dna_alphabet)
     expect = len(seq_data)
     # Check property and slot
     assert sv.parent_len == expect
@@ -2523,6 +2563,23 @@ def test_seqview_seq_len_modified_seq(dna_alphabet):
     assert sv.parent_len == len(seq)
 
 
+@pytest.mark.parametrize("offset", [0, 4])
+def test_seqview_with_offset(offset, dna_alphabet):
+    seq = "ACGGTGGGAC"
+    sv = new_sequence.SeqView(parent=seq, alphabet=dna_alphabet)
+    got = sv.with_offset(offset)
+    assert got is not sv
+    assert got.offset == offset
+
+
+@pytest.mark.parametrize("offset", [0, 4])
+def test_seqview_with_offset_fails(offset, dna_alphabet):
+    seq = "ACGGTGGGAC"
+    sv = new_sequence.SeqView(parent=seq, alphabet=dna_alphabet, offset=1)
+    with pytest.raises(ValueError):
+        _ = sv.with_offset(offset)
+
+
 def test_sequence_str_bytes_array():
     data = "ACGGTGGGAC"
     seq = new_moltype.DNA.make_seq(seq=data)