Repository: cogent3/cogent3
Pull request #1885 (2024-06-04 04:26:45)

API: added refactor of genetic code


Patch:
diff --git a/src/cogent3/core/new_alphabet.py b/src/cogent3/core/new_alphabet.py
index 839ad6ea0..bab715e57 100644
--- a/src/cogent3/core/new_alphabet.py
+++ b/src/cogent3/core/new_alphabet.py
@@ -167,9 +167,9 @@ class bytes_to_array:
 
 
 class array_to_bytes:
-    """converts utf8 to numpy array"""
+    """converts numpy array to utf8"""
 
-    def __init__(self, chars, dtype):
+    def __init__(self, chars: bytes, dtype):
         # we want a bytes translation map
         self._table = b"".maketrans(
             bytes(bytearray(range(len(chars)))),
@@ -284,9 +284,9 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
 
 
 @numba.jit(nopython=True)
-def coord_conversion_coeffs(num_states, k):  # pragma: no cover
+def coord_conversion_coeffs(num_states, k, dtype=None):  # 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)])
+    return numpy.array([num_states ** (i - 1) for i in range(k, 0, -1)], dtype=dtype)
 
 
 @numba.jit(nopython=True)
@@ -296,10 +296,10 @@ def coord_to_index(coord, coeffs):  # pragma: no cover
 
 
 @numba.jit(nopython=True)
-def index_to_coord(index, coeffs):  # pragma: no cover
+def index_to_coord(index: int, coeffs: numpy.ndarray):  # pragma: no cover
     """converts a 1D index into a multi-dimensional coordinate"""
     ndim = len(coeffs)
-    coord = numpy.zeros(ndim, dtype=numpy.uint64)
+    coord = numpy.zeros(ndim, dtype=coeffs.dtype)
     remainder = index
     for i in range(ndim):
         n, remainder = numpy.divmod(remainder, coeffs[i])
@@ -411,7 +411,7 @@ class KmerAlphabet(tuple, AlphabetABC):
         self.k = k
 
         size = len(self.monomers) - 1 if self.monomers.gap_char else len(self.monomers)
-        self._coeffs = coord_conversion_coeffs(size, k)
+        self._coeffs = coord_conversion_coeffs(size, k, dtype=self.dtype)
 
     @functools.singledispatchmethod
     def to_indices(self, seq, independent_kmer: bool = True) -> numpy.ndarray:
@@ -463,7 +463,7 @@ class KmerAlphabet(tuple, AlphabetABC):
     def with_gap_motif(self): ...
 
     @functools.singledispatchmethod
-    def pack(self, seq) -> numpy.ndarray:
+    def to_index(self, seq) -> numpy.ndarray:
         """encodes a k-mer as a single integer
 
         Parameters
@@ -475,7 +475,7 @@ class KmerAlphabet(tuple, AlphabetABC):
         """
         raise TypeError(f"{type(seq)} not supported")
 
-    @pack.register
+    @to_index.register
     def _(self, seq: str) -> numpy.ndarray:
         """encodes a k-mer as a single integer
 
@@ -487,9 +487,9 @@ class KmerAlphabet(tuple, AlphabetABC):
             if False, performs operation on sequential k-mers, e.g. codons
         """
         seq = self.monomers.to_indices(seq)
-        return self.pack(seq)
+        return self.to_index(seq)
 
-    @pack.register
+    @to_index.register
     def _(self, seq: bytes) -> numpy.ndarray:
         """encodes a k-mer as a single integer
 
@@ -501,9 +501,9 @@ class KmerAlphabet(tuple, AlphabetABC):
             if False, performs operation on sequential k-mers, e.g. codons
         """
         seq = self.monomers.to_indices(seq)
-        return self.pack(seq)
+        return self.to_index(seq)
 
-    @pack.register
+    @to_index.register
     def _(self, seq: numpy.ndarray) -> numpy.ndarray:
         """encodes a k-mer as a single integer
 
@@ -514,7 +514,7 @@ class KmerAlphabet(tuple, AlphabetABC):
         """
         return coord_to_index(seq, self._coeffs)
 
-    def unpack(self, kmer_index: int) -> 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)
 
diff --git a/src/cogent3/core/new_genetic_code.py b/src/cogent3/core/new_genetic_code.py
new file mode 100644
index 000000000..6f9ec2804
--- /dev/null
+++ b/src/cogent3/core/new_genetic_code.py
@@ -0,0 +1,434 @@
+"""Translates RNA or DNA string to amino acid sequence.
+
+Notes
+ ----
+
+* is used to denote termination (as per NCBI standard).
+
+Although the genetic code objects convert DNA to RNA and vice
+versa, lists of codons that they produce will be provided in DNA format.
+"""
+
+import collections
+import contextlib
+import dataclasses
+import itertools
+import typing
+
+import numpy
+
+from cogent3.core import new_alphabet, new_moltype
+from cogent3.util.table import Table
+
+
+OptStr = typing.Optional[str]
+SetStr = typing.Set[str]
+ConverterType = typing.Callable[[bytes, bytes], bytes]
+StrORInt = typing.Union[str, int]
+StrORBytesORArray = typing.Union[str, numpy.ndarray]
+
+
+class GeneticCodeError(Exception):
+    pass
+
+
+class GeneticCodeInitError(ValueError, GeneticCodeError):
+    pass
+
+
+class InvalidCodonError(KeyError, GeneticCodeError):
+    pass
+
+
+def _make_mappings(
+    codons: new_alphabet.KmerAlphabet, code_sequence: str
+) -> typing.Tuple[typing.Dict[str, str], typing.Dict[str, SetStr], SetStr]:
+    """makes amino acid / codon mappings and stop codon group
+
+    Parameters
+    ----------
+    codons
+        alphabet of codons
+    code_sequence
+        64-character string containing NCBI representation of the genetic code.
+
+    Returns
+    -------
+    codon to amino acid mapping, the reverse mapping, the set of stop codons
+    """
+    stops = set()
+    codon_to_aa = {}
+    aa_to_codon = collections.defaultdict(set)
+    for codon, aa in zip(codons, code_sequence):
+        if aa == "*":
+            stops.add(codon)
+        codon_to_aa[codon] = aa
+        aa_to_codon[aa].add(codon)
+    return codon_to_aa, aa_to_codon, stops
+
+
+def _get_start_codon_indices(start_codon_map: str) -> tuple[int, ...]:
+    return tuple(i for i, start in enumerate(start_codon_map) if start == "M")
+
+
+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
+    # 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
+    )
+    return new_alphabet.convert_alphabet(kmers.tobytes(), code_sequence.encode("utf8"))
+
+
+@dataclasses.dataclass
+class GeneticCode:
+    """Holds codon to amino acid mapping, and vice versa."""
+
+    ID: int
+    name: str
+    ncbi_code_sequence: dataclasses.InitVar[str]
+    ncbi_start_codon_map: dataclasses.InitVar[str]
+    moltype: new_moltype.MolType = new_moltype.DNA
+    _codon_to_aa: typing.Dict[str, str] = dataclasses.field(init=False, default=None)
+    _aa_to_codon: typing.Dict[str, typing.List[str]] = dataclasses.field(
+        init=False, default=None
+    )
+    _sense_codons: SetStr = dataclasses.field(init=False, default=None)
+    _stop_codons: SetStr = dataclasses.field(init=False, default=None)
+    _start_codons: SetStr = dataclasses.field(init=False, default=None)
+    codons: new_alphabet.KmerAlphabet = dataclasses.field(init=False, default=None)
+    anticodons: typing.Tuple[str, ...] = dataclasses.field(init=False, default=None)
+    # callables for translating on the plus strand, or the minus strand
+    _translate_plus: ConverterType = dataclasses.field(init=False, default=None)
+    _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)
+        self._codon_to_aa, self._aa_to_codon, self._stop_codons = _make_mappings(
+            trinuc_alpha, ncbi_code_sequence
+        )
+        self.codons = trinuc_alpha
+        self._start_codons = {
+            self.codons[i] for i in _get_start_codon_indices(ncbi_start_codon_map)
+        }
+        self._sense_codons = tuple(
+            c for c in self.codons if self._codon_to_aa[c] != "*"
+        )
+        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
+        )
+
+    @property
+    def stop_codons(self) -> SetStr:
+        return self._stop_codons
+
+    @property
+    def start_codons(self) -> SetStr:
+        return self._start_codons
+
+    @property
+    def sense_codons(self) -> SetStr:
+        return self._sense_codons
+
+    def to_table(self):
+        """returns aa to codon mapping as a cogent3 Table"""
+        rows = []
+        headers = ["aa", "IUPAC code", "codons"]
+        for code, aa in new_moltype.IUPAC_PROTEIN_code_aa.items():
+            codons = ",".join(self[code])
+            row = [aa, code, codons]
+            rows.append(row)
+        return Table(header=headers, data=rows, title=self.name)
+
+    def __repr__(self):
+        display = self.to_table()
+        return str(display)
+
+    def _repr_html_(self):
+        """Returns the html representation of GeneticCode."""
+        display = self.to_table()
+        display.set_repr_policy(show_shape=False)
+        return display._repr_html_()
+
+    def __eq__(self, other):
+        """Allows two GeneticCode objects to be compared to each other.
+        Two GeneticCode objects are equal if they have equal CodeSequences.
+        """
+        return str(self) == str(other)
+
+    def __getitem__(self, item):
+        """Returns amino acid corresponding to codon, or codons for an aa.
+
+        Returns [] for empty list of codons, 'X' for unknown amino acid.
+        """
+        item = str(item)
+        if len(item) == 1:  # amino acid
+            return self._aa_to_codon.get(item, set())
+
+        if len(item) != 3:
+            raise InvalidCodonError(f"Codon or aa {item} has wrong length")
+
+        key = item.upper()
+        key = key.replace("U", "T")
+        return self._codon_to_aa.get(key, "X")
+
+    def translate(
+        self, dna: StrORBytesORArray, start: int = 0, rc: bool = False
+    ) -> str:
+        """Translates DNA to protein with current GeneticCode.
+
+        Parameters
+        ----------
+        dna
+            a string of nucleotides
+        start
+            position to begin translation (used to implement frames)
+        rc
+            if True, returns the translation of the reverse complement sequence
+
+        Notes
+        -----
+        Sequences are truncated to be a multiple of 3.
+
+        Returns
+        -------
+        String containing amino acid sequence.
+        """
+        if start:
+            dna = dna[start:]
+
+        diff = len(dna) % 3
+        if diff != 0:
+            dna = dna[:-diff]
+
+        # convert to indices and then bytes
+        seq = self.codons.to_indices(dna).tobytes()
+
+        if rc:
+            return self._translate_minus(seq).decode("utf8")[::-1]
+
+        return self._translate_plus(seq).decode("utf8")
+
+    def sixframes(self, seq: str) -> typing.Iterable[typing.Tuple[str, int, str]]:
+        """Returns the six reading frames of the genetic code.
+
+        Returns
+        -------
+        A dictionary with keys (strand, start) where strand is "+"/"-"
+        """
+
+        for strand, start in itertools.product(("+", "-"), range(3)):
+            yield strand, start, self.translate(seq, start, rc=strand == "-")
+
+
+_mapping_cols = "ncbi_code_sequence", "ID", "name", "ncbi_start_codon_map"
+# code mappings are based on the product of bases in order TCAG
+code_mapping = (
+    (
+        "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        1,
+        "Standard",
+        "---M---------------M---------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSS**VVVVAAAADDEEGGGG",
+        2,
+        "Vertebrate Mitochondrial",
+        "--------------------------------MMMM---------------M------------",
+    ),
+    (
+        "FFLLSSSSYY**CCWWTTTTPPPPHHQQRRRRIIMMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        3,
+        "Yeast Mitochondrial",
+        "----------------------------------MM---------------M------------",
+    ),
+    (
+        "FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        4,
+        "Mold Mitochondrial; Protozoan Mitochondrial; Coelenterate "
+        "Mitochondrial; Mycoplasma; Spiroplasma",
+        "--MM---------------M------------MMMM---------------M------------",
+    ),
+    (
+        "FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSSSSVVVVAAAADDEEGGGG",
+        5,
+        "Invertebrate Mitochondrial",
+        "---M----------------------------MMMM---------------M------------",
+    ),
+    (
+        "FFLLSSSSYYQQCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        6,
+        "Ciliate Nuclear; Dasycladacean Nuclear; Hexamita Nuclear",
+        "-----------------------------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNNKSSSSVVVVAAAADDEEGGGG",
+        9,
+        "Echinoderm Mitochondrial; Flatworm Mitochondrial",
+        "-----------------------------------M---------------M------------",
+    ),
+    (
+        "FFLLSSSSYY**CCCWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        10,
+        "Euplotid Nuclear",
+        "-----------------------------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        11,
+        "Bacterial, Archaeal and Plant Plastid",
+        "---M---------------M------------MMMM---------------M------------",
+    ),
+    (
+        "FFLLSSSSYY**CC*WLLLSPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        12,
+        "Alternative Yeast Nuclear",
+        "-------------------M---------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSSGGVVVVAAAADDEEGGGG",
+        13,
+        "Ascidian Mitochondrial",
+        "---M------------------------------MM---------------M------------",
+    ),
+    (
+        "FFLLSSSSYYY*CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNNKSSSSVVVVAAAADDEEGGGG",
+        14,
+        "Alternative Flatworm Mitochondrial",
+        "-----------------------------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYY*QCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        15,
+        "Blepharisma Macronuclear",
+        "-----------------------------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYY*LCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        16,
+        "Chlorophycean Mitochondrial",
+        "-----------------------------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNNKSSSSVVVVAAAADDEEGGGG",
+        21,
+        "Trematode Mitochondrial",
+        "-----------------------------------M---------------M------------",
+    ),
+    (
+        "FFLLSS*SYY*LCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        22,
+        "Scenedesmus obliquus Mitochondrial",
+        "-----------------------------------M----------------------------",
+    ),
+    (
+        "FF*LSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        23,
+        "Thraustochytrium Mitochondrial",
+        "--------------------------------M--M---------------M------------",
+    ),
+    (
+        "FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSSKVVVVAAAADDEEGGGG",
+        24,
+        "Rhabdopleuridae Mitochondrial",
+        "---M---------------M---------------M---------------M------------",
+    ),
+    (
+        "FFLLSSSSYY**CCGWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        25,
+        "Candidate Division SR1 and Gracilibacteria",
+        "---M-------------------------------M---------------M------------",
+    ),
+    (
+        "FFLLSSSSYY**CC*WLLLAPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        26,
+        "Pachysolen tannophilus Nuclear",
+        "-------------------M---------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYYQQCCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        27,
+        "Karyorelict Nuclear",
+        "-----------------------------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYYQQCCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        28,
+        "Condylostoma Nuclear",
+        "-----------------------------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYYYYCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        29,
+        "Mesodinium Nuclear",
+        "-----------------------------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYYEECC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        30,
+        "Peritrich Nuclear",
+        "-----------------------------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYYEECCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        31,
+        "Blastocrithidia Nuclear",
+        "-----------------------------------M----------------------------",
+    ),
+    (
+        "FFLLSSSSYY*WCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG",
+        32,
+        "Balanophoraceae Plastid",
+        "---M---------------M------------MMMM---------------M------------",
+    ),
+    (
+        "FFLLSSSSYYY*CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSSKVVVVAAAADDEEGGGG",
+        33,
+        "Cephalodiscidae Mitochondrial",
+        "---M---------------M---------------M---------------M------------",
+    ),
+)
+_CODES = {}
+for mapping in code_mapping:
+    code = GeneticCode(**dict(zip(_mapping_cols, mapping)))
+    _CODES[code.ID] = code
+    _CODES[code.name] = code
+
+
+DEFAULT = _CODES[1]
+
+
+def get_code(code_id: StrORInt = 1) -> GeneticCode:
+    """returns the genetic code
+
+    Parameters
+    ----------
+    code_id
+        genetic code identifier, name, number or string(number), defaults to
+        standard genetic code
+    """
+    with contextlib.suppress(ValueError):
+        code_id = int(code_id)
+
+    if code_id not in _CODES:
+        raise GeneticCodeError(f"Unknown genetic code {code_id}")
+    return _CODES[code_id]
+
+
+def available_codes():
+    """returns Table listing the available genetic codes"""
+    rows = [(k, code.name) for k, code in _CODES.items() if isinstance(k, int)]
+    header = ["Code ID", "Name"]
+    return Table(
+        header=header,
+        data=rows,
+        index_name="Code ID",
+        title="Specify a genetic code using either 'Name' or "
+        "Code ID (as an integer or string)",
+    )
diff --git a/src/cogent3/core/new_moltype.py b/src/cogent3/core/new_moltype.py
index 215627749..d3f781a9d 100644
--- a/src/cogent3/core/new_moltype.py
+++ b/src/cogent3/core/new_moltype.py
@@ -345,7 +345,7 @@ class MolType:
         self,
         monomers: StrORBytes,
         make_seq: typing.Type,
-        complements: dataclasses.InitVar[typing.Optional[dict[str, str]]],
+        complements: typing.Optional[dict[str, str]],
         colors: typing.Optional[dict[str, str]],
     ):
         self._colors = colors or defaultdict(_DefaultValue("black"))
@@ -476,6 +476,10 @@ class MolType:
     def _(self, seq: bytes) -> str:
         return self._complement(seq).decode("utf8")
 
+    def rc(self, seq: str) -> str:
+        """reverse reverse complement of a sequence"""
+        return self.complement(seq)[::-1]
+
 
 def _make_moltype_dict() -> dict[str, MolType]:
     """make a dictionary of local name space molecular types"""


Test patch:
diff --git a/tests/test_core/test_new_alphabet.py b/tests/test_core/test_new_alphabet.py
index 5d8c5071e..9d0818bc2 100644
--- a/tests/test_core/test_new_alphabet.py
+++ b/tests/test_core/test_new_alphabet.py
@@ -182,33 +182,33 @@ def get_rand_coord_seq(k, cast):
 
 @pytest.mark.parametrize("cast", (None, bytes, str))
 @pytest.mark.parametrize("k", (2, 3))
-def test_kmer_alphabet_pack(k, cast):
+def test_kmer_alphabet_to_index(k, cast):
     # should match numpy ravel
     dna = new_moltype.get_moltype("dna")
     monomers = dna.gapped_alphabet
     kmers = monomers.get_kmer_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
+    assert kmers.to_index(seq) == expect
 
 
-def test_kmer_alphabet_pack_invalid():
+def test_kmer_alphabet_to_index_invalid():
     # should match numpy ravel
     dna = new_moltype.get_moltype("dna")
     kmers = dna.gapped_alphabet.get_kmer_alphabet(3, include_gap=True)
     with pytest.raises(TypeError):
-        kmers.pack([0, 1, 2])
+        kmers.to_index([0, 1, 2])
 
 
 @pytest.mark.parametrize("k", (2, 3))
-def test_kmer_alphabet_unpack(k):
+def test_kmer_alphabet_from_index(k):
     # should match numpy ravel
     dna = new_moltype.get_moltype("dna")
     monomers = dna.gapped_alphabet
     kmers = monomers.get_kmer_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()
+    assert (kmers.from_index(index) == coord).all()
 
 
 def test_seq_to_kmer_indices():
diff --git a/tests/test_core/test_new_genetic_code.py b/tests/test_core/test_new_genetic_code.py
new file mode 100644
index 000000000..dae0a42d4
--- /dev/null
+++ b/tests/test_core/test_new_genetic_code.py
@@ -0,0 +1,74 @@
+from cogent3.core import new_genetic_code
+
+
+def test_get_code():
+    code = new_genetic_code.get_code(1)
+    assert code.name == "Standard"
+
+
+def test_get_aa():
+    code = new_genetic_code.DEFAULT
+    assert code["ATG"] == "M"
+    assert code["TAA"] == "*"
+
+
+def test_stop_codons():
+    code = new_genetic_code.DEFAULT
+    assert code.stop_codons == {"TAA", "TAG", "TGA"}
+
+
+def test_start_codons():
+    code = new_genetic_code.DEFAULT
+    dna = code.moltype.alphabet
+    trinucs = code.codons
+    # the starts are in the ncbi_start_codon_map
+    expect = {
+        dna.from_indices(trinucs.from_index(i))
+        for i, e in enumerate(new_genetic_code.code_mapping[0][3])
+        if e == "M"
+    }
+    assert code.start_codons == expect
+
+
+def test_sense_codons():
+    code = new_genetic_code.DEFAULT
+    assert len(code.sense_codons) == 61
+
+
+def test_translate():
+    code = new_genetic_code.DEFAULT
+    assert code.translate("ATGGGG") == code["ATG"] + code["GGG"]
+
+
+def test_translate_rc():
+    code = new_genetic_code.DEFAULT
+    seq = "ATGGGG"
+    rc = code.moltype.rc(seq)
+    assert code.translate(rc, rc=True) == code["ATG"] + code["GGG"]
+
+
+def test_to_table():
+    code = new_genetic_code.DEFAULT
+    table = code.to_table()
+    assert table.shape[0] == 21
+
+
+def test_available_codes():
+    codes = new_genetic_code.available_codes()
+    assert len(codes) == len(new_genetic_code.code_mapping)
+
+
+def test_sizeframes():
+    code = new_genetic_code.DEFAULT
+    seq = "ATGGGGTAACAT"
+    # following also validated against old and new genetic codes
+    expect = {
+        ("+", 0, "MG*H"),
+        ("+", 1, "WGN"),
+        ("+", 2, "GVT"),
+        ("-", 0, "MLPH"),
+        ("-", 1, "VTP"),
+        ("-", 2, "CYP"),
+    }
+    got = set(code.sixframes(seq))
+    assert got == expect