Repository: cogent3/cogent3
Pull request #2107 (2024-11-13 00:33:31)

Performance optimisations
## Summary by Sourcery

Optimize sequence data handling for improved performance and add tests for data type conversions.

Enhancements:
- Improve performance by optimizing the handling of sequence data types in the SeqView class, allowing for more efficient conversion between string, bytes, and array representations.

Tests:
- Add tests to verify the correct conversion of sequence data types to and from indices, ensuring compatibility with different input formats such as strings, bytes, and numpy arrays.


Patch:
diff --git a/src/cogent3/core/moltype.py b/src/cogent3/core/moltype.py
index 86810ec66..eff2c3f7c 100644
--- a/src/cogent3/core/moltype.py
+++ b/src/cogent3/core/moltype.py
@@ -1541,15 +1541,13 @@ def get_moltype(name, new_type: bool = False):
         The default will be changed to True in 2024.12. Support for the old
         style will be removed as of 2025.6.
     """
-    from cogent3.core import new_moltype
-
     if new_type or "COGENT3_NEW_TYPE" in os.environ:
-        return new_moltype.get_moltype(name=name)
+        from cogent3.core.new_moltype import get_moltype as new_get_moltype
+
+        return new_get_moltype(name=name)
+
     if isinstance(name, MolType):
         return name
-    if isinstance(name, new_moltype.MolType):
-        return name
-
     name = name.lower()
     if name not in moltypes:
         raise ValueError(f"unknown moltype {name!r}")
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index f3448fe2e..71ade1e56 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -65,8 +65,6 @@ OptFloat = Optional[float]
 OptStr = Optional[str]
 OptList = Optional[list]
 OptDict = Optional[dict]
-OptBool = Optional[bool]
-OptSliceRecord = Optional[new_sequence.SliceRecord]
 DictStrStr = dict[str, str]
 DictStrInt = dict[str, int]
 OptCallable = Optional[Callable]
@@ -178,6 +176,22 @@ class GapsOk:
         gap_frac = self._get_gap_frac(data)
         return gap_frac >= self.allowed_frac
 
+    def gap_run_ok(self, seq: str) -> bool:
+        """runs of gaps <= allowed_run"""
+        # refactor: design, this is not used in any of the new alignment code
+        curr_run = 0
+        is_gap = self.gap_chars.__contains__
+        result = True
+        for i in seq:
+            if is_gap(i):
+                curr_run += 1
+                if curr_run > self.allowed_run:
+                    result = False
+                    break
+            else:
+                curr_run = 0
+        return result
+
     def __call__(self, data: Union[tuple[str, ...], str]):
         return self._func(data)
 
@@ -333,6 +347,9 @@ class SeqsDataABC(ABC):
     @abstractmethod
     def get_view(self, seqid: str) -> new_sequence.SeqViewABC: ...
 
+    @abstractmethod
+    def subset(self, names: Union[str, typing.Sequence[str]]) -> SeqsDataABC: ...
+
     @abstractmethod
     def to_alphabet(self, alphabet: new_alphabet.AlphabetABC) -> SeqsDataABC: ...
 
@@ -379,6 +396,7 @@ class SeqsData(SeqsDataABC):
         data: dict[str, StrORBytesORArray],
         alphabet: new_alphabet.AlphabetABC,
         offset: dict[str, int] = None,
+        reversed: bool = False,
         check: bool = True,
     ):
         self._alphabet = alphabet
@@ -396,6 +414,8 @@ class SeqsData(SeqsDataABC):
             arr = self._alphabet.to_indices(seq)
             arr.flags.writeable = False
             self._data[str(name)] = arr
+        self._reversed = reversed
+        # refactor: rename reversed argument to not clash with python built-in
 
     @classmethod
     def from_seqs(
@@ -419,6 +439,26 @@ class SeqsData(SeqsDataABC):
     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,
+            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, ... }."""
         return {name: seq.shape[0] for name, seq in self._data.items()}
@@ -442,14 +482,36 @@ class SeqsData(SeqsDataABC):
 
     def get_view(self, seqid: str) -> SeqDataView:
         seq_len = len(self._data[seqid])
+        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,
-            offset=self._offset.get(seqid, 0),
+            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,
+                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 add_seqs(
         self,
         seqs: dict[str, StrORBytesORArray],
@@ -458,6 +520,11 @@ class SeqsData(SeqsDataABC):
     ) -> SeqsData:
         """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 = {
@@ -468,6 +535,7 @@ class SeqsData(SeqsDataABC):
             data=new_data,
             alphabet=self.alphabet,
             offset={**self._offset, **(offset or {})},
+            reversed=self._reversed,
         )
 
     def to_alphabet(
@@ -485,6 +553,7 @@ class SeqsData(SeqsDataABC):
                 data=self._data,
                 alphabet=alphabet,
                 offset=self._offset,
+                reversed=self._reversed,
             )
 
         new_data = {}
@@ -510,6 +579,7 @@ class SeqsData(SeqsDataABC):
             data=new_data,
             alphabet=alphabet,
             offset=self._offset,
+            reversed=self._reversed,
             check=False,
         )
 
@@ -522,9 +592,6 @@ class SeqsData(SeqsDataABC):
 
     @__getitem__.register
     def _(self, index: str) -> new_sequence.SeqViewABC:
-        # refactor: design
-        # note that this will always return the plus strand, even if the collection
-        # has been reversed
         return self.get_view(seqid=index)
 
     @__getitem__.register
@@ -538,6 +605,7 @@ class SeqsData(SeqsDataABC):
                 "data": {name: self.get_seq_str(seqid=name) for name in self.names},
                 "alphabet": self.alphabet.to_rich_dict(),
                 "offset": self._offset,
+                "reversed": self.is_reversed,
             },
             "type": get_object_provenance(self),
             "version": __version__,
@@ -551,6 +619,7 @@ class SeqsData(SeqsDataABC):
             data=data["init_args"]["data"],
             alphabet=alphabet,
             offset=data["init_args"]["offset"],
+            reversed=data["init_args"]["reversed"],
         )
 
 
@@ -560,41 +629,16 @@ def deserialise_seqs_data(data: dict[str, str | dict[str, str]]) -> SeqsData:
 
 
 class SequenceCollection:
-    """A container of unaligned sequences"""
-
     def __init__(
         self,
         *,
         seqs_data: SeqsDataABC,
         moltype: new_moltype.MolType,
+        name_map: OptDict = None,
         info: Optional[Union[dict, InfoClass]] = None,
         source: OptPathType = None,
         annotation_db: Optional[SupportsFeatures] = None,
-        name_map: OptDict = None,
-        is_reversed: bool = False,
     ):
-        """Initialises a new SequenceCollection.
-
-        Parameters
-        ----------
-        seqs_data
-            a SeqsDataABC instance containg the sequence data
-        moltype
-            the molecular type of the sequences
-        info
-            additional information about the collection
-        source
-            the source of the sequence data
-        annotation_db
-            a database of annotations for the sequences
-        name_map
-            map between the names specified in the collection and names used in
-            the underlying seqs_data. Used for when the names have been changed,
-            but we want to query for annotations using the original names.
-        is_reversed
-            flag indicating the sequences are reversed with respect to the
-            underlying data in seqs_data
-        """
         self._seqs_data = seqs_data
         self.moltype = moltype
         self._name_map = name_map or {name: name for name in seqs_data.names}
@@ -605,7 +649,6 @@ class SequenceCollection:
         self._repr_policy = dict(num_seqs=10, num_pos=60, ref_name="longest", wrap=60)
         self._annotation_db = annotation_db or DEFAULT_ANNOTATION_DB()
         self._seqs = None
-        self._is_reversed = is_reversed
         self._post_init()
 
     def _post_init(self):
@@ -615,27 +658,11 @@ class SequenceCollection:
     def _make_seq(self, name: str) -> new_sequence.Sequence:
         # seqview is given the name of the parent (if different from the current name)
         # the sequence is given the current name
-        seqid = self._name_map.get(name, name)
-        sv = self._seqs_data.get_view(seqid)
-        sv = sv[::-1] if self._is_reversed else sv
+        sv = self._seqs_data.get_view(self._name_map.get(name, name))
         seq = self.moltype.make_seq(seq=sv, name=name)
         seq.replace_annotation_db(self.annotation_db)
         return seq
 
-    def _get_init_kwargs(self) -> dict:
-        """dict of all the arguments needed to initialise a new instance"""
-        # both SequenceCollection and Alignment implement _get_init_kwargs,
-        # ensuring methods in SequenceCollection that are inherited by Alignment
-        # capture initialisation arguments unique to the subclass.
-        return {
-            "seqs_data": self._seqs_data,
-            "moltype": self.moltype,
-            "name_map": self._name_map,
-            "info": self.info,
-            "annotation_db": self.annotation_db,
-            "is_reversed": self._is_reversed,
-        }
-
     @property
     def seqs(self) -> _IndexableSeqs:
         return self._seqs
@@ -728,10 +755,14 @@ class SequenceCollection:
 
         selected_name_map = {name: self._name_map[name] for name in names}
 
-        init_kwargs = self._get_init_kwargs()
-        init_kwargs["name_map"] = selected_name_map
-
-        result = self.__class__(**init_kwargs)
+        result = self.__class__(
+            seqs_data=self._seqs_data,
+            moltype=self.moltype,
+            name_map=selected_name_map,
+            info=self.info,
+            source=self.source,
+            **kwargs,
+        )
         if self.annotation_db:
             if copy_annotations:
                 result.annotation_db = type(self.annotation_db)()
@@ -834,11 +865,13 @@ class SequenceCollection:
         }
         if len(new_name_map) != len(self._name_map):
             raise ValueError(f"non-unique names produced by {renamer=}")
-
-        init_args = self._get_init_kwargs()
-        init_args["name_map"] = new_name_map
-
-        return self.__class__(**init_args)
+        return self.__class__(
+            seqs_data=self._seqs_data,
+            moltype=self.moltype,
+            name_map=new_name_map,
+            info=self.info,
+            annotation_db=self.annotation_db,
+        )
 
     def to_dict(self, as_array: bool = False) -> dict[str, Union[str, numpy.ndarray]]:
         """Return a dictionary of sequences.
@@ -921,18 +954,21 @@ class SequenceCollection:
             seq = self._seqs_data.get_seq_array(seqid=self._name_map.get(name, name))
             data[name] = self.moltype.degap(seq)
 
-        init_kwargs = self._get_init_kwargs()
-        init_kwargs.pop("annotation_db", None)
-        init_kwargs["seqs_data"] = self._seqs_data.from_seqs(
+        seqs_data = self._seqs_data.from_seqs(
             data=data,
             alphabet=self._seqs_data.alphabet,
             offset=self._seqs_data.offset,
+            reversed=self._seqs_data.is_reversed,
             check=False,
         )
+        return self.__class__(
+            seqs_data=seqs_data,
+            moltype=self.moltype,
+            info=self.info,
+            source=self.source,
+        )
 
-        return self.__class__(**init_kwargs)
-
-    def to_moltype(self, moltype: MolTypes) -> SequenceCollection:
+    def to_moltype(self, moltype: str) -> SequenceCollection:
         """returns copy of self with changed moltype
 
         Parameters
@@ -957,11 +993,13 @@ class SequenceCollection:
                 f"Failed to convert moltype from {self.moltype.label} to {moltype}"
             ) from e
 
-        init_kwargs = self._get_init_kwargs()
-        init_kwargs["seqs_data"] = new_seqs_data
-        init_kwargs["moltype"] = mtype
-
-        return self.__class__(**init_kwargs)
+        return self.__class__(
+            seqs_data=new_seqs_data,
+            moltype=mtype,
+            info=self.info,
+            source=self.source,
+            annotation_db=self.annotation_db,
+        )
 
     def to_dna(self):
         """returns copy of self as a collection of DNA moltype seqs"""
@@ -1017,7 +1055,7 @@ class SequenceCollection:
                 include_stop=include_stop,
                 trim_stop=trim_stop,
             )
-            translated[seq._seq.seqid] = numpy.array(pep)
+            translated[seq.name] = numpy.array(pep)
 
         pep_moltype = new_moltype.get_moltype(
             "protein_with_stop" if include_stop else "protein"
@@ -1026,28 +1064,42 @@ class SequenceCollection:
             data=translated,
             alphabet=pep_moltype.most_degen_alphabet(),
             offset=self._seqs_data.offset,
+            reversed=False,  # reversed not meaningful for a protein sequence
         )
         return self.__class__(
             seqs_data=seqs_data,
             moltype=pep_moltype,
-            name_map=self._name_map,
             info=self.info,
             source=self.source,
-            is_reversed=False,  # reversed not meaningful for a protein
             **kwargs,
         )
 
     def rc(self):
         """Returns the reverse complement of all sequences in the collection.
         A synonym for reverse_complement.
+
+        Notes
+        -----
+        Reverse complementing the collection will break the relationship to an
+        annotation_db if present.
         """
-        init_kwargs = self._get_init_kwargs()
-        init_kwargs["is_reversed"] = not self._is_reversed
-        return self.__class__(**init_kwargs)
+        return self.__class__(
+            seqs_data=self._seqs_data.reverse(),
+            name_map=self._name_map,
+            info=self.info,
+            moltype=self.moltype,
+            source=self.source,
+        )
 
     def reverse_complement(self):
         """Returns the reverse complement of all sequences in the collection.
         A synonym for rc.
+
+        Notes
+        -----
+        Reverse complementing the collection will break the relationship to an
+        annotation_db if present.
+
         """
         return self.rc()
 
@@ -1509,15 +1561,20 @@ class SequenceCollection:
             for s in self.seqs
         }
 
-        init_kwargs = self._get_init_kwargs()
-        init_kwargs.pop("annotation_db", None)
-        init_kwargs["seqs_data"] = self._seqs_data.from_seqs(
+        seqs_data = self._seqs_data.from_seqs(
             data=new_seqs,
             alphabet=self._seqs_data.alphabet,
             offset=self._seqs_data.offset,
+            reversed=self._seqs_data.is_reversed,
             check=False,
         )
-        result = self.__class__(**init_kwargs)
+        result = self.__class__(
+            seqs_data=seqs_data,
+            name_map=self._name_map,
+            moltype=self.moltype,
+            info=self.info,
+            source=self.source,
+        )
         if self.annotation_db:
             result.annotation_db = self.annotation_db
         return result
@@ -1771,7 +1828,7 @@ class SequenceCollection:
         return counts.row_sum()
 
     def pad_seqs(self, pad_length: OptInt = None):
-        """Returns copy in which sequences are padded with the gap character to same length.
+        """Returns copy in which sequences are padded to same length.
 
         Parameters
         ----------
@@ -1785,33 +1842,29 @@ class SequenceCollection:
         if pad_length is None:
             pad_length = max_len
         elif pad_length < max_len:
-            pad_length = max_len
-
-        padded_seqs = {}
-        for seq in self.seqs:
-            padded_seq = numpy.full(
-                shape=pad_length,
-                fill_value=self.moltype.gapped_alphabet.gap_index,
-                dtype=self.moltype.most_degen_alphabet().dtype,
+            raise ValueError(
+                f"pad_length must be at greater or equal to maximum sequence length: {str(max_len)}"
             )
-            padded_seq[: len(seq)] = numpy.array(seq)
-            # the padded_seqs dict will be used to create the seqs_data, so the
-            # keys should be the seqids from the original seqs_data, if this differs
-            # from the seq name, this will be recorded in the name_map
-            parent_name = seq._seq.seqid
-            padded_seqs[parent_name] = padded_seq
-
-        init_kwargs = self._get_init_kwargs()
-        # when we access .seqs, if the collection has been reversed, this will have
-        # been applied to the returned Sequence, so we reset it to False for the
-        # returned collection
-        init_kwargs["is_reversed"] = False
-        init_kwargs["seqs_data"] = self._seqs_data.from_seqs(
-            data=padded_seqs,
+
+        new_seqs = {}
+        for seq_name in self._name_map.values():
+            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_data.from_seqs(
+            data=new_seqs,
             alphabet=self._seqs_data.alphabet,
             offset=self._seqs_data.offset,
+            reversed=self._seqs_data.is_reversed,
+        )
+        return self.__class__(
+            seqs_data=seqs_data,
+            moltype=self.moltype,
+            info=self.info,
+            source=self.source,
+            annotation_db=self.annotation_db,
         )
-        return self.__class__(**init_kwargs)
 
     def strand_symmetry(self, motif_length: int = 1):
         """returns dict of strand symmetry test results per seq"""
@@ -2352,7 +2405,7 @@ def prep_for_seqs_data(data, moltype: new_moltype.MolType, seq_namer: _SeqNamer)
 def _(data: dict, moltype: new_moltype.MolType, seq_namer: _SeqNamer) -> CT:
     seqs = {}  # for the (Aligned)SeqsDataABC
     offsets = {}  # for the (Aligned)SeqsDataABC
-    rvd = 0
+    is_reversed = 0
     name_map = {}  # for the sequence collection
     # if we have a dict of Sequences, {name: seq, ...}, then the provided names
     # may differ to the name attribute on the Sequences. If the Sequences have
@@ -2365,10 +2418,10 @@ def _(data: dict, moltype: new_moltype.MolType, seq_namer: _SeqNamer) -> CT:
         seq_data = coerce_to_raw_seq_data(seq, moltype, name=name)
         offsets[name] = seq_data.offset
         seqs[seq_data.parent_name or seq_data.name] = seq_data.seq
-        rvd += 1 if seq_data.is_reversed else 0
+        is_reversed += 1 if seq_data.is_reversed else 0
         name_map[name] = seq_data.parent_name or name
 
-    return seqs, offsets, rvd, name_map
+    return seqs, offsets, is_reversed, name_map
 
 
 @prep_for_seqs_data.register
@@ -2409,7 +2462,6 @@ def make_unaligned_seqs(
     annotation_db: SupportsFeatures = None,
     offset: typing.Optional[DictStrInt] = None,
     name_map: typing.Optional[DictStrStr] = None,
-    is_reversed: OptBool = None,
 ) -> SequenceCollection:
     """Initialise an unaligned collection of sequences.
 
@@ -2433,9 +2485,6 @@ def make_unaligned_seqs(
     name_map
         a dict mapping sequence names to "parent" sequence names. The parent
         name will be used for querying a annotation_db.
-    is_reversed
-        flag indicating whether the sequences are reversed with respect to the
-        underlying data.
 
     Notes
     -----
@@ -2444,6 +2493,13 @@ def make_unaligned_seqs(
     If the sequences are annotated AND an annotation_db is provided, only the
     annotation_db is used.
     """
+    # refactor: design/simplify
+    # the dispatches above handle the different type of data that the previous
+    # make_unaligned_seqs handled. This includes dicts, lists (where items are
+    # either pairs of [name, seq], or just seqs), tuples, Sequences, etc.
+    # We could simplify it greatly by only supporting dicts, SeqsDataABC,
+    # or SequenceCollections.
+
     # refactor: design
     # rename offset to offsets as it could track potentially multiple offsets
 
@@ -2451,10 +2507,6 @@ def make_unaligned_seqs(
     # define a source attribute rather than storing as .info["source"]
     # this will also replace the previous name attribute
 
-    # refactor: design
-    # currently reversal can only be applied to the entire collection.
-    # This should be made more flexible.
-
     moltype = new_moltype.get_moltype(moltype)
     alphabet = moltype.most_degen_alphabet()
 
@@ -2470,13 +2522,8 @@ def make_unaligned_seqs(
     seqs_data, offs, rvd, nm = prep_for_seqs_data(data, moltype, assign_names)
     # rvd is the count of sequences that have been reversed. It should be equal 0
     # or the number of sequences. If inbetween, the sequences are a mix of the two
-    # which for now we do not support. However, if the user has provided a value
-    # for is_reversed, we will use that.
-    if is_reversed is not None:
-        rvd = is_reversed
-    elif rvd in {0, len(seqs_data)}:
-        rvd = bool(rvd)
-    else:
+    # which for now we do not support.
+    if 0 < rvd < len(seqs_data):
         rvd = False
         if annotation_db and len(annotation_db) > 0:
             warnings.warn(
@@ -2487,7 +2534,7 @@ def make_unaligned_seqs(
 
     name_map = nm if name_map is None else name_map
     # seqs_data keys should be the same as the value of name_map, not the keys
-    seqs_data = SeqsData(data=seqs_data, alphabet=alphabet, offset=offset)
+    seqs_data = SeqsData(data=seqs_data, alphabet=alphabet, offset=offset, reversed=rvd)
     # we do not pass on offset/label_to_name as they are handled in this function
     return make_unaligned_seqs(
         seqs_data,
@@ -2496,7 +2543,6 @@ def make_unaligned_seqs(
         source=source,
         annotation_db=annotation_db,
         name_map=name_map,
-        is_reversed=rvd,
     )
 
 
@@ -2511,7 +2557,6 @@ def _(
     annotation_db: SupportsFeatures = None,
     offset: dict[str, int] = None,
     name_map: dict[str, str] = None,
-    is_reversed: OptBool = None,
 ) -> SequenceCollection:
     moltype = new_moltype.get_moltype(moltype)
     if not moltype.is_compatible_alphabet(data.alphabet):
@@ -2532,7 +2577,6 @@ def _(
         annotation_db=annotation_db,
         source=source,
         name_map=name_map,
-        is_reversed=is_reversed or False,
     )
     if label_to_name:
         seqs = seqs.rename_seqs(label_to_name)
@@ -2698,7 +2742,7 @@ class Aligned:
     @property
     def gapped_seq(self) -> new_sequence.Sequence:
         """Returns Sequence object, including gaps."""
-        seq = self.data.gapped_str_value
+        seq = self.data.gapped_array_value
         if self.data.slice_record.step < 0:
             seq = self.moltype.complement(seq)
         return self.moltype.make_seq(seq=seq, name=self.data.seqid)
@@ -3136,8 +3180,6 @@ class AlignedSeqsData(AlignedSeqsDataABC):
 
     @__getitem__.register
     def _(self, index: str):
-        # refactor: design
-        # this view will return the whole sequence, even if the alignment is sliced
         return self.get_view(index)
 
     @__getitem__.register
@@ -3151,16 +3193,17 @@ class AlignedSeqsData(AlignedSeqsDataABC):
         return {name: len(seq) for name, seq in self._seqs.items()}
 
     @singledispatchmethod
-    def get_view(self, seqid: str):
+    def get_view(self, seqid: str, slice_record: new_sequence.SliceRecord = None):
         return AlignedDataView(
             parent=self,
             seqid=seqid,
             alphabet=self.alphabet,
+            slice_record=slice_record,
         )
 
     @get_view.register
-    def _(self, seqid: int):
-        return self.get_view(self.names[seqid])
+    def _(self, seqid: int, slice_record: new_sequence.SliceRecord = None):
+        return self.get_view(self.names[seqid], slice_record)
 
     def get_gaps(self, seqid: str) -> numpy.ndarray:
         return self._gaps[seqid]
@@ -3226,8 +3269,6 @@ class AlignedSeqsData(AlignedSeqsDataABC):
 
         # convert from alignment coordinates to sequences coordinates
         # selecting only the indices that are in the frame of the stride
-        # refactor: design
-        # only select indices if we have values for start/stop/step
         indices = []
         for span in indel_map.nongap():
             indices += [
@@ -3372,6 +3413,28 @@ class AlignedSeqsData(AlignedSeqsDataABC):
             align_len=self.align_len,
         )
 
+    def subset(self, names: Union[str, typing.Sequence[str]]):
+        """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,
+                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, check_valid: bool = True):
         """Returns a new AlignedSeqsData object with the same underlying data
         with a new alphabet."""
@@ -3455,7 +3518,7 @@ class AlignedDataView(new_sequence.SeqViewABC):
         parent: AlignedSeqsDataABC,
         seqid: str,
         alphabet: new_alphabet.AlphabetABC,
-        slice_record: OptSliceRecord = None,
+        slice_record: new_sequence.SliceRecord = None,
     ):
         self.parent = parent
         self._seqid = seqid
@@ -3730,11 +3793,10 @@ class _IndexableSeqs:
 
 
 class Alignment(SequenceCollection):
-    # refactor: docstring
     def __init__(
         self,
         seqs_data: AlignedSeqsDataABC,  # seqs_data
-        slice_record: OptSliceRecord = None,
+        slice_record: new_sequence.SliceRecord = None,
         **kwargs,
     ):
         super().__init__(seqs_data=seqs_data, **kwargs)
@@ -3748,17 +3810,6 @@ class Alignment(SequenceCollection):
     def _post_init(self):
         self._seqs = _IndexableSeqs(self, make_seq=self._make_aligned)
 
-    def _get_init_kwargs(self) -> dict:
-        """returns the kwargs needed to re-instantiate the object"""
-        return {
-            "seqs_data": self._seqs_data,
-            "moltype": self.moltype,
-            "name_map": self._name_map,
-            "info": self.info,
-            "annotation_db": self.annotation_db,
-            "slice_record": self._slice_record,
-        }
-
     @singledispatchmethod
     def __getitem__(self, index):
         raise NotImplementedError(f"__getitem__ not implemented for {type(index)}")
@@ -3821,8 +3872,11 @@ class Alignment(SequenceCollection):
         return self.array_seqs
 
     def _make_aligned(self, seqid: str) -> Aligned:
-        data = self._seqs_data.get_view(self._name_map.get(seqid, seqid))
-        data.slice_record = self._slice_record
+        # refactor: design
+        # add slice_record as an argument to the get_view() method
+        data = self._seqs_data.get_view(
+            self._name_map.get(seqid, seqid), self._slice_record
+        )
         return Aligned(data=data, moltype=self.moltype, name=seqid)
 
     @property
@@ -3891,10 +3945,20 @@ class Alignment(SequenceCollection):
     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.
         """
-        init_kwargs = self._get_init_kwargs()
-        init_kwargs["slice_record"] = self._slice_record[::-1]
-        return self.__class__(**init_kwargs)
+        return self.__class__(
+            seqs_data=self._seqs_data,
+            slice_record=self._slice_record[::-1],
+            name_map=self._name_map,
+            info=self.info,
+            moltype=self.moltype,
+            source=self.source,
+        )
 
     def alignment_quality(self, app_name: str = "ic_score", **kwargs):
         """
@@ -3925,9 +3989,14 @@ class Alignment(SequenceCollection):
         if len(new_name_map) != len(self._name_map):
             raise ValueError(f"non-unique names produced by {renamer=}")
 
-        init_kwargs = self._get_init_kwargs()
-        init_kwargs["name_map"] = new_name_map
-        new = self.__class__(**init_kwargs)
+        new = self.__class__(
+            seqs_data=self._seqs_data,
+            moltype=self.moltype,
+            name_map=new_name_map,
+            slice_record=self._slice_record,
+            info=self.info,
+            annotation_db=self.annotation_db,
+        )
 
         if self._array_seqs is not None:
             new._array_seqs = self._array_seqs
@@ -3987,14 +4056,13 @@ class Alignment(SequenceCollection):
         negate
             if True, all columns except those in cols are kept
         """
-        # refactor: array - use array operations throughout method
         if negate:
             col_lookup = dict.fromkeys(cols)
             cols = [i for i in range(len(self)) if i not in col_lookup]
 
         new_data = {
-            aligned.data.seqid: numpy.array(aligned.gapped_seq)[cols]
-            for aligned in self.seqs
+            seqid: self._seqs_data.get_gapped_seq_array(seqid=seqid)[cols]
+            for seqid in self.names
         }
         seqs_data = self._seqs_data.from_seqs(
             data=new_data, alphabet=self.moltype.most_degen_alphabet()
@@ -4020,16 +4088,16 @@ class Alignment(SequenceCollection):
             included
         """
         result = numpy.full((self.num_seqs, len(self)), False, dtype=bool)
-        for i, aligned in enumerate(self.seqs):
+        for i, seqid in enumerate(self._name_map.values()):
             if include_ambiguity:
-                seq_array = numpy.array(aligned)
+                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(aligned.data.seqid)
+                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
@@ -4427,18 +4495,12 @@ class Alignment(SequenceCollection):
             info=self.info,
         )
 
-    def matching_ref(self, ref_name: str, gap_fraction: float, gap_run: int):
+    def matching_ref(self, ref_name, gap_fraction, gap_run):
         """Returns new alignment with seqs well aligned with a reference.
 
-        Parameters
-        ----------
-        ref_name
-            name of the sequence to use as the reference
-        gap_fraction
-            fraction of positions that either have a gap in the
+        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
+        gap_run = number of consecutive gaps tolerated in query relative to
             sequence or sequence relative to query
         """
         template = self.seqs[ref_name]
@@ -4676,9 +4738,9 @@ class Alignment(SequenceCollection):
             )
 
         new_seqs = {}
-        for aligned in self.seqs:
-            sampled = numpy.array(aligned)[positions]
-            new_seqs[aligned.data.seqid] = sampled
+        for seqid in self.names:
+            sampled = self._seqs_data.get_gapped_seq_array(seqid=seqid)[positions]
+            new_seqs[seqid] = sampled
 
         new_seqs_data = self._seqs_data.from_seqs(
             data=new_seqs, alphabet=self.moltype.most_degen_alphabet()
@@ -4687,117 +4749,6 @@ class Alignment(SequenceCollection):
             seqs_data=new_seqs_data, info=self.info, moltype=self.moltype
         )
 
-    def distance_matrix(
-        self,
-        calc: str = "pdist",
-        show_progress: bool = False,
-        drop_invalid: bool = False,
-    ):
-        """Returns pairwise distances between sequences.
-
-        Parameters
-        ----------
-        calc
-            a pairwise distance calculator or name of one. For options see
-            cogent3.evolve.fast_distance.available_distances
-        show_progress
-            controls progress display for distance calculation
-        drop_invalid
-            If True, sequences for which a pairwise distance could not be
-            calculated are excluded. If False, an ArithmeticError is raised if
-            a distance could not be computed on observed data.
-        """
-        from cogent3.evolve.fast_distance import get_distance_calculator
-
-        try:
-            calculator = get_distance_calculator(
-                calc,
-                moltype=self.moltype,
-                alignment=self,
-                invalid_raises=not drop_invalid,
-            )
-            calculator.run(show_progress=show_progress)
-        except ArithmeticError as e:
-            msg = "not all pairwise distances could be computed, try drop_invalid=True"
-            raise ArithmeticError(msg) from e
-        result = calculator.get_pairwise_distances()
-        if drop_invalid:
-            result = result.drop_invalid()
-        return result
-
-    @UI.display_wrap
-    def quick_tree(
-        self,
-        calc: str = "pdist",
-        bootstrap: OptInt = None,
-        drop_invalid: bool = False,
-        show_progress: bool = False,
-        ui=None,  # refactor: type hint
-    ):
-        """Returns a phylogenetic tree.
-
-        Parameters
-        ----------
-        calc
-            a pairwise distance calculator or name of one. For options see
-            cogent3.evolve.fast_distance.available_distances
-        bootstrap
-            Number of non-parametric bootstrap replicates. Resamples alignment
-            columns with replacement and builds a phylogeny for each such
-            resampling.
-        drop_invalid
-            If True, sequences for which a pairwise distance could not be
-            calculated are excluded. If False, an ArithmeticError is raised if
-            a distance could not be computed on observed data.
-        show_progress
-            controls progress display for distance calculation
-
-        Returns
-        -------
-        a phylogenetic tree. If bootstrap specified, returns the weighted
-        majority consensus. Support for each node is stored as
-        edge.params['params'].
-
-        Notes
-        -----
-        Sequences in the observed alignment for which distances could not be
-        computed are omitted. Bootstrap replicates are required to have
-        distances for all seqs present in the observed data distance matrix.
-        """
-        from cogent3.phylo.consensus import weighted_majority_rule
-        from cogent3.phylo.nj import gnj
-
-        dm = self.distance_matrix(
-            calc=calc, show_progress=show_progress, drop_invalid=drop_invalid
-        )
-        results = gnj(dm, keep=1, show_progress=show_progress)
-        kept_names = set(dm.template.names[0])
-        if bootstrap:
-            subaln = (
-                self.take_seqs(kept_names) if set(self.names) != kept_names else self
-            )
-            for _ in ui.series(range(bootstrap), count=bootstrap, noun="bootstrap"):
-                b = subaln.sample(with_replacement=True)
-                try:
-                    bdist = b.distance_matrix(
-                        calc=calc, show_progress=show_progress, drop_invalid=False
-                    )
-                except ArithmeticError:
-                    # all sequences must have a pairwise distance to allow
-                    # constructing a consensus tree
-                    continue
-                bresult = gnj(bdist, keep=1, show_progress=show_progress)
-                results.extend(bresult)
-
-            consense = weighted_majority_rule(results)
-            assert len(consense) == 1
-            results = consense
-
-        tree = results[0]
-        if not bootstrap:
-            tree = tree[1]
-        return tree
-
     def trim_stop_codons(self, gc: Any = None, strict: bool = False, **kwargs):
         # refactor: array
         if not self.has_terminal_stop(gc=gc, strict=strict):
@@ -4829,6 +4780,29 @@ class Alignment(SequenceCollection):
 
         return result
 
+    @extend_docstring_from(SequenceCollection.to_moltype)
+    def to_moltype(self, moltype: str):
+        mtype = new_moltype.get_moltype(moltype)
+        if mtype is self.moltype:
+            return self  # nothing to be done
+
+        alpha = mtype.most_degen_alphabet()
+        try:
+            new_seqs_data = self._seqs_data.to_alphabet(alpha)
+        except new_moltype.MolTypeError as e:
+            raise new_moltype.MolTypeError(
+                f"Failed to convert moltype from {self.moltype.label} to {moltype}"
+            ) from e
+
+        return self.__class__(
+            seqs_data=new_seqs_data,
+            moltype=mtype,
+            info=self.info,
+            source=self.source,
+            slice_record=self._slice_record,
+            annotation_db=self.annotation_db,
+        )
+
     @extend_docstring_from(SequenceCollection.get_translation)
     def get_translation(
         self,
@@ -5638,7 +5612,6 @@ def make_aligned_seqs(
     annotation_db: typing.Optional[SupportsFeatures] = None,
     offset: typing.Optional[DictStrInt] = None,
     name_map: typing.Optional[DictStrStr] = None,
-    is_reversed: OptBool = None,
 ) -> Alignment:
     if len(data) == 0:
         raise ValueError("data must be at least one sequence.")
@@ -5654,13 +5627,8 @@ def make_aligned_seqs(
     seqs_data, offs, rvd, nm = prep_for_seqs_data(data, moltype, assign_names)
     # rvd is the count of sequences that have been reversed. It should be equal 0
     # or the number of sequences. If inbetween, the sequences are a mix of the two
-    # which for now we do not support. However, if the user has provided a value
-    # for is_reversed, we will use that.
-    if is_reversed is not None:
-        rvd = is_reversed
-    elif rvd in {0, len(seqs_data)}:
-        rvd = bool(rvd)
-    else:
+    # which for now we do not support.
+    if 0 < rvd < len(seqs_data):
         rvd = False
         if annotation_db and len(annotation_db) > 0:
             warnings.warn(
@@ -5684,7 +5652,6 @@ def make_aligned_seqs(
         annotation_db=annotation_db,
         source=source,
         name_map=name_map,
-        is_reversed=rvd,
     )
 
 
@@ -5699,7 +5666,6 @@ def _(
     annotation_db: typing.Optional[SupportsFeatures] = None,
     offset: typing.Optional[DictStrInt] = None,
     name_map: typing.Optional[DictStrStr] = None,
-    is_reversed: OptBool = None,
 ) -> Alignment:
     moltype = new_moltype.get_moltype(moltype)
     if not moltype.is_compatible_alphabet(data.alphabet):
@@ -5715,11 +5681,6 @@ def _(
     info = info if isinstance(info, dict) else {}
     info["source"] = str(source) if source else str(info.get("source", "unknown"))
 
-    sr = (
-        new_sequence.SliceRecord(parent_len=data.align_len, step=-1)
-        if is_reversed
-        else None
-    )
     aln = Alignment(
         seqs_data=data,
         moltype=moltype,
@@ -5727,7 +5688,6 @@ def _(
         source=source,
         annotation_db=annotation_db,
         name_map=name_map,
-        slice_record=sr,
     )
     if label_to_name:
         aln = aln.rename_seqs(label_to_name)
diff --git a/src/cogent3/core/new_alphabet.py b/src/cogent3/core/new_alphabet.py
index 10ab88348..832dd782f 100644
--- a/src/cogent3/core/new_alphabet.py
+++ b/src/cogent3/core/new_alphabet.py
@@ -107,7 +107,7 @@ class AlphabetABC(ABC):
     def with_gap_motif(self): ...
 
     @abstractmethod
-    def to_indices(self, seq: str) -> numpy.ndarray: ...
+    def to_indices(self, seq: StrORBytesORArray) -> numpy.ndarray: ...
 
     @abstractmethod
     def from_indices(self, seq: numpy.ndarray) -> str: ...
@@ -329,9 +329,17 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
         return seq.astype(self.dtype)
 
     @functools.singledispatchmethod
-    def from_indices(self, seq: numpy.ndarray) -> str:
+    def from_indices(self, seq: StrORBytesORArray) -> str:
         raise TypeError(f"{type(seq)} is invalid")
 
+    @from_indices.register
+    def _(self, seq: str) -> str:
+        return seq
+
+    @from_indices.register
+    def _(self, seq: bytes) -> str:
+        return seq.decode("utf8")
+
     @from_indices.register
     def _(self, seq: numpy.ndarray) -> str:
         return self._arr2bytes(seq).decode("utf8")
@@ -750,7 +758,7 @@ class KmerAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
         return monomers.get_kmer_alphabet(self.k, include_gap=True)
 
     @functools.singledispatchmethod
-    def to_index(self, seq) -> numpy.ndarray:
+    def to_index(self, seq) -> int:
         """encodes a k-mer as a single integer
 
         Parameters
@@ -771,17 +779,17 @@ class KmerAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
         raise TypeError(f"{type(seq)} not supported")
 
     @to_index.register
-    def _(self, seq: str) -> numpy.ndarray:
+    def _(self, seq: str) -> int:
         seq = self.monomers.to_indices(seq)
         return self.to_index(seq)
 
     @to_index.register
-    def _(self, seq: bytes) -> numpy.ndarray:
+    def _(self, seq: bytes) -> int:
         seq = self.monomers.to_indices(seq)
         return self.to_index(seq)
 
     @to_index.register
-    def _(self, seq: numpy.ndarray) -> numpy.ndarray:
+    def _(self, seq: numpy.ndarray) -> int:
         assert len(seq) == self.k
         if (self.monomers.num_canonical > seq).all():
             return coord_to_index(seq, self._coeffs)
diff --git a/src/cogent3/core/new_moltype.py b/src/cogent3/core/new_moltype.py
index af9151ccd..5ffa42089 100644
--- a/src/cogent3/core/new_moltype.py
+++ b/src/cogent3/core/new_moltype.py
@@ -575,7 +575,8 @@ class MolType:
             assert self.is_valid(
                 seq
             ), f"{seq[:4]!r} not valid for moltype {self.name!r}"
-        return self._make_seq(moltype=self, seq=seq or "", name=name, **kwargs)
+        seq = "" if seq is None else seq
+        return self._make_seq(moltype=self, seq=seq, name=name, **kwargs)
 
     @functools.singledispatchmethod
     def complement(self, seq: StrORBytesORArray, validate: bool = True) -> str:
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index 9b53a5329..2fbe71ed0 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -2597,7 +2597,7 @@ class SeqView(SeqViewABC):
     def __init__(
         self,
         *,
-        parent: typing.Union[str, "SeqsDataABC"],
+        parent: StrORBytesORArray,
         alphabet: new_alphabet.AlphabetABC,
         seqid: OptStr = None,
         parent_len: OptInt = None,
@@ -2640,15 +2640,24 @@ class SeqView(SeqViewABC):
             "slice_record": self.slice_record,
         }
 
+    # DESIGN NOTE
+    # alphabet.to_indices() and alphabet.from_indices() cope with most primitive
+    # types that parent may be
     @property
     def str_value(self):
-        return self.parent[
-            self.slice_record.start : self.slice_record.stop : self.slice_record.step
-        ]
+        return self.alphabet.from_indices(
+            self.parent[
+                self.slice_record.start : self.slice_record.stop : self.slice_record.step
+            ]
+        )
 
     @property
     def array_value(self):
-        return self.alphabet.to_indices(self.str_value)
+        return self.alphabet.to_indices(
+            self.parent[
+                self.slice_record.start : self.slice_record.stop : self.slice_record.step
+            ]
+        )
 
     @property
     def bytes_value(self):
@@ -2745,7 +2754,9 @@ class SeqView(SeqViewABC):
 
 
 @singledispatch
-def _coerce_to_seqview(data, seqid, alphabet, offset) -> SeqViewABC:
+def _coerce_to_seqview(
+    data, seqid: str, alphabet: new_alphabet.AlphabetABC, offset: int
+) -> SeqViewABC:
     from cogent3.core.alignment import Aligned
     from cogent3.core.sequence import Sequence as old_Sequence
     from cogent3.core.sequence import SeqView as old_SeqView
@@ -2756,7 +2767,9 @@ def _coerce_to_seqview(data, seqid, alphabet, offset) -> SeqViewABC:
 
 
 @_coerce_to_seqview.register
-def _(data: SeqViewABC, seqid, alphabet, offset) -> SeqViewABC:
+def _(
+    data: SeqViewABC, seqid: str, alphabet: new_alphabet.AlphabetABC, offset: int
+) -> SeqViewABC:
     # we require the indexes of shared states in alphabets to be the same
     # SeqView has an alphabet but SeqViewABC does NOT because that is
     # more general and covers the case where the SeqsData collection has the
@@ -2778,26 +2791,48 @@ def _(data: SeqViewABC, seqid, alphabet, offset) -> SeqViewABC:
 
 
 @_coerce_to_seqview.register
-def _(data: Sequence, seqid, alphabet, offset) -> SeqViewABC:
+def _(
+    data: Sequence, seqid: str, alphabet: new_alphabet.AlphabetABC, offset: int
+) -> SeqViewABC:
     return _coerce_to_seqview(data._seq, seqid, alphabet, offset)
 
 
 @_coerce_to_seqview.register
-def _(data: str, seqid, alphabet, offset) -> SeqViewABC:
+def _(
+    data: str, seqid: str, alphabet: new_alphabet.AlphabetABC, offset: int
+) -> SeqViewABC:
     return SeqView(parent=data, seqid=seqid, alphabet=alphabet, offset=offset)
 
 
 @_coerce_to_seqview.register
-def _(data: bytes, seqid, alphabet, offset) -> SeqViewABC:
+def _(
+    data: bytes, seqid: str, alphabet: new_alphabet.AlphabetABC, offset: int
+) -> SeqViewABC:
     data = data.decode("utf8")
     return SeqView(parent=data, seqid=seqid, alphabet=alphabet, offset=offset)
 
 
 @_coerce_to_seqview.register
-def _(data: tuple, seqid, alphabet, offset) -> SeqViewABC:
+def _(
+    data: numpy.ndarray, seqid: str, alphabet: new_alphabet.AlphabetABC, offset: int
+) -> SeqViewABC:
+    return SeqView(
+        parent=data.astype(alphabet.dtype),
+        seqid=seqid,
+        alphabet=alphabet,
+        offset=offset,
+    )
+
+
+@_coerce_to_seqview.register
+def _(
+    data: tuple, seqid: str, alphabet: new_alphabet.AlphabetABC, offset: int
+) -> SeqViewABC:
     return _coerce_to_seqview("".join(data), seqid, alphabet, offset)
 
 
 @_coerce_to_seqview.register
-def _(data: list, seqid, alphabet, offset) -> SeqViewABC:
+def _(
+    data: list, seqid: str, alphabet: new_alphabet.AlphabetABC, offset: int
+) -> SeqViewABC:
     return _coerce_to_seqview("".join(data), seqid, alphabet, offset)


Test patch:
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index 987a2a7f7..104d56677 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 get_app, load_aligned_seqs, 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
@@ -313,6 +313,38 @@ def test_seqs_data_getitem_raises(dna_sd):
         _ = dna_sd[invalid_index]
 
 
+def test_seqs_data_subset(dna_sd):
+    got = dna_sd.subset("seq1")
+    assert isinstance(got, new_alignment.SeqsData)
+    assert got.names == ["seq1"]
+    assert got.get_seq_str(seqid="seq1") == dna_sd.get_seq_str(seqid="seq1")
+
+    got = dna_sd.subset(["seq1", "seq2"])
+    assert isinstance(got, new_alignment.SeqsData)
+    assert got.names == ["seq1", "seq2"]
+    assert got.get_seq_str(seqid="seq1") == dna_sd.get_seq_str(seqid="seq1")
+    assert got.get_seq_str(seqid="seq2") == dna_sd.get_seq_str(seqid="seq2")
+
+    got = dna_sd.subset(["seq2", "seq1"])
+    assert got.names == ["seq2", "seq1"]
+    assert got.get_seq_str(seqid="seq1") == dna_sd.get_seq_str(seqid="seq1")
+    assert got.get_seq_str(seqid="seq2") == dna_sd.get_seq_str(seqid="seq2")
+
+    got = dna_sd.subset(["seq3"])
+    assert got.names == ["seq3"]
+    assert got.get_seq_str(seqid="seq3") == dna_sd.get_seq_str(seqid="seq3")
+
+
+def test_seqs_data_subset_raises(dna_sd):
+    # should handle missing seqids
+    got = dna_sd.subset(["seq1", "seq4"])
+    assert got.names == ["seq1"]
+
+    # but will raise error if resulting in empty selection
+    with pytest.raises(ValueError):
+        _ = dna_sd.subset(["seq4"])
+
+
 def test_seqs_data_to_alphabet():
     ASCII = new_moltype.ASCII.alphabet
     DNA = new_moltype.DNA.degen_gapped_alphabet
@@ -340,15 +372,18 @@ def test_seqs_data_to_alphabet_invalid():
         _ = seqs.to_alphabet(DNA)
 
 
-def test_seqs_data_round_trip(dna_alphabet):
+@pytest.mark.parametrize("reverse", (False, True))
+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() if reverse else seqs_data
+
     rd = seqs_data.to_rich_dict()
     got = deserialise_object(rd)
     assert isinstance(got, new_alignment.SeqsData)
     assert got.to_rich_dict() == seqs_data.to_rich_dict()
-    expect = "ACGG"
+    expect = "ACGG"[::-1] if reverse else "ACGG"
     assert str(got.get_view("seq1")) == expect
 
 
@@ -726,14 +761,6 @@ def test_sequence_collection_iter_seqs_ragged(ragged):
     assert seqs[0] == ragged.seqs["b"]
 
 
-def test_sequence_collection_iter_seqs_renamed(ragged_padded_dict):
-    """SequenceCollection iter_seqs() method should support renaming of seqs"""
-    coll = new_alignment.make_unaligned_seqs(ragged_padded_dict, moltype="dna")
-    renamed = coll.rename_seqs(renamer=lambda x: f"seq_{x}")
-    seqs = list(renamed.iter_seqs())
-    assert seqs == ["AAAAAA", "AAA---", "AAAA--"]
-
-
 def test_sequence_collection_repr():
     data = {
         "ENSMUSG00000056468": "GCCAGGGGGAAAAGGGAGAA",
@@ -847,15 +874,15 @@ def test_sequence_collection_set_repr_policy_valid_input(seqs):
 
 
 @pytest.mark.parametrize(
-    "mk_cls",
+    "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, mk_cls, sample):
+def test_sequence_collection_take_seqs(ragged_padded_dict, collection_maker, sample):
     """take_seqs should return new SequenceCollection/Alignment with selected seqs."""
-    orig = mk_cls(ragged_padded_dict, moltype="dna")
+    orig = collection_maker(ragged_padded_dict, moltype="dna")
     subset = orig.take_seqs(sample)
     assert subset.names == sample
     assert subset.num_seqs == len(sample)
@@ -865,25 +892,12 @@ def test_sequence_collection_take_seqs(ragged_padded_dict, mk_cls, sample):
 
 
 @pytest.mark.parametrize(
-    "mk_cls",
-    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
-)
-def test_sequence_collection_take_seqs_rc(mk_cls):
-    data = {"a": "ACGT", "b": "CGTA", "c": "TTTT"}
-    orig = mk_cls(data, moltype="dna")
-    rc_seqs = orig.rc().take_seqs(["a", "b"])
-    got = rc_seqs.to_dict()
-    expect = {"a": "ACGT", "b": "TACG"}
-    assert got == expect
-
-
-@pytest.mark.parametrize(
-    "mk_cls",
+    "collection_maker",
     [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
 )
-def test_sequence_collection_take_seqs_str(ragged_padded_dict, mk_cls):
+def test_sequence_collection_take_seqs_str(ragged_padded_dict, collection_maker):
     """string arg to SequenceCollection take_seqs should work."""
-    orig = mk_cls(ragged_padded_dict, moltype="dna")
+    orig = collection_maker(ragged_padded_dict, moltype="dna")
     subset = orig.take_seqs("a")
     assert subset.names == ["a"]
     assert subset.num_seqs == 1
@@ -895,12 +909,12 @@ def test_sequence_collection_take_seqs_str(ragged_padded_dict, mk_cls):
 
 
 @pytest.mark.parametrize(
-    "mk_cls",
+    "collection_maker",
     [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
 )
-def test_sequence_collection_take_seqs_info(ragged_padded_dict, mk_cls):
+def test_sequence_collection_take_seqs_info(ragged_padded_dict, collection_maker):
     """take_seqs should preserve info attribute"""
-    orig = mk_cls(
+    orig = collection_maker(
         ragged_padded_dict,
         moltype="dna",
         info={"key": "value"},
@@ -934,12 +948,12 @@ def test_sequence_collection_take_seqs_empty_names(
 
 
 @pytest.mark.parametrize(
-    "mk_cls",
+    "collection_maker",
     [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
 )
-def test_sequence_collection_take_seqs_copy_annotations(gff_db, mk_cls):
+def test_sequence_collection_take_seqs_copy_annotations(gff_db, collection_maker):
     data = {"test_seq": "ACGT--", "test_seq2": "CGTTTA"}
-    seq_coll = mk_cls(data, moltype="dna", annotation_db=gff_db)
+    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)
@@ -1067,7 +1081,7 @@ def test_sequence_collection_degap_info(mk_cls, gap_ambig_seqs):
     assert got.info.path == "blah"
 
 
-def test_alignment_degap_sliced(gap_ambig_seqs):
+def test_alignemnt_degap_sliced(gap_ambig_seqs):
     """degap should apply slice_record to alignment"""
     aln = new_alignment.make_aligned_seqs(gap_ambig_seqs, moltype="dna")
     sliced = aln[:3]
@@ -1332,17 +1346,6 @@ def test_get_translation(seqs, mk_cls):
     assert got.moltype == new_moltype.PROTEIN
 
 
-@pytest.mark.parametrize(
-    "mk_cls",
-    (new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs),
-)
-def test_get_translation_renamed(seqs, mk_cls):
-    seqs = mk_cls({"s1": "GATTTT", "s2": "GATCTT"}, moltype="dna")
-    renamed = seqs.rename_seqs(lambda x: f"{x}_renamed")
-    got = renamed.get_translation(incomplete_ok=True)
-    assert got.names == ["s1_renamed", "s2_renamed"]
-
-
 @pytest.mark.parametrize(
     "mk_cls",
     (new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs),
@@ -1964,40 +1967,12 @@ def test_sequence_collection_init_seqs_have_annotations():
         assert db is coll_db
 
 
-@pytest.mark.parametrize("rc", (True, False))
-def test_sequence_collection_init_seqs_rc(rc):
-    """the rc status of the input seqs is propagated and annotations maintained
-    when all seqs have the same rc status"""
-    data = {"seq1": _make_seq("seq1"), "seq2": _make_seq("seq2")}
-    data = {k: v.rc() for k, v in data.items()} if rc else data
-    seq_coll = new_alignment.make_unaligned_seqs(data, moltype="dna")
-    assert seq_coll._is_reversed == rc
-    assert len(seq_coll.annotation_db) > 0
-    got = seq_coll.to_dict()
-    expect = {k: str(v) for k, v in data.items()}
-    assert got == expect
-
-    # when is_reversed is specified, it should override the rc status of the input seqs
-    seq_coll = new_alignment.make_unaligned_seqs(
-        data, moltype="dna", is_reversed=not rc
-    )
-    assert seq_coll._is_reversed == (not rc)
-    assert len(seq_coll.annotation_db) > 0
-    got = seq_coll.to_dict()
-    expect = {k: str(v.rc()) for k, v in data.items()}
-    assert got == expect
-
-
-def test_sequence_collection_init_seqs_mixed_rc():
-    """annotations on input seqs with mixed rc means dropping annotations, the
-    rc of the collection is set to False"""
+def test_sequence_collection_init_seqs_mixed_rc(capsys):
+    """annotations on input seqs with mixed rc means dropping annotations"""
     data = {"seq1": _make_seq("seq1"), "seq2": _make_seq("seq2").rc()}
-    with catch_warnings():
-        filterwarnings("ignore", category=UserWarning)
-        seq_coll = new_alignment.make_unaligned_seqs(data, moltype="dna")
+    seq_coll = new_alignment.make_unaligned_seqs(data, moltype="dna")
     coll_db = seq_coll.annotation_db
     assert not len(coll_db)
-    assert not seq_coll._is_reversed
 
 
 def test_sequence_collection_add_to_seq_updates_coll():
@@ -2037,13 +2012,6 @@ def test_sequence_collection_to_moltype(moltype, mk_cls, seq_cls):
     assert got.moltype.label == moltype
     assert isinstance(got, seq_cls)
 
-    # should also work with moltype objects
-    mt = new_moltype.get_moltype(moltype)
-    mt_seqs = seqs.to_moltype(moltype=mt)
-    got = mt_seqs.seqs["s1"]
-    assert got.moltype.label == moltype
-    assert isinstance(got, seq_cls)
-
 
 @pytest.mark.parametrize("rc", (True, False))
 @pytest.mark.parametrize(
@@ -2169,44 +2137,30 @@ def test_sequence_collection_to_rna(mk_cls):
 
 
 @pytest.mark.parametrize(
-    "mk_cls",
-    [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
-)
-def test_sequence_collection_add_seqs(mk_cls):
-    data = dict(
-        [("name1", "AAA"), ("name2", "A--"), ("name3", "AAA"), ("name4", "AAA")]
-    )
-    data2 = dict([("name5", "TTT"), ("name6", "---")])
-    aln = mk_cls(data, moltype="dna", info={"key": "foo"})
-    out_aln = aln.add_seqs(data2)
-    assert len(out_aln.names) == 6
-
-
-@pytest.mark.parametrize(
-    "mk_cls",
+    "collection_maker",
     [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
 )
-def test_sequence_collection_add_seqs_reversed(mk_cls):
+def test_sequence_collection_add_seqs(collection_maker):
     data = dict(
         [("name1", "AAA"), ("name2", "A--"), ("name3", "AAA"), ("name4", "AAA")]
     )
     data2 = dict([("name5", "TTT"), ("name6", "---")])
-    aln = mk_cls(data, moltype="dna", info={"key": "foo"})
+    aln = collection_maker(data, moltype="dna", info={"key": "foo"})
     out_aln = aln.add_seqs(data2)
     assert len(out_aln.names) == 6
 
 
 @pytest.mark.parametrize(
-    "mk_cls",
+    "collection_maker",
     [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
 )
-def test_sequence_collection_add_seqs_duplicate_raises(mk_cls):
+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 = mk_cls(data, moltype="text", info={"key": "foo"})
+    aln = collection_maker(data, moltype="text", info={"key": "foo"})
     with pytest.raises(ValueError):
         _ = aln.add_seqs(data2)
 
@@ -2216,17 +2170,17 @@ def test_sequence_collection_add_seqs_duplicate_raises(mk_cls):
 
 
 @pytest.mark.parametrize(
-    "mk_cls",
+    "collection_maker",
     [new_alignment.make_unaligned_seqs, new_alignment.make_aligned_seqs],
 )
-def test_sequence_collection_add_seqs_info(mk_cls):
+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 = mk_cls(data, moltype="text", info={"key": "foo"})
-    aln2 = mk_cls(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"
 
@@ -2301,25 +2255,9 @@ def test_sequence_collection_pad_seqs(ragged):
     seqs2 = list(padded2.iter_seqs(seq_order=["a", "b", "c"]))
     assert list(map(str, seqs2)) == ["AAAAAA----", "AAA-------", "AAAA------"]
 
-    # will pad to max sequence length if pad_length less than max length
-    padded3 = ragged.pad_seqs(pad_length=5)
-    seqs3 = list(padded3.iter_seqs(seq_order=["a", "b", "c"]))
-    assert list(map(str, seqs3)) == ["AAAAAA", "AAA---", "AAAA--"]
-
-
-def test_sequence_collection_pad_seqs_reversed():
-    mk_seq = new_moltype.DNA.make_seq
-    data = {
-        "a": mk_seq(seq="T", name="A"),
-        "b": mk_seq(seq="TCG", name="B"),
-        "c": mk_seq(seq="TTGG", name="C"),
-    }
-    ragged = new_alignment.make_unaligned_seqs(data, moltype="dna")
-    rc = ragged.rc()
-    padded = rc.pad_seqs()
-    got = padded.to_dict()
-    expect = {"a": "A---", "b": "CGA-", "c": "CCAA"}
-    assert got == expect
+    # assertRaises error when pad_length is less than max seq length
+    with pytest.raises(ValueError):
+        _ = ragged.pad_seqs(pad_length=5)
 
 
 @pytest.fixture
@@ -2517,6 +2455,7 @@ def test_sequence_collection_to_rich_dict():
             },
             "alphabet": seqs.moltype.most_degen_alphabet().to_rich_dict(),
             "offset": seqs._seqs_data._offset,
+            "reversed": seqs._seqs_data.is_reversed,
         },
         "type": get_object_provenance(seqs._seqs_data),
         "version": __version__,
@@ -2548,6 +2487,7 @@ def test_sequence_collection_to_rich_dict_annotation_db():
             },
             "alphabet": seqs.moltype.most_degen_alphabet().to_rich_dict(),
             "offset": seqs._seqs_data._offset,
+            "reversed": seqs._seqs_data.is_reversed,
         },
         "type": get_object_provenance(seqs._seqs_data),
         "version": __version__,
@@ -2579,6 +2519,7 @@ def test_sequence_collection_to_rich_dict_reversed_seqs():
             },
             "alphabet": seqs.moltype.most_degen_alphabet().to_rich_dict(),
             "offset": seqs._seqs_data._offset,
+            "reversed": True,
         },
         "type": get_object_provenance(seqs._seqs_data),
         "version": __version__,
@@ -3288,6 +3229,14 @@ def test_aligned_seqs_data_get_aligned_view(aligned_dict, seqid, dna_alphabet):
     assert str(got) == aligned_dict[seqid].replace("-", "")
 
 
+def test_aligned_seqs_data_subset_raises(aligned_dict, dna_alphabet):
+    ad = new_alignment.AlignedSeqsData.from_seqs(
+        data=aligned_dict, alphabet=dna_alphabet
+    )
+    with pytest.raises(ValueError):
+        _ = ad.subset(["seq99"])
+
+
 @pytest.mark.parametrize("seqid", ("seq1", "seq2", "seq3", "seq4"))
 def test_aligned_data_view_array(aligned_array_dict, dna_alphabet, seqid):
     ad = new_alignment.AlignedSeqsData.from_seqs(
@@ -3876,12 +3825,6 @@ def test_alignment_get_gap_array():
     )
     assert numpy.allclose(got, expect)
 
-    # if we slice the alignment, the gap array should reflect the slice
-    sliced = aln[:2]
-    got = sliced.get_gap_array()
-    expect = expect[:, :2]
-    assert numpy.allclose(got, expect)
-
 
 def test_get_position_indices():
     """get_position_indices should return names of cols where f(col)"""
@@ -4167,7 +4110,7 @@ def test_sliding_windows():
     assert result[3].to_dict() == {"seq3": "TACGT", "seq2": "TACGT", "seq1": "TACGT"}
 
 
-def test_alignment_get_feature():
+def test_get_feature():
     aln = new_alignment.make_aligned_seqs(
         {"x": "-AAAAAAAAA", "y": "TTTT--CCCT"}, moltype="dna"
     )
@@ -4178,33 +4121,6 @@ def test_alignment_get_feature():
     assert feat.get_slice().to_dict() == dict(x="AAA", y="CCT")
 
 
-def test_alignment_distance_matrix():
-    """Alignment distance_matrix should produce correct scores"""
-    data = dict([("s1", "ACGTACGTA"), ("s2", "GTGTACGTA")])
-    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
-    dists = aln.distance_matrix(calc="hamming", show_progress=False)
-    assert dists == {("s1", "s2"): 2.0, ("s2", "s1"): 2.0}
-    # and for protein
-    aa = aln.get_translation()
-    dists = aa.distance_matrix(calc="hamming")
-    assert dists == {("s1", "s2"): 1.0, ("s2", "s1"): 1.0}
-
-    # when there are invalid data
-    data = dict(
-        seq1="AGGGGGGGGGGCCCCCCCCCCCCCCCCCGGGGGGGGGGGGGGGCGGTTTTTTTTTTTTTTTTTT",
-        seq2="TAAAAAAAAAAGGGGGGGGGGGGGGGGGGTTTTTTTTTTTTTTTTTTCCCCCCCCCCCCCCCCC",
-        seq3="TACAAAAAAAAGGGGCGGGGGGGGGGGGGTTTTTTTTTTTTTTTTTTCCCCCCCCCCCCCCCCC",
-    )
-
-    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
-    with pytest.raises(ArithmeticError):
-        # default settings cause an exception
-        _ = aln.distance_matrix(calc="paralinear")
-    # but setting drop_invalid=False allows calc
-    dists = aln.distance_matrix(calc="paralinear", drop_invalid=True)
-    assert dists is None
-
-
 def test_alignment_sample_with_replacement():
     # test with replacement -- just verify that it rnus
     alignment = new_alignment.make_aligned_seqs(
@@ -4242,21 +4158,6 @@ def test_alignment_sample_tuples():
     assert all(len(set(seqs[0][i : i + 2])) == 1 for i in range(0, len(seqs[0]), 2))
 
 
-def test_alignment_sample_sliced():
-    """sample should only return characters from current view"""
-    # not a deterministic test, but has a small false positive rate of 1/1001
-    data = {"seq1": "A" * 1000 + "C"}
-    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
-    sliced = aln[-1:]
-    got = sliced.sample(n=1)
-    assert got.to_dict() == {"seq1": "C"}
-
-    rc = aln.rc()
-    sliced = rc[:1]
-    got = sliced.sample(n=1)
-    assert got.to_dict() == {"seq1": "G"}
-
-
 def test_alignment_take_positions():
     """SequenceCollection take_positions should return new alignment w/ specified pos"""
     gaps = new_alignment.make_aligned_seqs(
@@ -4276,29 +4177,6 @@ def test_alignment_take_positions():
     }
 
 
-def test_alignment_take_positions_sliced():
-    #       012345
-    #       -TAA--
-    #       TTAGGG
-    # slice: ***
-    # pos:    **
-
-    data = {
-        "seq1": "-TAA--",
-        "seq2": "TTAGGG",
-    }
-    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
-    sliced = aln[1:4]
-    got = sliced.take_positions([1, 2]).to_dict()
-    expect = {"seq1": "AA", "seq2": "AG"}
-    assert got == expect
-
-    rev = aln.rc()
-    got = rev.take_positions([1, 2]).to_dict()
-    expect = {"seq1": "-T", "seq2": "CC"}
-    assert got == expect
-
-
 def test_alignment_take_positions_info():
     aln = new_alignment.make_aligned_seqs(
         {"a": "AAAAAAA", "b": "A--A-AA", "c": "AA-----"},
@@ -4512,31 +4390,6 @@ def test_omit_gap_pos():
     assert new_aln.omit_gap_pos(0) is None
 
 
-@pytest.fixture(scope="session")
-def brca1_data():
-    return load_aligned_seqs("data/brca1.fasta", moltype="dna", new_type=True).to_dict()
-
-
-@pytest.mark.parametrize("calc", ("hamming", None))
-def test_alignment_quick_tree(calc, brca1_data):
-    """quick tree method returns tree"""
-    aln = new_alignment.make_aligned_seqs(brca1_data, moltype="dna")[:100]
-    aln = aln.take_seqs(["Human", "Rhesus", "HowlerMon", "Galago", "Mouse"])
-    kwargs = dict(show_progress=False)
-    if calc:
-        kwargs["calc"] = calc
-
-    # bootstrap
-    tree = aln.quick_tree(bootstrap=2, **kwargs)
-    assert set(tree.get_tip_names()) == set(aln.names)
-    types = {
-        type(float(edge.params["support"]))
-        for edge in tree.preorder()
-        if not edge.is_root()
-    }
-    assert types == {float}
-
-
 @pytest.fixture
 def alignment():
     data = {
diff --git a/tests/test_core/test_new_alphabet.py b/tests/test_core/test_new_alphabet.py
index 13e032121..2ec9986e2 100644
--- a/tests/test_core/test_new_alphabet.py
+++ b/tests/test_core/test_new_alphabet.py
@@ -650,3 +650,27 @@ def test_alpha_no_moltype():
 
 def test_codon_alphabet_moltype(calpha):
     assert calpha.moltype is new_moltype.DNA
+
+
+@pytest.mark.parametrize(
+    "seq",
+    ("GGTAC", "GGTAC".encode("utf8"), numpy.array([3, 3, 0, 2, 1], dtype=numpy.uint8)),
+)
+def test_char_alphabet_to_indices_types(seq):
+    dna = new_moltype.get_moltype("dna")
+    alpha = dna.alphabet
+    got = alpha.to_indices(seq)
+    expect = numpy.array([3, 3, 0, 2, 1], dtype=numpy.uint8)
+    assert_allclose(got, expect)
+
+
+@pytest.mark.parametrize(
+    "seq",
+    ("GGTAC", "GGTAC".encode("utf8"), numpy.array([3, 3, 0, 2, 1], dtype=numpy.uint8)),
+)
+def test_char_alphabet_from_indices_types(seq):
+    dna = new_moltype.get_moltype("dna")
+    alpha = dna.alphabet
+    got = alpha.from_indices(seq)
+    expect = "GGTAC"
+    assert got == expect
diff --git a/tests/test_core/test_new_sequence.py b/tests/test_core/test_new_sequence.py
index 92d4f462d..5592df5c5 100644
--- a/tests/test_core/test_new_sequence.py
+++ b/tests/test_core/test_new_sequence.py
@@ -2713,3 +2713,12 @@ def test_make_seq_wrong_order_alpha():
     )
     with pytest.raises(new_alphabet.AlphabetError):
         new_sequence._coerce_to_seqview(sv, sv.seqid, new_moltype.DNA.degen_alphabet, 0)
+
+
+@pytest.mark.parametrize(
+    "raw_seq",
+    ("GGTAC", "GGTAC".encode("utf8"), numpy.array([3, 3, 0, 2, 1], dtype=numpy.uint8)),
+)
+def test_make_seq_from_types(raw_seq):
+    seq = new_moltype.DNA.make_seq(seq=raw_seq)
+    assert str(seq) == "GGTAC"