Repository: aeon-toolkit/aeon
Pull request #2377 (2024-11-21 18:08:53)

[MNT] Update and consolidate general estimator checks
Final part of [AEP 05](https://github.com/aeon-toolkit/aeon-admin/pull/10/files). As far as I know.

For the most part the all-estimator checks were mostly good, but could use some tidying. Some tests i merged to reduce `fit` calls and to reduce redundancy.

- `check_fit_updates_state` now also checks cloning, to avoid having to fit just to check clone resets `is_fitted`
- `check_set_params` had two tests essentially with a bit of redundancy, these have been merged
- `check_constructor` has the most confusing changes probably. It should mostly the same except all kw params are tested and all `callable` parameters are now allowed as defaults (matching sklearn)

aeon-actions-bot[bot]: 
## Thank you for contributing to `aeon`

I have added the following labels to this PR based on the title: **[ $\\color{#EC843A}{\\textsf{maintenance}}$ ]**.
I have added the following labels to this PR based on the changes made: **[ $\\color{#2C2F20}{\\textsf{testing}}$ ]**. Feel free to change these if they do not properly represent the PR.

The [Checks](https://github.com/aeon-toolkit/aeon/pull/2377/checks) tab will show the status of our automated tests. You can click on individual test runs in the tab or "Details" in the panel below to see more information if there is a failure.

If our `pre-commit` code quality check fails, any trivial fixes will automatically be pushed to your PR unless it is a draft.

Don't hesitate to ask questions on the `aeon` [Slack](https://join.slack.com/t/aeon-toolkit/shared_invite/zt-22vwvut29-HDpCu~7VBUozyfL_8j3dLA) channel if you have any.

### PR CI actions

These checkboxes will add labels to enable/disable CI functionality for this PR. This may not take effect immediately, and a new commit may be required to run the new configuration.

- [ ] Run `pre-commit` checks for all files
- [ ] Run `mypy` typecheck tests
- [ ] Run all `pytest` tests and configurations
- [ ] Run all notebook example tests
- [ ] Run numba-disabled `codecov` tests
- [ ] Stop automatic `pre-commit` fixes (always disabled for drafts)
- [ ] Disable numba cache loading
- [ ] Push an empty commit to re-run CI checks
    

Patch:
diff --git a/aeon/base/_base_collection.py b/aeon/base/_base_collection.py
index d3d298e8..490233b8 100644
--- a/aeon/base/_base_collection.py
+++ b/aeon/base/_base_collection.py
@@ -1,5 +1,7 @@
 """Base class for estimators that fit collections of time series."""
 
+import numpy as np
+
 from aeon.base._base import BaseAeonEstimator
 from aeon.utils.conversion import (
     convert_collection,
@@ -79,6 +81,8 @@ class BaseCollectionEstimator(BaseAeonEstimator):
         >>> X2.shape
         (10, 1, 20)
         """
+        if isinstance(X, list) and isinstance(X[0], np.ndarray):
+            X = self._reshape_np_list(X)
         meta = self._check_X(X)
         if len(self.metadata_) == 0 and store_metadata:
             self.metadata_ = meta
@@ -267,3 +271,20 @@ class BaseCollectionEstimator(BaseAeonEstimator):
             None if metadata["unequal_length"] else get_n_timepoints(X)
         )
         return metadata
+
+    @staticmethod
+    def _reshape_np_list(X):
+        """Reshape 1D numpy to be 2D."""
+        reshape = False
+        for x in X:
+            if x.ndim == 1:
+                reshape = True
+                break
+        if reshape:
+            X2 = []
+            for x in X:
+                if x.ndim == 1:
+                    x = x.reshape(1, -1)
+                X2.append(x)
+            return X2
+        return X
diff --git a/aeon/clustering/__init__.py b/aeon/clustering/__init__.py
index 084778ae..2eec5142 100644
--- a/aeon/clustering/__init__.py
+++ b/aeon/clustering/__init__.py
@@ -7,7 +7,6 @@ __all__ = [
     "TimeSeriesCLARANS",
     "TimeSeriesKMeans",
     "TimeSeriesKShape",
-    "TimeSeriesKShapes",
     "TimeSeriesKernelKMeans",
     "ElasticSOM",
     "KSpectralCentroid",
@@ -21,7 +20,6 @@ from aeon.clustering._k_means import TimeSeriesKMeans
 from aeon.clustering._k_medoids import TimeSeriesKMedoids
 from aeon.clustering._k_sc import KSpectralCentroid
 from aeon.clustering._k_shape import TimeSeriesKShape
-from aeon.clustering._k_shapes import TimeSeriesKShapes
 from aeon.clustering._kernel_k_means import TimeSeriesKernelKMeans
 from aeon.clustering.base import BaseClusterer
 from aeon.clustering.dummy import DummyClusterer
diff --git a/aeon/clustering/_k_means.py b/aeon/clustering/_k_means.py
index dce7cd00..e68d09e1 100644
--- a/aeon/clustering/_k_means.py
+++ b/aeon/clustering/_k_means.py
@@ -169,20 +169,8 @@ class TimeSeriesKMeans(BaseClusterer):
         averaging_method: Union[str, Callable[[np.ndarray], np.ndarray]] = "ba",
         distance_params: Optional[dict] = None,
         average_params: Optional[dict] = None,
-        init_algorithm: Optional[Union[str, np.ndarray]] = None,
     ):
         self.init = init
-        self.init_algorithm = init_algorithm
-        if init_algorithm is not None:
-            import warnings
-
-            warnings.warn(
-                "The 'init_algorithm' parameter is deprecated and will be "
-                "removed in a future. Version Use 'init' instead.",
-                DeprecationWarning,
-                stacklevel=2,
-            )
-            self.init = self.init_algorithm
         self.distance = distance
         self.n_init = n_init
         self.max_iter = max_iter
diff --git a/aeon/clustering/_k_shapes.py b/aeon/clustering/_k_shapes.py
deleted file mode 100644
index 41148a4f..00000000
--- a/aeon/clustering/_k_shapes.py
+++ /dev/null
@@ -1,183 +0,0 @@
-"""Time series kshapes."""
-
-from typing import Optional, Union
-
-import numpy as np
-from deprecated.sphinx import deprecated
-from numpy.random import RandomState
-
-from aeon.clustering.base import BaseClusterer
-
-
-# TODO: remove in v1.0.0
-@deprecated(
-    version="1.0.0",
-    reason="TimeSeriesKShapes class has been renamed to TimeSeriesKShape. "
-    "The TimeSeriesKShapes version will be removed in version 1.0.0.",
-    category=FutureWarning,
-)
-class TimeSeriesKShapes(BaseClusterer):
-    """Kshape algorithm: wrapper of the ``tslearn`` implementation.
-
-    Parameters
-    ----------
-    n_clusters: int, default=8
-        The number of clusters to form as well as the number of
-        centroids to generate.
-    init_algorithm: str or np.ndarray, default='random'
-        Method for initializing cluster centres. Any of the following are valid:
-        ['random']. Or a np.ndarray of shape (n_clusters, n_channels, n_timepoints)
-        and gives the initial cluster centres.
-    n_init: int, default=10
-        Number of times the k-means algorithm will be run with different
-        centroid seeds. The final result will be the best output of n_init
-        consecutive runs in terms of inertia.
-    max_iter: int, default=30
-        Maximum number of iterations of the k-means algorithm for a single
-        run.
-    tol: float, default=1e-4
-        Relative tolerance with regards to Frobenius norm of the difference
-        in the cluster centres of two consecutive iterations to declare
-        convergence.
-    verbose: bool, default=False
-        Verbosity mode.
-    random_state: int or np.random.RandomState instance or None, default=None
-        Determines random number generation for centroid initialization.
-
-    Attributes
-    ----------
-    labels_: np.ndarray (1d array of shape (n_cases,))
-        Labels that is the index each time series belongs to.
-    inertia_: float
-        Sum of squared distances of samples to their closest cluster centre, weighted by
-        the sample weights if provided.
-    n_iter_: int
-        Number of iterations run.
-
-    Examples
-    --------
-    >>> from aeon.clustering import TimeSeriesKShapes
-    >>> from aeon.datasets import load_basic_motions
-    >>> # Load data
-    >>> X_train, y_train = load_basic_motions(split="TRAIN")[0:10]
-    >>> X_test, y_test = load_basic_motions(split="TEST")[0:10]
-    >>> # Example of KShapes clustering
-    >>> ks = TimeSeriesKShapes(n_clusters=3, random_state=1)  # doctest: +SKIP
-    >>> ks.fit(X_train)  # doctest: +SKIP
-    TimeSeriesKShapes(n_clusters=3, random_state=1)
-    >>> preds = ks.predict(X_test)  # doctest: +SKIP
-    """
-
-    _tags = {
-        "capability:multivariate": True,
-        "python_dependencies": "tslearn",
-        "algorithm_type": "distance",
-    }
-
-    def __init__(
-        self,
-        n_clusters: int = 8,
-        init_algorithm: Union[str, np.ndarray] = "random",
-        n_init: int = 10,
-        max_iter: int = 300,
-        tol: float = 1e-4,
-        verbose: bool = False,
-        random_state: Optional[Union[int, RandomState]] = None,
-    ):
-        self.init_algorithm = init_algorithm
-        self.n_init = n_init
-        self.max_iter = max_iter
-        self.tol = tol
-        self.verbose = verbose
-        self.random_state = random_state
-        self.n_clusters = n_clusters
-
-        self.cluster_centers_ = None
-        self.labels_ = None
-        self.inertia_ = None
-        self.n_iter_ = 0
-
-        self._tslearn_k_shapes = None
-
-        super().__init__()
-
-    def _fit(self, X, y=None):
-        """Fit time series clusterer to training data.
-
-        Parameters
-        ----------
-        X: np.ndarray, of shape (n_cases, n_channels, n_timepoints) or
-                (n_cases, n_timepoints)
-            A collection of time series instances.
-        y: ignored, exists for API consistency reasons.
-
-        Returns
-        -------
-        self:
-            Fitted estimator.
-        """
-        from tslearn.clustering import KShape
-
-        self._tslearn_k_shapes = KShape(
-            n_clusters=self.n_clusters,
-            max_iter=self.max_iter,
-            tol=self.tol,
-            random_state=self.random_state,
-            n_init=self.n_init,
-            verbose=self.verbose,
-            init=self.init_algorithm,
-        )
-
-        _X = X.swapaxes(1, 2)
-
-        self._tslearn_k_shapes.fit(_X)
-        self._cluster_centers = self._tslearn_k_shapes.cluster_centers_
-        self.labels_ = self._tslearn_k_shapes.predict(_X)
-        self.inertia_ = self._tslearn_k_shapes.inertia_
-        self.n_iter_ = self._tslearn_k_shapes.n_iter_
-
-    def _predict(self, X, y=None) -> np.ndarray:
-        """Predict the closest cluster each sample in X belongs to.
-
-        Parameters
-        ----------
-        X: np.ndarray, of shape (n_cases, n_channels, n_timepoints) or
-                (n_cases, n_timepoints)
-            A collection of time series instances.
-        y: ignored, exists for API consistency reasons.
-
-        Returns
-        -------
-        np.ndarray (1d array of shape (n_cases,))
-            Index of the cluster each time series in X belongs to.
-        """
-        _X = X.swapaxes(1, 2)
-        return self._tslearn_k_shapes.predict(_X)
-
-    @classmethod
-    def _get_test_params(cls, parameter_set="default"):
-        """Return testing parameter settings for the estimator.
-
-        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.
-        """
-        return {
-            "n_clusters": 2,
-            "init_algorithm": "random",
-            "n_init": 1,
-            "max_iter": 1,
-            "tol": 1e-4,
-            "verbose": False,
-            "random_state": 1,
-        }
diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md
index b2ebb3e6..0c49c2b0 100644
--- a/docs/code_of_conduct.md
+++ b/docs/code_of_conduct.md
@@ -13,7 +13,7 @@ It is expected that community members will abide by the principles of the [Pytho
 Software Foundation Code of Conduct](https://www.python.org/psf/codeofconduct/) to
 help maintain an open, considerate and respectful environment.
 
-Unacceptable behavior and code of conduct violations are determined and dealt with
+Unacceptable behaviour and code of conduct violations are determined and dealt with
 at the discretion of the `aeon` [Code of Conduct Workgroup (COCW)](./governance.md#code-of-conduct-workgroup).
 Possible actions taken include warnings, mediated meeting and/or apologies, and
 expulsion from the project depending on the severity of the breach. The COCW can edit
diff --git a/docs/governance.md b/docs/governance.md
index fafee7cf..54b6558a 100644
--- a/docs/governance.md
+++ b/docs/governance.md
@@ -1,6 +1,6 @@
 # Governance
 
-The purpose of this document is to formalize the governance process used by the `aeon`
+The purpose of this document is to formalise the governance process used by the `aeon`
 project, to clarify how decisions are made and how the various elements of our community
 interact. Our goal is to ensure a transparent, democratic, and inclusive decision-making
 process that empowers all community members to contribute to the project.
@@ -109,15 +109,15 @@ necessary.
 Decisions about the future of the project are announced publicly to allow discussion
 with all members of the community. The whole process from proposal to implementation
 is fully visible, apart from topics considered sensitive. All non-sensitive project
-management discussion takes place on the contributors channel on the project
-Slack and/or the issue tracker. Occasionally, sensitive discussion and votes such as
-appointments will occur in private Slack channels.
+management discussion takes place on the project Slack and/or the issue tracker.
+Occasionally, sensitive discussion and votes such as appointments will occur in private
+Slack channels or meetings.
 
-For most decisions a consensus seeking process of all interested contributors is used.
+For most decisions, a consensus seeking process of all interested contributors is used.
 Contributors try to find a resolution that has no open objections among core developers.
 If a reasonable amount of time (at least 7-days for non-trivial changes) has passed
 since the last change to a proposed contribution, the proposal has at least one approval
-(+1) and no rejections (-1) from core developers, it can be approved by lazy consensus.
+(+1), and no rejections (-1) from core developers, it can be approved by lazy consensus.
 If a change is rejected, it is expected that an explanation and description of
 conditions (if any) to withdraw the rejection is provided.
 
@@ -137,15 +137,15 @@ emergencies where harm will come to the project unless timely action is taken.
 ### Enhancement Proposals
 
 For contentious decision-making votes (not including appointment votes), a proposal
-must have been made public for discussion before the vote. It is recommended that this
+should be made public for discussion before the vote. It is recommended that this
 proposal is made as a consolidated document, in the form of an “aeon Enhancement
 Proposal” (AEP). The AEP template is available
 [here](https://github.com/aeon-toolkit/aeon-admin/blob/main/aep/aep_template.md), but
 the use of said template is not a requirement. A detailed issue or pull request can
-substitute an AEP if all parties believe it is sufficient, but a more formal proposal
+substitute an AEP if all parties believe it is enough, but a more formal proposal
 can be requested by any core developer.
 
-Having a rejection on a pull request does not necessitate the creation of an AEP and
+Having a rejection on a pull request does not require the creation of an AEP and
 further discussion to find consensus can be held, but one must be created prior to any
 vote to bypass a rejection.
 
@@ -155,7 +155,7 @@ submitted to the community for discussion and comment.
 
 ## Acknowledgements
 
-Substantial portions of this document were adapted from or inspired by the following
+Significant portions of this document were adapted from or inspired by the following
 projects governance documents:
 
 - [Scikit-learn](https://scikit-learn.org/stable/governance.html)
diff --git a/docs/index.md b/docs/index.md
index 58950313..a1b8f0cb 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -4,9 +4,9 @@ hide-toc: true
 
 # Welcome to __aeon__
 
-`aeon` is a scikit-learn compatible toolkit for time series machine learning tasks
-such as anomaly detection, classification, clustering, forecasting, regression,
-segmentation and similarity search.</p>
+`aeon` is a `scikit-learn` compatible toolkit for time series machine learning tasks
+such as classification, regression, clustering, anomaly detection,
+segmentation and similarity search.
 
 - We provide a broad library of time series algorithms, including the latest
   advances and state-of-the-art for many tasks.
@@ -17,22 +17,6 @@ segmentation and similarity search.</p>
 - We provide a range of tools for reproducing benchmarking results and evaluating time
   series algorithms implemented in `aeon` and other `scikit-learn` compatible packages.
 
-```{admonition} Large scale changes for aeon v1.0.0
-We are currently working on v1.0.0 of aeon, which includes a number of large changes
-to better support the maintainability of the library and the direction the developer
-community wish to take the package.
-
-This includes the removal of current modules such as forecasting (to be reintroduced
-under a different interface), datatypes and the legacy BaseTransformer interface. We
-will also be making large changes to the base class API, removing `BaseObject` various
-functions associated with the base module currently.
-
-We hope these changes and the removal of long-untouched legacy code will allow aeon to
-grow and develop in a more sustainable way without the need for such large breaking
-changes in the future. Some of these changes will not come with a deprecation warning,
-so be wary of this when updating to v1.0.0 when it is released.
-```
-
 ## Community Channels
 
 **GitHub**: [github.com/aeon-toolkit/aeon](https://github.com/aeon-toolkit/aeon)
@@ -43,6 +27,8 @@ so be wary of this when updating to v1.0.0 when it is released.
 
 **LinkedIn**: [linkedin/aeon-toolkit](https://www.linkedin.com/company/aeon-toolkit)
 
+**Email**: [contact@aeon-toolkit.org](mailto:contact@aeon-toolkit.org)
+
 ## Modules
 
 ::::{grid} 1 2 2 2
@@ -129,7 +115,7 @@ Anomaly Detection
 :class-img-top: aeon-card-image
 :text-align: center
 
-Segmentation
+Get started with segmentation
 
 +++
 
@@ -186,7 +172,7 @@ Distances
 :class-img-top: aeon-card-image
 :text-align: center
 
-Similarity Search
+Get started with time series similarity search
 
 +++
 
@@ -259,6 +245,17 @@ Networks
 
 ::::
 
+## Experimental Modules
+
+Some modules of `aeon` are still experimental and may have changing interfaces.
+To support development on these modules, the [deprecation policy](developer_guide/deprecation.md)
+is relaxed, so it is suggested that you integrate these modules with care. The current
+experimental modules are:
+
+- `anomaly_detection`
+- `segmentation`
+- `similarity_search`
+- `visualisation`
 
 ```{toctree}
 :caption: Using aeon
diff --git a/examples/networks/deep_learning.ipynb b/examples/networks/deep_learning.ipynb
index 498498f5..9e06feab 100644
--- a/examples/networks/deep_learning.ipynb
+++ b/examples/networks/deep_learning.ipynb
@@ -62,12 +62,17 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 1,
-   "metadata": {},
-   "outputs": [],
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2024-11-21T11:14:08.477299Z",
+     "start_time": "2024-11-21T11:14:08.433390Z"
+    }
+   },
    "source": [
     "import os\n",
     "\n",
+    "from sklearn.cluster import KMeans\n",
+    "\n",
     "# avoid imports warning for tensorflow\n",
     "os.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"2\"\n",
     "\n",
@@ -90,7 +95,9 @@
     "from aeon.datasets import load_classification, load_regression\n",
     "from aeon.networks import InceptionNetwork\n",
     "from aeon.regression.deep_learning import InceptionTimeRegressor"
-   ]
+   ],
+   "outputs": [],
+   "execution_count": 7
   },
   {
    "attachments": {},
@@ -107,30 +114,12 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "2023-10-06 17:31:12.574639: E tensorflow/compiler/xla/stream_executor/cuda/cuda_driver.cc:266] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected\n"
-     ]
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "3/3 [==============================] - 0s 68ms/step\n",
-      "3/3 [==============================] - 0s 52ms/step\n",
-      "3/3 [==============================] - 0s 54ms/step\n",
-      "3/3 [==============================] - 0s 84ms/step\n",
-      "3/3 [==============================] - 0s 86ms/step\n",
-      "Predictions:  ['0' '0' '0' '0' '0']\n",
-      "Ground Truth:  ['0' '0' '0' '0' '0']\n"
-     ]
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2024-11-21T11:12:15.141664Z",
+     "start_time": "2024-11-21T11:12:02.084792Z"
     }
-   ],
+   },
    "source": [
     "xtrain, ytrain = load_classification(name=\"ArrowHead\", split=\"train\")\n",
     "xtest, ytest = load_classification(name=\"ArrowHead\", split=\"test\")\n",
@@ -141,7 +130,23 @@
     "\n",
     "print(\"Predictions: \", ypred[0:5])\n",
     "print(\"Ground Truth: \", ytest[0:5])"
-   ]
+   ],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\u001B[1m3/3\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 72ms/step\n",
+      "\u001B[1m3/3\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 78ms/step\n",
+      "\u001B[1m3/3\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 72ms/step\n",
+      "\u001B[1m3/3\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 74ms/step\n",
+      "\u001B[1m3/3\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 82ms/step\n",
+      "Predictions:  ['2' '2' '2' '2' '2']\n",
+      "Ground Truth:  ['0' '0' '0' '0' '0']\n"
+     ]
+    }
+   ],
+   "execution_count": 2
   },
   {
    "attachments": {},
@@ -163,23 +168,12 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "3/3 [==============================] - 0s 59ms/step\n",
-      "3/3 [==============================] - 0s 51ms/step\n",
-      "3/3 [==============================] - 0s 60ms/step\n",
-      "3/3 [==============================] - 0s 74ms/step\n",
-      "3/3 [==============================] - 0s 54ms/step\n",
-      "Predictions:  ['0' '0' '0' '0' '0']\n",
-      "Ground Truth:  ['0' '0' '0' '0' '0']\n"
-     ]
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2024-11-21T11:12:30.252190Z",
+     "start_time": "2024-11-21T11:12:15.222773Z"
     }
-   ],
+   },
    "source": [
     "xtrain, ytrain = load_classification(name=\"ArrowHead\", split=\"train\")\n",
     "xtest, ytest = load_classification(name=\"ArrowHead\", split=\"test\")\n",
@@ -190,7 +184,23 @@
     "\n",
     "print(\"Predictions: \", ypred[0:5])\n",
     "print(\"Ground Truth: \", ytest[0:5])"
-   ]
+   ],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\u001B[1m3/3\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 94ms/step\n",
+      "\u001B[1m3/3\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 91ms/step\n",
+      "\u001B[1m3/3\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 94ms/step\n",
+      "\u001B[1m3/3\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 97ms/step\n",
+      "\u001B[1m3/3\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 96ms/step\n",
+      "Predictions:  ['0' '0' '0' '0' '0']\n",
+      "Ground Truth:  ['0' '0' '0' '0' '0']\n"
+     ]
+    }
+   ],
+   "execution_count": 3
   },
   {
    "cell_type": "markdown",
@@ -205,23 +215,12 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "1/1 [==============================] - 0s 198ms/step\n",
-      "1/1 [==============================] - 0s 219ms/step\n",
-      "1/1 [==============================] - 0s 196ms/step\n",
-      "1/1 [==============================] - 0s 195ms/step\n",
-      "1/1 [==============================] - 0s 209ms/step\n",
-      "Predictions:  [-0.23141143 -0.0241483  -0.01730516 -0.34630919 -3.14292431]\n",
-      "Ground Truth:  [0.0118838  0.00379507 0.08298755 0.04510921 0.12783075]\n"
-     ]
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2024-11-21T11:12:43.183566Z",
+     "start_time": "2024-11-21T11:12:30.257417Z"
     }
-   ],
+   },
    "source": [
     "xtrain, ytrain = load_regression(name=\"Covid3Month\", split=\"train\")\n",
     "xtest, ytest = load_regression(name=\"Covid3Month\", split=\"test\")\n",
@@ -232,7 +231,23 @@
     "\n",
     "print(\"Predictions: \", ypred[0:5])\n",
     "print(\"Ground Truth: \", ytest[0:5])"
-   ]
+   ],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\u001B[1m1/1\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 107ms/step\n",
+      "\u001B[1m1/1\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 109ms/step\n",
+      "\u001B[1m1/1\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 112ms/step\n",
+      "\u001B[1m1/1\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 114ms/step\n",
+      "\u001B[1m1/1\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 108ms/step\n",
+      "Predictions:  [-0.4258549  -0.0387525  -0.01732254 -0.60533425 -4.51287463]\n",
+      "Ground Truth:  [0.0118838  0.00379507 0.08298755 0.04510921 0.12783075]\n"
+     ]
+    }
+   ],
+   "execution_count": 4
   },
   {
    "attachments": {},
@@ -257,30 +272,19 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "2/2 [==============================] - 0s 5ms/step\n",
-      "6/6 [==============================] - 0s 25ms/step\n",
-      "Predictions:  [0 1 0 0 0]\n",
-      "Ground Truth:  ['0' '0' '0' '0' '0']\n",
-      "\n",
-      "6/6 [==============================] - 0s 32ms/step\n",
-      "Score :  -701.7947322426963\n"
-     ]
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2024-11-21T11:14:19.577898Z",
+     "start_time": "2024-11-21T11:14:16.120345Z"
     }
-   ],
+   },
    "source": [
     "xtrain, _ = load_classification(name=\"ArrowHead\", split=\"train\")\n",
     "xtest, ytest = load_classification(name=\"ArrowHead\", split=\"test\")\n",
     "\n",
     "aefcn = AEFCNClusterer(\n",
     "    temporal_latent_space=False,\n",
-    "    clustering_algorithm=\"kmeans\",\n",
+    "    estimator=KMeans(n_clusters=3),\n",
     "    n_epochs=10,\n",
     ")\n",
     "\n",
@@ -288,7 +292,20 @@
     "ypred = aefcn.predict(X=xtest)\n",
     "print(\"Predictions: \", ypred[0:5])\n",
     "print(\"Ground Truth: \", ytest[0:5])"
-   ]
+   ],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\u001B[1m2/2\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 27ms/step\n",
+      "\u001B[1m6/6\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m0s\u001B[0m 8ms/step \n",
+      "Predictions:  [2 0 2 2 2]\n",
+      "Ground Truth:  ['0' '0' '0' '0' '0']\n"
+     ]
+    }
+   ],
+   "execution_count": 8
   },
   {
    "attachments": {},
@@ -727,6 +744,13 @@
     "- [7] Lafabregue, Baptiste, et al. \"End-to-end deep representation learning for time series clustering:\n",
     "    a comparative study.\" Data Mining and Knowledge Discovery 36.1 (2022): 29-81."
    ]
+  },
+  {
+   "metadata": {},
+   "cell_type": "code",
+   "outputs": [],
+   "execution_count": null,
+   "source": ""
   }
  ],
  "metadata": {


Test patch:
diff --git a/aeon/base/tests/test_base_collection.py b/aeon/base/tests/test_base_collection.py
index ee800996..9fd887eb 100644
--- a/aeon/base/tests/test_base_collection.py
+++ b/aeon/base/tests/test_base_collection.py
@@ -234,3 +234,24 @@ def test_preprocess_collection(data):
     meta = cls.metadata_
     cls._preprocess_collection(data2)
     assert meta == cls.metadata_
+
+
+def test_convert_np_list():
+    """Test np-list of 1D numpy converted to 2D."""
+    x1 = np.random.random(size=(1, 10))
+    x2 = np.random.rand(20)
+    x3 = np.random.rand(30)
+    np_list = [x1, x2, x3]
+    np2 = BaseCollectionEstimator._reshape_np_list(np_list)
+    assert len(np2) == len(np_list)
+    assert np2[0].shape == (1, 10)
+    assert np2[1].shape == (1, 20)
+    assert np2[2].shape == (1, 30)
+    dummy1 = BaseCollectionEstimator()
+    x1 = np.random.random(size=(1, 10))
+    x2 = np.random.rand(10)
+    x3 = np.random.rand(10)
+    np_list = [x1, x2, x3]
+    np3 = dummy1._preprocess_collection(np_list)
+    assert isinstance(np3, np.ndarray)
+    assert np3.shape == (3, 1, 10)
diff --git a/aeon/testing/estimator_checking/_estimator_checking.py b/aeon/testing/estimator_checking/_estimator_checking.py
index 2211e659..0ea0ebfb 100644
--- a/aeon/testing/estimator_checking/_estimator_checking.py
+++ b/aeon/testing/estimator_checking/_estimator_checking.py
@@ -184,8 +184,8 @@ def check_estimator(
     >>> results = check_estimator(MockClassifier())
 
     Running specific check for MockClassifier
-    >>> check_estimator(MockClassifier, checks_to_run="check_clone")
-    {'check_clone(estimator=MockClassifier())': 'PASSED'}
+    >>> check_estimator(MockClassifier, checks_to_run="check_get_params")
+    {'check_get_params(estimator=MockClassifier())': 'PASSED'}
     """
     # check if estimator has soft dependencies installed
     _check_estimator_deps(estimator)
diff --git a/aeon/testing/estimator_checking/_yield_classification_checks.py b/aeon/testing/estimator_checking/_yield_classification_checks.py
index 2828063f..582632b9 100644
--- a/aeon/testing/estimator_checking/_yield_classification_checks.py
+++ b/aeon/testing/estimator_checking/_yield_classification_checks.py
@@ -154,9 +154,6 @@ def check_classifier_overrides_and_tags(estimator_class):
                 f"Override _{method} instead."
             )
 
-    # axis class parameter is for internal use only
-    assert "axis" not in estimator_class.__dict__
-
     # Test valid tag for X_inner_type
     X_inner_type = estimator_class.get_class_tag(tag_name="X_inner_type")
     if isinstance(X_inner_type, str):
@@ -172,11 +169,6 @@ def check_classifier_overrides_and_tags(estimator_class):
         else:  # must be a list
             assert any([t in valid_unequal_types for t in X_inner_type])
 
-    # Must have at least one set to True
-    multi = estimator_class.get_class_tag(tag_name="capability:multivariate")
-    uni = estimator_class.get_class_tag(tag_name="capability:univariate")
-    assert multi or uni
-
     valid_algorithm_types = [
         "distance",
         "deeplearning",
diff --git a/aeon/testing/estimator_checking/_yield_estimator_checks.py b/aeon/testing/estimator_checking/_yield_estimator_checks.py
index 112fc96a..0078c6db 100644
--- a/aeon/testing/estimator_checking/_yield_estimator_checks.py
+++ b/aeon/testing/estimator_checking/_yield_estimator_checks.py
@@ -1,9 +1,8 @@
 """Tests for all estimators."""
 
-import inspect
+import math
 import numbers
 import pickle
-import types
 from copy import deepcopy
 from functools import partial
 from inspect import getfullargspec, isclass, signature
@@ -13,7 +12,6 @@ import numpy as np
 import pytest
 from numpy.testing import assert_array_almost_equal
 from sklearn.exceptions import NotFittedError
-from sklearn.utils.estimator_checks import check_get_params_invariance
 
 from aeon.anomaly_detection.base import BaseAnomalyDetector
 from aeon.base import BaseAeonEstimator
@@ -175,7 +173,7 @@ def _yield_estimator_checks(estimator_class, estimator_instances, datatypes):
     yield partial(check_create_test_instance, estimator_class=estimator_class)
     yield partial(check_inheritance, estimator_class=estimator_class)
     yield partial(check_has_common_interface, estimator_class=estimator_class)
-    yield partial(check_set_params_sklearn, estimator_class=estimator_class)
+    yield partial(check_set_params, estimator_class=estimator_class)
     yield partial(check_constructor, estimator_class=estimator_class)
     yield partial(check_estimator_class_tags, estimator_class=estimator_class)
 
@@ -183,8 +181,6 @@ def _yield_estimator_checks(estimator_class, estimator_instances, datatypes):
     for i, estimator in enumerate(estimator_instances):
         # no data needed
         yield partial(check_get_params, estimator=estimator)
-        yield partial(check_set_params, estimator=estimator)
-        yield partial(check_clone, estimator=estimator)
         yield partial(check_repr, estimator=estimator)
         yield partial(check_estimator_tags, estimator=estimator)
 
@@ -202,7 +198,9 @@ def _yield_estimator_checks(estimator_class, estimator_instances, datatypes):
             datatype=datatypes[i][0],
         )
         yield partial(
-            check_fit_updates_state, estimator=estimator, datatype=datatypes[i][0]
+            check_fit_updates_state_and_cloning,
+            estimator=estimator,
+            datatype=datatypes[i][0],
         )
 
         if not _get_tag(estimator, "fit_is_empty", default=False):
@@ -230,13 +228,6 @@ def check_create_test_instance(estimator_class):
 
     _create_test_instance is the key method used to create test instances in testing.
     If this test does not pass, the validity of the other tests cannot be guaranteed.
-
-    Also tests inheritance and super call logic in the constructor.
-
-    Tests that:
-    * _create_test_instance results in an instance of estimator_class
-    * __init__ calls super.__init__
-    * _tags_dynamic attribute for tag inspection is present after construction
     """
     estimator = estimator_class._create_test_instance()
 
@@ -246,38 +237,25 @@ def check_create_test_instance(estimator_class):
         f"found {type(estimator)}"
     )
 
-    msg = (
-        f"{estimator_class.__name__}.__init__ should call super().__init__, the "
-        "estimator does not produce the attributes this call would produce."
-    )
-    assert hasattr(estimator, "_tags_dynamic"), msg
-
 
-# todo consider removing the multiple base class allowance.
 def check_inheritance(estimator_class):
     """Check that estimator inherits from BaseAeonEstimator."""
     assert issubclass(
         estimator_class, BaseAeonEstimator
     ), f"object {estimator_class} is not a sub-class of BaseAeonEstimator."
 
-    if hasattr(estimator_class, "fit"):
-        assert issubclass(estimator_class, BaseAeonEstimator), (
-            f"estimator: {estimator_class} has fit method, but"
-            f"is not a sub-class of BaseAeonEstimator."
-        )
-
     # Usually estimators inherit only from one BaseAeonEstimator type, but in some cases
-    # they may be predictor and transformer at the same time (e.g. pipelines)
+    # they may inherit both as part of a series/collection split
     n_base_types = sum(
         issubclass(estimator_class, cls) for cls in VALID_ESTIMATOR_BASES.values()
     )
+    assert 2 >= n_base_types >= 1, "Estimator should inherit from 1 or 2 base types."
 
-    assert 2 >= n_base_types >= 1
-
-    # If the estimator inherits from more than one base estimator type, we check if
-    # one of them is a transformer base type
+    # Only transformers can inherit from multiple base types currently
     if n_base_types > 1:
-        assert issubclass(estimator_class, BaseTransformer)
+        assert issubclass(
+            estimator_class, BaseTransformer
+        ), "Only transformers can inherit from multiple base types."
 
 
 def check_has_common_interface(estimator_class):
@@ -299,23 +277,36 @@ def check_has_common_interface(estimator_class):
         estimator_class.get_fitted_params
     )
 
+    # axis class parameter is for internal use only
+    assert (
+        "axis" not in estimator_class.__dict__
+    ), "axis should not be a class parameter"
 
-def check_set_params_sklearn(estimator_class):
-    """Check that set_params works correctly, mirrors sklearn check_set_params.
 
-    Instead of the "fuzz values" in sklearn's check_set_params,
-    we use the other test parameter settings (which are assumed valid).
-    This guarantees settings which play along with the __init__ content.
-    """
+def check_set_params(estimator_class):
+    """Check that set_params works correctly."""
+    # some parameters do not have default values, we need to set them
     estimator = estimator_class._create_test_instance()
+    required_params_names = [
+        p.name
+        for p in signature(estimator_class.__init__).parameters.values()
+        # dont include self and *args, **kwargs
+        if p.name != "self" and p.kind not in [p.VAR_KEYWORD, p.VAR_POSITIONAL]
+        # has no default
+        and p.default == p.empty
+    ]
+    params = estimator.get_params()
+    init_params = {p: params[p] for p in params if p in required_params_names}
+
+    # default constructed instance except for required parameters
+    estimator = estimator_class(**init_params)
+
     test_params = estimator_class._get_test_params()
     if not isinstance(test_params, list):
         test_params = [test_params]
 
     for params in test_params:
-        # we construct the full parameter set for params
-        # params may only have parameters that are deviating from defaults
-        # in order to set non-default parameters back to defaults
+        # parameter sets may only have parameters that are deviating from defaults
         params_full = estimator.get_params(deep=False)
         params_full.update(params)
 
@@ -338,28 +329,37 @@ def check_set_params_sklearn(estimator_class):
 def check_constructor(estimator_class):
     """Check that the constructor has sklearn compatible signature and behaviour.
 
-    Based on sklearn check_estimator testing of __init__ logic.
-    Uses _create_test_instance to create an instance.
-    Assumes test_create_test_instance has passed and certified _create_test_instance.
-
     Tests that:
     * constructor has no varargs
     * tests that constructor constructs an instance of the class
     * tests that all parameters are set in init to an attribute of the same name
     * tests that parameter values are always copied to the attribute and not changed
-    * tests that default parameters are one of the following:
-        None, str, int, float, bool, tuple, function, joblib memory, numpy primitive
-        (other type parameters should be None, default handling should be by writing
-        the default to attribute of a different name, e.g., my_param_ not my_param)
+    * tests that default parameters are a valid type or callable
     """
-    msg = "constructor __init__ should have no varargs"
-    assert getfullargspec(estimator_class.__init__).varkw is None, msg
+    assert (
+        getfullargspec(estimator_class.__init__).varkw is None
+    ), "constructor __init__ should have no varargs"
 
     estimator = estimator_class._create_test_instance()
-    assert isinstance(estimator, estimator_class)
 
-    # Ensure that each parameter is set in init
-    init_params = inspect.signature(estimator_class.__init__).parameters
+    # ensure base class super is called in constructor
+    assert hasattr(estimator, "is_fitted"), (
+        "Estimator should have an is_fitted attribute after init, if not make sure "
+        "you call super().__init__ in the constructor"
+    )
+    assert (
+        estimator.is_fitted is False
+    ), "Estimator is_fitted attribute should be set to False after init"
+    assert hasattr(estimator, "_tags_dynamic"), (
+        "Estimator should have a _tags_dynamic attribute after init, if not make sure "
+        "you call super().__init__ in the constructor"
+    )
+    assert isinstance(
+        estimator._tags_dynamic, dict
+    ), "Estimator _tags_dynamic attribute should be a dict after init"
+
+    # ensure that each parameter is set in init
+    init_params = signature(estimator_class.__init__).parameters
     invalid_attr = set(init_params) - set(vars(estimator)) - {"self"}
     assert not invalid_attr, (
         "Estimator %s should store all parameters"
@@ -367,60 +367,62 @@ def check_constructor(estimator_class):
         "attributes `%s`." % (estimator.__class__.__name__, sorted(invalid_attr))
     )
 
-    # Ensure that init does nothing but set parameters
-    # No logic/interaction with other parameters
-    def param_filter(p):
-        """Identify hyper parameters of an estimator."""
-        return p.name != "self" and p.kind not in [p.VAR_KEYWORD, p.VAR_POSITIONAL]
-
-    init_params = [
-        p for p in signature(estimator.__init__).parameters.values() if param_filter(p)
+    param_values = [
+        p
+        for p in init_params.values()
+        # dont include self and *args, **kwargs
+        if p.name != "self" and p.kind not in [p.VAR_KEYWORD, p.VAR_POSITIONAL]
     ]
+    required_params_names = [p.name for p in param_values if p.default == p.empty]
+    default_value_params = [p for p in param_values if p.default != p.empty]
 
     params = estimator.get_params()
+    init_params = {p: params[p] for p in params if p in required_params_names}
 
-    test_params = estimator_class._get_test_params()
-    if isinstance(test_params, list):
-        test_params = test_params[0]
-    test_params = test_params.keys()
-
-    init_params = [param for param in init_params if param.name not in test_params]
+    # default constructed instance except for required parameters
+    estimator = estimator_class(**init_params)
+    params = estimator.get_params()
 
-    for param in init_params:
-        assert param.default != param.empty, (
-            "parameter `%s` for %s has no default value and is not "
-            "set in _get_test_params" % (param.name, estimator.__class__.__name__)
+    for param in default_value_params:
+        allowed_types = {
+            str,
+            int,
+            float,
+            bool,
+            tuple,
+            type(None),
+            type,
+            np.float64,
+            np.int64,
+            np.nan,
+        }
+
+        assert type(param.default) in allowed_types or callable(param.default), (
+            f"Default value of parameter {param.name} is not callable or one of "
+            f"the allowed types: {allowed_types}"
         )
-        if type(param.default) is type:
-            assert param.default in [np.float64, np.int64]
-        else:
-            assert type(param.default) in [
-                str,
-                int,
-                float,
-                bool,
-                tuple,
-                type(None),
-                np.float64,
-                types.FunctionType,
-                joblib.Memory,
-            ]
 
         param_value = params[param.name]
+        msg = (
+            f"Parameter {param.name} was mutated on init. All parameters must be "
+            f"stored unchanged."
+        )
         if isinstance(param_value, np.ndarray):
-            np.testing.assert_array_equal(param_value, param.default)
+            np.testing.assert_array_equal(param_value, param.default, err_msg=msg)
         else:
-            if bool(isinstance(param_value, numbers.Real) and np.isnan(param_value)):
-                # Allows to set default parameters to np.nan
-                assert param_value is param.default, param.name
+            if (
+                not isinstance(param_value, numbers.Integral)
+                and isinstance(param_value, numbers.Real)
+                and math.isnan(param_value)
+            ):
+                # Allows setting default parameters to np.nan
+                assert param_value is param.default, msg
             else:
-                assert param_value == param.default, param.name
+                assert param_value == param.default, msg
 
 
 def check_estimator_class_tags(estimator_class):
     """Check conventions on estimator tags for class."""
-    # check get_class_tags method is retained from base
-    assert hasattr(estimator_class, "get_class_tags")
     all_tags = estimator_class.get_class_tags()
     assert isinstance(all_tags, dict)
     assert all(isinstance(key, str) for key in all_tags.keys())
@@ -446,56 +448,38 @@ def check_estimator_class_tags(estimator_class):
             f"estimator tags."
         )
 
+    # Must have at least one set to True
+    multi = estimator_class.get_class_tag(tag_name="capability:multivariate")
+    uni = estimator_class.get_class_tag(tag_name="capability:univariate")
+    assert multi or uni, (
+        "Estimator must have at least one of capability:multivariate or "
+        "capability:univariate set to True"
+    )
+
 
 def check_get_params(estimator):
     """Check that get_params works correctly."""
-    params = estimator.get_params()
-    assert isinstance(params, dict)
-    check_get_params_invariance(estimator.__class__.__name__, estimator)
-
-
-def check_set_params(estimator):
-    """Check that set_params works correctly."""
     estimator = _clone_estimator(estimator)
-    params = estimator.get_params()
-
-    assert (
-        estimator.set_params(**params) is estimator
-    ), f"set_params of {type(estimator).__name__} does not return self"
-
-    is_equal, equals_msg = deep_equals(estimator.get_params(), params, return_msg=True)
-    msg = (
-        f"get_params result of {type(estimator).__name__} (x) does not match "
-        f"what was passed to set_params (y). Reason for discrepancy: {equals_msg}"
-    )
-    assert is_equal, msg
 
+    params = estimator.get_params()
+    assert isinstance(params, dict)
 
-def check_clone(estimator):
-    """Check that clone method does not raise exceptions and results in a clone.
+    shallow_params = estimator.get_params(deep=False)
+    deep_params = estimator.get_params(deep=True)
 
-    A clone of an object x is an object that:
-    * has same class and parameters as x
-    * is not identical with x
-     * is unfitted (even if x was fitted)
-    """
-    est_clone = estimator.clone()
-    assert isinstance(est_clone, type(estimator))
-    assert est_clone is not estimator
-    if hasattr(est_clone, "is_fitted"):
-        assert not est_clone.is_fitted
+    assert all(item in deep_params.items() for item in shallow_params.items())
 
 
-# todo roll into another test
 def check_repr(estimator):
     """Check that __repr__ call to instance does not raise exceptions."""
-    repr(estimator)
+    estimator = _clone_estimator(estimator)
+    assert isinstance(repr(estimator), str)
 
 
 def check_estimator_tags(estimator):
     """Check conventions on estimator tags for test objects."""
-    # check get_tags method is retained from base
-    assert hasattr(estimator, "get_tags")
+    estimator = _clone_estimator(estimator)
+
     all_tags = estimator.get_tags()
     assert isinstance(all_tags, dict)
     assert all(isinstance(key, str) for key in all_tags.keys())
@@ -513,7 +497,9 @@ def check_estimator_tags(estimator):
 
 
 def check_dl_constructor_initializes_deeply(estimator):
-    """Test DL estimators that they pass custom parameters to underlying Network."""
+    """Test deep learning estimators pass custom parameters to underlying Network."""
+    estimator = _clone_estimator(estimator)
+
     for key, value in estimator.__dict__.items():
         assert vars(estimator)[key] == value
         # some keys are only relevant to the final model (eg: n_epochs)
@@ -523,16 +509,10 @@ def check_dl_constructor_initializes_deeply(estimator):
 
 
 def check_non_state_changing_method(estimator, datatype):
-    """Check that non-state-changing methods behave as per interface contract.
-
-    Check the following contract on non-state-changing methods:
-    1. do not change state of the estimator, i.e., any attributes
-        (including hyper-parameters and fitted parameters)
-    2. expected output type of the method matches actual output type
-        - only for abstract BaseAeonEstimator methods, common to all estimators.
-        List of BaseAeonEstimator methods tested: get_fitted_params
-        Subclass specific method outputs are tested in TestAll[estimatortype] class
-    3. the state of method arguments does not change
+    """Check that non-state-changing methods behave correctly.
+
+    Non-state-changing methods should not alter the estimator attributes or the
+    input arguments. We also check fit does not alter the input arguments here.
     """
     estimator = _clone_estimator(estimator)
 
@@ -566,33 +546,31 @@ def check_non_state_changing_method(estimator, datatype):
         )
 
 
-def check_fit_updates_state(estimator, datatype):
+def check_fit_updates_state_and_cloning(estimator, datatype):
     """Check fit/update state change.
 
-    1. Check estimator_instance calls base class constructor
-    2. Check is_fitted attribute is set correctly to False before fit, at init
-        This is testing base class functionality, but its fast
-    3. Check fit returns self
-    4. Check is_fitted attribute is updated correctly to True after calling fit
-    5. Check estimator hyper parameters are not changed in fit
-    """
-    # Check that fit updates the is-fitted states
-    estimator = _clone_estimator(estimator)
+    We test clone here to avoid fitting again in a separate cloning test.
 
-    msg = (
-        f"{type(estimator).__name__}.__init__ should call "
-        f"super({type(estimator).__name__}, self).__init__, "
-        "but that does not seem to be the case. Please ensure to call the "
-        f"parent class's constructor in {type(estimator).__name__}.__init__"
-    )
-    assert hasattr(estimator, "is_fitted"), msg
-
-    # Check is_fitted attribute is set correctly to False before fit, at init
+    Tests that:
+    * clone returns a new unfitted instance of the estimator
+    * fit returns self
+    * is_fitted attribute is updated correctly to True after calling fit
+    * estimator hyper parameters are not changed in fit
+    """
+    # do some basic checks for cloning
+    estimator_clone = estimator.clone()
+    assert isinstance(
+        estimator_clone, type(estimator)
+    ), "Estimator clone should be of the same type as the original estimator"
     assert (
-        not estimator.is_fitted
-    ), f"Estimator: {estimator} does not initiate attribute: is_fitted to False"
+        estimator_clone is not estimator
+    ), "Estimator clone should not be the same object as the original estimator"
+    assert (
+        estimator_clone.is_fitted is False
+    ), "Estimator is_fitted attribute should be set to False after cloning and init"
 
     # Make a physical copy of the original estimator parameters before fitting.
+    estimator = estimator_clone
     original_params = deepcopy(estimator.get_params())
 
     fitted_estimator = _run_estimator_method(estimator, "fit", datatype, "train")
@@ -604,7 +582,7 @@ def check_fit_updates_state(estimator, datatype):
 
     # Check is_fitted attribute is updated correctly to True after calling fit
     assert (
-        fitted_estimator.is_fitted
+        fitted_estimator.is_fitted is True
     ), f"Estimator: {estimator} does not update attribute: is_fitted during fit"
 
     # Compare the state of the model parameters with the original parameters
@@ -617,28 +595,24 @@ def check_fit_updates_state(estimator, datatype):
         # that introspects recursively any subobjects to compute a checksum.
         # The only exception to this rule of immutable constructor parameters
         # is possible RandomState instance but in this check we explicitly
-        # fixed the random_state params recursively to be integer seeds.
+        # fixed the random_state params recursively to be integer seeds via clone.
         assert joblib.hash(new_value) == joblib.hash(original_value), (
             "Estimator %s should not change or mutate "
             " the parameter %s from %s to %s during fit."
             % (estimator.__class__.__name__, param_name, original_value, new_value)
         )
 
+    # check that estimator cloned from fitted estimator is not fitted
+    estimator_clone = estimator.clone()
+    assert (
+        estimator_clone.is_fitted is False
+    ), "Estimator is_fitted attribute should be set to False after cloning"
 
-def check_raises_not_fitted_error(estimator, datatype):
-    """Check exception raised for non-fit method calls to unfitted estimators.
-
-    Tries to run all methods in NON_STATE_CHANGING_METHODS with valid scenario,
-    but before fit has been called on the estimator.
 
-    This should raise a NotFittedError if correctly caught,
-    normally by a self.check_is_fitted() call in the method's boilerplate.
+def check_raises_not_fitted_error(estimator, datatype):
+    """Check exception raised for non-fit method calls to unfitted estimators."""
+    estimator = _clone_estimator(estimator)
 
-    Raises
-    ------
-    Exception if NotFittedError is not raised by non-state changing method
-    """
-    # call methods without prior fitting and check that they raise NotFittedError
     for method in NON_STATE_CHANGING_METHODS:
         if hasattr(estimator, method):
             with pytest.raises(NotFittedError, match=r"has not been fitted"):
@@ -648,6 +622,7 @@ def check_raises_not_fitted_error(estimator, datatype):
 def check_persistence_via_pickle(estimator, datatype):
     """Check that we can pickle all estimators."""
     estimator = _clone_estimator(estimator, random_state=0)
+
     _run_estimator_method(estimator, "fit", datatype, "train")
 
     results = []
@@ -664,14 +639,12 @@ def check_persistence_via_pickle(estimator, datatype):
     for method in NON_STATE_CHANGING_METHODS_ARRAYLIKE:
         if hasattr(estimator, method) and callable(getattr(estimator, method)):
             output = _run_estimator_method(estimator, method, datatype, "test")
-
             assert_array_almost_equal(
                 output,
                 results[i],
                 err_msg=f"Running {method} after fit twice with test "
                 f"parameters gives different results.",
             )
-
             i += 1
 
 
@@ -696,48 +669,10 @@ def check_fit_deterministic(estimator, datatype):
     for method in NON_STATE_CHANGING_METHODS_ARRAYLIKE:
         if hasattr(estimator, method) and callable(getattr(estimator, method)):
             output = _run_estimator_method(estimator, method, datatype, "test")
-
             assert_array_almost_equal(
                 output,
                 results[i],
                 err_msg=f"Running {method} after fit twice with test "
                 f"parameters gives different results.",
             )
-
             i += 1
-
-
-# def check_multiprocessing_idempotent(estimator):
-#     """Test that single and multi-process run results are identical.
-#
-#     Check that running an estimator on a single process is no different to running
-#     it on multiple processes. We also check that we can set n_jobs=-1 to make use
-#     of all CPUs. The test is not really necessary though, as we rely on joblib for
-#     parallelization and can trust that it works as expected.
-#     """
-#     method_nsc = method_nsc_arraylike
-#     params = estimator_instance.get_params()
-#
-#     if "n_jobs" in params:
-#         # run on a single process
-#         # -----------------------
-#         estimator = deepcopy(estimator_instance)
-#         estimator.set_params(n_jobs=1)
-#         set_random_state(estimator)
-#         result_single_process = scenario.run(
-#             estimator, method_sequence=["fit", method_nsc]
-#         )
-#
-#         # run on multiple processes
-#         # -------------------------
-#         estimator = deepcopy(estimator_instance)
-#         estimator.set_params(n_jobs=-1)
-#         set_random_state(estimator)
-#         result_multiple_process = scenario.run(
-#             estimator, method_sequence=["fit", method_nsc]
-#         )
-#         _assert_array_equal(
-#             result_single_process,
-#             result_multiple_process,
-#             err_msg="Results are not equal for n_jobs=1 and n_jobs=-1",
-#         )
diff --git a/aeon/testing/estimator_checking/_yield_regression_checks.py b/aeon/testing/estimator_checking/_yield_regression_checks.py
index 48c53269..bf6d2cb5 100644
--- a/aeon/testing/estimator_checking/_yield_regression_checks.py
+++ b/aeon/testing/estimator_checking/_yield_regression_checks.py
@@ -145,9 +145,6 @@ def check_regressor_overrides_and_tags(estimator_class):
                 f"Override _{method} instead."
             )
 
-    # axis class parameter is for internal use only
-    assert "axis" not in estimator_class.__dict__
-
     # Test valid tag for X_inner_type
     X_inner_type = estimator_class.get_class_tag(tag_name="X_inner_type")
     if isinstance(X_inner_type, str):
@@ -163,11 +160,6 @@ def check_regressor_overrides_and_tags(estimator_class):
         else:  # must be a list
             assert any([t in valid_unequal_types for t in X_inner_type])
 
-    # Must have at least one set to True
-    multi = estimator_class.get_class_tag(tag_name="capability:multivariate")
-    uni = estimator_class.get_class_tag(tag_name="capability:univariate")
-    assert multi or uni
-
     valid_algorithm_types = [
         "distance",
         "deeplearning",
diff --git a/aeon/testing/estimator_checking/tests/test_check_estimator.py b/aeon/testing/estimator_checking/tests/test_check_estimator.py
index ed16d383..97f3fff5 100644
--- a/aeon/testing/estimator_checking/tests/test_check_estimator.py
+++ b/aeon/testing/estimator_checking/tests/test_check_estimator.py
@@ -104,13 +104,13 @@ def test_check_estimator_subset_tests():
     tests_to_run = [
         "check_get_params",
         "check_set_params",
-        "check_clone",
+        "check_inheritance",
     ]
     tests_to_exclude = ["check_set_params"]
 
     expected_tests = [
+        "check_inheritance(estimator_class=MockClassifier)",
         "check_get_params(estimator=MockClassifier())",
-        "check_clone(estimator=MockClassifier())",
     ]
 
     results = check_estimator(
diff --git a/aeon/testing/testing_config.py b/aeon/testing/testing_config.py
index 7caf96c7..cdc4e773 100644
--- a/aeon/testing/testing_config.py
+++ b/aeon/testing/testing_config.py
@@ -9,7 +9,7 @@ import aeon.testing._cicd_numba_caching  # noqa: F401
 # per os/version default is False, can be set to True by pytest --prtesting True flag
 PR_TESTING = False
 
-# Exclude estimators here for short term fixes
+# exclude estimators here for short term fixes
 EXCLUDE_ESTIMATORS = [
     "ClearSkyTransformer",
     # See #2071
@@ -76,20 +76,17 @@ EXCLUDED_TESTS = {
     "RDSTRegressor": ["check_regressor_against_expected_results"],
 }
 
-# NON_STATE_CHANGING_METHODS =
-# methods that should not change the state of the estimator, that is, they should
-# not change fitted parameters or hyper-parameters. They are also the methods that
-# "apply" the fitted estimator to data and useful for checking results.
+
+# estimator methods post-fit that should not change the state of the estimator
 
 # non-state-changing methods that return an array-like output
 NON_STATE_CHANGING_METHODS_ARRAYLIKE = (
     "predict",
-    "predict_var",
     "predict_proba",
-    "decision_function",
     "transform",
 )
 
+# all non-state-changing methods
 NON_STATE_CHANGING_METHODS = NON_STATE_CHANGING_METHODS_ARRAYLIKE + (
     "get_fitted_params",
 )