Repository: cogent3/cogent3
Pull request #2256 (2025-03-20 01:58:36)

More work on migration to new_type default
## Summary by Sourcery

Refactors the NotCompleted object instantiation to use keyword arguments for better readability and consistency. Updates examples to use moltype="dna" when creating aligned sequences. Modifies the get_gap_array method in the new_alignment module to improve performance by directly comparing the array sequences to the gap index.

Enhancements:
- Refactor NotCompleted object instantiation to use keyword arguments.
- Improve performance of get_gap_array method by directly comparing array sequences to the gap index.


Patch:
diff --git a/src/cogent3/app/evo.py b/src/cogent3/app/evo.py
index ea54ad7c9..4cb96cb23 100644
--- a/src/cogent3/app/evo.py
+++ b/src/cogent3/app/evo.py
@@ -157,7 +157,8 @@ class model:
         ...         "Human": "ATGCGGCTCGCGGAGGCCGCGCTCGCGGAG",
         ...         "Mouse": "ATGCCCGGCGCCAAGGCAGCGCTGGCGGAG",
         ...         "Opossum": "ATGCCAGTGAAAGTGGCGGCGGTGGCTGAG",
-        ...     }
+        ...     },
+        ...     moltype="dna",
         ... )
         >>> app = get_app(
         ...     "model", "F81", opt_args=dict(limit_action="ignore", max_evaluations=10)
@@ -180,7 +181,8 @@ class model:
         ...         "Gorilla": "ATGCGGCGCGCGGAGGCCGCGCTCGCGGAG",
         ...         "Mouse": "ATGCCCGGCGCCAAGGCAGCGCTGGCGGAG",
         ...         "Opossum": "ATGCCAGTGAAAGTGGCGGCGGTGGCTGAG",
-        ...     }
+        ...     },
+        ...     moltype="dna",
         ... )
         >>> app_tr = get_app("model", "F81", tree=tree)
 
diff --git a/src/cogent3/app/sample.py b/src/cogent3/app/sample.py
index 1539efb46..f67f56bc5 100644
--- a/src/cogent3/app/sample.py
+++ b/src/cogent3/app/sample.py
@@ -216,15 +216,6 @@ class omit_degenerates:
         >>> print(result.to_pretty())
         s1    ACGA
         s2    GATG
-
-        A NotCompleted object (see https://cogent3.org/doc/app/not-completed.html)
-        is returned if the moltype is not specified in the alignment or app
-
-        >>> aln = make_aligned_seqs({"s1": "ACGA-GACG", "s2": "GATGATGYT"})
-        >>> app = get_app("omit_degenerates")
-        >>> result = app(aln)
-        >>> result.message
-        'Traceback...
         """
         if moltype:
             moltype = cogent3.get_moltype(moltype)
@@ -244,7 +235,12 @@ class omit_degenerates:
         return aln.no_degenerates(
             motif_length=self._motif_length,
             allow_gap=self._allow_gap,
-        ) or NotCompleted("FAIL", self, "all columns contained degenerates", source=aln)
+        ) or NotCompleted(
+            "FAIL",
+            origin=self,
+            message="all columns contained degenerates",
+            source=aln,
+        )
 
 
 @define_app
@@ -276,7 +272,9 @@ class omit_gap_pos:
         Sites with over 99% gaps are excluded by default.
 
         >>> from cogent3 import make_aligned_seqs, get_app
-        >>> aln = make_aligned_seqs({"s1": "ACGA-GA-CG", "s2": "GATGATG-AT"})
+        >>> aln = make_aligned_seqs(
+        ...     {"s1": "ACGA-GA-CG", "s2": "GATGATG-AT"}, moltype="dna"
+        ... )
 
         >>> app = get_app("omit_gap_pos", moltype="dna")
         >>> result = app(aln)
@@ -305,7 +303,9 @@ class omit_gap_pos:
         A NotCompleted object (see https://cogent3.org/doc/app/not-completed.html)
         is returned if all sites are excluded.
 
-        >>> aln = make_aligned_seqs({"s1": "ACGA------", "s2": "----ATG-AT"})
+        >>> aln = make_aligned_seqs(
+        ...     {"s1": "ACGA------", "s2": "----ATG-AT"}, moltype="dna"
+        ... )
         >>> app = get_app("omit_gap_pos", allowed_frac=0, motif_length=3, moltype="dna")
         >>> result = app(aln)
         >>> result.message
@@ -330,9 +330,9 @@ class omit_gap_pos:
             allowed_gap_frac=self._allowed_frac,
             motif_length=self._motif_length,
         ) or NotCompleted(
-            "FAIL",
-            self,
-            "all columns exceeded gap threshold",
+            type="FAIL",
+            origin=self,
+            message="all columns exceeded gap threshold",
             source=aln,
         )
 
@@ -371,7 +371,9 @@ class take_codon_positions:
         position from an alignment.
 
         >>> from cogent3 import make_aligned_seqs, get_app
-        >>> aln = make_aligned_seqs({"s1": "ACGACGACG", "s2": "GATGATGAT"})
+        >>> aln = make_aligned_seqs(
+        ...     {"s1": "ACGACGACG", "s2": "GATGATGAT"}, moltype="dna"
+        ... )
         >>> take_pos3 = get_app("take_codon_positions", 3, moltype="dna")
         >>> result = take_pos3(aln)
         >>> print(result.to_pretty())
@@ -390,7 +392,9 @@ class take_codon_positions:
         Create a sample alignment and an app that returns the 3rd codon
         positions from four-fold degenerate codons.
 
-        >>> aln_ff = make_aligned_seqs({"s1": "GCAAGCGTTTAT", "s2": "GCTTTTGTCAAT"})
+        >>> aln_ff = make_aligned_seqs(
+        ...     {"s1": "GCAAGCGTTTAT", "s2": "GCTTTTGTCAAT"}, moltype="dna"
+        ... )
         >>> take_fourfold = get_app(
         ...     "take_codon_positions", fourfold_degenerate=True, moltype="dna"
         ... )
@@ -404,7 +408,7 @@ class take_codon_positions:
 
         >>> result = take_fourfold(aln)
         >>> result.message
-        'Traceback ...
+        'result is empty'
         """
         assert moltype is not None
         moltype = cogent3.get_moltype(moltype)
@@ -441,7 +445,8 @@ class take_codon_positions:
         self._positions = positions
 
     def take_fourfold_positions(self, aln):
-        if self._moltype and self._moltype != aln.moltype:
+        if self._moltype and self._moltype.label != aln.moltype.label:
+            # checking label will work across types
             aln = aln.to_moltype(self._moltype)
 
         fourfold_codon_sets = self._fourfold_degen_sets
@@ -451,6 +456,13 @@ class take_codon_positions:
             return any(x <= codon_set for codon_set in fourfold_codon_sets)
 
         new = aln.filtered(ffold, motif_length=3)
+        if not new:
+            return NotCompleted(
+                type="FAIL",
+                origin=self,
+                message="result is empty",
+                source=aln,
+            )
         return new[2::3]
 
     def take_codon_position(self, aln):
@@ -494,7 +506,8 @@ class take_named_seqs:
 
         >>> from cogent3 import make_aligned_seqs, get_app
         >>> aln = make_aligned_seqs(
-        ...     {"s1": "GCAAGC", "s2": "GCTTTT", "s3": "GC--GC", "s4": "GCAAGC"}
+        ...     {"s1": "GCAAGC", "s2": "GCTTTT", "s3": "GC--GC", "s4": "GCAAGC"},
+        ...     moltype="dna",
         ... )
         >>> app = get_app("take_named_seqs", "s1", "s2")
         >>> result = app(aln)
@@ -563,7 +576,8 @@ class take_n_seqs:
 
         >>> from cogent3 import make_aligned_seqs, get_app
         >>> aln = make_aligned_seqs(
-        ...     {"s1": "ACGT", "s2": "ACG-", "s3": "ACGN", "s4": "ACGG", "s5": "ACGG"}
+        ...     {"s1": "ACGT", "s2": "ACG-", "s3": "ACGN", "s4": "ACGG", "s5": "ACGG"},
+        ...     moltype="dna",
         ... )
         >>> app_first_n = get_app("take_n_seqs", number=3)
         >>> result = app_first_n(aln)
@@ -593,7 +607,8 @@ class take_n_seqs:
         ...         "s3": "GCG-",
         ...         "s4": "GCGG",
         ...         "s5": "GCGG",
-        ...     }
+        ...     },
+        ...     moltype="dna",
         ... )
         >>> app_fixed = get_app("take_n_seqs", number=3, random=True, fixed_choice=True)
         >>> result1 = app_fixed(aln).names
@@ -687,14 +702,6 @@ class min_length:
         >>> result = app(aln)
         >>> result.message
         '4 < min_length 7'
-
-        If the moltype of the alignment is not specified, returns a NotCompleted
-
-        >>> aln = make_aligned_seqs({"s1": "ACGGT", "s2": "AC-GT"})
-        >>> app = get_app("min_length", 3)
-        >>> result = app(aln)
-        >>> result.message
-        'Traceback...
         """
         if motif_length > 1:
             length //= motif_length
@@ -790,7 +797,9 @@ class fixed_length:
         of the alignment.
 
         >>> from cogent3 import make_aligned_seqs, get_app
-        >>> aln = make_aligned_seqs({"s1": "GCAAGCGTTTAT", "s2": "GCTTTTGTCAAT"})
+        >>> aln = make_aligned_seqs(
+        ...     {"s1": "GCAAGCGTTTAT", "s2": "GCTTTTGTCAAT"}, moltype="dna"
+        ... )
         >>> app_4 = get_app("fixed_length", length=4)
         >>> result = app_4(aln)
         >>> print(result.to_pretty())
@@ -819,7 +828,9 @@ class fixed_length:
         Sequences are split into non-overlapping sections of ``motif_length=3``
         before sampling (i.e. codon positions are preserved).
 
-        >>> aln = make_aligned_seqs({"s1": "GCAAGCGTTTAT", "s2": "GCTTTTGTCAAT"})
+        >>> aln = make_aligned_seqs(
+        ...     {"s1": "GCAAGCGTTTAT", "s2": "GCTTTTGTCAAT"}, moltype="dna"
+        ... )
         >>> app_motif3 = get_app(
         ...     "fixed_length", length=6, motif_length=3, random=True, seed=9
         ... )
@@ -1059,7 +1070,7 @@ class omit_duplicated:
         number of gaps and ambiguous characters. In this case, only one of 'c' and
         'd' will be retained.
 
-        >>> seqs = make_aligned_seqs(data, moltype="DNA")
+        >>> seqs = make_aligned_seqs(data, moltype="dna")
         >>> app = get_app("omit_duplicated", moltype="dna", choose="longest")
         >>> result = app(seqs)
         >>> print(result.to_pretty())
@@ -1087,10 +1098,10 @@ class omit_duplicated:
         ...         "s2": "ATYG",  # matches s1 with ambiguity
         ...         "s3": "GGTA",
         ...     },
-        ...     moltype="DNA",
+        ...     moltype="dna",
         ... )
         >>> app_dna = get_app(
-        ...     "omit_duplicated", mask_degen=True, choose="longest", moltype="DNA"
+        ...     "omit_duplicated", mask_degen=True, choose="longest", moltype="dna"
         ... )
         >>> result = app_dna(data)
         >>> print(result.to_pretty())
diff --git a/src/cogent3/app/translate.py b/src/cogent3/app/translate.py
index 07db92356..ea3552863 100644
--- a/src/cogent3/app/translate.py
+++ b/src/cogent3/app/translate.py
@@ -217,7 +217,8 @@ class select_translatable:
         ...     {
         ...         "s1": "AATATAAATGCCAGCTCATTACAGCATGAGAACAGCAGTTTATTACTTCATAAAGTCATA",
         ...         "s1_rc": "TATGACTTTATGAAGTAATAAACTGCTGTTCTCATGCTGTAATGAGCTGGCATTTATATT",
-        ...     }
+        ...     },
+        ...     moltype="dna",
         ... )
         >>> app = get_app("select_translatable")
         >>> result = app(aln)
@@ -239,7 +240,8 @@ class select_translatable:
         ...     {
         ...         "internal_stop": "AATTAAATGTGA",
         ...         "s2": "TATGACTAA",
-        ...     }
+        ...     },
+        ...     moltype="dna",
         ... )
         >>> app = get_app("select_translatable", frame=1)
         >>> result = app(aln)
@@ -348,7 +350,7 @@ class translate_seqs:
         with a stop codon are excluded with ``trim_terminal_stop=True``.
 
         >>> from cogent3 import make_aligned_seqs, get_app
-        >>> aln = make_aligned_seqs({"s1": "ATGAGG", "s2": "ATGTAA"})
+        >>> aln = make_aligned_seqs({"s1": "ATGAGG", "s2": "ATGTAA"}, moltype="dna")
         >>> app_translate = get_app("translate_seqs", trim_terminal_stop=True)
         >>> result = app_translate(aln)
         >>> print(result.to_pretty())
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index 99d7ae1b5..b5ebdb103 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -4597,17 +4597,11 @@ class Alignment(SequenceCollection):
             if True, ambiguity characters that include the gap state are
             included
         """
-        result = numpy.full((self.num_seqs, len(self)), False, dtype=bool)
         alpha = self.moltype.most_degen_alphabet()
-        for i, aligned in enumerate(self.seqs):
-            seq_array = numpy.array(aligned)
-            gaps = (
-                (seq_array >= alpha.gap_index)
-                if include_ambiguity
-                else seq_array == alpha.gap_index
-            )
-            result[i] = gaps
-        return result
+        gapped = self.array_seqs == alpha.gap_index
+        if include_ambiguity:
+            gapped = gapped | (self.array_seqs == alpha.missing_index)
+        return gapped
 
     def iupac_consensus(self, allow_gap: bool = True) -> str:
         """Returns string containing IUPAC consensus sequence of the alignment."""


Test patch:
diff --git a/tests/test_app/test_dist.py b/tests/test_app/test_dist.py
index 07bd4f3cd..83b95463a 100644
--- a/tests/test_app/test_dist.py
+++ b/tests/test_app/test_dist.py
@@ -460,7 +460,7 @@ def test_gap_dist():
         "b": "TTGAAGAATATGT------GAAAGAG",
         "c": "CTGAAGAACCTGTGAAAGTGAAAGAG",
     }
-    aln = make_aligned_seqs(data, moltype="dna", array_align=True)
+    aln = make_aligned_seqs(data, moltype="dna", new_type=True)
     expect = {
         ("a", "b"): 14.0,  # one gap diff of size 4
         ("a", "c"): 30.0,
@@ -476,7 +476,7 @@ def test_gap_dist():
         "b": "TTGAAGAATATGTA------AAAGAG",
         "c": "CTGAAGAACCTGTGAAAGTGAAAGAG",
     }
-    aln = make_aligned_seqs(data, moltype="dna", array_align=True)
+    aln = make_aligned_seqs(data, moltype="dna", new_type=True)
     expect = {
         ("a", "b"): 45.0,  # 3 gaps diff of size 15
         ("a", "c"): 29.0,
@@ -492,7 +492,7 @@ def test_gap_dist():
         "b": "TGGAGT--GA",
         "c": "TGGAGTGTGA",
     }
-    aln = make_aligned_seqs(data, moltype="dna", array_align=True)
+    aln = make_aligned_seqs(data, moltype="dna", new_type=True)
     expect = {
         ("a", "b"): 38,  # 3 gaps diff of size 8
         ("a", "c"): 26.0,
@@ -502,7 +502,7 @@ def test_gap_dist():
     dmat = app.main(aln)
     assert dmat.to_dict() == expect.to_dict()
     data = {"a": "AAGAA-A", "b": "-ATAATG", "c": "C-TGG-G"}
-    aln = make_aligned_seqs(data, moltype="dna", array_align=True)
+    aln = make_aligned_seqs(data, moltype="dna", new_type=True)
     expect = {
         ("a", "b"): 22.0,  # 2 gaps diff of size 2
         ("a", "c"): 11.0,
diff --git a/tests/test_app/test_sample.py b/tests/test_app/test_sample.py
index ec5c23856..fb13e7155 100644
--- a/tests/test_app/test_sample.py
+++ b/tests/test_app/test_sample.py
@@ -50,21 +50,6 @@ class TranslateTests(TestCase):
         """correctly return codon positions from Alignment"""
         self._codon_positions(array_align=False)
 
-    def test_codon_positions_4fold_degen(self):
-        """codon_positions correctly return fourfold degenerate bases"""
-        #                           **4---**4---
-        aln = cogent3.make_aligned_seqs(
-            data=[("a", "GCAAGCGTTTAT"), ("b", "GCTTTTGTCAAT")],
-            moltype=DNA,
-        )
-        expect = {"a": "AT", "b": "TC"}
-        ffold = sample.take_codon_positions(fourfold_degenerate=True)
-        got = ffold(aln)
-        assert got.to_dict() == expect
-        # error if no moltype
-        with pytest.raises(AssertionError):
-            _ = sample.take_codon_positions(moltype=None)
-
     def test_take_named_3(self):
         """3 named seqs"""
         select = sample.take_named_seqs("a", "b", "c")
@@ -701,3 +686,35 @@ def test_minlength():
     got = [aln.to_dict() for aln in map(ml, alns) if aln]  # pylint: disable=not-callable
     expected = []
     assert got == expected
+
+
+def test_codon_positions_4fold_degen():
+    """codon_positions correctly return fourfold degenerate bases"""
+    #                           **4---**4---
+    aln = cogent3.make_aligned_seqs(
+        data=[("a", "GCAAGCGTTTAT"), ("b", "GCTTTTGTCAAT")],
+        moltype=DNA,
+    )
+    expect = {"a": "AT", "b": "TC"}
+    ffold = sample.take_codon_positions(fourfold_degenerate=True)
+    got = ffold(aln)
+    assert got.to_dict() == expect
+    # error if no moltype
+    with pytest.raises(AssertionError):
+        _ = sample.take_codon_positions(moltype=None)
+
+
+def test_fourfold_empty_alignment():
+    aln = cogent3.make_aligned_seqs(
+        {"s1": "ACGACGACG", "s2": "GATGATGAT"},
+        moltype="dna",
+        new_type=True,
+    )
+    take_fourfold = cogent3.get_app(
+        "take_codon_positions",
+        fourfold_degenerate=True,
+        moltype="dna",
+    )
+    result = take_fourfold.main(aln)
+    assert isinstance(result, NotCompleted)
+    assert result.message == "result is empty"
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index e12cb7680..036c704a9 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -4104,35 +4104,6 @@ def test_alignment_counts_per_pos():
     assert numpy.array_equal(set(c.motifs), set("ACGT-"))
 
 
-def test_alignment_get_gap_array():
-    data = {
-        "DogFaced": "TG-",
-        "FreeTaile": "T-G",
-        "LittleBro": "---",
-        "BigBro": "GTT",
-        "MiddleBro": "-T-",
-    }
-    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
-    got = aln.get_gap_array()
-    expect = numpy.array(
-        [
-            [False, False, True],
-            [False, True, False],
-            [True, True, True],
-            [False, False, False],
-            [True, False, True],
-        ],
-        dtype=bool,
-    )
-    assert numpy.allclose(got, expect)
-
-    # if we slice the alignment, the gap array should reflect the slice
-    sliced = aln[:2]
-    got = sliced.get_gap_array()
-    expect = expect[:, :2]
-    assert numpy.allclose(got, expect)
-
-
 def test_get_position_indices():
     """get_position_indices should return names of cols where f(col)"""
 
@@ -4290,7 +4261,7 @@ def test_get_gap_array():
     got = aln.get_gap_array()
     expect = numpy.array(
         [
-            [False, True, False, True],
+            [False, True, False, False],
             [False, False, True, True],
             [True, True, True, True],
         ],
@@ -4307,6 +4278,35 @@ def test_get_gap_array():
     )
 
 
+def test_alignment_get_gap_array():
+    data = {
+        "DogFaced": "TG-",
+        "FreeTaile": "T-G",
+        "LittleBro": "---",
+        "BigBro": "GTT",
+        "MiddleBro": "-T-",
+    }
+    aln = new_alignment.make_aligned_seqs(data, moltype="dna")
+    got = aln.get_gap_array()
+    expect = numpy.array(
+        [
+            [False, False, True],
+            [False, True, False],
+            [True, True, True],
+            [False, False, False],
+            [True, False, True],
+        ],
+        dtype=bool,
+    )
+    assert numpy.allclose(got, expect)
+
+    # if we slice the alignment, the gap array should reflect the slice
+    sliced = aln[:2]
+    got = sliced.get_gap_array()
+    expect = expect[:, :2]
+    assert numpy.allclose(got, expect)
+
+
 def test_count_gaps_per_pos():
     """correctly compute the number of gaps"""
     data = {"a": "AAAA---GGT", "b": "CCC--GG?GT"}