Repository: aeon-toolkit/aeon
Pull request #2808 (2025-05-15 15:13:43)

[MNT] Lower bound keras to discard old bugged versions of early keras3
The beginning of keras3 had lots of bugs, logical because of the huge change, but i prefer adding a lower bound, as the more i use older version than 3.6.0 the more i notice bugs that can lead to false research results

Example:

with keras 3.0 it discards the "trainable" boolean of a tensorflow operation, making for instance LITE work differently (unreported issue).

This is to archive for future references

hadifawaz1999: > is 3.6 the latest version? Don't mind the change but if there are some previous non-buggy versions would be nice to allow those

the choice for 3.6 is because 3.4 and 3.5 had bugs with saving and loading (if you recall about this, we added an upper bound back then) so 3.6 seems the reasonable choice for me, its also the one i use locally so i dont seem to have issues with it

Patch:
diff --git a/aeon/classification/sklearn/_rotation_forest_classifier.py b/aeon/classification/sklearn/_rotation_forest_classifier.py
index f9a77d0f..e89776d0 100644
--- a/aeon/classification/sklearn/_rotation_forest_classifier.py
+++ b/aeon/classification/sklearn/_rotation_forest_classifier.py
@@ -327,6 +327,10 @@ class RotationForestClassifier(ClassifierMixin, BaseEstimator):
         # remove useless attributes
         self._useful_atts = ~np.all(X[1:] == X[:-1], axis=0)
         X = X[:, self._useful_atts]
+        if sum(self._useful_atts) == 0:
+            raise ValueError(
+                "All attributes in X contain the same value.",
+            )
 
         self._n_atts = X.shape[1]
 
diff --git a/aeon/regression/sklearn/_rotation_forest_regressor.py b/aeon/regression/sklearn/_rotation_forest_regressor.py
index b4ff8e68..e1871817 100644
--- a/aeon/regression/sklearn/_rotation_forest_regressor.py
+++ b/aeon/regression/sklearn/_rotation_forest_regressor.py
@@ -241,6 +241,10 @@ class RotationForestRegressor(RegressorMixin, BaseEstimator):
         # remove useless attributes
         self._useful_atts = ~np.all(X[1:] == X[:-1], axis=0)
         X = X[:, self._useful_atts]
+        if sum(self._useful_atts) == 0:
+            raise ValueError(
+                "All attributes in X contain the same value.",
+            )
 
         self._n_atts = X.shape[1]
 
diff --git a/aeon/segmentation/_eagglo.py b/aeon/segmentation/_eagglo.py
index 74a585ed..e481ab95 100644
--- a/aeon/segmentation/_eagglo.py
+++ b/aeon/segmentation/_eagglo.py
@@ -31,25 +31,25 @@ class EAggloSegmenter(BaseSegmenter):
     ----------
     member : array_like (default=None)
         Assigns points to the initial cluster membership, therefore the first
-        dimension should be the same as for data. If `None` it will be initialized
+        dimension should be the same as for data. If ``None`` it will be initialized
         to dummy vector where each point is assigned to separate cluster.
     alpha : float (default=1.0)
         Fixed constant alpha in (0, 2] used in the divergence measure, as the
         alpha-th absolute moment, see equation (4) in [1]_.
     penalty : str or callable or None (default=None)
         Function that defines a penalization of the sequence of goodness-of-fit
-        statistic, when overfitting is a concern. If `None` not penalty is applied.
-        Could also be an existing penalty name, either `len_penalty` or
-        `mean_diff_penalty`.
+        statistic, when overfitting is a concern. If ``None`` not penalty is applied.
+        Could also be an existing penalty name, either ``len_penalty`` or
+        ``mean_diff_penalty``.
 
     Attributes
     ----------
     merged_ : array_like
-        2D `array_like` outlining which clusters were merged_ at each step.
+        2D ``array_like`` outlining which clusters were merged_ at each step.
     gof_ : float
         goodness-of-fit statistic for current clsutering.
     cluster_ : array_like
-        1D `array_like` specifying which cluster each row of input data
+        1D ``array_like`` specifying which cluster each row of input data
         X belongs to.
 
     Notes
diff --git a/aeon/segmentation/_hmm.py b/aeon/segmentation/_hmm.py
index 6b829603..483ef0c9 100644
--- a/aeon/segmentation/_hmm.py
+++ b/aeon/segmentation/_hmm.py
@@ -50,7 +50,7 @@ class HMMSegmenter(BaseSegmenter):
     calculated - these are both nxm matrices, where n is the number of
     hidden states and m is the number of observations. The transition
     probability matrices record the probability of the most likely
-    sequence which has observation `m` being assigned to hidden state n.
+    sequence which has observation ``m`` being assigned to hidden state n.
     The transition_id matrix records the step before hidden state n that
     proceeds it in the most likely path.  This logic is mostly carried
     out by helper function _calculate_trans_mats.
@@ -76,7 +76,7 @@ class HMMSegmenter(BaseSegmenter):
         probability of transitioning from state i to state j.)
     initial_probs: 1D np.ndarray, shape = [num hidden states], optional
         A array of probabilities that the sequence of hidden states starts in each
-        of the hidden states. If passed, should be of length `n` the number of
+        of the hidden states. If passed, should be of length ``n`` the number of
         hidden states and  should match the length of both the emission funcs
         list and the transition_prob_mat. The initial probs should be reflective
         of prior beliefs.  If none is passed will each hidden state will be
diff --git a/aeon/segmentation/_igts.py b/aeon/segmentation/_igts.py
index b59bbcfb..1bd673b3 100644
--- a/aeon/segmentation/_igts.py
+++ b/aeon/segmentation/_igts.py
@@ -279,7 +279,7 @@ class InformationGainSegmenter(BaseSegmenter):
 
     GTS uses top-down search method to greedily find the next change point
     location that creates the maximum information gain. Once this is found, it
-    repeats the process until it finds `k_max` splits of the time series.
+    repeats the process until it finds ``k_max`` splits of the time series.
 
     .. note::
 
@@ -295,8 +295,8 @@ class InformationGainSegmenter(BaseSegmenter):
 
     step: : int, default=5
         Step size, or stride for selecting candidate locations of change points.
-        Fox example a `step=5` would produce candidates [0, 5, 10, ...]. Has the same
-        meaning as `step` in `range` function.
+        Fox example a ``step=5`` would produce candidates [0, 5, 10, ...]. Has the same
+        meaning as ``step`` in ``range`` function.
 
     Attributes
     ----------
@@ -304,7 +304,7 @@ class InformationGainSegmenter(BaseSegmenter):
         Locations of change points as integer indexes. By convention change points
         include the identity segmentation, i.e. first and last index + 1 values.
 
-    intermediate_results_: list of `ChangePointResult`
+    intermediate_results_: list of ``ChangePointResult``
         Intermediate segmentation results for each k value, where k=1, 2, ..., k_max
 
     Notes
diff --git a/pyproject.toml b/pyproject.toml
index 06408e7b..4c4981a9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -70,12 +70,14 @@ all_extras = [
     "statsmodels>=0.12.1",
     "stumpy>=1.5.1",
     "tensorflow>=2.14; python_version < '3.13'",
+    "keras>=3.6.0; python_version < '3.13'",
     "torch>=1.13.1",
     "tsfresh>=0.20.0",
     "tslearn>=0.5.2",
 ]
 dl = [
     "tensorflow>=2.14; python_version < '3.13'",
+    "keras>=3.6.0; python_version < '3.13'",
 ]
 unstable_extras = [
     # requires gcc and fftw to be installed for Windows and some other OS (see http://www.fftw.org/index.html)


Test patch:
diff --git a/aeon/classification/sklearn/tests/test_rotation_forest_classifier.py b/aeon/classification/sklearn/tests/test_rotation_forest_classifier.py
index bbaea821..07549e2f 100644
--- a/aeon/classification/sklearn/tests/test_rotation_forest_classifier.py
+++ b/aeon/classification/sklearn/tests/test_rotation_forest_classifier.py
@@ -6,7 +6,6 @@ from sklearn.metrics import accuracy_score
 
 from aeon.classification.sklearn import RotationForestClassifier
 from aeon.datasets import load_unit_test
-from aeon.testing.data_generation import make_example_3d_numpy
 
 
 def test_rotf_output():
@@ -88,8 +87,10 @@ def test_rotf_fit_predict():
 def test_rotf_input():
     """Test RotF with incorrect input."""
     rotf = RotationForestClassifier()
-    X2 = rotf._check_X(np.random.random((10, 1, 100)))
-    assert X2.shape == (10, 100)
+
+    X = rotf._check_X(np.random.random((10, 1, 100)))
+    assert X.shape == (10, 100)
+
     with pytest.raises(
         ValueError, match="RotationForestClassifier is not a time series classifier"
     ):
@@ -98,4 +99,10 @@ def test_rotf_input():
         ValueError, match="RotationForestClassifier is not a time series classifier"
     ):
         rotf._check_X([[1, 2, 3], [4, 5], [6, 7, 8]])
-    X, y = make_example_3d_numpy()
+
+    X2 = np.zeros((10, 10))
+    y = np.zeros(10)
+    y[0:5] = 1
+
+    with pytest.raises(ValueError, match="All attributes in X contain the same value."):
+        rotf.fit_predict(X2, y)
diff --git a/aeon/regression/sklearn/tests/test_rotation_forest_regressor.py b/aeon/regression/sklearn/tests/test_rotation_forest_regressor.py
index 9a89bbcb..d089e363 100644
--- a/aeon/regression/sklearn/tests/test_rotation_forest_regressor.py
+++ b/aeon/regression/sklearn/tests/test_rotation_forest_regressor.py
@@ -3,6 +3,7 @@
 __maintainer__ = ["MatthewMiddlehurst"]
 
 import numpy as np
+import pytest
 from sklearn.metrics import mean_squared_error
 from sklearn.tree import DecisionTreeRegressor
 
@@ -84,3 +85,27 @@ def test_rotf_fit_predict():
     y_pred = rotf.predict(X_train)
     assert isinstance(y_pred, np.ndarray)
     assert len(y_pred) == len(y_train)
+
+
+def test_rotf_input():
+    """Test RotF with incorrect input."""
+    rotf = RotationForestRegressor()
+
+    X = rotf._check_X(np.random.random((10, 1, 100)))
+    assert X.shape == (10, 100)
+
+    with pytest.raises(
+        ValueError, match="RotationForestRegressor is not a time series regressor"
+    ):
+        rotf._check_X(np.random.random((10, 10, 100)))
+    with pytest.raises(
+        ValueError, match="RotationForestRegressor is not a time series regressor"
+    ):
+        rotf._check_X([[1, 2, 3], [4, 5], [6, 7, 8]])
+
+    X2 = np.zeros((10, 10))
+    y = np.zeros(10)
+    y[0:5] = 1
+
+    with pytest.raises(ValueError, match="All attributes in X contain the same value."):
+        rotf.fit_predict(X2, y)