Repository: cogent3/cogent3
Pull request #1909 (2024-06-21 02:01:06)

Amendments to improve genetic code translate


Patch:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 5bc4d6605..358f36df9 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -1445,7 +1445,7 @@ class _SequenceCollectionBase:
         """returns copy of self as an alignment of RNA moltype seqs"""
         return self.to_moltype("rna")
 
-    def to_protein(self):  # will not port
+    def to_protein(self):  # ported
         """returns copy of self as an alignment of PROTEIN moltype seqs"""
         return self.to_moltype("protein")
 
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index c5bc4d0e6..7bc505b5d 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -37,9 +37,6 @@ from cogent3.util.misc import get_setting_from_environ, negate_condition
 
 DEFAULT_ANNOTATION_DB = BasicAnnotationDb
 
-OptInt = typing.Optional[int]
-OptStr = typing.Optional[str]
-OptCallable = typing.Optional[typing.Callable]
 PrimitiveSeqTypes = Union[str, bytes, numpy.ndarray]
 
 
@@ -48,9 +45,10 @@ 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_seq.SeqViewABC, new_seq.SliceRecordABC):
+class SeqDataView(new_seq.SliceRecordABC):
     """
-    A view class for SeqsData, providing properties for different representations.
+    A view class for SeqsData, providing properties for different
+    representations.
 
     self.seqs is a SeqsData() instance, but other properties are a reference to a
     single seqid only.
@@ -60,32 +58,31 @@ class SeqDataView(new_seq.SeqViewABC, new_seq.SliceRecordABC):
     data = {"seq1": "ACGT", "seq2": "GTTTGCA"}
     sd = SeqsData(data=data)
     sdv = sd.get_seq_view(seqid="seq1")
-    sdv.array_value
-    # array([3, 1, 2, 0], dtype=int8)
     """
 
-    __slots__ = ("seq", "start", "stop", "step", "_offset", "_seqid", "_seq_len")
+    __slots__ = ("seqs", "start", "stop", "step", "_offset", "_seqid", "_seq_len")
 
     def __init__(
         self,
         *,
-        seq: SeqsData,
+        seqs: SeqsData,
         seq_len: int,
-        start: OptInt = None,
-        stop: OptInt = None,
-        step: OptInt = None,
+        start: Optional[int] = None,
+        stop: Optional[int] = None,
+        step: Optional[int] = None,
         offset: int = 0,
-        seqid: OptStr = None,
+        seqid: Optional[str] = None,
     ):
         if step == 0:
             raise ValueError("step cannot be 0")
         step = 1 if step is None else step
+
         self._seq_len = self._checked_seq_len(seq_len)
         func = (
             new_seq._input_vals_pos_step if step > 0 else new_seq._input_vals_neg_step
         )
         start, stop, step = func(self._seq_len, start, stop, step)
-        self.seq = seq
+        self.seqs = seqs
         self.start = start
         self.stop = stop
         self.step = step
@@ -99,7 +96,7 @@ class SeqDataView(new_seq.SeqViewABC, new_seq.SliceRecordABC):
     @property
     def _zero_slice(self):
         return self.__class__(
-            seq=self.seq, seqid=self.seqid, seq_len=self._seq_len, start=0, stop=0
+            seqs=self.seqs, seqid=self.seqid, seq_len=self._seq_len, start=0, stop=0
         )
 
     @property
@@ -111,34 +108,39 @@ class SeqDataView(new_seq.SeqViewABC, new_seq.SliceRecordABC):
         return self._seq_len
 
     def _get_init_kwargs(self):
-        return {"seq": self.seq, "seqid": self.seqid}
+        return {"seqs": self.seqs, "seqid": self.seqid}
 
     @property
     def str_value(self) -> str:
-        """returns the sequence as a string"""
-        raw = self.seq.get_seq_str(
+        raw = self.seqs.get_seq_str(
             seqid=self.seqid, start=self.parent_start, stop=self.parent_stop
         )
         return raw if self.step == 1 else raw[:: self.step]
 
     @property
     def array_value(self) -> numpy.ndarray:
-        """returns the sequence as a numpy array"""
-        raw = self.seq.get_seq_array(
+        raw = self.seqs.get_seq_array(
             seqid=self.seqid, start=self.parent_start, stop=self.parent_stop
         )
         return raw if self.step == 1 else raw[:: self.step]
 
     @property
     def bytes_value(self) -> bytes:
-        """returns the sequence as bytes"""
-        raw = self.seq.get_seq_bytes(
+        raw = self.seqs.get_seq_bytes(
             seqid=self.seqid, start=self.parent_start, stop=self.parent_stop
         )
         return raw if self.step == 1 else raw[:: self.step]
 
+    def __str__(self):
+        return self.str_value
+
+    def __array__(self):
+        return self.array_value
+
+    def __bytes__(self):
+        return self.bytes_value
+
     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__}(seq={seq}, start={self.start}, "
@@ -157,7 +159,10 @@ class SeqsDataABC(ABC):
     a SequenceCollection
     """
 
-    __slots__ = ()
+    alphabet: new_alpha.CharAlphabet
+    make_seq: Callable = (
+        None  # refactor: type hint for callable should specificy input/return type
+    )
 
     @abstractmethod
     def seq_lengths(self) -> dict[str, int]: ...
@@ -170,8 +175,6 @@ class SeqsDataABC(ABC):
     @abstractmethod
     def make_seq(self): ...
 
-    # refactor: docstring
-
     @make_seq.setter
     @abstractmethod
     def make_seq(self, make_seq: Callable) -> None: ...
@@ -182,17 +185,17 @@ class SeqsDataABC(ABC):
 
     @abstractmethod
     def get_seq_array(
-        self, *, seqid: str, start: OptInt = None, stop: OptInt = None
+        self, *, seqid: str, start: int = None, stop: int = None
     ) -> numpy.ndarray: ...
 
     @abstractmethod
     def get_seq_str(
-        self, *, seqid: str, start: OptInt = None, stop: OptInt = None
+        self, *, seqid: str, start: int = None, stop: int = None
     ) -> str: ...
 
     @abstractmethod
     def get_seq_bytes(
-        self, *, seqid: str, start: OptInt = None, stop: OptInt = None
+        self, *, seqid: str, start: int = None, stop: int = None
     ) -> bytes: ...
 
     @abstractmethod
@@ -217,18 +220,14 @@ class SeqsDataABC(ABC):
 
 
 class SeqsData(SeqsDataABC):
-    __slots__ = ("_data", "_alphabet", "_make_seq")
-    # todo: kath
-    # refactor: design
-    # SeqsData needs new fields that record the offsets and possibly whether
-    # the sequence is reversed
+    __slots__ = ("_data", "_alphabet", "_names", "_make_seq")
 
     def __init__(
         self,
         *,
         data: dict[str, PrimitiveSeqTypes],
         alphabet: new_alpha.CharAlphabet,
-        make_seq: OptCallable = None,
+        make_seq: Callable = None,
     ):
         self._alphabet = alphabet
         self._make_seq = make_seq
@@ -256,7 +255,6 @@ class SeqsData(SeqsDataABC):
 
     @make_seq.setter
     def make_seq(self, make_seq: Callable) -> None:
-        # refactor: add type hints for callable function
         self._make_seq = make_seq
 
     @property
@@ -264,29 +262,26 @@ class SeqsData(SeqsDataABC):
         return self._alphabet
 
     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()}
 
     def get_seq_array(
-        self, *, seqid: str, start: OptInt = None, stop: OptInt = None
+        self, *, seqid: str, start: int = None, stop: int = None
     ) -> numpy.ndarray:
         return self._data[seqid][start:stop]
 
-    def get_seq_str(
-        self, *, seqid: str, start: OptInt = None, stop: OptInt = None
-    ) -> str:
+    def get_seq_str(self, *, seqid: str, start: int = None, stop: int = None) -> str:
         return self._alphabet.from_indices(
             self.get_seq_array(seqid=seqid, start=start, stop=stop)
         )
 
     def get_seq_bytes(
-        self, *, seqid: str, start: OptInt = None, stop: OptInt = None
+        self, *, seqid: str, start: int = None, stop: int = None
     ) -> bytes:
         return self.get_seq_str(seqid=seqid, start=start, stop=stop).encode("utf8")
 
     def get_seq_view(self, seqid: str) -> SeqDataView:
         seq_len = len(self._data[seqid])
-        return SeqDataView(seq=self, seqid=seqid, seq_len=seq_len)
+        return SeqDataView(seqs=self, seqid=seqid, seq_len=seq_len)
 
     def subset(self, names: Union[str, typing.Sequence[str]]) -> SeqsData:
         """Returns a new SeqsData object with only the specified names."""
@@ -357,7 +352,7 @@ class SeqsData(SeqsDataABC):
         return (
             sdv
             if self.make_seq is None
-            else self.make_seq(seq=sdv, name=index, check_seq=False)
+            else self.make_seq(seq=sdv.str_value, name=index)
         )
 
     @__getitem__.register
@@ -373,9 +368,9 @@ class SequenceCollection:
         moltype: new_moltype.MolType,
         names: list[str] = None,
         info: Union[dict, InfoClass] = None,
-        source: OptStr = None,
+        source: str = None,  # refactor: need to deal with optional args type hints
         annotation_db: SupportsFeatures = None,
-    ):  # refactor: need to deal with optional args type hints
+    ):
         self._seqs_data = seqs_data
         self.moltype = moltype
         self.names = names
@@ -432,9 +427,7 @@ class SequenceCollection:
 
     def iter_seqs(
         self, seq_order: list = None
-    ) -> Iterator[
-        Union[new_seq.Sequence, SeqsData]
-    ]:  # refactor: need to deal with optional args type hints
+    ) -> Iterator[Union[new_seq.Sequence, SeqsData]]:
         """Iterates over sequences in the collection, in order.
 
         Parameters
@@ -582,11 +575,6 @@ class SequenceCollection:
             annotation_db=self.annotation_db,
         )
 
-    def rename_seqs(self, renamer):
-        # todo: kath
-        # returns new SeqsData instance
-        ...
-
     def to_dict(self, as_array: bool = False) -> dict[str, Union[str, numpy.ndarray]]:
         """Return a dictionary of sequences.
 
@@ -638,7 +626,11 @@ class SequenceCollection:
         if mtype is self.moltype:
             return self  # nothing to be done
 
-        alpha = mtype.most_degen_alphabet()
+        alpha = (
+            mtype.degen_gapped_alphabet
+            if mtype.degen_gapped_alphabet is not None
+            else mtype.gapped_alphabet
+        )
         try:
             new_seqs_data = self.seqs.to_alphabet(alpha)
         except ValueError as e:
@@ -653,106 +645,6 @@ class SequenceCollection:
             annotation_db=self.annotation_db,
         )
 
-    def to_dna(self):
-        """returns copy of self as an alignment of DNA moltype seqs"""
-        # refactor: design
-        # do we need this in addition to to_moltype?
-        return self.to_moltype("dna")
-
-    def to_rna(self):
-        """returns copy of self as an alignment of RNA moltype seqs"""
-        return self.to_moltype("rna")
-
-    def get_translation(
-        self,
-        gc: int = 1,
-        incomplete_ok: bool = False,
-        include_stop: bool = False,
-        trim_stop: bool = True,
-        **kwargs,
-    ):
-        """translate from nucleic acid to protein
-
-        Parameters
-        ----------
-        gc
-            genetic code, either the number or name
-            (use cogent3.core.genetic_code.available_codes)
-        incomplete_ok
-            codons that are mixes of nucleotide and gaps converted to '?'.
-            raises a ValueError if False
-        include_stop
-            whether to allow a stops in the translated sequence
-        trim_stop
-            exclude terminal stop codons if they exist
-        kwargs
-            related to construction of the resulting object
-
-        Returns
-        -------
-        A new instance of self translated into protein
-        """
-        # todo: kath, add check for when include_stop==True, the seq.get_translation method should be
-        # returning sequences with a protein_with_stop moltype.
-
-        if len(self.moltype.alphabet) != 4:
-            raise TypeError("Sequences must be a DNA/RNA")
-
-        translated = {}
-        if trim_stop and not include_stop:
-            seqs = self.trim_stop_codons(gc=gc, strict=not incomplete_ok)
-        else:
-            seqs = self
-        # do the translation
-        for seqname in seqs.names:
-            seq = seqs.seqs[seqname]
-            pep = seq.get_translation(
-                gc, incomplete_ok=True, include_stop=include_stop, trim_stop=trim_stop
-            )
-            translated[seqname] = pep
-        pep_moltype = pep.moltype
-        seqs_data = SeqsData(
-            data=translated,
-            alphabet=pep_moltype.most_degen_alphabet(),
-            make_seq=pep_moltype.make_seq,
-        )
-        return self.__class__(
-            seqs_data=seqs_data, info=self.info, source=self.source, **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.
-
-        """
-        rc_seqs = {name: self.seqs[name].rc() for name in self.names}
-        seqs_data = self.seqs.__class__(
-            data=rc_seqs, alphabet=self.moltype.alphabet, make_seq=self.moltype.make_seq
-        )
-        return self.__class__(
-            seqs_data=seqs_data,
-            name=self.name,
-            info=self.info,
-            moltype=self.moltype,
-        )
-
-    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()
-
     def distance_matrix(self, calc: str = "pdist"):
         """Estimated pairwise distance between sequences
 
@@ -827,8 +719,6 @@ class SequenceCollection:
         if self.annotation_db is None:
             self.annotation_db = type(seq_db)()
 
-        # refactor
-        # if we update a db with the same db, it just hangs... need to fix
         if self.annotation_db.compatible(seq_db, symmetric=False):
             # our db contains the tables in other, so we update in place
             self.annotation_db.update(annot_db=seq_db, seqids=self.names)
@@ -868,7 +758,7 @@ class SequenceCollection:
         biotype: str,
         name: str,
         spans: list[tuple[int, int]],
-        parent_id: OptStr = None,
+        parent_id: Optional[str] = None,
         strand: str = "+",
     ) -> Feature:
         """
@@ -909,12 +799,10 @@ class SequenceCollection:
         self,
         *,
         seqid: Union[str, Iterator[str]] = None,
-        biotype: OptStr = None,
-        name: OptStr = None,
-        start: OptInt = None,
-        stop: OptInt = None,
+        biotype: Optional[str] = None,
+        name: Optional[str] = None,
         allow_partial: bool = False,
-    ) -> Iterator[Feature]:  # refactor: need to deal with optional args type hints
+    ) -> Iterator[Feature]:
         """yields Feature instances
 
         Parameters
@@ -925,21 +813,14 @@ class SequenceCollection:
             biotype of the feature, e.g. CDS, gene
         name
             name of the feature
-        start
-            start position of the feature (not inclusive)
-        stop
-            stop position of the feature (inclusive)
         allow_partial
             allow features partially overlaping self
 
         Notes
         -----
-        - When dealing with a nucleic acid moltype, the returned features will
+        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.
-        - start is non-inclusive, so if allow_partial is False, only features
-        strictly starting after start will be returned.
-
         """
         if not self.annotation_db:
             return None
@@ -955,8 +836,6 @@ class SequenceCollection:
             biotype=biotype,
             name=name,
             on_alignment=False,
-            start=start,
-            stop=stop,
             allow_partial=allow_partial,
         ):
             seqname = seqid_to_seqname[feature["seqid"]]
@@ -993,7 +872,7 @@ class SequenceCollection:
 
         return alignment_to_phylip(self.to_dict())
 
-    def write(self, filename: str, file_format: OptStr = None, **kwargs):
+    def write(self, filename: str = None, file_format: str = None, **kwargs):
         """Write the sequences to a file, preserving order of sequences.
 
         Parameters
@@ -1011,6 +890,9 @@ class SequenceCollection:
         """
 
         # todo: kath, add support for json
+        if filename is None:
+            raise IOError("no filename specified")
+
         suffix, _ = get_format_suffixes(filename)
         if file_format is None and suffix:
             file_format = suffix
@@ -1027,14 +909,14 @@ class SequenceCollection:
 
     def dotplot(
         self,
-        name1: OptStr = None,
-        name2: OptStr = None,
+        name1: Optional[str] = None,
+        name2: Optional[str] = None,
         window: int = 20,
-        threshold: OptInt = None,
-        k: OptInt = None,
+        threshold: Optional[int] = None,
+        k: Optional[int] = None,
         min_gap: int = 0,
         width: int = 500,
-        title: OptStr = None,
+        title: Optional[str] = None,
         rc: bool = False,
         show_progress: bool = False,
     ):
@@ -1136,7 +1018,7 @@ class SequenceCollection:
     def apply_pssm(
         self,
         pssm: PSSM = None,
-        path: OptStr = None,
+        path: str = None,
         background: numpy.array = None,
         pseudocount: int = 0,
         names: list = None,
@@ -1190,10 +1072,19 @@ class SequenceCollection:
                 pssm = pwm.to_pssm(background=background, pseudocount=pseudocount)
 
         assert isinstance(pssm, PSSM)
+        array_align = hasattr(self, "array_seqs")
         assert set(pssm.motifs) == set(self.moltype)
+        if array_align and list(pssm.motifs) == list(self.moltype):
+            if names:
+                name_indices = [self.names.index(n) for n in names]
+                data = self.array_seqs.take(name_indices, axis=0)
+            else:
+                data = self.array_seqs
 
-        seqs = [self.seqs[n] for n in names] if names else self.seqs
-        result = [pssm.score_seq(seq) for seq in ui.series(seqs)]
+            result = [pssm.score_indexed_seq(seq) for seq in ui.series(data)]
+        else:
+            seqs = [self.seqs[n] for n in names] if names else self.seqs
+            result = [pssm.score_seq(seq) for seq in ui.series(seqs)]
 
         return numpy.array(result)
 
@@ -1324,18 +1215,15 @@ class SequenceCollection:
 
         Parameters
         ----------
-        alphabet
-            alphabet to use for motifs
         include_ambiguity
             if True resolved ambiguous codes are included in estimation of
-            frequencies.
+            frequencies, default is False.
         exclude_unobserved
             if True, motifs that are not present in the alignment are excluded
-            from the returned dictionary.
+            from the returned dictionary,
+            default is False.
         allow_gap
             allow gap motif
-        pseudocount
-            value to add to each count
 
         Notes
         -----
@@ -1346,12 +1234,12 @@ class SequenceCollection:
         if alphabet is None:
             alphabet = moltype.alphabet
             if allow_gap:
-                alphabet = moltype.gapped_alphabet
+                alphabet = alphabet.gapped
 
         counts = {}
         for seq_name in self.names:
             sequence = self.seqs[seq_name]
-            motif_len = alphabet.motif_len
+            motif_len = alphabet.get_motif_len()
             if motif_len > 1:
                 posns = list(range(0, len(sequence) + 1 - motif_len, motif_len))
                 sequence = [sequence[i : i + motif_len] for i in posns]
@@ -1393,23 +1281,7 @@ class SequenceCollection:
         exclude_unobserved: bool = False,
         warn: bool = False,
     ) -> MotifFreqsArray:
-        """return frequency array of motifs per sequence
-
-        Parameters
-        ----------
-        motif_length
-            number of characters per motif
-        include_ambiguity
-            if True, include motifs containing ambiguous characters
-        allow_gap
-            if True, include motifs containing a gap character
-        exclude_unobserved
-            if True, exclude motifs not present in the sequences in
-            the resulting array
-        warn
-            warns if motif_length > 1 and collection trimmed to produce motif
-            columns.
-        """
+        """return MotifFreqsArray per sequence"""
 
         counts = self.counts_per_seq(
             motif_length=motif_length,
@@ -1479,7 +1351,7 @@ class SequenceCollection:
         )
         return counts.row_sum()
 
-    def pad_seqs(self, pad_length: OptInt = None):
+    def pad_seqs(self, pad_length: int = None):
         """Returns copy in which sequences are padded to same length.
 
         Parameters
@@ -1736,7 +1608,7 @@ class SequenceCollection:
         self,
         name_order: Optional[typing.Sequence[str]] = None,
         wrap: int = 60,
-        limit: OptInt = None,
+        limit: Optional[int] = None,
         colors: Optional[Mapping[str, str]] = None,
         font_size: int = 12,
         font_family: str = "Lucida Console",
@@ -1887,10 +1759,10 @@ class SequenceCollection:
 
     def set_repr_policy(
         self,
-        num_seqs: OptInt = None,
-        num_pos: OptInt = None,
-        ref_name: OptInt = None,
-        wrap: OptInt = None,
+        num_seqs: Optional[int] = None,
+        num_pos: Optional[int] = None,
+        ref_name: Optional[str] = None,
+        wrap: Optional[int] = None,
     ):
         """specify policy for repr(self)
 
@@ -1976,17 +1848,17 @@ def _(seqs: dict) -> SupportsFeatures:
 
 @singledispatch
 def coerce_to_seqs_data_dict(
-    data, label_to_name: OptCallable = None
-) -> dict[str, PrimitiveSeqTypes]:
-    # refactor: type hint for callable should specificy input/return type
-    # refactor: handle conversion of SeqView to SeqDataView.
+    data, label_to_name: Callable = None
+) -> dict[
+    str, PrimitiveSeqTypes
+]:  # refactor: type hint for callable should specificy input/return type
     raise NotImplementedError(
         f"coerce_to_seqs_data_dict not implemented for {type(data)}"
     )
 
 
 @coerce_to_seqs_data_dict.register
-def _(data: dict, label_to_name: OptCallable = None) -> dict[str, PrimitiveSeqTypes]:
+def _(data: dict, label_to_name: Callable = None) -> dict[str, PrimitiveSeqTypes]:
     is_sequence = isinstance(next(iter(data.values()), None), new_seq.Sequence)
     return {
         (label_to_name(k) if label_to_name else k): (str(v) if is_sequence else v)
@@ -1995,14 +1867,14 @@ def _(data: dict, label_to_name: OptCallable = None) -> dict[str, PrimitiveSeqTy
 
 
 @coerce_to_seqs_data_dict.register
-def _(data: list, label_to_name: OptCallable = None) -> dict[str, PrimitiveSeqTypes]:
+def _(data: list, label_to_name: Callable = None) -> dict[str, PrimitiveSeqTypes]:
     first = data[0]
     labelled_seqs = assign_names(first, data=data)
     return coerce_to_seqs_data_dict(labelled_seqs, label_to_name=label_to_name)
 
 
 @coerce_to_seqs_data_dict.register
-def _(data: set, label_to_name: OptCallable = None) -> dict[str, PrimitiveSeqTypes]:
+def _(data: set, label_to_name: Callable = None) -> dict[str, PrimitiveSeqTypes]:
     first = next(iter(data))
     labelled_seqs = assign_names(first, data=data)
     return coerce_to_seqs_data_dict(labelled_seqs, label_to_name=label_to_name)
@@ -2010,7 +1882,7 @@ def _(data: set, label_to_name: OptCallable = None) -> dict[str, PrimitiveSeqTyp
 
 @coerce_to_seqs_data_dict.register
 def _(
-    data: SequenceCollection, label_to_name: OptCallable = None
+    data: SequenceCollection, label_to_name: Callable = None
 ) -> dict[str, PrimitiveSeqTypes]:
     return coerce_to_seqs_data_dict(
         data.to_dict(as_array=True), label_to_name=label_to_name
@@ -2047,7 +1919,7 @@ def make_unaligned_seqs(
     data: Union[dict[str, PrimitiveSeqTypes], SeqsData, list],
     *,
     moltype: Union[str, new_moltype.MolType],
-    label_to_name: OptCallable = None,
+    label_to_name: Callable = None,
     info: dict = None,
     source: Union[str, Path] = None,
     annotation_db: SupportsFeatures = None,
@@ -2086,7 +1958,9 @@ def make_unaligned_seqs(
     seqs_data = coerce_to_seqs_data_dict(data, label_to_name=label_to_name)
 
     moltype = new_moltype.get_moltype(moltype)
-    alphabet = moltype.most_degen_alphabet()
+    alphabet = (
+        moltype.degen_gapped_alphabet or moltype.gapped_alphabet or moltype.alphabet
+    )
 
     seqs_data = SeqsData(data=seqs_data, alphabet=alphabet)
     return make_unaligned_seqs(
@@ -2101,10 +1975,10 @@ def make_unaligned_seqs(
 
 @make_unaligned_seqs.register
 def _(
-    data: SeqsDataABC,
+    data: SeqsData,
     *,
     moltype: Union[str, new_moltype.MolType],
-    label_to_name: OptCallable = None,
+    label_to_name: Callable = None,
     info: dict = None,
     source: Union[str, Path] = None,
     annotation_db: SupportsFeatures = None,
diff --git a/src/cogent3/core/new_alphabet.py b/src/cogent3/core/new_alphabet.py
index 00ec94fd8..27cac65c6 100644
--- a/src/cogent3/core/new_alphabet.py
+++ b/src/cogent3/core/new_alphabet.py
@@ -93,6 +93,10 @@ class convert_alphabet:
 
 class AlphabetABC(ABC):
 
+    @property
+    @abstractmethod
+    def num_canonical(self) -> int: ...
+
     @property
     @abstractmethod
     def motif_len(self) -> int: ...
@@ -259,6 +263,10 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
         self._bytes2arr = bytes_to_array(byte_chars, dtype=self.dtype)
         self._arr2bytes = array_to_bytes(byte_chars)
 
+    @property
+    def num_canonical(self) -> int:
+        return self._num_canonical
+
     @property
     def gap_char(self) -> OptStr:
         return self._gap_char
@@ -286,10 +294,9 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
 
     @to_indices.register
     def _(self, seq: bytes) -> numpy.ndarray:
-        result = self._bytes2arr(seq)
         # any non-canonical characters should lie outside the range
         # we replace these with a single value
-        return result
+        return self._bytes2arr(seq)
 
     @to_indices.register
     def _(self, seq: str) -> numpy.ndarray:
@@ -319,9 +326,14 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
         """
         if self.gap_char and self.missing_char:
             return self
-        chars = tuple(self[: self._num_canonical]) + (gap_char, missing_char)
+        chars = (
+            tuple(self[: self._num_canonical]) + (gap_char, missing_char)
+            if missing_char
+            else (gap_char,)
+        )
         return self.__class__(chars, gap=gap_char, missing=missing_char)
 
+    @functools.cache
     def get_kmer_alphabet(self, k: int, include_gap: bool = True) -> "KmerAlphabet":
         """returns kmer alphabet with words of size k
 
@@ -337,16 +349,19 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
         if k == 1:
             return self
 
-        if self.gap_char:
-            chars = tuple(c for c in self if c != self.gap_char)
-        else:
-            chars = tuple(self)
+        chars = tuple(self[: self.num_canonical])
 
         gap = self._gap_char * k if include_gap and self._gap_char is not None else None
+        missing = (
+            self._missing_char * k
+            if self._missing_char is not None and include_gap
+            else None
+        )
         words = tuple("".join(e) for e in itertools.product(chars, repeat=k))
-        if include_gap and gap:
-            words += (gap,)
-        return KmerAlphabet(words=words, monomers=self, gap=gap, k=k)
+        words += (gap,) if include_gap and gap else ()
+        words += (missing,) if include_gap and missing else ()
+
+        return KmerAlphabet(words=words, monomers=self, gap=gap, k=k, missing=missing)
 
     @functools.singledispatchmethod
     def is_valid(self, seq: StrORBytesORArray) -> bool:
@@ -362,9 +377,7 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
 
     @is_valid.register
     def _(self, seq: numpy.ndarray) -> bool:
-        if not len(seq):
-            return True
-        return seq.min() >= 0 and seq.max() < len(self)
+        return seq.min() >= 0 and seq.max() < len(self) if len(seq) else True
 
     def as_bytes(self) -> bytes:
         """returns self as a byte string"""
@@ -409,6 +422,8 @@ def seq_to_kmer_indices(
     coeffs: numpy.ndarray,
     num_states: int,
     k: int,
+    gap_char_index: int = -1,
+    gap_index: int = -1,
     independent_kmer: bool = True,
 ) -> numpy.ndarray:  # pragma: no cover
     """return 1D indices for valid k-mers
@@ -423,17 +438,22 @@ def seq_to_kmer_indices(
     coeffs
         result of calling coord_conversion_coeffs() with num_states and k
     num_states
-        defines range of possible ints at a position
+        the number of canonical states, which defines range of allowed
+        ints at a position
     k
         k-mer size
+    gap_char_index
+        value of a gap character in seq. If > 0, any k-mer containing
+        a gap character is assigned an index of num_states**k. Any
+        k-mer containing an integer greater than gap_index is assigned an
+        index of (num_states**k) + 1. If gap_char_index < 0, then any
+        k-mer containing an integer greater than num_states is assigned an
+        index of num_states**k.
+    gap_index
+        value to be assigned when gap_char_index is != -1, must be positive
     independent_kmer
         if True, sets returns indices for non-overlapping k-mers, otherwise
         returns indices for all k-mers
-
-    Notes
-    -----
-    If a k-mer has an element > num_states, that k-mer index is assigned
-    to 1+num_states**k
     """
     # check the result length is consistent with the settings
     step: int = k if independent_kmer else 1
@@ -441,11 +461,15 @@ def seq_to_kmer_indices(
     if len(result) < size:
         raise ValueError(f"size of result {len(result)} <= {size}")
 
+    missing_index: int = gap_index + 1 if gap_char_index > 0 else num_states**k
     for result_idx, i in enumerate(range(0, len(seq) - k + 1, step)):
-        if (num_states > seq[i : i + k]).all():
+        segment = seq[i : i + k]
+        if (num_states > segment).all():
             result[result_idx] = coord_to_index(seq[i : i + k], coeffs)
+        elif gap_char_index > 0 and segment.max() == gap_char_index:
+            result[result_idx] = gap_index
         else:
-            continue
+            result[result_idx] = missing_index
     return result
 
 
@@ -456,12 +480,25 @@ def kmer_indices_to_seq(
     result: numpy.ndarray,
     coeffs: numpy.ndarray,
     k: int,
+    gap_index: int = -1,
+    gap_char_index: int = -1,
     independent_kmer: bool = True,
 ) -> numpy.ndarray:  # pragma: no cover
+    if gap_index > 0:
+        assert (
+            gap_char_index > 0
+        ), f"gap_index={gap_index} but gap_char_index={gap_char_index}"
+        gap_state = numpy.array([gap_char_index] * k, dtype=result.dtype)
+    else:
+        gap_state = numpy.empty(0, dtype=result.dtype)
     for index, kmer_index in enumerate(kmer_indices):
-        coord = index_to_coord(kmer_index, coeffs)
+        if kmer_index == gap_index:
+            coord = gap_state
+        else:
+            coord = index_to_coord(kmer_index, coeffs)
+
         if index == 0:
-            result[0:3] = coord
+            result[:k] = coord
         elif independent_kmer:
             seq_index = index * k
             result[seq_index : seq_index + k] = coord
@@ -487,6 +524,7 @@ class KmerAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
         monomers: CharAlphabet,
         k: int,
         gap: OptStr = None,
+        missing: OptStr = None,
     ):
         if not words:
             raise ValueError(f"cannot create empty {cls.__name__!r}")
@@ -495,7 +533,7 @@ class KmerAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
             assert _coerce_to_type(words[0], gap) in words
 
         consistent_words(words, length=k)
-        return tuple.__new__(cls, words, monomers=monomers, gap=gap)
+        return tuple.__new__(cls, words, monomers=monomers, gap=gap, missing=missing)
 
     def __init__(
         self,
@@ -503,15 +541,63 @@ class KmerAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
         monomers: CharAlphabet,
         k: int,
         gap: OptStr = None,
+        missing: OptStr = None,
     ):
-        self._gap_char = gap
+        """
+        Parameters
+        ----------
+        words
+            series of k length strings
+        monomers
+            base CharAlphabet instance
+        k
+            word size
+        gap
+            the gap state ("-" * k) if present
+        """
         self.monomers = monomers
         self.dtype = get_array_type(len(self))
         self._words = set(self)  # for quick lookup
         self.k = k
 
-        size = len(self.monomers) - 1 if self.monomers.gap_char else len(self.monomers)
-        self._coeffs = coord_conversion_coeffs(size, k, dtype=self.dtype)
+        self._gap_char = gap
+        self._gap_index = self.index(gap) if gap else None
+        self._missing_char = missing
+        self._missing_index = self.index(missing) if missing else None
+        if gap:
+            assert self.monomers.gap_char is not None
+            assert gap == self.monomers.gap_char * k
+            assert gap in self
+
+        if missing:
+            assert self.monomers.missing_char is not None
+            assert missing == self.monomers.missing_char * k
+            assert missing in self
+
+        self._coeffs = coord_conversion_coeffs(
+            self.monomers.num_canonical, k, dtype=self.dtype
+        )
+        self._num_canonical = self.monomers.num_canonical**k
+
+    @property
+    def gap_char(self) -> OptStr:
+        return self._gap_char
+
+    @property
+    def gap_index(self) -> OptInt:
+        return self._gap_index
+
+    @property
+    def missing_char(self) -> OptStr:
+        return self._missing_char
+
+    @property
+    def missing_index(self) -> OptInt:
+        return self._missing_index
+
+    @property
+    def num_canonical(self) -> int:
+        return self._num_canonical
 
     @functools.singledispatchmethod
     def to_indices(self, seq, independent_kmer: bool = True) -> numpy.ndarray:
@@ -523,7 +609,16 @@ class KmerAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
             a sequence of monomers
         independent_kmer
             if True, returns non-overlapping k-mers
-        """
+
+        Notes
+        -----
+        If self.gap_char is not None, then the following rules apply:
+        If a sequence k-mer contains a gap character it is
+        assigned an index of (num. monomer states**k). If
+        a k-mer contains a non-canonical and non-gap character,
+        it is assigned an index of (num. monomer states**k) + 1.
+        If self.gap_char is None, then both of the above cases
+        are defined as (num. monomer states**k)."""
         # todo: handle case of non-modulo sequences
         raise TypeError(f"{type(seq)} is invalid")
 
@@ -538,8 +633,17 @@ class KmerAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
         if independent_kmer:
             size = int(numpy.ceil(size / self.k))
         result = numpy.zeros(size, dtype=get_array_type(size))
+        gap_char_index = self.monomers.gap_index or -1
+        gap_index = self.gap_index or -1
         return seq_to_kmer_indices(
-            seq, result, self._coeffs, len(self.monomers), self.k, independent_kmer
+            seq,
+            result,
+            self._coeffs,
+            self.monomers.num_canonical,
+            self.k,
+            gap_char_index=gap_char_index,
+            gap_index=gap_index,
+            independent_kmer=independent_kmer,
         )
 
     def from_indices(
@@ -552,15 +656,31 @@ class KmerAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
             size = len(kmer_indices) + self.k - 1
 
         result = numpy.zeros(size, dtype=self.dtype)
+        gap_char_index = self.monomers.gap_index or -1
+        gap_index = self.gap_index or -1
         return kmer_indices_to_seq(
             kmer_indices,
             result,
             self._coeffs,
             self.k,
+            gap_char_index=gap_char_index,
+            gap_index=gap_index,
             independent_kmer=independent_kmer,
         )
 
-    def with_gap_motif(self): ...
+    def with_gap_motif(self, include_missing: bool = False):
+        """returns a new KmerAlphabet with the gap motif added
+
+        Notes
+        -----
+        Adds gap state to monomers and recreates k-mer alphabet
+        for self
+        """
+        if self.gap_char:
+            return self
+        missing = "?" if include_missing else None
+        monomers = self.monomers.with_gap_motif(missing_char=missing)
+        return monomers.get_kmer_alphabet(self.k, include_gap=True)
 
     @functools.singledispatchmethod
     def to_index(self, seq) -> numpy.ndarray:
@@ -572,51 +692,69 @@ class KmerAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
             sequence to be encoded, can be either a string or numpy array
         overlapping
             if False, performs operation on sequential k-mers, e.g. codons
-        """
+
+
+        Notes
+        -----
+        If self.gap_char is defined, then the following rules apply:
+        returns num_states**k if a k-mer contains a gap character,
+        otherwise returns num_states**k + 1 if a k-mer contains a
+        non-canonical character. If self.gap_char is not defined,
+        returns num_states**k for both cases."""
         raise TypeError(f"{type(seq)} not supported")
 
     @to_index.register
     def _(self, seq: str) -> numpy.ndarray:
-        """encodes a k-mer as a single integer
-
-        Parameters
-        ----------
-        seq
-            sequence to be encoded
-        overlapping
-            if False, performs operation on sequential k-mers, e.g. codons
-        """
         seq = self.monomers.to_indices(seq)
         return self.to_index(seq)
 
     @to_index.register
     def _(self, seq: bytes) -> numpy.ndarray:
-        """encodes a k-mer as a single integer
-
-        Parameters
-        ----------
-        seq
-            sequence to be encoded
-        overlapping
-            if False, performs operation on sequential k-mers, e.g. codons
-        """
         seq = self.monomers.to_indices(seq)
         return self.to_index(seq)
 
     @to_index.register
     def _(self, seq: numpy.ndarray) -> numpy.ndarray:
-        """encodes a k-mer as a single integer
+        assert len(seq) == self.k
+        if (self.monomers.num_canonical > seq).all():
+            return coord_to_index(seq, self._coeffs)
+
+        gap_char_index = self.monomers.gap_index or self.monomers.num_canonical
+        gap_index = self.gap_index or self.num_canonical
+        missing_index = gap_index + 1 if self.gap_char else gap_index
+        return gap_index if seq.max() == gap_char_index else missing_index
+
+    def from_index(self, kmer_index: int) -> numpy.ndarray:
+        """decodes an integer into a k-mer"""
+        if self.gap_index is not None and kmer_index == self.gap_index:
+            return numpy.array([self.monomers.gap_index] * self.k, dtype=self.dtype)
+        elif self.missing_index is not None and kmer_index == self.missing_index:
+            return numpy.array([self.monomers.missing_index] * self.k, dtype=self.dtype)
+        elif kmer_index >= len(self):
+            raise ValueError(f"{kmer_index} is out of range")
+
+        return index_to_coord(kmer_index, self._coeffs)
+
+    @functools.singledispatchmethod
+    def is_valid(self, seq: numpy.ndarray) -> bool:
+        """whether integers are within the valid range
 
         Parameters
         ----------
         seq
-            sequence to be encoded
+            a numpy array of integers
+
+        Notes
+        -------
+        This will raise a TypeError for string or bytes. Using to_indices() to
+        convert those ensures a valid result.
         """
-        return coord_to_index(seq, self._coeffs)
+        raise TypeError(f"{type(seq)} is invalid, must be numpy.ndarray")
 
-    def from_index(self, kmer_index: int) -> numpy.ndarray:
-        """decodes an integer into a k-mer"""
-        return index_to_coord(kmer_index, self._coeffs)
+    @is_valid.register
+    def _(self, seq: numpy.ndarray) -> bool:
+        max_val = max(self.missing_index or 0, self.gap_index or 0, self.num_canonical)
+        return seq.min() >= 0 and seq.max() <= max_val if len(seq) else True
 
 
 _alphabet_moltype_map = {}
diff --git a/src/cogent3/core/new_genetic_code.py b/src/cogent3/core/new_genetic_code.py
index ede9558e7..cd50100df 100644
--- a/src/cogent3/core/new_genetic_code.py
+++ b/src/cogent3/core/new_genetic_code.py
@@ -74,7 +74,22 @@ def _get_start_codon_indices(start_codon_map: str) -> tuple[int, ...]:
 def _make_converter(
     kmer_alpha: new_alphabet.KmerAlphabet, codons: tuple[str, ...], code_sequence: str
 ) -> typing.Callable[[bytes, bytes], bytes]:
-    assert len(codons) == len(code_sequence) == 64
+    """returns a converter of codon indices into amino acid indices
+
+    Parameters
+    ----------
+    kmer_alpha
+        a complete trinucleotide alphabet
+    codons
+        the order of codons corresponding to code_sequence
+    code_sequence
+        NCBI genetic code sequence plus gap and missing states
+
+    Returns
+    -------
+    callable for converting codon sequence as bytes to amino acid sequence
+    as bytes
+    """
     # we get the index of the codon in the kmer alphabet
     kmers = numpy.array(
         [kmer_alpha.to_index(codon) for codon in codons], dtype=numpy.uint8
@@ -84,7 +99,13 @@ def _make_converter(
 
 @dataclasses.dataclass
 class GeneticCode:
-    """Holds codon to amino acid mapping, and vice versa."""
+    """Holds codon to amino acid mapping, and vice versa.
+
+    Notes
+    -----
+    We add additional states to the genetic code to represent gapped codons
+    and missing data.
+    """
 
     ID: int
     name: str
@@ -105,24 +126,24 @@ class GeneticCode:
     _translate_minus: ConverterType = dataclasses.field(init=False, default=None)
 
     def __post_init__(self, ncbi_code_sequence: str, ncbi_start_codon_map: str):
-        trinuc_alpha = self.moltype.alphabet.get_kmer_alphabet(k=3, include_gap=False)
+        trinuc_alpha = self.moltype.gapped_alphabet.with_gap_motif().get_kmer_alphabet(
+            k=3, include_gap=True
+        )
+        code_seq = f"{ncbi_code_sequence}-X"
+        start_map = f"{ncbi_start_codon_map}--"
         self._codon_to_aa, self._aa_to_codon, self._stop_codons = _make_mappings(
-            trinuc_alpha, ncbi_code_sequence
+            trinuc_alpha, code_seq
         )
         self.codons = trinuc_alpha
         self._start_codons = {
-            self.codons[i] for i in _get_start_codon_indices(ncbi_start_codon_map)
+            self.codons[i] for i in _get_start_codon_indices(start_map)
         }
         self._sense_codons = tuple(
-            c for c in self.codons if self._codon_to_aa[c] != "*"
+            c for c in self.codons if self._codon_to_aa[c] not in "*-X"
         )
         self.anticodons = tuple(self.moltype.rc(codon) for codon in self.codons)
-        self._translate_plus = _make_converter(
-            trinuc_alpha, self.codons, ncbi_code_sequence
-        )
-        self._translate_minus = _make_converter(
-            trinuc_alpha, self.anticodons, ncbi_code_sequence
-        )
+        self._translate_plus = _make_converter(trinuc_alpha, self.codons, code_seq)
+        self._translate_minus = _make_converter(trinuc_alpha, self.anticodons, code_seq)
 
     def __hash__(self):
         return hash(self.name)
@@ -182,7 +203,7 @@ class GeneticCode:
     def translate(
         self, dna: StrORBytesORArray, start: int = 0, rc: bool = False
     ) -> str:
-        """Translates DNA to protein with current GeneticCode.
+        """Translates DNA to protein.
 
         Parameters
         ----------
@@ -196,6 +217,9 @@ class GeneticCode:
         Notes
         -----
         Sequences are truncated to be a multiple of 3.
+        Codons containing ambiguous nucleotides are translated as 'X',
+        codons containing a gap character are translated as '-'. Codons with
+        a mix of ambiguous nucleotides and gaps are translated as 'X'.
 
         Returns
         -------
@@ -209,12 +233,12 @@ class GeneticCode:
             dna = dna[:-diff]
 
         # convert to indices and then bytes
-        seq = self.codons.to_indices(dna).tobytes()
+        seq = self.codons.to_indices(dna)
 
         if rc:
-            return self._translate_minus(seq).decode("utf8")[::-1]
+            return self._translate_minus(seq.tobytes()).decode("utf8")[::-1]
 
-        return self._translate_plus(seq).decode("utf8")
+        return self._translate_plus(seq.tobytes()).decode("utf8")
 
     def sixframes(self, seq: str) -> typing.Iterable[typing.Tuple[str, int, str]]:
         """Returns the six reading frames of the genetic code.
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index f47b45330..e5e1e9631 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -24,7 +24,6 @@ from random import shuffle
 from numpy import array, floating, integer, issubdtype
 
 from cogent3._version import __version__
-from cogent3.core import new_alphabet
 from cogent3.core.annotation import Feature
 from cogent3.core.annotation_db import (
     BasicAnnotationDb,
@@ -33,8 +32,10 @@ from cogent3.core.annotation_db import (
     SupportsFeatures,
     load_annotations,
 )
+from cogent3.core.genetic_code import get_code
 from cogent3.core.info import Info as InfoClass
 from cogent3.core.location import FeatureMap, IndelMap, LostSpan
+from cogent3.core.new_alphabet import AlphabetError
 from cogent3.format.fasta import alignment_to_fasta
 from cogent3.maths.stats.contingency import CategoryCounts
 from cogent3.maths.stats.number import CategoryCounter
@@ -103,7 +104,7 @@ class Sequence:
         """
         self.moltype = moltype
         self.name = name
-        self._seq = _coerce_to_seqview(seq, name, self.moltype.most_degen_alphabet())
+        self._seq = _coerce_to_seqview(seq, name)
         info = info or {}
         self.info = InfoClass(**info)
         self._repr_policy = dict(num_pos=60)
@@ -113,12 +114,6 @@ class Sequence:
     def __str__(self):
         return str(self._seq)
 
-    def __bytes__(self):
-        return bytes(self._seq)
-
-    def __array__(self):
-        return array(self._seq)
-
     def to_fasta(self, make_seqlabel=None, block_size=60):
         """Return string of self in FASTA format, no trailing newline
 
@@ -552,7 +547,6 @@ class Sequence:
         Returns 0 if one sequence is empty.
         """
         # refactor: simplify
-        # refactor: array - make use of self._seq.array_value
         if not self or not other:
             return 0.0
 
@@ -1087,7 +1081,7 @@ class Sequence:
 
         s = moltype.coerce_str(self._seq.value)
         moltype.verify_sequence(s, gaps_allowed=True, wildcards_allowed=True)
-        sv = SeqView(seq=s, alphabet=moltype.most_degen_alphabet())
+        sv = SeqView(seq=s)
         new = moltype.make_seq(sv, name=self.name, info=self.info)
         new.annotation_db = self.annotation_db
         return new
@@ -1684,7 +1678,7 @@ class NucleicAcidSequenceMixin:
             return gc.is_stop(end3)
 
         if strict:
-            raise new_alphabet.AlphabetError(f"{self.name!r} length not divisible by 3")
+            raise AlphabetError(f"{self.name!r} length not divisible by 3")
 
         return False
 
@@ -1778,12 +1772,12 @@ class NucleicAcidSequenceMixin:
         #  with stop. The genetic code should also do the translation off
         #  raw data, either an array of ints or a string. So this method
         #  should only deal with trimming terminal stops, modulo 3 etc...
-        from cogent3.core import new_genetic_code, new_moltype
+        from cogent3.core import new_moltype
 
         protein = new_moltype.get_moltype(
             "protein_with_stop" if include_stop else "protein"
         )
-        gc = new_genetic_code.get_code(gc)
+        gc = get_code(gc)
         codon_alphabet = gc.get_alphabet(include_stop=include_stop).with_gap_motif()
         moltype = self.moltype
         # translate the codons
@@ -1800,9 +1794,9 @@ class NucleicAcidSequenceMixin:
                 resolved = moltype.resolve_ambiguity(
                     orig_codon, alphabet=codon_alphabet
                 )
-            except new_alphabet.AlphabetError:
+            except AlphabetError:
                 if not incomplete_ok or "-" not in orig_codon:
-                    raise new_alphabet.AlphabetError(
+                    raise AlphabetError(
                         f"unresolvable codon {orig_codon!r} in {self.name}"
                     )
                 resolved = (orig_codon,)
@@ -1813,16 +1807,14 @@ class NucleicAcidSequenceMixin:
                 elif "-" in codon:
                     aa = "?"
                     if not incomplete_ok:
-                        raise new_alphabet.AlphabetError(
-                            f"incomplete codon {codon} in {self.name}"
-                        )
+                        raise AlphabetError(f"incomplete codon {codon} in {self.name}")
                 else:
                     aa = gc[codon]
                     if aa == "*" and not include_stop:
                         continue
                 trans.append(aa)
             if not trans:
-                raise new_alphabet.AlphabetError(orig_codon)
+                raise AlphabetError(orig_codon)
             aa = protein.what_ambiguity(trans)
             translation.append(aa)
 
@@ -1913,11 +1905,11 @@ class SliceRecordABC(ABC):
     seq_len refers to a Python typing.Sequence object, e.g. array, str, list.
     """
 
-    __slots__ = ("start", "stop", "step", "_offset")
-
-    @property
-    @abstractmethod
-    def seq_len(self) -> int: ...
+    start: int
+    stop: int
+    step: int
+    seq_len: int
+    _offset: int
 
     @abstractmethod
     def _get_init_kwargs(self) -> dict:
@@ -1928,8 +1920,6 @@ class SliceRecordABC(ABC):
     @abstractmethod
     def copy(self): ...
 
-    # refactor: design
-    # can we remove the need for this method on the ABC and inheriting
     @property
     @abstractmethod
     def _zero_slice(self): ...
@@ -2006,11 +1996,12 @@ class SliceRecordABC(ABC):
         seq_index, _, _ = self._get_index(rel_index, include_boundary=include_boundary)
 
         offset = self.offset
-        return (
-            offset + self.seq_len + seq_index + 1
-            if self.is_reversed
-            else offset + seq_index
-        )
+        if self.is_reversed:
+            abs_index = offset + self.seq_len + seq_index + 1
+        else:
+            abs_index = offset + seq_index
+
+        return abs_index
 
     def relative_position(self, abs_index: int, stop: bool = False):
         """converts an index on the original "Python sequence" into an index
@@ -2290,60 +2281,13 @@ class SliceRecordABC(ABC):
         )
 
 
-class SeqViewABC(ABC):
-    # refactor: docstring
-
-    __slots__ = ()
-
-    @property
-    def seqid(self) -> str: ...
-
-    @property
-    @abstractmethod
-    def str_value(self) -> str: ...
-
-    @property
-    @abstractmethod
-    def array_value(self) -> array: ...
-
-    @property
-    @abstractmethod
-    def bytes_value(self) -> bytes: ...
-
-    @abstractmethod
-    def copy(self, sliced: bool = False): ...
-
-    def __str__(self) -> str:
-        return self.str_value
-
-    def __array__(self, dtype=None):
-        arr = self.array_value
-        if dtype is not None:
-            arr = arr.astype(dtype)
-        return arr
-
-    def __bytes__(self):
-        return self.bytes_value
-
-
-class SeqView(SeqViewABC, SliceRecordABC):
-    # refactor: docstring
-    __slots__ = (
-        "seq",
-        "alphabet",
-        "start",
-        "stop",
-        "step",
-        "_offset",
-        "_seqid",
-        "_seq_len",
-    )
+class SeqView(SliceRecordABC):
+    __slots__ = ("seq", "start", "stop", "step", "_offset", "_seqid", "_seq_len")
 
     def __init__(
         self,
         *,
         seq: str,
-        alphabet: new_alphabet.AlphabetABC,
         start: OptInt = None,
         stop: OptInt = None,
         step: OptInt = None,
@@ -2354,7 +2298,7 @@ class SeqView(SeqViewABC, SliceRecordABC):
         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(seq), start, stop, step)
         self.seq = seq
@@ -2370,7 +2314,7 @@ class SeqView(SeqViewABC, SliceRecordABC):
 
     @property
     def _zero_slice(self):
-        return self.__class__(seq="", alphabet=self.alphabet)
+        return self.__class__(seq="")
 
     @property
     def seqid(self) -> str:
@@ -2381,19 +2325,32 @@ class SeqView(SeqViewABC, SliceRecordABC):
         return self._seq_len
 
     def _get_init_kwargs(self):
-        return {"seq": self.seq, "seqid": self.seqid, "alphabet": self.alphabet}
+        return {"seq": self.seq, "seqid": self.seqid}
 
     @property
-    def str_value(self):
+    def value(self):
         return self.seq[self.start : self.stop : self.step]
 
-    @property
-    def array_value(self):
-        return self.alphabet.to_indices(self.str_value)
+    def replace(self, old: str, new: str):
+        new_seq = self.seq.replace(old, new)
+        if len(old) == len(new):
+            return self.__class__(
+                seq=new_seq,
+                start=self.start,
+                stop=self.stop,
+                step=self.step,
+                offset=self.offset,
+                seqid=self.seqid,
+                seq_len=self.seq_len,
+            )
 
-    @property
-    def bytes_value(self):
-        return self.str_value.encode("utf-8")
+        return self.__class__(seq=new_seq)
+
+    def __iter__(self):
+        return iter(self.value)
+
+    def __str__(self) -> str:
+        return self.value
 
     def __repr__(self) -> str:
         seq = f"{self.seq[:10]}...{self.seq[-5:]}" if self.seq_len > 15 else self.seq
@@ -2439,12 +2396,11 @@ class SeqView(SeqViewABC, SliceRecordABC):
         if not sliced:
             return self.__class__(
                 seq=self.seq,
-                seqid=self.seqid,
-                alphabet=self.alphabet,
                 start=self.start,
                 stop=self.stop,
                 step=self.step,
                 offset=self.offset,
+                seqid=self.seqid,
                 seq_len=self.seq_len,
             )
         return self.from_rich_dict(self.to_rich_dict())
@@ -2469,40 +2425,40 @@ class RnaSequence(Sequence, NucleicAcidSequenceMixin):
 
 
 @singledispatch
-def _coerce_to_seqview(data, seqid, alphabet) -> SeqViewABC:
+def _coerce_to_seqview(data, seqid):
     from cogent3.core.alignment import Aligned
 
     if isinstance(data, Aligned):
-        return _coerce_to_seqview(str(data), seqid, alphabet)
+        return _coerce_to_seqview(str(data), seqid)
     raise NotImplementedError(f"{type(data)}")
 
 
 @_coerce_to_seqview.register
-def _(data: SeqViewABC, seqid, alphabet) -> SeqViewABC:
+def _(data: SeqView, seqid):
     return data
 
 
 @_coerce_to_seqview.register
-def _(data: Sequence, seqid, alphabet) -> SeqViewABC:
-    return _coerce_to_seqview(data._seq, seqid, alphabet)
+def _(data: Sequence, seqid):
+    return _coerce_to_seqview(data._seq, seqid)
 
 
 @_coerce_to_seqview.register
-def _(data: str, seqid, alphabet) -> SeqViewABC:
-    return SeqView(seq=data, seqid=seqid, alphabet=alphabet)
+def _(data: str, seqid):
+    return SeqView(seq=data, seqid=seqid)
 
 
 @_coerce_to_seqview.register
-def _(data: bytes, seqid, alphabet) -> SeqViewABC:
+def _(data: bytes, seqid):
     data = data.decode("utf8")
-    return SeqView(seq=data, seqid=seqid, alphabet=alphabet)
+    return SeqView(seq=data, seqid=seqid)
 
 
 @_coerce_to_seqview.register
-def _(data: tuple, seqid, alphabet) -> SeqViewABC:
-    return _coerce_to_seqview("".join(data), seqid, alphabet)
+def _(data: tuple, seqid):
+    return _coerce_to_seqview("".join(data), seqid)
 
 
 @_coerce_to_seqview.register
-def _(data: list, seqid, alphabet) -> SeqViewABC:
-    return _coerce_to_seqview("".join(data), seqid, alphabet)
+def _(data: list, seqid):
+    return _coerce_to_seqview("".join(data), seqid)


Test patch:
diff --git a/tests/test_core/test_alignment.py b/tests/test_core/test_alignment.py
index e5b849b74..d1a22e54e 100644
--- a/tests/test_core/test_alignment.py
+++ b/tests/test_core/test_alignment.py
@@ -522,7 +522,7 @@ class SequenceCollectionBaseTests(object):
         got = aln.degap()
         self.assertEqual(got.info.path, "blah")
 
-    def test_with_modified_termini(self):  # will not port
+    def test_with_modified_termini(self):
         """SequenceCollection.with_modified_termini should code trailing gaps as ?"""
         aln = self.Class({"s1": "AATGR--", "s2": "-T-AG?-"}, moltype=DNA)
         self.assertEqual(
@@ -2074,7 +2074,7 @@ def test_to_dna_raises():
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment))
-def test_annotate_from_gff3(cls):  # will not port
+def test_annotate_from_gff3(cls):  # ported for SequenceCollection
     """annotate_from_gff should work on data from gff3 files"""
     fasta_path = os.path.join("data/c_elegans_WS199_dna_shortened.fasta")
     gff3_path = os.path.join("data/c_elegans_WS199_shortened_gff.gff3")
@@ -2914,7 +2914,7 @@ def test_to_unaligned_seqs(data):
 
 @pytest.mark.parametrize("moltype", ("dna", "protein"))
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment, ArrayAlignment))
-def test_same_moltype(cls, moltype):  # ported for SequenceCollection
+def test_same_moltype(cls, moltype):
     moltype = get_moltype(moltype)
     data = dict(s1="ACGTT", s2="ACCTT")
     seqs = cls(data, moltype=moltype)
@@ -2923,7 +2923,7 @@ def test_same_moltype(cls, moltype):  # ported for SequenceCollection
 
 
 @pytest.mark.parametrize("cls", (SequenceCollection, Alignment, ArrayAlignment))
-def test_add(cls):  # will not port for SequenceCollection
+def test_add(cls):
     """__add__ should concatenate sequence data, by name"""
     align1 = cls({"a": "AAAA", "b": "TTTT", "c": "CCCC"})
     align2 = cls({"a": "GGGG", "b": "----", "c": "NNNN"})
@@ -3185,7 +3185,7 @@ def test_aln_from_fasta_parser(cls):
     "d", ({"a": "AAAAA", "b": "BBBBB"}, {"a": b"AAAAA", "b": b"BBBBB"})
 )
 @pytest.mark.parametrize("cls", (Alignment, ArrayAlignment, SequenceCollection))
-def test_init_dict(cls, d):  # will not port for SequenceCollection
+def test_init_dict(cls, d):
     """SequenceCollection init from dict should work as expected"""
     # from bytes strings
     a = cls(d, names=["a", "b"])
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index e9e83999c..1e4436361 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -1,6 +1,4 @@
-import json
 import os
-import pathlib
 import re
 
 from warnings import catch_warnings, filterwarnings
@@ -14,8 +12,7 @@ import cogent3.core.new_moltype as new_moltype
 import cogent3.core.new_sequence as new_seq
 
 # todo: kath, update to using new_sequence.load_seq when implemented
-from cogent3 import load_seq, open_
-from cogent3.core.annotation import Feature
+from cogent3 import load_seq
 from cogent3.core.annotation_db import GffAnnotationDb, load_annotations
 
 
@@ -65,7 +62,7 @@ def sdv_s2(dna_sd: new_aln.SeqsData) -> new_aln.SeqDataView:
     return dna_sd.get_seq_view(seqid="seq2")
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture
 def seqs() -> new_aln.SequenceCollection:
     data = {"seq1": "AAAAAA", "seq2": "TTTT", "seq3": "ATTCCCC"}
     return new_aln.make_unaligned_seqs(data, moltype="dna")
@@ -151,7 +148,7 @@ def test_seqs_data_default_attributes(dna_sd: new_aln.SeqsData):
 
 def test_seqs_data_view_zero_step_raises(dna_sd):
     with pytest.raises(ValueError):
-        new_aln.SeqDataView(seq=dna_sd, step=0, seq_len=4)
+        new_aln.SeqDataView(seqs=dna_sd, step=0, seq_len=4)
 
 
 @pytest.mark.parametrize("seqid", ["seq1", "seq2"])
@@ -200,7 +197,7 @@ def test_seqs_data_get_seq_view(str_seqs_dict, alpha, seqid):
     seq_len = len(seq)
     got = sd.get_seq_view(seqid)
     assert isinstance(got, new_aln.SeqDataView)
-    assert got.seq == sd
+    assert got.seqs == sd
     assert got.stop == seq_len
     assert got.seqid == seqid
     assert got.seq_len == seq_len
@@ -275,7 +272,7 @@ def test_seqs_data_make_seq_setget(dna_sd):
 @pytest.mark.parametrize("seq", ("seq1", "seq2"))
 def test_seqs_data_getitem_str_1(dna_sd, seq):
     got = dna_sd[seq]
-    assert got.seq == dna_sd
+    assert got.seqs == dna_sd
     assert got.seqid == seq
 
 
@@ -283,7 +280,7 @@ def test_seqs_data_getitem_str_1(dna_sd, seq):
 def test_seqs_data_getitem_int(str_seqs_dict, alpha, idx):
     sd = new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
     got = sd[idx]
-    assert got.seq == sd
+    assert got.seqs == sd
     assert got.seqid == list(str_seqs_dict)[idx]
 
 
@@ -293,7 +290,7 @@ def test_seqs_data_getitem_str(
     seqid,
 ):
     got = dna_sd[seqid]
-    assert got.seq == dna_sd
+    assert got.seqs == dna_sd
     assert got.seqid == seqid
 
 
@@ -373,7 +370,7 @@ def test_seqs_data_to_alphabet_invalid():
     ],
 )
 def test_seq_data_view_slice_returns_self(seq1: str, index: slice):
-    sdv = new_aln.SeqDataView(seq=seq1, seqid="seq1", seq_len=len(seq1))
+    sdv = new_aln.SeqDataView(seqs=seq1, seqid="seq1", seq_len=len(seq1))
     got = sdv[index]
     assert isinstance(got, new_aln.SeqDataView)
 
@@ -453,7 +450,7 @@ def test_get_aligned_view(aligned_dict, seqid):
     ad = new_aln.AlignedData.from_gapped_seqs(aligned_dict)
     got = ad.get_aligned_view(seqid)
     assert isinstance(got, new_aln.AlignedDataView)
-    assert got.seq == ad
+    assert got.seqs == ad
     assert got.stop == ad.align_len
     assert got.seq_len == ad.align_len
 
@@ -644,14 +641,6 @@ def test_sequence_collection_init_ordered(ordered1, ordered2):
     assert sec.names == ["c", "a"]
 
 
-@pytest.mark.xfail(reason="todo: kath, load_unaligned_seqs not creating new style seqs")
-def test_sequence_collection_info_source():
-    """info.source exists if load seqs given a filename"""
-    path = pathlib.Path("data/brca1.fasta")
-    seqs = new_aln.load_unaligned_seqs(path)
-    assert seqs.info.source == str(path)
-
-
 def test_sequence_collection_init_ambig():
     """SequenceCollection should tolerate ambiguous chars"""
     _ = new_aln.make_unaligned_seqs(["AAA", "CCC"], moltype="dna")
@@ -938,20 +927,8 @@ def test_sequence_collection_get_seq():
 
 def test_sequence_collection_degap():
     """SequenceCollection.degap should strip gaps from each seq"""
-    data = {"s1": "ATGRY?", "s2": "T-AG??"}
-    seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
-    got = seqs.degap().to_dict()
-    expect = {"s1": "ATGRY", "s2": "TAG"}
-    assert got == expect
-
-
-def test_sequence_collection_degap_info():
-    """.degap should preserve info attributes"""
-    data = {"s1": "ATGRY?", "s2": "T-AG??"}
-    aln = new_aln.make_unaligned_seqs(data, moltype="dna")
-    aln.info.path = "blah"
-    got = aln.degap()
-    assert got.info.path == "blah"
+    seqs = new_aln.make_unaligned_seqs({"s1": "ATGRY?", "s2": "T-AG??"}, moltype="dna")
+    assert seqs.degap().to_dict() == {"s1": "ATGRY", "s2": "TAG"}
 
 
 def test_sequence_collection_to_fasta():
@@ -1040,126 +1017,11 @@ def test_sequence_collection_has_terminal_stop_strict():
         seq_coll.has_terminal_stop(gc=1, strict=True)
 
 
-@pytest.mark.xfail(
-    reason="todo: relies on Sequence.get_translation which requires a codon alphabet class'"
-)
-def test_sequence_collection_get_translation_trim_stop():
-    data = {"seq1": "GATTCCTAG", "seq2": "GATTCCTCC"}
-    seq_coll = new_aln.make_unaligned_seqs(data, moltype="dna")
-    got = seq_coll.get_translation(trim_stop=True)
-    expect = {"seq1": "DS", "seq2": "DSS"}
-    assert got.to_dict() == expect
-
-
-@pytest.mark.xfail(
-    reason="todo: relies on Sequence.get_translation which requires a codon alphabet class'"
-)
-def test_sequence_collection_get_translation_raises():
-    """should raise error if self.moltype is not a nucleic acid"""
-    data = {"seq1": "PAR", "seq2": "PQR"}
-    seq_coll = new_aln.make_unaligned_seqs(data, moltype="protein")
-    with pytest.raises(TypeError):
-        _ = seq_coll.get_translation(trim_stop=True)
-
-
-@pytest.mark.xfail(
-    reason="todo: relies on Sequence.get_translation which requires a codon alphabet class'"
-)
-@pytest.mark.parametrize(
-    "seqs", ({"seq1": "GATTTT", "seq2": "GATC??"}, {"seq1": "GAT---", "seq2": "?GATCT"})
-)
-def test_sequence_collection_get_translation2(seqs):
-    """SequenceCollection.get_translation translates each seq"""
-    seq_coll = new_aln.make_unaligned_seqs(seqs, moltype="dna")
-    got = seq_coll.get_translation()
-    assert len(got) == 2
-    assert got.moltype == new_moltype.PROTEIN
-
-
-@pytest.mark.xfail(
-    reason="todo: relies on Sequence.get_translation which requires a codon alphabet class'"
-)
-def test_sequence_collection_get_translation_with_stop():
-    data = {"seq1": "GATTAG", "seq2": "?GATCT"}
-    seq_coll = new_aln.make_unaligned_seqs(data, moltype="dna")
-    got = seq_coll.get_translation(include_stop=True)
-    assert got.to_dict() == {"seq1": "D*", "seq2": "XS"}
-
-
-@pytest.mark.xfail(
-    reason="todo: relies on Sequence.get_translation which requires a codon alphabet class'"
-)
-@pytest.mark.parametrize(
-    "data", ({"seq1": "GATTTT", "seq2": "GATC??"}, {"seq1": "GAT---", "seq2": "?GATCT"})
-)
-def test_sequence_collection_get_translation_error(data):
-    """SequenceCollection.get_translation translates each seq"""
-    # check for a failure when no moltype specified
-    seq_coll = new_aln.make_unaligned_seqs(data, moltype="dna")
-    with pytest.raises(TypeError):
-        seq_coll.get_translation()
-
-
-@pytest.mark.xfail(
-    reason="todo: relies on Sequence.get_translation which requires a codon alphabet class'"
-)
-@pytest.mark.parametrize(
-    "data",
-    (
-        {"seq1": "GATTTT", "seq2": "GATC??"},
-        {"seq1": "GAT---", "seq2": "?GATCT"},
-    ),
-)
-def test_sequence_collection_get_translation_info(data):
-    """SequenceCollection.get_translation preserves info attribute"""
-    seq_coll = new_aln.make_unaligned_seqs(data, moltype="dna")
-    got = seq_coll.get_translation()
-    assert got.info["key"] == "value"
-
-
-@pytest.mark.xfail(
-    reason="todo: relies on Sequence.get_translation which requires a codon alphabet class'"
-)
-def test_sequence_collection_get_translation_incomplete():
-    """get translation works on incomplete codons"""
-    data = {"seq1": "GATN--", "seq2": "?GATCT"}
-    seq_coll = new_aln.make_unaligned_seqs(data, moltype="dna")
-    got = seq_coll.get_translation(incomplete_ok=True)
-    assert got.to_dict() == {"seq1": "D?", "seq2": "XS"}
-    with pytest.raises(new_alpha.AlphabetError):
-        _ = seq_coll.get_translation(incomplete_ok=False)
-
-
-@pytest.mark.parametrize(
-    "gc,seqs",
-    (
-        (1, ("--AT-CTGA", "GATAAATT?")),
-        (1, ("ACGTGA---", "ACGAC----", "ACGCAATGA")),
-        (1, ("CCTCA-", "ATTTTA")),
-        (2, ("GATTTT", "TCCAGG")),
-    ),
-)
-def test_sequence_collection_trim_stop_codons(gc, seqs):
-    data = {f"s{i}": s for i, s in enumerate(seqs)}
-
-    expect = {}
-    for k, v in data.items():
-        if "-" in v:
-            v = re.sub("(TGA|AGG)(?=[-]*$)", "---", v)
-        else:
-            v = re.sub("(TGA|AGG)", "", v)
-        expect[k] = v
-    seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
-    got = seqs.trim_stop_codons(gc=gc).to_dict()
-
-    assert got == expect
-
-
 @pytest.mark.parametrize(
     "gc,seqs",
     ((1, ("T-CTGC", "GATAA?")), (2, ("GATTTT", "TCCCGG")), (1, ("CCTGC", "GATAA"))),
 )
-def test_sequence_collection_trim_stop_codons_no_stop(gc, seqs):
+def test_sequence_collection_trim_terminal_stops_no_stop(gc, seqs):
     data = {f"s{i}": s for i, s in enumerate(seqs)}
     seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
     got = seqs.trim_stop_codons(gc=gc)
@@ -1169,7 +1031,7 @@ def test_sequence_collection_trim_stop_codons_no_stop(gc, seqs):
 @pytest.mark.parametrize(
     "data", ({"s1": "CCTCA", "s2": "ATTTT"}, {"s1": "CCTCA-", "s2": "ATTTTA"})
 )
-def test_sequence_collection_trim_stop_codons_strict(data):
+def test_sequence_collection_trim_terminal_stops_strict(data):
     seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
     with pytest.raises(new_alpha.AlphabetError):
         seqs.trim_stop_codons(gc=1, strict=True)
@@ -1178,26 +1040,13 @@ def test_sequence_collection_trim_stop_codons_strict(data):
 def test_sequence_collection_trim_stop_codons_info():
     """trim_stop_codons should preserve info attribute"""
     data = {"seq1": "ACGTAA", "seq2": "ACGACG", "seq3": "ACGCGT"}
-    seq_coll = new_aln.make_unaligned_seqs(
+    coll = new_aln.make_unaligned_seqs(
         data,
         moltype="dna",
         info={"key": "value"},
     )
-    seq_coll = seq_coll.trim_stop_codons()
-    assert seq_coll.info["key"] == "value"
-
-
-def test_sequence_collection_trim_stop_codons_annotation_db(gff_db):
-    """trim_stop_codons should preserve info attribute"""
-    data = {"seq_1": "ACGTAA", "seq_2": "ACGACG", "seq_3": "ACGCGT"}
-    seq_coll = new_aln.make_unaligned_seqs(
-        data,
-        moltype="dna",
-        info={"key": "value"},
-    )
-    seq_coll.annotation_db = gff_db
-    trimmed = seq_coll.trim_stop_codons()
-    assert trimmed.annotation_db == seq_coll.annotation_db
+    coll = coll.trim_stop_codons()
+    assert coll.info["key"] == "value"
 
 
 @pytest.fixture(scope="function")
@@ -1300,17 +1149,7 @@ def test_sequence_collection_get_identical_sets_text():
     assert got == expect
 
 
-def test_sequence_collection_get_identical_sets_ragged_raises(ragged):
-    with pytest.raises(ValueError):
-        _ = ragged.get_identical_sets()
-
-
-def rev(seq):
-    return seq[::-1]
-
-
-@pytest.mark.parametrize("transform", [rev, lambda x: x, None])
-def test_sequence_collection_get_similar(transform):
+def test_sequence_collection_get_similar():
     data = {
         "a": "AAAAAAAAAA",  # target
         "b": "AAAAAAAAAA",  # identical
@@ -1321,17 +1160,13 @@ def test_sequence_collection_get_similar(transform):
     }
     seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
     target = seqs.get_seq("a")
-    got = seqs.get_similar(target, min_similarity=0.8, transform=transform)
+    got = seqs.get_similar(target, min_similarity=0.8)
     assert got.names == ["a", "b", "c", "d"]
-    got = seqs.get_similar(target, min_similarity=0.81, transform=transform)
+    got = seqs.get_similar(target, min_similarity=0.81)
     assert got.names == ["a", "b", "c"]
-    got = seqs.get_similar(
-        target, min_similarity=0.75, max_similarity=0.9, transform=transform
-    )
+    got = seqs.get_similar(target, min_similarity=0.75, max_similarity=0.9)
     assert got.names == ["c", "d"]
-    got = seqs.get_similar(
-        target, min_similarity=0.75, max_similarity=0.89, transform=transform
-    )
+    got = seqs.get_similar(target, min_similarity=0.75, max_similarity=0.89)
     assert got.names == ["d"]
 
 
@@ -1344,60 +1179,6 @@ def test_sequence_collection_init_annotated_seqs():
     assert len(features) == 1
 
 
-def test_sequence_collection_get_motif_probs():
-    data = {"a": "AC-", "b": "AC", "c": "AC"}
-    aln = new_aln.make_unaligned_seqs(data, moltype="dna")
-    got = aln.get_motif_probs()
-    expect = {"A": 0.5, "C": 0.5, "G": 0.0, "T": 0.0}
-    assert got == expect
-
-    # exclude unobserved
-    data = {"a": "AC-", "b": "AC", "c": "AC"}
-    aln = new_aln.make_unaligned_seqs(data, moltype="dna")
-    got = aln.get_motif_probs(exclude_unobserved=True)
-    expect = {"A": 0.5, "C": 0.5}
-    assert got == expect
-
-    # allow gap
-    data = {"a": "----", "b": "ACGT", "c": "ACGT"}
-    aln = new_aln.make_unaligned_seqs(data, moltype="dna")
-    got = aln.get_motif_probs(allow_gap=True)
-    expect = {"A": 2 / 12, "C": 2 / 12, "G": 2 / 12, "T": 2 / 12, "-": 4 / 12}
-    assert got == expect
-
-    # add pseudocounts
-    data = {"a": "AC-", "b": "AC", "c": "AC"}
-    aln = new_aln.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}
-
-    # pseudocount and allow gap
-    data = {"a": "AC-", "b": "AC", "c": "AC"}
-    aln = new_aln.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}
-
-
-def test_sequence_collection_get_motif_probs_protein():
-    data = {"a": "MVSB", "b": "MVS", "c": "MVP"}
-    aln = new_aln.make_unaligned_seqs(data, moltype="protein")
-    got = aln.get_motif_probs()
-    alphabet = aln.moltype.alphabet
-    expect = {k: 0 for k in alphabet}
-    expect["M"] = 1 / 3
-    expect["V"] = 1 / 3
-    expect["S"] = 2 / 9
-    expect["P"] = 1 / 9
-    assert got == expect
-
-    got = aln.get_motif_probs(exclude_unobserved=True)
-    expect = {"M": 1 / 3, "V": 1 / 3, "S": 2 / 9, "P": 1 / 9}
-    assert got == expect
-
-    got = aln.get_motif_probs(include_ambiguity=True, exclude_unobserved=True)
-    expect = {"M": 3 / 10, "V": 3 / 10, "S": 2 / 10, "P": 1 / 10, "B": 1 / 10}
-
-
 def test_sequence_collection_counts_per_seq():
     """SequenceCollection.counts_per_seq handles motif length, allow_gaps etc.."""
     data = {"a": "AAAA??????", "b": "CCCGGG--NN", "c": "CCGGTTCCAA"}
@@ -1425,24 +1206,6 @@ def test_sequence_collection_counts_per_seq():
         assert c[k] == expect[k]
 
 
-def test_sequence_collection_probs_per_seq():
-    data = {"seq1": "AA??", "seq2": "CG-N", "seq3": "CGAA"}
-    coll = new_aln.make_unaligned_seqs(data, moltype="dna")
-    got = coll.probs_per_seq()
-    assert got["seq1", "A"] == 1.0
-    assert got["seq2", "C"] == 0.5
-    assert got["seq2", "G"] == 0.5
-    assert got["seq3", "C"] == 0.25
-    assert got["seq3", "G"] == 0.25
-    assert got["seq3", "A"] == 0.5
-
-    got = coll.probs_per_seq(allow_gap=True)
-    assert got["seq2", "-"] == 1 / 3
-
-    got = coll.probs_per_seq(include_ambiguity=True, allow_gap=True)
-    assert got["seq2", "N"] == 0.25
-
-
 def test_sequence_collection_counts():
     """SequenceCollection.counts handles motif length, allow_gaps etc.."""
     data = {"a": "AAAA??????", "b": "CCCGGG--NN"}
@@ -1456,7 +1219,7 @@ def test_sequence_collection_counts():
     assert all(got[k] == v for k, v in expect.items())
 
     got = coll.counts(motif_length=2, allow_gap=True)
-    expect["--"] = 1
+    expect.update({"--": 1})
     assert all(got[k] == v for k, v in expect.items())
 
     got = coll.counts(motif_length=2, include_ambiguity=True, allow_gap=True)
@@ -1512,59 +1275,6 @@ def test_sequence_collection_get_annotations_from_any_seq():
     assert "biotype='exon', name='annotation3', map=[3:6]/9" in str(got[0])
 
 
-def test_sequence_collection_add_feature(seqs):
-    _ = seqs.add_feature(seqid="seq1", biotype="xyz", name="abc", spans=[(1, 2)])
-    assert seqs.annotation_db.num_matches() == 1
-
-    # if seqid is not in seqs, raise error
-    with pytest.raises(ValueError):
-        _ = seqs.add_feature(seqid="bad_seq", biotype="xyz", name="abc", spans=[(1, 2)])
-
-
-def test_sequence_collection_make_feature(seqs):
-    data = {"seqid": "seq1", "biotype": "xyz", "name": "abc", "spans": [(1, 2)]}
-    feat = seqs.make_feature(feature=data)
-    assert isinstance(feat, Feature)
-    assert seqs.annotation_db.num_matches() == 0
-
-
-def test_sequence_collection_get_features(seqs):
-    # no annotation db should return empty list
-    got = seqs.get_features(seqid="seq1")
-    assert not list(got)
-
-    seqs.add_feature(seqid="seq1", biotype="xyz", name="abc", spans=[(1, 3)])
-    seqs.add_feature(seqid="seq2", biotype="xyz", name="abc", spans=[(1, 3)])
-    seqs.add_feature(seqid="seq3", biotype="xyz", name="abcde", spans=[(3, 5)])
-    got = seqs.get_features(seqid="seq1")
-    assert len(list(got)) == 1
-
-    got = seqs.get_features(biotype="xyz")
-    assert len(list(got)) == 3
-
-    got = seqs.get_features(name="abc")
-    assert len(list(got)) == 2
-
-    got = seqs.get_features(biotype="xyz", name="abc")
-    assert len(list(got)) == 2
-
-    got = seqs.get_features(biotype="xyz", start=0, stop=5, allow_partial=True)
-    assert len(list(got)) == 3
-
-    got = seqs.get_features(start=2, stop=6, allow_partial=False)
-    assert len(list(got)) == 1
-
-    got = seqs.get_features(biotype="xyz", start=2, stop=4, allow_partial=True)
-    assert len(list(got)) == 3
-
-    with pytest.raises(ValueError):
-        _ = list(
-            seqs.get_features(
-                seqid="bad_seq", biotype="xyz", start=2, stop=4, allow_partial=True
-            )
-        )
-
-
 def test_sequence_collection_copy_annotations(gff_db):
     """copy_annotations copies records from annotation db"""
     data = {"seq1": "ACGU", "seq2": "CGUA", "test_seq": "CCGU"}
@@ -1575,53 +1285,6 @@ def test_sequence_collection_copy_annotations(gff_db):
     seq_coll.copy_annotations(gff_db)
     assert seq_coll.annotation_db.num_matches() == expect
 
-    # copy annotations with no current annotations
-    seq_coll = new_aln.make_unaligned_seqs(data, moltype="rna")
-    seq_coll.copy_annotations(gff_db)
-    assert seq_coll.annotation_db.num_matches() == gff_db.num_matches()
-
-
-@pytest.mark.xfail(reason="todo: this just hangs... ")
-def test_sequence_collection_copy_annotations_same_annotations(gff_db):
-    # define a timeout for this test at 5 seconds
-    import signal
-
-    def handler(signum, frame):
-        raise TimeoutError("Test timed out")
-
-    signal.signal(signal.SIGALRM, handler)
-    signal.alarm(5)
-
-    data = {"seq1": "ACGU", "seq2": "CGUA", "test_seq": "CCGU"}
-    seq_coll = new_aln.make_unaligned_seqs(data, moltype="rna")
-    seq_coll = new_aln.make_unaligned_seqs(data, moltype="rna")
-
-    # copy annotations with the same annotation_db
-    seq_coll.annotation_db = gff_db
-    seq_coll.copy_annotations(gff_db)
-
-    assert seq_coll.annotation_db.num_matches() == gff_db.num_matches()
-
-
-def test_sequence_collection_copy_annotations_none_matching(gff_db):
-    """copy annotations should old copy annotations for matching seqids"""
-    data = {"name1": "ACGU", "name2": "CGUA", "name_3": "CCGU"}
-    seq_coll = new_aln.make_unaligned_seqs(data, moltype="rna")
-    seq_coll.add_feature(seqid="name1", biotype="xyz", name="abc", spans=[(1, 2)])
-    seq_coll.add_feature(seqid="name2", biotype="xyzzz", name="abc", spans=[(1, 2)])
-    expect = seq_coll.annotation_db.num_matches()
-    assert gff_db.num_matches() > 0
-    seq_coll.copy_annotations(gff_db)
-    assert seq_coll.annotation_db.num_matches() == expect
-
-
-def test_sequence_collection_copy_annotations_no_db(gff_db):
-    data = {"seq1": "ACGU", "seq2": "CGUA", "test_seq": "CCGU"}
-    seq_coll = new_aln.make_unaligned_seqs(data, moltype="rna")
-
-    seq_coll.copy_annotations(gff_db)
-    assert seq_coll.annotation_db.num_matches() == gff_db.num_matches()
-
 
 def test_sequence_collection_get_seq_annotated():
     """SequenceCollection.get_seq should return specified seq"""
@@ -1681,14 +1344,6 @@ def test_sequence_collection_copy_annotations_incompat_type_fails(seqcoll_db, se
         seqcoll_db.copy_annotations(seqs)
 
 
-@pytest.mark.parametrize("moltype", ("dna", "protein"))
-def test_sequence_collection_to_moltype_same_moltype(moltype):
-    data = dict(s1="ACGTT", s2="ACCTT")
-    seqs = new_aln.make_unaligned_seqs(data, moltype=moltype)
-    got = seqs.to_moltype(moltype=moltype)
-    assert got is seqs
-
-
 def test_sequence_collection_to_moltype_with_gaps():
     """correctly convert to specified moltype"""
     data = {"seq1": "ACGTACGTA", "seq2": "ACCGAA---", "seq3": "ACGTACGTT"}
@@ -1697,11 +1352,6 @@ def test_sequence_collection_to_moltype_with_gaps():
     assert dna_seqs.moltype.label == "dna"
     assert dna_seqs.to_dict() == data
 
-    # we can convert from text to dna to rna
-    rna_seqs = dna_seqs.to_moltype("rna")
-    assert rna_seqs.moltype.label == "rna"
-
-    # but not from text to rna directly
     with pytest.raises(ValueError):
         seqs.to_moltype("rna")
 
@@ -1727,81 +1377,27 @@ def test_sequence_collection_to_moltype_annotation_db():
     assert len(dna.annotation_db) == 3
 
 
-def test_sequence_collection_to_dna():
-    """correctly convert to dna"""
-    data = {"seq1": "ACGTACGTA", "seq2": "ACCGAA---", "seq3": "ACGTACGTT"}
-    seqs = new_aln.make_unaligned_seqs(data, moltype="text")
-
-    # convert from text to dna
-    dna_seqs = seqs.to_dna()
-    assert dna_seqs.moltype.label == "dna"
-    assert dna_seqs.to_dict() == data
-
-    # convert from rna to dna
-    data = {"seq1": "ACGUACGUA", "seq2": "ACCGAA---", "seq3": "ACGUACGUU"}
-    seqs = new_aln.make_unaligned_seqs(data, moltype="RNA")
-    dna_seqs = seqs.to_dna()
-    assert dna_seqs.moltype.label == "dna"
-    assert dna_seqs.to_dict() == {
-        name: seq.replace("U", "T") for name, seq in data.items()
-    }
-
-
-def test_sequence_collection_to_rna():
-    data = {"seq1": "ACGUACGUA", "seq2": "ACCGAA---", "seq3": "ACGUACGUU"}
-    seqs = new_aln.make_unaligned_seqs(data, moltype="text")
-
-    # convert from text to rna
-    rna_seqs = seqs.to_rna()
-    assert rna_seqs.moltype.label == "rna"
-    assert rna_seqs.to_dict() == data
-
-    # convert from dna to rna
+def test_sequence_collection_to_moltype_same_moltype():
     data = {"seq1": "ACGTACGTA", "seq2": "ACCGAA---", "seq3": "ACGTACGTT"}
     seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
-    rna_seqs = seqs.to_rna()
-    assert rna_seqs.moltype.label == "rna"
-    assert rna_seqs.to_dict() == {
-        name: seq.replace("T", "U") for name, seq in data.items()
-    }
+    to_same_seqs = seqs.to_moltype("dna")
+    assert to_same_seqs is seqs
 
 
-@pytest.fixture(scope="function")
-def dotplot_seqs():
+def test_sequence_collection_dotplot():
+    """exercising dotplot method"""
     data = {
         "Human": "CAGATTTGGCAGTT-",
         "Mouse": "CAGATTCAGCAGGTG",
-        "Rat": "CAGATTCAGCAGG--G",
+        "Rat": "CAGATTCAGCAGGTG",
     }
-    return new_aln.make_unaligned_seqs(data, moltype="dna")
-
-
-def test_sequence_collection_dotplot(dotplot_seqs):
-    """exercising dotplot method"""
-    # with provided names
-    plot = dotplot_seqs.dotplot(name1="Human", name2="Mouse")
-    assert str(plot.seq1) != str(plot.seq2)
-
-    # without providing names
-    plot = dotplot_seqs.dotplot()
-    assert str(plot.seq1) != str(plot.seq2)
-
-    # providing only one name
-    plot = dotplot_seqs.dotplot(name1="Human")
-    assert str(plot.seq1) != str(plot.seq2)
-
-    # a collection of one sequence should make dotplot with itself
-    less_seqs = dotplot_seqs.take_seqs("Human")
-    plot = less_seqs.dotplot()
-    assert str(plot.seq1) == str(plot.seq2)
-
-    # k larger than window should raise an error
+    seqs = new_aln.make_unaligned_seqs(
+        data,
+        moltype="dna",
+    )
+    _ = seqs.dotplot()
     with pytest.raises(AssertionError):
-        dotplot_seqs.dotplot(window=5, k=11)
-
-    # names not in the collection should raise an error
-    with pytest.raises(ValueError):
-        dotplot_seqs.dotplot(name1="Human", name2="Dog")
+        seqs.dotplot(window=5, k=11)
 
 
 def test_sequence_collection_dotplot_annotated():
@@ -1927,13 +1523,6 @@ def test_sequence_collection_get_lengths(ambigs_coll):
     assert got == expect
 
 
-def test_sequence_collection_get_lengths_include_ambiguity(ambigs_coll):
-    # NOTE '?' is excluded as it could be a gap
-    got = ambigs_coll.get_lengths(include_ambiguity=True)
-    expect = {"a": 4, "b": 8}
-    assert got == expect
-
-
 def test_sequence_collection_get_lengths_allow_gap(ambigs_coll):
     got = ambigs_coll.get_lengths(include_ambiguity=True, allow_gap=True)
     expect = {"a": 10, "b": 10}
@@ -1949,18 +1538,6 @@ def test_sequence_collection_strand_symmetry():
     assert numpy.allclose(result["seq2"].observed.array, [[3, 0], [2, 1]])
 
 
-@pytest.mark.xfail(reason="todo: kath, decide on how to handle rename_seqs")
-def test_sequence_collection_rename_seqs():
-    """successfully rename sequences"""
-    data = {"seq1": "ACGTACGTA", "seq2": "ACCGAA---", "seq3": "ACGTACGTT"}
-    seqs = new_aln.make_unaligned_seqs(data, moltype="dna")
-    new = seqs.rename_seqs(lambda x: x.upper())
-    expect = {n.upper() for n in data}
-    assert set(new.names) == expect
-    # renamed contains the name map as info attribute
-    assert new.info.name_map == {k.upper(): k for k in data}
-
-
 def test_sequence_collection_apply_pssm():
     """should successfully produce pssm scores"""
     from cogent3.parse import jaspar
@@ -2045,18 +1622,6 @@ def test_sequence_collection_get_seq_entropy():
     assert numpy.allclose(entropy, numpy.array([e, 1.5]))
 
 
-@pytest.mark.xfail(reason="todo: kath, design of serialisation is unresolved")
-def test_sequence_collection_write_to_json(tmp_path):
-    # test writing to json file
-    data = {"a": "AAAA", "b": "TTTT", "c": "CCCC"}
-    aln = new_aln.make_unaligned_seqs(data, moltype="dna")
-    path = str(tmp_path / "sample.json")
-    aln.write(path)
-    with open_(path) as fn:
-        got = json.loads(fn.read())
-        assert got == aln.to_rich_dict()
-
-
 @pytest.mark.parametrize("moltype", ("dna", "rna"))
 def test_sequence_collection_distance_matrix_singleton_collection(moltype):
     """SequenceCollection.distance_matrix() should raise error if collection
@@ -2089,7 +1654,7 @@ def test_sequence_collection_distance_matrix_same_seq(moltype):
 
 
 @pytest.mark.parametrize("moltype", ("protein", "text", "bytes"))
-def test_sequence_collection_distance_matrix_raises_wrong_moltype(moltype):
+def test_sequence_collection_distance_matrix_fails_wrong_moltype(moltype):
     data = {"s1": "ACGTA", "s2": "ACGTA"}
     seqs = new_aln.make_unaligned_seqs(data, moltype=moltype)
     with pytest.raises(NotImplementedError):
diff --git a/tests/test_core/test_new_alphabet.py b/tests/test_core/test_new_alphabet.py
index a7af187ae..76dd3f275 100644
--- a/tests/test_core/test_new_alphabet.py
+++ b/tests/test_core/test_new_alphabet.py
@@ -83,6 +83,16 @@ def test_charalphabet_to_indices(seq):
     assert (got == numpy.array([2, 2, 3, 0, 2], dtype=numpy.uint8)).all()
 
 
+@pytest.mark.parametrize("gap", ("", "-"))
+def test_charalphabet_to_indices_non_canonical(gap):
+    # non-canonical characters should be beyond limit of
+    # canonical
+    canonical = list(f"TCAG{gap}")
+    alpha = new_alphabet.CharAlphabet(canonical, gap=gap or None)
+    got = alpha.to_indices("ACNGT")
+    assert got.max() >= len(canonical)
+
+
 def test_charalphabet_to_indices_invalid():
     alpha = new_alphabet.CharAlphabet(list("TCAG"))
     with pytest.raises(TypeError):
@@ -187,6 +197,110 @@ def test_kmer_alphabet_construction(k):
     assert len(kmers) == 4**k
 
 
+@pytest.mark.parametrize("gap_index", (-1, 4))
+@pytest.mark.parametrize("indices", (2, 3, (2, 3)))
+def test_seq_to_kmer_indices_handle_gap(indices, gap_index):
+    k = 2
+    coeffs = new_alphabet.coord_conversion_coeffs(4, k)
+    seq = numpy.array([0, 1, 1, 3], dtype=numpy.uint8)
+    result = numpy.zeros(len(seq) // k, dtype=numpy.uint8)
+    seq.put(indices, gap_index)
+    got = new_alphabet.seq_to_kmer_indices(
+        seq,
+        result,
+        coeffs,
+        4,
+        k,
+        gap_char_index=gap_index,
+        gap_index=-1 if gap_index == -1 else 16,
+        independent_kmer=True,
+    )
+    expect = numpy.array([1, 16], dtype=numpy.uint8)
+    assert (got == expect).all()
+
+
+@pytest.mark.parametrize("gap_index", (-1, 4))
+def test_seq_to_kmer_indices_handle_missing(gap_index):
+    k = 2
+    coeffs = new_alphabet.coord_conversion_coeffs(4, k)
+    # first dinuc has missing, last dinuc has a gap
+    seq = numpy.array([5, 1, 0, 1, 0, 4], dtype=numpy.uint8)
+    result = numpy.zeros(len(seq) // k, dtype=numpy.uint8)
+    expect = numpy.array([17 if gap_index > 0 else 16, 1, 16], dtype=numpy.uint8)
+    got = new_alphabet.seq_to_kmer_indices(
+        seq,
+        result,
+        coeffs,
+        4,
+        k,
+        gap_char_index=gap_index,
+        gap_index=-1 if gap_index == -1 else 16,
+        independent_kmer=True,
+    )
+    assert (got == expect).all()
+
+
+@pytest.mark.parametrize("gap", ("", "-"))
+def test_kmer_alpha_to_indices_to_index_with_gap(gap):
+    alpha = new_alphabet.CharAlphabet(list(f"TCAG{gap}"), gap=gap or None)
+    dinucs = alpha.get_kmer_alphabet(k=2, include_gap=bool(gap))
+    got1 = int(dinucs.to_indices("--")[0])
+    got2 = dinucs.to_index("--")
+    assert got1 == got2 == 16
+
+
+@pytest.mark.parametrize("seq", ("AC", numpy.array([2, 1], dtype=numpy.uint8)))
+def test_kmer_alphabet_to_index_mixed(seq):
+    dna = new_moltype.get_moltype("dna")
+    kmers = dna.alphabet.get_kmer_alphabet(k=2, include_gap=False)
+    kmer_index = kmers.to_index(seq)
+    # we compare result to tuple result
+    assert kmer_index == 9
+
+
+@pytest.mark.parametrize("gap", ("", "-"))
+def test_kmer_index_to_seq(gap):
+    alpha = new_alphabet.CharAlphabet(list(f"TCAG{gap}"), gap=gap or None)
+    dinuc_alpha = alpha.get_kmer_alphabet(k=2, include_gap=bool(gap))
+    seq = alpha.to_indices("ACTG")
+    expect = numpy.array(
+        [dinuc_alpha.index(d) for d in ("AC", "TG")], dtype=numpy.uint8
+    )
+    assert (dinuc_alpha.to_indices(seq) == expect).all()
+
+
+def test_kmer_index_to_gapped_seq():
+    alpha = new_alphabet.CharAlphabet(list("TCAG-"), gap="-")
+    dinuc_alpha = alpha.get_kmer_alphabet(k=2, include_gap=True)
+    seq = alpha.to_indices("AC--TG")
+    expect = numpy.array(
+        [dinuc_alpha.index(d) for d in ("AC", "--", "TG")], dtype=numpy.uint8
+    )
+    assert (dinuc_alpha.to_indices(seq) == expect).all()
+
+
+@pytest.mark.parametrize("gap", ("", "-"))
+@pytest.mark.parametrize("k", (2, 3))
+def test_kmer_alphabet_with_gap_motif(gap, k):
+    alpha = new_alphabet.CharAlphabet(list(f"TCAG{gap}"), gap=gap or None)
+    kmers = alpha.get_kmer_alphabet(k=k)
+    gap_state = "-" * k
+    gapped = kmers.with_gap_motif()
+    assert gap_state in gapped
+    assert gapped.gap_char == gap_state
+    assert gapped.gap_index == len(gapped) - 1
+
+
+@pytest.mark.parametrize("gap", ("", "-"))
+def test_kmer_alpha_to_indices_with_gap(gap):
+    alpha = new_alphabet.CharAlphabet(list(f"TCAG{gap}"), gap=gap or None)
+    dinucs = alpha.get_kmer_alphabet(k=2, include_gap=bool(gap))
+    seq = numpy.array([5, 1, 0, 1, 0, 4], dtype=numpy.uint8)
+    got = dinucs.to_indices(seq)
+    expect = numpy.array([17 if gap else 16, 1, 16], dtype=numpy.uint8)
+    assert (got == expect).all()
+
+
 @pytest.mark.parametrize("k", (2, 3))
 def test_kmer_gapped_alphabet_construction(k):
     dna = new_moltype.get_moltype("dna")
@@ -290,6 +404,28 @@ def test_kmer_alphabet_to_indices_invalid():
         trinuc_alpha.to_indices(["ACG", "TGG"])
 
 
+@pytest.mark.parametrize("seq", ("ACGT", "ACYG", "NN"))
+def test_kmer_alphabet_is_valid(seq):
+    dna = new_moltype.get_moltype("dna")
+    dinuc_alpha = dna.alphabet.get_kmer_alphabet(k=2, include_gap=False)
+    dinucs = dinuc_alpha.to_indices(seq)
+    assert dinuc_alpha.is_valid(dinucs)
+
+
+def test_kmer_alphabet_not_is_valid():
+    dna = new_moltype.get_moltype("dna")
+    seq = numpy.array([2, 5, 3, 17], dtype=numpy.uint8)
+    dinuc_alpha = dna.alphabet.get_kmer_alphabet(k=2, include_gap=False)
+    assert not dinuc_alpha.is_valid(seq)
+
+
+def test_kmer_alphabet_invalid_is_valid():
+    dna = new_moltype.get_moltype("dna")
+    dinuc_alpha = dna.alphabet.get_kmer_alphabet(k=2, include_gap=False)
+    with pytest.raises(TypeError):
+        dinuc_alpha.is_valid("ACGT")
+
+
 def test_kmer_alphabet_from_indices_independent():
     dna = new_moltype.get_moltype("dna")
     seq = "ATGGGCAGA"
@@ -300,6 +436,16 @@ def test_kmer_alphabet_from_indices_independent():
     assert_allclose(got, arr)
 
 
+def test_kmer_alphabet_from_indices_independent_with_gap():
+    dna = new_moltype.get_moltype("dna")
+    seq = "AT--"
+    array_seq = dna.gapped_alphabet.to_indices(seq)
+    dinuc_alpha = dna.gapped_alphabet.get_kmer_alphabet(k=2, include_gap=True)
+    kmer_seq = dinuc_alpha.to_indices(array_seq, independent_kmer=True)
+    got = dinuc_alpha.from_indices(kmer_seq)
+    assert_allclose(got, array_seq)
+
+
 def test_kmer_alphabet_from_indices_not_independent():
     dna = new_moltype.get_moltype("dna")
     seq = "ATGGGCAGA"
@@ -308,3 +454,49 @@ def test_kmer_alphabet_from_indices_not_independent():
     kmer_seq = trinuc_alpha.to_indices(arr, independent_kmer=False)
     got = trinuc_alpha.from_indices(kmer_seq, independent_kmer=False)
     assert_allclose(got, arr)
+
+
+@pytest.mark.parametrize("gap", ("", "-"))
+@pytest.mark.parametrize("missing", ("", "?"))
+def test_char_alphabet_num_canonical(gap, missing):
+    alpha = new_alphabet.CharAlphabet(
+        list(f"TCAG{gap}{missing}"), gap=gap or None, missing=missing or None
+    )
+    assert alpha.num_canonical == 4
+
+
+@pytest.mark.parametrize("include_gap", (True, False))
+@pytest.mark.parametrize("gap", ("", "-"))
+@pytest.mark.parametrize("missing", ("", "?"))
+def test_char_kmer_alphabet_construct_gap_state(gap, include_gap, missing):
+    gap_state = "--"
+    missing_state = "??"
+    alpha = new_alphabet.CharAlphabet(
+        list(f"TCAG{gap}{missing}"), gap=gap or None, missing=missing or None
+    )
+    dinucs = alpha.get_kmer_alphabet(k=2, include_gap=include_gap)
+    assert gap_state in dinucs if include_gap and gap else gap_state not in dinucs
+    assert (
+        missing_state in dinucs
+        if include_gap and missing
+        else missing_state not in dinucs
+    )
+
+
+def test_kmeralpha_from_index_gap_or_missing():
+    alpha = new_alphabet.CharAlphabet(list("TCAG-?"), gap="-", missing="?")
+    dinuc = alpha.get_kmer_alphabet(k=2, include_gap=True)
+    # should be a gap motif
+    expect = alpha.to_indices("--")
+    got = dinuc.from_index(16)
+    assert (got == expect).all()
+    expect = alpha.to_indices("??")
+    got = dinuc.from_index(17)
+    assert (got == expect).all()
+
+
+def test_kmeralpha_from_index_invalid():
+    alpha = new_alphabet.CharAlphabet(list("TCAG-?"), gap="-", missing="?")
+    dinuc = alpha.get_kmer_alphabet(k=2, include_gap=False)
+    with pytest.raises(ValueError):
+        dinuc.from_index(16)
diff --git a/tests/test_core/test_new_genetic_code.py b/tests/test_core/test_new_genetic_code.py
index ed337e46a..78f7c1ef9 100644
--- a/tests/test_core/test_new_genetic_code.py
+++ b/tests/test_core/test_new_genetic_code.py
@@ -95,3 +95,35 @@ def test_eq_false(code_id):
     gc0 = new_genetic_code.get_code(code_id)
     gc1 = new_genetic_code.get_code(code_id + 1)
     assert gc0 != gc1
+
+
+@pytest.mark.parametrize(
+    "seq, expect",
+    [
+        ("?GATCT", "XS"),
+        ("GAT-T-", "D-"),
+        ("GAT---", "D-"),
+        ("GAT?CT", "DX"),
+        ("GAT?C-", "DX"),
+    ],
+)
+def test_translate_incomplete(seq, expect):
+    gc = new_genetic_code.DEFAULT
+    got = gc.translate(seq)
+    assert got == expect
+
+
+@pytest.mark.parametrize(
+    "seq, expect",
+    [
+        ("AGATC?", "XS"),
+        ("-A-ATC", "D-"),
+        ("---ATC", "D-"),
+        ("AG?ATC", "DX"),
+        ("-G?ATC", "DX"),
+    ],
+)
+def test_translate_incomplete_rc(seq, expect):
+    gc = new_genetic_code.DEFAULT
+    got = gc.translate(seq, rc=True)
+    assert got == expect
diff --git a/tests/test_core/test_new_moltype.py b/tests/test_core/test_new_moltype.py
index c8306a2c2..b497f1349 100644
--- a/tests/test_core/test_new_moltype.py
+++ b/tests/test_core/test_new_moltype.py
@@ -365,5 +365,4 @@ def test_degap_raises():
 def test_is_invalid_rna():
     rna = new_moltype.RNA
     seq = "ACGYAUGCUGYEWEWNFMNFUWBYBCWUYBCJWBEIWFUB"
-    ambigs = set(seq) - set("ACGU")
     assert not rna.is_valid(seq)
diff --git a/tests/test_core/test_new_sequence.py b/tests/test_core/test_new_sequence.py
index 5fc7f9bde..0087713f8 100644
--- a/tests/test_core/test_new_sequence.py
+++ b/tests/test_core/test_new_sequence.py
@@ -1,30 +1,14 @@
-import json
 import re
 
 import numpy
 import pytest
 
-import cogent3
-
-from cogent3._version import __version__
 from cogent3.core import (
     new_alphabet,
     new_genetic_code,
     new_moltype,
     new_sequence,
 )
-from cogent3.util.deserialise import deserialise_object
-from cogent3.util.misc import get_object_provenance
-
-
-@pytest.fixture(scope="function")
-def dna_alphabet():
-    return new_moltype.DNA.degen_gapped_alphabet
-
-
-@pytest.fixture(scope="function")
-def ascii_alphabet():
-    return new_moltype.ASCII.alphabet
 
 
 @pytest.mark.parametrize("name", ("dna", "rna", "protein", "protein_with_stop", "text"))
@@ -46,177 +30,6 @@ def test_moltype_make_bytes_seq():
 
 
 # Tests from test_sequence.py
-@pytest.mark.xfail(reason="refactor: how to serialise an alphabet")
-def test_to_json():
-    """to_json roundtrip recreates to_dict"""
-    dna = new_moltype.DNA
-    r = dna.make_seq(seq="AAGGCC", name="seq1")
-    got = json.loads(r.to_json())
-    seq = new_sequence.SeqView(
-        seq="AAGGCC", seqid="seq1", alphabet=dna.most_degen_alphabet()
-    ).to_rich_dict()
-
-    expect = {
-        "name": "seq1",
-        "seq": seq,
-        "moltype": dna.label,
-        "info": None,
-        "type": get_object_provenance(r),
-        "version": __version__,
-        "annotation_offset": 0,
-    }
-
-    assert got == expect
-
-
-@pytest.mark.xfail(
-    reason="NotImplementedError: deserialising 'cogent3.core.new_sequence.DnaSequence' from json"
-)
-def test_offset_with_multiple_slices(DATA_DIR):
-    seq = new_moltype.DNA.make_seq(
-        seq="ACCCCGGAAAATTTTTTTTTAAGGGGGAAAAAAAAACCCCCCC", name="22"
-    )
-    gff3_path = DATA_DIR / "ensembl_sample.gff3"
-    # TODO: directly assign an annotation_db, annotate_from_gff to be discontinued
-    seq.annotate_from_gff(gff3_path)
-    rd = seq[2:].to_rich_dict()
-    s1 = deserialise_object(rd)
-    assert s1.annotation_offset == 2
-    rd = s1[3:].to_rich_dict()
-    s2 = deserialise_object(rd)
-    assert s2.annotation_offset == 5
-    expect = {(f.seqid, f.biotype, f.name) for f in seq.get_features(start=5)}
-    got = {(f.seqid, f.biotype, f.name) for f in s2.get_features()}
-    assert got == expect
-
-
-@pytest.mark.parametrize("coord", ("start", "stop"))
-def test_seqview_to_rich_dict(coord, dna_alphabet):
-    parent = "ACCCCGGAAAATTTTTTTTTAAGGGGGAAAAAAAAACCCCCCC"
-    sv = new_sequence.SeqView(seq=parent, alphabet=dna_alphabet)
-    plus = sv.to_rich_dict()
-    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 coord not in plus
-    assert coord not in minus
-
-
-@pytest.mark.xfail(
-    reason="NotImplementedError: deserialising 'cogent3.core.new_sequence.SeqView' from json"
-)
-@pytest.mark.parametrize("reverse", (False, True))
-def test_seqview_round_trip(reverse, dna_alphabet):
-    parent = "ACCCCGGAAAATTTTTTTTTAAGGGGGAAAAAAAAACCCCCCC"
-    sv = new_sequence.SeqView(seq=parent, alphabet=dna_alphabet)
-    sv = sv[::-1] if reverse else sv
-
-    rd = sv.to_rich_dict()
-    got = deserialise_object(rd)
-    assert isinstance(got, new_sequence.SeqView)
-    assert got.to_rich_dict() == sv.to_rich_dict()
-
-
-@pytest.mark.parametrize("reverse", (False, True))
-def test_sliced_seqview_rich_dict(reverse, dna_alphabet):
-    parent = "ACCCCGGAAAATTTTTTTTTAAGGGGGAAAAAAAAACCCCCCC"
-    sl = slice(2, 13)
-    sv = new_sequence.SeqView(seq=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"]["offset"] == 2
-
-
-@pytest.mark.parametrize(
-    "sl",
-    (
-        slice(2, 5, 1),  # positive indices, positive step
-        slice(-8, -5, 1),  # negative indices, positive step
-        slice(4, 1, -1),  # positive indices, negative step
-        slice(-6, -9, -1),  # negative indices, negative step
-    ),
-)
-@pytest.mark.parametrize("offset", (4, 0))
-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(seq=data, alphabet=ascii_alphabet)
-    sv.offset = offset
-    sv = sv[sl]
-    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)
-
-
-@pytest.mark.parametrize(
-    "sl",
-    (
-        slice(None, None, 1),  # slice whole sequence plus strand
-        slice(None, None, -1),  # slice whole sequence minus strand
-    ),
-)
-def test_parent_start_stop_limits(sl, ascii_alphabet):
-    data = "0123456789"
-    # check our slice matches the expectation for rest of test
-    expect = data[sl]
-    sv = new_sequence.SeqView(seq=data, alphabet=ascii_alphabet)
-    sv = sv[sl]
-    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)
-
-
-@pytest.mark.parametrize("rev", (False, True))
-def test_parent_start_stop_empty(rev, ascii_alphabet):
-    data = "0123456789"
-    # check our slice matches the expectation for rest of test
-    expect = ""
-    sv = new_sequence.SeqView(seq=data, alphabet=ascii_alphabet)
-    sv = sv[0 : 0 : -1 if rev else 1]
-    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)
-
-
-@pytest.mark.parametrize("rev", (False, True))
-@pytest.mark.parametrize("index", range(9))
-def test_parent_start_stop_singletons(index, rev, ascii_alphabet):
-    data = "0123456789"
-    start, stop = (-(10 - index), -(10 - index + 1)) if rev else (index, index + 1)
-    sl = slice(start, stop, -1 if rev else 1)
-    # check our slice matches the expectation for rest of test
-    expect = data[sl]
-    sv = new_sequence.SeqView(seq=data, alphabet=ascii_alphabet)
-    sv = sv[sl]
-    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)
-
-
-def test_get_drawable(DATA_DIR):
-    seq = cogent3.load_seq(DATA_DIR / "annotated_seq.gb")
-    seq = seq[2000:4000]
-    biotypes = "CDS", "gene", "mRNA"
-    for feat in seq.get_features(biotype=biotypes, allow_partial=True):
-        draw = feat.get_drawable()
-        assert "(incomplete)" in draw.text
-
-    full = seq.get_drawable(biotype=biotypes)
-    # should only include elements that overlap the segment
-    assert len(full.traces) == len(biotypes)
-    # and their names should indicate they're incomplete
-    for trace in full.traces:
-        assert "(incomplete)" in trace.text
-
-
 @pytest.mark.parametrize("gc,seq", ((1, "TCCTGA"), (1, "ACGTAA---"), (2, "TCCAGG")))
 def test_has_terminal_stop_true(gc, seq):
     gc = new_genetic_code.get_code(gc)
@@ -347,55 +160,38 @@ def test_parent_coordinates(rev):
     assert seq.parent_coordinates() == (None, 0, 0, 1)
 
 
-@pytest.mark.parametrize("cls", (str, bytes))
-def test_coerce_to_seqview_str_bytes(cls, dna_alphabet):
-    seq = "AC--GGTGGGAC"
-    seqid = "seq1"
-    s = bytes(seq, "utf8") if cls == bytes else seq
-    got = new_sequence._coerce_to_seqview(s, seqid, alphabet=dna_alphabet)
-    assert got.str_value == seq
-    assert isinstance(got, new_sequence.SeqView)
-
-
-def test_coerce_to_seqview_sequence(dna_alphabet):
-    seq = "AC--GGTGGGAC"
-    seqid = "seq1"
-    got = new_sequence._coerce_to_seqview(
-        new_moltype.DNA.make_seq(seq=seq), seqid, alphabet=dna_alphabet
-    )
-    assert got.str_value == seq
-    assert isinstance(got, new_sequence.SeqView)
-
-
-def test_coerce_to_seqview_already_seqview(dna_alphabet):
+@pytest.mark.parametrize(
+    "cls", (new_moltype.DNA.make_seq, new_sequence.SeqView, str, bytes)
+)
+def test_coerce_to_seqview(cls):
     seq = "AC--GGTGGGAC"
     seqid = "seq1"
-    got = new_sequence._coerce_to_seqview(
-        new_sequence.SeqView(seq=seq, alphabet=dna_alphabet),
-        seqid,
-        alphabet=dna_alphabet,
-    )
-    assert got.str_value == seq
+    if cls in (str, bytes):
+        s = bytes(seq, "utf8") if cls == bytes else seq
+        got = new_sequence._coerce_to_seqview(s, seqid)
+    else:
+        got = new_sequence._coerce_to_seqview(cls(seq=seq), seqid)
+    assert got.value == seq
     assert isinstance(got, new_sequence.SeqView)
 
 
-def test_seqview_seqid(dna_alphabet):
-    sv = new_sequence.SeqView(seq="ACGGTGGGAC", alphabet=dna_alphabet)
+def test_seqview_seqid():
+    sv = new_sequence.SeqView(seq="ACGGTGGGAC")
     assert sv.seqid is None
 
-    sv = new_sequence.SeqView(seq="ACGGTGGGAC", seqid="seq1", alphabet=dna_alphabet)
+    sv = new_sequence.SeqView(seq="ACGGTGGGAC", seqid="seq1")
     assert sv.seqid == "seq1"
 
 
-def test_seqview_rich_dict_round_trip_seqid(dna_alphabet):
-    sv = new_sequence.SeqView(seq="ACGGTGGGAC", seqid="seq1", alphabet=dna_alphabet)
+def test_seqview_rich_dict_round_trip_seqid():
+    sv = new_sequence.SeqView(seq="ACGGTGGGAC", seqid="seq1")
     rd = sv.to_rich_dict()
     assert rd["init_args"]["seqid"] == "seq1"
 
     got = new_sequence.SeqView.from_rich_dict(rd)
     assert got.seqid == "seq1"
 
-    sv = new_sequence.SeqView(seq="ACGGTGGGAC", alphabet=dna_alphabet)
+    sv = new_sequence.SeqView(seq="ACGGTGGGAC")
     rd = sv.to_rich_dict()
     assert rd["init_args"]["seqid"] is None
 
@@ -403,8 +199,8 @@ def test_seqview_rich_dict_round_trip_seqid(dna_alphabet):
     assert got.seqid is None
 
 
-def test_seqview_slice_propagates_seqid(dna_alphabet):
-    sv = new_sequence.SeqView(seq="ACGGTGGGAC", seqid="seq1", alphabet=dna_alphabet)
+def test_seqview_slice_propagates_seqid():
+    sv = new_sequence.SeqView(seq="ACGGTGGGAC", seqid="seq1")
     sliced_sv = sv[1:8:2]
     assert sliced_sv.seqid == "seq1"
 
@@ -431,8 +227,8 @@ def test_make_seq_assigns_to_seqview():
     assert seq.name == seq._seq.seqid == "s1"
 
 
-def test_empty_seqview_translate_position(dna_alphabet):
-    sv = new_sequence.SeqView(seq="", alphabet=dna_alphabet)
+def test_empty_seqview_translate_position():
+    sv = new_sequence.SeqView(seq="")
     assert sv.absolute_position(0) == 0
     assert sv.relative_position(0) == 0
 
@@ -441,12 +237,10 @@ def test_empty_seqview_translate_position(dna_alphabet):
 @pytest.mark.parametrize("stop", (None, 10, 8, 1, 0, -1, -11))
 @pytest.mark.parametrize("step", (None, 1, 2, -1, -2))
 @pytest.mark.parametrize("length", (1, 8, 999))
-def test_seqview_seq_len_init(start, stop, step, length, dna_alphabet):
+def test_seqview_seq_len_init(start, stop, step, length):
     # seq_len is length of seq when None
     seq_data = "A" * length
-    sv = new_sequence.SeqView(
-        seq=seq_data, start=start, stop=stop, step=step, alphabet=dna_alphabet
-    )
+    sv = new_sequence.SeqView(seq=seq_data, start=start, stop=stop, step=step)
     expect = len(seq_data)
     # Check property and slot
     assert sv.seq_len == expect
@@ -454,33 +248,22 @@ def test_seqview_seq_len_init(start, stop, step, length, dna_alphabet):
 
 
 @pytest.mark.parametrize("seq, seq_len", [("A", 0), ("", 1), ("A", 2)])
-def test_seqview_seq_len_mismatch(seq, seq_len, dna_alphabet):
+def test_seqview_seq_len_mismatch(seq, seq_len):
     # If provided, seq_len must match len(seq)
     with pytest.raises(AssertionError):
-        new_sequence.SeqView(seq=seq, seq_len=seq_len, alphabet=dna_alphabet)
+        new_sequence.SeqView(seq=seq, seq_len=seq_len)
 
 
-def test_seqview_copy_propagates_seq_len(dna_alphabet):
+def test_seqview_copy_propagates_seq_len():
     seq = "ACGGTGGGAC"
-    sv = new_sequence.SeqView(seq=seq, alphabet=dna_alphabet)
+    sv = new_sequence.SeqView(seq=seq)
     copied = sv.copy()
     assert copied.seq_len == len(seq)
 
 
-def test_seqview_seq_len_modified_seq(dna_alphabet):
+def test_seqview_seq_len_modified_seq():
     seq = "ACGGTGGGAC"
-    sv = new_sequence.SeqView(seq=seq, alphabet=dna_alphabet)
+    sv = new_sequence.SeqView(seq=seq)
 
     sv.seq = "ATGC"  # this should not modify seq_len
     assert sv.seq_len == len(seq)
-
-
-def test_sequence_str_bytes_array():
-    data = "ACGGTGGGAC"
-    seq = new_moltype.DNA.make_seq(seq=data)
-    print(type(seq))
-    assert str(seq) == data
-    assert bytes(seq) == data.encode("utf8")
-    assert numpy.array_equal(
-        numpy.array(seq), new_moltype.DNA.alphabet.to_indices(data)
-    )
diff --git a/tests/test_core/test_sequence.py b/tests/test_core/test_sequence.py
index 76a7c152c..11b86381d 100644
--- a/tests/test_core/test_sequence.py
+++ b/tests/test_core/test_sequence.py
@@ -2333,12 +2333,16 @@ def test_to_rich_dict(cls, with_offset):
     assert got == expect
 
 
-def test_to_json():  # ported for Sequence
+@pytest.mark.parametrize("cls,with_offset", ((ArraySequence, False), (Sequence, True)))
+def test_to_json(cls, with_offset):
     """to_json roundtrip recreates to_dict"""
-    seq = "AAGGCC"
-    r = ArraySequence(seq, name="seq1")
+    r = cls("AAGGCC", name="seq1")
     got = json.loads(r.to_json())
 
+    seq = "AAGGCC"
+    if cls == Sequence:
+        seq = SeqView(seq=seq, seqid="seq1").to_rich_dict()
+
     expect = {
         "name": "seq1",
         "seq": seq,
@@ -2347,10 +2351,157 @@ def test_to_json():  # ported for Sequence
         "type": get_object_provenance(r),
         "version": __version__,
     }
+    if with_offset:
+        expect["annotation_offset"] = 0
+
+    assert got == expect
 
+
+def test_offset_with_multiple_slices(DATA_DIR):
+    from cogent3.util.deserialise import deserialise_object
+
+    seq = DNA.make_seq("ACCCCGGAAAATTTTTTTTTAAGGGGGAAAAAAAAACCCCCCC", name="22")
+    gff3_path = DATA_DIR / "ensembl_sample.gff3"
+    seq.annotate_from_gff(gff3_path)
+    rd = seq[2:].to_rich_dict()
+    s1 = deserialise_object(rd)
+    assert s1.annotation_offset == 2
+    rd = s1[3:].to_rich_dict()
+    s2 = deserialise_object(rd)
+    assert s2.annotation_offset == 5
+    expect = {(f.seqid, f.biotype, f.name) for f in seq.get_features(start=5)}
+    got = {(f.seqid, f.biotype, f.name) for f in s2.get_features()}
     assert got == expect
 
 
+def test_seqview_to_rich_dict():
+    parent = "ACCCCGGAAAATTTTTTTTTAAGGGGGAAAAAAAAACCCCCCC"
+    sv = SeqView(seq=parent)
+    plus = sv.to_rich_dict()
+    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"]
+    for coord in ("start", "stop"):
+        assert coord not in plus
+        assert coord not in minus
+
+
+@pytest.mark.parametrize("reverse", (False, True))
+def test_seqview_round_trip(reverse):
+    from cogent3.util.deserialise import deserialise_object
+
+    parent = "ACCCCGGAAAATTTTTTTTTAAGGGGGAAAAAAAAACCCCCCC"
+    sv = SeqView(seq=parent)
+    if reverse:
+        sv = sv[::-1]
+
+    rd = sv.to_rich_dict()
+    got = deserialise_object(rd)
+    assert isinstance(got, SeqView)
+    assert got.to_rich_dict() == sv.to_rich_dict()
+
+
+@pytest.mark.parametrize("reverse", (False, True))
+def test_sliced_seqview_rich_dict(reverse):
+    parent = "ACCCCGGAAAATTTTTTTTTAAGGGGGAAAAAAAAACCCCCCC"
+    sl = slice(2, 13)
+    sv = SeqView(seq=parent)[sl]
+    if reverse:
+        sv = sv[::-1]
+    rd = sv.to_rich_dict()
+    assert rd["init_args"]["seq"] == parent[sl]
+    assert rd["init_args"]["offset"] == 2
+
+
+@pytest.mark.parametrize(
+    "sl",
+    (
+        slice(2, 5, 1),  # positive indices, positive step
+        slice(-8, -5, 1),  # negative indices, positive step
+        slice(4, 1, -1),  # positive indices, negative step
+        slice(-6, -9, -1),  # negative indices, negative step
+    ),
+)
+@pytest.mark.parametrize("offset", (4, 0))
+def test_parent_start_stop(sl, offset):
+    data = "0123456789"
+    # check our slice matches the expectation for rest of test
+    expect = "234" if sl.step > 0 else "432"
+    sv = SeqView(seq=data)
+    sv.offset = offset
+    sv = sv[sl]
+    assert sv.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)
+
+
+@pytest.mark.parametrize(
+    "sl",
+    (
+        slice(None, None, 1),  # slice whole sequence plus strand
+        slice(None, None, -1),  # slice whole sequence minus strand
+    ),
+)
+def test_parent_start_stop_limits(sl):
+    data = "0123456789"
+    # check our slice matches the expectation for rest of test
+    expect = data[sl]
+    sv = SeqView(seq=data)
+    sv = sv[sl]
+    assert sv.value == expect
+    # now check that start / stop are always the same
+    # irrespective of step sign
+    assert (sv.parent_start, sv.parent_stop) == (0, 10)
+
+
+@pytest.mark.parametrize("rev", (False, True))
+def test_parent_start_stop_empty(rev):
+    data = "0123456789"
+    # check our slice matches the expectation for rest of test
+    expect = ""
+    sv = SeqView(seq=data)
+    sv = sv[0 : 0 : -1 if rev else 1]
+    assert sv.value == expect
+    # now check that start / stop are always the same
+    # irrespective of step sign
+    assert (sv.parent_start, sv.parent_stop) == (0, 0)
+
+
+@pytest.mark.parametrize("rev", (False, True))
+@pytest.mark.parametrize("index", range(9))
+def test_parent_start_stop_singletons(index, rev):
+    data = "0123456789"
+    start, stop = (-(10 - index), -(10 - index + 1)) if rev else (index, index + 1)
+    sl = slice(start, stop, -1 if rev else 1)
+    # check our slice matches the expectation for rest of test
+    expect = data[sl]
+    sv = SeqView(seq=data)
+    sv = sv[sl]
+    assert sv.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)
+
+
+def test_get_drawable(DATA_DIR):
+    seq = cogent3.load_seq(DATA_DIR / "annotated_seq.gb")
+    seq = seq[2000:4000]
+    biotypes = "CDS", "gene", "mRNA"
+    for feat in seq.get_features(biotype=biotypes, allow_partial=True):
+        draw = feat.get_drawable()
+        assert "(incomplete)" in draw.text
+
+    full = seq.get_drawable(biotype=biotypes)
+    # should only include elements that overlap the segment
+    assert len(full.traces) == len(biotypes)
+    # and their names should indicate they're incomplete
+    for trace in full.traces:
+        assert "(incomplete)" in trace.text
+
+
 @pytest.mark.parametrize(
     "cls", (Aligned, ArraySequence)
 )  # ported for Sequence, SeqView, str and bytes