Repository: cogent3/cogent3
Pull request #1751 (2024-03-12 03:24:34)

Feature/issue 1736
Fixed issue#1736, discontinued `cogent3.maths.special.permutations(), cogent3.maths.special.permutations_exact(), cogent3.maths.special.ln_permutations()` and removed corresponding test `test_maths/test_stats/test_special.py::SpecialTests::test_permutations(),test_permutations_errors(),test_permutations_float(),test_permutations_range(),test_permutations_exact(),test_ln_permutations()`



Patch:
diff --git a/requirements.txt b/requirements.txt
index 03b4568a2..33e73d40f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,6 +8,6 @@ numba>0.54; python_version>='3.10'
 numba>=0.57.0; python_version=='3.11'
 scipy==1.12.0
 nox==2024.3.2
-plotly==5.20.0
+plotly==5.19.0
 typing_extensions>=3.7
 .[dev]
diff --git a/src/cogent3/app/sample.py b/src/cogent3/app/sample.py
index 3ae9a4d6a..e47ad759d 100644
--- a/src/cogent3/app/sample.py
+++ b/src/cogent3/app/sample.py
@@ -327,66 +327,24 @@ class take_codon_positions:
 
     def __init__(
         self,
-        *positions: int,
-        fourfold_degenerate: bool = False,
-        gc: Union[str, int] = "Standard",
-        moltype: str = "dna",
+        *positions,
+        fourfold_degenerate=False,
+        gc="Standard",
+        moltype="dna",
     ):
         """
         Parameters
         ----------
         positions
-            either a single integer from (1, 2, 3), or additional keyword
-            arguments of position numbers, e.g. 3 is third position, (1,2)
-            is first and second codon position
-        fourfold_degenerate
+            either an integer (1, 2, 3), or a tuple of position numbers,
+            e.g. 3 is third position, (1,2) is first and second codon position
+        fourfold_degenerate : bool
             if True, returns third positions from four-fold degenerate codons.
             Overrides positions.
         gc
-            identifier for a genetic code or a genetic code instance.
-            see https://cogent3.org/doc/cookbook/what_codes.html
-        moltype
+            identifier for a genetic code or a genetic code instance
+        moltype : str
             molecular type, must be either DNA or RNA
-
-        Examples
-        --------
-
-        Create a sample alignment and an app that extracts the 3rd codon
-        position from an alignment.
-
-        >>> from cogent3 import make_aligned_seqs, get_app
-        >>> aln = make_aligned_seqs({"s1": "ACGACGACG", "s2": "GATGATGAT"})
-        >>> take_pos3 = get_app("take_codon_positions", 3, moltype="dna")
-        >>> result = take_pos3(aln)
-        >>> print(result.to_pretty())
-        s1    GGG
-        s2    TTT
-
-        Create an app that extracts the 1st and 2nd codon positions from an
-        alignment.
-
-        >>> take_pos12 = get_app("take_codon_positions", 1, 2, moltype="dna")
-        >>> result = take_pos12(aln)
-        >>> print(result.to_pretty())
-        s1    ACACAC
-        s2    GAGAGA
-
-        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"})
-        >>> take_fourfold = get_app("take_codon_positions", fourfold_degenerate=True, moltype="dna")
-        >>> result = take_fourfold(aln_ff)
-        >>> print(result.to_pretty())
-        s1    AT
-        s2    TC
-
-        A NotCompleted object (see https://cogent3.org/doc/app/not-completed.html)
-        is returned if all sites are excluded.
-
-        >>> result = take_fourfold(aln)
-        >>> result.message
-        'Traceback ...
         """
         assert moltype is not None
         moltype = get_moltype(moltype)
diff --git a/src/cogent3/core/alignment.py b/src/cogent3/core/alignment.py
index 4e8860f78..88072defa 100644
--- a/src/cogent3/core/alignment.py
+++ b/src/cogent3/core/alignment.py
@@ -18,7 +18,6 @@
     passed in a stream of two-item label, sequence pairs. However, this can
     cause confusion when testing.
 """
-
 from __future__ import annotations
 
 import functools
@@ -5290,7 +5289,7 @@ class Alignment(AlignmentI, SequenceCollection):
             seqname = seqid_to_seqname[seqid]
             seq = self.named_seqs[seqname]
             # we use parent seqid, stored on SeqView
-            parent_id, start, stop, _ = seq.data.parent_coordinates()
+            parent_id, start, end, _ = seq.data.parent_coordinates()
             offset = seq.data.annotation_offset
 
             for feature in self.annotation_db.get_features_matching(
@@ -5300,7 +5299,7 @@ class Alignment(AlignmentI, SequenceCollection):
                 on_alignment=False,
                 allow_partial=allow_partial,
                 start=start,
-                stop=stop,
+                end=end,
             ):
                 if offset:
                     feature["spans"] = (array(feature["spans"]) - offset).tolist()
diff --git a/src/cogent3/core/annotation.py b/src/cogent3/core/annotation.py
index 6d7ee6811..5f8a4270b 100644
--- a/src/cogent3/core/annotation.py
+++ b/src/cogent3/core/annotation.py
@@ -163,7 +163,7 @@ class Feature:
         """generator returns sub-features of self optionally matching biotype"""
         offset = getattr(self.parent, "annotation_offset", 0)
         start = self.map.start + offset
-        stop = self.map.end + offset
+        end = self.map.end + offset
 
         make_feature = self.parent.make_feature
         db = self.parent.annotation_db
@@ -171,7 +171,7 @@ class Feature:
             biotype=biotype,
             name=self.name,
             start=start,
-            stop=stop,
+            end=end,
             **kwargs,
         ):
             record["spans"] = array(record["spans"]) - offset
@@ -187,14 +187,14 @@ class Feature:
         """generator returns parent features of self optionally matching biotype"""
         offset = getattr(self.parent, "annotation_offset", 0)
         start = self.map.start + offset
-        stop = self.map.end + offset
+        end = self.map.end + offset
 
         make_feature = self.parent.make_feature
         db = self.parent.annotation_db
         for record in db.get_feature_parent(
             name=self.name,
             start=start,
-            stop=stop,
+            end=end,
             **kwargs,
         ):
             record["spans"] = array(record["spans"]) - offset
diff --git a/src/cogent3/core/annotation_db.py b/src/cogent3/core/annotation_db.py
index bfaf6d991..9e05b2e7f 100644
--- a/src/cogent3/core/annotation_db.py
+++ b/src/cogent3/core/annotation_db.py
@@ -14,8 +14,6 @@ import typing
 import numpy
 import typing_extensions
 
-import cogent3.util.warning as c3warn
-
 from cogent3._version import __version__
 from cogent3.util.deserialise import register_deserialiser
 from cogent3.util.misc import extend_docstring_from, get_object_provenance
@@ -87,11 +85,6 @@ class SerialisableType(typing.Protocol):  # pragma: no cover
 @typing.runtime_checkable
 class SupportsQueryFeatures(typing.Protocol):  # pragma: no cover
     # should be defined centrally
-    @c3warn.deprecated_args(
-        "2024.9",
-        reason="replace usage of SQL keyword as column name",
-        old_new=[("end", "stop")],
-    )
     def get_features_matching(
         self,
         *,
@@ -99,40 +92,20 @@ class SupportsQueryFeatures(typing.Protocol):  # pragma: no cover
         biotype: OptionalStr = None,
         name: OptionalStr = None,
         start: OptionalInt = None,
-        stop: OptionalInt = None,
+        end: OptionalInt = None,
         strand: OptionalStr = None,
         attributes: OptionalStr = None,
         on_alignment: OptionalBool = None,
     ) -> typing.Iterator[FeatureDataType]:
         ...
 
-    @c3warn.deprecated_args(
-        "2024.9",
-        reason="replace usage of SQL keyword as column name",
-        old_new=[("end", "stop")],
-    )
     def get_feature_children(
-        self,
-        *,
-        name: str,
-        start: OptionalInt = None,
-        stop: OptionalInt = None,
-        **kwargs,
+        self, *, name: str, start: OptionalInt = None, end: OptionalInt = None, **kwargs
     ) -> typing.List[FeatureDataType]:
         ...
 
-    @c3warn.deprecated_args(
-        "2024.9",
-        reason="replace usage of SQL keyword as column name",
-        old_new=[("end", "stop")],
-    )
     def get_feature_parent(
-        self,
-        *,
-        name: str,
-        start: OptionalInt = None,
-        stop: OptionalInt = None,
-        **kwargs,
+        self, *, name: str, start: OptionalInt = None, end: OptionalInt = None, **kwargs
     ) -> typing.List[FeatureDataType]:
         ...
 
@@ -148,11 +121,6 @@ class SupportsQueryFeatures(typing.Protocol):  # pragma: no cover
     ) -> int:
         ...
 
-    @c3warn.deprecated_args(
-        "2024.9",
-        reason="replace usage of SQL keyword as column name",
-        old_new=[("end", "stop")],
-    )
     def subset(
         self,
         *,
@@ -160,7 +128,7 @@ class SupportsQueryFeatures(typing.Protocol):  # pragma: no cover
         biotype: OptionalStr = None,
         name: OptionalStr = None,
         start: OptionalInt = None,
-        stop: OptionalInt = None,
+        end: OptionalInt = None,
         strand: OptionalStr = None,
         attributes: OptionalStr = None,
     ):
@@ -279,8 +247,8 @@ def _matching_conditions(
     conditions : dict
         column name and values to be matched
     allow_partial : bool, optional
-        if False, only records within start, stop are included. If True,
-        all records that overlap the segment defined by start, stop are included.
+        if False, only records within start, end are included. If True,
+        all records that overlap the segment defined by start, end are included.
 
     Returns
     -------
@@ -288,7 +256,7 @@ def _matching_conditions(
         the SQL statement and the tuple of values
     """
     start = conditions.pop("start", None)
-    stop = conditions.pop("stop", None)
+    end = conditions.pop("end", None)
 
     sql = []
     vals = ()
@@ -310,27 +278,27 @@ def _matching_conditions(
         sql.append(" AND ".join(conds))
         vals = tuple(vals)
 
-    if isinstance(start, int) and isinstance(stop, int):
+    if isinstance(start, int) and isinstance(end, int):
         if allow_partial:
             # allow matches that overlap the segment
             cond = [
-                f"(start >= {start} AND stop <= {stop})",  # lies within the segment
-                f"(start <= {start} AND stop > {start})",  # straddles beginning of segment
-                f"(start < {stop} AND stop >= {stop})",  # straddles stop of segment
-                f"(start <= {start} AND stop >= {stop})",  # includes segment
+                f"(start >= {start} AND end <= {end})",  # lies within the segment
+                f"(start <= {start} AND end > {start})",  # straddles beginning of segment
+                f"(start < {end} AND end >= {end})",  # straddles end of segment
+                f"(start <= {start} AND end >= {end})",  # includes segment
             ]
             cond = " OR ".join(cond)
         else:
             # only matches within bounds
-            cond = f"start >= {start} AND stop <= {stop}"
+            cond = f"start >= {start} AND end <= {end}"
         sql.append(f"({cond})")
     elif isinstance(start, int):
-        # if query has no stop, then any feature containing start
-        cond = f"(start <= {start} AND {start} < stop)"
+        # if query has no end, then any feature containing start
+        cond = f"(start <= {start} AND {start} < end)"
         sql.append(f"({cond})")
-    elif isinstance(stop, int):
-        # if query has no start, then any feature containing stop
-        cond = f"(start <= {stop} AND {stop} < stop)"
+    elif isinstance(end, int):
+        # if query has no start, then any feature containing end
+        cond = f"(start <= {end} AND {end} < end)"
         sql.append(f"({cond})")
 
     sql = f"{' AND '.join(sql)}"
@@ -341,7 +309,7 @@ def _del_records_sql(
     table_name: str,
     conditions: dict,
     start: OptionalInt = None,
-    stop: OptionalInt = None,
+    end: OptionalInt = None,
     allow_partial=True,
 ) -> ReturnType:
     """creates the SQL and values for identifying records to be deleted
@@ -352,12 +320,12 @@ def _del_records_sql(
         table to have records deleted from
     conditions : dict
         column name and values to be matched
-    start, stop : OptionalInt
-        select records whose (start, stop) values lie between start and stop,
+    start, end : OptionalInt
+        select records whose (start, end) values lie between start and end,
         or overlap them if (allow_partial is True)
     allow_partial : bool, optional
-        if False, only records within start, stop are included. If True,
-        all records that overlap the segment defined by start, stop are included.
+        if False, only records within start, end are included. If True,
+        all records that overlap the segment defined by start, end are included.
 
     Returns
     -------
@@ -365,7 +333,7 @@ def _del_records_sql(
         the SQL statement and the tuple of values
     """
     where, vals = _matching_conditions(
-        conditions=conditions, start=start, stop=stop, allow_partial=allow_partial
+        conditions=conditions, start=start, end=end, allow_partial=allow_partial
     )
     sql = f"DELETE FROM {table_name}"
     if not where:
@@ -380,7 +348,7 @@ def _select_records_sql(
     conditions: dict,
     columns: typing.Optional[typing.List[str]] = None,
     start: OptionalInt = None,
-    stop: OptionalInt = None,
+    end: OptionalInt = None,
     allow_partial=True,
 ) -> ReturnType:
     """create SQL select statement and values
@@ -393,12 +361,12 @@ def _select_records_sql(
         values to select
     conditions : dict
         the WHERE conditions
-    start, stop : OptionalInt
-        select records whose (start, stop) values lie between start and stop,
+    start, end : OptionalInt
+        select records whose (start, end) values lie between start and end,
         or overlap them if (allow_partial is True)
     allow_partial : bool, optional
-        if False, only records within start, stop are included. If True,
-        all records that overlap the segment defined by start, stop are included.
+        if False, only records within start, end are included. If True,
+        all records that overlap the segment defined by start, end are included.
 
     Returns
     -------
@@ -423,7 +391,7 @@ def _count_records_sql(
     conditions: dict,
     columns: typing.Optional[typing.List[str]] = None,
     start: OptionalInt = None,
-    stop: OptionalInt = None,
+    end: OptionalInt = None,
     allow_partial=True,
 ) -> ReturnType:
     """create SQL count statement and values
@@ -436,12 +404,12 @@ def _count_records_sql(
         values to select
     conditions : dict
         the WHERE conditions
-    start, stop : OptionalInt
-        select records whose (start, stop) values lie between start and stop,
+    start, end : OptionalInt
+        select records whose (start, end) values lie between start and end,
         or overlap them if (allow_partial is True)
     allow_partial : bool, optional
-        if False, only records within start, stop are included. If True,
-        all records that overlap the segment defined by start, stop are included.
+        if False, only records within start, end are included. If True,
+        all records that overlap the segment defined by start, end are included.
 
     Returns
     -------
@@ -480,38 +448,6 @@ def _compatible_schema(db: sqlite3.Connection, schema: dict[str, set]) -> bool:
     return True
 
 
-def _rename_column_if_exists(
-    db: sqlite3.Connection, table_name: str, old_column: str, new_column: str
-) -> None:
-    """Rename a column in a sqlite3 database only if it exists.
-
-    Parameters
-    ----------
-    db : sqlite3.Connection
-        the sqlite3 connection
-    table_name : str
-        the table name
-    old_column : str
-        the column to rename if it exists
-    new_column : str
-        the new name of old_column
-    """
-    cur = db.cursor()
-    table_info = cur.execute(f"PRAGMA table_info({table_name});").fetchall()
-
-    # Check if the column exists
-    for column_info in table_info:
-        if column_info["name"] == old_column:
-            break
-    else:
-        return  # There is no column to rename
-
-    sql = f'ALTER TABLE {table_name} RENAME COLUMN "{old_column}" TO {new_column}'
-
-    cur.execute(sql)
-    db.commit()
-
-
 class SqliteAnnotationDbMixin:
     # table schema for user provided annotations
     _user_schema = {
@@ -520,7 +456,7 @@ class SqliteAnnotationDbMixin:
         "name": "TEXT",
         "parent_id": "TEXT",
         "start": "INTEGER",
-        "stop": "INTEGER",
+        "end": "INTEGER",
         "strand": "TEXT",
         "spans": "array",
         "attributes": "TEXT",
@@ -642,9 +578,6 @@ class SqliteAnnotationDbMixin:
             )
             self._db.row_factory = sqlite3.Row
 
-            for table_name in self.table_names:
-                _rename_column_if_exists(self._db, table_name, "end", "stop")
-
         return self._db
 
     def _execute_sql(self, cmnd, values=None):
@@ -686,10 +619,10 @@ class SqliteAnnotationDbMixin:
             whether the annotation is an alignment annotation
         """
         spans = numpy.array(sorted(sorted(coords) for coords in spans), dtype=int)
-        # the start, stop variables are added into the record via the loop
+        # the start, end variables are added into the record via the loop
         # on local variables
         start = int(spans.min())
-        stop = int(spans.max())
+        end = int(spans.max())
         # we define record as all defined variables from local name space,
         # excluding "self"
         record = {k: v for k, v in locals().items() if k != "self" and v is not None}
@@ -719,7 +652,7 @@ class SqliteAnnotationDbMixin:
         column: str,
         name: str,
         start: OptionalInt = None,
-        stop: OptionalInt = None,
+        end: OptionalInt = None,
         biotype: OptionalStr = None,
         allow_partial: bool = False,
     ) -> typing.List[FeatureDataType]:
@@ -729,7 +662,7 @@ class SqliteAnnotationDbMixin:
             conditions={column: name, "biotype": biotype},
             columns=columns,
             start=start,
-            stop=stop,
+            end=end,
             allow_partial=allow_partial,
         )
         for result in self._execute_sql(sql, values=vals):
@@ -742,7 +675,7 @@ class SqliteAnnotationDbMixin:
         self, name: str, biotype: OptionalStr = None, **kwargs
     ) -> typing.Iterator[FeatureDataType]:
         """yields children of name"""
-        # kwargs is used because other classes need start / stop
+        # kwargs is used because other classes need start / end
         # just uses search matching
         for table_name in self.table_names:
             columns = "seqid", "biotype", "spans", "strand", "name", "parent_id"
@@ -794,11 +727,6 @@ class SqliteAnnotationDbMixin:
                         )  # remove invalid field for the FeatureDataType
                         yield parent
 
-    @c3warn.deprecated_args(
-        "2024.9",
-        reason="replace usage of SQL keyword as column name",
-        old_new=[("end", "stop")],
-    )
     def get_records_matching(
         self,
         *,
@@ -806,7 +734,7 @@ class SqliteAnnotationDbMixin:
         seqid: str = None,
         name: str = None,
         start: int = None,
-        stop: int = None,
+        end: int = None,
         strand: OptionalStr = None,
         attributes: OptionalStr = None,
         on_alignment: bool = None,
@@ -823,11 +751,6 @@ class SqliteAnnotationDbMixin:
             for result in self._get_records_matching(table_name, **kwargs):
                 yield dict(zip(result.keys(), result))
 
-    @c3warn.deprecated_args(
-        "2024.9",
-        reason="replace usage of SQL keyword as column name",
-        old_new=[("end", "stop")],
-    )
     def get_features_matching(
         self,
         *,
@@ -835,7 +758,7 @@ class SqliteAnnotationDbMixin:
         seqid: str = None,
         name: str = None,
         start: int = None,
-        stop: int = None,
+        end: int = None,
         strand: OptionalStr = None,
         attributes: OptionalStr = None,
         on_alignment: bool = None,
@@ -1118,11 +1041,6 @@ class SqliteAnnotationDbMixin:
             self.db.backup(backup)
         backup.close()
 
-    @c3warn.deprecated_args(
-        "2024.9",
-        reason="replace usage of SQL keyword as column name",
-        old_new=[("end", "stop")],
-    )
     def subset(
         self,
         *,
@@ -1131,7 +1049,7 @@ class SqliteAnnotationDbMixin:
         seqid: str = None,
         name: str = None,
         start: OptionalInt = None,
-        stop: OptionalInt = None,
+        end: OptionalInt = None,
         strand: OptionalStr = None,
         attributes: OptionalStr = None,
         allow_partial: bool = False,
@@ -1139,7 +1057,7 @@ class SqliteAnnotationDbMixin:
         """returns a new db instance with records matching the provided conditions"""
         # make sure python, not numpy, integers
         start = start if start is None else int(start)
-        stop = stop if stop is None else int(stop)
+        end = end if end is None else int(end)
 
         kwargs = {k: v for k, v in locals().items() if k not in {"self", "source"}}
 
@@ -1225,7 +1143,7 @@ class GffAnnotationDb(SqliteAnnotationDbMixin):
         "source": "TEXT",
         "biotype": "TEXT",  # type in GFF
         "start": "INTEGER",
-        "stop": "INTEGER",
+        "end": "INTEGER",
         "score": "TEXT",  # check defn
         "strand": "TEXT",
         "phase": "TEXT",
@@ -1264,12 +1182,12 @@ class GffAnnotationDb(SqliteAnnotationDbMixin):
         # required
         rows = []
         for record in reduced.values():
-            # our Feature code assumes start always < stop,
+            # our Feature code assumes start always < end,
             # we record direction using Strand
             spans = numpy.array(sorted(record["spans"]), dtype=int)  # sorts the rows
             spans.sort(axis=1)
             record["start"] = int(spans.min())
-            record["stop"] = int(spans.max())
+            record["end"] = int(spans.max())
             record["spans"] = spans
             rows.append(tuple(record.get(c) for c in col_order))
 
@@ -1293,7 +1211,6 @@ class GffAnnotationDb(SqliteAnnotationDbMixin):
         # record.
         for record in records:
             record["biotype"] = record.pop("Type")
-            record["stop"] = record.pop("End")
 
             # we force all keys that map to table column names to be lower case
             for key in tuple(record):
@@ -1317,7 +1234,7 @@ class GffAnnotationDb(SqliteAnnotationDbMixin):
                 reduced[record_id]["spans"] = []
 
             # should this just be an append?
-            reduced[record_id]["spans"].append((record["start"], record["stop"]))
+            reduced[record_id]["spans"].append((record["start"], record["end"]))
 
         del records
 
@@ -1350,7 +1267,7 @@ class GenbankAnnotationDb(SqliteAnnotationDbMixin):
         "source": "TEXT",
         "biotype": "TEXT",  # type in GFF
         "start": "INTEGER",
-        "stop": "INTEGER",
+        "end": "INTEGER",
         "strand": "TEXT",
         "comments": "TEXT",
         "spans": "array",  # aggregation of coords across records
@@ -1401,7 +1318,7 @@ class GenbankAnnotationDb(SqliteAnnotationDbMixin):
         rows = []
         exclude = {"translation", "location", "type"}  # location is grabbed directly
         for record in records:
-            # our Feature code assumes start always < stop,
+            # our Feature code assumes start always < end,
             store = {"seqid": seqid}
             # we create the location data directly
             if location := record.get("location", None):
@@ -1409,7 +1326,7 @@ class GenbankAnnotationDb(SqliteAnnotationDbMixin):
                 if strand := location.strand:
                     store["strand"] = "-" if strand == -1 else "+"
                 store["start"] = int(store["spans"].min())
-                store["stop"] = int(store["spans"].max())
+                store["end"] = int(store["spans"].max())
 
             attrs_keys = record.keys() - exclude
             store["biotype"] = record["type"]
@@ -1450,22 +1367,17 @@ class GenbankAnnotationDb(SqliteAnnotationDbMixin):
         self._num_fakeids += 1
         return name
 
-    @c3warn.deprecated_args(
-        "2024.9",
-        reason="replace usage of SQL keyword as column name",
-        old_new=[("end", "stop")],
-    )
     def get_feature_children(
         self,
         name: str,
         biotype: OptionalStr = None,
         exclude_biotype: OptionalStr = None,
         start: OptionalInt = None,
-        stop: OptionalInt = None,
+        end: OptionalInt = None,
     ) -> typing.Iterator[FeatureDataType]:
         """yields children of name"""
         # children must lie within the parent coordinates
-        if start is None or stop is None:
+        if start is None or end is None:
             name = self.__class__.__name__
             msg = f"coordinates required to query children for {name!r}"
             raise ValueError(msg)
@@ -1479,7 +1391,7 @@ class GenbankAnnotationDb(SqliteAnnotationDbMixin):
                 "name",
                 "parent_id",
                 "start",
-                "stop",
+                "end",
             )
             if table_name == "user":
                 columns += ("on_alignment",)
@@ -1491,32 +1403,27 @@ class GenbankAnnotationDb(SqliteAnnotationDbMixin):
                 name=name,
                 biotype=biotype,
                 start=start,
-                stop=stop,
+                end=end,
                 allow_partial=False,
             ):
                 if feat["biotype"] == exclude_biotype:
                     continue
-                cstart, cstop = feat.pop("start"), feat.pop("stop")
-                if not (start <= cstart < stop and start < cstop <= stop):
+                cstart, cend = feat.pop("start"), feat.pop("end")
+                if not (start <= cstart < end and start < cend <= end):
                     continue
 
                 feat.pop("parent_id")  # remove invalid field for the FeatureDataType
                 yield feat
 
-    @c3warn.deprecated_args(
-        "2024.9",
-        reason="replace usage of SQL keyword as column name",
-        old_new=[("end", "stop")],
-    )
     def get_feature_parent(
         self,
         name: str,
         exclude_biotype: OptionalStr = None,
         start: OptionalInt = None,
-        stop: OptionalInt = None,
+        end: OptionalInt = None,
     ) -> typing.Iterator[FeatureDataType]:
         """yields parents of name"""
-        if start is None or stop is None:
+        if start is None or end is None:
             name = self.__class__.__name__
             msg = f"coordinates required to query parent for {name!r}"
             raise ValueError(msg)
@@ -1531,7 +1438,7 @@ class GenbankAnnotationDb(SqliteAnnotationDbMixin):
                 "name",
                 "parent_id",
                 "start",
-                "stop",
+                "end",
             )
             if table_name == "user":
                 columns += ("on_alignment",)
@@ -1541,15 +1448,15 @@ class GenbankAnnotationDb(SqliteAnnotationDbMixin):
                 column="name",
                 name=name,
                 start=start,
-                stop=stop,
+                end=end,
                 allow_partial=False,
             ):
                 # add support for != operation to SQL where clause generation
                 if feat["biotype"] == exclude_biotype:
                     continue
 
-                cstart, cstop = feat.pop("start"), feat.pop("stop")
-                if cstart > start or stop > cstop:
+                cstart, cend = feat.pop("start"), feat.pop("end")
+                if cstart > start or end > cend:
                     continue
 
                 feat.pop("parent_id")  # remove invalid field for the FeatureDataType
diff --git a/src/cogent3/core/sequence.py b/src/cogent3/core/sequence.py
index ebe4870c3..14279b9dd 100644
--- a/src/cogent3/core/sequence.py
+++ b/src/cogent3/core/sequence.py
@@ -10,7 +10,6 @@ Sequences are intended to be immutable. This is not enforced by the code for
 performance reasons, but don't alter the MolType or the sequence data after
 creation.
 """
-
 from __future__ import annotations
 
 import contextlib
@@ -921,7 +920,7 @@ class Sequence(SequenceI):
         name
             name of the feature
         start, stop
-            start, stop positions to search between, relative to offset
+            start, end positions to search between, relative to offset
             of this sequence. If not provided, entire span of sequence is used.
 
         Notes
@@ -956,11 +955,11 @@ class Sequence(SequenceI):
         query_start = self._seq.absolute_position(start, include_boundary=False)
         # we set include_boundary=True because stop is exclusive indexing,
         # i,e., the stop can be equal to the length of the view
-        query_stop = self._seq.absolute_position(stop, include_boundary=True)
+        query_end = self._seq.absolute_position(stop, include_boundary=True)
 
         rev_strand = strand == -1
         if rev_strand:
-            query_start, query_stop = query_stop, query_start
+            query_start, query_end = query_end, query_start
 
         query_start = max(query_start, 0)
         # in the underlying db, features are always plus strand oriented
@@ -984,7 +983,7 @@ class Sequence(SequenceI):
             name=name,
             biotype=biotype,
             start=query_start,
-            stop=query_stop,
+            end=query_end,  # todo gah end should be stop
             allow_partial=allow_partial,
         ):
             # spans need to be converted from absolute to relative positions
diff --git a/src/cogent3/maths/stats/distribution.py b/src/cogent3/maths/stats/distribution.py
index 48157bb35..aad172bd9 100644
--- a/src/cogent3/maths/stats/distribution.py
+++ b/src/cogent3/maths/stats/distribution.py
@@ -23,7 +23,6 @@ from cogent3.maths.stats.special import (
     log1p,
     ndtri,
 )
-from cogent3.util import warning as c3warn
 
 
 # ndtri import b/c it should be available via this module
@@ -44,13 +43,11 @@ def tprob(x, df):
     return 2 * t.sf(abs(x), df)
 
 
-@c3warn.deprecated_callable(
-    version="2024.9",
-    reason="use scipy.stats.poisson.sf() instead",
-    is_discontinued=True,
-)
-def poisson_high(successes, mean):  # pragma: no cover
-    """being removed"""
+def poisson_high(successes, mean):
+    """Returns right tail of Poission distribution, Pr(X > x).
+
+    successes ranges from 0 to infinity. mean must be positive.
+    """
     return pdtrc(successes, mean)
 
 
diff --git a/src/cogent3/maths/stats/special.py b/src/cogent3/maths/stats/special.py
index 18a8b2a33..5f9a837ee 100644
--- a/src/cogent3/maths/stats/special.py
+++ b/src/cogent3/maths/stats/special.py
@@ -4,6 +4,8 @@
 
 from numpy import exp, floor, log, sin, sqrt
 
+from cogent3.util import warning as c3warn
+
 
 log_epsilon = 1e-6  # for threshold in log/exp close to 1
 # For IEEE arithmetic (IBMPC):
@@ -58,7 +60,10 @@ def one_minus_exp(x):
         return 1 - exp(x)
 
 
-def permutations(n, k):
+@c3warn.deprecated_callable(
+    version="2024.9", reason="use scipy.special.perm() instead", is_discontinued=True
+)
+def permutations(n, k):  # pragma: no cover
     """Returns the number of ways of choosing k items from n, in order.
 
     Defined as n!/(n-k)!.
@@ -76,7 +81,10 @@ def permutations(n, k):
         return exp(ln_permutations(n, k))
 
 
-def permutations_exact(n, k):
+@c3warn.deprecated_callable(
+    version="2024.9", reason="use scipy.special.perm() instead", is_discontinued=True
+)
+def permutations_exact(n, k):  # pragma: no cover
     """Calculates permutations by integer division.
 
     Preferred method for small permutations, but slow on larger ones.
@@ -89,7 +97,10 @@ def permutations_exact(n, k):
     return product
 
 
-def ln_permutations(n, k):
+@c3warn.deprecated_callable(
+    version="2024.9", reason="use scipy.special.perm() instead", is_discontinued=True
+)
+def ln_permutations(n, k):  # pragma: no cover
     """Calculates permutations by difference in log of gamma function.
 
     Preferred method for large permutations, but slow on smaller ones.
diff --git a/src/cogent3/util/warning.py b/src/cogent3/util/warning.py
index f4859b831..5994a8205 100644
--- a/src/cogent3/util/warning.py
+++ b/src/cogent3/util/warning.py
@@ -203,11 +203,6 @@ def deprecated_callable(
         sig = set(inspect.signature(func).parameters)
         _type = "method" if sig & {"self", "cls", "klass"} else "function"
         old = func.__name__
-        if is_discontinued and old == "__init__":
-            # we're really deprecating a class, so get that name
-            old = func.__qualname__.split(".")[-2]
-            _type = "class"
-
         params = dict(
             _type=_type,
             old=old,


Test patch:
diff --git a/tests/test_core/test_annotation_db.py b/tests/test_core/test_annotation_db.py
index 858aa0675..28e45da99 100644
--- a/tests/test_core/test_annotation_db.py
+++ b/tests/test_core/test_annotation_db.py
@@ -8,7 +8,6 @@ from cogent3.core.annotation_db import (
     GffAnnotationDb,
     SupportsFeatures,
     _matching_conditions,
-    _rename_column_if_exists,
     load_annotations,
 )
 from cogent3.core.sequence import Sequence
@@ -271,7 +270,7 @@ def test_empty_data():
 
 
 # testing GenBank files
-@pytest.fixture(scope="function")
+@pytest.fixture(scope="session")
 def gb_db(DATA_DIR):
     return load_annotations(path=DATA_DIR / "annotated_seq.gb")
 
@@ -293,7 +292,7 @@ def test_gb_get_children(gb_db, parent_biotype, name):
             name=name,
             exclude_biotype=parent_biotype,
             start=coords.min(),
-            stop=coords.max(),
+            end=coords.max(),
         )
     )[0]
     assert child["biotype"] != parent["biotype"]
@@ -309,7 +308,7 @@ def test_gb_get_parent(gb_db):
             name=cds_id,
             exclude_biotype="CDS",
             start=coords.min(),
-            stop=coords.max(),
+            end=coords.max(),
         )
     )[0]
     assert parent["biotype"] != cds["biotype"]
@@ -475,8 +474,8 @@ def test_get_features_matching_start_stop(DATA_DIR, seq):
 
 
 def test_matching_conditions():
-    got, _ = _matching_conditions({"start": 1, "stop": 5}, allow_partial=True)
-    expect = "((start >= 1 AND stop <= 5) OR (start <= 1 AND stop > 1) OR (start < 5 AND stop >= 5) OR (start <= 1 AND stop >= 5))"
+    got, _ = _matching_conditions({"start": 1, "end": 5}, allow_partial=True)
+    expect = "((start >= 1 AND end <= 5) OR (start <= 1 AND end > 1) OR (start < 5 AND end >= 5) OR (start <= 1 AND end >= 5))"
     assert got == expect
 
 
@@ -938,48 +937,6 @@ def test_load_anns_with_write(DATA_DIR, tmp_dir):
     assert got_data["tables"] == expect_data["tables"]
 
 
-def test_gff_end_renamed_to_stop(gff_db, tmp_path):
-    bad_col_path = tmp_path / "bad_col.gffdb"
-
-    correct_rich_dict = gff_db.to_rich_dict()
-    del correct_rich_dict["init_args"]
-
-    for table_name in gff_db.table_names:
-        # Convert in memory db stop column to the old end name
-        _rename_column_if_exists(gff_db.db, table_name, "stop", "end")
-
-    gff_db.write(tmp_path / bad_col_path)
-
-    # Test that end column is renamed to stop on construction
-    loaded_gff_db = GffAnnotationDb(source=bad_col_path)
-
-    new_rich_dict = loaded_gff_db.to_rich_dict()
-    del new_rich_dict["init_args"]
-
-    assert new_rich_dict == correct_rich_dict
-
-
-def test_gb_end_renamed_to_stop(gb_db, tmp_path):
-    bad_col_path = tmp_path / "bad_col.gbdb"
-
-    correct_rich_dict = gb_db.to_rich_dict()
-    del correct_rich_dict["init_args"]
-
-    for table_name in gb_db.table_names:
-        # Convert in memory db stop column to the old end name
-        _rename_column_if_exists(gb_db.db, table_name, "stop", "end")
-
-    gb_db.write(tmp_path / bad_col_path)
-
-    # Test that end column is renamed to stop on construction
-    loaded_gb_db = GenbankAnnotationDb(source=bad_col_path)
-
-    new_rich_dict = loaded_gb_db.to_rich_dict()
-    del new_rich_dict["init_args"]
-
-    assert new_rich_dict == correct_rich_dict
-
-
 def test_gbdb_get_children_fails_no_coords(gb_db):
     with pytest.raises(ValueError):
         _ = list(gb_db.get_feature_children(name="CNA00110"))
@@ -998,7 +955,7 @@ def test_load_annotations_invalid_path():
 @pytest.mark.parametrize("integer", (int, numpy.int64))
 def test_subset_gff3_db(gff_db, integer):
     subset = gff_db.subset(
-        seqid="I", start=integer(40), stop=integer(70), allow_partial=True
+        seqid="I", start=integer(40), end=integer(70), allow_partial=True
     )
     # manual inspection of the original GFF3 file indicates 7 records
     # BUT the CDS records get merged into a single row
@@ -1006,7 +963,7 @@ def test_subset_gff3_db(gff_db, integer):
 
 
 def test_subset_empty_db(gff_db):
-    subset = gff_db.subset(seqid="X", start=40, stop=70, allow_partial=True)
+    subset = gff_db.subset(seqid="X", start=40, end=70, allow_partial=True)
     # no records
     assert not len(subset)
 
@@ -1016,7 +973,7 @@ def test_subset_gff3_db_with_user(gff_db):
         seqid="I", name="gene-01", biotype="gene", spans=[(23, 43)], strand="+"
     )
     gff_db.add_feature(**record)
-    subset = gff_db.subset(seqid="I", start=40, stop=70, allow_partial=True)
+    subset = gff_db.subset(seqid="I", start=40, end=70, allow_partial=True)
     # manual inspection of the original GFF3 file indicates 7 records
     # BUT the CDS records get merged into a single row
     assert len(subset) == 7
@@ -1031,7 +988,7 @@ def test_subset_gb_db(gb_db):
 def test_subset_gff3_db_source(gff_db, tmp_dir):
     outpath = tmp_dir / "subset.gff3db"
     subset = gff_db.subset(
-        seqid="I", start=40, stop=70, allow_partial=True, source=outpath
+        seqid="I", start=40, end=70, allow_partial=True, source=outpath
     )
     subset.db.close()
 
diff --git a/tests/test_maths/test_stats/test_distribution.py b/tests/test_maths/test_stats/test_distribution.py
index fc7f88e85..f55901bf6 100644
--- a/tests/test_maths/test_stats/test_distribution.py
+++ b/tests/test_maths/test_stats/test_distribution.py
@@ -148,6 +148,28 @@ class DistributionsTests(TestCase):
         for key, value in list(expected.items()):
             assert_allclose(poisson_low(*key), value, rtol=1e-6)
 
+    def test_poisson_high(self):
+        """Upper tail of poisson should match R for integer successes"""
+        # WARNING: Results only guaranteed for integer successes: floating
+        # point _should_ yield reasonable values, but R rounds to int.
+        expected = {
+            (0, 0): 0,
+            (0, 0.75): 0.5276334,
+            (0, 1): 0.6321206,
+            (0, 5): 0.993262,
+            (0, 113.7): 1,
+            (2, 0): 0,
+            (2, 3): 0.5768099,
+            (2, 17.8): 0.9999967,
+            (17, 29.6): 0.9912467,
+            (180, 0): 0,
+            (180, 137.4): 0.0002159856,
+            (180, 318): 1,
+            (180, 1024): 1,
+        }
+        for key, value in list(expected.items()):
+            assert_allclose(poisson_high(*key), value)
+
     def test_poisson_exact(self):
         """Poisson exact should match expected values from R"""
         expected = {
diff --git a/tests/test_maths/test_stats/test_special.py b/tests/test_maths/test_stats/test_special.py
index 5056c37fd..a4de1df14 100644
--- a/tests/test_maths/test_stats/test_special.py
+++ b/tests/test_maths/test_stats/test_special.py
@@ -27,71 +27,6 @@ from cogent3.maths.stats.special import (
 class SpecialTests(TestCase):
     """Tests miscellaneous functions."""
 
-    def test_permutations(self):
-        """permutations should return expected results"""
-        self.assertEqual(permutations(1, 1), 1)
-        self.assertEqual(permutations(2, 1), 2)
-        self.assertEqual(permutations(3, 1), 3)
-        self.assertEqual(permutations(4, 1), 4)
-        self.assertEqual(permutations(4, 2), 12)
-        self.assertEqual(permutations(4, 3), 24)
-        self.assertEqual(permutations(4, 4), 24)
-        assert_allclose(permutations(300, 100), 3.8807387193009318e239)
-
-    def test_permutations_errors(self):
-        """permutations should raise errors on invalid input"""
-        self.assertRaises(IndexError, permutations, 10, 50)
-        self.assertRaises(IndexError, permutations, -1, 50)
-        self.assertRaises(IndexError, permutations, 10, -5)
-
-    def test_permutations_float(self):
-        """permutations should use gamma function when floats as input"""
-        assert_allclose(permutations(1.0, 1), 1)
-        assert_allclose(permutations(2, 1.0), 2)
-        assert_allclose(permutations(3.0, 1.0), 3)
-        assert_allclose(permutations(4.0, 1), 4)
-        assert_allclose(permutations(4.0, 2.0), 12)
-        assert_allclose(permutations(4.0, 3.0), 24)
-        assert_allclose(permutations(4, 4.0), 24)
-        assert_allclose(permutations(300, 100), 3.8807387193009318e239)
-
-    def test_permutations_range(self):
-        """permutations should increase gradually with increasing k"""
-        start = 5  # permuations(10,5) = 30240
-        end = 6  # permutations(10,6) = 151200
-        step = 0.1
-        lower_lim = 30240
-        upper_lim = 151200
-        previous_value = 30239.9999
-        while start <= end:
-            obs = permutations(10, start)
-            assert lower_lim <= obs <= upper_lim
-            assert obs > previous_value
-            previous_value = obs
-            start += step
-
-    def test_permutations_exact(self):
-        """permutations_exact should return expected results"""
-        assert_allclose(permutations_exact(1, 1), 1)
-        assert_allclose(permutations_exact(2, 1), 2)
-        assert_allclose(permutations_exact(3, 1), 3)
-        assert_allclose(permutations_exact(4, 1), 4)
-        assert_allclose(permutations_exact(4, 2), 12)
-        assert_allclose(permutations_exact(4, 3), 24)
-        assert_allclose(permutations_exact(4, 4), 24)
-        assert_allclose(permutations_exact(300, 100) / 3.8807387193009318e239, 1.0)
-
-    def test_ln_permutations(self):
-        """ln_permutations should return expected results"""
-        assert_allclose(ln_permutations(1, 1), math.log(1))
-        assert_allclose(ln_permutations(2, 1), math.log(2))
-        assert_allclose(ln_permutations(3, 1.0), math.log(3))
-        assert_allclose(ln_permutations(4, 1), math.log(4))
-        assert_allclose(ln_permutations(4.0, 2), math.log(12))
-        assert_allclose(ln_permutations(4, 3.0), math.log(24))
-        assert_allclose(ln_permutations(4, 4), math.log(24))
-        assert_allclose(ln_permutations(300.0, 100), math.log(3.8807387193009318e239))
-
     def test_combinations(self):
         """combinations should return expected results when int as input"""
         self.assertEqual(combinations(1, 1), 1)
diff --git a/tests/test_util/test_warning.py b/tests/test_util/test_warning.py
index 9363fca25..e4dbe916d 100644
--- a/tests/test_util/test_warning.py
+++ b/tests/test_util/test_warning.py
@@ -227,14 +227,3 @@ def test_function_deprecated_args_deprecated_callable_chained_decorators(recwarn
     assert any("argument x which will be removed" in warning for warning in warnings)
     assert any("argument b is discontinued" in warning for warning in warnings)
     assert any("function changed is discontinued" in warning for warning in warnings)
-
-
-def test_class_deprecated(recwarn):
-    class fooclass:
-        @deprecated_callable("2023.6", "Improved change function", is_discontinued=True)
-        def __init__(self):
-            ...
-
-    fooclass()
-    warnings = [warning.message.args[0] for warning in recwarn.list]
-    assert any("fooclass is discontinued" in warning for warning in warnings)