Repository: aeon-toolkit/aeon (python)

Issue:
[BUG] MockUnivariateForecasterLogger fails test_non_state_changing_method_contract
### Describe the bug

since refactoring the MockUnivariateForecasterLogger fails the state changing method

https://github.com/aeon-toolkit/aeon/actions/runs/7564190592/job/20597915895

I dont know why it did not fail before, possibly some weird exclusion somewhere. Its caused I think by this code changing the state of _MockEstimatorMixin__log

```python
   def add_log_item(self, value):
        """Append an item to the log.

        State change:
        self.log - `value` is appended to the list self.log

        Parameters
        ----------
        value : any object
        """
        if not hasattr(self, "_MockEstimatorMixin__log"):
            self._MockEstimatorMixin__log = [value]
        else:
            self._MockEstimatorMixin__log = self._MockEstimatorMixin__log + [value]
```

Unless someone else can figure out a better way of doing this, I think either exclude he MockUnivariateForecasterLogger  or remove it, doesnt seem to be used elsewhere. Any thoughts @ltsaprounis ?
 



### Steps/Code to reproduce the bug

https://github.com/aeon-toolkit/aeon/actions/runs/7564190592/job/20597915895

### Expected results

pass tests

### Actual results

fail tests

### Versions

_No response_

Pull request:
[BUG] unexclude mock forecaster
Fixes #1067 which no longer seems an issue 

Patch:
diff --git a/.github/workflows/github_maintenance.yml b/.github/workflows/github_maintenance.yml
deleted file mode 100644
index d706c309..00000000
--- a/.github/workflows/github_maintenance.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: GitHub Maintenance
-
-on:
-  schedule:
-    # every Monday at 01:00 AM UTC
-    - cron:  "0 1 * * 1"
-  workflow_dispatch:
-
-permissions:
-  issues: write
-  contents: write
-
-jobs:
-  stale_branches:
-    runs-on: ubuntu-20.04
-
-    steps:
-      - name: Create app token
-        uses: actions/create-github-app-token@v1
-        id: app-token
-        with:
-          app-id: ${{ vars.PR_APP_ID }}
-          private-key: ${{ secrets.PR_APP_KEY }}
-
-      - name: Stale Branches
-        uses: crs-k/stale-branches@v5.0.0
-        with:
-          repo-token: ${{ steps.app-token.outputs.token }}
-          days-before-stale: 140
-          days-before-delete: 9999  # todo take down to 175
-          comment-updates: true
-          tag-committer: true
-          stale-branch-label: "stale branch"
-          compare-branches: "info"
-          pr-check: true
diff --git a/.github/workflows/periodic_github_maintenace.yml b/.github/workflows/periodic_github_maintenace.yml
index c50ff2f6..ef5373f4 100644
--- a/.github/workflows/periodic_github_maintenace.yml
+++ b/.github/workflows/periodic_github_maintenace.yml
@@ -27,7 +27,7 @@ jobs:
         with:
           repo-token: ${{ steps.app-token.outputs.token }}
           days-before-stale: 140
-          days-before-delete: 9999  # todo take down to 175
+          days-before-delete: 175
           comment-updates: true
           tag-committer: true
           stale-branch-label: "stale branch"
diff --git a/aeon/anomaly_detection/__init__.py b/aeon/anomaly_detection/__init__.py
index edaf9ec2..ddbc2c12 100644
--- a/aeon/anomaly_detection/__init__.py
+++ b/aeon/anomaly_detection/__init__.py
@@ -1,9 +1,11 @@
 """Time Series Anomaly Detection."""
 
 __all__ = [
+    "DWT_MLEAD",
     "MERLIN",
     "STRAY",
 ]
 
+from aeon.anomaly_detection._dwt_mlead import DWT_MLEAD
 from aeon.anomaly_detection._merlin import MERLIN
 from aeon.anomaly_detection._stray import STRAY
diff --git a/aeon/anomaly_detection/_dwt_mlead.py b/aeon/anomaly_detection/_dwt_mlead.py
new file mode 100644
index 00000000..22c5d57c
--- /dev/null
+++ b/aeon/anomaly_detection/_dwt_mlead.py
@@ -0,0 +1,250 @@
+"""DWT-MLEAD anomaly detector."""
+
+__maintainer__ = ["CodeLionX"]
+__all__ = ["DWT_MLEAD"]
+
+import warnings
+from typing import Any, Iterable, List, Tuple
+
+import numpy as np
+from numpy.lib.stride_tricks import sliding_window_view
+from sklearn.covariance import EmpiricalCovariance
+
+from aeon.anomaly_detection.base import BaseAnomalyDetector
+from aeon.utils.numba.wavelets import multilevel_haar_transform
+
+
+def _pad_series(x: np.ndarray) -> Tuple[np.ndarray, int, int]:
+    """Pad input signal to the next power of 2 using periodic padding mode."""
+    n = x.shape[0]
+    exp = np.ceil(np.log2(n))
+    m = int(np.power(2, exp))
+    return np.pad(x, (0, m - n), mode="wrap"), n, m
+
+
+def _combine_alternating(xs: List[Any], ys: List[Any]) -> Iterable[Any]:
+    """Combine two lists by alternating their elements."""
+    for x, y in zip(xs, ys):
+        yield x
+        yield y
+
+
+class DWT_MLEAD(BaseAnomalyDetector):
+    """DWT-MLEAD anomaly detector.
+
+    DWT-MLEAD is an anomaly detection algorithm that uses the Discrete Wavelet Transform
+    (DWT) and Maximum Likelihood Estimation (MLE) to detect anomalies in univariate
+    time series. The algorithm performs mutli-level DWT using the Haar wavelet, slides
+    windows over the DWT coefficients, and estimates the likelihood of each window
+    using a Gaussian distribution. Anomalies are detected by comparing the likelihoods
+    to a quantile boundary in each level and passing down the anomaly counts to the
+    individual time points, which we use as anomaly scores. The original paper [1]_
+    subsequently clusters the anomalies to determine the anomaly centers. This step is
+    not implemented in this version.
+
+    Notes
+    -----
+    This implementation does not exactly match the original paper [1]_. We make the
+    following changes:
+
+    - We use window sizes for the DWT coefficients that decrease with the level number
+      because otherwise we would have too few items to slide the window over.
+    - We exclude the highest level coefficients because they contain only a single entry
+      and are, thus, not suitable for sliding a window of length 2 over it.
+    - We have not implemented the Monte Carlo quantile boundary type yet.
+    - We do not perform the anomaly clustering step to determine the anomaly centers.
+      Instead, we return the anomaly scores for each timestep in the original time
+      series.
+
+    Parameters
+    ----------
+    start_level : int, default=3
+        The level at which to start the anomaly detection. Must be >= 0 and less than
+        log_2(n_timepoints).
+    quantile_boundary_type : str, default='percentile'
+        The type of boundary to use for the quantile. Must be 'percentile',
+        'monte-carlo' is not implemented yet.
+    quantile_epsilon : float, default=0.01
+        The epsilon value for the quantile boundary. Must be in [0, 1].
+
+    References
+    ----------
+    .. [1] Thill, Markus, Wolfgang Konen, and Thomas Bäck. "Time Series Anomaly
+           Detection with Discrete Wavelet Transforms and Maximum Likelihood
+           Estimation." In Proceedings of the International Conference on Time Series
+           (ITISE). Granada, Spain, 2017.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from aeon.anomaly_detection import DWT_MLEAD
+    >>> X = np.array([1, 2, 3, 4, 1, 2, 3, 3, 2, 8, 9, 8, 1, 2, 3, 4], dtype=np.float_)
+    >>> detector = DWT_MLEAD(
+    ...    start_level=1, quantile_boundary_type='percentile', quantile_epsilon=0.01
+    ... )
+    >>> detector.fit_predict(X)
+    array([0., 0., 0., 0., 0., 0., 0., 0., 2., 2., 2., 2., 0., 0., 0., 0.])
+    """
+
+    _tags = {
+        "capability:univariate": True,
+        "capability:multivariate": False,
+        "capability:missing_values": False,
+    }
+
+    def __init__(
+        self,
+        start_level: int = 3,
+        quantile_boundary_type: str = "percentile",
+        quantile_epsilon: float = 0.01,
+    ):
+        self.start_level = start_level
+        self.quantile_boundary_type = quantile_boundary_type
+        self.quantile_epsilon = quantile_epsilon
+
+        super().__init__(axis=0)
+
+    def _predict(self, X) -> np.ndarray:
+        X = X.squeeze()
+
+        if self.start_level < 0:
+            raise ValueError("start_level must be >= 0")
+        if self.quantile_boundary_type != "percentile":
+            if self.quantile_boundary_type not in ["percentile", "monte-carlo"]:
+                raise ValueError(
+                    "quantile_boundary_type must be 'percentile' or 'monte-carlo', "
+                    f"but is {self.quantile_boundary_type}"
+                )
+            else:
+                raise NotImplementedError(
+                    f"The quantile boundary type '{self.quantile_boundary_type}' "
+                    "is not implemented yet!"
+                )
+        if self.quantile_epsilon < 0 or self.quantile_epsilon > 1:
+            raise ValueError("quantile_epsilon must be in [0, 1]")
+
+        X, self.N_, self.M_ = _pad_series(X)
+        max_level = int(np.log2(self.M_))
+
+        if self.start_level >= max_level:
+            raise ValueError(
+                f"start_level ({self.start_level}) must be less than "
+                f"log_2(n_timepoints) ({max_level})"
+            )
+
+        # perform multilevel DWT and capture coefficients
+        levels, approx_coeffs, detail_coeffs = self._multilevel_dwt(X, max_level)
+
+        # extract anomalies in each level
+        window_sizes = np.array(
+            [
+                max(2, max_level - level - self.start_level + 1)
+                for level in range(max_level)
+            ],
+            dtype=np.int_,
+        )
+        coef_anomaly_counts = []
+        for x, level in zip(
+            _combine_alternating(detail_coeffs, approx_coeffs), levels.repeat(2, axis=0)
+        ):
+            w = window_sizes[level]
+            windows = sliding_window_view(x, w)
+
+            p = self._estimate_gaussian_likelihoods(windows)
+            a = self._mark_anomalous_windows(p)
+            xa = self._reverse_windowing(a, window_length=w, full_length=x.shape[0])
+            coef_anomaly_counts.append(xa)
+
+        # aggregate anomaly counts (leaf counters)
+        point_anomaly_scores = self._push_anomaly_counts_down_to_points(
+            coef_anomaly_counts
+        )
+        return point_anomaly_scores
+
+    def _multilevel_dwt(
+        self, X: np.ndarray, max_level: int
+    ) -> Tuple[np.ndarray, List[np.ndarray], List[np.ndarray]]:
+        ls_ = np.arange(self.start_level - 1, max_level - 1, dtype=np.int_) + 1
+        as_, ds_ = multilevel_haar_transform(X, max_level - 1)
+        as_ = as_[self.start_level :]
+        ds_ = ds_[self.start_level - 1 :]
+        return ls_, as_, ds_
+
+    @staticmethod
+    def _estimate_gaussian_likelihoods(x_windows: np.ndarray) -> np.ndarray:
+        with warnings.catch_warnings():
+            warnings.filterwarnings(action="ignore", category=UserWarning)
+
+            # fit gaussion distribution with mean and covariance
+            estimator = EmpiricalCovariance(assume_centered=False)
+            estimator.fit(x_windows)
+
+            # compute log likelihood for each window x in x_view
+            n_windows = x_windows.shape[0]
+            p = np.empty(shape=n_windows)
+            for i in range(n_windows):
+                p[i] = estimator.score(x_windows[i].reshape(1, -1))
+        return p
+
+    def _mark_anomalous_windows(self, p: np.ndarray) -> np.ndarray:
+        if self.quantile_boundary_type == "percentile":
+            z_eps = np.percentile(p, self.quantile_epsilon * 100)
+        else:  # self.quantile_boundary_type == "monte-carlo"
+            raise ValueError(
+                f"The quantile boundary type '{self.quantile_boundary_type}' "
+                "is not implemented yet!"
+            )
+
+        return p < z_eps
+
+    @staticmethod
+    def _reverse_windowing(
+        x: np.ndarray, window_length: int, full_length: int
+    ) -> np.ndarray:
+        mapped = np.full(shape=(full_length, window_length), fill_value=0)
+        mapped[: x.shape[0], 0] = x
+
+        for w in range(1, window_length):
+            mapped[:, w] = np.roll(mapped[:, 0], w)
+
+        return np.sum(mapped, axis=1)
+
+    def _push_anomaly_counts_down_to_points(
+        self, coef_anomaly_counts: List[np.ndarray]
+    ) -> np.ndarray:
+        # sum up counters of detail coeffs (orig. D^l) and approx coeffs (orig. C^l)
+        anomaly_counts = coef_anomaly_counts[0::2] + coef_anomaly_counts[1::2]
+
+        # extrapolate anomaly counts to the original series' points
+        counter = np.zeros(self.M_)
+        for ac in anomaly_counts:
+            counter += ac.repeat(self.M_ // ac.shape[0], axis=0)
+        # set event counters with count < 2 to 0
+        counter[counter < 2] = 0
+        return counter[: self.N_]
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Only supports 'default'-parameter set.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+
+        Returns
+        -------
+        params : dict or list of dict, default={}
+            Parameters to create testing instances of the class.
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`.
+        """
+        return {
+            "start_level": 2,
+            "quantile_boundary_type": "percentile",
+            "quantile_epsilon": 0.01,
+        }
diff --git a/aeon/distances/_lcss.py b/aeon/distances/_lcss.py
index e415442f..f2cc07d6 100644
--- a/aeon/distances/_lcss.py
+++ b/aeon/distances/_lcss.py
@@ -194,7 +194,10 @@ def _lcss_distance(
     x: np.ndarray, y: np.ndarray, bounding_matrix: np.ndarray, epsilon: float
 ) -> float:
     distance = _lcss_cost_matrix(x, y, bounding_matrix, epsilon)[x.shape[1], y.shape[1]]
-    return 1 - (float(distance / min(x.shape[1], y.shape[1])))
+    distance = 1 - (float(distance / min(x.shape[1], y.shape[1])))
+    if distance < 0.0:
+        return 0.0
+    return distance
 
 
 @njit(cache=True, fastmath=True)
diff --git a/aeon/networks/__init__.py b/aeon/networks/__init__.py
index 946dc9bd..28e68570 100644
--- a/aeon/networks/__init__.py
+++ b/aeon/networks/__init__.py
@@ -2,6 +2,7 @@
 
 __all__ = [
     "BaseDeepNetwork",
+    "BaseDeepLearningNetwork",
     "CNNNetwork",
     "EncoderNetwork",
     "FCNNetwork",
@@ -23,4 +24,4 @@ from aeon.networks._lite import LITENetwork
 from aeon.networks._mlp import MLPNetwork
 from aeon.networks._resnet import ResNetNetwork
 from aeon.networks._tapnet import TapNetNetwork
-from aeon.networks.base import BaseDeepNetwork
+from aeon.networks.base import BaseDeepLearningNetwork, BaseDeepNetwork
diff --git a/aeon/networks/_ae_fcn.py b/aeon/networks/_ae_fcn.py
index 6e1dffa0..8693953c 100644
--- a/aeon/networks/_ae_fcn.py
+++ b/aeon/networks/_ae_fcn.py
@@ -4,10 +4,10 @@ __maintainer__ = []
 
 import numpy as np
 
-from aeon.networks.base import BaseDeepNetwork
+from aeon.networks.base import BaseDeepLearningNetwork
 
 
-class AEFCNNetwork(BaseDeepNetwork):
+class AEFCNNetwork(BaseDeepLearningNetwork):
     """Establish the network structure for a AE-FCN.
 
     Auto-Encoder based Fully Convolutional Netwwork (AE-FCN),
diff --git a/aeon/networks/_ae_resnet.py b/aeon/networks/_ae_resnet.py
index 7ec1c6fe..e66cec45 100644
--- a/aeon/networks/_ae_resnet.py
+++ b/aeon/networks/_ae_resnet.py
@@ -2,10 +2,10 @@
 
 import numpy as np
 
-from aeon.networks.base import BaseDeepNetwork
+from aeon.networks.base import BaseDeepLearningNetwork
 
 
-class AEResNetNetwork(BaseDeepNetwork):
+class AEResNetNetwork(BaseDeepLearningNetwork):
     """
     Establish the network structure for a AE-ResNet.
 
diff --git a/aeon/networks/_cnn.py b/aeon/networks/_cnn.py
index ca618e8b..cb40b8f3 100644
--- a/aeon/networks/_cnn.py
+++ b/aeon/networks/_cnn.py
@@ -2,10 +2,10 @@
 
 __maintainer__ = []
 
-from aeon.networks.base import BaseDeepNetwork
+from aeon.networks.base import BaseDeepLearningNetwork
 
 
-class CNNNetwork(BaseDeepNetwork):
+class CNNNetwork(BaseDeepLearningNetwork):
     """Establish the network structure for a CNN.
 
     Adapted from the implementation used in [1]_.
diff --git a/aeon/networks/_encoder.py b/aeon/networks/_encoder.py
index 459b1811..cc8dd072 100644
--- a/aeon/networks/_encoder.py
+++ b/aeon/networks/_encoder.py
@@ -2,10 +2,10 @@
 
 __maintainer__ = []
 
-from aeon.networks.base import BaseDeepNetwork
+from aeon.networks.base import BaseDeepLearningNetwork
 
 
-class EncoderNetwork(BaseDeepNetwork):
+class EncoderNetwork(BaseDeepLearningNetwork):
     """Establish the network structure for an Encoder.
 
     Adapted from the implementation used in [1]
diff --git a/aeon/networks/_fcn.py b/aeon/networks/_fcn.py
index 5969f519..9b6c10de 100644
--- a/aeon/networks/_fcn.py
+++ b/aeon/networks/_fcn.py
@@ -2,10 +2,10 @@
 
 __maintainer__ = []
 
-from aeon.networks.base import BaseDeepNetwork
+from aeon.networks.base import BaseDeepLearningNetwork
 
 
-class FCNNetwork(BaseDeepNetwork):
+class FCNNetwork(BaseDeepLearningNetwork):
     """
     Establish the network structure for a FCN.
 
diff --git a/aeon/networks/_inception.py b/aeon/networks/_inception.py
index 0f19d8cf..1b209dd8 100644
--- a/aeon/networks/_inception.py
+++ b/aeon/networks/_inception.py
@@ -2,10 +2,10 @@
 
 __maintainer__ = []
 
-from aeon.networks.base import BaseDeepNetwork
+from aeon.networks.base import BaseDeepLearningNetwork
 
 
-class InceptionNetwork(BaseDeepNetwork):
+class InceptionNetwork(BaseDeepLearningNetwork):
     """Inception Network.
 
     Parameters
diff --git a/aeon/networks/_lite.py b/aeon/networks/_lite.py
index 2c04aa8f..64a5961a 100644
--- a/aeon/networks/_lite.py
+++ b/aeon/networks/_lite.py
@@ -2,10 +2,10 @@
 
 __maintainer__ = []
 
-from aeon.networks.base import BaseDeepNetwork
+from aeon.networks.base import BaseDeepLearningNetwork
 
 
-class LITENetwork(BaseDeepNetwork):
+class LITENetwork(BaseDeepLearningNetwork):
     """LITE Network.
 
     LITE deep neural network architecture from [1]_.
@@ -36,8 +36,6 @@ class LITENetwork(BaseDeepNetwork):
     https://github.com/MSD-IRIMAS/LITE
     """
 
-    _tags = {"python_dependencies": "tensorflow"}
-
     def __init__(
         self,
         n_filters=32,
diff --git a/aeon/networks/_mlp.py b/aeon/networks/_mlp.py
index 43a59cd2..537fadce 100644
--- a/aeon/networks/_mlp.py
+++ b/aeon/networks/_mlp.py
@@ -2,10 +2,10 @@
 
 __maintainer__ = []
 
-from aeon.networks.base import BaseDeepNetwork
+from aeon.networks.base import BaseDeepLearningNetwork
 
 
-class MLPNetwork(BaseDeepNetwork):
+class MLPNetwork(BaseDeepLearningNetwork):
     """Establish the network structure for a MLP.
 
     Adapted from the implementation used in [1]_
@@ -24,7 +24,6 @@ class MLPNetwork(BaseDeepNetwork):
     def __init__(
         self,
     ):
-
         super().__init__()
 
     def build_network(self, input_shape, **kwargs):
diff --git a/aeon/networks/_resnet.py b/aeon/networks/_resnet.py
index df93d70e..91aaf5b1 100644
--- a/aeon/networks/_resnet.py
+++ b/aeon/networks/_resnet.py
@@ -2,10 +2,10 @@
 
 __maintainer__ = []
 
-from aeon.networks.base import BaseDeepNetwork
+from aeon.networks.base import BaseDeepLearningNetwork
 
 
-class ResNetNetwork(BaseDeepNetwork):
+class ResNetNetwork(BaseDeepLearningNetwork):
     """
     Establish the network structure for a ResNet.
 
diff --git a/aeon/networks/_tapnet.py b/aeon/networks/_tapnet.py
index c8598526..4e06d942 100644
--- a/aeon/networks/_tapnet.py
+++ b/aeon/networks/_tapnet.py
@@ -7,10 +7,10 @@ import math
 
 import numpy as np
 
-from aeon.networks.base import BaseDeepNetwork
+from aeon.networks.base import BaseDeepLearningNetwork
 
 
-class TapNetNetwork(BaseDeepNetwork):
+class TapNetNetwork(BaseDeepLearningNetwork):
     """Establish Network structure for TapNet.
 
     Adapted from the implementation used in [1]
@@ -47,8 +47,6 @@ class TapNetNetwork(BaseDeepNetwork):
     34(4), 6845-6852, 2020
     """
 
-    _tags = {"python_dependencies": ["tensorflow", "keras_self_attention"]}
-
     def __init__(
         self,
         dropout=0.5,
@@ -79,7 +77,9 @@ class TapNetNetwork(BaseDeepNetwork):
         self.use_rp = use_rp
         self.rp_params = rp_params
 
-        super().__init__()
+        super().__init__(
+            soft_dependencies=["tensorflow", "keras_self_attention"],
+        )
 
     @staticmethod
     def output_conv_size(in_size, kernel_size, strides, padding):
diff --git a/aeon/networks/base.py b/aeon/networks/base.py
index 86aaaa80..4f09da11 100644
--- a/aeon/networks/base.py
+++ b/aeon/networks/base.py
@@ -4,10 +4,23 @@ __maintainer__ = []
 
 from abc import ABC, abstractmethod
 
+from deprecated.sphinx import deprecated
+
 from aeon.base import BaseObject
-from aeon.utils.validation._dependencies import _check_estimator_deps
+from aeon.utils.validation._dependencies import (
+    _check_estimator_deps,
+    _check_python_version,
+    _check_soft_dependencies,
+)
 
 
+# TODO: remove v0.11.0
+@deprecated(
+    version="0.10.0",
+    reason="BaseDeepNetwork will be removed in 0.11.0, use BaseDeepLearningNetwork "
+    "instead. The new class does not inherit from BaseObject.",
+    category=FutureWarning,
+)
 class BaseDeepNetwork(BaseObject, ABC):
     """Abstract base class for deep learning networks."""
 
@@ -35,3 +48,28 @@ class BaseDeepNetwork(BaseObject, ABC):
         output_layer : a keras layer
         """
         ...
+
+
+class BaseDeepLearningNetwork(ABC):
+    """Abstract base class for deep learning networks."""
+
+    def __init__(self, soft_dependencies="tensorflow", python_version="<3.12"):
+        _check_soft_dependencies(soft_dependencies)
+        _check_python_version(python_version)
+        super().__init__()
+
+    @abstractmethod
+    def build_network(self, input_shape, **kwargs):
+        """Construct a network and return its input and output layers.
+
+        Parameters
+        ----------
+        input_shape : tuple
+            The shape of the data fed into the input layer
+
+        Returns
+        -------
+        input_layer : a keras layer
+        output_layer : a keras layer
+        """
+        ...
diff --git a/aeon/registry/_base_classes.py b/aeon/registry/_base_classes.py
index 8b2323b2..dfc5dd61 100644
--- a/aeon/registry/_base_classes.py
+++ b/aeon/registry/_base_classes.py
@@ -44,7 +44,6 @@ from aeon.classification.early_classification import BaseEarlyClassifier
 from aeon.clustering.base import BaseClusterer
 from aeon.forecasting.base import BaseForecaster
 from aeon.forecasting.model_selection._split import BaseSplitter
-from aeon.networks.base import BaseDeepNetwork
 from aeon.performance_metrics.base import BaseMetric
 from aeon.regression.base import BaseRegressor
 from aeon.segmentation.base import BaseSegmenter
@@ -64,7 +63,6 @@ BASE_CLASS_REGISTER = [
     ("early_classifier", BaseEarlyClassifier, "early time series classifier"),
     ("forecaster", BaseForecaster, "forecaster"),
     ("metric", BaseMetric, "performance metric"),
-    ("network", BaseDeepNetwork, "deep learning network"),
     ("regressor", BaseRegressor, "regressor"),
     ("segmenter", BaseSegmenter, "segmenter"),
     ("series-estimator", BaseSeriesEstimator, "single series estimator"),
diff --git a/aeon/transformations/collection/convolution_based/_minirocket.py b/aeon/transformations/collection/convolution_based/_minirocket.py
index 451ab7d5..39fed798 100644
--- a/aeon/transformations/collection/convolution_based/_minirocket.py
+++ b/aeon/transformations/collection/convolution_based/_minirocket.py
@@ -92,8 +92,8 @@ class MiniRocket(BaseCollectionTransformer):
         random_state = (
             np.int32(self.random_state) if isinstance(self.random_state, int) else None
         )
-
-        X = X[:, 0, :].astype(np.float32)
+        X = X.squeeze()
+        X = X.astype(np.float32)
         _, n_timepoints = X.shape
         if n_timepoints < 9:
             raise ValueError(
@@ -118,7 +118,8 @@ class MiniRocket(BaseCollectionTransformer):
         -------
         pandas DataFrame, transformed features
         """
-        X = X[:, 0, :].astype(np.float32)
+        X = X.squeeze()
+        X = X.astype(np.float32)
 
         # change n_jobs dependend on value and existing cores
         prev_threads = get_num_threads()
diff --git a/aeon/transformations/collection/convolution_based/_multirocket.py b/aeon/transformations/collection/convolution_based/_multirocket.py
index 34afc45b..611233bb 100644
--- a/aeon/transformations/collection/convolution_based/_multirocket.py
+++ b/aeon/transformations/collection/convolution_based/_multirocket.py
@@ -112,8 +112,8 @@ class MultiRocket(BaseCollectionTransformer):
         -------
         self
         """
-        X = X.astype(np.float64)
         X = X.squeeze()
+        X = X.astype(np.float32)
         if self.normalise:
             X = (X - X.mean(axis=-1, keepdims=True)) / (
                 X.std(axis=-1, keepdims=True) + 1e-8
@@ -139,8 +139,8 @@ class MultiRocket(BaseCollectionTransformer):
         -------
         pandas DataFrame, transformed features
         """
-        X = X.astype(np.float64)
         X = X.squeeze()
+        X = X.astype(np.float32)
         if self.normalise:
             X = (X - X.mean(axis=-1, keepdims=True)) / (
                 X.std(axis=-1, keepdims=True) + 1e-8
@@ -191,7 +191,7 @@ class MultiRocket(BaseCollectionTransformer):
 
 
 @njit(
-    "float32[:,:](float64[:,:],float64[:,:],Tuple((int32[:],int32[:],float32[:])),"
+    "float32[:,:](float32[:,:],float32[:,:],Tuple((int32[:],int32[:],float32[:])),"
     "Tuple((int32[:],int32[:],float32[:])),int32)",
     fastmath=True,
     parallel=True,
@@ -725,7 +725,7 @@ def _transform(X, X1, parameters, parameters1, n_features_per_kernel):
 
 
 @njit(
-    "float32[:](float64[:,:],int32[:],int32[:],float32[:],optional(int64))",
+    "float32[:](float32[:,:],int32[:],int32[:],float32[:],optional(int64))",
     fastmath=True,
     parallel=False,
     cache=True,
diff --git a/aeon/transformations/collection/convolution_based/_multirocket_multivariate.py b/aeon/transformations/collection/convolution_based/_multirocket_multivariate.py
index 17364cb2..feac1322 100644
--- a/aeon/transformations/collection/convolution_based/_multirocket_multivariate.py
+++ b/aeon/transformations/collection/convolution_based/_multirocket_multivariate.py
@@ -128,7 +128,7 @@ class MultiRocketMultivariate(BaseCollectionTransformer):
             X = _X1
             del _X1
 
-        X = X.astype(np.float64)
+        X = X.astype(np.float32)
 
         self.parameter = self._get_parameter(X)
         _X1 = np.diff(X, 1)
@@ -154,7 +154,7 @@ class MultiRocketMultivariate(BaseCollectionTransformer):
             X = (X - X.mean(axis=-1, keepdims=True)) / (
                 X.std(axis=-1, keepdims=True) + 1e-8
             )
-
+        X = X.astype(np.float32)
         _X1 = np.diff(X, 1)
 
         # change n_jobs dependend on value and existing cores
@@ -235,7 +235,7 @@ class MultiRocketMultivariate(BaseCollectionTransformer):
 
 
 @njit(
-    "float32[:](float64[:,:,:],int32[:],int32[:],int32[:],int32[:],float32[:],"
+    "float32[:](float32[:,:,:],int32[:],int32[:],int32[:],int32[:],float32[:],"
     "optional(int64))",
     fastmath=True,
     parallel=False,
@@ -633,7 +633,7 @@ def _quantiles(n):
 
 
 @njit(
-    "float32[:,:](float64[:,:,:],float64[:,:,:],"
+    "float32[:,:](float32[:,:,:],float32[:,:,:],"
     "Tuple((int32[:],int32[:],int32[:],int32[:],float32[:])),"
     "Tuple((int32[:],int32[:],int32[:],int32[:],float32[:])),int32)",
     fastmath=True,
diff --git a/aeon/transformations/series/base.py b/aeon/transformations/series/base.py
index b2a64ea4..83a0e112 100644
--- a/aeon/transformations/series/base.py
+++ b/aeon/transformations/series/base.py
@@ -11,6 +11,9 @@ fit & transform - fit_transform(self, X, y=None)
 from abc import ABCMeta, abstractmethod
 from typing import final
 
+import numpy as np
+import pandas as pd
+
 from aeon.base import BaseSeriesEstimator
 from aeon.transformations.base import BaseTransformer
 
@@ -76,7 +79,7 @@ class BaseSeriesTransformer(BaseSeriesEstimator, BaseTransformer, metaclass=ABCM
         return self
 
     @final
-    def transform(self, X, axis=1):
+    def transform(self, X, y=None, axis=1):
         """Transform X and return a transformed version.
 
         State required:
@@ -349,3 +352,12 @@ class BaseSeriesTransformer(BaseSeriesEstimator, BaseTransformer, metaclass=ABCM
             return Xt
         else:
             return Xt.T
+
+    def _check_y(self, y):
+        # Check y valid input for supervised transform
+        if not isinstance(y, (pd.Series, np.ndarray)):
+            raise TypeError(
+                f"y must be a np.array or a pd.Series, but found type: {type(y)}"
+            )
+        if isinstance(y, np.ndarray) and y.ndim > 1:
+            raise TypeError(f"y must be 1-dimensional, found {y.ndim} dimensions")
diff --git a/aeon/utils/validation/_dependencies.py b/aeon/utils/validation/_dependencies.py
index 4d58946b..594e3b2e 100644
--- a/aeon/utils/validation/_dependencies.py
+++ b/aeon/utils/validation/_dependencies.py
@@ -253,9 +253,14 @@ def _check_python_version(obj, package=None, msg=None, severity="error"):
         incompatible with the system python version. If package is given,
         error message gives package as the reason for incompatibility.
     """
-    est_specifier_tag = obj.get_class_tag("python_version", tag_value_default="None")
-    if est_specifier_tag in ["None", None]:
-        return True
+    if isinstance(obj, str):
+        est_specifier_tag = obj
+    else:
+        est_specifier_tag = obj.get_class_tag(
+            "python_version", tag_value_default="None"
+        )
+        if est_specifier_tag in ["None", None]:
+            return True
 
     try:
         est_specifier = SpecifierSet(est_specifier_tag)
diff --git a/build_tools/issue_assign.py b/build_tools/issue_assign.py
index 3f6d6ad0..0acc0025 100644
--- a/build_tools/issue_assign.py
+++ b/build_tools/issue_assign.py
@@ -21,10 +21,11 @@ issue = repo.get_issue(number=issue_number)
 comment_body = context_dict["event"]["comment"]["body"]
 
 # Assign tagged used to the issue if the comment includes the trigger phrase
-# and starts with a mention to the bot
-if comment_body.startswith("@aeon-actions-bot") and "assign" in comment_body.lower():
+body = comment_body.lower()
+if "@aeon-actions-bot" in body and "assign" in body:
     mentioned_users = re.findall(r"@[a-zA-Z0-9_-]+", comment_body)
-    mentioned_users = [user[1:] for user in mentioned_users[1:]]
+    mentioned_users = [user[1:] for user in mentioned_users]
+    mentioned_users.remove("aeon-actions-bot")
 
     for user in mentioned_users:
         issue.add_to_assignees(user)
diff --git a/docs/api_reference/anomaly_detection.rst b/docs/api_reference/anomaly_detection.rst
index 10d92e51..b5286be9 100644
--- a/docs/api_reference/anomaly_detection.rst
+++ b/docs/api_reference/anomaly_detection.rst
@@ -6,7 +6,7 @@ Anomaly Detection
 Time Series Anomaly Detection aims at discovering regions of a time series that in
 some way not representative of the underlying generative process.
 The :mod:`aeon.anomaly_detection` module contains algorithms and tools
-for time series segmentation.
+for time series anomaly detection.
 
 .. currentmodule:: aeon.anomaly_detection
 
@@ -15,3 +15,5 @@ for time series segmentation.
     :template: class.rst
 
     STRAY
+    MERLIN
+    DWT_MLEAD


Test patch:
diff --git a/aeon/anomaly_detection/tests/test_dwt_mlead.py b/aeon/anomaly_detection/tests/test_dwt_mlead.py
new file mode 100644
index 00000000..ba055bed
--- /dev/null
+++ b/aeon/anomaly_detection/tests/test_dwt_mlead.py
@@ -0,0 +1,32 @@
+"""Tests for the DWT_MLEAD class."""
+
+__maintainer__ = ["CodeLionX"]
+
+import numpy as np
+import pytest
+
+from aeon.anomaly_detection import DWT_MLEAD
+from aeon.testing.utils.data_gen import make_series
+
+
+def test_dwt_mlead_output():
+    """Test DWT_MLEAD output."""
+    series = make_series(n_timepoints=100, return_numpy=True, random_state=42)
+    series[50:58] -= 5
+
+    ad = DWT_MLEAD(start_level=2)
+    pred = ad.predict(series)
+
+    assert pred.shape == (100,)
+    assert pred.dtype == np.float_
+    assert 50 <= np.argmax(pred) <= 58
+
+
+def test_dwt_mlead_monte_carlo_unimplemented():
+    """Test that the monte-carlo method is not implemented."""
+    ad = DWT_MLEAD(quantile_boundary_type="monte-carlo")
+    with pytest.raises(
+        NotImplementedError,
+        match=".*quantile boundary type 'monte-carlo' is not implemented.*",
+    ):
+        ad.predict(np.array([0.5, 1.0, 0.8]))
diff --git a/aeon/distances/tests/test_pairwise.py b/aeon/distances/tests/test_pairwise.py
index 6a7a29cd..64576b62 100644
--- a/aeon/distances/tests/test_pairwise.py
+++ b/aeon/distances/tests/test_pairwise.py
@@ -513,3 +513,18 @@ def test_single_to_multiple_distances(dist):
             dist["distance"],
             dist["pairwise_distance"],
         )
+
+
+@pytest.mark.parametrize("seed", [1, 10, 42, 52, 100])
+@pytest.mark.parametrize("dist", DISTANCES)
+def test_pairwise_distance_non_negative(dist, seed):
+    """Most estimators require distances to be non-negative."""
+    X = make_example_3d_numpy(
+        n_cases=5, n_channels=1, n_timepoints=10, random_state=seed, return_y=False
+    )
+    X2 = make_example_3d_numpy(
+        n_cases=10, n_channels=1, n_timepoints=10, random_state=seed + 1, return_y=False
+    )
+    pairwise = dist["pairwise_distance"]
+    Xt2 = pairwise(X2, X)
+    assert Xt2.min() >= 0, f"Distance {dist['name']} is negative"
diff --git a/aeon/networks/tests/test_all_networks.py b/aeon/networks/tests/test_all_networks.py
index b6485f0a..aa73e937 100644
--- a/aeon/networks/tests/test_all_networks.py
+++ b/aeon/networks/tests/test_all_networks.py
@@ -2,18 +2,11 @@
 
 import inspect
 
-import pytest
-
 from aeon import networks
-from aeon.utils.validation._dependencies import _check_soft_dependencies
 
 __maintainer__ = []
 
 
-@pytest.mark.skipif(
-    not _check_soft_dependencies(["tensorflow", "tensorflow_addons"], severity="none"),
-    reason="skip test if required soft dependency not available",
-)
 def test_all_networks_functionality():
     """Test the functionality of all networks."""
     network_classes = [
@@ -22,12 +15,18 @@ def test_all_networks_functionality():
     input_shape = (100, 2)
 
     for i in range(len(network_classes)):
-        if "BaseDeepNetwork" in str(network_classes[i]) or "AEFCNNetwork" in str(
-            network_classes[i]
+        if (
+            "BaseDeepNetwork" in str(network_classes[i])
+            or "BaseDeepLearningNetwork" in str(network_classes[i])
+            or "AEFCNNetwork" in str(network_classes[i])
+            or "EncoderNetwork" in str(network_classes[i])
         ):
             continue
 
-        my_network = network_classes[i]()
+        try:
+            my_network = network_classes[i]()
+        except ModuleNotFoundError:
+            continue
 
         input_layer, output_layer = my_network.build_network(input_shape=input_shape)
 
diff --git a/aeon/networks/tests/test_network_base.py b/aeon/networks/tests/test_network_base.py
index d44c65c1..f55e2953 100644
--- a/aeon/networks/tests/test_network_base.py
+++ b/aeon/networks/tests/test_network_base.py
@@ -2,14 +2,14 @@
 
 import pytest
 
-from aeon.networks.base import BaseDeepNetwork
+from aeon.networks.base import BaseDeepLearningNetwork
 from aeon.testing.utils.data_gen import make_example_3d_numpy
 from aeon.utils.validation._dependencies import _check_soft_dependencies
 
 __maintainer__ = []
 
 
-class DummyDeepNetwork(BaseDeepNetwork):
+class DummyDeepNetwork(BaseDeepLearningNetwork):
     """A Dummy Deep Network for testing empty base network class save utilities."""
 
     def __init__(self):
@@ -26,7 +26,7 @@ class DummyDeepNetwork(BaseDeepNetwork):
 
 
 @pytest.mark.skipif(
-    not _check_soft_dependencies(["tensorflow", "pydot"], severity="none"),
+    not _check_soft_dependencies("tensorflow", severity="none"),
     reason="skip test if required soft dependency not available",
 )
 def test_dummy_deep_network():
diff --git a/aeon/registry/tests/test_lookup.py b/aeon/registry/tests/test_lookup.py
index 078109ad..f7ab5efc 100644
--- a/aeon/registry/tests/test_lookup.py
+++ b/aeon/registry/tests/test_lookup.py
@@ -16,7 +16,6 @@ CLASSES_WITHOUT_TAGS = [
     "series-annotator",
     "object",
     "splitter",
-    "network",
     "collection-transformer",
     "collection-estimator",
     "series-estimator",
diff --git a/aeon/testing/test_config.py b/aeon/testing/test_config.py
index 94a487cf..0698840c 100644
--- a/aeon/testing/test_config.py
+++ b/aeon/testing/test_config.py
@@ -47,9 +47,7 @@ EXCLUDED_TESTS = {
     # test fails several variants of inversion, see
     # https://github.com/aeon-toolkit/aeon/issues/700
     "Differencer": ["test_transform_inverse_transform_equivalent"],
-    # Test fails, see https://github.com/aeon-toolkit/aeon/issues/1067
-    "MockUnivariateForecasterLogger": ["test_non_state_changing_method_contract"],
-    # has a keras fail, unknown reason
+    # has a keras fail, unknown reason, see #1387
     "LearningShapeletClassifier": ["test_fit_deterministic"],
 }
 
diff --git a/aeon/transformations/collection/convolution_based/tests/test_all_rockets.py b/aeon/transformations/collection/convolution_based/tests/test_all_rockets.py
new file mode 100644
index 00000000..355da741
--- /dev/null
+++ b/aeon/transformations/collection/convolution_based/tests/test_all_rockets.py
@@ -0,0 +1,40 @@
+"""Rocket test code."""
+
+import numpy as np
+
+from aeon.transformations.collection.convolution_based import (
+    MiniRocket,
+    MiniRocketMultivariate,
+    MultiRocket,
+    MultiRocketMultivariate,
+    Rocket,
+)
+
+
+def test_datatype_input():
+    """Test rocket variants accept all input types."""
+    uni_rockets = [
+        Rocket(num_kernels=100),
+        MiniRocket(num_kernels=100),
+        MultiRocket(num_kernels=100),
+    ]
+
+    shape = (10, 1, 20)
+    types = [np.float32, np.float64, np.int32, np.int64]
+    for r in uni_rockets:
+        for t in types:
+            X = np.random.rand(*shape).astype(t)
+            r.fit_transform(X)
+            r.fit(X)
+            r.transform(X)
+    multi_rockets = [
+        MiniRocketMultivariate(num_kernels=100),
+        MultiRocketMultivariate(num_kernels=100),
+    ]
+    shape = (10, 3, 20)
+    for r in multi_rockets:
+        for t in types:
+            X = np.random.rand(*shape).astype(t)
+            r.fit_transform(X)
+            r.fit(X)
+            r.transform(X)
diff --git a/aeon/transformations/series/tests/test_base.py b/aeon/transformations/series/tests/test_base.py
index 3320beba..76aed38f 100644
--- a/aeon/transformations/series/tests/test_base.py
+++ b/aeon/transformations/series/tests/test_base.py
@@ -43,8 +43,13 @@ def test_fit_transform_univariate():
     transformer = MockUnivariateSeriesTransformer()
     x2 = transformer.fit_transform(x1)
     assert x1.shape == x2.shape
+    x2 = transformer.fit_transform(x1, x2)
     x3 = np.array([[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]])
     x4 = transformer.fit_transform(x3)
     assert isinstance(x4, np.ndarray) and x4.ndim == 1
     with pytest.raises(ValueError, match="Multivariate data not supported"):
         transformer.fit_transform(np.random.rand(2, 10))
+    transformer.fit(x1, x2)
+    x3 = transformer.transform(x1, x2)
+    x4 = transformer.fit_transform(x1, x2)
+    np.testing.assert_array_almost_equal(x3, x4)