Repository: aeon-toolkit/aeon
Pull request #2148 (2024-10-05 11:47:01)

[ENH] Add BOSS tests
single line of coverage for BOSS

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

I have added the following labels to this PR based on the title: **[ $\\color{#FEF1BE}{\\textsf{enhancement}}$ ]**.
I would have added the following labels to this PR based on the changes made: **[ $\\color{#BCAE15}{\\textsf{classification}}$ ]**, however some package labels are already present.

The [Checks](https://github.com/aeon-toolkit/aeon/pull/2148/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/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 265e81c2..4a647e75 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.6.0
+    rev: v5.0.0
     hooks:
       - id: check-ast
       - id: check-added-large-files
@@ -29,7 +29,7 @@ repos:
         args: [ "--create", "--python-folders", "aeon" ]
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.6.8
+    rev: v0.6.9
     hooks:
       - id: ruff
         args: [ "--fix"]
diff --git a/aeon/anomaly_detection/_left_stampi.py b/aeon/anomaly_detection/_left_stampi.py
index 38c08f2b..a6980dc9 100644
--- a/aeon/anomaly_detection/_left_stampi.py
+++ b/aeon/anomaly_detection/_left_stampi.py
@@ -77,7 +77,7 @@ class LeftSTAMPi(BaseAnomalyDetector):
         "capability:multivariate": False,
         "capability:missing_values": False,
         "fit_is_empty": False,
-        "cant-pickle": True,
+        "cant_pickle": True,
         "python_dependencies": ["stumpy"],
     }
 
diff --git a/aeon/anomaly_detection/base.py b/aeon/anomaly_detection/base.py
index 9d6896c5..b5b0acb0 100644
--- a/aeon/anomaly_detection/base.py
+++ b/aeon/anomaly_detection/base.py
@@ -82,8 +82,6 @@ class BaseAnomalyDetector(BaseSeriesEstimator, ABC):
     }
 
     def __init__(self, axis):
-        self._is_fitted = False
-
         super().__init__(axis=axis)
 
     @final
@@ -118,7 +116,7 @@ class BaseAnomalyDetector(BaseSeriesEstimator, ABC):
             The fitted estimator, reference to self.
         """
         if self.get_class_tag("fit_is_empty"):
-            self._is_fitted = True
+            self.is_fitted = True
             return self
 
         if self.get_class_tag("requires_y"):
@@ -135,7 +133,7 @@ class BaseAnomalyDetector(BaseSeriesEstimator, ABC):
         self._fit(X=X, y=y)
 
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     @final
@@ -206,7 +204,7 @@ class BaseAnomalyDetector(BaseSeriesEstimator, ABC):
         X = self._preprocess_series(X, axis, True)
 
         if self.get_class_tag("fit_is_empty"):
-            self._is_fitted = True
+            self.is_fitted = True
             return self._predict(X)
 
         if y is not None:
@@ -215,7 +213,7 @@ class BaseAnomalyDetector(BaseSeriesEstimator, ABC):
         pred = self._fit_predict(X, y)
 
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return pred
 
     def _fit(self, X, y):
diff --git a/aeon/base/_base.py b/aeon/base/_base.py
index 80324d34..f25134c9 100644
--- a/aeon/base/_base.py
+++ b/aeon/base/_base.py
@@ -1,37 +1,4 @@
-"""
-Base class template for estimators.
-
-Interface specifications below.
-
----
-
-Parameter inspection and setter methods
-    inspect parameter values      - get_params()
-    setting parameter values      - set_params(**params)
-    fitted parameter inspection - get_fitted_params()
-
-Tag inspection and setter methods
-    inspect tags (all)            - get_tags()
-    inspect tags (one tag)        - get_tag(tag_name: str, tag_value_default=None)
-    inspect tags (class method)   - get_class_tags()
-    inspect tags (one tag, class) - get_class_tag(tag_name:str, tag_value_default=None)
-    setting dynamic tags          - set_tags(**tag_dict: dict)
-    set/clone dynamic tags        - clone_tags(estimator, tag_names=None)
-
-Blueprinting: resetting and cloning, post-init state with same hyper-parameters
-    reset estimator to post-init  - reset()
-    cloneestimator (copy&reset)   - clone()
-
-Testing with default parameters methods
-    getting default parameters (all sets)         - get_test_params()
-    get one test instance with default parameters - create_test_instance()
-    get list of all test instances plus name list - create_test_instances_and_names()
-
-State:
-    fitted model/strategy   - by convention, any attributes ending in "_"
-    fitted state flag       - is_fitted (property)
-    fitted state check      - check_is_fitted (raises error if not is_fitted)
-"""
+"""Base class template for aeon estimators."""
 
 __maintainer__ = ["MatthewMiddlehurst", "TonyBagnall"]
 __all__ = ["BaseEstimator"]
@@ -46,40 +13,72 @@ from sklearn.exceptions import NotFittedError
 
 
 class BaseEstimator(_BaseEstimator):
-    """Base class for defining estimators in aeon."""
+    """
+    Base class for defining estimators in aeon.
+
+    Contains the following methods:
+
+    reset estimator to post-init  - reset(keep)
+    clonee stimator (copy)        - clone(random_state)
+    inspect tags (class method)   - get_class_tags()
+    inspect tags (one tag, class) - get_class_tag(tag_name, tag_value_default,
+                                                                    raise_error)
+    inspect tags (all)            - get_tags()
+    inspect tags (one tag)        - get_tag(tag_name, tag_value_default, raise_error)
+    setting dynamic tags          - set_tags(**tag_dict)
+
+    All estimators have the attribute:
+
+    fitted state flag             - is_fitted
+    """
 
     _tags = {
         "python_version": None,
         "python_dependencies": None,
-        "cant-pickle": False,
-        "non-deterministic": False,
+        "cant_pickle": False,
+        "non_deterministic": False,
         "algorithm_type": None,
         "capability:missing_values": False,
         "capability:multithreading": False,
     }
 
     def __init__(self):
-        self._is_fitted = False  # flag to indicate if fit has been called
+        self.is_fitted = False  # flag to indicate if fit has been called
         self._tags_dynamic = dict()  # storage for dynamic tags
 
         super().__init__()
 
     def reset(self, keep=None):
-        """Reset the object to a clean post-init state.
+        """
+        Reset the object to a clean post-init state.
 
-        Equivalent to sklearn.clone but overwrites self.
-        After ``self.reset()`` call, self is equal in value to
-        ``type(self)(**self.get_params(deep=False))``
+        After a ``self.reset()`` call, self is equal or similar in value to
+        ``type(self)(**self.get_params(deep=False))``, assuming no other attributes
+        were kept using ``keep``.
 
-        Detail behaviour:
-        removes any object attributes, except:
-            hyper-parameters = arguments of ``__init__``
-            object attributes containing double-underscores, i.e., the string "__"
-        runs ``__init__`` with current values of hyper-parameters (result of get_params)
+        Detailed behaviour:
+            removes any object attributes, except:
+                hyper-parameters (arguments of ``__init__``)
+                object attributes containing double-underscores, i.e., the string "__"
+            runs ``__init__`` with current values of hyperparameters (result of
+            get_params)
 
         Not affected by the reset are:
-        object attributes containing double-underscores
-        class and object methods, class attributes
+            object attributes containing double-underscores
+            class and object methods, class attributes
+            any attributes specified in the ``keep`` argument
+
+        Parameters
+        ----------
+        keep : None, str, or list of str, default=None
+            If None, all attributes are removed except hyper-parameters.
+            If str, only the attribute with this name is kept.
+            If list of str, only the attributes with these names are kept.
+
+        Returns
+        -------
+        self
+            Reference to self.
         """
         # retrieve parameters to copy them later
         params = self.get_params(deep=False)
@@ -111,15 +110,23 @@ class BaseEstimator(_BaseEstimator):
 
     def clone(self, random_state=None):
         """
-        Obtain a clone of the object with same hyper-parameters.
+        Obtain a clone of the object with the same hyperparameters.
 
         A clone is a different object without shared references, in post-init state.
-        This function is equivalent to returning sklearn.clone of self.
+        This function is equivalent to returning ``sklearn.clone`` of self.
         Equal in value to ``type(self)(**self.get_params(deep=False))``.
 
+        Parameters
+        ----------
+        random_state : int, RandomState instance, or None, default=None
+            Sets the random state of the clone. If None, the random state is not set.
+            If int, random_state is the seed used by the random number generator.
+            If RandomState instance, random_state is the random number generator.
+
         Returns
         -------
-        instance of ``type(self)``, clone of self (see above)
+        estimator : object
+            Instance of ``type(self)``, clone of self (see above)
         """
         estimator = clone(self)
 
@@ -136,19 +143,18 @@ class BaseEstimator(_BaseEstimator):
         Returns
         -------
         collected_tags : dict
-            Dictionary of tag name : tag value pairs. Collected from _tags
-            class attribute via nested inheritance. NOT overridden by dynamic
-            tags set by set_tags or mirror_tags.
+            Dictionary of tag name and tag value pairs.
+            Collected from ``_tags`` class attribute via nested inheritance.
+            These are not overridden by dynamic tags set by ``set_tags`` or class
+            ``__init__`` calls.
         """
         collected_tags = dict()
 
         # We exclude the last two parent classes: sklearn.base.BaseEstimator and
         # the basic Python object.
         for parent_class in reversed(inspect.getmro(cls)[:-2]):
+            # Need the if here because classes might not have non-default tags
             if hasattr(parent_class, "_tags"):
-                # Need the if here because mixins might not have _more_tags
-                # but might do redundant work in estimators
-                # (i.e. calling more tags on BaseEstimator multiple times)
                 more_tags = parent_class._tags
                 collected_tags.update(more_tags)
 
@@ -170,20 +176,16 @@ class BaseEstimator(_BaseEstimator):
 
         Returns
         -------
-        tag_value :
-            Value of the `tag_name` tag in self. If not found, returns an error if
-            raise_error is True, otherwise it returns `tag_value_default`.
+        tag_value
+            Value of the ``tag_name`` tag in self.
+            If not found, returns an error if raise_error is True, otherwise it
+            returns `tag_value_default`.
 
         Raises
         ------
-        ValueError if raise_error is True i.e. if tag_name is not in self.get_tags(
-        ).keys()
-
-        See Also
-        --------
-        get_tag : Get a single tag from an object.
-        get_tags : Get all tags from an object.
-        get_class_tag : Get a single tag from a class.
+        ValueError
+            if raise_error is ``True`` and ``tag_name`` is not in
+            ``self.get_tags().keys()``
 
         Examples
         --------
@@ -202,41 +204,26 @@ class BaseEstimator(_BaseEstimator):
 
     def get_tags(self):
         """
-        Get tags from estimator class.
+        Get tags from estimator.
 
-        Includes the dynamic tag overrides.
+        Includes dynamic and overridden tags.
 
         Returns
         -------
-        dict
-            Dictionary of tag name : tag value pairs. Collected from _tags
-            class attribute via nested inheritance and then any overrides
-            and new tags from _tags_dynamic object attribute.
-
-        See Also
-        --------
-        get_tag : Get a single tag from an object.
-        get_class_tags : Get all tags from a class.
-        get_class_tag : Get a single tag from a class.
-
-        Examples
-        --------
-        >>> from aeon.classification import DummyClassifier
-        >>> d = DummyClassifier()
-        >>> tags = d.get_tags()
+        collected_tags : dict
+            Dictionary of tag name and tag value pairs.
+            Collected from ``_tags`` class attribute via nested inheritance and
+            then any overridden and new tags from ``__init__`` or ``set_tags``.
         """
         collected_tags = self.get_class_tags()
-
-        if hasattr(self, "_tags_dynamic"):
-            collected_tags.update(self._tags_dynamic)
-
+        collected_tags.update(self._tags_dynamic)
         return deepcopy(collected_tags)
 
     def get_tag(self, tag_name, tag_value_default=None, raise_error=True):
         """
         Get tag value from estimator class.
 
-        Uses dynamic tag overrides.
+        Includes dynamic and overridden tags.
 
         Parameters
         ----------
@@ -249,20 +236,16 @@ class BaseEstimator(_BaseEstimator):
 
         Returns
         -------
-        tag_value :
-            Value of the `tag_name` tag in self. If not found, returns an error if
-            raise_error is True, otherwise it returns `tag_value_default`.
+        tag_value
+            Value of the ``tag_name`` tag in self.
+            If not found, returns an error if raise_error is True, otherwise it
+            returns `tag_value_default`.
 
         Raises
         ------
-        ValueError if raise_error is True i.e. if tag_name is not in self.get_tags(
-        ).keys()
-
-        See Also
-        --------
-        get_tags : Get all tags from an object.
-        get_clas_tags : Get all tags from a class.
-        get_class_tag : Get a single tag from a class.
+        ValueError
+            if raise_error is ``True`` and ``tag_name`` is not in
+            ``self.get_tags().keys()``
 
         Examples
         --------
@@ -287,24 +270,15 @@ class BaseEstimator(_BaseEstimator):
         Parameters
         ----------
         **tag_dict : dict
-            Dictionary of tag name : tag value pairs.
+            Dictionary of tag name and tag value pairs.
 
         Returns
         -------
-        Self :
+        self
             Reference to self.
-
-        Notes
-        -----
-        Changes object state by setting tag values in tag_dict as dynamic tags
-        in self.
         """
         tag_update = deepcopy(tag_dict)
-        if hasattr(self, "_tags_dynamic"):
-            self._tags_dynamic.update(tag_update)
-        else:
-            self._tags_dynamic = tag_update
-
+        self._tags_dynamic.update(tag_update)
         return self
 
     @classmethod
@@ -352,11 +326,7 @@ class BaseEstimator(_BaseEstimator):
             Instance of the class with default parameters. If return_first
             is False, returns list of instances.
         """
-        # todo, update all methods to use parameter_set and remove when done
-        if "parameter_set" in inspect.getfullargspec(cls.get_test_params).args:
-            params = cls.get_test_params(parameter_set=parameter_set)
-        else:
-            params = cls.get_test_params()
+        params = cls.get_test_params(parameter_set=parameter_set)
 
         if isinstance(params, list):
             if return_first:
@@ -369,113 +339,6 @@ class BaseEstimator(_BaseEstimator):
             else:
                 return [cls(**params)]
 
-    @classmethod
-    def create_test_instances_and_names(cls, parameter_set="default"):
-        """
-        Create list of all test instances and a list of names for them.
-
-        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
-        -------
-        objs : list of instances of cls
-            i-th instance is cls(**cls.get_test_params()[i]).
-        names : list of str, same length as objs
-            i-th element is name of i-th instance of obj in tests
-            convention is {cls.__name__}-{i} if more than one instance
-            otherwise {cls.__name__}.
-        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.
-        """
-        if "parameter_set" in inspect.getfullargspec(cls.get_test_params).args:
-            param_list = cls.get_test_params(parameter_set=parameter_set)
-        else:
-            param_list = cls.get_test_params()
-
-        objs = []
-        if not isinstance(param_list, (dict, list)):
-            raise RuntimeError(
-                f"Error in {cls.__name__}.get_test_params, "
-                "return must be param dict for class, or list thereof"
-            )
-        if isinstance(param_list, dict):
-            param_list = [param_list]
-        for params in param_list:
-            if not isinstance(params, dict):
-                raise RuntimeError(
-                    f"Error in {cls.__name__}.get_test_params, "
-                    "return must be param dict for class, or list thereof"
-                )
-            objs += [cls(**params)]
-
-        n_cases = len(param_list)
-        if n_cases > 1:
-            names = [cls.__name__ + "-" + str(i) for i in range(n_cases)]
-        else:
-            names = [cls.__name__]
-
-        return objs, names
-
-    @classmethod
-    def _has_implementation_of(cls, method):
-        """
-        Check if method has a concrete implementation in this class.
-
-        This assumes that having an implementation is equivalent to
-            one or more overrides of `method` in the method resolution order.
-
-        Parameters
-        ----------
-        method : str
-            name of method to check implementation of.
-
-        Returns
-        -------
-        bool, whether method has implementation in cls
-            True if cls.method has been overridden at least once in
-                the inheritance tree (according to method resolution order).
-        """
-        # walk through method resolution order and inspect methods
-        #   of classes and direct parents, "adjacent" classes in mro
-        mro = inspect.getmro(cls)
-        # collect all methods that are not none
-        methods = [getattr(c, method, None) for c in mro]
-        methods = [m for m in methods if m is not None]
-
-        for i in range(len(methods) - 1):
-            # the method has been overridden once iff
-            #  at least two of the methods collected are not equal
-            #  equivalently: some two adjacent methods are not equal
-            overridden = methods[i] != methods[i + 1]
-            if overridden:
-                return True
-
-        return False
-
-    def is_composite(self):
-        """
-        Check if the object is composite.
-
-        A composite object is an object which contains objects, as parameters.
-        Called on an instance, since this may differ by instance.
-
-        Returns
-        -------
-        composite: bool
-            Whether self contains a parameter which is BaseEstimator.
-        """
-        # walk through method resolution order and inspect methods
-        #   of classes and direct parents, "adjacent" classes in mro
-        params = self.get_params(deep=False)
-        composite = any(isinstance(x, BaseEstimator) for x in params.values())
-
-        return composite
-
     def _components(self, base_class=None):
         """
         Return references to all state changing BaseEstimator type attributes.
@@ -605,11 +468,6 @@ class BaseEstimator(_BaseEstimator):
         with ZipFile(serial, "r") as file:
             return pickle.loads(file.open("_obj").read())
 
-    @property
-    def is_fitted(self):
-        """Whether ``fit`` has been called."""
-        return self._is_fitted
-
     def check_is_fitted(self):
         """
         Check if the estimator has been fitted.
@@ -679,7 +537,7 @@ class BaseEstimator(_BaseEstimator):
         # add all nested parameters from components that are aeon BaseEstimator
         c_dict = self._components()
         for c, comp in c_dict.items():
-            if isinstance(comp, BaseEstimator) and comp._is_fitted:
+            if isinstance(comp, BaseEstimator) and comp.is_fitted:
                 c_f_params = comp.get_fitted_params()
                 c_f_params = {f"{sh(c)}__{k}": v for k, v in c_f_params.items()}
                 fitted_params.update(c_f_params)
@@ -745,8 +603,21 @@ class BaseEstimator(_BaseEstimator):
         """
         return self._get_fitted_params_default()
 
+    # override some sklearn private methods
+
+    def __sklearn_is_fitted__(self):
+        """Check fitted status and return a Boolean value."""
+        return self.is_fitted
+
+    def _validate_data(self, **kwargs):
+        """Sklearn data validation."""
+        raise NotImplementedError(
+            "aeon estimators do not have a _validate_data method."
+        )
+
 
 def _clone_estimator(base_estimator, random_state=None):
+    """Clone an estimator."""
     estimator = clone(base_estimator)
 
     if random_state is not None:
diff --git a/aeon/base/estimator/compose/collection_pipeline.py b/aeon/base/estimator/compose/collection_pipeline.py
index 0cac282a..a5708af7 100644
--- a/aeon/base/estimator/compose/collection_pipeline.py
+++ b/aeon/base/estimator/compose/collection_pipeline.py
@@ -111,9 +111,7 @@ class BaseCollectionPipeline(_HeterogenousMetaEstimator, BaseCollectionEstimator
             if (
                 isinstance(e[1], BaseEstimator)
                 and e[1].get_tag("capability:missing_values", False, raise_error=False)
-                and e[1].get_tag(
-                    "capability:missing_values:removes", False, raise_error=False
-                )
+                and e[1].get_tag("removes_missing_values", False, raise_error=False)
             ):
                 missing_rm_tag = True
                 break
@@ -142,9 +140,7 @@ class BaseCollectionPipeline(_HeterogenousMetaEstimator, BaseCollectionEstimator
                 isinstance(e[1], BaseEstimator)
                 and e[1].get_tag("capability:unequal_length", False, raise_error=False)
                 and (
-                    e[1].get_tag(
-                        "capability:unequal_length:removes", False, raise_error=False
-                    )
+                    e[1].get_tag("removes_unequal_length", False, raise_error=False)
                     or e[1].get_tag("output_data_type", raise_error=False) == "Tabular"
                 )
             ):
diff --git a/aeon/classification/base.py b/aeon/classification/base.py
index 81fcd74a..b43e5ec7 100644
--- a/aeon/classification/base.py
+++ b/aeon/classification/base.py
@@ -119,7 +119,7 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
 
         self.fit_time_ = int(round(time.time() * 1000)) - start
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     @final
@@ -271,7 +271,7 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
             y_pred = self._fit_predict(X, y, **kwargs)
 
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return y_pred
 
     @final
@@ -340,7 +340,7 @@ class BaseClassifier(BaseCollectionEstimator, ABC):
             y_proba = self._fit_predict_proba(X, y, **kwargs)
 
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return y_proba
 
     def score(
diff --git a/aeon/classification/deep_learning/_inception_time.py b/aeon/classification/deep_learning/_inception_time.py
index 6b377d56..90a9edd9 100644
--- a/aeon/classification/deep_learning/_inception_time.py
+++ b/aeon/classification/deep_learning/_inception_time.py
@@ -160,8 +160,8 @@ class InceptionTimeClassifier(BaseClassifier):
     _tags = {
         "python_dependencies": "tensorflow",
         "capability:multivariate": True,
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "algorithm_type": "deeplearning",
     }
 
diff --git a/aeon/classification/deep_learning/_lite_time.py b/aeon/classification/deep_learning/_lite_time.py
index f16e136d..e00abe79 100644
--- a/aeon/classification/deep_learning/_lite_time.py
+++ b/aeon/classification/deep_learning/_lite_time.py
@@ -110,8 +110,8 @@ class LITETimeClassifier(BaseClassifier):
     _tags = {
         "python_dependencies": "tensorflow",
         "capability:multivariate": True,
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "algorithm_type": "deeplearning",
     }
 
diff --git a/aeon/classification/deep_learning/base.py b/aeon/classification/deep_learning/base.py
index 837945ee..efa83ba6 100644
--- a/aeon/classification/deep_learning/base.py
+++ b/aeon/classification/deep_learning/base.py
@@ -42,8 +42,8 @@ class BaseDeepClassifier(BaseClassifier, ABC):
         "X_inner_type": "numpy3D",
         "capability:multivariate": True,
         "algorithm_type": "deeplearning",
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "python_dependencies": "tensorflow",
     }
 
@@ -184,7 +184,7 @@ class BaseDeepClassifier(BaseClassifier, ABC):
         import tensorflow as tf
 
         self.model_ = tf.keras.models.load_model(model_path)
-        self._is_fitted = True
+        self.is_fitted = True
 
         self.classes_ = classes
         self.n_classes_ = len(self.classes_)
diff --git a/aeon/classification/dictionary_based/_boss.py b/aeon/classification/dictionary_based/_boss.py
index b88b5e58..fb37e098 100644
--- a/aeon/classification/dictionary_based/_boss.py
+++ b/aeon/classification/dictionary_based/_boss.py
@@ -659,7 +659,7 @@ class IndividualBOSS(BaseClassifier):
         new_boss.classes_ = self.classes_
         new_boss._class_dictionary = self._class_dictionary
         new_boss.metadata_ = self.metadata_
-        new_boss._is_fitted = True
+        new_boss.is_fitted = True
 
         return new_boss
 
diff --git a/aeon/classification/dictionary_based/_mrsqm.py b/aeon/classification/dictionary_based/_mrsqm.py
index 3ebe00a0..ce793f54 100644
--- a/aeon/classification/dictionary_based/_mrsqm.py
+++ b/aeon/classification/dictionary_based/_mrsqm.py
@@ -73,7 +73,7 @@ class MrSQMClassifier(BaseClassifier):
     _tags = {
         "X_inner_type": "numpy3D",
         "algorithm_type": "dictionary",
-        "cant-pickle": True,
+        "cant_pickle": True,
         "python_dependencies": "mrsqm",
     }
 
diff --git a/aeon/classification/dictionary_based/_muse.py b/aeon/classification/dictionary_based/_muse.py
index f2d0a42a..2052d875 100644
--- a/aeon/classification/dictionary_based/_muse.py
+++ b/aeon/classification/dictionary_based/_muse.py
@@ -107,7 +107,7 @@ class MUSE(BaseClassifier):
     References
     ----------
     .. [1] Patrick Schäfer and Ulf Leser, "Multivariate time series classification
-        with WEASEL+MUSE", in proc 3rd ECML/PKDD Workshop on AALTD}, 2018
+        with WEASEL+MUSE", in proc 3rd ECML/PKDD Workshop on AALTD, 2018
         https://arxiv.org/abs/1711.11343
 
     Notes
@@ -131,6 +131,7 @@ class MUSE(BaseClassifier):
     """
 
     _tags = {
+        "capability:univariate": False,
         "capability:multivariate": True,
         "capability:multithreading": True,
         "algorithm_type": "dictionary",
diff --git a/aeon/classification/dictionary_based/_tde.py b/aeon/classification/dictionary_based/_tde.py
index cdeffc42..2c706720 100644
--- a/aeon/classification/dictionary_based/_tde.py
+++ b/aeon/classification/dictionary_based/_tde.py
@@ -410,13 +410,6 @@ class TemporalDictionaryEnsemble(BaseClassifier):
             n_cases, n_classes_).
 
         """
-        _, _, n_timepoints = X.shape
-        if n_timepoints != self.n_timepoints_:
-            raise TypeError(
-                "ERROR number of attributes in the train does not match "
-                "that in the test data"
-            )
-
         sums = np.zeros((X.shape[0], self.n_classes_))
 
         for n, clf in enumerate(self.estimators_):
@@ -1003,14 +996,14 @@ def histogram_intersection(first, second):
     """Find the distance between two histograms using the histogram intersection.
 
     This distance function is designed for sparse matrix, represented as a
-    dictionary or numba Dict, but can accept arrays.
+    dictionary or numba Dict, but can accept arrays in dense format.
 
     Parameters
     ----------
-    first : dict, numba.Dict or array
-        First dictionary used in distance measurement.
-    second : dict, numba.Dict or array
-        Second dictionary that will be used to measure distance from `first`.
+    first : dict, numba.Dict or 1 D array of integers
+        First histogram used in distance measurement.
+    second : dict, numba.Dict or 1 D array of integers
+        Second histogram that will be used to measure distance from `first`.
 
     Returns
     -------
@@ -1028,7 +1021,7 @@ def histogram_intersection(first, second):
     else:
         return np.sum(
             [
-                0 if first[n] == 0 else np.min(first[n], second[n])
+                0 if first[n] == 0 else np.minimum(first[n], second[n])
                 for n in range(len(first))
             ]
         )
diff --git a/aeon/classification/early_classification/base.py b/aeon/classification/early_classification/base.py
index 87449b7c..407954ea 100644
--- a/aeon/classification/early_classification/base.py
+++ b/aeon/classification/early_classification/base.py
@@ -120,7 +120,7 @@ class BaseEarlyClassifier(BaseCollectionEstimator, ABC):
         self._fit(X, y)
         self.fit_time_ = int(round(time.time() * 1000)) - start
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     def predict(self, X) -> tuple[np.ndarray, np.ndarray]:
diff --git a/aeon/classification/hybrid/_rist.py b/aeon/classification/hybrid/_rist.py
index b991ba4f..3e85336e 100644
--- a/aeon/classification/hybrid/_rist.py
+++ b/aeon/classification/hybrid/_rist.py
@@ -142,7 +142,7 @@ class RISTClassifier(BaseRIST, BaseClassifier):
     }
 
     @classmethod
-    def get_test_params(cls, parameter_set=None):
+    def get_test_params(cls, parameter_set="default"):
         """Return unit test parameter settings for the estimator.
 
         Parameters
diff --git a/aeon/classification/shapelet_based/_ls.py b/aeon/classification/shapelet_based/_ls.py
index 1c667611..06cc9fbc 100644
--- a/aeon/classification/shapelet_based/_ls.py
+++ b/aeon/classification/shapelet_based/_ls.py
@@ -95,7 +95,7 @@ class LearningShapeletClassifier(BaseClassifier):
     _tags = {
         "capability:multivariate": True,
         "algorithm_type": "shapelet",
-        "cant-pickle": True,
+        "cant_pickle": True,
         "python_dependencies": ["tslearn", "tensorflow"],
     }
 
@@ -173,7 +173,7 @@ class LearningShapeletClassifier(BaseClassifier):
         array of shape=(n_ts, n_shapelets)
             Shapelet-Transform of the provided time series.
         """
-        if not self._is_fitted:
+        if not self.is_fitted:
             raise ValueError(
                 "You must fit the classifier before recovering the transform"
             )
@@ -197,7 +197,7 @@ class LearningShapeletClassifier(BaseClassifier):
         array of shape=(n_ts, n_shapelets)
             Location of the shapelet matches for the provided time series.
         """
-        if not self._is_fitted:
+        if not self.is_fitted:
             raise ValueError(
                 "You must fit the classifier before recovering the transform"
             )
diff --git a/aeon/classification/shapelet_based/_rdst.py b/aeon/classification/shapelet_based/_rdst.py
index 2e0b0ab6..166ca100 100644
--- a/aeon/classification/shapelet_based/_rdst.py
+++ b/aeon/classification/shapelet_based/_rdst.py
@@ -60,9 +60,6 @@ class RDSTClassifier(BaseClassifier):
         If True, restricts the value of the shapelet dilation parameter to be prime
         values. This can greatly speed-up the algorithm for long time series and/or
         short shapelet lengths, possibly at the cost of some accuracy.
-    distance: str="manhattan"
-        Name of the distance function to be used. By default this is the
-        manhattan distance. Other distances from the aeon distance modules can be used.
     estimator : BaseEstimator or None, default=None
         Base estimator for the ensemble, can be supplied a sklearn `BaseEstimator`. If
         `None` a default `RidgeClassifierCV` classifier is used with standard scaling.
@@ -135,7 +132,7 @@ class RDSTClassifier(BaseClassifier):
         "capability:unequal_length": True,
         "capability:multithreading": True,
         "X_inner_type": ["np-list", "numpy3D"],
-        "non-deterministic": True,  # due to random_state bug in MacOS #324
+        "non_deterministic": True,  # due to random_state bug in MacOS #324
         "algorithm_type": "shapelet",
     }
 
@@ -147,7 +144,6 @@ class RDSTClassifier(BaseClassifier):
         threshold_percentiles=None,
         alpha_similarity: float = 0.5,
         use_prime_dilations: bool = False,
-        distance: str = "manhattan",
         estimator=None,
         save_transformed_data: bool = False,
         class_weight=None,
@@ -160,7 +156,6 @@ class RDSTClassifier(BaseClassifier):
         self.threshold_percentiles = threshold_percentiles
         self.alpha_similarity = alpha_similarity
         self.use_prime_dilations = use_prime_dilations
-        self.distance = distance
         self.estimator = estimator
         self.save_transformed_data = save_transformed_data
         self.class_weight = class_weight
@@ -203,7 +198,6 @@ class RDSTClassifier(BaseClassifier):
             use_prime_dilations=self.use_prime_dilations,
             n_jobs=self.n_jobs,
             random_state=self.random_state,
-            distance=self.distance,
         )
         if self.estimator is None:
             self._estimator = make_pipeline(
diff --git a/aeon/clustering/_k_means.py b/aeon/clustering/_k_means.py
index 54d05e8c..c947be4a 100644
--- a/aeon/clustering/_k_means.py
+++ b/aeon/clustering/_k_means.py
@@ -226,7 +226,6 @@ class TimeSeriesKMeans(BaseClusterer):
                     print("Resumed because of empty cluster")  # noqa: T001, T201
 
         if best_labels is None:
-            self._is_fitted = False
             raise ValueError(
                 "Unable to find a valid cluster configuration "
                 "with parameters specified (empty clusters kept "
diff --git a/aeon/clustering/base.py b/aeon/clustering/base.py
index 2cfbc9c2..0c9a6c56 100644
--- a/aeon/clustering/base.py
+++ b/aeon/clustering/base.py
@@ -63,7 +63,7 @@ class BaseClusterer(BaseCollectionEstimator, ABC):
         X = self._preprocess_collection(X)
         self._fit(X)
         self.fit_time_ = int(round(time.time() * 1000)) - _start_time
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     @final
diff --git a/aeon/clustering/deep_learning/base.py b/aeon/clustering/deep_learning/base.py
index f0c7eba1..fc7b54c7 100644
--- a/aeon/clustering/deep_learning/base.py
+++ b/aeon/clustering/deep_learning/base.py
@@ -35,8 +35,8 @@ class BaseDeepClusterer(BaseClusterer, ABC):
         "X_inner_type": "numpy3D",
         "capability:multivariate": True,
         "algorithm_type": "deeplearning",
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "python_dependencies": "tensorflow",
     }
 
diff --git a/aeon/datasets/__init__.py b/aeon/datasets/__init__.py
index 52cc29cb..66c2cb06 100644
--- a/aeon/datasets/__init__.py
+++ b/aeon/datasets/__init__.py
@@ -16,7 +16,6 @@ __all__ = [
     "load_human_activity_segmentation_datasets",
     # Write functions
     "write_to_tsfile",
-    "write_to_tsf_file",
     "write_to_arff_file",
     "write_results_to_uea_format",
     # Single problem loaders
@@ -67,7 +66,6 @@ from aeon.datasets._data_loaders import (
 from aeon.datasets._data_writers import (
     write_results_to_uea_format,
     write_to_arff_file,
-    write_to_tsf_file,
     write_to_tsfile,
 )
 from aeon.datasets._dataframe_loaders import (
diff --git a/aeon/datasets/_data_writers.py b/aeon/datasets/_data_writers.py
index 4c4a7b11..3aa4ed50 100644
--- a/aeon/datasets/_data_writers.py
+++ b/aeon/datasets/_data_writers.py
@@ -107,7 +107,7 @@ def _write_data_to_tsfile(
     """
     # ensure data provided is a ndarray
     if not isinstance(X, np.ndarray) and not isinstance(X, list):
-        raise TypeError("Data provided must be a ndarray or a list")
+        raise ValueError("Data provided must be a ndarray or a list")
     class_labels = None
     if y is not None:
         # ensure number of cases is same as the class value list
@@ -245,201 +245,6 @@ def _write_header(
     return file
 
 
-def write_to_tsf_file(
-    X,
-    path,
-    y=None,
-    problem_name="sample_data.tsf",
-    header=None,
-    attribute=None,
-    frequency=None,
-    horizon=0,
-):
-    """Write an aeon collection of time series to text file in .tsf format.
-
-    Write metadata and data stored in aeon compatible data set to file.
-    A description of the tsf format is in examples/load_data.ipynb.
-
-    Parameters
-    ----------
-    X : pd.DataFrame, each cell a pd.Series
-        Collection of time series: univariate, multivariate, equal or unequal length.
-    path : string.
-        Location of the directory to write file
-    y: None or pd.Series, default = None
-        Response variable, discrete for classification, continuous for regression
-        None if clustering.
-    problem_name : string, default = "sample_data"
-        The file is written to <path>/<problem_name>/<problem_name>.tsf
-    header: string, default = None
-        Optional text at the top of the file that is ignored when loading.
-    """
-    if not (isinstance(X, pd.DataFrame)):
-        raise TypeError(f" Wrong input data type {type(X)} convert to pd.DataFrame")
-
-    # See if passed file name contains .tsf extension or not
-    split = problem_name.split(".")
-    if split[-1] != "tsf":
-        problem_name = problem_name + ".tsf"
-
-    _write_dataframe_to_tsf_file(
-        X,
-        path,
-        y=None,
-        problem_name=problem_name,
-        attribute=attribute,
-        frequency=frequency,
-        horizon=horizon,
-        comment=header,
-    )
-
-
-def _write_dataframe_to_tsf_file(
-    X,
-    path,
-    y=None,
-    problem_name="sample_data",
-    comment=None,
-    attribute=None,
-    frequency=None,
-    horizon=0,
-):
-    # ensure data provided is a dataframe
-    if not isinstance(X, pd.DataFrame):
-        raise ValueError(f"Data provided must be a DataFrame, passed a {type(X)}")
-    # See if passed file name contains .tsf extension or not
-    split = problem_name.split(".")
-    if split[-1] != "tsf":
-        problem_name = problem_name + ".tsf"
-    equal_length = not X.isnull().values.any()
-    missing = X.isnull().values.any()
-    if frequency is None:
-        frequency = calculate_frequency(X)
-    if attribute is None:
-        attribute = {"series_name": "string", "start_timestamp": "date"}
-
-    file = _write_header_tsf(
-        path,
-        problem_name=problem_name,
-        attribute=attribute,
-        equal_length=equal_length,
-        frequency=frequency,
-        horizon=horizon,
-        missing=missing,
-        comment=comment,
-    )
-
-    X = X.reset_index(drop=False, inplace=False)
-
-    n_cases, n_channels = X.shape
-
-    for j in range(1, n_channels):
-        column_name = X.columns[j]
-        file.write(f"{str(column_name)}:")
-
-        # Find the index of the first non-empty value in the column
-        first_non_empty_index = X.iloc[:, j].first_valid_index()
-        start_timestamp_index = None
-
-        if first_non_empty_index is not None:
-            start_timestamp_index = X.index[first_non_empty_index]
-            start_timestamp = X.iloc[start_timestamp_index, 0].strftime(
-                "%Y-%m-%d %H-%M-%S"
-            )
-        file.write(f"{str(start_timestamp)}:")
-
-        for i in range(start_timestamp_index, n_cases - 1):
-            series = X.iloc[i, j]
-            # Check if the value is NaN
-            if pd.notna(series):
-                series_str = str(series)
-            else:
-                series_str = "?"  # Replace NaN with a ?
-
-            # Write the series string to the file
-            file.write(f"{series_str},")
-
-        series = X.iloc[-1, j]
-        # Check if he value is NaN
-        if pd.notna(series):
-            series_str = str(series)
-        else:
-            series_str = "?"  # Replace NaN with a ?
-        # Write the series string to the file
-        file.write(f"{series_str}")
-
-        # Check if y is not None before accessing its elements
-        if y is not None:
-            file.write(f"{y[i]}\n")
-        else:
-            file.write("\n")  # Write a newline if y is None
-    file.close()
-
-
-def _write_header_tsf(
-    path,
-    problem_name,
-    attribute,
-    equal_length=True,
-    frequency=None,
-    horizon=0,
-    missing=False,
-    comment=None,
-):
-    if not os.path.exists(path):
-        os.makedirs(path)
-    # See if passed file name contains .tsf extension or not
-    split = problem_name.split(".")
-    if split[-1] != "tsf":
-        problem_name = problem_name + ".tsf"
-    load_path = f"{path}/{problem_name}"
-
-    file = open(load_path, "w")
-
-    if comment is not None:
-        file.write("\n# ".join(textwrap.wrap("# " + comment)))
-        file.write("\n")
-
-    file.write(f"@relation {str(split[0]).lower()}\n")
-    # Write attribute metadata for each column
-    if attribute is not None:
-        for attr in attribute:
-            file.write(f"@attribute {str(attr)} {str(attribute[attr])}\n")
-    file.write(f"@frequency {str(frequency).lower()}\n")
-    file.write(f"@horizon {str(horizon).lower()}\n")
-    file.write(f"@missing {str(missing).lower()}\n")
-    file.write(f"@equallength {str(equal_length).lower()}\n")
-    file.write("@data\n")
-
-    return file
-
-
-def calculate_frequency(df):
-    # Convert timestamps to DateTime format
-    df["Timestamp"] = pd.to_datetime(df.index)
-
-    # Calculate time differences
-    time_diffs = df["Timestamp"].diff().dropna()
-
-    # Calculate median time difference
-    median_diff = time_diffs.median()
-
-    # Determine frequency based on median time difference
-    if median_diff <= pd.Timedelta(days=1):
-        frequency = "daily"
-    elif median_diff <= pd.Timedelta(weeks=1):
-        frequency = "weekly"
-    elif median_diff <= pd.Timedelta(days=30):
-        frequency = "monthly"
-    elif median_diff <= pd.Timedelta(days=365):
-        frequency = "yearly"
-    else:
-        frequency = "other"  # You can define more granular frequencies as needed
-    df.drop("Timestamp", axis=1, inplace=True)
-
-    return frequency
-
-
 def write_to_arff_file(
     X,
     y,
diff --git a/aeon/distances/_shift_scale_invariant.py b/aeon/distances/_shift_scale_invariant.py
index 4c99182c..2a07beb2 100644
--- a/aeon/distances/_shift_scale_invariant.py
+++ b/aeon/distances/_shift_scale_invariant.py
@@ -132,7 +132,7 @@ def _univariate_shift_scale_invariant_distance(
     x: np.ndarray, y: np.ndarray, max_shift: int
 ) -> tuple[float, np.ndarray]:
     if np.array_equal(x, y):
-        return 0, y
+        return 0.0, y
     min_dist = _scale_d(x, y)
     best_shifted_y = y
 
diff --git a/aeon/performance_metrics/forecasting/_functions.py b/aeon/performance_metrics/forecasting/_functions.py
index 0d8f56a4..c3928f2f 100644
--- a/aeon/performance_metrics/forecasting/_functions.py
+++ b/aeon/performance_metrics/forecasting/_functions.py
@@ -14,8 +14,8 @@ from sklearn.metrics._regression import _check_reg_targets
 from sklearn.utils.stats import _weighted_percentile
 from sklearn.utils.validation import check_consistent_length
 
+from aeon.utils import weighted_geometric_mean
 from aeon.utils.validation.series import check_series
-from aeon.utils.weighted_metrics import _weighted_geometric_mean
 
 __maintainer__ = []
 __all__ = [
@@ -1299,9 +1299,9 @@ def geometric_mean_absolute_error(
         output_errors = gmean(np.abs(errors), axis=0)
     else:
         check_consistent_length(y_true, horizon_weight)
-        output_errors = _weighted_geometric_mean(
+        output_errors = weighted_geometric_mean(
             np.abs(errors),
-            sample_weight=horizon_weight,
+            weights=horizon_weight,
             axis=0,
         )
 
@@ -1424,9 +1424,9 @@ def geometric_mean_squared_error(
         output_errors = gmean(np.square(errors), axis=0)
     else:
         check_consistent_length(y_true, horizon_weight)
-        output_errors = _weighted_geometric_mean(
+        output_errors = weighted_geometric_mean(
             np.square(errors),
-            sample_weight=horizon_weight,
+            weights=horizon_weight,
             axis=0,
         )
 
@@ -2258,7 +2258,7 @@ def geometric_mean_relative_absolute_error(
         )
     else:
         check_consistent_length(y_true, horizon_weight)
-        output_errors = _weighted_geometric_mean(
+        output_errors = weighted_geometric_mean(
             np.where(relative_errors == 0.0, EPS, relative_errors),
             weights=horizon_weight,
             axis=0,
@@ -2384,7 +2384,7 @@ def geometric_mean_relative_squared_error(
         )
     else:
         check_consistent_length(y_true, horizon_weight)
-        output_errors = _weighted_geometric_mean(
+        output_errors = weighted_geometric_mean(
             np.where(relative_errors == 0.0, EPS, relative_errors),
             weights=horizon_weight,
             axis=0,
diff --git a/aeon/regression/base.py b/aeon/regression/base.py
index ce3b1e34..f3e0be80 100644
--- a/aeon/regression/base.py
+++ b/aeon/regression/base.py
@@ -120,7 +120,7 @@ class BaseRegressor(BaseCollectionEstimator, ABC):
 
         self.fit_time_ = int(round(time.time() * 1000)) - start
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     @final
@@ -209,7 +209,7 @@ class BaseRegressor(BaseCollectionEstimator, ABC):
         y_pred = self._fit_predict(X, y)
 
         # this should happen last
-        self._is_fitted = True
+        self.is_fitted = True
         return y_pred
 
     def score(self, X, y, metric="r2", metric_params=None) -> float:
diff --git a/aeon/regression/convolution_based/_mr_hydra.py b/aeon/regression/convolution_based/_mr_hydra.py
index bcb66d96..c360f437 100644
--- a/aeon/regression/convolution_based/_mr_hydra.py
+++ b/aeon/regression/convolution_based/_mr_hydra.py
@@ -61,7 +61,7 @@ class MultiRocketHydraRegressor(BaseRegressor):
         "capability:multithreading": True,
         "algorithm_type": "convolution",
         "python_dependencies": "torch",
-        "non-deterministic": True,  # todo, presumed issue with mutlirocket
+        "non_deterministic": True,  # todo, presumed issue with mutlirocket
     }
 
     def __init__(self, n_kernels=8, n_groups=64, n_jobs=1, random_state=None):
diff --git a/aeon/regression/deep_learning/_inception_time.py b/aeon/regression/deep_learning/_inception_time.py
index a5bd4595..19493ac6 100644
--- a/aeon/regression/deep_learning/_inception_time.py
+++ b/aeon/regression/deep_learning/_inception_time.py
@@ -165,8 +165,8 @@ class InceptionTimeRegressor(BaseRegressor):
     _tags = {
         "python_dependencies": "tensorflow",
         "capability:multivariate": True,
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "algorithm_type": "deeplearning",
     }
 
diff --git a/aeon/regression/deep_learning/_lite_time.py b/aeon/regression/deep_learning/_lite_time.py
index 5a2079df..0d010431 100644
--- a/aeon/regression/deep_learning/_lite_time.py
+++ b/aeon/regression/deep_learning/_lite_time.py
@@ -111,8 +111,8 @@ class LITETimeRegressor(BaseRegressor):
     _tags = {
         "python_dependencies": "tensorflow",
         "capability:multivariate": True,
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "algorithm_type": "deeplearning",
     }
 
diff --git a/aeon/regression/deep_learning/base.py b/aeon/regression/deep_learning/base.py
index 987ea890..bf12de8a 100644
--- a/aeon/regression/deep_learning/base.py
+++ b/aeon/regression/deep_learning/base.py
@@ -35,8 +35,8 @@ class BaseDeepRegressor(BaseRegressor, ABC):
         "X_inner_type": "numpy3D",
         "capability:multivariate": True,
         "algorithm_type": "deeplearning",
-        "non-deterministic": True,
-        "cant-pickle": True,
+        "non_deterministic": True,
+        "cant_pickle": True,
         "python_dependencies": "tensorflow",
     }
 
@@ -129,7 +129,7 @@ class BaseDeepRegressor(BaseRegressor, ABC):
         import tensorflow as tf
 
         self.model_ = tf.keras.models.load_model(model_path)
-        self._is_fitted = True
+        self.is_fitted = True
 
     def _get_model_checkpoint_callback(self, callbacks, file_path, file_name):
         import tensorflow as tf
diff --git a/aeon/regression/hybrid/_rist.py b/aeon/regression/hybrid/_rist.py
index 735a1321..cf17bbea 100644
--- a/aeon/regression/hybrid/_rist.py
+++ b/aeon/regression/hybrid/_rist.py
@@ -134,7 +134,7 @@ class RISTRegressor(BaseRIST, BaseRegressor):
     }
 
     @classmethod
-    def get_test_params(cls, parameter_set=None):
+    def get_test_params(cls, parameter_set="default"):
         """Return unit test parameter settings for the estimator.
 
         Parameters
diff --git a/aeon/regression/shapelet_based/_rdst.py b/aeon/regression/shapelet_based/_rdst.py
index 6e0ef58d..4bd5e53d 100644
--- a/aeon/regression/shapelet_based/_rdst.py
+++ b/aeon/regression/shapelet_based/_rdst.py
@@ -113,7 +113,7 @@ class RDSTRegressor(BaseRegressor):
         "capability:unequal_length": True,
         "capability:multithreading": True,
         "X_inner_type": ["np-list", "numpy3D"],
-        "non-deterministic": True,  # due to random_state bug in MacOS #324
+        "non_deterministic": True,  # due to random_state bug in MacOS #324
         "algorithm_type": "shapelet",
     }
 
diff --git a/aeon/segmentation/_eagglo.py b/aeon/segmentation/_eagglo.py
index 9605d579..8ffa22cc 100644
--- a/aeon/segmentation/_eagglo.py
+++ b/aeon/segmentation/_eagglo.py
@@ -350,7 +350,7 @@ class EAggloSegmenter(BaseSegmenter):
         )
 
     @classmethod
-    def get_test_params(cls) -> list[dict]:
+    def get_test_params(cls, parameter_set: str = "default") -> list[dict]:
         """Test parameters."""
         return [
             {"alpha": 1.0, "penalty": None},
diff --git a/aeon/segmentation/base.py b/aeon/segmentation/base.py
index dee4e21a..6c294fc3 100644
--- a/aeon/segmentation/base.py
+++ b/aeon/segmentation/base.py
@@ -72,7 +72,6 @@ class BaseSegmenter(BaseSeriesEstimator, ABC):
 
     def __init__(self, axis, n_segments=2):
         self.n_segments = n_segments
-        self._is_fitted = False
 
         super().__init__(axis=axis)
 
@@ -107,7 +106,7 @@ class BaseSegmenter(BaseSeriesEstimator, ABC):
             Fitted estimator
         """
         if self.get_class_tag("fit_is_empty"):
-            self._is_fitted = True
+            self.is_fitted = True
             return self
         if self.get_class_tag("requires_y"):
             if y is None:
@@ -120,7 +119,7 @@ class BaseSegmenter(BaseSeriesEstimator, ABC):
         if y is not None:
             y = self._check_y(y)
         self._fit(X=X, y=y)
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     @final
diff --git a/aeon/transformations/base.py b/aeon/transformations/base.py
index bde6bc04..c6b53133 100644
--- a/aeon/transformations/base.py
+++ b/aeon/transformations/base.py
@@ -16,7 +16,7 @@ class BaseTransformer(BaseEstimator, ABC):
         "fit_is_empty": False,
         "capability:inverse_transform": False,
         "capability:missing_values": False,
-        "capability:missing_values:removes": False,
+        "removes_missing_values": False,
     }
 
     def __init__(self):
diff --git a/aeon/transformations/collection/_pad.py b/aeon/transformations/collection/_pad.py
index f7f66a63..f3e7f7de 100644
--- a/aeon/transformations/collection/_pad.py
+++ b/aeon/transformations/collection/_pad.py
@@ -53,7 +53,7 @@ class Padder(BaseCollectionTransformer):
         "fit_is_empty": False,
         "capability:multivariate": True,
         "capability:unequal_length": True,
-        "capability:unequal_length:removes": True,
+        "removes_unequal_length": True,
     }
 
     def __init__(self, pad_length=None, fill_value=0):
diff --git a/aeon/transformations/collection/_resize.py b/aeon/transformations/collection/_resize.py
index a622e7c0..c8189300 100644
--- a/aeon/transformations/collection/_resize.py
+++ b/aeon/transformations/collection/_resize.py
@@ -83,7 +83,7 @@ class Resizer(BaseCollectionTransformer):
         return np.array(Xt)
 
     @classmethod
-    def get_test_params(cls):
+    def get_test_params(cls, parameter_set="default"):
         """Return testing parameter settings for the estimator.
 
         Returns
diff --git a/aeon/transformations/collection/_truncate.py b/aeon/transformations/collection/_truncate.py
index ff90c5c2..8818b138 100644
--- a/aeon/transformations/collection/_truncate.py
+++ b/aeon/transformations/collection/_truncate.py
@@ -39,7 +39,7 @@ class Truncator(BaseCollectionTransformer):
         "X_inner_type": ["np-list", "numpy3D"],
         "capability:multivariate": True,
         "capability:unequal_length": True,
-        "capability:unequal_length:removes": True,
+        "removes_unequal_length": True,
     }
 
     def __init__(self, truncated_length=None):
diff --git a/aeon/transformations/collection/base.py b/aeon/transformations/collection/base.py
index edaa76be..10991aa7 100644
--- a/aeon/transformations/collection/base.py
+++ b/aeon/transformations/collection/base.py
@@ -43,7 +43,7 @@ class BaseCollectionTransformer(
     _tags = {
         "input_data_type": "Collection",
         "output_data_type": "Collection",
-        "capability:unequal_length:removes": False,
+        "removes_unequal_length": False,
     }
 
     def __init__(self):
@@ -93,7 +93,7 @@ class BaseCollectionTransformer(
                 raise ValueError("Tag requires_y is true, but fit called with y=None")
         # skip the rest if fit_is_empty is True
         if self.get_tag("fit_is_empty"):
-            self._is_fitted = True
+            self.is_fitted = True
             return self
         self.reset()
 
@@ -102,7 +102,7 @@ class BaseCollectionTransformer(
         y_inner = y
         self._fit(X=X_inner, y=y_inner)
 
-        self._is_fitted = True
+        self.is_fitted = True
 
         return self
 
@@ -203,7 +203,7 @@ class BaseCollectionTransformer(
         y_inner = y
         Xt = self._fit_transform(X=X_inner, y=y_inner)
 
-        self._is_fitted = True
+        self.is_fitted = True
 
         return Xt
 
diff --git a/aeon/transformations/collection/channel_selection/_channel_scorer.py b/aeon/transformations/collection/channel_selection/_channel_scorer.py
index 32c211dc..1559d7c5 100644
--- a/aeon/transformations/collection/channel_selection/_channel_scorer.py
+++ b/aeon/transformations/collection/channel_selection/_channel_scorer.py
@@ -1,30 +1,51 @@
+"""Selects channels based on an estimate of performance."""
+
+__maintainer__ = ["TonyBagnall"]
+__all__ = ["ChannelScorer"]
+
 import math
-from typing import Optional
+from typing import Callable
+from typing import Dict as TypingDict
+from typing import List as TypingList
+from typing import Optional, Union
 
 import numpy as np
-from sklearn.metrics import accuracy_score
+from sklearn.metrics import accuracy_score, mean_squared_error
 
+from aeon.base import BaseEstimator
 from aeon.classification.base import BaseClassifier
-from aeon.classification.convolution_based._rocket_classifier import RocketClassifier
+from aeon.regression.base import BaseRegressor
 from aeon.transformations.collection.channel_selection.base import BaseChannelSelector
 
-__maintainer__ = ["TonyBagnall"]
-__all__ = ["ChannelScorer"]
-
 
 class ChannelScorer(BaseChannelSelector):
-    """Channel scorer performs channel selection using a single channel classifier.
+    """Performs channel selection using a single channel classifier or regressor.
 
-    ChannelScorer uses a time series classifier to score each channel using an
-    estimate of accuracy on the training data, then selects a proportion of the top
+    ChannelScorer uses a time series classifier or a regressor to score each channel
+    using an estimate of accuracy on the training data fro classifier or mean
+    squared error for regressor, then selects a proportion of the top
     channels to keep. Can be configured through the constructor to use any time
-    series classifier and could easily be adapted to use forward selection or elbow
+    series estimator and could easily be adapted to use forward selection or elbow
     class methods. Approximately as described in [1]_.
 
     Parameters
     ----------
-    classifier, BaseClassifier, default = MiniROCKET
-    proportion : float, default = 0.2
+    estimator : BaseEstimator
+        The time series estimator used to score each channel.
+
+    scoring_function : Callable, optional (default=None)
+        Scoring function used to evaluate the performance of each channel.
+        Defaults to:
+        - `accuracy_score` if using a classifier.
+        - `mean_squared_error` if using a regressor.
+
+    score_sign : float, optional (default=None)
+        The sign used to determine whether higher or lower scores are better.
+        Defaults to:
+        - 1.0 for accuracy_score (higher scores are better).
+        - -1.0 for mean_squared_error (lower scores are better).
+
+    proportion : float, default = 0.4
         Proportion of channels to keep, rounded up to nearest integer.
 
     References
@@ -41,16 +62,20 @@ class ChannelScorer(BaseChannelSelector):
 
     def __init__(
         self,
-        classifier: Optional[BaseClassifier] = None,
+        estimator: Optional[BaseEstimator],
+        scoring_function: Callable = None,
+        score_sign: float = None,
         proportion: float = 0.4,
     ):
         self.proportion = proportion
-        self.classifier = classifier
+        self.estimator = estimator
+        self.scoring_function = scoring_function
+        self.score_sign = score_sign
         super().__init__()
 
-    def _fit(self, X, y):
+    def _fit(self, X: np.ndarray, y: Union[np.ndarray, TypingList]):
         """
-        Fit ECP to a specified X and y.
+        Fit to a specified X and y.
 
         Parameters
         ----------
@@ -66,23 +91,37 @@ class ChannelScorer(BaseChannelSelector):
         if self.proportion <= 0 or self.proportion > 1:
             raise ValueError("proportion must be in the range 0-1")
 
-        if self.classifier is None:
-            self.classifier_ = RocketClassifier(
-                rocket_transform="minirocket", num_kernels=5000
+        if isinstance(self.estimator, BaseClassifier):
+            self.estimator_ = self.estimator.clone()
+            scoring_function = (
+                accuracy_score
+                if self.scoring_function is None
+                else self.scoring_function
             )
-        elif not isinstance(self.classifier, BaseClassifier):
-            raise ValueError(
-                "parameter classifier must be None or an instance of a  "
-                "BaseClassifier."
+            score_sign = 1.0 if self.score_sign is None else self.score_sign
+        elif isinstance(self.estimator, BaseRegressor):
+            self.estimator_ = self.estimator.clone()
+            scoring_function = (
+                mean_squared_error
+                if self.scoring_function is None
+                else self.scoring_function
             )
+            score_sign = -1.0 if self.score_sign is None else self.score_sign
         else:
-            self.classifier_ = self.classifier.clone()
+            raise ValueError(
+                "parameter estimator must be an instance of BaseClassifier, "
+                "BaseRegressor."
+            )
+
         n_channels = X.shape[1]
         scores = np.zeros(n_channels)
         # Evaluate each channel with the classifier
         for i in range(n_channels):
-            preds = self.classifier_.fit_predict(X[:, i, :], y)
-            scores[i] = accuracy_score(y, preds)
+            preds = self.estimator_.fit_predict(X[:, i, :], y)
+            scores[i] = score_sign * scoring_function(
+                y, preds
+            )  # Applying the proper scoring function
+
         # Select the top n_keep channels
         sorted_indices = np.argsort(-scores)
         n_keep = math.ceil(n_channels * self.proportion)
@@ -90,7 +129,7 @@ class ChannelScorer(BaseChannelSelector):
         return self
 
     @classmethod
-    def get_test_params(cls, parameter_set="default"):
+    def get_test_params(cls, parameter_set: str = "default") -> TypingDict[str, any]:
         """Return testing parameter settings for the estimator.
 
         Parameters
@@ -106,4 +145,4 @@ class ChannelScorer(BaseChannelSelector):
         """
         from aeon.classification import DummyClassifier
 
-        return {"classifier": DummyClassifier(), "proportion": 0.4}
+        return {"estimator": DummyClassifier(), "proportion": 0.4}
diff --git a/aeon/transformations/collection/channel_selection/_elbow_class.py b/aeon/transformations/collection/channel_selection/_elbow_class.py
index dbbf12f0..d32b7fbb 100644
--- a/aeon/transformations/collection/channel_selection/_elbow_class.py
+++ b/aeon/transformations/collection/channel_selection/_elbow_class.py
@@ -288,7 +288,6 @@ class ElbowClassSum(BaseChannelSelector):
         self.distance = distance
         self.mean_center = mean_center
         self.prototype_type = prototype_type
-        self._is_fitted = False
 
         super().__init__()
 
@@ -321,7 +320,6 @@ class ElbowClassSum(BaseChannelSelector):
 
         self.channels_selected_.extend(_detect_knee_point(distance, indices))
         self.rank = self.channels_selected_
-        self._is_fitted = True
 
         return self
 
@@ -400,7 +398,6 @@ class ElbowClassPairwise(BaseChannelSelector):
         self.distance = distance
         self.prototype_type = prototype_type
         self.mean_center = mean_center
-        self._is_fitted = False
 
         super().__init__()
 
@@ -438,7 +435,7 @@ class ElbowClassPairwise(BaseChannelSelector):
 
         self.rank = self._rank()
         self.channels_selected_ = list(set(self.channels_selected_))
-        self._is_fitted = True
+
         return self
 
     def _rank(self) -> list[int]:
diff --git a/aeon/transformations/collection/convolution_based/rocketGPU/base.py b/aeon/transformations/collection/convolution_based/rocketGPU/base.py
index 9e87231e..c6e689cf 100644
--- a/aeon/transformations/collection/convolution_based/rocketGPU/base.py
+++ b/aeon/transformations/collection/convolution_based/rocketGPU/base.py
@@ -21,7 +21,7 @@ class BaseROCKETGPU(BaseCollectionTransformer):
         "capability:multivariate": True,
         "algorithm_type": "convolution",
         "capability:unequal_length": False,
-        "cant-pickle": True,
+        "cant_pickle": True,
         "python_dependencies": "tensorflow",
     }
 
diff --git a/aeon/transformations/collection/dictionary_based/_sfa.py b/aeon/transformations/collection/dictionary_based/_sfa.py
index df3f5160..8a67793c 100644
--- a/aeon/transformations/collection/dictionary_based/_sfa.py
+++ b/aeon/transformations/collection/dictionary_based/_sfa.py
@@ -261,7 +261,6 @@ class SFA(BaseCollectionTransformer):
         self.n_cases, self.n_timepoints = X.shape
         self.breakpoints = self._binning(X, y)
 
-        self._is_fitted = True
         return self
 
     def _transform(self, X, y=None):
diff --git a/aeon/transformations/collection/dictionary_based/_sfa_fast.py b/aeon/transformations/collection/dictionary_based/_sfa_fast.py
index 04421715..1e27b908 100644
--- a/aeon/transformations/collection/dictionary_based/_sfa_fast.py
+++ b/aeon/transformations/collection/dictionary_based/_sfa_fast.py
@@ -270,7 +270,6 @@ class SFAFast(BaseCollectionTransformer):
 
         self.n_cases, self.n_timepoints = X2.shape
         self.breakpoints = self._binning(X2, y)
-        self._is_fitted = True
 
         words, dfts = _transform_case(
             X2,
@@ -738,10 +737,6 @@ class SFAFast(BaseCollectionTransformer):
         }
         return params
 
-    def set_fitted(self):
-        """Whether `fit` has been called."""
-        self._is_fitted = True
-
     def __getstate__(self):
         """Return state as dictionary for pickling, required for typed Dict objects."""
         state = self.__dict__.copy()
diff --git a/aeon/transformations/collection/feature_based/_catch22.py b/aeon/transformations/collection/feature_based/_catch22.py
index 2134f45f..3867e3a5 100644
--- a/aeon/transformations/collection/feature_based/_catch22.py
+++ b/aeon/transformations/collection/feature_based/_catch22.py
@@ -192,11 +192,11 @@ class Catch22(BaseCollectionTransformer):
         self.n_jobs = n_jobs
         self.parallel_backend = parallel_backend
 
+        super().__init__()
+
         if use_pycatch22:
             self.set_tags(**{"python_dependencies": "pycatch22"})
 
-        super().__init__()
-
     def _transform(self, X, y=None):
         """Transform X into the catch22 features.
 
diff --git a/aeon/transformations/collection/shapelet_based/_dilated_shapelet_transform.py b/aeon/transformations/collection/shapelet_based/_dilated_shapelet_transform.py
index 714d1a82..48af1d3b 100644
--- a/aeon/transformations/collection/shapelet_based/_dilated_shapelet_transform.py
+++ b/aeon/transformations/collection/shapelet_based/_dilated_shapelet_transform.py
@@ -14,11 +14,10 @@ from typing import Optional, Union
 
 import numpy as np
 from numba import njit, prange, set_num_threads
-from numba.core.registry import CPUDispatcher
 from numba.typed import List
+from numpy.random._generator import Generator
 from sklearn.preprocessing import LabelEncoder
 
-from aeon.distances import get_distance_function
 from aeon.transformations.collection import BaseCollectionTransformer
 from aeon.utils.numba.general import (
     AEON_NUMBA_STD_THRESHOLD,
@@ -26,7 +25,6 @@ from aeon.utils.numba.general import (
     combinations_1d,
     get_subsequence,
     get_subsequence_with_mean_std,
-    set_numba_random_seed,
     sliding_mean_std_one_series,
 )
 from aeon.utils.numba.stats import prime_up_to
@@ -87,9 +85,6 @@ class RandomDilatedShapeletTransform(BaseCollectionTransformer):
         If True, restricts the value of the shapelet dilation parameter to be prime
         values. This can greatly speed up the algorithm for long time series and/or
         short shapelet lengths, possibly at the cost of some accuracy.
-    distance: str="manhattan"
-        Name of the distance function to be used. By default this is the
-        manhattan distance. Other distances from the aeon distance modules can be used.
     n_jobs : int, default=1
         The number of threads used for both `fit` and `transform`.
     random_state : int or None, default=None
@@ -161,7 +156,6 @@ class RandomDilatedShapeletTransform(BaseCollectionTransformer):
         alpha_similarity: float = 0.5,
         use_prime_dilations: bool = False,
         random_state: Optional[int] = None,
-        distance: CPUDispatcher = "manhattan",
         n_jobs: int = 1,
     ):
         self.max_shapelets = max_shapelets
@@ -171,7 +165,6 @@ class RandomDilatedShapeletTransform(BaseCollectionTransformer):
         self.alpha_similarity = alpha_similarity
         self.use_prime_dilations = use_prime_dilations
         self.random_state = random_state
-        self.distance = distance
         self.n_jobs = n_jobs
 
         super().__init__()
@@ -193,12 +186,15 @@ class RandomDilatedShapeletTransform(BaseCollectionTransformer):
         self : RandomDilatedShapeletTransform
             This estimator.
         """
-        self.distance_func = get_distance_function(self.distance)
-
         if isinstance(self.random_state, int):
-            self._random_state = np.int32(self.random_state)
+            self._random_generator = np.random.default_rng(self.random_state)
+        elif self.random_state is None:
+            self._random_generator = np.random.default_rng()
         else:
-            self._random_state = np.int32(np.random.randint(0, 2**31))
+            raise ValueError(
+                "Expected integer or None for random_state argument but got"
+                f"{type(self.random_state)}"
+            )
 
         n_cases_ = len(X)
         self.min_n_timepoints_ = min([X[i].shape[1] for i in range(n_cases_)])
@@ -228,8 +224,7 @@ class RandomDilatedShapeletTransform(BaseCollectionTransformer):
             self.threshold_percentiles_,
             self.alpha_similarity,
             self.use_prime_dilations,
-            self._random_state,
-            self.distance_func,
+            self._random_generator,
         )
         if len(self.shapelets_[0]) == 0:
             raise RuntimeError(
@@ -276,7 +271,6 @@ class RandomDilatedShapeletTransform(BaseCollectionTransformer):
         X_new = dilated_shapelet_transform(
             X,
             self.shapelets_,
-            self.distance_func,
         )
         if np.isinf(X_new).any() or np.isnan(X_new).any():
             warnings.warn(
@@ -310,7 +304,7 @@ class RandomDilatedShapeletTransform(BaseCollectionTransformer):
                     "an array (got {}).".format(self.shapelet_lengths_)
                 )
 
-            self.shapelet_lengths_ = np.array(self.shapelet_lengths_, dtype=np.int32)
+            self.shapelet_lengths_ = np.array(self.shapelet_lengths_, dtype=np.int_)
             if not np.all(self.shapelet_lengths_ >= 2):
                 warnings.warn(
                     "Some values in 'shapelet_lengths' are inferior to 2."
@@ -389,6 +383,7 @@ def _init_random_shapelet_params(
     use_prime_dilations: bool,
     n_channels: int,
     n_timepoints: int,
+    random_generator: Generator,
 ):
     """Randomly initialize the parameters of the shapelets.
 
@@ -414,6 +409,8 @@ def _init_random_shapelet_params(
         Number of channels of the input time series.
     n_timepoints : int
         Size of the input time series.
+    random_generator :
+        The random generator used for random operations.
 
     Returns
     -------
@@ -438,46 +435,52 @@ def _init_random_shapelet_params(
 
     """
     # Init startpoint array
-    startpoints = np.zeros(max_shapelets, dtype=np.int32)
+    startpoints = np.zeros(max_shapelets, dtype=np.int_)
     # Init class array
-    classes = np.zeros(max_shapelets, dtype=np.int32)
+    classes = np.zeros(max_shapelets, dtype=np.int_)
 
     # Lengths of the shapelets
     # test dtypes correctness
-    lengths = np.random.choice(shapelet_lengths, size=max_shapelets).astype(np.int32)
+    lengths = shapelet_lengths[
+        random_generator.integers(
+            0, high=len(shapelet_lengths), size=max_shapelets
+        ).astype(np.int_)
+    ]
     # Upper bound values for dilations
-    dilations = np.zeros(max_shapelets, dtype=np.int32)
+    dilations = np.zeros(max_shapelets, dtype=np.int_)
     upper_bounds = np.log2(np.floor_divide(n_timepoints - 1, lengths - 1))
 
     if use_prime_dilations:
-        _primes = prime_up_to(np.int32(2 ** upper_bounds.max()))
+        _primes = prime_up_to(np.int_(2 ** upper_bounds.max()))
         # 1 is not prime, but it is still a valid dilation for the "prime" scheme
-        primes = np.zeros((_primes.shape[0] + 1), dtype=np.int32)
+        primes = np.zeros((_primes.shape[0] + 1), dtype=np.int_)
         primes[0] = 1
         primes[1:] = _primes
         for i in prange(max_shapelets):
-            shp_primes = primes[primes <= np.int32(2 ** upper_bounds[i])]
-            dilations[i] = shp_primes[choice_log(shp_primes.shape[0], 1)[0]]
+            shp_primes = primes[primes <= np.int_(2 ** upper_bounds[i])]
+            dilations[i] = shp_primes[
+                choice_log(shp_primes.shape[0], 1, random_generator)[0]
+            ]
     else:
         for i in prange(max_shapelets):
-            dilations[i] = np.int32(2 ** np.random.uniform(0, upper_bounds[i]))
+            dilations[i] = np.int_(2 ** random_generator.uniform(0, upper_bounds[i]))
 
     # Init threshold array
-    threshold = np.zeros(max_shapelets, dtype=np.float64)
+    threshold = np.zeros(max_shapelets, dtype=np.float_)
 
     # Init values array
     values = np.full(
         (max_shapelets, n_channels, max(shapelet_lengths)),
         np.inf,
-        dtype=np.float64,
+        dtype=np.float_,
     )
 
     # Is shapelet using z-normalization ?
-    normalize = np.random.random(size=max_shapelets)
+    normalize = random_generator.uniform(0, 1, size=max_shapelets)
     normalize = normalize < proba_normalization
 
-    means = np.zeros((max_shapelets, n_channels), dtype=np.float64)
-    stds = np.zeros((max_shapelets, n_channels), dtype=np.float64)
+    means = np.zeros((max_shapelets, n_channels), dtype=np.float_)
+    stds = np.zeros((max_shapelets, n_channels), dtype=np.float_)
 
     return (
         values,
@@ -493,14 +496,14 @@ def _init_random_shapelet_params(
 
 
 @njit(cache=True)
-def _get_admissible_sampling_point(current_mask):
+def _get_admissible_sampling_point(current_mask, random_generator):
     n_cases = len(current_mask)
     # Count the number of admissible points per sample as cumsum
     n_admissible_points = 0
     for i in range(n_cases):
         n_admissible_points += current_mask[i].shape[0]
     if n_admissible_points > 0:
-        idx_choice = np.random.choice(n_admissible_points)
+        idx_choice = random_generator.integers(0, high=n_admissible_points)
         for i in range(n_cases):
             _new_val = idx_choice - current_mask[i].shape[0]
             if _new_val < 0 and current_mask[i].shape[0] > 0:
@@ -520,8 +523,7 @@ def random_dilated_shapelet_extraction(
     threshold_percentiles: np.ndarray,
     alpha_similarity: float,
     use_prime_dilations: bool,
-    seed: int,
-    distance: CPUDispatcher,
+    random_generator: Generator,
 ):
     """Randomly generate a set of shapelets given the input parameters.
 
@@ -556,12 +558,8 @@ def random_dilated_shapelet_extraction(
         If True, restrict the value of the shapelet dilation parameter to be prime
         values. This can greatly speed up the algorithm for long time series and/or
         short shapelet length, possibly at the cost of some accuracy.
-    seed : int
-        Seed for random number generation.
-    distance: CPUDispatcher
-        A Numba function used to compute the distance between two multidimensional
-        time series of shape (n_channels, length). Used as distance function between
-        shapelets and candidate subsequences
+    random_generator : Generator
+        The random generator used for random operations.
 
     Returns
     -------
@@ -588,13 +586,11 @@ def random_dilated_shapelet_extraction(
     """
     n_cases = len(X)
     n_channels = X[0].shape[0]
-    n_timepointss = np.zeros(n_cases, dtype=np.int64)
+    n_timepointss = np.zeros(n_cases, dtype=np.int_)
     for i in range(n_cases):
         n_timepointss[i] = X[i].shape[1]
     min_n_timepoints = n_timepointss.min()
     max_n_timepoints = n_timepointss.max()
-    # Fix the random seed
-    set_numba_random_seed(seed)
 
     # Initialize shapelets
     (
@@ -614,6 +610,7 @@ def random_dilated_shapelet_extraction(
         use_prime_dilations,
         n_channels,
         min_n_timepoints,
+        random_generator,
     )
     # Get unique dilations to loop over
     unique_dil = np.unique(dilations)
@@ -647,7 +644,9 @@ def random_dilated_shapelet_extraction(
                     for _i in range(n_cases)
                 ]
             )
-            idx_sample, idx_timestamp = _get_admissible_sampling_point(current_mask)
+            idx_sample, idx_timestamp = _get_admissible_sampling_point(
+                current_mask, random_generator
+            )
             if idx_sample >= 0:
                 # Update the mask in two directions from the sampling point
                 alpha_size = length - int(max(1, (1 - alpha_similarity) * min_len))
@@ -679,7 +678,9 @@ def random_dilated_shapelet_extraction(
                 loc_others = np.where(y == y[idx_sample])[0]
                 if loc_others.shape[0] > 1:
                     loc_others = loc_others[loc_others != idx_sample]
-                    id_test = np.random.choice(loc_others)
+                    id_test = loc_others[
+                        random_generator.integers(0, high=loc_others.shape[0])
+                    ]
                 else:
                     id_test = idx_sample
 
@@ -691,12 +692,12 @@ def random_dilated_shapelet_extraction(
                         X[id_test], length, dilation
                     )
                     X_subs = normalize_subsequences(X_subs, X_means, X_stds)
-                x_dist = compute_shapelet_dist_vector(X_subs, _val, length, distance)
+                x_dist = compute_shapelet_dist_vector(X_subs, _val)
 
                 lower_bound = np.percentile(x_dist, threshold_percentiles[0])
                 upper_bound = np.percentile(x_dist, threshold_percentiles[1])
 
-                threshold[i_shp] = np.random.uniform(lower_bound, upper_bound)
+                threshold[i_shp] = random_generator.uniform(lower_bound, upper_bound)
                 values[i_shp, :, :length] = _val
 
                 # Extract the starting point index of the shapelet
@@ -741,7 +742,6 @@ def dilated_shapelet_transform(
         np.ndarray,
         np.ndarray,
     ],
-    distance: CPUDispatcher,
 ):
     """Perform the shapelet transform with a set of shapelets and a set of time series.
 
@@ -769,9 +769,6 @@ def dilated_shapelet_transform(
             Standard deviation of the shapelets
         - classes : array, shape (max_shapelets)
         An initialized (empty) startpoint array for each shapelet
-    distance: CPUDispatcher
-        A Numba function used to compute the distance between two multidimensional
-        time series of shape (n_channels, length).
 
 
     Returns
@@ -810,9 +807,7 @@ def dilated_shapelet_transform(
             idx_no_norm = id_shps[np.where(~normalize[id_shps])[0]]
             for i_shp in idx_no_norm:
                 X_new[i_x, (n_ft * i_shp) : (n_ft * i_shp + n_ft)] = (
-                    compute_shapelet_features(
-                        X_subs, values[i_shp], length, threshold[i_shp], distance
-                    )
+                    compute_shapelet_features(X_subs, values[i_shp], threshold[i_shp])
                 )
 
             idx_norm = id_shps[np.where(normalize[id_shps])[0]]
@@ -822,7 +817,7 @@ def dilated_shapelet_transform(
                 for i_shp in idx_norm:
                     X_new[i_x, (n_ft * i_shp) : (n_ft * i_shp + n_ft)] = (
                         compute_shapelet_features(
-                            X_subs, values[i_shp], length, threshold[i_shp], distance
+                            X_subs, values[i_shp], threshold[i_shp]
                         )
                     )
     return X_new
@@ -830,9 +825,9 @@ def dilated_shapelet_transform(
 
 @njit(fastmath=True, cache=True)
 def normalize_subsequences(
-    X_subs: np.ndarray[np.float64],
-    X_means: np.ndarray[np.float64],
-    X_stds: np.ndarray[np.float64],
+    X_subs: np.ndarray[np.float_],
+    X_means: np.ndarray[np.float_],
+    X_stds: np.ndarray[np.float_],
 ):
     """
     Generate subsequences from a time series given the length and dilation parameters.
@@ -866,7 +861,7 @@ def normalize_subsequences(
 @njit(fastmath=True, cache=True)
 def get_all_subsequences(X: np.ndarray, length: int, dilation: int) -> np.ndarray:
     """
-    Generate subsequences from a time series given the length and dilation parameters.
+    Generate a view of subsequcnes from a time series given length and dilation values.
 
     Parameters
     ----------
@@ -880,27 +875,20 @@ def get_all_subsequences(X: np.ndarray, length: int, dilation: int) -> np.ndarra
     Returns
     -------
     array, shape = (n_timestamps-(length-1)*dilation, n_channels, length)
-        Subsequences of the input time series.
+        The view of the subsequences of the input time series.
     """
-    n_channels, n_timestamps = X.shape
-    n_subsequences = n_timestamps - (length - 1) * dilation
-    X_subs = np.zeros((n_subsequences, n_channels, length))
-    for i_sub in prange(n_subsequences):
-        for i_channel in prange(n_channels):
-            for i_length in prange(length):
-                X_subs[i_sub, i_channel, i_length] = X[
-                    i_channel, i_sub + (i_length * dilation)
-                ]
-    return X_subs
+    n_features, n_timestamps = X.shape
+    s0, s1 = X.strides
+    out_shape = (n_timestamps - (length - 1) * dilation, n_features, np.int64(length))
+    strides = (s1, s0, s1 * dilation)
+    return np.lib.stride_tricks.as_strided(X, shape=out_shape, strides=strides)
 
 
 @njit(fastmath=True, cache=True)
 def compute_shapelet_features(
     X_subs: np.ndarray,
     values: np.ndarray,
-    length: int,
     threshold: float,
-    distance: CPUDispatcher,
 ):
     """Extract the features from a shapelet distance vector.
 
@@ -921,9 +909,6 @@ def compute_shapelet_features(
         Length of the shapelet
     threshold : float
         The threshold parameter of the shapelet
-    distance: CPUDispatcher
-        A Numba function used to compute the distance between two multidimensional
-        time series of shape (n_channels, length).
 
     Returns
     -------
@@ -934,25 +919,26 @@ def compute_shapelet_features(
     _argmin = np.inf
     _SO = 0
 
-    n_subsequences = X_subs.shape[0]
+    n_subsequences, n_channels, length = X_subs.shape
 
     for i_sub in prange(n_subsequences):
-        _dist = distance(X_subs[i_sub], values[:, :length])
+        _dist = 0
+        for k in prange(n_channels):
+            for i_len in prange(length):
+                _dist += abs(X_subs[i_sub, k, i_len] - values[k, i_len])
         if _dist < _min:
             _min = _dist
             _argmin = i_sub
         if _dist < threshold:
             _SO += 1
 
-    return np.float64(_min), np.float64(_argmin), np.float64(_SO)
+    return np.float_(_min), np.float_(_argmin), np.float_(_SO)
 
 
 @njit(fastmath=True, cache=True)
 def compute_shapelet_dist_vector(
     X_subs: np.ndarray,
     values: np.ndarray,
-    length: int,
-    distance: CPUDispatcher,
 ):
     """Extract the features from a shapelet distance vector.
 
@@ -980,8 +966,10 @@ def compute_shapelet_dist_vector(
     dist_vector : array, shape = (n_timestamps-(length-1)*dilation)
         The distance vector between the shapelets and candidate subsequences
     """
-    n_subsequences = X_subs.shape[0]
+    n_subsequences, n_channels, length = X_subs.shape
     dist_vector = np.zeros(n_subsequences)
     for i_sub in prange(n_subsequences):
-        dist_vector[i_sub] = distance(X_subs[i_sub], values[:, :length])
+        for k in prange(n_channels):
+            for i_len in prange(length):
+                dist_vector[i_sub] += abs(X_subs[i_sub, k, i_len] - values[k, i_len])
     return dist_vector
diff --git a/aeon/transformations/series/base.py b/aeon/transformations/series/base.py
index 462dc9de..e98c9e46 100644
--- a/aeon/transformations/series/base.py
+++ b/aeon/transformations/series/base.py
@@ -59,7 +59,7 @@ class BaseSeriesTransformer(BaseSeriesEstimator, BaseTransformer, metaclass=ABCM
         """
         # skip the rest if fit_is_empty is True
         if self.get_tag("fit_is_empty"):
-            self._is_fitted = True
+            self.is_fitted = True
             return self
         if self.get_tag("requires_y"):
             if y is None:
@@ -70,7 +70,7 @@ class BaseSeriesTransformer(BaseSeriesEstimator, BaseTransformer, metaclass=ABCM
         if y is not None:
             self._check_y(y)
         self._fit(X=X, y=y)
-        self._is_fitted = True
+        self.is_fitted = True
         return self
 
     @final
@@ -140,7 +140,7 @@ class BaseSeriesTransformer(BaseSeriesEstimator, BaseTransformer, metaclass=ABCM
         self.reset()
         X = self._preprocess_series(X, axis=axis, store_metadata=True)
         Xt = self._fit_transform(X=X, y=y)
-        self._is_fitted = True
+        self.is_fitted = True
         return self._postprocess_series(Xt, axis=axis)
 
     @final
diff --git a/aeon/utils/__init__.py b/aeon/utils/__init__.py
index ba84bfa0..bb8f469e 100644
--- a/aeon/utils/__init__.py
+++ b/aeon/utils/__init__.py
@@ -2,6 +2,7 @@
 
 __all__ = [
     "split_series",
+    "weighted_geometric_mean",
     "ALL_TIME_SERIES_TYPES",
     "COLLECTIONS_DATA_TYPES",
     "SERIES_DATA_TYPES",
@@ -18,3 +19,4 @@ from aeon.utils._data_types import (
 )
 from aeon.utils._show_versions import show_versions
 from aeon.utils._split import split_series
+from aeon.utils._weighted_metrics import weighted_geometric_mean
diff --git a/aeon/utils/_show_versions.py b/aeon/utils/_show_versions.py
index 1d7ac014..b385f734 100644
--- a/aeon/utils/_show_versions.py
+++ b/aeon/utils/_show_versions.py
@@ -25,15 +25,24 @@ deps = [
 
 def show_versions():
     """Print useful debugging information."""
+    print(_show_versions())  # noqa: T001, T201
+
+
+def _show_versions():
+    """Print useful debugging information.
+
+    Returns
+    -------
+    str
+    """
     sys_info = {
         "python": sys.version.replace("\n", " "),
         "executable": sys.executable,
         "machine": platform.platform(),
     }
-
-    print("\nSystem:")  # noqa: T001, T201
+    str = "\nSystem:"
     for k, stat in sys_info.items():
-        print(f"{k:>10}: {stat}")  # noqa: T001, T201
+        str = f"{str}\n{k:>10}: {stat}"
 
     deps_info = {"aeon": __version__}
     for modname in deps:
@@ -41,7 +50,7 @@ def show_versions():
             deps_info[modname] = version(modname)
         except PackageNotFoundError:
             deps_info[modname] = None
-
-    print("\nPython dependencies:")  # noqa: T001, T201
+    str = f"{str}\nPython dependencies:"
     for k, stat in deps_info.items():
-        print(f"{k:>13}: {stat}")  # noqa: T001, T201
+        str = f"{str}\n{k:>13}: {stat}"  # noqa: T001, T201
+    return str
diff --git a/aeon/utils/_weighted_metrics.py b/aeon/utils/_weighted_metrics.py
new file mode 100644
index 00000000..84ec005d
--- /dev/null
+++ b/aeon/utils/_weighted_metrics.py
@@ -0,0 +1,44 @@
+"""Statistical functionality used throughout aeon."""
+
+import numpy as np
+from sklearn.utils.validation import check_consistent_length
+
+__maintainer__ = []
+__all__ = [
+    "weighted_geometric_mean",
+]
+
+
+def weighted_geometric_mean(y, weights, axis=None):
+    """Calculate weighted version of geometric mean.
+
+    Parameters
+    ----------
+    y : np.ndarray
+        Values to take the weighted geometric mean of.
+    weights: np.ndarray
+        Weights for each value in `array`. Must be same shape as `array` or
+        of shape `(array.shape[0],)` if axis=0 or `(array.shape[1], ) if axis=1.
+    axis : int
+        The axis of `y` to apply the weights to.
+
+    Returns
+    -------
+    geometric_mean : float
+        Weighted geometric mean
+    """
+    if weights.ndim == 1:
+        if axis == 0:
+            check_consistent_length(y, weights)
+        elif axis == 1:
+            if y.shape[1] != len(weights):
+                raise ValueError(
+                    f"Input features ({y.shape[1]}) do not match "
+                    f"number of `weights` ({len(weights)})."
+                )
+        weight_sums = np.sum(weights)
+    else:
+        if y.shape != weights.shape:
+            raise ValueError("Input data and weights have inconsistent shapes.")
+        weight_sums = np.sum(weights, axis=axis)
+    return np.exp(np.sum(weights * np.log(y), axis=axis) / weight_sums)
diff --git a/aeon/utils/datetime.py b/aeon/utils/datetime.py
deleted file mode 100644
index 1478d86b..00000000
--- a/aeon/utils/datetime.py
+++ /dev/null
@@ -1,285 +0,0 @@
-"""Time format related utilities."""
-
-from typing import Optional
-
-__maintainer__ = []
-__all__ = []
-
-import warnings
-from typing import Union
-
-import numpy as np
-import pandas as pd
-from pandas.api.types import is_integer_dtype
-
-from aeon.utils.validation.series import check_time_index, is_integer_index
-
-
-def _get_index(x):
-    if hasattr(x, "index"):
-        return x.index
-    else:
-        # select last dimension for time index
-        return pd.RangeIndex(x.shape[-1])
-
-
-def get_time_index(X):
-    """Get index of time series data, helper function.
-
-    Parameters
-    ----------
-    X : pd.DataFrame, pd.Series, np.ndarray
-
-    Returns
-    -------
-    time_index : pandas.Index
-        Index of time series
-    """
-    # assumes that all samples share the same the time index, only looks at
-    # first row
-    if isinstance(X, (pd.DataFrame, pd.Series)):
-        # pd-multiindex or pd_multiindex_hier
-        if isinstance(X.index, pd.MultiIndex):
-            index_tuple = tuple(list(X.index[0])[:-1])
-            index = X.loc[index_tuple].index
-            return index
-        # nested_univ
-        elif isinstance(X, pd.DataFrame) and isinstance(X.iloc[0, 0], pd.DataFrame):
-            return _get_index(X.iloc[0, 0])
-        # pd.Series or pd.DataFrame
-        else:
-            return X.index
-    # numpy3D and np.ndarray
-    elif isinstance(X, np.ndarray):
-        # np.ndarray
-        if X.ndim < 3:
-            return pd.RangeIndex(X.shape[0])
-        # numpy3D
-        else:
-            return pd.RangeIndex(X.shape[-1])
-    elif hasattr(X, "X"):
-        return get_time_index(X.X)
-    else:
-        raise ValueError(
-            f"X must be pd.DataFrame, pd.Series, or np.ndarray, but found: {type(X)}"
-        )
-
-
-def _coerce_duration_to_int(
-    duration: Union[int, pd.Timedelta, pd.tseries.offsets.BaseOffset, pd.Index],
-    freq: Optional[str] = None,
-) -> Union[int, pd.Index]:
-    """Coerce durations into integer representations for a given unit of duration.
-
-    Parameters
-    ----------
-    duration : pd.DateOffset, pd.Timedelta, pd.TimedeltaIndex, pd.Index, int
-        Duration type or collection of duration types
-    freq : str
-        Frequency of the above duration type.
-
-    Returns
-    -------
-    ret : int
-        Duration in integer values for given unit
-    """
-    if isinstance(duration, int):
-        return duration
-    elif isinstance(duration, pd.tseries.offsets.BaseOffset):
-        return int(duration.n / _get_intervals_count_and_unit(freq)[0])
-    elif isinstance(duration, pd.Index) and isinstance(
-        duration[0], pd.tseries.offsets.BaseOffset
-    ):
-        count = _get_intervals_count_and_unit(freq)[0]
-        return pd.Index([d.n / count for d in duration], dtype=int)
-    elif isinstance(duration, (pd.Timedelta, pd.TimedeltaIndex)):
-        count, unit = _get_intervals_count_and_unit(freq)
-        # integer conversion only works reliably with non-ambiguous units (
-        # e.g. days, seconds but not months, years)
-        try:
-            if isinstance(duration, pd.Timedelta):
-                return int(duration / pd.Timedelta(count, unit))
-            if isinstance(duration, pd.TimedeltaIndex):
-                return (duration / pd.Timedelta(count, unit)).astype(int)
-        except ValueError:
-            raise ValueError(
-                "Index type not supported. Please consider using pd.PeriodIndex."
-            )
-    else:
-        raise TypeError("`duration` type not understood.")
-
-
-def _get_intervals_count_and_unit(freq: str) -> tuple[int, str]:
-    """Extract interval count and unit from frequency string.
-
-    Supports eg: W, 3W, W-SUN, BQS, (B)Q(S)-MAR patterns, from which we
-    extract the count and the unit. See
-    https://pandas.pydata.org/docs/user_guide/timeseries.html#timeseries-offset-aliases
-    """
-    if freq is None:
-        raise ValueError("frequency is missing")
-    else:
-        offset = pd.tseries.frequencies.to_offset(freq)
-        count, unit = offset.n, offset.base.freqstr
-        return count, unit
-
-
-def _get_freq(x):
-    """Get unit for conversion of time deltas to integer."""
-    if hasattr(x, "freqstr"):
-        if x.freqstr is None:
-            return None
-        elif "-" in x.freqstr:
-            return x.freqstr.split("-")[0]
-        else:
-            return x.freqstr
-    else:
-        return None
-
-
-def set_hier_freq(x):
-    """Set frequency for multiindex dataframes without frequency.
-
-    As of pandas 1.5.1, only a pd.PeriodIndex time index can have freq other None in
-    a pandas multiindex data set. A DatetimeIndex will always have a frequency equal
-    to None.
-    This function converts the DatetimeIndex to a PeriodIndex, which supports
-    frequency arguments.
-
-    Parameters
-    ----------
-    y : Panel, or Hierarchical object, or VectorizedDF with timeindex as
-        pd.DatetimeIndex or pd.PeriodIndex
-
-    Returns
-    -------
-    Series, Panel, or Hierarchical object, or VectorizedDF with pd.PeriodIndex as
-    time index and freq set.
-    """
-    if not isinstance(x.index, pd.MultiIndex):
-        raise ValueError("Only intended for use with MultiIndex.")
-
-    timepoints = get_time_index(x)
-    if not isinstance(timepoints, (pd.DatetimeIndex)):
-        raise ValueError("Set_freq only supported for DatetimeIndex.")
-
-    if timepoints.freq is not None:
-        warnings.warn("Frequency already set.", stacklevel=2)
-    else:
-        time_names = x.index.names[-1]
-        x = (
-            x.reset_index(-1)
-            .groupby(level=0, group_keys=True)
-            .apply(lambda df: df.set_index(time_names).to_period())
-        )
-    return x
-
-
-def infer_freq(y):
-    """Infer frequency string from the time series object.
-
-    Parameters
-    ----------
-    y : pd.DataFrame, pd.Series, or np.ndarray object
-
-    Returns
-    -------
-    str
-        Frequency string inferred from the pandas index,
-        or `None`, if inference fails.
-    """
-    index = get_time_index(y)
-    if hasattr(index, "freqstr"):
-        return index.freqstr
-    else:
-        try:
-            return pd.infer_freq(index)
-        except (TypeError, ValueError):
-            return None
-
-
-def _shift(x, by=1, return_index=False):
-    """Shift time point `x` by a step (`by`) given frequency of `x`.
-
-    Parameters
-    ----------
-    x : pd.Index, pd.Period, int. If pd.Index or pd.Period, must have `freq` attribute.
-        If pd.Index, must be of integer type, PeriodIndex, or DateTimeIndex
-        Time point to shift
-    by : int, optional, default=1
-    return_index : bool, optional, default=False
-        whether to return an index element (False) or a pandas Index (True)
-
-    Returns
-    -------
-    ret : pd.Index if return_index = True; int, pd.Period, or pd.Timestamp if False.
-        Shifted time point, `x` shifted by `by` periods
-        if return_index = True: pd.Index coerced `x`, shifted by `by` periods.
-        if return_index = False: index element coerced `x`, shifted by `by` periods.
-            if `x` is index, is coerced to index element by selecting first element
-        Period shift is integer for `x: int`, and `freq` if `x` is temporal with `freq`
-    """
-    if isinstance(x, pd.Timestamp):
-        raise TypeError("_shift does not support x of type pd.Timestamp")
-
-    # we ensure idx is pd.Index, x is first (and usually only) element
-    if isinstance(x, pd.Index):
-        idx = x
-        x = idx[0]
-    else:
-        idx = pd.Index([x])
-
-    # if we want index, we can simply use add dunder or shift
-    if return_index:
-        if is_integer_dtype(idx):
-            return idx + by
-        else:
-            return idx.shift(by)
-
-    # if not return_index, i.e., we want an index element
-    assert isinstance(x, (pd.Period, pd.Timestamp, int, np.integer)), type(x)
-    assert isinstance(by, (int, np.integer)) or is_integer_index(by), type(by)
-
-    # we need to get freq from idx, since pd.Timestamp freq is deprecated
-    if isinstance(x, pd.Timestamp):
-        by *= idx.freq
-    return x + by
-
-
-def _get_duration(x, y=None, coerce_to_int=False, unit=None):
-    """Compute duration between the time indices.
-
-    Parameters
-    ----------
-    x : pd.Index, pd.Timestamp, pd.Period, int
-    y : pd.Timestamp, pd.Period, int, default=None
-    coerce_to_int : bool
-        If True, duration is returned as integer value for given unit
-    unit : str
-        Time unit
-
-    Returns
-    -------
-    ret : duration type
-        Duration
-    """
-    if y is None:
-        x = check_time_index(x)
-        duration = x[-1] - x[0]
-    else:
-        assert isinstance(x, (int, np.integer, pd.Period, pd.Timestamp))
-        # check types allowing (np.integer, int) combinations to pass
-        assert type(x) is type(y) or (
-            isinstance(x, (np.integer, int)) and isinstance(x, (np.integer, int))
-        )
-        duration = x - y
-
-    # coerce to integer result for given time unit
-    if coerce_to_int and isinstance(
-        x, (pd.PeriodIndex, pd.DatetimeIndex, pd.Period, pd.Timestamp)
-    ):
-        if unit is None:
-            # try to get the unit from the data if not given
-            unit = _get_freq(x)
-        duration = _coerce_duration_to_int(duration, freq=unit)
-    return duration
diff --git a/aeon/utils/numba/general.py b/aeon/utils/numba/general.py
index 6765033f..e0ccaf98 100644
--- a/aeon/utils/numba/general.py
+++ b/aeon/utils/numba/general.py
@@ -32,6 +32,7 @@ from copy import deepcopy
 import numpy as np
 from numba import njit, prange
 from numba.core.registry import CPUDispatcher
+from numpy.random._generator import Generator
 
 import aeon.utils.numba.stats as stats
 
@@ -164,7 +165,7 @@ def unique_count(X: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
         X = np.sort(X)
         unique = np.zeros(X.shape[0])
         unique[0] = X[0]
-        counts = np.zeros(X.shape[0], dtype=np.int32)
+        counts = np.zeros(X.shape[0], dtype=np.int_)
         counts[0] = 1
         uc = 0
 
@@ -176,7 +177,7 @@ def unique_count(X: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
             else:
                 counts[uc] += 1
         return unique[: uc + 1], counts[: uc + 1]
-    return np.zeros(0), np.zeros(0, dtype=np.int32)
+    return np.zeros(0), np.zeros(0, dtype=np.int_)
 
 
 @njit(fastmath=True, cache=True)
@@ -438,7 +439,7 @@ def set_numba_random_seed(seed: int) -> None:
 
 
 @njit(fastmath=True, cache=True)
-def choice_log(n_choice: int, n_sample: int) -> np.ndarray:
+def choice_log(n_choice: int, n_sample: int, random_generator: Generator) -> np.ndarray:
     """Random choice function with log probability rather than uniform.
 
     To seed the function the `np.random.seed` must be set in a numba function prior to
@@ -451,6 +452,7 @@ def choice_log(n_choice: int, n_sample: int) -> np.ndarray:
         n_choice-1.
     n_sample : int
         Number of choice to sample.
+    random_generator : random_generator
 
     Returns
     -------
@@ -462,12 +464,12 @@ def choice_log(n_choice: int, n_sample: int) -> np.ndarray:
         P = np.array([1 / 2 ** np.log(i) for i in range(1, n_choice + 1)])
         # Bring everything between 0 and 1 as a cumulative probability
         P = P.cumsum() / P.sum()
-        loc = np.zeros(n_sample, dtype=np.int32)
+        loc = np.zeros(n_sample, dtype=np.int_)
         for i in prange(n_sample):
-            loc[i] = np.where(P >= np.random.rand())[0][0]
+            loc[i] = np.where(P >= random_generator.random())[0][0]
         return loc
     else:
-        return np.zeros(n_sample, dtype=np.int32)
+        return np.zeros(n_sample, dtype=np.int_)
 
 
 @njit(fastmath=True, cache=True)
@@ -529,9 +531,9 @@ def get_subsequence_with_mean_std(
         The std of each channel
     """
     n_channels, _ = X.shape
-    values = np.zeros((n_channels, length), dtype=np.float64)
-    means = np.zeros(n_channels, dtype=np.float64)
-    stds = np.zeros(n_channels, dtype=np.float64)
+    values = np.zeros((n_channels, length), dtype=np.float_)
+    means = np.zeros(n_channels, dtype=np.float_)
+    stds = np.zeros(n_channels, dtype=np.float_)
     for i_channel in prange(n_channels):
         _sum = 0
         _sum2 = 0
@@ -587,7 +589,7 @@ def sliding_mean_std_one_series(
 
     for i_mod_dil in prange(dilation):
         # Array mainting indexes of a dilated subsequence
-        _idx_sub = np.zeros(length, dtype=np.int32)
+        _idx_sub = np.zeros(length, dtype=np.int_)
         for i_length in prange(length):
             _idx_sub[i_length] = (i_length * dilation) + i_mod_dil
 
@@ -690,7 +692,7 @@ def combinations_1d(x: np.ndarray, y: np.ndarray) -> np.ndarray:
 
     for i in range(x.shape[0]):
         u_mask[np.where(u_x == x[i])[0][0], np.where(u_y == y[i])[0][0]] = True
-    combinations = np.zeros((u_mask.sum(), 2), dtype=np.int32)
+    combinations = np.zeros((u_mask.sum(), 2), dtype=np.int_)
     i_comb = 0
     for i in range(x.shape[0]):
         if u_mask[np.where(u_x == x[i])[0][0], np.where(u_y == y[i])[0][0]]:
@@ -846,7 +848,7 @@ def generate_combinations(n, k):
     """
     comb_array = np.arange(k)
     num_combinations = _comb(n, k)  # Using our efficient comb function
-    combinations = np.empty((num_combinations, k), dtype=np.int32)
+    combinations = np.empty((num_combinations, k), dtype=np.int_)
 
     for idx in range(num_combinations):
         combinations[idx, :] = comb_array
diff --git a/aeon/utils/sklearn.py b/aeon/utils/sklearn.py
index f398a5d2..a33be068 100644
--- a/aeon/utils/sklearn.py
+++ b/aeon/utils/sklearn.py
@@ -65,6 +65,12 @@ def sklearn_estimator_identifier(obj, var_name="obj"):
     """
     if not is_sklearn_estimator(obj):
         raise TypeError(f"{var_name} is not an sklearn estimator, has type {type(obj)}")
+    # deal with sklearn pipelines: type is determined by the last element
+    if isinstance(obj, Pipeline) or hasattr(obj, "steps"):
+        obj = obj.steps[-1][1]
+    # deal with generic composites: type is type of wrapped "estimator"
+    if isinstance(obj, (GridSearchCV, RandomizedSearchCV)) or hasattr(obj, "estimator"):
+        obj = obj.estimator
 
     # first check whether obj class inherits from sklearn mixins
     sklearn_mixins = tuple(mixin_to_identifier.keys())
@@ -78,14 +84,6 @@ def sklearn_estimator_identifier(obj, var_name="obj"):
             if issubclass(obj_class, mx):
                 return mixin_to_identifier[mx]
 
-    # deal with sklearn pipelines: type is determined by the last element
-    if isinstance(obj, Pipeline) or hasattr(obj, "steps"):
-        return sklearn_estimator_identifier(obj.steps[-1][1], var_name=var_name)
-
-    # deal with generic composites: type is type of wrapped "estimator"
-    if isinstance(obj, (GridSearchCV, RandomizedSearchCV)) or hasattr(obj, "estimator"):
-        return sklearn_estimator_identifier(obj.estimator, var_name=var_name)
-
     # fallback - estimator of indeterminate type
     return "estimator"
 
diff --git a/aeon/utils/tags/_tags.py b/aeon/utils/tags/_tags.py
index 4f06c3cc..650a7c4d 100644
--- a/aeon/utils/tags/_tags.py
+++ b/aeon/utils/tags/_tags.py
@@ -34,13 +34,13 @@ ESTIMATOR_TAGS = {
         "as str or list of str. e.g. 'tsfresh>=0.20.0', "
         "'[tsfresh>=0.20.0, pandas<2.0.0]'. If None, no restriction.",
     },
-    "non-deterministic": {
+    "non_deterministic": {
         "class": "estimator",
         "type": "bool",
         "description": "The estimator is non-deterministic, and multiple runs will "
         "not produce the same output even if a `random_state` is set.",
     },
-    "cant-pickle": {
+    "cant_pickle": {
         "class": "estimator",
         "type": "bool",
         "description": "The estimator cannot be pickled.",
@@ -134,13 +134,13 @@ ESTIMATOR_TAGS = {
         "type": "bool",
         "description": "Does this estimator require y to be passed in its methods?",
     },
-    "capability:unequal_length:removes": {
+    "removes_unequal_length": {
         "class": "collection-transformer",
         "type": "bool",
         "description": "Is the transformer result guaranteed to be equal length series "
         "or tabular?",
     },
-    "capability:missing_values:removes": {
+    "removes_missing_values": {
         "class": "transformer",
         "type": "bool",
         "description": "Is the transformer result guaranteed to have no missing "
diff --git a/aeon/utils/weighted_metrics.py b/aeon/utils/weighted_metrics.py
deleted file mode 100644
index b1526464..00000000
--- a/aeon/utils/weighted_metrics.py
+++ /dev/null
@@ -1,132 +0,0 @@
-"""Statistical functionality used throughout aeon."""
-
-import numpy as np
-from sklearn.utils.stats import _weighted_percentile
-from sklearn.utils.validation import check_consistent_length
-
-__maintainer__ = []
-__all__ = [
-    "_weighted_geometric_mean",
-    "_weighted_median",
-    "_weighted_min",
-    "_weighted_max",
-]
-
-
-def _weighted_geometric_mean(y, weights=None, axis=None):
-    """Calculate weighted version of geometric mean.
-
-    Parameters
-    ----------
-    y : np.ndarray
-        Values to take the weighted geometric mean of.
-    weights: np.ndarray
-        Weights for each value in `array`. Must be same shape as `array` or
-        of shape `(array.shape[0],)` if axis=0 or `(array.shape[1], ) if axis=1.
-    axis : int
-        The axis of `y` to apply the weights to.
-
-    Returns
-    -------
-    geometric_mean : float
-        Weighted geometric mean
-    """
-    if weights.ndim == 1:
-        if axis == 0:
-            check_consistent_length(y, weights)
-        elif axis == 1:
-            if y.shape[1] != len(weights):
-                raise ValueError(
-                    f"Input features ({y.shape[1]}) do not match "
-                    f"number of `weights` ({len(weights)})."
-                )
-        weight_sums = np.sum(weights)
-    else:
-        if y.shape != weights.shape:
-            raise ValueError("Input data and weights have inconsistent shapes.")
-        weight_sums = np.sum(weights, axis=axis)
-    return np.exp(np.sum(weights * np.log(y), axis=axis) / weight_sums)
-
-
-def _weighted_median(y, axis=1, weights=None):
-    """Calculate weighted median.
-
-    Parameters
-    ----------
-    y : np.ndarray, pd.Series or pd.DataFrame
-        Values to take the weighted median of.
-    weights: np.ndarray
-        Weights for each value in `array`. Must be same shape as `array` or
-        of shape `(array.shape[0],)` if axis=0 or `(array.shape[1], ) if axis=1.
-    axis : int
-        The axis of `y` to apply the weights to.
-
-    Returns
-    -------
-    w_median : float
-        Weighted median
-    """
-    w_median = np.apply_along_axis(
-        func1d=_weighted_percentile,
-        axis=axis,
-        arr=y,
-        sample_weight=weights,
-        percentile=50,
-    )
-    return w_median
-
-
-def _weighted_min(y, axis=1, weights=None):
-    """Calculate weighted minimum.
-
-    Parameters
-    ----------
-    y : np.ndarray, pd.Series or pd.DataFrame
-        Values to take the weighted minimum of.
-    weights: np.ndarray
-        Weights for each value in `array`. Must be same shape as `array` or
-        of shape `(array.shape[0],)` if axis=0 or `(array.shape[1], ) if axis=1.
-    axis : int
-        The axis of `y` to apply the weights to.
-
-    Returns
-    -------
-    w_min : float
-        Weighted minimum
-    """
-    w_min = np.apply_along_axis(
-        func1d=_weighted_percentile,
-        axis=axis,
-        arr=y,
-        sample_weight=weights,
-        percentile=0,
-    )
-    return w_min
-
-
-def _weighted_max(y, axis=1, weights=None):
-    """Calculate weighted maximum.
-
-    Parameters
-    ----------
-    y : np.ndarray, pd.Series or pd.DataFrame
-        Values to take the weighted maximum of.
-    weights: np.ndarray
-        Weights for each value in `array`. Must be same shape as `array` or
-        of shape `(array.shape[0],)` if axis=0 or `(array.shape[1], ) if axis=1.
-    axis : int
-        The axis of `y` to apply the weights to.
-
-    Returns
-    -------
-    w_max : float
-        Weighted maximum
-    """
-    w_max = np.apply_along_axis(
-        func1d=_weighted_percentile,
-        axis=axis,
-        arr=y,
-        sample_weight=weights,
-        percentile=100,
-    )
-    return w_max
diff --git a/aeon/visualisation/estimator/_shapelets.py b/aeon/visualisation/estimator/_shapelets.py
index 43e670d1..2a070288 100644
--- a/aeon/visualisation/estimator/_shapelets.py
+++ b/aeon/visualisation/estimator/_shapelets.py
@@ -8,6 +8,8 @@ import copy
 import warnings
 
 import numpy as np
+from numba import njit
+from numba.core.registry import CPUDispatcher
 from sklearn.ensemble._forest import BaseForest
 from sklearn.linear_model._base import LinearClassifierMixin
 from sklearn.pipeline import Pipeline
@@ -27,7 +29,6 @@ from aeon.transformations.collection.shapelet_based import (
     RandomShapeletTransform,
 )
 from aeon.transformations.collection.shapelet_based._dilated_shapelet_transform import (
-    compute_shapelet_dist_vector,
     get_all_subsequences,
     normalize_subsequences,
 )
@@ -35,6 +36,45 @@ from aeon.utils.numba.general import sliding_mean_std_one_series
 from aeon.utils.validation._dependencies import _check_soft_dependencies
 
 
+@njit(fastmath=True, cache=True)
+def compute_shapelet_dist_vector(
+    X_subs: np.ndarray,
+    values: np.ndarray,
+    dist_func: CPUDispatcher,
+):
+    """Extract the features from a shapelet distance vector.
+
+    Given a shapelet and a time series, extract three features from the resulting
+    distance vector:
+        - min
+        - argmin
+        - Shapelet Occurence : number of point in the distance vector inferior to the
+        threshold parameter
+
+    Parameters
+    ----------
+    X_subs : array, shape (n_timestamps-(length-1)*dilation, n_channels, length)
+        The subsequences of an input time series given the length and dilation parameter
+    values : array, shape (n_channels, length)
+        The value array of the shapelet
+    length : int
+        Length of the shapelet
+    distance: CPUDispatcher
+        A Numba function used to compute the distance between two multidimensional
+        time series of shape (n_channels, length).
+
+    Returns
+    -------
+    dist_vector : array, shape = (n_timestamps-(length-1)*dilation)
+        The distance vector between the shapelets and candidate subsequences
+    """
+    n_subsequences, n_channels, length = X_subs.shape
+    dist_vector = np.zeros(n_subsequences)
+    for i_sub in range(n_subsequences):
+        dist_vector[i_sub] += dist_func(X_subs[i_sub], values)
+    return dist_vector
+
+
 class ShapeletVisualizer:
     """
     A Shapelet object to use for ploting operations.
@@ -249,9 +289,7 @@ class ShapeletVisualizer:
             _values = self.values
 
         # Compute distance vector
-        c = compute_shapelet_dist_vector(
-            X_subs, _values, self.length, self.distance_func
-        )
+        c = compute_shapelet_dist_vector(X_subs, _values, self.distance_func)
 
         # Get best match index
         idx_best = c.argmin()
@@ -361,9 +399,7 @@ class ShapeletVisualizer:
             )
         else:
             _values = self.values
-        c = compute_shapelet_dist_vector(
-            X_subs, _values, self.length, self.distance_func
-        )
+        c = compute_shapelet_dist_vector(X_subs, _values, self.distance_func)
 
         if ax is None:
             plt.style.use(matplotlib_style)
@@ -406,7 +442,7 @@ class ShapeletTransformerVisualizer:
             dilation_ = self.estimator.shapelets_[3][id_shapelet]
             threshold_ = self.estimator.shapelets_[4][id_shapelet]
             normalize_ = self.estimator.shapelets_[5][id_shapelet]
-            distance = self.estimator.distance
+            distance = "manhattan"
 
         elif isinstance(self.estimator, (RSAST, SAST)):
             values_ = self.estimator._kernel_orig[id_shapelet]
diff --git a/examples/classification/shapelet_based.ipynb b/examples/classification/shapelet_based.ipynb
index 4c271f27..d15b5f2b 100644
--- a/examples/classification/shapelet_based.ipynb
+++ b/examples/classification/shapelet_based.ipynb
@@ -559,8 +559,7 @@
     "\n",
     "<img src=\"img/ex_dil_shp_slide.png\" alt=\"drawing\" width=\"500\"/>\n",
     "\n",
-    "\n",
-    "Note that here we use the euclidean distance, but we can replace it with any distance using the `distance` parameter of both `RandomDilatedShapeletTransform` and `RDSTClassifier` in aeon. For example : `RandomDilatedShapeletTransform(distance='dtw')`. "
+    "Note that `RDST` uses the manhattan distance instead of the euclidean distance."
    ]
   },
   {
@@ -771,7 +770,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.12.4"
+   "version": "3.10.13"
   }
  },
  "nbformat": 4,


Test patch:
diff --git a/aeon/anomaly_detection/tests/test_base.py b/aeon/anomaly_detection/tests/test_base.py
index d61d01fd..02ef537b 100644
--- a/aeon/anomaly_detection/tests/test_base.py
+++ b/aeon/anomaly_detection/tests/test_base.py
@@ -33,18 +33,18 @@ def test_fit(series):
 
     # test fit is empty
     ad.fit(series)
-    assert ad._is_fitted
+    assert ad.is_fitted
 
     ad.fit(series, test_y)
-    assert ad._is_fitted
+    assert ad.is_fitted
 
     # test requires fit
     ad_fit.fit(series)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X.squeeze(), series)
 
     ad_fit.fit(series, test_y)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X.squeeze(), series)
 
     # test requires y
@@ -52,7 +52,7 @@ def test_fit(series):
         ad_y.fit(series)
 
     ad_y.fit(series, test_y)
-    assert ad_y._is_fitted
+    assert ad_y.is_fitted
     assert_almost_equal(ad_y._X.squeeze(), series)
     assert_almost_equal(ad_y._y, test_y)
 
@@ -65,11 +65,11 @@ def test_fit_axis(series):
     invert_series = series.T
 
     ad_fit.fit(series)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X, series)
 
     ad_fit.fit(invert_series, axis=0)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X, series)
 
 
@@ -129,27 +129,27 @@ def test_fit_predict(series):
 
     # test fit is empty
     pred = ad.fit_predict(series)
-    assert ad._is_fitted
+    assert ad.is_fitted
     assert isinstance(pred, np.ndarray)
     assert pred.shape == (10,)
     assert issubclass(pred.dtype.type, (np.integer, np.floating, np.bool_))
 
     pred = ad.fit_predict(series, test_y)
-    assert ad._is_fitted
+    assert ad.is_fitted
     assert isinstance(pred, np.ndarray)
     assert pred.shape == (10,)
     assert issubclass(pred.dtype.type, (np.integer, np.floating, np.bool_))
 
     # test requires fit
     pred = ad_fit.fit_predict(series)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X.squeeze(), series)
     assert isinstance(pred, np.ndarray)
     assert pred.shape == (10,)
     assert issubclass(pred.dtype.type, (np.integer, np.floating, np.bool_))
 
     pred = ad_fit.fit_predict(series, test_y)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X.squeeze(), series)
     assert isinstance(pred, np.ndarray)
     assert pred.shape == (10,)
@@ -160,7 +160,7 @@ def test_fit_predict(series):
         ad_y.fit_predict(series)
 
     pred = ad_y.fit_predict(series, test_y)
-    assert ad_y._is_fitted
+    assert ad_y.is_fitted
     assert_almost_equal(ad_y._X.squeeze(), series)
     assert_almost_equal(ad_y._y, test_y)
     assert isinstance(pred, np.ndarray)
@@ -176,12 +176,12 @@ def test_fit_predict_axis(series):
     invert_series = series.T
 
     pred = ad_fit.fit_predict(series)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X, series)
     assert len(pred) == series.shape[1]
 
     pred = ad_fit.fit_predict(invert_series, axis=0)
-    assert ad_fit._is_fitted
+    assert ad_fit.is_fitted
     assert_almost_equal(ad_fit._X, series)
     assert len(pred) == series.shape[1]
 
diff --git a/aeon/base/tests/test_base.py b/aeon/base/tests/test_base.py
index 3de208cf..988af2c4 100644
--- a/aeon/base/tests/test_base.py
+++ b/aeon/base/tests/test_base.py
@@ -52,8 +52,8 @@ FIXTURE_CLASSCHILD = FixtureClassChild
 FIXTURE_CLASSCHILD_TAGS = {
     "python_version": None,
     "python_dependencies": None,
-    "cant-pickle": False,
-    "non-deterministic": False,
+    "cant_pickle": False,
+    "non_deterministic": False,
     "algorithm_type": None,
     "capability:missing_values": False,
     "capability:multithreading": False,
@@ -70,8 +70,8 @@ FIXTURE_OBJECT._tags_dynamic = {"A": 42424241, "B": 3}
 FIXTURE_OBJECT_TAGS = {
     "python_version": None,
     "python_dependencies": None,
-    "cant-pickle": False,
-    "non-deterministic": False,
+    "cant_pickle": False,
+    "non_deterministic": False,
     "algorithm_type": None,
     "capability:missing_values": False,
     "capability:multithreading": False,
@@ -182,8 +182,8 @@ FIXTURE_OBJECT_SET = deepcopy(FIXTURE_OBJECT).set_tags(**FIXTURE_TAG_SET)
 FIXTURE_OBJECT_SET_TAGS = {
     "python_version": None,
     "python_dependencies": None,
-    "cant-pickle": False,
-    "non-deterministic": False,
+    "cant_pickle": False,
+    "non_deterministic": False,
     "algorithm_type": None,
     "capability:missing_values": False,
     "capability:multithreading": False,
@@ -218,20 +218,6 @@ class CompositionDummy(BaseEstimator):
         self.bar = bar
 
 
-def test_is_composite():
-    """Tests is_composite tag for correctness.
-
-    Raises
-    ------
-    AssertionError if logic behind is_composite is incorrect
-    """
-    non_composite = CompositionDummy(foo=42)
-    composite = CompositionDummy(foo=non_composite)
-
-    assert not non_composite.is_composite()
-    assert composite.is_composite()
-
-
 class ResetTester(BaseEstimator):
     clsvar = 210
 
@@ -325,7 +311,7 @@ class FittableCompositionDummy(BaseEstimator):
     def fit(self):
         if hasattr(self.foo_, "fit"):
             self.foo_.fit()
-        self._is_fitted = True
+        self.is_fitted = True
 
 
 def test_get_fitted_params():
diff --git a/aeon/classification/compose/tests/test_pipeline.py b/aeon/classification/compose/tests/test_pipeline.py
index fe3ab025..2d1e607f 100644
--- a/aeon/classification/compose/tests/test_pipeline.py
+++ b/aeon/classification/compose/tests/test_pipeline.py
@@ -118,10 +118,10 @@ def test_unequal_tag_inference():
     assert t1.get_tag("capability:unequal_length")
     assert t1.get_tag("output_data_type") == "Tabular"
     assert t2.get_tag("capability:unequal_length")
-    assert t2.get_tag("capability:unequal_length:removes")
+    assert t2.get_tag("removes_unequal_length")
     assert not t2.get_tag("output_data_type") == "Tabular"
     assert t3.get_tag("capability:unequal_length")
-    assert not t3.get_tag("capability:unequal_length:removes")
+    assert not t3.get_tag("removes_unequal_length")
     assert not t3.get_tag("output_data_type") == "Tabular"
     assert not t4.get_tag("capability:unequal_length")
 
@@ -179,14 +179,14 @@ def test_missing_tag_inference():
 
     t1 = MockCollectionTransformer()
     t1 = t1.set_tags(
-        **{"capability:missing_values": True, "capability:missing_values:removes": True}
+        **{"capability:missing_values": True, "removes_missing_values": True}
     )
     t2 = TimeSeriesScaler()
     t3 = StandardScaler()
     t4 = Tabularizer()
 
     assert t1.get_tag("capability:missing_values")
-    assert t1.get_tag("capability:missing_values:removes")
+    assert t1.get_tag("removes_missing_values")
     assert not t2.get_tag("capability:missing_values")
 
     c1 = DummyClassifier()
diff --git a/aeon/classification/dictionary_based/tests/test_boss.py b/aeon/classification/dictionary_based/tests/test_boss.py
index 749754c3..4b7d7277 100644
--- a/aeon/classification/dictionary_based/tests/test_boss.py
+++ b/aeon/classification/dictionary_based/tests/test_boss.py
@@ -1,7 +1,12 @@
 """BOSS test code."""
 
-from aeon.classification.dictionary_based import ContractableBOSS
-from aeon.testing.data_generation import make_example_2d_numpy_collection
+import pytest
+
+from aeon.classification.dictionary_based import BOSSEnsemble, ContractableBOSS
+from aeon.testing.data_generation import (
+    make_example_2d_numpy_collection,
+    make_example_3d_numpy,
+)
 
 
 def test_cboss_small_train():
@@ -10,3 +15,11 @@ def test_cboss_small_train():
     cboss = ContractableBOSS(n_parameter_samples=10, max_ensemble_size=3)
     cboss.fit(X, y)
     cboss.predict(X)
+
+
+def test_boss_min_window():
+    """Test BOSS throws error when min window too big."""
+    boss = BOSSEnsemble(min_window=20)
+    X, y = make_example_3d_numpy(n_cases=10, n_channels=1, n_timepoints=10)
+    with pytest.raises(ValueError, match="Error in BOSSEnsemble, min_window"):
+        boss._fit(X, y)
diff --git a/aeon/classification/dictionary_based/tests/test_muse.py b/aeon/classification/dictionary_based/tests/test_muse.py
new file mode 100644
index 00000000..4509d01a
--- /dev/null
+++ b/aeon/classification/dictionary_based/tests/test_muse.py
@@ -0,0 +1,16 @@
+"""Test MUSE multivariate classifier."""
+
+import pytest
+
+from aeon.classification.dictionary_based import MUSE
+from aeon.testing.data_generation import make_example_3d_numpy
+
+
+def test_muse():
+    """Test MUSE with first order differences and incorrect input."""
+    muse = MUSE(use_first_order_differences=True)
+    X, y = make_example_3d_numpy(n_cases=10, n_channels=3, n_timepoints=5)
+    X2 = muse._add_first_order_differences(X)
+    assert X2.shape[2] == X.shape[2] and X2.shape[1] == X.shape[1] * 2
+    with pytest.raises(ValueError, match="Error in MUSE, min_window"):
+        muse.fit(X, y)
diff --git a/aeon/classification/dictionary_based/tests/test_tde.py b/aeon/classification/dictionary_based/tests/test_tde.py
index d7914e48..09aa0db6 100644
--- a/aeon/classification/dictionary_based/tests/test_tde.py
+++ b/aeon/classification/dictionary_based/tests/test_tde.py
@@ -1,9 +1,19 @@
 """TDE test code."""
 
+import pickle
+
 import numpy as np
+import pytest
+from numba import types
+from numba.typed import Dict
 
-from aeon.classification.dictionary_based._tde import TemporalDictionaryEnsemble
+from aeon.classification.dictionary_based._tde import (
+    IndividualTDE,
+    TemporalDictionaryEnsemble,
+    histogram_intersection,
+)
 from aeon.datasets import load_unit_test
+from aeon.testing.data_generation import make_example_3d_numpy
 
 
 def test_tde_oob_train_estimate():
@@ -24,3 +34,72 @@ def test_tde_oob_train_estimate():
     assert isinstance(train_proba, np.ndarray)
     assert train_proba.shape == (len(X_train), 2)
     np.testing.assert_almost_equal(train_proba.sum(axis=1), 1, decimal=4)
+
+
+def test_tde_incorrect_input():
+    """Test TDE with incorrect input."""
+    # train TDE
+    tde = TemporalDictionaryEnsemble(
+        n_parameter_samples=5,
+        max_ensemble_size=2,
+        randomly_selected_params=3,
+        train_estimate_method="FOOBAR",
+        random_state=0,
+    )
+    X, y = make_example_3d_numpy(n_cases=10, n_channels=1, n_timepoints=50)
+    with pytest.raises(ValueError, match="Invalid train_estimate_method"):
+        tde.fit_predict_proba(X, y)
+    tde = TemporalDictionaryEnsemble(
+        n_parameter_samples=5,
+        max_ensemble_size=2,
+        randomly_selected_params=3,
+        min_window=100,
+        random_state=0,
+        bigrams=True,
+    )
+    import warnings
+
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore")
+        tde.fit(X, y)
+        assert tde._min_window == 50
+
+
+def test_tde_multivariate():
+    """Test TDE with incorrect input."""
+    # train TDE
+    X, y = make_example_3d_numpy(n_cases=10, n_channels=10, n_timepoints=50)
+    tde = IndividualTDE(max_dims=1)
+    tde._fit(X, y)
+    assert len(tde._dims) == 1
+
+
+def test_tde_pickle():
+    """Test the dict conversion work around for pickle."""
+    X, y = make_example_3d_numpy(n_cases=10, n_channels=1, n_timepoints=50)
+    tde = IndividualTDE(typed_dict=True)
+    tde.fit(X, y)
+    pickled_tde = pickle.dumps(tde)
+    unpickled_tde = pickle.loads(pickled_tde)
+    assert isinstance(unpickled_tde, IndividualTDE)
+
+
+def test_histogram_intersection():
+    """Test the histogram intersection function used by TDE."""
+    first = np.array([1, 0, 0, 1, 0])
+    second = np.array([1, 2, 3, 5, 10])
+    res = histogram_intersection(first, second)
+    assert res == 2
+    first = {1: 1, 4: 1}
+    second = {1: 1, 2: 2, 3: 3, 4: 5, 5: 10}
+    res = histogram_intersection(first, second)
+    assert res == 2
+    numba_first = Dict.empty(key_type=types.int64, value_type=types.int64)
+    for key, value in first.items():
+        numba_first[key] = value
+    numba_second = Dict.empty(key_type=types.int64, value_type=types.int64)
+    for key, value in second.items():
+        numba_second[key] = value
+
+    res = histogram_intersection(numba_first, numba_second)
+    assert res == 2
diff --git a/aeon/clustering/compose/tests/test_pipeline.py b/aeon/clustering/compose/tests/test_pipeline.py
index f239b704..5225e1d2 100644
--- a/aeon/clustering/compose/tests/test_pipeline.py
+++ b/aeon/clustering/compose/tests/test_pipeline.py
@@ -117,10 +117,10 @@ def test_unequal_tag_inference():
     assert t1.get_tag("capability:unequal_length")
     assert t1.get_tag("output_data_type") == "Tabular"
     assert t2.get_tag("capability:unequal_length")
-    assert t2.get_tag("capability:unequal_length:removes")
+    assert t2.get_tag("removes_unequal_length")
     assert not t2.get_tag("output_data_type") == "Tabular"
     assert t3.get_tag("capability:unequal_length")
-    assert not t3.get_tag("capability:unequal_length:removes")
+    assert not t3.get_tag("removes_unequal_length")
     assert not t3.get_tag("output_data_type") == "Tabular"
     assert not t4.get_tag("capability:unequal_length")
 
@@ -178,15 +178,13 @@ def test_missing_tag_inference():
     # X[5, 0, 4] = np.nan
 
     t1 = MockCollectionTransformer()
-    t1.set_tags(
-        **{"capability:missing_values": True, "capability:missing_values:removes": True}
-    )
+    t1.set_tags(**{"capability:missing_values": True, "removes_missing_values": True})
     t2 = TimeSeriesScaler()
     t3 = StandardScaler()
     t4 = Tabularizer()
 
     assert t1.get_tag("capability:missing_values")
-    assert t1.get_tag("capability:missing_values:removes")
+    assert t1.get_tag("removes_missing_values")
     assert not t2.get_tag("capability:missing_values")
 
     # todo revisit with mock clusterer
diff --git a/aeon/datasets/tests/test_data_writers.py b/aeon/datasets/tests/test_data_writers.py
index bf26cedc..8f5f2548 100644
--- a/aeon/datasets/tests/test_data_writers.py
+++ b/aeon/datasets/tests/test_data_writers.py
@@ -104,7 +104,7 @@ def test_write_data_to_tsfile_invalid():
     """Test function to check the handling of invalid inputs by write_to_tsfile."""
     with pytest.raises(TypeError, match="Wrong input data type"):
         write_to_tsfile("A string", "path")
-    with pytest.raises(TypeError, match="Data provided must be a ndarray or a list"):
+    with pytest.raises(ValueError, match="Data provided must be a ndarray or a list"):
         _write_data_to_tsfile("AFC", "49", "undefeated")
     X, _ = make_example_3d_numpy(n_cases=6, n_timepoints=10, n_channels=1)
     y = np.ndarray([0, 1, 1, 0, 1])
@@ -141,6 +141,37 @@ def test_write_dataframe_to_ts(tsfile_writer):
         np.testing.assert_array_almost_equal(newy.astype(int), y)
 
 
+def test_write_inputs():
+    """Tests whether error thrown if wrong input."""
+    # load an example dataset
+    problem_name = "Testy.ts"
+    with tempfile.TemporaryDirectory() as tmp:
+        # output the dataframe in a ts file
+        X, y = make_example_nested_dataframe(min_n_timepoints=12)
+        X2, y2 = make_example_3d_numpy()
+        X3, y3 = make_example_3d_numpy_list()
+        with pytest.raises(ValueError, match="Data provided must be a ndarray"):
+            _write_data_to_tsfile(
+                X=X,
+                path=tmp,
+                y=y,
+                problem_name=problem_name,
+            )
+        _write_data_to_tsfile(X=X3, path=tmp, y=y3, problem_name=problem_name)
+        with pytest.raises(ValueError, match="Data provided must be a DataFrame"):
+            _write_dataframe_to_tsfile(
+                X=X2,
+                path=tmp,
+                y=y2,
+                problem_name=problem_name,
+            )
+        with pytest.raises(TypeError, match="Wrong input data type"):
+            write_to_arff_file(X, y, tmp)
+        X2, y2 = make_example_3d_numpy(n_cases=5, n_channels=2)
+        with pytest.raises(ValueError, match="must be a 3D array with shape"):
+            write_to_arff_file(X2, y2, tmp)
+
+
 def test_write_header():
     """Test _write_header."""
     with tempfile.TemporaryDirectory() as tmp:
@@ -186,6 +217,18 @@ def test_write_results_to_uea_format():
             write_results_to_uea_format(
                 "HC", "Testy", y_pred=y_pred, y_true=y_true, output_path=tmp
             )
+        with pytest.raises(ValueError, match="Unknown 'split' value"):
+            write_results_to_uea_format(
+                "HC",
+                "Testy",
+                y_pred=y_pred,
+                output_path=tmp,
+                full_path=False,
+                split="FOO",
+                timing_type="seconds",
+                first_line_comment="Hello",
+            )
+
         y_true = np.array([0, 1, 1, 0])
         write_results_to_uea_format(
             "HC",
@@ -211,3 +254,14 @@ def test_write_results_to_uea_format():
             first_line_comment="Hello",
             predicted_probs=probs,
         )
+        write_results_to_uea_format(
+            "HC",
+            "Testy2",
+            y_pred=y_pred,
+            output_path=tmp,
+            full_path=False,
+            split="TEST",
+            timing_type="seconds",
+            first_line_comment="Hello",
+            predicted_probs=probs,
+        )
diff --git a/aeon/performance_metrics/forecasting/tests/test_metrics.py b/aeon/performance_metrics/forecasting/tests/test_metrics.py
index 8d34c80c..1d7fef49 100644
--- a/aeon/performance_metrics/forecasting/tests/test_metrics.py
+++ b/aeon/performance_metrics/forecasting/tests/test_metrics.py
@@ -1,7 +1,6 @@
 """Tests for some metrics."""
 
-# currently this consists entirely of doctests from _classes and _functions
-# since the numpy output print changes between versions
+__maintainer__ = []
 
 import numpy as np
 
diff --git a/aeon/performance_metrics/forecasting/tests/test_performance_metrics_forecasting.py b/aeon/performance_metrics/forecasting/tests/test_performance_measures.py
similarity index 100%
rename from aeon/performance_metrics/forecasting/tests/test_performance_metrics_forecasting.py
rename to aeon/performance_metrics/forecasting/tests/test_performance_measures.py
diff --git a/aeon/performance_metrics/tests/test_numpy_metrics.py b/aeon/performance_metrics/tests/test_numpy_metrics.py
index 2012ded8..ddd17421 100644
--- a/aeon/performance_metrics/tests/test_numpy_metrics.py
+++ b/aeon/performance_metrics/tests/test_numpy_metrics.py
@@ -11,7 +11,7 @@ from aeon.testing.data_generation._legacy import make_series
 
 numpy_metrics = getmembers(_functions, isfunction)
 
-exclude_starts_with = ("_", "check", "gmean")
+exclude_starts_with = ("_", "check", "gmean", "weighted_geometric_mean")
 numpy_metrics = [x for x in numpy_metrics if not x[0].startswith(exclude_starts_with)]
 
 names, metrics = zip(*numpy_metrics)
diff --git a/aeon/regression/compose/tests/test_pipeline.py b/aeon/regression/compose/tests/test_pipeline.py
index ce3085d2..c644e9f6 100644
--- a/aeon/regression/compose/tests/test_pipeline.py
+++ b/aeon/regression/compose/tests/test_pipeline.py
@@ -118,10 +118,10 @@ def test_unequal_tag_inference():
     assert t1.get_tag("capability:unequal_length")
     assert t1.get_tag("output_data_type") == "Tabular"
     assert t2.get_tag("capability:unequal_length")
-    assert t2.get_tag("capability:unequal_length:removes")
+    assert t2.get_tag("removes_unequal_length")
     assert not t2.get_tag("output_data_type") == "Tabular"
     assert t3.get_tag("capability:unequal_length")
-    assert not t3.get_tag("capability:unequal_length:removes")
+    assert not t3.get_tag("removes_unequal_length")
     assert not t3.get_tag("output_data_type") == "Tabular"
     assert not t4.get_tag("capability:unequal_length")
 
@@ -178,15 +178,13 @@ def test_missing_tag_inference():
     # X[5, 0, 4] = np.nan
 
     t1 = MockCollectionTransformer()
-    t1.set_tags(
-        **{"capability:missing_values": True, "capability:missing_values:removes": True}
-    )
+    t1.set_tags(**{"capability:missing_values": True, "removes_missing_values": True})
     t2 = TimeSeriesScaler()
     t3 = StandardScaler()
     t4 = Tabularizer()
 
     assert t1.get_tag("capability:missing_values")
-    assert t1.get_tag("capability:missing_values:removes")
+    assert t1.get_tag("removes_missing_values")
     assert not t2.get_tag("capability:missing_values")
 
     c1 = DummyRegressor()
diff --git a/aeon/testing/estimator_checking/_estimator_checking.py b/aeon/testing/estimator_checking/_estimator_checking.py
index bd5af8ff..1e0f1621 100644
--- a/aeon/testing/estimator_checking/_estimator_checking.py
+++ b/aeon/testing/estimator_checking/_estimator_checking.py
@@ -113,7 +113,7 @@ def check_estimator(
     general checks for all estimators, and module-specific tests for classifiers,
     anomaly detectors, transformer, etc.
     Some checks may be skipped if the estimator has certain tags i.e.
-    `non-deterministic`.
+    `non_deterministic`.
 
     Parameters
     ----------
diff --git a/aeon/testing/estimator_checking/_yield_collection_transformation_checks.py b/aeon/testing/estimator_checking/_yield_collection_transformation_checks.py
index 20f859d5..ee7636d5 100644
--- a/aeon/testing/estimator_checking/_yield_collection_transformation_checks.py
+++ b/aeon/testing/estimator_checking/_yield_collection_transformation_checks.py
@@ -35,7 +35,7 @@ def check_channel_selectors(estimator_class):
     """
     X, _ = make_example_3d_numpy(n_cases=20, n_channels=6, n_timepoints=30)
     y = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1])
-    cs = estimator_class()
+    cs = estimator_class.create_test_instance(return_first=True)
     assert not cs.get_tag("fit_is_empty")
     cs.fit(X, y)
     assert cs.channels_selected_ is not None
diff --git a/aeon/testing/estimator_checking/_yield_estimator_checks.py b/aeon/testing/estimator_checking/_yield_estimator_checks.py
index 172fc35b..7c623c05 100644
--- a/aeon/testing/estimator_checking/_yield_estimator_checks.py
+++ b/aeon/testing/estimator_checking/_yield_estimator_checks.py
@@ -177,9 +177,6 @@ def _yield_estimator_checks(estimator_class, estimator_instances, datatypes):
     """Yield all general checks for an aeon estimator."""
     # only class required
     yield partial(check_create_test_instance, estimator_class=estimator_class)
-    yield partial(
-        check_create_test_instances_and_names, 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)
@@ -219,14 +216,14 @@ def _yield_estimator_checks(estimator_class, estimator_instances, datatypes):
                 datatype=datatypes[i][0],
             )
 
-        if not _get_tag(estimator, "cant-pickle", default=False):
+        if not _get_tag(estimator, "cant_pickle", default=False):
             yield partial(
                 check_persistence_via_pickle,
                 estimator=estimator,
                 datatype=datatypes[i][0],
             )
 
-        if not _get_tag(estimator, "non-deterministic", default=False):
+        if not _get_tag(estimator, "non_deterministic", default=False):
             yield partial(
                 check_fit_deterministic, estimator=estimator, datatype=datatypes[i][0]
             )
@@ -261,43 +258,6 @@ def check_create_test_instance(estimator_class):
     assert hasattr(estimator, "_tags_dynamic"), msg
 
 
-# todo consider deprecation
-def check_create_test_instances_and_names(estimator_class):
-    """Check that create_test_instances_and_names works.
-
-    create_test_instance and create_test_instances_and_names are the
-    key methods used to create test instances in testing.
-    If this test does not pass, validity of the other tests cannot be guaranteed.
-
-    Tests expected function signature of create_test_instances_and_names.
-    """
-    estimators, names = estimator_class.create_test_instances_and_names()
-
-    assert isinstance(estimators, list), (
-        "first return of create_test_instances_and_names must be a list, "
-        f"found {type(estimators)}"
-    )
-    assert isinstance(names, list), (
-        "second return of create_test_instances_and_names must be a list, "
-        f"found {type(names)}"
-    )
-
-    assert np.all([isinstance(est, estimator_class) for est in estimators]), (
-        "list elements of first return returned by create_test_instances_and_names "
-        "all must be an instance of the class"
-    )
-
-    assert np.all([isinstance(name, str) for name in names]), (
-        "list elements of second return returned by create_test_instances_and_names"
-        " all must be strings"
-    )
-
-    assert len(estimators) == len(names), (
-        "the two lists returned by create_test_instances_and_names must have "
-        "equal length"
-    )
-
-
 # todo consider removing the multiple base class allowance.
 def check_inheritance(estimator_class):
     """Check that estimator inherits from BaseEstimator."""
@@ -620,8 +580,6 @@ def check_fit_updates_state(estimator, datatype):
     5. Check estimator hyper parameters are not changed in fit
     """
     # Check that fit updates the is-fitted states
-    attrs = ["_is_fitted", "is_fitted"]
-
     estimator = _clone_estimator(estimator)
 
     msg = (
@@ -630,13 +588,12 @@ def check_fit_updates_state(estimator, datatype):
         "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
+    assert hasattr(estimator, "is_fitted"), msg
 
     # Check is_fitted attribute is set correctly to False before fit, at init
-    for attr in attrs:
-        assert not getattr(
-            estimator, attr
-        ), f"Estimator: {estimator} does not initiate attribute: {attr} to False"
+    assert (
+        not estimator.is_fitted
+    ), f"Estimator: {estimator} does not initiate attribute: is_fitted to False"
 
     # Make a physical copy of the original estimator parameters before fitting.
     original_params = deepcopy(estimator.get_params())
@@ -649,10 +606,9 @@ def check_fit_updates_state(estimator, datatype):
     ), f"Estimator: {estimator} does not return self when calling fit"
 
     # Check is_fitted attribute is updated correctly to True after calling fit
-    for attr in attrs:
-        assert getattr(
-            fitted_estimator, attr
-        ), f"Estimator: {estimator} does not update attribute: {attr} during fit"
+    assert (
+        fitted_estimator.is_fitted
+    ), f"Estimator: {estimator} does not update attribute: is_fitted during fit"
 
     # Compare the state of the model parameters with the original parameters
     new_params = fitted_estimator.get_params()
diff --git a/aeon/testing/testing_config.py b/aeon/testing/testing_config.py
index 2a6ca32a..9d8f96ab 100644
--- a/aeon/testing/testing_config.py
+++ b/aeon/testing/testing_config.py
@@ -15,10 +15,12 @@ PR_TESTING = False
 EXCLUDE_ESTIMATORS = [
     "SeriesSearch",
     "QuerySearch",
+    "ClearSkyTransformer",
     # See #2071
     "RandomDilatedShapeletTransform",
     "RDSTClassifier",
     "RDSTRegressor",
+    "RISTRegressor",
 ]
 
 
diff --git a/aeon/transformations/collection/channel_selection/tests/test_channel_scorer.py b/aeon/transformations/collection/channel_selection/tests/test_channel_scorer.py
index 5607be3a..a57ac558 100644
--- a/aeon/transformations/collection/channel_selection/tests/test_channel_scorer.py
+++ b/aeon/transformations/collection/channel_selection/tests/test_channel_scorer.py
@@ -3,32 +3,79 @@
 import numpy as np
 import pytest
 
+from aeon.regression import DummyRegressor
 from aeon.testing.data_generation import make_example_3d_numpy
 from aeon.testing.mock_estimators import MockClassifierFullTags
 from aeon.transformations.collection.channel_selection import ChannelScorer
 
 
-def test_channel_scorer():
-    """Test the channel scorer."""
+def test_channel_scorer_with_classifier():
+    """Test the channel scorer for classifier."""
     # Test selects the correct number of channels
-    cs = ChannelScorer(classifier=MockClassifierFullTags(), proportion=0.5)
+    cs = ChannelScorer(estimator=MockClassifierFullTags(), proportion=0.5)
     X, _ = make_example_3d_numpy(n_cases=20, n_channels=10)
     y = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1])
     cs.fit(X, y)
     assert len(cs.channels_selected_) == 5
     X, _ = make_example_3d_numpy(n_cases=20, n_channels=9)
     assert len(cs.channels_selected_) == 5
+
+    # Test for proportion out of range
     with pytest.raises(ValueError, match="proportion must be in the range 0-1"):
-        cs = ChannelScorer(classifier=MockClassifierFullTags(), proportion=1.1)
+        cs = ChannelScorer(estimator=MockClassifierFullTags(), proportion=1.1)
         cs.fit(X, y)
+
     with pytest.raises(ValueError, match="proportion must be in the range 0-1"):
-        ChannelScorer(classifier=MockClassifierFullTags(), proportion=-1)
+        ChannelScorer(estimator=MockClassifierFullTags(), proportion=-1)
         cs.fit(X, y)
-    cs = ChannelScorer(classifier=MockClassifierFullTags(), proportion=1.0)
+
+    # Test for selecting all channels when proportion=1.0
+    cs = ChannelScorer(estimator=MockClassifierFullTags(), proportion=1.0)
     cs.fit(X, y)
     assert len(cs.channels_selected_) == 9
+
+    # Test for invalid estimator
+    with pytest.raises(
+        ValueError,
+        match="parameter estimator must be an instance of BaseClassifier, "
+        "BaseRegressor",
+    ):
+        cs = ChannelScorer(estimator="FOOBAR", proportion=0.5)
+        cs.fit(X, y)
+
+
+def test_channel_scorer_with_regressor():
+    """Test the channel scorer for regressor."""
+    # Test selects the correct number of channels
+    cs = ChannelScorer(estimator=DummyRegressor(), proportion=0.5)
+    X, _ = make_example_3d_numpy(n_cases=20, n_channels=10)
+    y = np.random.rand(20)
+    cs.fit(X, y)
+    assert len(cs.channels_selected_) == 5
+
+    X, _ = make_example_3d_numpy(n_cases=20, n_channels=9)
+    cs.fit(X, y)
+    assert len(cs.channels_selected_) == 5
+
+    # Test for proportion out of range
+    with pytest.raises(ValueError, match="proportion must be in the range 0-1"):
+        cs = ChannelScorer(estimator=DummyRegressor(), proportion=1.1)
+        cs.fit(X, y)
+
+    with pytest.raises(ValueError, match="proportion must be in the range 0-1"):
+        cs = ChannelScorer(estimator=DummyRegressor(), proportion=-0.1)
+        cs.fit(X, y)
+
+    # Test for selecting all channels when proportion=1.0
+    cs = ChannelScorer(estimator=DummyRegressor(), proportion=1.0)
+    cs.fit(X, y)
+    assert len(cs.channels_selected_) == 9
+
+    # Test for invalid estimator
     with pytest.raises(
-        ValueError, match="parameter classifier must be None or an instance of"
+        ValueError,
+        match="parameter estimator must be an instance of BaseClassifier, "
+        "BaseRegressor",
     ):
-        cs = ChannelScorer(classifier="FOOBAR", proportion=0.5)
+        cs = ChannelScorer(estimator="FOOBAR", proportion=0.5)
         cs.fit(X, y)
diff --git a/aeon/transformations/collection/compose/tests/test_pipeline.py b/aeon/transformations/collection/compose/tests/test_pipeline.py
index 8320b6fd..21131233 100644
--- a/aeon/transformations/collection/compose/tests/test_pipeline.py
+++ b/aeon/transformations/collection/compose/tests/test_pipeline.py
@@ -69,10 +69,10 @@ def test_unequal_tag_inference():
     assert t1.get_tag("capability:unequal_length")
     assert t1.get_tag("output_data_type") == "Tabular"
     assert t2.get_tag("capability:unequal_length")
-    assert t2.get_tag("capability:unequal_length:removes")
+    assert t2.get_tag("removes_unequal_length")
     assert not t2.get_tag("output_data_type") == "Tabular"
     assert t3.get_tag("capability:unequal_length")
-    assert not t3.get_tag("capability:unequal_length:removes")
+    assert not t3.get_tag("removes_unequal_length")
     assert not t3.get_tag("output_data_type") == "Tabular"
     assert not t4.get_tag("capability:unequal_length")
 
@@ -115,15 +115,13 @@ def test_missing_tag_inference():
     X, y = make_example_3d_numpy(n_cases=10, n_timepoints=12)
 
     t1 = MockCollectionTransformer()
-    t1.set_tags(
-        **{"capability:missing_values": True, "capability:missing_values:removes": True}
-    )
+    t1.set_tags(**{"capability:missing_values": True, "removes_missing_values": True})
     t2 = TimeSeriesScaler()
     t3 = StandardScaler()
     t4 = Tabularizer()
 
     assert t1.get_tag("capability:missing_values")
-    assert t1.get_tag("capability:missing_values:removes")
+    assert t1.get_tag("removes_missing_values")
     assert not t2.get_tag("capability:missing_values")
 
     # transformer chain removes missing values
diff --git a/aeon/transformations/collection/shapelet_based/tests/test_dilated_shapelet_transform.py b/aeon/transformations/collection/shapelet_based/tests/test_dilated_shapelet_transform.py
index 2391af51..124cf07d 100644
--- a/aeon/transformations/collection/shapelet_based/tests/test_dilated_shapelet_transform.py
+++ b/aeon/transformations/collection/shapelet_based/tests/test_dilated_shapelet_transform.py
@@ -144,9 +144,7 @@ def test_compute_shapelet_features(dtype):
     dilation = 1
     threshold = 0.01
     X_subs = get_all_subsequences(X, length, dilation)
-    _min, _argmin, SO = compute_shapelet_features(
-        X_subs, values, length, threshold, manhattan_distance
-    )
+    _min, _argmin, SO = compute_shapelet_features(X_subs, values, threshold)
 
     # On some occasion, float32 precision with fasmath retruns things like
     # 2.1835059227370834e-07 instead of 0
@@ -157,9 +155,7 @@ def test_compute_shapelet_features(dtype):
     dilation = 2
     threshold = 0.1
     X_subs = get_all_subsequences(X, length, dilation)
-    _min, _argmin, SO = compute_shapelet_features(
-        X_subs, values, length, threshold, manhattan_distance
-    )
+    _min, _argmin, SO = compute_shapelet_features(X_subs, values, threshold)
 
     assert_almost_equal(_min, 0.0, decimal=4)
     assert _argmin == 7.0
@@ -168,9 +164,7 @@ def test_compute_shapelet_features(dtype):
     dilation = 4
     threshold = 2
     X_subs = get_all_subsequences(X, length, dilation)
-    _min, _argmin, SO = compute_shapelet_features(
-        X_subs, values, length, threshold, manhattan_distance
-    )
+    _min, _argmin, SO = compute_shapelet_features(X_subs, values, threshold)
 
     assert_almost_equal(_min, 0.0, decimal=4)
     assert _argmin == 3.0
@@ -185,9 +179,7 @@ def test_compute_shapelet_dist_vector(dtype):
         for dilation in [1, 3, 5]:
             values = np.random.rand(3, length).astype(dtype)
             X_subs = get_all_subsequences(X, length, dilation)
-            d_vect = compute_shapelet_dist_vector(
-                X_subs, values, length, manhattan_distance
-            )
+            d_vect = compute_shapelet_dist_vector(X_subs, values)
             true_vect = np.zeros(X.shape[1] - (length - 1) * dilation)
             for i_sub in range(true_vect.shape[0]):
                 _idx = [i_sub + j * dilation for j in range(length)]
diff --git a/aeon/utils/tags/tests/test_lookup.py b/aeon/utils/tags/tests/test_lookup.py
index f36202eb..fd3d57fa 100644
--- a/aeon/utils/tags/tests/test_lookup.py
+++ b/aeon/utils/tags/tests/test_lookup.py
@@ -16,7 +16,7 @@ def test_all_tags_for_estimator_classification():
 
     assert isinstance(tags, dict)
     assert "python_version" in tags
-    assert "non-deterministic" in tags
+    assert "non_deterministic" in tags
     assert "capability:unequal_length" in tags
     assert "capability:contractable" in tags
 
@@ -37,7 +37,7 @@ def test_all_tags_for_estimator_anomaly_detection():
 
     assert isinstance(tags, dict)
     assert "python_version" in tags
-    assert "non-deterministic" in tags
+    assert "non_deterministic" in tags
     assert "capability:unequal_length" not in tags
     assert "capability:contractable" not in tags
 
diff --git a/aeon/utils/tags/tests/test_validate.py b/aeon/utils/tags/tests/test_validate.py
index daf1afeb..ca80135c 100644
--- a/aeon/utils/tags/tests/test_validate.py
+++ b/aeon/utils/tags/tests/test_validate.py
@@ -25,7 +25,7 @@ def test_check_valid_tags(estimator):
     tags = {
         "python_version": ">3.8",
         "python_dependencies": None,
-        "non-deterministic": False,
+        "non_deterministic": False,
     }
 
     check_valid_tags(estimator, tags=tags, error_on_missing=False)
@@ -61,8 +61,8 @@ def test_check_valid_tags_invalid():
     tags = {
         "python_version": ">3.8",
         "python_dependencies": None,
-        "non-deterministic": 1,
+        "non_deterministic": 1,
     }
 
-    with pytest.raises(ValueError, match="Tag non-deterministic has an invalid value"):
+    with pytest.raises(ValueError, match="Tag non_deterministic has an invalid value"):
         check_valid_tags(MockClassifier, tags=tags)
diff --git a/aeon/utils/tests/test_show_versions.py b/aeon/utils/tests/test_show_versions.py
new file mode 100644
index 00000000..2d2f1ec4
--- /dev/null
+++ b/aeon/utils/tests/test_show_versions.py
@@ -0,0 +1,10 @@
+"""Test the show versions function."""
+
+from aeon.utils._show_versions import _show_versions
+
+
+def test_show_versions():
+    """Test show versions function."""
+    str = _show_versions()
+    assert "System" in str
+    assert "Python dependencies" in str
diff --git a/aeon/utils/tests/test_sklearn_typing.py b/aeon/utils/tests/test_sklearn_typing.py
index 1facf351..cc0592d1 100644
--- a/aeon/utils/tests/test_sklearn_typing.py
+++ b/aeon/utils/tests/test_sklearn_typing.py
@@ -4,8 +4,11 @@ __maintainer__ = []
 
 
 import pytest
+from sklearn import svm
 from sklearn.cluster import KMeans
+from sklearn.model_selection import GridSearchCV
 from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor
+from sklearn.pipeline import Pipeline
 from sklearn.preprocessing import StandardScaler
 
 from aeon.classification.feature_based import SummaryClassifier
@@ -53,3 +56,17 @@ def test_sklearn_identifiers(estimator):
         f" {expected_identifier}, but {identifier} was returned."
     )
     assert identifier == expected_identifier, msg
+
+
+def test_sklearn_identifiers_inputs():
+    """Test variant inputs for  sklearn_estimator_identifier."""
+    with pytest.raises(TypeError, match="not an sklearn estimator"):
+        sklearn_estimator_identifier("ARSENAL")
+    pipe = Pipeline([("scaler", StandardScaler()), ("kmeans", KMeans())])
+    id = sklearn_estimator_identifier(pipe)
+    assert id == "clusterer"
+    parameters = {"kernel": ("linear", "rbf"), "C": [1, 10]}
+    svc = svm.SVC()
+    cls = GridSearchCV(svc, parameters)
+    id = sklearn_estimator_identifier(cls)
+    assert id == "classifier"
diff --git a/aeon/utils/tests/test_weighted_metrics.py b/aeon/utils/tests/test_weighted_metrics.py
new file mode 100644
index 00000000..f35d0394
--- /dev/null
+++ b/aeon/utils/tests/test_weighted_metrics.py
@@ -0,0 +1,25 @@
+"""Test weighted metric."""
+
+import numpy as np
+import pytest
+
+from aeon.utils._weighted_metrics import weighted_geometric_mean
+
+
+def test_weighted_geometric_mean():
+    """Test weighted_geometric_mean."""
+    y = np.array([1.0, 2.0, 3.0])
+    w = np.array([0.1, 0.8, 0.1])
+    w2 = np.array([[0.1, 0.8, 0.1]]).T
+    res = weighted_geometric_mean(y, w)
+    assert round(res, 5) == 1.94328
+    res2 = weighted_geometric_mean(y, w, axis=0)
+    assert res == res2
+    y2 = np.array([[1.0, 2.0, 3.0]]).T
+    with pytest.raises(ValueError, match="do not match"):
+        weighted_geometric_mean(y2, w, axis=1)
+    weighted_geometric_mean(y2, w2, axis=1)
+    with pytest.raises(
+        ValueError, match="Input data and weights have inconsistent shapes"
+    ):
+        weighted_geometric_mean(y, w2)
diff --git a/aeon/utils/tests/test_windowing.py b/aeon/utils/tests/test_windowing.py
index d99c69cb..8e3bfe48 100644
--- a/aeon/utils/tests/test_windowing.py
+++ b/aeon/utils/tests/test_windowing.py
@@ -303,3 +303,11 @@ def test_all_reverse_windowing_implementations_equal(ws):
     np.testing.assert_array_equal(res_vec, res_strided)
     np.testing.assert_array_equal(res_vec, res_iter)
     np.testing.assert_array_equal(res_strided, res_iter)
+
+
+def test_wrong_input():
+    """Test error raises."""
+    with pytest.raises(
+        ValueError, match="padding_length must be provided when stride " "is not 1"
+    ):
+        reverse_windowing(_DATA[:9], window_size=2, stride=2)