Repository: cogent3/cogent3
Pull request #2159 (2024-12-06 01:32:23)

MAINT: collection of patches
## Summary by Sourcery

Enhance the to_indices method to support tuple inputs across various sequence types, improving input flexibility. Add corresponding test cases to ensure correct functionality. Refactor the make_matches function for cleaner code.

Enhancements:
- Add support for tuple input in the to_indices method for various sequence types, allowing more flexible input handling.

Tests:
- Add new test cases for kmer and sensecodon alphabet to_indices method to handle list and tuple inputs.

Chores:
- Refactor the make_matches function to simplify the initialization of the result dictionary.


Patch:
diff --git a/src/cogent3/core/new_alphabet.py b/src/cogent3/core/new_alphabet.py
index 64061fe05..5a542671a 100644
--- a/src/cogent3/core/new_alphabet.py
+++ b/src/cogent3/core/new_alphabet.py
@@ -332,9 +332,16 @@ class CharAlphabet(tuple, AlphabetABC, MonomerAlphabetABC):
         return self.motif_len
 
     @functools.singledispatchmethod
-    def to_indices(self, seq: StrORBytesORArray) -> numpy.ndarray[int]:
+    def to_indices(self, seq: StrORBytesORArray | tuple) -> numpy.ndarray[int]:
         raise TypeError(f"{type(seq)} is invalid")
 
+    @to_indices.register
+    def _(self, seq: tuple) -> numpy.ndarray[int]:
+        indices = []
+        for c in seq:
+            indices.extend(self.to_indices(c).tolist())
+        return numpy.array(indices, dtype=self.dtype)
+
     @to_indices.register
     def _(self, seq: bytes) -> numpy.ndarray[int]:
         # any non-canonical characters should lie outside the range
@@ -743,6 +750,14 @@ class KmerAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
         # TODO: handle case of non-modulo sequences
         raise TypeError(f"{type(seq)} is invalid")
 
+    @to_indices.register
+    def _(self, seq: tuple, **kwargs) -> numpy.ndarray[int]:
+        return numpy.array([self.to_index(c) for c in seq], dtype=self.dtype)
+
+    @to_indices.register
+    def _(self, seq: list, **kwargs) -> numpy.ndarray:
+        return numpy.array([self.to_index(c) for c in seq], dtype=self.dtype)
+
     @to_indices.register
     def _(self, seq: str, independent_kmer: bool = True) -> numpy.ndarray:
         seq = self.monomers.to_indices(seq)
@@ -908,6 +923,10 @@ class KmerAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
             missing=data["missing"],
         )
 
+    @property
+    def motif_len(self) -> int:
+        return self.k
+
 
 @register_deserialiser(get_object_provenance(KmerAlphabet))
 def deserialise_kmer_alphabet(data: dict) -> KmerAlphabet:
@@ -955,7 +974,7 @@ class SenseCodonAlphabet(tuple, AlphabetABC):
         self._words = set(self)  # for quick lookup
         self._to_indices = {codon: i for i, codon in enumerate(self)}
         self._from_indices = {i: codon for codon, i in self._to_indices.items()}
-        self.motif_length = 3
+        self._motif_len = 3
         if monomers.moltype:
             _alphabet_moltype_map[self] = monomers.moltype
 
@@ -990,6 +1009,10 @@ class SenseCodonAlphabet(tuple, AlphabetABC):
     def _(self, seq: list) -> numpy.ndarray:
         return numpy.array([self.to_index(c) for c in seq], dtype=self.dtype)
 
+    @to_indices.register
+    def _(self, seq: tuple) -> numpy.ndarray[int]:
+        return self.to_indices(list(seq))
+
     @to_indices.register
     def _(self, seq: numpy.ndarray) -> numpy.ndarray:
         # we assume that this is a dna sequence encoded as a numpy array
@@ -1067,6 +1090,10 @@ class SenseCodonAlphabet(tuple, AlphabetABC):
         data.pop("version", None)
         return cls(**data)
 
+    @property
+    def motif_len(self):
+        return self._motif_len
+
 
 @register_deserialiser(get_object_provenance(SenseCodonAlphabet))
 def deserialise_codon_alphabet(data: dict) -> SenseCodonAlphabet:
diff --git a/src/cogent3/core/new_moltype.py b/src/cogent3/core/new_moltype.py
index d477161f6..81627ca42 100644
--- a/src/cogent3/core/new_moltype.py
+++ b/src/cogent3/core/new_moltype.py
@@ -10,6 +10,9 @@ import numpy
 from cogent3.core import new_alphabet, new_sequence
 from cogent3.data.molecular_weight import DnaMW, ProteinMW, RnaMW, WeightCalculator
 
+if typing.TYPE_CHECKING:
+    from cogent3.core.sequence import SeqViewABC
+
 OptStr = typing.Optional[str]
 OptFloat = typing.Optional[float]
 OptCallable = typing.Optional[typing.Callable]
@@ -198,12 +201,7 @@ def make_matches(
     monomers = monomers or {}
     gaps = gaps or {}
     degenerates = degenerates or {}
-    result = {}
-
-    # all monomers always match themselves and no other monomers
-    for i in monomers:
-        result[(i, i)] = True
-
+    result = {(i, i): True for i in monomers}
     # all gaps always match all other gaps
     for i, j in itertools.combinations_with_replacement(gaps, 2):
         result[(i, j)], result[(j, i)] = True, True
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index 66cb4d26e..07d1d6397 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -139,7 +139,7 @@ class Sequence:
 
     def __init__(
         self,
-        moltype: MolType,
+        moltype: new_moltype.MolType,
         seq: StrORBytesORArray | SeqViewABC,
         *,
         name: OptStr = None,
diff --git a/src/cogent3/evolve/predicate.py b/src/cogent3/evolve/predicate.py
index ef4f0e7a3..5889ea191 100644
--- a/src/cogent3/evolve/predicate.py
+++ b/src/cogent3/evolve/predicate.py
@@ -229,10 +229,10 @@ class DirectedMotifChange(predicate):
                 % (alphabet.motif_len, repr(self), self.motiflen),
             )
 
-        resolve = model.moltype.ambiguities.__getitem__
+        ambigs = model.moltype.ambiguities
 
-        from_motifs = [resolve(m) for m in self.from_motif]
-        to_motifs = [resolve(m) for m in self.to_motif]
+        from_motifs = [ambigs[m] for m in self.from_motif]
+        to_motifs = [ambigs[m] for m in self.to_motif]
 
         def call(x, y):
             diffs = [X != Y for (X, Y) in zip(x, y, strict=False)]
diff --git a/src/cogent3/format/clustal.py b/src/cogent3/format/clustal.py
index 7682aa784..7212cf6e6 100644
--- a/src/cogent3/format/clustal.py
+++ b/src/cogent3/format/clustal.py
@@ -41,7 +41,7 @@ def clustal_from_alignment(aln, wrap=None):
             + "Cannot generate Clustal format.",
         )
 
-    aln_len = seqs.seq_len
+    aln_len = len(seqs.seqs[0])
     # Get all labels
     labels = copy(seqs.names)
 


Test patch:
diff --git a/tests/test_core/test_new_alphabet.py b/tests/test_core/test_new_alphabet.py
index a3547642e..61f4d6350 100644
--- a/tests/test_core/test_new_alphabet.py
+++ b/tests/test_core/test_new_alphabet.py
@@ -197,6 +197,7 @@ def test_kmer_alphabet_construction(k):
     monomers = dna.alphabet
     kmers = monomers.get_kmer_alphabet(k)
     assert len(kmers) == 4**k
+    assert kmers.motif_len == k
 
 
 @pytest.mark.parametrize("gap_index", (-1, 4))
@@ -402,11 +403,24 @@ def test_kmer_alphabet_to_indices_non_independent():
     assert_allclose(got, expect)
 
 
+@pytest.mark.parametrize("cast", (list, tuple))
+def test_kmer_alphabet_to_indices_list_tuple(cast):
+    seq = cast(["ACG", "TCC"])
+    dna = new_moltype.get_moltype("dna")
+    trinuc_alpha = dna.alphabet.get_kmer_alphabet(k=3, include_gap=False)
+    got = trinuc_alpha.to_indices(seq)
+    expect = numpy.array(
+        [trinuc_alpha.to_index(kmer) for kmer in seq],
+        dtype=trinuc_alpha.dtype,
+    )
+    assert_allclose(got, expect)
+
+
 def test_kmer_alphabet_to_indices_invalid():
     dna = new_moltype.get_moltype("dna")
     trinuc_alpha = dna.alphabet.get_kmer_alphabet(k=3, include_gap=False)
     with pytest.raises(TypeError):
-        trinuc_alpha.to_indices(["ACG", "TGG"])
+        trinuc_alpha.to_indices({"ACG", "TGG"})
 
 
 @pytest.mark.parametrize("seq", ("ACGT", "ACYG", "NN"))
@@ -571,6 +585,7 @@ def calpha():
 
 def test_codon_alphabet_init(calpha):
     assert len(calpha) == 61
+    assert calpha.motif_len == 3
 
 
 def test_codon_alphabet_index(calpha):
@@ -669,7 +684,12 @@ def test_codon_alphabet_moltype(calpha):
 
 @pytest.mark.parametrize(
     "seq",
-    ("GGTAC", b"GGTAC", numpy.array([3, 3, 0, 2, 1], dtype=numpy.uint8)),
+    (
+        "GGTAC",
+        b"GGTAC",
+        numpy.array([3, 3, 0, 2, 1], dtype=numpy.uint8),
+        tuple("GGTAC"),
+    ),
 )
 def test_char_alphabet_to_indices_types(seq):
     dna = new_moltype.get_moltype("dna")
@@ -689,3 +709,14 @@ def test_char_alphabet_from_indices_types(seq):
     got = alpha.from_indices(seq)
     expect = "GGTAC"
     assert got == expect
+
+
+@pytest.mark.parametrize("cast", (list, tuple))
+def test_sensecodon_alphabet_to_indices_list_tuple(cast, calpha):
+    seq = cast(["ACG", "TCC"])
+    got = calpha.to_indices(seq)
+    expect = numpy.array(
+        [calpha.to_index(kmer) for kmer in seq],
+        dtype=calpha.dtype,
+    )
+    assert_allclose(got, expect)
diff --git a/tests/test_evolve/test_newq.py b/tests/test_evolve/test_newq.py
index 0307835ba..95f0bb6ed 100644
--- a/tests/test_evolve/test_newq.py
+++ b/tests/test_evolve/test_newq.py
@@ -4,13 +4,7 @@ from unittest import TestCase
 from numpy import dot, ones
 from numpy.testing import assert_allclose
 
-from cogent3 import (
-    DNA,
-    load_aligned_seqs,
-    load_tree,
-    make_aligned_seqs,
-    make_tree,
-)
+import cogent3
 from cogent3.evolve.ns_substitution_model import (
     NonReversibleCodon,
     NonReversibleNucleotide,
@@ -25,6 +19,8 @@ from cogent3.maths.matrix_exponentiation import PadeExponentiator as expm
 warnings.filterwarnings("ignore", "Motif probs overspecified")
 warnings.filterwarnings("ignore", "Model not reversible")
 
+DNA = cogent3.get_moltype("dna")
+
 
 def _dinuc_root_probs(x, y=None):
     if y is None:
@@ -65,14 +61,14 @@ def make_p(length, coord, val):
 
 
 class NewQ(TestCase):
-    aln = make_aligned_seqs(
+    aln = cogent3.make_aligned_seqs(
         data={
             "seq1": "TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT",
             "seq2": "TGTGGCACAAATACTCATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTCACT",
         },
         moltype=DNA,
     )
-    tree = make_tree(tip_names=["seq1", "seq2"])
+    tree = cogent3.make_tree(tip_names=["seq1", "seq2"])
 
     symm_nuc_probs = dict(A=0.25, T=0.25, C=0.25, G=0.25)
     symm_root_probs = _dinuc_root_probs(symm_nuc_probs)
@@ -216,8 +212,8 @@ class NewQ(TestCase):
             posn2.append([name, "".join(p2)])
 
         # the position specific alignments
-        posn1 = make_aligned_seqs(data=posn1)
-        posn2 = make_aligned_seqs(data=posn2)
+        posn1 = cogent3.make_aligned_seqs(data=posn1, moltype=DNA)
+        posn2 = cogent3.make_aligned_seqs(data=posn2, moltype=DNA)
 
         # a newQ dinucleotide model
         sm = TimeReversibleNucleotide(motif_length=2, mprob_model="monomer")
@@ -325,9 +321,10 @@ class NewQ(TestCase):
 
     def test_getting_node_mprobs(self):
         """return correct motif probability vector for tree nodes"""
-        tree = make_tree(treestring="(a:.2,b:.2,(c:.1,d:.1):.1)")
-        aln = make_aligned_seqs(
+        tree = cogent3.make_tree(treestring="(a:.2,b:.2,(c:.1,d:.1):.1)")
+        aln = cogent3.make_aligned_seqs(
             data={"a": "TGTG", "b": "TGTG", "c": "TGTG", "d": "TGTG"},
+            moltype="dna",
         )
 
         motifs = ["T", "C", "A", "G"]
@@ -376,10 +373,10 @@ class NewQ(TestCase):
         """handles different statespace dimensions from process and stationary distribution"""
         from cogent3.evolve.models import get_model
 
-        aln = load_aligned_seqs("data/primates_brca1.fasta", moltype="dna")
+        aln = cogent3.load_aligned_seqs("data/primates_brca1.fasta", moltype="dna")
         aln = aln.no_degenerates(motif_length=3)
 
-        tree = load_tree("data/primates_brca1.tree")
+        tree = cogent3.load_tree("data/primates_brca1.tree")
 
         # root mprobs are constant
         sm = get_model("MG94HKY")
diff --git a/tests/test_format/test_clustal.py b/tests/test_format/test_clustal.py
index a90f0b39d..e41c26705 100644
--- a/tests/test_format/test_clustal.py
+++ b/tests/test_format/test_clustal.py
@@ -2,7 +2,7 @@
 
 from unittest import TestCase
 
-from cogent3.core.alignment import Alignment
+import cogent3
 from cogent3.format.clustal import clustal_from_alignment
 
 
@@ -24,7 +24,10 @@ class ClustalTests(TestCase):
             "4th": "UUUU",
         }
         # create alignment change order.
-        self.alignment_object = Alignment(self.alignment_dict)
+        self.alignment_object = cogent3.make_aligned_seqs(
+            self.alignment_dict,
+            moltype="text",
+        )
         self.alignment_order = ["2nd", "4th", "3rd", "1st"]
         self.alignment_object.RowOrder = self.alignment_order
 
diff --git a/tests/test_format/test_fasta.py b/tests/test_format/test_fasta.py
index 89fcd6b17..80c0790f5 100644
--- a/tests/test_format/test_fasta.py
+++ b/tests/test_format/test_fasta.py
@@ -2,9 +2,8 @@
 
 from unittest import TestCase
 
-from cogent3.core.alignment import Alignment
+import cogent3
 from cogent3.core.info import Info
-from cogent3.core.sequence import Sequence
 from cogent3.format.fasta import seqs_to_fasta
 
 
@@ -16,15 +15,15 @@ class FastaTests(TestCase):
         self.strings = ["AAAA", "CCCC", "gggg", "uuuu"]
         self.labels = ["1st", "2nd", "3rd", "4th"]
         self.infos = ["Dog", "Cat", "Mouse", "Rat"]
-        self.sequences_with_labels = list(map(Sequence, self.strings))
-        self.sequences_with_names = list(map(Sequence, self.strings))
+        ASCII = cogent3.get_moltype("text")
+        self.sequences_with_labels = [ASCII.make_seq(seq=seq) for seq in self.strings]
+        self.sequences_with_names = [ASCII.make_seq(seq=seq) for seq in self.strings]
         for l, sl, sn in zip(
             self.labels,
             self.sequences_with_labels,
             self.sequences_with_names,
             strict=False,
         ):
-            sl.label = l
             sn.name = l
         self.fasta_no_label = ">0\nAAAA\n>1\nCCCC\n>2\ngggg\n>3\nuuuu\n"
         self.fasta_with_label = ">1st\nAAAA\n>2nd\nCCCC\n>3rd\nGGGG\n>4th\nUUUU\n"
@@ -37,7 +36,10 @@ class FastaTests(TestCase):
             "3rd": "GGGG",
             "4th": "UUUU",
         }
-        self.alignment_object = Alignment(self.alignment_dict)
+        self.alignment_object = cogent3.make_aligned_seqs(
+            self.alignment_dict,
+            moltype="text",
+        )
         for label, info in zip(self.labels, self.infos, strict=False):
             self.alignment_object.named_seqs[label].info = Info(species=info)
         self.fasta_with_label_species = (