Repository: aeon-toolkit/aeon
Pull request #2380 (2024-11-22 16:24:29)

[MNT] Deprecation in deep clustering module
Part of #1518 
deprecating `clustering_algorithm` and `clustering_params` arguments as they were replaced by `estimator`

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{#4011F3}{\\textsf{clustering}}$ ]**. Feel free to change these if they do not properly represent the PR.

The [Checks](https://github.com/aeon-toolkit/aeon/pull/2380/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/anomaly_detection/_copod.py b/aeon/anomaly_detection/_copod.py
index 1194f98b..f89cb8ba 100644
--- a/aeon/anomaly_detection/_copod.py
+++ b/aeon/anomaly_detection/_copod.py
@@ -43,6 +43,7 @@ class COPOD(PyODAdapter):
         "capability:multivariate": True,
         "capability:univariate": True,
         "capability:missing_values": False,
+        "capability:multithreading": True,
         "fit_is_empty": False,
         "python_dependencies": ["pyod"],
     }
diff --git a/aeon/anomaly_detection/_iforest.py b/aeon/anomaly_detection/_iforest.py
index c0980309..f14e368c 100644
--- a/aeon/anomaly_detection/_iforest.py
+++ b/aeon/anomaly_detection/_iforest.py
@@ -88,6 +88,7 @@ class IsolationForest(PyODAdapter):
         "capability:multivariate": True,
         "capability:univariate": True,
         "capability:missing_values": False,
+        "capability:multithreading": True,
         "fit_is_empty": False,
         "python_dependencies": ["pyod"],
     }
diff --git a/aeon/base/_base_collection.py b/aeon/base/_base_collection.py
index d3d298e8..8319f45d 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,
@@ -20,11 +22,12 @@ from aeon.utils.validation.collection import (
 
 
 class BaseCollectionEstimator(BaseAeonEstimator):
-    """Base class for estimators that use collections of time series for method fit.
+    """Base class for estimators that use collections of time series for ``fit``.
 
-    Provides functions that are common to BaseClassifier, BaseRegressor,
-    BaseClusterer and BaseCollectionTransformer for the checking and
-    conversion of input to fit, predict and predict_proba, where relevant.
+    Provides functions that are common to estimators which use colections such as
+    ``BaseClassifier``, ``BaseRegressor``, ``BaseClusterer``, ``BaseSimilaritySearch``
+    and ``BaseCollectionTransformer``. Functionality includes checking and
+    conversion of input to ``fit, predict and predict_proba, where relevant.
 
     It also stores the common default tags used by all the subclasses and meta data
     describing the characteristics of time series passed to ``fit``.
@@ -79,6 +82,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 +272,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/classification/distance_based/_time_series_neighbors.py b/aeon/classification/distance_based/_time_series_neighbors.py
index 40d65f9a..a24459d1 100644
--- a/aeon/classification/distance_based/_time_series_neighbors.py
+++ b/aeon/classification/distance_based/_time_series_neighbors.py
@@ -67,6 +67,7 @@ class KNeighborsTimeSeriesClassifier(BaseClassifier):
     _tags = {
         "capability:multivariate": True,
         "capability:unequal_length": True,
+        "capability:multithreading": True,
         "X_inner_type": ["np-list", "numpy3D"],
         "algorithm_type": "distance",
     }
diff --git a/aeon/classification/shapelet_based/_rsast.py b/aeon/classification/shapelet_based/_rsast.py
index 9cd19cb6..48025229 100644
--- a/aeon/classification/shapelet_based/_rsast.py
+++ b/aeon/classification/shapelet_based/_rsast.py
@@ -70,7 +70,7 @@ class RSASTClassifier(BaseClassifier):
         nb_inst_per_class=10,
         seed=None,
         classifier=None,
-        n_jobs=-1,
+        n_jobs=1,
     ):
         super().__init__()
         self.n_random_points = n_random_points
diff --git a/aeon/classification/shapelet_based/_sast.py b/aeon/classification/shapelet_based/_sast.py
index 35a840a7..c5ef836c 100644
--- a/aeon/classification/shapelet_based/_sast.py
+++ b/aeon/classification/shapelet_based/_sast.py
@@ -73,7 +73,7 @@ class SASTClassifier(BaseClassifier):
         nb_inst_per_class: int = 1,
         seed: Optional[int] = None,
         classifier=None,
-        n_jobs: int = -1,
+        n_jobs: int = 1,
     ) -> None:
         super().__init__()
         self.length_list = length_list
diff --git a/aeon/clustering/_kernel_k_means.py b/aeon/clustering/_kernel_k_means.py
index 9c92d250..6aab712d 100644
--- a/aeon/clustering/_kernel_k_means.py
+++ b/aeon/clustering/_kernel_k_means.py
@@ -85,6 +85,7 @@ class TimeSeriesKernelKMeans(BaseClusterer):
 
     _tags = {
         "capability:multivariate": True,
+        "capability:multithreading": True,
         "python_dependencies": "tslearn",
     }
 
@@ -97,7 +98,7 @@ class TimeSeriesKernelKMeans(BaseClusterer):
         tol: float = 1e-4,
         kernel_params: Union[dict, None] = None,
         verbose: bool = False,
-        n_jobs: Union[int, None] = None,
+        n_jobs: Union[int, None] = 1,
         random_state: Optional[Union[int, RandomState]] = None,
     ):
         self.kernel = kernel
@@ -200,8 +201,4 @@ class TimeSeriesKernelKMeans(BaseClusterer):
             "n_init": 1,
             "max_iter": 1,
             "tol": 0.0001,
-            "kernel_params": None,
-            "verbose": False,
-            "n_jobs": 1,
-            "random_state": 1,
         }
diff --git a/aeon/clustering/deep_learning/_ae_abgru.py b/aeon/clustering/deep_learning/_ae_abgru.py
index 3b41dbfd..20a3281c 100644
--- a/aeon/clustering/deep_learning/_ae_abgru.py
+++ b/aeon/clustering/deep_learning/_ae_abgru.py
@@ -20,10 +20,6 @@ class AEAttentionBiGRUClusterer(BaseDeepClusterer):
 
     Parameters
     ----------
-    clustering_algorithm : str, default="deprecated"
-        Use 'estimator' parameter instead.
-    clustering_params : dict, default=None
-        Use 'estimator' parameter instead.
     estimator : aeon clusterer, default=None
         An aeon estimator to be built using the transformed data.
         Defaults to aeon TimeSeriesKMeans() with euclidean distance
@@ -105,8 +101,6 @@ class AEAttentionBiGRUClusterer(BaseDeepClusterer):
     def __init__(
         self,
         estimator=None,
-        clustering_algorithm="deprecated",
-        clustering_params=None,
         latent_space_dim=128,
         n_layers_encoder=2,
         n_layers_decoder=2,
@@ -150,8 +144,6 @@ class AEAttentionBiGRUClusterer(BaseDeepClusterer):
         self.random_state = random_state
 
         super().__init__(
-            clustering_algorithm=clustering_algorithm,
-            clustering_params=clustering_params,
             estimator=estimator,
             batch_size=batch_size,
             last_file_name=last_file_name,
diff --git a/aeon/clustering/deep_learning/_ae_bgru.py b/aeon/clustering/deep_learning/_ae_bgru.py
index 609eaf5d..340e12a4 100644
--- a/aeon/clustering/deep_learning/_ae_bgru.py
+++ b/aeon/clustering/deep_learning/_ae_bgru.py
@@ -20,10 +20,6 @@ class AEBiGRUClusterer(BaseDeepClusterer):
 
     Parameters
     ----------
-    clustering_algorithm : str, default="deprecated"
-        Use 'estimator' parameter instead.
-    clustering_params : dict, default=None
-        Use 'estimator' parameter instead.
     estimator : aeon clusterer, default=None
         An aeon estimator to be built using the transformed data.
         Defaults to aeon TimeSeriesKMeans() with euclidean distance
@@ -103,9 +99,7 @@ class AEBiGRUClusterer(BaseDeepClusterer):
 
     def __init__(
         self,
-        clustering_algorithm="deprecated",
         estimator=None,
-        clustering_params=None,
         latent_space_dim=128,
         temporal_latent_space=False,
         n_layers=2,
@@ -149,8 +143,6 @@ class AEBiGRUClusterer(BaseDeepClusterer):
         self.random_state = random_state
 
         super().__init__(
-            clustering_algorithm=clustering_algorithm,
-            clustering_params=clustering_params,
             estimator=estimator,
             batch_size=batch_size,
             last_file_name=last_file_name,
diff --git a/aeon/clustering/deep_learning/_ae_dcnn.py b/aeon/clustering/deep_learning/_ae_dcnn.py
index d6c6b8c3..825f1e03 100644
--- a/aeon/clustering/deep_learning/_ae_dcnn.py
+++ b/aeon/clustering/deep_learning/_ae_dcnn.py
@@ -19,10 +19,6 @@ class AEDCNNClusterer(BaseDeepClusterer):
 
     Parameters
     ----------
-    clustering_algorithm : str, default="deprecated"
-        Use 'estimator' parameter instead.
-    clustering_params : dict, default=None
-        Use 'estimator' parameter instead.
     estimator : aeon clusterer, default=None
         An aeon estimator to be built using the transformed data.
         Defaults to aeon TimeSeriesKMeans() with euclidean distance
@@ -118,8 +114,6 @@ class AEDCNNClusterer(BaseDeepClusterer):
     def __init__(
         self,
         estimator=None,
-        clustering_algorithm="deprecated",
-        clustering_params=None,
         latent_space_dim=128,
         temporal_latent_space=False,
         n_layers=3,
@@ -171,8 +165,6 @@ class AEDCNNClusterer(BaseDeepClusterer):
         self.random_state = random_state
 
         super().__init__(
-            clustering_params=clustering_params,
-            clustering_algorithm=clustering_algorithm,
             estimator=estimator,
             batch_size=batch_size,
             last_file_name=last_file_name,
diff --git a/aeon/clustering/deep_learning/_ae_drnn.py b/aeon/clustering/deep_learning/_ae_drnn.py
index 0efedfb7..63fbc80f 100644
--- a/aeon/clustering/deep_learning/_ae_drnn.py
+++ b/aeon/clustering/deep_learning/_ae_drnn.py
@@ -24,14 +24,10 @@ class AEDRNNClusterer(BaseDeepClusterer):
 
     Parameters
     ----------
-    clustering_algorithm : str, default="deprecated"
-        Please use the 'estimator' parameter.
     estimator : aeon clusterer, default=None
         An aeon estimator to be built using the transformed data.
         Defaults to aeon TimeSeriesKMeans() with euclidean distance
         and mean averaging method and n_clusters set to 2.
-    clustering_params : dict, default=None
-        Please use 'estimator' parameter.
     latent_space_dim : int, default=128
         Dimension of the latent space of the auto-encoder.
     temporal_latent_space : bool, default = False
@@ -119,8 +115,6 @@ class AEDRNNClusterer(BaseDeepClusterer):
     def __init__(
         self,
         estimator=None,
-        clustering_algorithm="deprecated",
-        clustering_params=None,
         latent_space_dim=128,
         temporal_latent_space=False,
         n_layers_encoder=3,
@@ -175,8 +169,6 @@ class AEDRNNClusterer(BaseDeepClusterer):
 
         super().__init__(
             estimator=estimator,
-            clustering_algorithm=clustering_algorithm,
-            clustering_params=clustering_params,
             batch_size=batch_size,
             last_file_name=last_file_name,
         )
diff --git a/aeon/clustering/deep_learning/_ae_fcn.py b/aeon/clustering/deep_learning/_ae_fcn.py
index 196ebbcf..3105a3cd 100644
--- a/aeon/clustering/deep_learning/_ae_fcn.py
+++ b/aeon/clustering/deep_learning/_ae_fcn.py
@@ -25,10 +25,6 @@ class AEFCNClusterer(BaseDeepClusterer):
         An aeon estimator to be built using the transformed data.
         Defaults to aeon TimeSeriesKMeans() with euclidean distance
         and mean averaging method and n_clusters set to 2.
-    clustering_algorithm : str, default="deprecated"
-        Please use 'estimator' parameter.
-    clustering_params : dict, default=None
-        Please use 'estimator' parameter.
     latent_space_dim : int, default=128
         Dimension of the latent space of the auto-encoder.
     temporal_latent_space : bool, default = False
@@ -127,8 +123,6 @@ class AEFCNClusterer(BaseDeepClusterer):
     def __init__(
         self,
         estimator=None,
-        clustering_algorithm="deprecated",
-        clustering_params=None,
         latent_space_dim=128,
         temporal_latent_space=False,
         n_layers=3,
@@ -183,8 +177,6 @@ class AEFCNClusterer(BaseDeepClusterer):
 
         super().__init__(
             estimator=estimator,
-            clustering_algorithm=clustering_algorithm,
-            clustering_params=clustering_params,
             batch_size=batch_size,
             last_file_name=last_file_name,
         )
diff --git a/aeon/clustering/deep_learning/_ae_resnet.py b/aeon/clustering/deep_learning/_ae_resnet.py
index ff341432..37bca732 100644
--- a/aeon/clustering/deep_learning/_ae_resnet.py
+++ b/aeon/clustering/deep_learning/_ae_resnet.py
@@ -28,10 +28,6 @@ class AEResNetClusterer(BaseDeepClusterer):
         An aeon estimator to be built using the transformed data.
         Defaults to aeon TimeSeriesKMeans() with euclidean distance
         and mean averaging method and n_clusters set to 2.
-    clustering_algorithm : str, default="deprecated"
-        Please use 'estimator' parameter.
-    clustering_params : dict, default=None
-        Please use 'estimator' parameter.
     latent_space_dim : int, default=128
         Dimension of the latent space of the auto-encoder.
     temporal_latent_space : bool, default = False
@@ -137,8 +133,6 @@ class AEResNetClusterer(BaseDeepClusterer):
         self,
         estimator=None,
         n_residual_blocks=3,
-        clustering_algorithm="deprecated",
-        clustering_params=None,
         n_conv_per_residual_block=3,
         n_filters=None,
         kernel_size=None,
@@ -193,8 +187,6 @@ class AEResNetClusterer(BaseDeepClusterer):
 
         super().__init__(
             estimator=estimator,
-            clustering_algorithm=clustering_algorithm,
-            clustering_params=clustering_params,
             batch_size=batch_size,
             last_file_name=last_file_name,
         )
diff --git a/aeon/clustering/deep_learning/base.py b/aeon/clustering/deep_learning/base.py
index 3b2c5b9d..cedac335 100644
--- a/aeon/clustering/deep_learning/base.py
+++ b/aeon/clustering/deep_learning/base.py
@@ -15,16 +15,10 @@ class BaseDeepClusterer(BaseClusterer):
 
     Parameters
     ----------
-    n_clusters : int, default=None
-        Please use 'estimator' parameter.
     estimator : aeon clusterer, default=None
         An aeon estimator to be built using the transformed data.
         Defaults to aeon TimeSeriesKMeans() with euclidean distance
         and mean averaging method and n_clusters set to 2.
-    clustering_algorithm : str, default="deprecated"
-        Please use 'estimator' parameter.
-    clustering_params : dict, default=None
-        Please use 'estimator' parameter.
     batch_size : int, default = 40
         training batch size for the model
     last_file_name : str, default = "last_model"
@@ -46,14 +40,10 @@ class BaseDeepClusterer(BaseClusterer):
     def __init__(
         self,
         estimator=None,
-        clustering_algorithm="deprecated",
-        clustering_params=None,
         batch_size=32,
         last_file_name="last_model",
     ):
         self.estimator = estimator
-        self.clustering_algorithm = clustering_algorithm
-        self.clustering_params = clustering_params
         self.batch_size = batch_size
         self.last_file_name = last_file_name
 
@@ -110,8 +100,6 @@ class BaseDeepClusterer(BaseClusterer):
         X : np.ndarray, shape=(n_cases, n_timepoints, n_channels)
             The input time series.
         """
-        import warnings
-
         self._estimator = (
             TimeSeriesKMeans(
                 n_clusters=2, distance="euclidean", averaging_method="mean"
@@ -120,22 +108,6 @@ class BaseDeepClusterer(BaseClusterer):
             else _clone_estimator(self.estimator)
         )
 
-        # to be removed in 1.0.0
-        if (
-            self.clustering_algorithm != "deprecated"
-            or self.clustering_params is not None
-        ):
-            warnings.warn(
-                "The 'n_clusters' 'clustering_algorithm' and "
-                "'clustering_params' parameters "
-                "will be removed in v1.0.0. "
-                "Their usage will not have an effect, "
-                "please use the new 'estimator' parameter to directly "
-                "give an aeon clusterer as input.",
-                FutureWarning,
-                stacklevel=2,
-            )
-
         latent_space = self.model_.layers[1].predict(X)
         self._estimator.fit(X=latent_space)
         if hasattr(self._estimator, "labels_"):
diff --git a/aeon/regression/distance_based/_time_series_neighbors.py b/aeon/regression/distance_based/_time_series_neighbors.py
index ed700311..29056de9 100644
--- a/aeon/regression/distance_based/_time_series_neighbors.py
+++ b/aeon/regression/distance_based/_time_series_neighbors.py
@@ -67,6 +67,7 @@ class KNeighborsTimeSeriesRegressor(BaseRegressor):
     _tags = {
         "capability:multivariate": True,
         "capability:unequal_length": True,
+        "capability:multithreading": True,
         "X_inner_type": ["np-list", "numpy3D"],
         "algorithm_type": "distance",
     }
diff --git a/aeon/segmentation/_clasp.py b/aeon/segmentation/_clasp.py
index 46dbc190..f214ea85 100644
--- a/aeon/segmentation/_clasp.py
+++ b/aeon/segmentation/_clasp.py
@@ -206,7 +206,7 @@ class ClaSPSegmenter(BaseSegmenter):
     >>> scores = clasp.scores
     """
 
-    _tags = {"fit_is_empty": True}  # for unit test cases
+    _tags = {"capability:multithreading": True, "fit_is_empty": True}
 
     def __init__(self, period_length=10, n_cps=1, exclusion_radius=0.05, n_jobs=1):
         self.period_length = int(period_length)
diff --git a/aeon/transformations/collection/convolution_based/_minirocket.py b/aeon/transformations/collection/convolution_based/_minirocket.py
index 62cf72b0..9d8e248b 100644
--- a/aeon/transformations/collection/convolution_based/_minirocket.py
+++ b/aeon/transformations/collection/convolution_based/_minirocket.py
@@ -74,6 +74,7 @@ class MiniRocket(BaseCollectionTransformer):
         "output_data_type": "Tabular",
         "algorithm_type": "convolution",
         "capability:multivariate": True,
+        "capability:multithreading": True,
     }
     # indices for the 84 kernels used by MiniRocket
     _indices = np.array([_ for _ in combinations(np.arange(9), 3)], dtype=np.int32)
diff --git a/aeon/transformations/collection/convolution_based/_minirocket_mv.py b/aeon/transformations/collection/convolution_based/_minirocket_mv.py
index fb9adeab..01d926cb 100644
--- a/aeon/transformations/collection/convolution_based/_minirocket_mv.py
+++ b/aeon/transformations/collection/convolution_based/_minirocket_mv.py
@@ -87,6 +87,7 @@ class MiniRocketMultivariateVariable(BaseCollectionTransformer):
         "output_data_type": "Tabular",
         "capability:multivariate": True,
         "capability:unequal_length": True,
+        "capability:multithreading": True,
         "X_inner_type": "np-list",
         "algorithm_type": "convolution",
     }
diff --git a/aeon/transformations/collection/convolution_based/_multirocket.py b/aeon/transformations/collection/convolution_based/_multirocket.py
index c3e3f451..f9207ecc 100644
--- a/aeon/transformations/collection/convolution_based/_multirocket.py
+++ b/aeon/transformations/collection/convolution_based/_multirocket.py
@@ -74,6 +74,7 @@ class MultiRocket(BaseCollectionTransformer):
         "output_data_type": "Tabular",
         "algorithm_type": "convolution",
         "capability:multivariate": True,
+        "capability:multithreading": True,
     }
     # indices for the 84 kernels used by MiniRocket
     _indices = np.array([_ for _ in combinations(np.arange(9), 3)], dtype=np.int32)
diff --git a/aeon/transformations/collection/convolution_based/_rocket.py b/aeon/transformations/collection/convolution_based/_rocket.py
index f8500a36..e5c4e39e 100644
--- a/aeon/transformations/collection/convolution_based/_rocket.py
+++ b/aeon/transformations/collection/convolution_based/_rocket.py
@@ -65,6 +65,7 @@ class Rocket(BaseCollectionTransformer):
     _tags = {
         "output_data_type": "Tabular",
         "capability:multivariate": True,
+        "capability:multithreading": True,
         "algorithm_type": "convolution",
     }
 
diff --git a/aeon/transformations/collection/dictionary_based/_sfa.py b/aeon/transformations/collection/dictionary_based/_sfa.py
index 49476483..bb399b7f 100644
--- a/aeon/transformations/collection/dictionary_based/_sfa.py
+++ b/aeon/transformations/collection/dictionary_based/_sfa.py
@@ -110,6 +110,7 @@ class SFA(BaseCollectionTransformer):
 
     _tags = {
         "requires_y": False,  # SFA is unsupervised for equi-depth and equi-width bins
+        "capability:multithreading": True,
         "algorithm_type": "dictionary",
     }
 
diff --git a/aeon/transformations/collection/dictionary_based/_sfa_fast.py b/aeon/transformations/collection/dictionary_based/_sfa_fast.py
index 78e3a18b..1166bc87 100644
--- a/aeon/transformations/collection/dictionary_based/_sfa_fast.py
+++ b/aeon/transformations/collection/dictionary_based/_sfa_fast.py
@@ -130,6 +130,7 @@ class SFAFast(BaseCollectionTransformer):
 
     _tags = {
         "requires_y": False,  # SFA is unsupervised for equi-depth and equi-width bins
+        "capability:multithreading": True,
         "algorithm_type": "dictionary",
     }
 
diff --git a/aeon/transformations/collection/feature_based/_catch22.py b/aeon/transformations/collection/feature_based/_catch22.py
index 9727d0be..daae46f5 100644
--- a/aeon/transformations/collection/feature_based/_catch22.py
+++ b/aeon/transformations/collection/feature_based/_catch22.py
@@ -173,6 +173,7 @@ class Catch22(BaseCollectionTransformer):
         "X_inner_type": ["np-list", "numpy3D"],
         "capability:unequal_length": True,
         "capability:multivariate": True,
+        "capability:multithreading": True,
         "fit_is_empty": True,
     }
 
diff --git a/aeon/transformations/collection/feature_based/_tsfresh.py b/aeon/transformations/collection/feature_based/_tsfresh.py
index 2420b226..f3604797 100644
--- a/aeon/transformations/collection/feature_based/_tsfresh.py
+++ b/aeon/transformations/collection/feature_based/_tsfresh.py
@@ -35,6 +35,7 @@ class _TSFresh(BaseCollectionTransformer):
     _tags = {
         "output_data_type": "Tabular",
         "capability:multivariate": True,
+        "capability:multithreading": True,
         "fit_is_empty": True,
         "python_dependencies": "tsfresh",
     }
diff --git a/aeon/transformations/collection/interval_based/_random_intervals.py b/aeon/transformations/collection/interval_based/_random_intervals.py
index afcae013..7d0dccc4 100644
--- a/aeon/transformations/collection/interval_based/_random_intervals.py
+++ b/aeon/transformations/collection/interval_based/_random_intervals.py
@@ -110,6 +110,7 @@ class RandomIntervals(BaseCollectionTransformer):
     _tags = {
         "output_data_type": "Tabular",
         "capability:multivariate": True,
+        "capability:multithreading": True,
         "fit_is_empty": False,
         "algorithm_type": "interval",
     }
diff --git a/aeon/transformations/collection/interval_based/_supervised_intervals.py b/aeon/transformations/collection/interval_based/_supervised_intervals.py
index fcdc1faf..d247bf67 100644
--- a/aeon/transformations/collection/interval_based/_supervised_intervals.py
+++ b/aeon/transformations/collection/interval_based/_supervised_intervals.py
@@ -136,6 +136,7 @@ class SupervisedIntervals(BaseCollectionTransformer):
     _tags = {
         "output_data_type": "Tabular",
         "capability:multivariate": True,
+        "capability:multithreading": True,
         "requires_y": True,
         "algorithm_type": "interval",
     }
diff --git a/aeon/transformations/collection/shapelet_based/_dilated_shapelet_transform.py b/aeon/transformations/collection/shapelet_based/_dilated_shapelet_transform.py
index 714b796a..0efacf8f 100644
--- a/aeon/transformations/collection/shapelet_based/_dilated_shapelet_transform.py
+++ b/aeon/transformations/collection/shapelet_based/_dilated_shapelet_transform.py
@@ -145,6 +145,7 @@ class RandomDilatedShapeletTransform(BaseCollectionTransformer):
         "output_data_type": "Tabular",
         "capability:multivariate": True,
         "capability:unequal_length": True,
+        "capability:multithreading": True,
         "X_inner_type": ["np-list", "numpy3D"],
         "algorithm_type": "shapelet",
     }
diff --git a/aeon/transformations/collection/shapelet_based/_rsast.py b/aeon/transformations/collection/shapelet_based/_rsast.py
index b353eac3..199e52ed 100644
--- a/aeon/transformations/collection/shapelet_based/_rsast.py
+++ b/aeon/transformations/collection/shapelet_based/_rsast.py
@@ -93,6 +93,7 @@ class RSAST(BaseCollectionTransformer):
     _tags = {
         "output_data_type": "Tabular",
         "capability:multivariate": False,
+        "capability:multithreading": True,
         "algorithm_type": "shapelet",
         "python_dependencies": "statsmodels",
     }
diff --git a/aeon/transformations/collection/shapelet_based/_sast.py b/aeon/transformations/collection/shapelet_based/_sast.py
index c69d799c..ffc51381 100644
--- a/aeon/transformations/collection/shapelet_based/_sast.py
+++ b/aeon/transformations/collection/shapelet_based/_sast.py
@@ -88,6 +88,7 @@ class SAST(BaseCollectionTransformer):
     _tags = {
         "output_data_type": "Tabular",
         "capability:multivariate": False,
+        "capability:multithreading": True,
         "algorithm_type": "shapelet",
     }
 
diff --git a/aeon/transformations/collection/shapelet_based/_shapelet_transform.py b/aeon/transformations/collection/shapelet_based/_shapelet_transform.py
index bed9582d..f041dde6 100644
--- a/aeon/transformations/collection/shapelet_based/_shapelet_transform.py
+++ b/aeon/transformations/collection/shapelet_based/_shapelet_transform.py
@@ -144,6 +144,7 @@ class RandomShapeletTransform(BaseCollectionTransformer):
         "output_data_type": "Tabular",
         "capability:multivariate": True,
         "capability:unequal_length": True,
+        "capability:multithreading": True,
         "X_inner_type": ["np-list", "numpy3D"],
         "requires_y": True,
         "algorithm_type": "shapelet",
diff --git a/aeon/transformations/series/_clasp.py b/aeon/transformations/series/_clasp.py
index f7272fff..dfdfd18d 100644
--- a/aeon/transformations/series/_clasp.py
+++ b/aeon/transformations/series/_clasp.py
@@ -438,7 +438,7 @@ class ClaSPTransformer(BaseSeriesTransformer):
         "X_inner_type": "np.ndarray",
         "fit_is_empty": True,
         "requires_y": False,
-        "capability:inverse_transform": False,
+        "capability:multithreading": True,
     }
 
     def __init__(


Test patch:
diff --git a/aeon/anomaly_detection/tests/test_left_stampi.py b/aeon/anomaly_detection/tests/test_left_stampi.py
index 7e38f49b..872f6af1 100644
--- a/aeon/anomaly_detection/tests/test_left_stampi.py
+++ b/aeon/anomaly_detection/tests/test_left_stampi.py
@@ -309,15 +309,3 @@ class TestLeftSTAMPi:
             ],
             any_order=True,
         )
-
-    def test_it_checks_soft_dependencies(self, mocker):
-        """Unit testing the dependency check."""
-        # given
-        deps_checker_stub = mocker.patch(
-            "aeon.base._base_series._check_estimator_deps", return_value=True
-        )
-        # deps_checker_stub.return_value = True
-        ad = LeftSTAMPi(window_size=5, n_init_train=10)
-
-        # then
-        deps_checker_stub.assert_called_once_with(ad)
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..ac83d1c8 100644
--- a/aeon/testing/estimator_checking/_yield_classification_checks.py
+++ b/aeon/testing/estimator_checking/_yield_classification_checks.py
@@ -74,7 +74,7 @@ def _yield_classification_checks(estimator_class, estimator_instances, datatypes
     # test class instances
     for i, estimator in enumerate(estimator_instances):
         # data type irrelevant
-        if _get_tag(estimator_class, "capability:train_estimate", raise_error=True):
+        if _get_tag(estimator, "capability:train_estimate", raise_error=True):
             yield partial(
                 check_classifier_train_estimate,
                 estimator=estimator,
@@ -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..098534d3 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
@@ -42,6 +40,9 @@ from aeon.testing.estimator_checking._yield_collection_transformation_checks imp
 from aeon.testing.estimator_checking._yield_early_classification_checks import (
     _yield_early_classification_checks,
 )
+from aeon.testing.estimator_checking._yield_multithreading_checks import (
+    _yield_multithreading_checks,
+)
 from aeon.testing.estimator_checking._yield_regression_checks import (
     _yield_regression_checks,
 )
@@ -118,6 +119,10 @@ def _yield_all_aeon_checks(
         estimator_class, estimator_instances, datatypes
     )
 
+    yield from _yield_multithreading_checks(
+        estimator_class, estimator_instances, datatypes
+    )
+
     if issubclass(estimator_class, BaseClassifier):
         yield from _yield_classification_checks(
             estimator_class, estimator_instances, datatypes
@@ -175,7 +180,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 +188,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 +205,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 +235,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 +244,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 +284,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 +336,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 +374,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 +455,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)
 
+    shallow_params = estimator.get_params(deep=False)
+    deep_params = estimator.get_params(deep=True)
 
-def check_clone(estimator):
-    """Check that clone method does not raise exceptions and results in a clone.
+    assert all(item in deep_params.items() for item in shallow_params.items())
 
-    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
 
-
-# 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 +504,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 +516,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 +553,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 (
+        estimator_clone is not estimator
+    ), "Estimator clone should not be the same object as the original estimator"
     assert (
-        not estimator.is_fitted
-    ), f"Estimator: {estimator} does not initiate attribute: is_fitted to False"
+        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 +589,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 +602,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 +629,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 +646,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 +676,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_multithreading_checks.py b/aeon/testing/estimator_checking/_yield_multithreading_checks.py
new file mode 100644
index 00000000..5d4f09a7
--- /dev/null
+++ b/aeon/testing/estimator_checking/_yield_multithreading_checks.py
@@ -0,0 +1,115 @@
+import inspect
+from functools import partial
+
+from numpy.testing import assert_array_almost_equal
+
+from aeon.base._base import _clone_estimator
+from aeon.testing.testing_config import (
+    MULTITHREAD_TESTING,
+    NON_STATE_CHANGING_METHODS_ARRAYLIKE,
+)
+from aeon.testing.utils.estimator_checks import _get_tag, _run_estimator_method
+from aeon.utils.validation import check_n_jobs
+
+
+def _yield_multithreading_checks(estimator_class, estimator_instances, datatypes):
+    """Yield all multithreading checks for an aeon estimator."""
+    can_thread = _get_tag(estimator_class, "capability:multithreading")
+
+    # only class required
+    if can_thread:
+        yield partial(check_multithreading_param, estimator_class=estimator_class)
+    else:
+        yield partial(check_no_multithreading_param, estimator_class=estimator_class)
+
+    if can_thread and MULTITHREAD_TESTING:
+        # test class instances
+        for i, estimator in enumerate(estimator_instances):
+            # test all data types
+            for datatype in datatypes[i]:
+                yield partial(
+                    check_estimator_multithreading,
+                    estimator=estimator,
+                    datatype=datatype,
+                )
+
+
+def check_multithreading_param(estimator_class):
+    """Test that estimators that can multithread have a n_jobs parameter."""
+    default_params = inspect.signature(estimator_class.__init__).parameters
+    n_jobs = default_params.get("n_jobs", None)
+
+    # check that the estimator has a n_jobs parameter
+    if n_jobs is None:
+        raise ValueError(
+            f"{estimator_class} which sets "
+            "capability:multithreading=True must have a n_jobs parameter."
+        )
+
+    # check that the default value is to use 1 thread
+    if n_jobs.default != 1:
+        raise ValueError(
+            "n_jobs parameter must have a default value of 1, "
+            "disabling multithreading by default."
+        )
+
+    # test parameters should not change the default value
+    params = estimator_class._get_test_params()
+    if not isinstance(params, list):
+        params = [params]
+    for param_set in params:
+        assert "n_jobs" not in param_set
+
+
+def check_no_multithreading_param(estimator_class):
+    """Test that estimators that cant multithread have no n_jobs parameter."""
+    default_params = inspect.signature(estimator_class.__init__).parameters
+
+    # check that the estimator does not have a n_jobs parameter
+    if default_params.get("n_jobs", None) is not None:
+        raise ValueError(
+            f"{estimator_class} has a n_jobs parameter, but does not set "
+            "capability:multithreading=True in its tags."
+        )
+
+
+def check_estimator_multithreading(estimator, datatype):
+    """Test that multithreaded estimators store n_jobs_ and produce same results."""
+    st_estimator = _clone_estimator(estimator, random_state=42)
+    mt_estimator = _clone_estimator(estimator, random_state=42)
+    n_jobs = max(2, check_n_jobs(-2))
+    mt_estimator.set_params(n_jobs=n_jobs)
+
+    # fit and get results for single thread estimator
+    _run_estimator_method(st_estimator, "fit", datatype, "train")
+
+    results = []
+    for method in NON_STATE_CHANGING_METHODS_ARRAYLIKE:
+        if hasattr(st_estimator, method) and callable(getattr(estimator, method)):
+            output = _run_estimator_method(st_estimator, method, datatype, "test")
+            results.append(output)
+
+    # fit multithreaded estimator
+    _run_estimator_method(mt_estimator, "fit", datatype, "train")
+
+    # check n_jobs_ attribute is set
+    assert mt_estimator.n_jobs_ == n_jobs, (
+        f"Multithreaded estimator {mt_estimator} does not store n_jobs_ "
+        f"attribute correctly. Expected {n_jobs}, got {mt_estimator.n_jobs_}."
+        f"It is recommended to use the check_n_jobs function to set n_jobs_ and use"
+        f"this for any multithreading."
+    )
+
+    # compare results from single and multithreaded estimators
+    i = 0
+    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
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..ddbc82de 100644
--- a/aeon/testing/testing_config.py
+++ b/aeon/testing/testing_config.py
@@ -8,8 +8,11 @@ import aeon.testing._cicd_numba_caching  # noqa: F401
 # whether to use smaller parameter matrices for test generation and subsample estimators
 # per os/version default is False, can be set to True by pytest --prtesting True flag
 PR_TESTING = False
+# whether to use multithreading in tests, can be set to True by pytest
+# --enablethreading True flag
+MULTITHREAD_TESTING = False
 
-# Exclude estimators here for short term fixes
+# exclude estimators here for short term fixes
 EXCLUDE_ESTIMATORS = [
     "ClearSkyTransformer",
     # See #2071
@@ -76,20 +79,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",
 )
diff --git a/conftest.py b/conftest.py
index 0c1299b8..015e145a 100644
--- a/conftest.py
+++ b/conftest.py
@@ -44,7 +44,11 @@ def pytest_configure(config):
     if config.getoption("--nonumba") in [True, "True", "true"]:
         os.environ["NUMBA_DISABLE_JIT"] = "1"
 
-    if not config.getoption("--enablethreading") in [True, "True", "true"]:
+    if config.getoption("--enablethreading") in [True, "True", "true"]:
+        from aeon.testing import testing_config
+
+        testing_config.MULTITHREAD_TESTING = True
+    else:
         # Must be called before any numpy imports
         os.environ["MKL_NUM_THREADS"] = "1"
         os.environ["NUMEXPR_NUM_THREADS"] = "1"