Repository: cogent3/cogent3
Pull request #1882 (2024-06-03 07:06:05)

Additions to new alphabet


Patch:
diff --git a/src/cogent3/__init__.py b/src/cogent3/__init__.py
index eacf4e790..7cd610989 100644
--- a/src/cogent3/__init__.py
+++ b/src/cogent3/__init__.py
@@ -120,7 +120,7 @@ def _make_seq_container(
 
 
 def make_unaligned_seqs(
-    data, moltype=None, label_to_name=None, info=None, source=None, new_type=False, **kw
+    data, moltype=None, label_to_name=None, info=None, source=None, **kw
 ):
     """Initialize an unaligned collection of sequences.
 
@@ -140,19 +140,7 @@ def make_unaligned_seqs(
     **kw
         other keyword arguments passed to SequenceCollection
     """
-    if new_type and moltype is None:
-        raise ValueError("Argument 'moltype' is required when 'new_type=True'")
-    elif new_type:
-        from cogent3.core import new_alignment
 
-        return new_alignment.make_unaligned_seqs(
-            data=data,
-            moltype=moltype,
-            label_to_name=label_to_name,
-            info=info,
-            source=source,
-            **kw,
-        )
     return _make_seq_container(
         SequenceCollection,
         data,
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index f548ec53e..136899823 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -1,46 +1,102 @@
 from __future__ import annotations
 
-import re
-import typing
-
-from collections import defaultdict
 from dataclasses import InitVar, dataclass, field
 from functools import singledispatch, singledispatchmethod
-from pathlib import Path
-from typing import Callable, Iterator, Mapping, Optional, Union
+from typing import Callable, Iterator, Optional, Union
 
 import numpy
 
-import cogent3.core.new_alphabet as new_alpha
-import cogent3.core.new_moltype as new_moltype
-import cogent3.core.new_sequence as new_seq
-
-from cogent3.core.info import Info as InfoClass
+from cogent3 import get_moltype
+from cogent3._version import __version__
+from cogent3.core.alphabet import CharAlphabet
 from cogent3.core.location import IndelMap
-from cogent3.util import warning as c3warn
-from cogent3.util.misc import get_setting_from_environ
+from cogent3.core.moltype import MolType
+from cogent3.core.sequence import (
+    Sequence,
+    SliceRecordABC,
+    _input_vals_neg_step,
+    _input_vals_pos_step,
+)
+from cogent3.util.misc import get_object_provenance
+
+
+SeqTypes = Union[str, bytes, numpy.ndarray]
+
+
+@singledispatch
+def seq_index(seq: SeqTypes, alphabet: CharAlphabet) -> numpy.ndarray:
+    raise NotImplementedError(
+        f"{seq_index.__name__} not implemented for type {type(seq)}"
+    )
+
 
+@seq_index.register
+def _(seq: str, alphabet: CharAlphabet) -> numpy.ndarray:
+    return alphabet.to_indices(seq)
 
-PrimitiveSeqTypes = Union[str, bytes, numpy.ndarray]
 
+@seq_index.register
+def _(seq: bytes, alphabet: CharAlphabet) -> numpy.ndarray:
+    return alphabet.to_indices(seq)
 
-def assign_sequential_names(num_seqs: int, base_name: str = "seq", start_at: int = 0):
-    """Returns list of num_seqs sequential, unique names."""
-    return [f"{base_name}_{i}" for i in range(start_at, start_at + num_seqs)]
 
+@seq_index.register
+def _(seq: numpy.ndarray, alphabet: CharAlphabet) -> numpy.ndarray:
+    return seq.astype(alphabet.array_type)
 
-class SeqDataView(new_seq.SliceRecordABC):
+
+@singledispatch
+def validate_names(correct_names: Union[dict, tuple, list], names: tuple) -> tuple:
     """
-    A view class for SeqsData, providing properties for different
+    Helper to check that names passed as args match sequence names.
+    dict (data) for constructor; tuple for SeqData instance
+    """
+    raise NotImplementedError(
+        f"validate_names not implemented for type {type(correct_names)}"
+    )
+
+
+@validate_names.register
+def _(correct_names: dict, names: tuple) -> tuple:
+    keys = correct_names.keys()
+    if names is None:
+        return tuple(keys)
+    if set(names) == set(keys):
+        return names
+    raise ValueError("names do not match dictionary keys")
+
+
+def _names_list_tuple(correct_names: Union[tuple, list], names: tuple):
+    """List and tuples have the same implementation for dispatch"""
+    if names is None:
+        return correct_names
+    if set(names) <= set(correct_names):
+        return tuple(names)
+    raise ValueError("some names do not match")
+
+
+@validate_names.register
+def _(correct_names: tuple, names: tuple) -> tuple:
+    return _names_list_tuple(correct_names, names)
+
+
+@validate_names.register
+def _(correct_names: list, names: tuple) -> tuple:
+    return _names_list_tuple(correct_names, names)
+
+
+class SeqDataView(SliceRecordABC):
+    """
+    A view class for SeqData, providing properties for different
     representations.
 
-    self.seq is a SeqsData() instance, but other properties are a reference to a
+    self.seq is a SeqData() instance, but other properties are a reference to a
     single seqid only.
 
     Example
     -------
     data = {"seq1": "ACGT", "seq2": "GTTTGCA"}
-    sd = SeqsData(data=data)
+    sd = SeqData(data=data)
     sdv = sd.get_seq_view(seqid="seq1")
     """
 
@@ -49,22 +105,20 @@ class SeqDataView(new_seq.SliceRecordABC):
     def __init__(
         self,
         *,
-        seq: SeqsData,
-        seq_len: int,
+        seq: SeqData,
         start: Optional[int] = None,
         stop: Optional[int] = None,
         step: Optional[int] = None,
         offset: int = 0,
         seqid: Optional[str] = None,
+        seq_len: Optional[int] = 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
-        )
+        func = _input_vals_pos_step if step > 0 else _input_vals_neg_step
         start, stop, step = func(self._seq_len, start, stop, step)
         self.seq = seq
         self.start = start
@@ -132,54 +186,94 @@ class SeqDataView(new_seq.SliceRecordABC):
             f"seqid={self.seqid!r}, seq_len={self.seq_len})"
         )
 
-    # todo: do we support copy? do we support copy with sliced?
+    def to_rich_dict(self):
+        # get the current state
+        data = {"type": get_object_provenance(self), "version": __version__}
+        data["init_args"] = self._get_init_kwargs()
+        # since we will truncate the seq, we don't need start, stop,
+        # step is sufficient
+        data["init_args"]["step"] = self.step
+        if self.is_reversed:
+            adj = self.seq_len + 1
+            start, stop = self.stop + adj, self.start + adj
+        else:
+            start, stop = self.start, self.stop
+
+        data["init_args"]["seq"] = str(self[start:stop])
+        data["init_args"]["offset"] = int(self.parent_start)
+        return data
+
+    @classmethod
+    def from_rich_dict(cls, data: dict):
+        init_args = data.pop("init_args")
+        if "offset" in data:
+            init_args["offset"] = data.pop("offset")
+        return cls(**init_args)
+
     def copy(self, sliced: bool = False):
-        """returns copy"""
-        return self
+        """returns copy
+
+        Parameters
+        ----------
+        sliced
+            if True, the underlying sequence is truncated and the start/stop
+            adjusted
+        """
+        if not sliced:
+            return self.__class__(
+                seq=self.seq,
+                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())
 
 
-class SeqsData:
+class SeqData:
     __slots__ = ("_data", "_alphabet", "_names", "_make_seq")
 
     def __init__(
         self,
-        data: dict[str, PrimitiveSeqTypes],
-        alphabet: new_alpha.CharAlphabet,
-        make_seq: Callable = None,
+        data: dict[str, SeqTypes],
+        alphabet: CharAlphabet,
+        names: Optional[Union[tuple[str], None]] = None,
+        make_seq: Optional[type] = None,
     ):
         self._alphabet = alphabet
+        self._names = validate_names(data, names)
         self._make_seq = make_seq
+        # convert from string to array of uint8
         self._data: dict[str, numpy.ndarray] = {
-            str(name): self._alphabet.to_indices(seq) for name, seq in data.items()
+            k: seq_index(v, self._alphabet) for k, v in data.items()
         }
 
     @property
-    def names(self) -> list:
-        return list(self._data.keys())
+    def names(self) -> Iterator[str]:
+        yield from self._names
 
     @property
-    def make_seq(self) -> Union[SeqDataView, new_seq.Sequence]:
-        """if set, returns a function that takes 'seq' and 'name' as keyword
-        arguments and returns a given Sequence from the collection.
-
-        Notes
-        -----
-        Can be set with any callable function that takes 'seq' and 'name' as
-        keyword arguments. Typically set with '<moltype-instance>.make_seq'.
-        """
+    def make_seq(self) -> Union[SeqDataView, Sequence]:
         return self._make_seq
 
     @make_seq.setter
     def make_seq(self, make_seq: Callable) -> None:
         self._make_seq = make_seq
 
-    @property
-    def num_seqs(self) -> int:
-        return len(self._data)
+    @singledispatchmethod
+    def __getitem__(self, index: Union[str, int]) -> SeqDataView:
+        raise NotImplementedError(f"__getitem__ not implemented for {type(index)}")
 
-    @property
-    def alphabet(self) -> new_alpha.CharAlphabet:
-        return self._alphabet
+    @__getitem__.register
+    def _(self, index: str) -> SeqDataView:
+        sdv = self.get_seq_view(seqid=index)
+        return sdv if self._make_seq is None else self.make_seq(sdv, seqid=index)
+
+    @__getitem__.register
+    def _(self, index: int) -> SeqDataView:
+        return self[self._names[index]]
 
     def get_seq_array(
         self, *, seqid: str, start: int = None, stop: int = None
@@ -200,387 +294,10 @@ class SeqsData:
         seq_len = len(self._data[seqid])
         return SeqDataView(seq=self, seqid=seqid, seq_len=seq_len)
 
-    def __len__(self):
-        return self.num_seqs
-
-    @singledispatchmethod
-    def __getitem__(
-        self, index: Union[str, int]
-    ) -> Union[SeqDataView, new_seq.Sequence]:
-        raise NotImplementedError(f"__getitem__ not implemented for {type(index)}")
-
-    @__getitem__.register
-    def _(self, index: str) -> Union[SeqDataView, new_seq.Sequence]:
-        sdv = self.get_seq_view(seqid=index)
-        return (
-            sdv
-            if self.make_seq is None
-            else self.make_seq(seq=sdv.str_value, name=index)
-        )
-
-    @__getitem__.register
-    def _(self, index: int) -> Union[SeqDataView, new_seq.Sequence]:
-        return self[self.names[index]]
-
-
-@singledispatch
-def prep_for_seqs_data(
-    data: Union[dict, SeqsData], moltype: new_moltype.MolType
-) -> dict[str, numpy.ndarray]:
-    raise NotImplementedError(f"SeqsData can not be constructed for type {type(data)}")
-
-
-@prep_for_seqs_data.register
-def _(data: SeqsData, moltype: new_moltype.MolType) -> dict[str, numpy.ndarray]:
-    if moltype.is_compatible_alphabet(data.alphabet):
-        return data
-    else:
-        raise ValueError(
-            f"Provided moltype: {moltype} is not compatible with SeqsData alphabet {data.alphabet}"
-        )
-
-
-@prep_for_seqs_data.register
-def _(data: dict, moltype: new_moltype.MolType) -> dict[str, numpy.ndarray]:
-    # todo: kath, when to use .alphabet and when to use .degen_gapped_alphabet?
-    alpha = moltype.alphabet
-    seqs_data = SeqsData(data=data, alphabet=alpha)
-    return prep_for_seqs_data(seqs_data, moltype)
-
-
-@prep_for_seqs_data.register
-def _(data: list, moltype: new_moltype.MolType) -> dict[str, numpy.ndarray]:
-    names = assign_sequential_names(len(data))
-    named_data = dict(zip(names, data))
-    return prep_for_seqs_data(named_data, moltype)
-
-
-def make_unaligned_seqs(
-    *,
-    data: Union[dict[str, PrimitiveSeqTypes], SeqsData, list],
-    moltype: Union[str, new_moltype.MolType],
-    label_to_name: Callable = None,
-    info: dict = None,
-    source: Union[str, Path] = None,
-    **kwargs,
-):
-    """Initialize an unaligned collection of sequences.
-
-    Parameters
-    ----------
-    data
-        sequences, assumes a dict {name: seq, ...} or a SeqsData
-    moltype
-        string representation of the moltype, e.g., 'dna', 'protein'.
-    label_to_name
-        function for converting original names into other names.
-    info
-        a dict from which to make an info object
-    source
-        origins of this data, defaults to 'unknown'. Converted to a string
-        and added to info["source"].
-    **kwargs
-        other keyword arguments to be passed to SequenceCollection
-    """
-    moltype = new_moltype.get_moltype(moltype)
-
-    if len(data) == 0:
-        raise ValueError("data must be at least one sequence.")
-    seqs_data = prep_for_seqs_data(data, moltype)
-
-    return SequenceCollection(seqs_data=seqs_data, moltype=moltype, info=info)
-
-
-class SequenceCollection:
-    def __init__(
-        self,
-        *,
-        seqs_data: SeqsData,
-        moltype: new_moltype.MolType,
-        info: Union[dict, InfoClass] = None,
-    ):
-        self._seqs_data = seqs_data
-        self.moltype = moltype
-        self._names = self._seqs_data.names
-        self._seqs_data.make_seq = self.moltype.make_seq
-        if not isinstance(info, InfoClass):
-            info = InfoClass(info) if info else InfoClass()
-        self.info = info
-        self._repr_policy = dict(num_seqs=10, num_pos=60, ref_name="longest", wrap=60)
-
-    @property
-    def names(self) -> Iterator[str]:
-        return self._names
-
-    @property
-    def seqs(self) -> SeqsData:
-        return self._seqs_data
-
-    def iter_seqs(
-        self, seq_order: list = None
-    ) -> Iterator[Union[new_seq.Sequence, SeqsData]]:
-        """Iterates over sequences in the collection, in order.
-
-        Parameters
-        ----------
-        seq_order:
-            list of seqids giving the order in which seqs will be returned.
-            Defaults to self.names
-        """
-
-        seqs = self.seqs
-        get = seqs.__getitem__
-        for key in seq_order or self.names:
-            yield get(key)
-
-    @property
-    def num_seqs(self) -> int:
-        return self._seqs_data.num_seqs
-
-    @property
-    @c3warn.deprecated_callable(
-        version="2025.5", reason=".seqs can now be indexed by name", new=".seqs"
-    )
-    def named_seqs(self) -> SeqsData:  # pragma: no cover
-        return self.seqs
-
-    def take_seqs(
-        self, names: Union[str, typing.Sequence[str]], negate: bool = False, **kwargs
-    ):
-        """Returns new collection containing only specified seqs.
-
-        Parameters
-        ----------
-        names
-            sequences to select (or exclude if negate=True)
-        negate
-            select all sequences EXCEPT names
-        kwargs
-            keyword arguments to be passed to the constructor of the new seqcollection
-        """
-
-        # todo: kath, add subset method onto SeqsData
-        if negate:
-            selected_seqs = {
-                name: self.seqs.get_seq_str(seqid=name)
-                for name in self.names
-                if name not in names
-            }
-        else:
-            selected_seqs = {
-                name: self.seqs.get_seq_str(seqid=name)
-                for name in names
-                if name in self.names
-            }
-
-        if not selected_seqs:
-            return {}
-
-        moltype = kwargs.pop("moltype", self.moltype)
-
-        seqs_data = SeqsData(data=selected_seqs, alphabet=moltype.alphabet)
-        result = self.__class__(
-            seqs_data=seqs_data, moltype=moltype, info=self.info, **kwargs
-        )
-        # todo: kath, update annotation_db and assign to result when we have annotations working
-        return result
-
-    def __repr__(self):
-        seqs = []
-        limit = 10
-        delimiter = ""
-
-        repr_seq_names = [min(self.names, key=lambda name: len(self.seqs[name]))]
-        if len(self.names) > 1:
-            # In case of a tie, min and max return first.
-            # reversed ensures if all seqs are of same length, different seqs are returned
-            repr_seq_names.append(
-                max(reversed(self.names), key=lambda name: len(self.seqs[name]))
-            )
-
-        for name in repr_seq_names:
-            elts = list(str(self.seqs[name])[: limit + 1])
-            if len(elts) > limit:
-                elts[-1] = "..."
-            seqs.append(f"{name}[{delimiter.join(elts)}]")
-
-        if len(self.names) > 2:
-            seqs.insert(1, "...")
-
-        seqs = ", ".join(seqs)
-
-        return f"{len(self.names)}x ({seqs}) {self.moltype.label} seqcollection"
-
-    def _repr_html_(self) -> str:
-        settings = self._repr_policy.copy()
-        env_vals = get_setting_from_environ(
-            "COGENT3_ALIGNMENT_REPR_POLICY",
-            dict(num_seqs=int, num_pos=int, wrap=int),
-        )
-        settings.update(env_vals)
-        return self.to_html(
-            name_order=self.names[: settings["num_seqs"]],
-            limit=settings["num_pos"],
-            wrap=settings["wrap"],
-        )
-
-    def to_html(
-        self,
-        name_order: Optional[typing.Sequence[str]] = None,
-        wrap: int = 60,
-        limit: Optional[int] = None,
-        colors: Optional[Mapping[str, str]] = None,
-        font_size: int = 12,
-        font_family: str = "Lucida Console",
-    ) -> str:
-        """returns html with embedded styles for sequence colouring
-
-        Parameters
-        ----------
-        name_order
-            order of names for display.
-        wrap
-            number of alignment columns per row
-        limit
-            truncate alignment to this length
-        colors
-            {character
-            moltype.
-        font_size
-            in points. Affects labels and sequence and line spacing
-            (proportional to value)
-        font_family
-            string denoting font family
-
-        Examples
-        ---------
-
-        In a jupyter notebook, this code is used to provide the representation.
-
-        .. code-block:: python
-
-            seq_col # is rendered by jupyter
-
-        You can directly use the result for display in a notebook as
-
-        .. code-block:: python
-
-            from IPython.core.display import HTML
-            HTML(seq_col.to_html())
-        """
-        css, styles = self.moltype.get_css_style(
-            colors=colors, font_size=font_size, font_family=font_family
-        )
-
-        seq_lengths = numpy.array([len(seq) for seq in self.seqs])
-        min_val = seq_lengths.min()
-        max_val = seq_lengths.max()
-        med_val = numpy.median(seq_lengths)
-
-        if name_order:
-            selected = self.take_seqs(name_order)
-        else:
-            name_order = self.names
-            selected = self
-
-        # Stylise each character in each sequence
-        gaps = "".join(selected.moltype.gaps)
-        template = '<span class="%s">%%s</span>'
-        styled_seqs = defaultdict(list)
-        max_truncated_len = 0
-        for name in name_order:
-            sequence = str(self.seqs[name])[:limit]
-            seq_len = len(sequence)
-            max_truncated_len = max(seq_len, max_truncated_len)
-            start_gap = re.search(f"^[{gaps}]+", sequence)
-            end_gap = re.search(f"[{gaps}]+$", sequence)
-            start = 0 if start_gap is None else start_gap.end()
-            end = seq_len if end_gap is None else end_gap.start()
-
-            seq = []
-            for i, char in enumerate(sequence):
-                if i < start or i >= end:
-                    style = f"terminal_ambig_{self.moltype.label}"
-                else:
-                    style = styles[char]
-                s = template % style
-                s = s % char
-                seq.append(s)
-
-            styled_seqs[name] = seq
-
-        # Ensure all sublists are of same length
-        for name in styled_seqs:
-            if len(styled_seqs[name]) < max_truncated_len:
-                styled_seqs[name].extend(
-                    [""] * (max_truncated_len - len(styled_seqs[name]))
-                )
-
-        # Make html table
-        seqs = numpy.array([styled_seqs[n] for n in name_order], dtype="O")
-        table = ["<table>"]
-        seq_ = "<td>%s</td>"
-        label_ = '<td class="label">%s</td>'
-        num_row_ = '<tr class="num_row"><td></td><td><b>{:,d}</b></td></tr>'
-        for i in range(0, max_truncated_len, wrap):
-            table.append(num_row_.format(i))
-            seqblock = seqs[:, i : i + wrap].tolist()
-            for n, s in zip(name_order, seqblock):
-                s = "".join(s)
-                # Filter out rows that are empty (due to combination of shorter sequences + wrapping)
-                if s != "":
-                    row = "".join([label_ % n, seq_ % s])
-                    table.append(f"<tr>{row}</tr>")
-        table.append("</table>")
-        if (
-            limit
-            and limit < len(selected)
-            or name_order
-            and len(name_order) < len(selected.names)
-        ):
-            summary = (
-                "%s x {min=%s, median=%s, max=%s} (truncated to %s x %s) %s sequence collection"
-            ) % (
-                self.num_seqs,
-                min_val,
-                med_val,
-                max_val,
-                len(name_order) if name_order else len(selected.names),
-                limit or len(selected),
-                selected.moltype.label,
-            )
-        else:
-            summary = ("%s x {min=%s, median=%s, max=%s} %s sequence collection") % (
-                self.num_seqs,
-                min_val,
-                med_val,
-                max_val,
-                selected.moltype.label,
-            )
-
-        text = [
-            "<style>",
-            ".c3align table {margin: 10px 0;}",
-            ".c3align td { border: none !important; text-align: left !important; }",
-            ".c3align tr:not(.num_row) td span {margin: 0 2px;}",
-            ".c3align tr:nth-child(even) {background: #f7f7f7;}",
-            ".c3align .num_row {background-color:rgba(161, 195, 209, 0.5) !important; border-top: solid 1px black; }",
-            ".c3align .label { font-size: %dpt ; text-align: right !important; "
-            "color: black !important; padding: 0 4px; display: table-cell !important; "
-            "font-weight: normal !important; }" % font_size,
-            "\n".join([".c3align " + style for style in css]),
-            "</style>",
-            '<div class="c3align">',
-            "\n".join(table),
-            f"<p><i>{summary}</i></p>",
-            "</div>",
-        ]
-        return "\n".join(text)
-
 
 @singledispatch
 def seq_to_gap_coords(
-    seq: Union[str, numpy.ndarray], moltype: new_moltype.MolType
+    seq: Union[str, numpy.ndarray], moltype: MolType
 ) -> tuple[str, IndelMap]:
     """
     Takes a sequence with (or without) gaps and returns an ungapped sequence
@@ -590,8 +307,8 @@ def seq_to_gap_coords(
 
 
 @seq_to_gap_coords.register
-def _(seq: str, moltype: new_moltype.MolType) -> tuple[str, IndelMap]:
-    seq = moltype.make_seq(seq=seq)
+def _(seq: str, moltype: MolType) -> tuple[str, IndelMap]:
+    seq = moltype.make_seq(seq)
     indel_map, ungapped_seq = seq.parse_out_gaps()
 
     if indel_map.num_gaps == 0:
@@ -601,8 +318,8 @@ def _(seq: str, moltype: new_moltype.MolType) -> tuple[str, IndelMap]:
 
 
 @seq_to_gap_coords.register
-def _(seq: numpy.ndarray, moltype: new_moltype.MolType) -> tuple[str, IndelMap]:
-    gap_char = moltype.degen_gapped_alphabet.index(moltype.gap)
+def _(seq: numpy.ndarray, moltype: MolType) -> tuple[str, IndelMap]:
+    gap_char = moltype.alphabets.degen_gapped.index(moltype.gap)
     # Create boolean array of gaps to get ungapped
     gaps_bool = seq == gap_char
     ungapped = seq[~gaps_bool]
@@ -652,26 +369,27 @@ class AlignedDataView(SeqDataView):
 
 @dataclass
 class AlignedData:
-    # Look out for any overlaps with SeqsData
+    # Look out for any overlaps with SeqData
     seqs: Optional[dict[str, numpy.ndarray]] = None
     gaps: Optional[dict[str, numpy.ndarray]] = None
-    _moltype: new_moltype.MolType = field(init=False)
+    _moltype: MolType = field(init=False)
     _names: tuple[str] = field(init=False)
-    _alpha: new_alpha.CharAlphabet = field(init=False)
+    _alpha: CharAlphabet = field(init=False)
     align_len: int = 0
     moltype: InitVar[Union[str, None]] = "dna"
     names: InitVar[Union[tuple[str], None]] = None
 
     def __post_init__(self, moltype, names):
-        self._moltype = new_moltype.get_moltype(moltype)
-        self._alpha = self._moltype.degen_gapped_alphabet
-        self.seqs = {k: self._alpha.to_indices(v) for k, v in self.seqs.items()}
+        self._moltype = get_moltype(moltype)
+        self._alpha = self._moltype.alphabets.degen_gapped
+        self._names = validate_names(self.seqs, names)
+        self.seqs = {k: seq_index(v, self._alpha) for k, v in self.seqs.items()}
 
     @classmethod
     def from_gapped_seqs(
         cls,
         data: dict[str, Union[str, numpy.ndarray]],
-        moltype: Union[str, new_moltype.MolType] = "dna",
+        moltype: Union[str, MolType] = "dna",
         names: Optional[tuple[str]] = None,
     ):
         """
@@ -682,14 +400,14 @@ class AlignedData:
             raise ValueError("All sequence lengths must be the same.")
 
         align_len = seq_lengths.pop()
-        moltype = new_moltype.get_moltype(moltype)
+        moltype = get_moltype(moltype)
 
         seqs = {}
         gaps = {}
         for name, seq in data.items():
             seqs[name], gaps[name] = seq_to_gap_coords(seq, moltype)
 
-        names = names or list(data.keys)
+        names = validate_names(seqs, names)
 
         return cls(
             seqs=seqs,
diff --git a/src/cogent3/core/new_alphabet.py b/src/cogent3/core/new_alphabet.py
index ff2eb8ba9..041a15846 100644
--- a/src/cogent3/core/new_alphabet.py
+++ b/src/cogent3/core/new_alphabet.py
@@ -1,8 +1,10 @@
 import functools
+import itertools
 import typing
 
 from abc import ABC, abstractmethod
 
+import numba
 import numpy
 
 
@@ -71,10 +73,10 @@ class convert_alphabet:
         --------
         >>> dna_to_ry = convert_alphabet(src=b"TCAG", dest=b"YYRR")
         >>> dna_to_ry(b"GCTA")
-        b"RYYR"
+        b'RYYR'
         >>> dna_to_ry = convert_alphabet(src=b"TCAG", dest=b"YYRR", delete=b"-")
         >>> dna_to_ry(b"GC-TA")
-        b"RYYR"
+        b'RYYR'
         """
         if len(src) != len(dest):
             raise ValueError(f"length of src={len(src)} != length of new {len(dest)}")
@@ -191,8 +193,9 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
         consistent_words(chars, length=1)
         return tuple.__new__(cls, chars, gap=gap)
 
-    def __init__(self, chars: typing.Sequence[StrORBytes], gap: str = "-"):
-        self._gap = gap
+    def __init__(self, chars: typing.Sequence[StrORBytes], gap: str = None):
+        self._gap_char = gap
+        self._gap_index = self.index(gap) if gap else None
         self.dtype = get_array_type(len(self))
         self._chars = set(self)  # for quick lookup
         try:
@@ -202,6 +205,14 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
         self._bytes2arr = bytes_to_array(byte_chars, dtype=self.dtype)
         self._arr2bytes = array_to_bytes(byte_chars, dtype=self.dtype)
 
+    @property
+    def gap_char(self) -> OptStr:
+        return self._gap_char
+
+    @property
+    def gap_index(self) -> OptInt:
+        return self._gap_index
+
     @property
     def motif_len(self) -> int:
         # this is enforced by consistent_words(length=1) in the constructor
@@ -233,7 +244,31 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
 
     def with_gap_motif(self): ...
 
-    def get_word_alphabet(self, size: int): ...
+    def get_word_alphabet(self, k: int, include_gap: bool = True) -> "KmerAlphabet":
+        """returns kmer alphabet with words of size k
+
+        Parameters
+        ----------
+        k
+            word size
+        include_gap
+            if True, and self.gap_char, we set
+            KmerAlphabet.gap_char = self.gap_char * k
+        """
+        # refactor: revisit whether the include_gap argument makes sense
+        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)
+
+        gap = self._gap_char * k if include_gap and self._gap_char is not None 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)
 
     @functools.singledispatchmethod
     def is_valid(self, seq) -> bool:
@@ -248,13 +283,241 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
         return seq.min() >= 0 and seq.max() < len(self)
 
 
-class KmerAlphabet(AlphabetABC):
-    def to_indices(self, seq: tuple[str, ...]) -> numpy.ndarray: ...
+@numba.jit(nopython=True)
+def coord_conversion_coeffs(num_states, k):  # pragma: no cover
+    """coefficients for multi-dimensional coordinate conversion into 1D index"""
+    return numpy.array([num_states ** (i - 1) for i in range(k, 0, -1)])
+
+
+@numba.jit(nopython=True)
+def coord_to_index(coord, coeffs):  # pragma: no cover
+    """converts a multi-dimensional coordinate into a 1D index"""
+    return (coord * coeffs).sum()
+
+
+@numba.jit(nopython=True)
+def index_to_coord(index, coeffs):  # pragma: no cover
+    """converts a 1D index into a multi-dimensional coordinate"""
+    ndim = len(coeffs)
+    coord = numpy.zeros(ndim, dtype=numpy.uint64)
+    remainder = index
+    for i in range(ndim):
+        n, remainder = numpy.divmod(remainder, coeffs[i])
+        coord[i] = n
+    return coord
+
+
+# todo: profile this against pure python,
+#  appears slower as a numba function!
+@numba.jit()
+def seq_to_kmer_indices(
+    seq: numpy.ndarray,
+    result: numpy.ndarray,
+    coeffs: numpy.ndarray,
+    num_states: int,
+    k: int,
+    independent_kmer: bool = True,
+) -> numpy.ndarray:  # pragma: no cover
+    """return 1D indices for valid k-mers
+
+    Parameters
+    ----------
+    seq
+        numpy array of uint, assumed that canonical characters have
+        sequential indexes which are all < num_states
+    result
+        array to be written into
+    coeffs
+        result of calling coord_conversion_coeffs() with num_states and k
+    num_states
+        defines range of possible ints at a position
+    k
+        k-mer size
+    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
+    size: int = int(numpy.ceil((len(seq) - k + 1) / step))
+    if len(result) < size:
+        raise ValueError(f"size of result {len(result)} <= {size}")
+
+    for result_idx, i in enumerate(range(0, len(seq) - k + 1, step)):
+        for j in range(i, i + k):
+            if seq[j] < num_states:
+                result[result_idx] = coord_to_index(seq[i : i + k], coeffs)
+            else:
+                continue
+    return result
+
+
+# todo: profile this against pure python
+@numba.jit()
+def kmer_indices_to_seq(
+    kmer_indices: numpy.ndarray,
+    result: numpy.ndarray,
+    coeffs: numpy.ndarray,
+    k: int,
+    independent_kmer: bool = True,
+) -> numpy.ndarray:  # pragma: no cover
+    for index, kmer_index in enumerate(kmer_indices):
+        coord = index_to_coord(kmer_index, coeffs)
+        if index == 0:
+            result[0:3] = coord
+        elif independent_kmer:
+            seq_index = index * k
+            result[seq_index : seq_index + k] = coord
+        else:
+            # we are just adding the last monomer
+            result[index + k - 1] = coord[-1]
+
+    return result
+
+
+class KmerAlphabet(tuple, AlphabetABC):
+    def __new__(
+        cls,
+        words: tuple[StrORBytes, ...],
+        monomers: CharAlphabet,
+        k: int,
+        gap: OptStr = None,
+    ):
+        if not words:
+            raise ValueError(f"cannot create empty {cls.__name__!r}")
+
+        if gap is not None:
+            assert _coerce_to_type(words[0], gap) in words
+
+        consistent_words(words, length=k)
+        return tuple.__new__(cls, words, monomers=monomers, gap=gap)
+
+    def __init__(
+        self,
+        words: tuple[StrORBytes, ...],
+        monomers: CharAlphabet,
+        k: int,
+        gap: OptStr = None,
+    ):
+        self._gap_char = gap
+        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)
+
+    @functools.singledispatchmethod
+    def to_indices(self, seq, independent_kmer: bool = True) -> numpy.ndarray:
+        """returns a sequence of k-mer indices
+
+        Parameters
+        ----------
+        seq
+            a sequence of monomers
+        independent_kmer
+            if True, returns non-overlapping k-mers
+        """
+        # todo: handle case of non-modulo sequences
+        raise TypeError(f"{type(seq)} is invalid")
+
+    @to_indices.register
+    def _(self, seq: str, independent_kmer: bool = True) -> numpy.ndarray:
+        seq = self.monomers.to_indices(seq)
+        return self.to_indices(seq, independent_kmer=independent_kmer)
+
+    @to_indices.register
+    def _(self, seq: numpy.ndarray, independent_kmer: bool = True) -> numpy.ndarray:
+        size = len(seq) - self.k + 1
+        if independent_kmer:
+            size = int(numpy.ceil(size / self.k))
+        result = numpy.zeros(size, dtype=get_array_type(size))
+        return seq_to_kmer_indices(
+            seq, result, self._coeffs, len(self.monomers), self.k, independent_kmer
+        )
 
-    def from_indices(self: numpy.ndarray) -> tuple[str, ...]: ...
+    def from_indices(
+        self, kmer_indices: numpy.ndarray, independent_kmer: bool = True
+    ) -> numpy.ndarray:
+
+        if independent_kmer:
+            size = len(kmer_indices) * self.k
+        else:
+            size = len(kmer_indices) + self.k - 1
+
+        result = numpy.zeros(size, dtype=self.dtype)
+        return kmer_indices_to_seq(
+            kmer_indices,
+            result,
+            self._coeffs,
+            self.k,
+            independent_kmer=independent_kmer,
+        )
 
     def with_gap_motif(self): ...
 
+    @functools.singledispatchmethod
+    def pack(self, seq) -> numpy.ndarray:
+        """encodes a k-mer as a single integer
+
+        Parameters
+        ----------
+        seq
+            sequence to be encoded, can be either a string or numpy array
+        overlapping
+            if False, performs operation on sequential k-mers, e.g. codons
+        """
+        raise TypeError(f"{type(seq)} not supported")
+
+    @pack.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.pack(seq)
+
+    @pack.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.pack(seq)
+
+    @pack.register
+    def _(self, seq: numpy.ndarray) -> numpy.ndarray:
+        """encodes a k-mer as a single integer
+
+        Parameters
+        ----------
+        seq
+            sequence to be encoded
+        """
+        return coord_to_index(seq, self._coeffs)
+
+    def unpack(self, kmer_index: int) -> numpy.ndarray:
+        """decodes an integer into a k-mer"""
+        return index_to_coord(kmer_index, self._coeffs)
+
 
 _alphabet_moltype_map = {}
 


Test patch:
diff --git a/tests/test_core/test_alignment.py b/tests/test_core/test_alignment.py
index acdc8caed..3e0f4e8c1 100644
--- a/tests/test_core/test_alignment.py
+++ b/tests/test_core/test_alignment.py
@@ -218,7 +218,7 @@ class SequenceCollectionBaseTests(object):
         self.assertTrue((un_list == first_list) or (un_list == sec_list))
         self.assertNotEqual(first_list, sec_list)
 
-    def test_iter_seqs(self):  # ported
+    def test_iter_seqs(self):
         """SequenceCollection iter_seqs() method should support reordering of seqs"""
         self.ragged_padded = self.Class(
             self.ragged_padded.named_seqs, names=["a", "b", "c"]
@@ -238,7 +238,7 @@ class SequenceCollectionBaseTests(object):
         self.assertEqual(list(self.ordered1.iter_selected()), ["A"] * 5 + ["B"] * 5)
         self.assertEqual(list(self.ordered2.iter_selected()), ["B"] * 5 + ["A"] * 5)
 
-    def test_take_seqs(self):  # ported
+    def test_take_seqs(self):
         """SequenceCollection take_seqs should return new SequenceCollection with selected seqs."""
         a = self.ragged_padded.take_seqs(list("bc"))
         self.assertTrue(isinstance(a, _SequenceCollectionBase))
@@ -247,7 +247,7 @@ class SequenceCollectionBaseTests(object):
         a = self.ragged_padded.take_seqs(list("bc"), negate=True)
         self.assertEqual(a, {"a": "AAAAAA"})
 
-    def test_take_seqs_str(self):  # ported
+    def test_take_seqs_str(self):
         """string arg to SequenceCollection take_seqs should work."""
         a = self.ragged_padded.take_seqs("a", negate=True)
         self.assertTrue(isinstance(a, _SequenceCollectionBase))
@@ -256,7 +256,7 @@ class SequenceCollectionBaseTests(object):
         a = self.ragged_padded.take_seqs("a")
         self.assertEqual(a, {"a": "AAAAAA"})
 
-    def test_take_seqs_info(self):  # ported
+    def test_take_seqs_info(self):
         """take_seqs should preserve info attribute"""
         orig = self.Class(
             data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}, info={"key": "value"}
@@ -264,7 +264,7 @@ class SequenceCollectionBaseTests(object):
         subset = orig.take_seqs(list("ab"))
         self.assertEqual(set(subset.info), set(orig.info))
 
-    def test_take_seqs_moltype(self):  # ported
+    def test_take_seqs_moltype(self):
         """take_seqs should preserve the MolType"""
         orig = self.Class(
             data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}, moltype=DNA
@@ -449,7 +449,7 @@ class SequenceCollectionBaseTests(object):
         got = align_norm.to_nexus("protein")
         self.assertEqual(got, expect)
 
-    def test_num_seqs(self):  # ported
+    def test_num_seqs(self):
         """SequenceCollection.num_seqs should count seqs."""
         aln = self.Class({"seq1": "ACGU", "seq2": "CGUA", "seq3": "CCGU"})
         self.assertEqual(aln.num_seqs, 3)
@@ -785,7 +785,7 @@ class SequenceCollectionBaseTests(object):
                 dict(num_seqs=10, num_pos=60, ref_name="longest", wrap=60),
             )
 
-    def test_set_wrap_affects_repr_html(self):  # ported for seqcollection
+    def test_set_wrap_affects_repr_html(self):
         """the wrap argument affects the number of columns"""
         # indirectly tested via counting number of occurrences of 'class="label"'
         seqs = self.Class({"a": "AAAAA", "b": "AAA--"})
@@ -845,7 +845,7 @@ class SequenceCollectionTests(SequenceCollectionBaseTests, TestCase):
         self.ragged.names = "bac"
         self.assertEqual(list(self.ragged.seqs), ["AAA", "AAAAAA", "AAAA"])
 
-    def test_iter_seqs_ragged(self):  # ported
+    def test_iter_seqs_ragged(self):
         """SequenceCollection iter_seqs() method should support reordering of seqs"""
         self.ragged.names = ["a", "b", "c"]
         seqs = list(self.ragged.iter_seqs())
@@ -3703,7 +3703,7 @@ def test_slice_aligned_featuremap_multi_spans():
     assert str(sliced) == "AAGGGCCC"
 
 
-def test_sequence_collection_repr():  # ported
+def test_sequence_collection_repr():
     data = {
         "ENSMUSG00000056468": "GCCAGGGGGAAAAGGGAGAA",
         "ENSMUSG00000039616": "GCCCTTCAAATTT",
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index 0241a33c1..823efa22c 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -1,13 +1,18 @@
-import os
-
 import numpy as numpy
 import pytest
 
-import cogent3.core.new_alignment as new_aln
-import cogent3.core.new_alphabet as new_alpha
-import cogent3.core.new_moltype as new_moltype
-
+from cogent3 import get_moltype
 from cogent3._version import __version__
+from cogent3.core.alphabet import CharAlphabet
+from cogent3.core.new_alignment import (
+    AlignedData,
+    AlignedDataView,
+    SeqData,
+    SeqDataView,
+    seq_index,
+    seq_to_gap_coords,
+    validate_names,
+)
 
 
 @pytest.fixture
@@ -16,25 +21,24 @@ def seq1():
 
 
 @pytest.fixture
-def str_seqs_dict(moltype="dna"):
-    if moltype == "dna":
-        return dict(seq1="ACGT", seq2="GTTTGCA")
+def simple_dict():
+    return dict(seq1="ACGT", seq2="GTTTGCA")
 
 
 @pytest.fixture
-def arr_seqs_dict():
+def simple_dict_arr():
     return dict(seq1=numpy.array([2, 1, 3, 0]), seq2=numpy.array([3, 0, 0, 0, 3, 1, 2]))
 
 
 @pytest.fixture
-def alpha(moltype="dna"):
-    moltype = new_moltype.get_moltype(moltype)
-    return moltype.degen_gapped_alphabet
+def alpha():
+    moltype = get_moltype("dna")
+    return moltype.alphabets.degen_gapped
 
 
 @pytest.fixture
-def dna_sd(str_seqs_dict: dict[str, str], alpha):
-    return new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
+def sd_demo(simple_dict: dict[str, str], alpha):
+    return SeqData(data=simple_dict, alphabet=alpha)
 
 
 @pytest.fixture
@@ -43,14 +47,8 @@ def int_arr():
 
 
 @pytest.fixture
-def sdv_s2(dna_sd: new_aln.SeqsData) -> new_aln.SeqDataView:
-    return dna_sd.get_seq_view(seqid="seq2")
-
-
-@pytest.fixture
-def simple_dna_seq_coll(dna_sd: new_aln.SeqsData) -> new_aln.SequenceCollection:
-    moltype = new_moltype.get_moltype("dna")
-    return new_aln.SequenceCollection(seqs_data=dna_sd, moltype=moltype)
+def sdv_s2(sd_demo: SeqData) -> SeqDataView:
+    return sd_demo.get_seq_view(seqid="seq2")
 
 
 @pytest.fixture
@@ -60,45 +58,70 @@ def aligned_dict():
 
 @pytest.fixture
 def ad_demo(aligned_dict: dict[str, str]):
-    return new_aln.AlignedData.from_gapped_seqs(aligned_dict)
+    return AlignedData.from_gapped_seqs(aligned_dict)
 
 
-@pytest.fixture
-def ragged_padded():
-    return new_aln.make_unaligned_seqs(
-        data={"a": "AAAAAA", "b": "AAA---", "c": "AAAA--"}, moltype="dna"
-    )
+def test_seqdata_default_attributes(sd_demo: SeqData):
+    assert sd_demo._names == ("seq1", "seq2")
+    assert isinstance(sd_demo._alphabet, CharAlphabet)
 
 
-@pytest.fixture
-def ragged():
-    return new_aln.make_unaligned_seqs(
-        data={"a": "AAAAAA", "b": "AAA", "c": "AAAA"}, moltype="dna"
-    )
+def test_seqdata_seq_if_str(seq1: str, alpha):
+    with pytest.raises(NotImplementedError):
+        SeqData(seq1, alphabet=alpha)
+    # assert SeqData(data)._names != ("A", "C", "T", "G")
 
 
-def test_seqsdata_default_attributes(dna_sd: new_aln.SeqsData):
-    assert dna_sd.names == ["seq1", "seq2"]
-    assert isinstance(dna_sd._alphabet, new_alpha.CharAlphabet)
+def test_validate_names_dict_happy(simple_dict):
+    got = validate_names(simple_dict, None)
+    assert got == ("seq1", "seq2")
+    got = validate_names(simple_dict, names=("seq2", "seq1"))
+    assert got == ("seq2", "seq1")
 
 
-@pytest.mark.xfail(reason="SeqsData currently expects correctly formatted data")
-def test_seqsdata_seq_if_str(seq1: str, alpha):
-    with pytest.raises(NotImplementedError):
-        new_aln.SeqsData(seq1, alphabet=alpha)
+@pytest.mark.parametrize(
+    "bad_names", [("bad"), ("bad",), ("bad2", "bad1"), ("seq1",), "seq1"]
+)
+def test_validate_names_dict_bad(simple_dict, bad_names):
+    with pytest.raises(ValueError):
+        validate_names(simple_dict, bad_names)
+
+
+@pytest.mark.parametrize(
+    "names", (["seq2"], ["seq2", "seq1"], ("seq2",), ("seq2", "seq1"))
+)
+def test_names_tuple_list_happy(names, sd_demo):
+    correct_names = sd_demo._names
+    got = validate_names(correct_names, names)
+    assert got == tuple(names)
+
 
+@pytest.mark.parametrize("bad_names", [("bad"), ("bad",), ("bad2", "bad1"), "seq1"])
+def test_names_tuple_list_bad(bad_names, sd_demo):
+    correct_names = sd_demo._names
+    with pytest.raises(ValueError):
+        validate_names(correct_names, bad_names)
+
+
+@pytest.mark.parametrize(
+    "bad_names", [("bad"), ("bad",), ("bad2", "bad1"), ("seq1",), "seq1"]
+)
+def test_names_init(simple_dict, alpha, bad_names):
+    sd = SeqData(simple_dict, alphabet=alpha)
+    assert sd._names == ("seq1", "seq2")
+    sd = SeqData(simple_dict, alphabet=alpha, names=("seq2", "seq1"))
+    assert sd._names == ("seq2", "seq1")
 
-def test_seqdataview_zero_step_raises(dna_sd):
     with pytest.raises(ValueError):
-        new_aln.SeqDataView(seq=dna_sd, step=0, seq_len=4)
+        SeqData(simple_dict, alphabet=alpha, names=bad_names)
 
 
 @pytest.mark.parametrize("seqid", ["seq1", "seq2"])
-def test_seqdataview_repr_default(dna_sd: new_aln.SeqsData, seqid: str):
-    seq = dna_sd.get_seq_str(seqid=seqid)
+def test_seqdataview_repr_default(sd_demo: SeqData, seqid: str):
+    seq = sd_demo.get_seq_str(seqid=seqid)
     seq_len = len(seq)
     expect = f"SeqDataView(seq={seq}, start=0, stop={seq_len}, step=1, offset=0, seqid='{seqid}', seq_len={seq_len})"
-    got = dna_sd.get_seq_view(seqid)
+    got = sd_demo.get_seq_view(seqid)
     assert repr(got) == expect
 
 
@@ -109,34 +132,29 @@ def test_seqdataview_repr_default_long(alpha):
     expect = f"SeqDataView(seq={trunc}, start=0, stop={seq_len}, step=1, offset=0, seqid='long', seq_len={seq_len})"
 
     d = {"long": longseq}
-    sd = new_aln.SeqsData(d, alphabet=alpha)
+    sd = SeqData(d, alphabet=alpha)
     got = sd.get_seq_view(seqid="long")
     assert repr(got) == expect
 
 
-@pytest.mark.xfail(reason="do we want to support copying and/or copying with slicing?")
 @pytest.mark.parametrize("seqid", ["seq1", "seq2"])
-@pytest.mark.parametrize("sliced", (False, True))
-@pytest.mark.parametrize("step", (1, -1))
-def test_seqdataview_copy(alpha, seqid, sliced, step):
-    seq1 = "ATGTTCTC"
-    seq2 = "ATGAACTCATT"
-    start, stop = 2, 6
-    data = {"seq1": seq1, "seq2": seq2}
-
-    sd = new_aln.SeqsData(data=data, alphabet=alpha)
-    sdv = sd.get_seq_view(seqid=seqid)
-    sliced_sdv = sdv[start:stop:step]
-
-    copied_sdv = sliced_sdv.copy(sliced=sliced)
+def test_seqdataview_to_rich_dict(sd_demo: SeqData, seqid):
+    seq = sd_demo.get_seq_str(seqid=seqid)
+    expect = {
+        "type": "cogent3.core.new_alignment.SeqDataView",
+        "version": __version__,
+        "init_args": {"seq": seq, "seqid": seqid, "step": 1, "offset": 0},
+    }
 
-    assert copied_sdv.str_value == data[seqid][start:stop:step]
+    sdv = sd_demo.get_seq_view(seqid)
+    got = sdv.to_rich_dict()
+    assert expect == got
 
 
 @pytest.mark.parametrize("seqid", ["seq1", "seq2"])
-def test_seqsdata_get_seq_view(str_seqs_dict, alpha, seqid):
-    sd = new_aln.SeqsData(str_seqs_dict, alphabet=alpha)
-    seq = str_seqs_dict[seqid]
+def test_seqdata_get_seq_view(simple_dict, alpha, seqid):
+    sd = SeqData(simple_dict, alphabet=alpha)
+    seq = simple_dict[seqid]
     seq_len = len(seq)
     got = sd.get_seq_view(seqid)
     assert got.seq == sd
@@ -148,101 +166,124 @@ def test_seqsdata_get_seq_view(str_seqs_dict, alpha, seqid):
 @pytest.mark.parametrize("seq", ("seq1", "seq2"))
 @pytest.mark.parametrize("start", (None, -1, 0, 1, 4))
 @pytest.mark.parametrize("stop", (None, -1, 0, 1, 4))
-def test_get_seq_str(str_seqs_dict, alpha, seq, start, stop):
+def test_get_seq_str(simple_dict, alpha, seq, start, stop):
     # slicing should be tested in test_get_seq_array
-    expect = str_seqs_dict[seq][start:stop]
-    sd = new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
+    expect = simple_dict[seq][start:stop]
+    sd = SeqData(data=simple_dict, alphabet=alpha)
     got = sd.get_seq_str(seqid=seq, start=start, stop=stop)
     assert expect == got
 
 
-def test_get_seq_str_empty(dna_sd: new_aln.SeqsData):
+def test_get_seq_str_empty(sd_demo: SeqData):
     with pytest.raises(TypeError):
-        dna_sd.get_seq_str()
+        sd_demo.get_seq_str()
 
 
-def test_names_get(str_seqs_dict, alpha):
-    expect = str_seqs_dict.keys()
-    sd = new_aln.SeqsData(str_seqs_dict, alphabet=alpha)
+def test_names_get(simple_dict, alpha):
+    expect = simple_dict.keys()
+    sd = SeqData(simple_dict, alphabet=alpha)
     got = sd.names
     # returns iterator
     assert list(got) == list(expect)
 
 
-def test_get_seq_array(str_seqs_dict, alpha):
+@pytest.mark.parametrize(
+    "seq, moltype_name",
+    [("TCAG-NRYWSKMBDHV?", "dna"), ("UCAG-NRYWSKMBDHV?", "rna")],
+)
+def test_seq_index_str(seq, moltype_name, int_arr):
+    alpha = get_moltype(moltype_name).alphabets.degen_gapped
+    got = seq_index(seq, alpha)
+    assert numpy.array_equal(got, int_arr)
+
+
+@pytest.mark.parametrize(
+    "seq, moltype_name",
+    [(b"TCAG-NRYWSKMBDHV?", "dna"), (b"UCAG-NRYWSKMBDHV?", "rna")],
+)
+def test_seq_index_bytes(seq, moltype_name, int_arr):
+    alpha = get_moltype(moltype_name).alphabets.degen_gapped
+    got = seq_index(seq, alpha)
+    assert numpy.array_equal(got, int_arr)
+
+
+@pytest.mark.parametrize("moltype_name", ("dna", "rna"))
+def test_seq_index_arr(moltype_name, int_arr):
+    alpha = get_moltype(moltype_name).alphabets.degen_gapped
+    got = seq_index(int_arr, alpha)
+    assert numpy.array_equal(got, int_arr)
+    assert got.dtype == int_arr.dtype
+
+
+def test_get_seq_array(simple_dict, alpha):
     # TODO: slicing should be tested here, not get_seq_str
-    # todo: kath, edit and parametrize for all moltypes
     # seq1
     expect = numpy.array([2, 1, 3, 0], dtype="uint8")
-    sd = new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
+    sd = SeqData(data=simple_dict, alphabet=alpha)
     got = sd.get_seq_array(seqid="seq1")
     assert numpy.array_equal(got, expect)
 
     # seq2
     expect = numpy.array([3, 0, 0, 0, 3, 1, 2], dtype="uint8")
-    sd = new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
+    sd = SeqData(data=simple_dict, alphabet=alpha)
     got = sd.get_seq_array(seqid="seq2")
     assert numpy.array_equal(got, expect)
 
 
-def test_get_seq_bytes(dna_sd: new_aln.SeqsData):
+def test_get_seq_bytes(sd_demo: SeqData):
     # getting seqid and slicing tested in test_get_seq_str
-    got = dna_sd.get_seq_bytes(seqid="seq1")
+    got = sd_demo.get_seq_bytes(seqid="seq1")
     assert isinstance(got, bytes)
 
 
-def test_seqdataview_make_seq_default(dna_sd):
-    assert dna_sd.make_seq is None
+# SeqData set_seq_maker tests
+def test_seqdataview_make_seq_default(sd_demo):
+    assert sd_demo.make_seq is None
 
 
-def test_seqdataview_make_seq_setget(dna_sd):
-    dna_sd.make_seq = new_moltype.get_moltype("dna").make_seq
-    assert dna_sd.make_seq == new_moltype.get_moltype("dna").make_seq
+@pytest.mark.parametrize(
+    "maker", (get_moltype("dna").make_seq, get_moltype("dna").make_array_seq)
+)
+def test_seqdataview_make_seq_setget(sd_demo, maker):
+    sd_demo.make_seq = maker
+    assert sd_demo.make_seq is maker
 
 
+# SeqData __getitem__ tests
 @pytest.mark.parametrize("seq", ("seq1", "seq2"))
-def test_getitem_str_1(dna_sd, seq):
-    got = dna_sd[seq]
-    assert got.seq == dna_sd
+def test_getitem_str_1(sd_demo, seq):
+    got = sd_demo[seq]
+    assert got.seq == sd_demo
     assert got.seqid == seq
 
 
 @pytest.mark.xfail(reason="'SeqDataView' object has no attribute 'replace'")
 @pytest.mark.parametrize("idx", (0, 1))
 @pytest.mark.parametrize("make_seq", (True, False))
-def test_getitem_int(str_seqs_dict, alpha, idx, make_seq):
-    sd = new_aln.SeqsData(str_seqs_dict, alphabet=alpha)
-    ms = new_moltype.get_moltype("dna").make_seq if make_seq else None
+def test_getitem_int(simple_dict, alpha, idx, make_seq):
+    sd = SeqData(simple_dict, alphabet=alpha)
+    ms = get_moltype("dna").make_seq if make_seq else None
     sd.make_seq = ms
     got = sd[idx]
     assert got.seq == sd
-    assert got.seqid == list(str_seqs_dict)[idx]
+    assert got.seqid == list(simple_dict)[idx]
 
 
 @pytest.mark.xfail(reason="'SeqDataView' object has no attribute 'replace'")
 @pytest.mark.parametrize("seqid", ("seq1", "seq2"))
 @pytest.mark.parametrize("make_seq", (True, False))
-def test_getitem_str(dna_sd, seqid, make_seq):
-    ms = new_moltype.get_moltype("dna").make_seq if make_seq else None
-    dna_sd.make_seq = ms
-    got = dna_sd[seqid]
-    assert got.seq == dna_sd
+def test_getitem_str(sd_demo, seqid, make_seq):
+    ms = get_moltype("dna").make_seq if make_seq else None
+    sd_demo.make_seq = ms
+    got = sd_demo[seqid]
+    assert got.seq == sd_demo
     assert got.seqid == seqid
 
 
-@pytest.mark.parametrize("make_seq", (True, False))
-def test_getitem_raises(dna_sd, make_seq):
-    ms = new_moltype.get_moltype("dna").make_seq if make_seq else None
-    dna_sd.make_seq = ms
-    invalid_index = ["this", "shouldn't", "work"]
-    with pytest.raises(NotImplementedError):
-        _ = dna_sd[invalid_index]
-
-
 # SeqDataView tests for returning an instance of itself
-def test_seqdataview_returns_self(dna_sd: new_aln.SeqsData):
-    sdv = dna_sd.get_seq_view("seq1")
-    assert isinstance(sdv, new_aln.SeqDataView)
+def test_seqdataview_returns_self(sd_demo: SeqData):
+    sdv = sd_demo.get_seq_view("seq1")
+    assert isinstance(sdv, SeqDataView)
 
 
 @pytest.mark.parametrize(
@@ -257,19 +298,19 @@ def test_seqdataview_returns_self(dna_sd: new_aln.SeqsData):
     ],
 )
 def test_seqdataview_slice_returns_self(seq1: str, index: slice):
-    sdv = new_aln.SeqDataView(seq=seq1, seqid="seq1", seq_len=len(seq1))
+    sdv = SeqDataView(seq=seq1, seqid="seq1", seq_len=len(seq1))
     got = sdv[index]
-    assert isinstance(got, new_aln.SeqDataView)
+    assert isinstance(got, SeqDataView)
 
 
 # SeqDataView tests for value properties
 @pytest.mark.parametrize("start", (None, 0, 1, 4, -1, -4))
 @pytest.mark.parametrize("stop", (None, 0, 1, 4, -1, -4))
 @pytest.mark.parametrize("step", (None, 1, 2, 3, -1, -2, -3))
-def test_seqdataview_value(str_seqs_dict: dict, alpha, start, stop, step):
+def test_seqdataview_value(simple_dict: dict, alpha, start, stop, step):
     seq = "seq2"
-    expect = str_seqs_dict[seq][start:stop:step]
-    sd = new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
+    expect = simple_dict[seq][start:stop:step]
+    sd = SeqData(data=simple_dict, alphabet=alpha)
     # Get SeqDataView on seq
     sdv = sd.get_seq_view(seqid=seq)
     sdv2 = sdv[start:stop:step]
@@ -280,10 +321,10 @@ def test_seqdataview_value(str_seqs_dict: dict, alpha, start, stop, step):
 @pytest.mark.parametrize("start", (None, 0, 1, 4, -1, -4))
 @pytest.mark.parametrize("stop", (None, 0, 1, 4, -1, -4))
 @pytest.mark.parametrize("step", (None, 1, 2, 3, -1, -2, -3))
-def test_seqsdata_array_value(arr_seqs_dict: dict, alpha, start, stop, step):
+def test_seqdata_array_value(simple_dict_arr: dict, alpha, start, stop, step):
     seq = "seq2"
-    expect = arr_seqs_dict[seq][start:stop:step]
-    sd = new_aln.SeqsData(data=arr_seqs_dict, alphabet=alpha)
+    expect = simple_dict_arr[seq][start:stop:step]
+    sd = SeqData(data=simple_dict_arr, alphabet=alpha)
     # Get SeqDataView on seq
     sdv = sd.get_seq_view(seqid=seq)
     got = sdv.array_value[start:stop:step]
@@ -293,11 +334,11 @@ def test_seqsdata_array_value(arr_seqs_dict: dict, alpha, start, stop, step):
 @pytest.mark.parametrize("start", (None, 0, 1, 4, -1, -4))
 @pytest.mark.parametrize("stop", (None, 0, 1, 4, -1, -4))
 @pytest.mark.parametrize("step", (None, 1, 2, 3, -1, -2, -3))
-def test_seqsdata_bytes_value(str_seqs_dict: dict, alpha, start, stop, step):
+def test_seqdata_bytes_value(simple_dict: dict, alpha, start, stop, step):
     seq = "seq2"
-    expect = str_seqs_dict[seq][start:stop:step]
+    expect = simple_dict[seq][start:stop:step]
     expect = expect.encode("utf8")
-    sd = new_aln.SeqsData(data=str_seqs_dict, alphabet=alpha)
+    sd = SeqData(data=simple_dict, alphabet=alpha)
     # Get SeqDataView on seq
     sdv = sd.get_seq_view(seqid=seq)
     got = sdv.bytes_value[start:stop:step]
@@ -305,13 +346,13 @@ def test_seqsdata_bytes_value(str_seqs_dict: dict, alpha, start, stop, step):
 
 
 # SeqDataView tests for special methods that access "value" properties
-def test_array(sdv_s2: new_aln.SeqDataView):
+def test_array(sdv_s2: SeqDataView):
     expect = sdv_s2.array_value
     got = numpy.array(sdv_s2)
     assert numpy.array_equal(expect, got)
 
 
-def test_bytes(sdv_s2: new_aln.SeqDataView):
+def test_bytes(sdv_s2: SeqDataView):
     expect = sdv_s2.bytes_value
     got = bytes(sdv_s2)
     assert expect == got
@@ -321,66 +362,53 @@ def test_bytes(sdv_s2: new_aln.SeqDataView):
 def test_from_string_unequal_seqlens():
     data = dict(seq1="A-A", seq2="AAAAAAA--")
     with pytest.raises(ValueError):
-        new_aln.AlignedData.from_gapped_seqs(data=data)
+        AlignedData.from_gapped_seqs(data=data)
 
 
-@pytest.mark.xfail(reason="AlignedData not yet functional")
 def test_aligned_from_string_returns_self(aligned_dict):
-    got = new_aln.AlignedData.from_gapped_seqs(data=aligned_dict)
-    assert isinstance(got, new_aln.AlignedData)
+    got = AlignedData.from_gapped_seqs(data=aligned_dict)
+    assert isinstance(got, AlignedData)
     # assert gap lengths
 
 
-@pytest.mark.xfail(reason="AlignedData not yet functional")
 @pytest.mark.parametrize("seqid", ("seq1", "seq2"))
 def test_get_aligned_view(aligned_dict, seqid):
-    ad = new_aln.AlignedData.from_gapped_seqs(aligned_dict)
+    ad = AlignedData.from_gapped_seqs(aligned_dict)
     got = ad.get_aligned_view(seqid)
-    assert isinstance(got, new_aln.AlignedDataView)
+    assert isinstance(got, AlignedDataView)
     assert got.seq == ad
     assert got.stop == ad.align_len
     assert got.seq_len == ad.align_len
 
 
-@pytest.mark.xfail(reason="AlignedData not yet functional")
+# AlignedData seq to gaps
 def test_seq_to_gap_coords_str_all_gaps():
     parent_seq = "-----"
     expect_gaplen = numpy.array([len(parent_seq)])
-    got_ungap, got_map = new_aln.seq_to_gap_coords(
-        parent_seq, moltype=new_moltype.get_moltype("dna")
-    )
+    got_ungap, got_map = seq_to_gap_coords(parent_seq, moltype=get_moltype("dna"))
     assert got_ungap == ""
     assert got_map.cum_gap_lengths == expect_gaplen
 
 
-@pytest.mark.xfail(reason="AlignedData not yet functional")
 def test_seq_to_gap_coords_str_no_gaps():
     parent_seq = "ACTGC"
-    got_ungap, got_empty_arr = new_aln.seq_to_gap_coords(
-        parent_seq, moltype=new_moltype.get_moltype("dna")
-    )
+    got_ungap, got_empty_arr = seq_to_gap_coords(parent_seq, moltype=get_moltype("dna"))
     assert got_ungap == parent_seq
     assert got_empty_arr.size == 0
 
 
-@pytest.mark.xfail(reason="AlignedData not yet functional")
 def test_seq_to_gap_coords_arr_all_gaps():
-    alpha = new_moltype.get_moltype("dna").degen_gapped_alphabet
-    parent_seq = alpha.to_indices("-----", alpha)
-    got_ungap, got_map = new_aln.seq_to_gap_coords(
-        parent_seq, moltype=new_moltype.get_moltype("dna")
-    )
+    alpha = get_moltype("dna").alphabets.degen_gapped
+    parent_seq = seq_index("-----", alpha)
+    got_ungap, got_map = seq_to_gap_coords(parent_seq, moltype=get_moltype("dna"))
     assert got_ungap.size == 0
     assert got_map.get_gap_coordinates() == [[0, 5]]
 
 
-@pytest.mark.xfail(reason="AlignedData not yet functional")
 def test_seq_to_gap_coords_arr_no_gaps():
-    alpha = new_moltype.get_moltype("dna").degen_gapped_alphabet
-    parent_seq = alpha.to_indices("ACTGC", alpha)
-    got_ungap, got_empty_arr = new_aln.seq_to_gap_coords(
-        parent_seq, moltype=new_moltype.get_moltype("dna")
-    )
+    alpha = get_moltype("dna").alphabets.degen_gapped
+    parent_seq = seq_index("ACTGC", alpha)
+    got_ungap, got_empty_arr = seq_to_gap_coords(parent_seq, moltype=get_moltype("dna"))
     assert numpy.array_equal(got_ungap, parent_seq)
     assert got_empty_arr.size == 0
 
@@ -394,222 +422,19 @@ def gap_seqs():
     ]
 
 
-@pytest.mark.xfail(reason="AlignedData not yet functional")
-@pytest.mark.parametrize("i", range(3))
+@pytest.mark.parametrize("i", range(3))  # range(len(gap_seqs()))
 def test_seq_to_gap_coords_str(gap_seqs, i):
     seq, gap_coords = gap_seqs[i]
-    got_ungapped, got_map = new_aln.seq_to_gap_coords(
-        seq, moltype=new_moltype.get_moltype("dna")
-    )
+    got_ungapped, got_map = seq_to_gap_coords(seq, moltype=get_moltype("dna"))
     assert got_ungapped == seq.replace("-", "")
     assert got_map.get_gap_coordinates() == gap_coords
 
 
-@pytest.mark.xfail(reason="AlignedData not yet functional")
-@pytest.mark.parametrize("i", range(3))
+@pytest.mark.parametrize("i", range(3))  # range(len(gap_seqs()))
 def test_seq_to_gap_coords_arr(gap_seqs, i):
     seq, gap_coords = gap_seqs[i]
-    alpha = new_moltype.get_moltype("dna").degen_gapped_alphabet
-    seq = alpha.to_indices(seq, alpha)  # convert to array repr
-    got_ungapped, got_map = new_aln.seq_to_gap_coords(
-        seq, moltype=new_moltype.get_moltype("dna")
-    )
+    alpha = get_moltype("dna").alphabets.degen_gapped
+    seq = seq_index(seq, alpha)  # convert to array repr
+    got_ungapped, got_map = seq_to_gap_coords(seq, moltype=get_moltype("dna"))
     assert numpy.array_equal(got_ungapped, seq[seq != 4])  # gap_char = 4
     assert got_map.get_gap_coordinates() == gap_coords
-
-
-def test_seq_coll_init(simple_dna_seq_coll):
-    assert isinstance(simple_dna_seq_coll.seqs, new_aln.SeqsData)
-    assert simple_dna_seq_coll.seqs.make_seq is not None
-
-
-@pytest.mark.parametrize("moltype", ("dna", "rna", "protein", "protein_with_stop"))
-def test_make_unaligned_seqs_dict(moltype):
-    """test SequenceCollection constructor utility function"""
-    data = {"a": "AGGCCC", "b": "AGAAAA"}
-    got = new_aln.make_unaligned_seqs(data=data, moltype=moltype)
-    assert isinstance(got, new_aln.SequenceCollection)
-    assert got._seqs_data.make_seq == new_moltype.get_moltype(moltype).make_seq
-
-    # should also work if seqs are arrays
-    data = {"a": numpy.array(["AGGCCC"]), "b": numpy.array(["AGAAAA"])}
-    got = new_aln.make_unaligned_seqs(data=data, moltype=moltype)
-    assert isinstance(got, new_aln.SequenceCollection)
-    assert got._seqs_data.make_seq == new_moltype.get_moltype(moltype).make_seq
-
-
-@pytest.mark.parametrize("moltype", ("dna", "rna", "protein", "protein_with_stop"))
-def test_make_unaligned_seqs_list(moltype):
-    """test SequenceCollection constructor utility function"""
-    data = ["AGGCCC", "AGAAAA"]
-    got = new_aln.make_unaligned_seqs(data=data, moltype=moltype)
-    assert isinstance(got, new_aln.SequenceCollection)
-    assert got._seqs_data.make_seq == new_moltype.get_moltype(moltype).make_seq
-
-    # should also work if seqs are arrays
-    data = [numpy.array(["AGGCCC"]), numpy.array(["AGAAAA"])]
-    got = new_aln.make_unaligned_seqs(data=data, moltype=moltype)
-    assert isinstance(got, new_aln.SequenceCollection)
-    assert got._seqs_data.make_seq == new_moltype.get_moltype(moltype).make_seq
-
-
-def test_make_unaligned_seqs_raises():
-    data = "AGTCCTGA"
-    with pytest.raises(NotImplementedError):
-        new_aln.make_unaligned_seqs(data=data, moltype="dna")
-
-
-def test_make_unaligned_seqs_incompatible_moltype(dna_sd):
-    with pytest.raises(ValueError):
-        _ = new_aln.make_unaligned_seqs(data=dna_sd, moltype="rna")
-
-
-def test_make_unaligned_seqs_no_seqs():
-    data = {}
-    with pytest.raises(ValueError):
-        new_aln.make_unaligned_seqs(data=data, moltype="dna")
-
-
-@pytest.mark.xfail(
-    reason="todo: kath, AttributeError: 'SeqDataView' object has no attribute 'replace'"
-)
-def test_iter_seqs_ragged_padded(ragged_padded):
-    """SequenceCollection.iter_seqs() method should support reordering of seqs"""
-    seqs = list(ragged_padded.iter_seqs())
-    assert seqs == ["AAAAAA", "AAA---", "AAAA--"]
-    seqs = list(ragged_padded.iter_seqs(seq_order=["b", "a", "a"]))
-    assert seqs == ["AAA---", "AAAAAA", "AAAAAA"]
-    assert seqs[1] is seqs[2]
-    assert seqs[0], ragged_padded.seqs["b"]
-
-
-@pytest.mark.xfail(
-    reason="todo: kath, AttributeError: 'SeqDataView' object has no attribute 'replace'"
-)
-def test_iter_seqs_ragged(ragged):
-    """SequenceCollection iter_seqs() method should support reordering of seqs"""
-    seqs = list(ragged.iter_seqs())
-    assert seqs == ["AAAAAA", "AAA", "AAAA"]
-    seqs = list(ragged.iter_seqs(seq_order=["b", "a", "a"]))
-    assert seqs == ["AAA", "AAAAAA", "AAAAAA"]
-    assert seqs[1] is seqs[2]
-    assert seqs[0], ragged.seqs["b"]
-
-
-def test_sequence_collection_repr():
-    data = {
-        "ENSMUSG00000056468": "GCCAGGGGGAAAAGGGAGAA",
-        "ENSMUSG00000039616": "GCCCTTCAAATTT",
-    }
-    seqs = new_aln.make_unaligned_seqs(data=data, moltype="dna")
-    assert (
-        repr(seqs)
-        == "2x (ENSMUSG00000039616[GCCCTTCAAA...], ENSMUSG00000056468[GCCAGGGGGA...]) dna seqcollection"
-    )
-
-    data = {
-        "ENSMUSG00000039616": "GCCCTTCAAATTT",
-        "ENSMUSG00000056468": "GCCAGGGGGAAAAGGGAGAA",
-    }
-    seqs = new_aln.make_unaligned_seqs(data=data, moltype="dna")
-    assert (
-        repr(seqs)
-        == "2x (ENSMUSG00000039616[GCCCTTCAAA...], ENSMUSG00000056468[GCCAGGGGGA...]) dna seqcollection"
-    )
-
-    data = {
-        "a": "TCGAT",
-    }
-    seqs = new_aln.make_unaligned_seqs(data=data, moltype="dna")
-    assert repr(seqs) == "1x (a[TCGAT]) dna seqcollection"
-
-    data = {
-        "a": "TCGAT" * 2,
-    }
-    seqs = new_aln.make_unaligned_seqs(data=data, moltype="dna")
-    assert repr(seqs) == "1x (a[TCGATTCGAT]) dna seqcollection"
-
-
-@pytest.mark.xfail(
-    reason="todo: kath, new 'MolType' object has no attribute 'get_css_style'"
-)
-def test_set_wrap_affects_repr_html():
-    """the wrap argument affects the number of columns"""
-    # indirectly tested via counting number of occurrences of 'class="label"'
-    seqs = new_aln.make_unaligned_seqs(data={"a": "AAAAA", "b": "AAA--"}, moltype="dna")
-    orig = seqs._repr_html_()
-    seqs.set_repr_policy(wrap=3)  # break alignment into 2
-    got = seqs._repr_html_()
-    token = 'class="label"'
-    assert got.count(token) == 2 * orig.count(token)
-
-    # using environment variable
-    env_name = "COGENT3_ALIGNMENT_REPR_POLICY"
-    os.environ[env_name] = "wrap=2"
-    seqs = new_aln.make_unaligned_seqs(data={"a": "AAAAA", "b": "AAA--"}, moltype="dna")
-    got = seqs._repr_html_()
-    os.environ.pop(env_name, None)
-    assert got.count(token) == 3 * orig.count(token)
-
-
-@pytest.mark.xfail(reason="todo: kath, need support for __eq__ betwen collections")
-def test_take_seqs(ragged_padded):
-    """SequenceCollection take_seqs should return new SequenceCollection with selected seqs."""
-    a = ragged_padded.take_seqs(list("bc"))
-    assert isinstance(a, new_aln.SequenceCollection)
-    assert a == {"b": "AAA---", "c": "AAAA--"}
-    # should be able to negate
-    a = ragged_padded.take_seqs(list("bc"), negate=True)
-    assert a == {"a": "AAAAAA"}
-
-
-@pytest.mark.xfail(reason="todo: kath, need support for __eq__ betwen collections")
-def test_take_seqs_str(ragged_padded):
-    """string arg to SequenceCollection take_seqs should work."""
-    a = ragged_padded.take_seqs("a")
-    assert a == {"a": "AAAAAA"}
-
-    # should be able to negate
-    a = ragged_padded.take_seqs("a", negate=True)
-    assert isinstance(a, new_aln.SequenceCollection)
-    assert a == {"b": "AAA---", "c": "AAAA--"}
-
-
-def test_take_seqs_info():
-    """take_seqs should preserve info attribute"""
-    moltype = new_moltype.get_moltype("dna")
-    orig = new_aln.make_unaligned_seqs(
-        data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"},
-        moltype=moltype,
-        info={"key": "value"},
-    )
-    subset = orig.take_seqs(list("ab"))
-    assert set(subset.info) == set(orig.info)
-
-
-def test_take_seqs_moltype():
-    """take_seqs should preserve the MolType"""
-    moltype = new_moltype.get_moltype("dna")
-    orig = new_aln.make_unaligned_seqs(
-        data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}, moltype=moltype
-    )
-    subset = orig.take_seqs(list("ab"))
-    assert set(subset.moltype) == set(orig.moltype)
-
-
-def test_take_seqs_empty_names():
-    moltype = new_moltype.get_moltype("dna")
-    orig = new_aln.make_unaligned_seqs(
-        data={"a": "CCCCCC", "b": "AAA---", "c": "AAAA--"}, moltype=moltype
-    )
-    subset = orig.take_seqs([])
-    assert subset == {}
-
-
-def test_num_seqs():
-    """SequenceCollection.num_seqs should count seqs."""
-
-    aln = new_aln.make_unaligned_seqs(
-        data={"seq1": "ACGU", "seq2": "CGUA", "seq3": "CCGU"}, moltype="rna"
-    )
-    assert aln.num_seqs == 3
diff --git a/tests/test_core/test_new_alphabet.py b/tests/test_core/test_new_alphabet.py
index 18eb00102..c39165306 100644
--- a/tests/test_core/test_new_alphabet.py
+++ b/tests/test_core/test_new_alphabet.py
@@ -1,7 +1,11 @@
+import itertools
+
 import numpy
 import pytest
 
-from cogent3.core import new_alphabet
+from numpy.testing import assert_allclose
+
+from cogent3.core import new_alphabet, new_moltype
 
 
 @pytest.mark.parametrize(
@@ -123,3 +127,157 @@ def test_is_valid_typerror():
     alpha = new_alphabet.CharAlphabet(list("TCAG"))
     with pytest.raises(TypeError):
         _ = alpha.is_valid(list("ACGT"))
+
+
+@pytest.mark.parametrize("num_states,ndim", ((2, 1), (4, 2), (4, 4)))
+def test_interconversion_of_coords_indices(num_states, ndim):
+    # make sure match numpy functions
+    coeffs = numpy.array(new_alphabet.coord_conversion_coeffs(num_states, ndim))
+    for coord in itertools.product(*(list(range(num_states)),) * ndim):
+        coord = numpy.array(coord, dtype=numpy.uint64)
+        nidx = numpy.ravel_multi_index(coord, dims=(num_states,) * ndim)
+        idx = new_alphabet.coord_to_index(coord, coeffs)
+        assert idx == nidx
+        ncoord = numpy.unravel_index(nidx, shape=(num_states,) * ndim)
+        got = new_alphabet.index_to_coord(idx, coeffs)
+        assert (got == ncoord).all()
+        assert (got == coord).all()
+
+
+def test_coord2index_fail():
+    coord = numpy.array((0, 1), dtype=numpy.uint64)
+    coeffs = numpy.array(new_alphabet.coord_conversion_coeffs(2, 4))
+    # dimension of coords inconsistent with coeffs
+    with pytest.raises(ValueError):
+        new_alphabet.coord_to_index(coord, coeffs)
+
+
+@pytest.mark.parametrize("k", (2, 3))
+def test_kmer_alphabet_construction(k):
+    dna = new_moltype.get_moltype("dna")
+    monomers = dna.alphabet
+    kmers = monomers.get_word_alphabet(k)
+    assert len(kmers) == 4**k
+
+
+@pytest.mark.parametrize("k", (2, 3))
+def test_kmer_gapped_alphabet_construction(k):
+    dna = new_moltype.get_moltype("dna")
+    monomers = dna.gapped_alphabet
+    kmers = monomers.get_word_alphabet(k, include_gap=True)
+    assert len(kmers) == 1 + 4**k
+
+
+def get_rand_coord_seq(k, cast):
+    dna = new_moltype.get_moltype("dna")
+    monomers = dna.gapped_alphabet
+    coord = numpy.random.randint(0, 4, size=k, dtype=numpy.uint8)
+    seq = monomers.from_indices(coord)
+    if cast == bytes:
+        seq = seq.encode("utf8")
+    elif cast is None:
+        seq = coord
+    return coord, seq
+
+
+@pytest.mark.parametrize("cast", (None, bytes, str))
+@pytest.mark.parametrize("k", (2, 3))
+def test_kmer_alphabet_pack(k, cast):
+    # should match numpy ravel
+    dna = new_moltype.get_moltype("dna")
+    monomers = dna.gapped_alphabet
+    kmers = monomers.get_word_alphabet(k, include_gap=True)
+    coord, seq = get_rand_coord_seq(k, cast)
+    expect = numpy.ravel_multi_index(coord, dims=(4,) * k)
+    assert kmers.pack(seq) == expect
+
+
+def test_kmer_alphabet_pack_invalid():
+    # should match numpy ravel
+    dna = new_moltype.get_moltype("dna")
+    kmers = dna.gapped_alphabet.get_word_alphabet(3, include_gap=True)
+    with pytest.raises(TypeError):
+        kmers.pack([0, 1, 2])
+
+
+@pytest.mark.parametrize("k", (2, 3))
+def test_kmer_alphabet_unpack(k):
+    # should match numpy ravel
+    dna = new_moltype.get_moltype("dna")
+    monomers = dna.gapped_alphabet
+    kmers = monomers.get_word_alphabet(k, include_gap=True)
+    coord = numpy.random.randint(0, 4, size=k, dtype=numpy.uint8)
+    index = numpy.ravel_multi_index(coord, dims=(4,) * k)
+    assert (kmers.unpack(index) == coord).all()
+
+
+def test_seq_to_kmer_indices():
+    numpy_func = numpy.ravel_multi_index
+    dims = 4, 4, 4
+    dna = new_moltype.get_moltype("dna")
+    seq = "ATGGGCAGA"
+    arr = dna.alphabet.to_indices(seq)
+    coeffs = new_alphabet.coord_conversion_coeffs(4, 3)
+    num = len(seq) // 3
+    result = numpy.zeros(num, dtype=numpy.uint8)
+    got = new_alphabet.seq_to_kmer_indices(arr, result, coeffs, 4, 3)
+    expect = numpy.array(
+        [numpy_func(arr[i : i + 3], dims=dims) for i in range(0, 9, 3)]
+    )
+    assert_allclose(got, expect)
+
+
+def test_kmer_alphabet_to_indices_independent():
+    numpy_func = numpy.ravel_multi_index
+    dims = 4, 4, 4
+    dna = new_moltype.get_moltype("dna")
+    seq = "ATGGGCAGA"
+    arr = dna.alphabet.to_indices(seq)
+    trinuc_alpha = dna.alphabet.get_word_alphabet(k=3, include_gap=False)
+    got = trinuc_alpha.to_indices(arr, independent_kmer=True)
+    expect = numpy.array(
+        [numpy_func(arr[i : i + 3], dims=dims) for i in range(0, 9, 3)]
+    )
+    assert_allclose(got, expect)
+
+
+def test_kmer_alphabet_to_indices_non_independent():
+    numpy_func = numpy.ravel_multi_index
+    dims = 4, 4, 4
+    dna = new_moltype.get_moltype("dna")
+    seq = "ATGGGCAGA"
+    arr = dna.alphabet.to_indices(seq)
+    trinuc_alpha = dna.alphabet.get_word_alphabet(k=3, include_gap=False)
+    got = trinuc_alpha.to_indices(arr, independent_kmer=False)
+    assert len(got) == len(seq) - 3 + 1
+    expect = numpy.array(
+        [numpy_func(arr[i : i + 3], dims=dims) for i in range(0, 9 - 3 + 1)]
+    )
+    assert_allclose(got, expect)
+
+
+def test_kmer_alphabet_to_indices_invalid():
+    dna = new_moltype.get_moltype("dna")
+    trinuc_alpha = dna.alphabet.get_word_alphabet(k=3, include_gap=False)
+    with pytest.raises(TypeError):
+        trinuc_alpha.to_indices(["ACG", "TGG"])
+
+
+def test_kmer_alphabet_from_indices_independent():
+    dna = new_moltype.get_moltype("dna")
+    seq = "ATGGGCAGA"
+    arr = dna.alphabet.to_indices(seq)
+    trinuc_alpha = dna.alphabet.get_word_alphabet(k=3, include_gap=False)
+    kmer_seq = trinuc_alpha.to_indices(arr, independent_kmer=True)
+    got = trinuc_alpha.from_indices(kmer_seq)
+    assert_allclose(got, arr)
+
+
+def test_kmer_alphabet_from_indices_not_independent():
+    dna = new_moltype.get_moltype("dna")
+    seq = "ATGGGCAGA"
+    arr = dna.alphabet.to_indices(seq)
+    trinuc_alpha = dna.alphabet.get_word_alphabet(k=3, include_gap=False)
+    kmer_seq = trinuc_alpha.to_indices(arr, independent_kmer=False)
+    got = trinuc_alpha.from_indices(kmer_seq, independent_kmer=False)
+    assert_allclose(got, arr)