Repository: cogent3/cogent3 (python)

Issue:
ENH: implement `new_moltype.MolType.degenerate_from_seq`
this will be necessary for integration of new style modules with code performing alignment 

Pull request description:
DEV: new moltype method and some documentation
in this PR: 

NEW: 
- added new `MolType.degenerate_from_seq()`, fixes #1948

DOC: 
- docstrings for view and slice record classes
- docstrings for make_seq 

DEP:
- removed deprecation warnings and deprecated arguments marked for this release
@GavinHuttley I note that the `Map` class is marked for removal at this release, however, I did not remove it



Patch:
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 05f498508..083a1d5b3 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -2633,11 +2633,6 @@ class AlignmentI(object):
         """Returns new Alignment containing cols where f(col) is True."""
         return self.take_positions(self.get_position_indices(f, negate=negate))
 
-    @c3warn.deprecated_args(
-        "2024.6",
-        reason="consistency with other methods",
-        old_new=[("allow_gaps", "allow_gap")],
-    )
     def iupac_consensus(self, alphabet=None, allow_gap=True):
         """Returns string containing IUPAC consensus sequence of the alignment."""
         if alphabet is None:
@@ -4038,11 +4033,6 @@ class ArrayAlignment(AlignmentI, _SequenceCollectionBase):
             result.append(">" + str(l) + "\n" + "".join(seq2str(s)))
         return "\n".join(result) + "\n"
 
-    @c3warn.deprecated_args(
-        "2024.6",
-        reason="consistency with other methods",
-        old_new=[("allow_gaps", "allow_gap")],
-    )
     def iupac_consensus(self, alphabet=None, allow_gap=True):
         """Returns string containing IUPAC consensus sequence of the alignment."""
         if alphabet is None:
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index 140bf7e15..09ff6ef81 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -288,7 +288,24 @@ class SeqsDataABC(ABC):
 
 
 class SeqsData(SeqsDataABC):
-    # refactor: docstring
+    """A collection of sequences underlying a SequenceCollection. The sequence
+    data is stored as numpy arrays, however the underlying data can be accessed
+    as strings, bytes, or numpy arrays.
+
+    Attributes
+    ----------
+    data
+        a dictionary of {name: sequence} pairs
+    alphabet
+        an instance of CharAlphabet valid for the sequences
+    make_seq
+        optional sequence constructor, takes 'seq' and 'name' as keyword arguments
+        and returns a Sequence instance. If not set, __getitem__ will return a
+        SeqDataView
+    reversed_seqs
+        a dictionary of {name: bool} pairs indicating if the sequence is reversed
+    """
+
     __slots__ = ("_data", "_alphabet", "_make_seq", "_reversed_seqs")
     # todo: kath
     # refactor: design
diff --git a/src/cogent3/core/new_moltype.py b/src/cogent3/core/new_moltype.py
index bbec2f23b..23dc83c9b 100644
--- a/src/cogent3/core/new_moltype.py
+++ b/src/cogent3/core/new_moltype.py
@@ -267,61 +267,69 @@ IUPAC_PROTEIN_code_aa = {
     "*": "STOP",
 }
 
-IUPAC_PROTEIN_chars = (
-    "A",
-    "C",
-    "D",
-    "E",
-    "F",
-    "G",
-    "H",
-    "I",
-    "K",
-    "L",
-    "M",
-    "N",
-    "P",
-    "Q",
-    "R",
-    "S",
-    "T",
-    "U",
-    "V",
-    "W",
-    "Y",
+IUPAC_PROTEIN_chars = frozenset(
+    [
+        "A",
+        "C",
+        "D",
+        "E",
+        "F",
+        "G",
+        "H",
+        "I",
+        "K",
+        "L",
+        "M",
+        "N",
+        "P",
+        "Q",
+        "R",
+        "S",
+        "T",
+        "U",
+        "V",
+        "W",
+        "Y",
+    ]
 )
 
-PROTEIN_WITH_STOP_chars = (
-    "A",
-    "C",
-    "D",
-    "E",
-    "F",
-    "G",
-    "H",
-    "I",
-    "K",
-    "L",
-    "M",
-    "N",
-    "P",
-    "Q",
-    "R",
-    "S",
-    "T",
-    "U",
-    "V",
-    "W",
-    "Y",
-    "*",
+PROTEIN_WITH_STOP_chars = frozenset(
+    [
+        "A",
+        "C",
+        "D",
+        "E",
+        "F",
+        "G",
+        "H",
+        "I",
+        "K",
+        "L",
+        "M",
+        "N",
+        "P",
+        "Q",
+        "R",
+        "S",
+        "T",
+        "U",
+        "V",
+        "W",
+        "Y",
+        "*",
+    ]
 )
 
-IUPAC_PROTEIN_ambiguities = {"B": ["N", "D"], "X": IUPAC_PROTEIN_chars, "Z": ["Q", "E"]}
+IUPAC_PROTEIN_ambiguities = {
+    "B": frozenset(["N", "D"]),
+    "X": IUPAC_PROTEIN_chars,
+    "Z": frozenset(["Q", "E"]),
+}
 
 PROTEIN_WITH_STOP_ambiguities = {
-    "B": ["N", "D"],
+    "B": frozenset(["N", "D"]),
     "X": PROTEIN_WITH_STOP_chars,
-    "Z": ["Q", "E"],
+    "Z": frozenset(["Q", "E"]),
 }
 
 # styling for moltype display
@@ -543,8 +551,26 @@ class MolType:
 
         return any(alpha == alphabet for alpha in self.iter_alphabets())
 
-    def make_seq(self, *, seq: str, name: OptStr = None, check_seq=True, **kwargs):
-        # refactor: docstring
+    def make_seq(
+        self, *, seq: str, name: OptStr = None, check_seq=True, **kwargs
+    ) -> new_sequence.Sequence:
+        """creates a Sequence object corresponding to the molecular type of
+        this instance.
+
+        Parameters
+        ----------
+        seq
+            the raw sequence data
+        name
+            the name of the sequence
+        check_seq
+            whether to validate the sequence data against the molecular type
+            if True, performs validation checks; set to False if the sequence
+            data is already known to be valid.
+        **kwargs
+            additional keyword arguments that may be required for creating the
+            Sequence object
+        """
         # refactor: design
         # for Sequence and SeqView object, a consistency check with moltype/alphabet
         # should be performed
@@ -1001,6 +1027,38 @@ class MolType:
     def _(self, seq: str) -> int:
         return numpy.prod([len(self.ambiguities.get(c, c)) for c in seq])
 
+    def degenerate_from_seq(self, seq: str) -> str:
+        """Returns least degenerate symbol that encompasses a set of characters"""
+        symbols = frozenset(seq)
+
+        # if the length of the set is 1, return the single character
+        if len(symbols) == 1:
+            return next(iter(symbols))
+
+        # all degenerate symbols should be added to the sets that they encompass
+        degens = self.ambiguities.copy()
+        for degen1 in degens:
+            for degen2, char_set in self.ambiguities.items():
+                if self.ambiguities[degen1] <= char_set:
+                    degens[degen2] = degens[degen2].union(frozenset(degen1))
+
+        # reverse the mapping between degenerate symbols and their encompassing sets
+        inv_degens = {frozenset(val): key for key, val in list(degens.items())}
+
+        # add gap and missing characters to the mapping
+        inv_degens[frozenset(self.gap)] = self.gap
+        inv_degens[frozenset(self.degen_gapped_alphabet)] = self.missing
+
+        # if we exactly match a set of symbols, return the corresponding degenerate
+        if result := inv_degens.get(symbols):
+            return result
+
+        # if we don't exactly match, sort all encompassing sets and return the
+        # least degenerate one
+        encompassing = [chars for chars in inv_degens if chars.issuperset(symbols)]
+        encompassing = sorted(encompassing, key=len)
+        return inv_degens[encompassing[0]]
+
     def strand_symmetric_motifs(
         self, motif_length: int = 1
     ) -> set[tuple[str, str]]:  # refactor: docstring
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index bfce5ced0..e8dc61247 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -1992,7 +1992,21 @@ def _input_vals_neg_step(seqlen, start, stop, step):
 
 class SliceRecordABC(ABC):
     """Abstract base class for recording the history of operations to be applied
-    to some underlying data
+    to some underlying data. Provides slicing functionality for the underlying data.
+
+    Parameters
+    ----------
+    start
+        start of the slice (inclusive indexing)
+    stop
+        stop of the slice (exclusive indexing)
+    step
+        step of the slice
+    offset
+        can be set with any additional offset that exists before the start of
+        the underlying data
+    seq_len
+        length of the underlying data (not including offset)
 
     Notes
     -----
@@ -2377,11 +2391,19 @@ class SliceRecordABC(ABC):
 
 
 class SeqViewABC(ABC):
-    # refactor: docstring
+    """
+    An abstract base class for providing a view of a sequence.
+
+    This class defines an interface for sequence views, by which operations
+    performed on the sequence do not modify the original sequence data. Instead,
+    modifications and operations are stored on the returned view of the sequence
+    and can be realised by accessing the values of the view.
+    """
 
     __slots__ = ()
 
     @property
+    @abstractmethod
     def seqid(self) -> OptStr: ...
 
     @property
@@ -2416,7 +2438,33 @@ class SeqViewABC(ABC):
 
 
 class SeqView(SeqViewABC, SliceRecordABC):
-    # refactor: docstring
+    """
+    Provides a view of a sequence with support for slicing operations.
+
+    This class represents a view of a sequence, allowing for efficient slicing
+    without altering the original sequence data.
+
+    Parameters
+    ----------
+    seq
+        the original sequence data
+    alphabet
+        the alphabet object defining valid characters for the sequence
+    start
+        the starting index of the slice. Defaults to the start of the sequence
+    stop
+        the stopping index of the slice. Defaults to the end of the sequence
+    step
+        the step size for the slice. Defaults to 1
+    offset
+        an offset used for adjusting the view's starting position. Defaults to 0
+    seqid
+        the name or identifier of the sequence
+    seq_len
+        the length of the sequence. Defaults to the length of the input sequence
+
+    """
+
     __slots__ = (
         "seq",
         "alphabet",
diff --git a/src/cogent3/core/sequence.py b/src/cogent3/core/sequence.py
index fde1fadbb..e33329c67 100644
--- a/src/cogent3/core/sequence.py
+++ b/src/cogent3/core/sequence.py
@@ -60,7 +60,6 @@ from cogent3.core.location import FeatureMap, IndelMap, LostSpan
 from cogent3.format.fasta import seqs_to_fasta
 from cogent3.maths.stats.contingency import CategoryCounts
 from cogent3.maths.stats.number import CategoryCounter
-from cogent3.util import warning as c3warn
 from cogent3.util.dict_array import DictArrayTemplate
 from cogent3.util.misc import (
     DistanceFromMatrix,
@@ -1427,11 +1426,6 @@ class Sequence(SequenceI):
             for pos in range(start, end, step):
                 yield self[pos : pos + window]
 
-    @c3warn.deprecated_args(
-        "2024.6",
-        reason="argument name consistency",
-        old_new=(("log_warnings", "warn"),),
-    )
     def get_in_motif_size(self, motif_length=1, warn=False):
         """returns sequence as list of non-overlapping motifs
 
diff --git a/src/cogent3/evolve/motif_prob_model.py b/src/cogent3/evolve/motif_prob_model.py
index 66d56e44d..235121554 100644
--- a/src/cogent3/evolve/motif_prob_model.py
+++ b/src/cogent3/evolve/motif_prob_model.py
@@ -4,8 +4,6 @@ from typing import Union
 
 import numpy
 
-import cogent3.util.warning as c3warn
-
 from cogent3.core.alphabet import Alphabet
 from cogent3.evolve.likelihood_tree import make_likelihood_tree_leaf
 from cogent3.recalculation.definition import CalcDefn, PartitionDefn
@@ -180,10 +178,7 @@ class MonomerProbModel(ComplexMotifProbModel):
         )
         return (monomer_probs, word_probs, mprobs_matrix)
 
-    @c3warn.deprecated_args(
-        "2024.6", reason="use warn instead", discontinued="auto", stack_level=4
-    )
-    def adapt_motif_probs(self, motif_probs, warn=False, auto=False):
+    def adapt_motif_probs(self, motif_probs, warn=False):
         try:
             motif_probs = adapt_motif_probs(self.monomer_alphabet, motif_probs)
         except ValueError:
diff --git a/src/cogent3/maths/optimisers.py b/src/cogent3/maths/optimisers.py
index 2c9f177f2..c6f10e590 100644
--- a/src/cogent3/maths/optimisers.py
+++ b/src/cogent3/maths/optimisers.py
@@ -140,9 +140,6 @@ def minimise(f, *args, **kw):
     return maximise(nf, *args, **kw)
 
 
-@c3warn.deprecated_args(
-    "2024.6", reason="has no effect in this function", discontinued="limit_action"
-)
 @UI.display_wrap
 def maximise(
     f,


Test patch:
diff --git a/tests/test_core/test_maps.py b/tests/test_core/test_maps.py
index 5eca67498..d384f868e 100644
--- a/tests/test_core/test_maps.py
+++ b/tests/test_core/test_maps.py
@@ -26,7 +26,7 @@ class MapTest(unittest.TestCase):
                 self.fail(repr((r, expected)))
 
     def test_get_by_annotation(self):
-        seq = DNA.make_seq("ATCGATCGAT" * 5, name="base")
+        seq = DNA.make_seq(seq="ATCGATCGAT" * 5, name="base")
         seq.add_feature(biotype="test_type", name="test_label", spans=[(5, 10)])
         seq.add_feature(biotype="test_type", name="test_label2", spans=[(15, 18)])
 
diff --git a/tests/test_core/test_new_alignment.py b/tests/test_core/test_new_alignment.py
index d00ea33e5..9743df137 100644
--- a/tests/test_core/test_new_alignment.py
+++ b/tests/test_core/test_new_alignment.py
@@ -8,7 +8,7 @@ from warnings import catch_warnings, filterwarnings
 import numpy
 import pytest
 
-from cogent3 import load_seq, load_unaligned_seqs, open_
+from cogent3 import load_unaligned_seqs, open_
 from cogent3._version import __version__
 from cogent3.core import new_alignment, new_alphabet, new_moltype, new_sequence
 from cogent3.core.annotation import Feature
@@ -234,7 +234,6 @@ def test_seqs_data_seq_lengths(str_seqs_dict, arr_seqs_dict, alpha):
 
 
 def test_seqs_data_get_seq_array(str_seqs_dict, alpha):
-    # todo: kath, edit and parametrize for all moltypes
     # seq1
     expect = numpy.array([2, 1, 3, 0], dtype="uint8")
     sd = new_alignment.SeqsData(data=str_seqs_dict, alphabet=alpha)
diff --git a/tests/test_core/test_new_moltype.py b/tests/test_core/test_new_moltype.py
index bcdb1a26f..96bf95197 100644
--- a/tests/test_core/test_new_moltype.py
+++ b/tests/test_core/test_new_moltype.py
@@ -519,3 +519,39 @@ def test_count_variants():
     assert p("H") == 3
     assert p("NRH") == 24
     assert p("AUGCNGUCAG-AURGAUC--GAUHCGAUACGWS") == 96
+
+
+def test_degenerate_from_seq():
+    """RnaMolType degenerate_from_seq should give correct results"""
+    rna = new_moltype.get_moltype("rna")
+    d = rna.degenerate_from_seq
+    # check monomers
+    assert d("A") == "A"
+    assert d("C") == "C"
+    # check seq of monomers
+    assert d("AAA") == "A"
+    # check some 2- to 4-way cases
+    assert d("AG") == "R"
+    assert d("CG") == "S"
+    assert d("ACG") == "V"
+    assert d("AGCU") == "N"
+    # check some cases with gaps
+    assert d("A---ACU") == "?"
+    assert d("AAA-") == "?"
+    assert d("---") == "-"
+    # check mixed case example
+    assert d("AAAAAA") == "A"
+    # check example with degenerate symbols in set
+    assert d("RS") == "V"
+    assert d("RN-") == "?"
+    # check that it works for proteins as well
+    protein = new_moltype.get_moltype("protein")
+    p = protein.degenerate_from_seq
+    assert p("A") == "A"
+    assert p("AAA") == "A"
+    assert p("DN") == "B"
+    assert p("---") == "-"
+    assert p("ACD") == "X"
+    assert p("ABD") == "X"
+    assert p("ACX") == "X"
+    assert p("AC-") == "?"
diff --git a/tests/test_core/test_profile.py b/tests/test_core/test_profile.py
index d8b43ecb2..5e9bd5fec 100644
--- a/tests/test_core/test_profile.py
+++ b/tests/test_core/test_profile.py
@@ -535,6 +535,6 @@ class PSSMTests(TestCase):
             [0.6, 0.15, 0.05, 0.2],
         ]
         pssm = PSSM(data, "ACTG")
-        seq = DNA.make_seq("".join("ACTG"[i] for i in [3, 1, 2, 0, 2, 2, 3]))
+        seq = DNA.make_seq(seq="".join("ACTG"[i] for i in [3, 1, 2, 0, 2, 2, 3]))
         scores = pssm.score_seq(seq)
         assert_allclose(scores, [-4.481, -5.703, -2.966], atol=1e-3)
diff --git a/tests/test_core/test_sequence.py b/tests/test_core/test_sequence.py
index ec50f14d0..c03e59186 100644
--- a/tests/test_core/test_sequence.py
+++ b/tests/test_core/test_sequence.py
@@ -1139,7 +1139,7 @@ class SequenceIntegrationTests(TestCase):  # not porting: not supporting ArraySe
 
     def test_regular_to_model(self):
         """Regular sequence should convert to model sequence"""
-        r = RNA.make_seq("AAA", name="x")
+        r = RNA.make_seq(seq="AAA", name="x")
         s = RNA.make_array_seq(seq=r)
         self.assertEqual(str(s), "AAA")
         self.assertEqual(s.moltype, RNA)
diff --git a/tests/test_evolve/test_likelihood_function.py b/tests/test_evolve/test_likelihood_function.py
index 5c32b8ea5..03964cf0b 100644
--- a/tests/test_evolve/test_likelihood_function.py
+++ b/tests/test_evolve/test_likelihood_function.py
@@ -539,7 +539,7 @@ DogFaced   root      1.00  1.00
             root = simalign.named_seqs["root"]
             self.assertEqual(str(root), str(root_sequence))
 
-        root_sequence = DNA.make_seq("GTAATT")
+        root_sequence = DNA.make_seq(seq="GTAATT")
         use_root_seq(root_sequence)  # as a sequence instance
         use_root_seq("GTAATC")  # as a string
 
diff --git a/tests/test_parse/test_cigar.py b/tests/test_parse/test_cigar.py
index e8547afb4..0c57181fc 100644
--- a/tests/test_parse/test_cigar.py
+++ b/tests/test_parse/test_cigar.py
@@ -14,8 +14,8 @@ from cogent3.parse.cigar import (
 class TestCigar(unittest.TestCase):
     def setUp(self):
         self.cigar_text = "3D2M3D6MDM2D3MD"
-        self.aln_seq = DNA.make_seq("---AA---GCTTAG-A--CCT-")
-        self.aln_seq1 = DNA.make_seq("CCAAAAAA---TAGT-GGC--G")
+        self.aln_seq = DNA.make_seq(seq="---AA---GCTTAG-A--CCT-")
+        self.aln_seq1 = DNA.make_seq(seq="CCAAAAAA---TAGT-GGC--G")
         self.map, self.seq = self.aln_seq.parse_out_gaps()
         self.map1, self.seq1 = self.aln_seq1.parse_out_gaps()
         self.slices = [(1, 4), (0, 8), (7, 12), (0, 1), (3, 5)]