Repository: aeon-toolkit/aeon
Pull request #979 (2023-12-07 00:09:50)

[ENH] Remove the alignment ADT from datatypes
Alignment was related to a long dead module, this code can I think just be removed

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 have added the following labels to this PR based on the changes made: **[ $\\color{#BDD9DB}{\\textsf{datatypes}}$ ]**. Feel free to change these if they do not properly represent the PR.

The [Checks](https://github.com/aeon-toolkit/aeon/pull/979/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!
    

Patch:
diff --git a/.github/workflows/pr_examples.yml b/.github/workflows/pr_examples.yml
index 677bf2d3..1c5b9dac 100644
--- a/.github/workflows/pr_examples.yml
+++ b/.github/workflows/pr_examples.yml
@@ -24,7 +24,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
diff --git a/.github/workflows/pr_opened.yml b/.github/workflows/pr_opened.yml
index dd9cb691..e78514fa 100644
--- a/.github/workflows/pr_opened.yml
+++ b/.github/workflows/pr_opened.yml
@@ -19,7 +19,7 @@ jobs:
         with:
           sparse-checkout: build_tools
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
diff --git a/.github/workflows/pr_precommit.yml b/.github/workflows/pr_precommit.yml
index 66d7f754..ad4bae2a 100644
--- a/.github/workflows/pr_precommit.yml
+++ b/.github/workflows/pr_precommit.yml
@@ -19,11 +19,11 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
-      - uses: tj-actions/changed-files@v40.2.0
+      - uses: tj-actions/changed-files@v40.2.2
         id: changed-files
 
       - name: List changed files
diff --git a/.github/workflows/precommit_autoupdate.yml b/.github/workflows/precommit_autoupdate.yml
index 2cf8d7f5..4951b78c 100644
--- a/.github/workflows/precommit_autoupdate.yml
+++ b/.github/workflows/precommit_autoupdate.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 987b8634..3bcaa0b5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
@@ -28,7 +28,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
@@ -57,7 +57,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
 
diff --git a/aeon/base/_base.py b/aeon/base/_base.py
index 85ec51ef..eecafe30 100644
--- a/aeon/base/_base.py
+++ b/aeon/base/_base.py
@@ -96,14 +96,14 @@ class BaseObject(_BaseEstimator):
         """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 ``self.reset()`` call, self is equal in value to
+        ``type(self)(**self.get_params(deep=False))``
 
         Detail behaviour:
         removes any object attributes, except:
-            hyper-parameters = arguments of __init__
+            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)
+        runs ``__init__`` with current values of hyper-parameters (result of get_params)
 
         Not affected by the reset are:
         object attributes containing double-underscores
@@ -130,11 +130,11 @@ class BaseObject(_BaseEstimator):
 
         A clone is a different object without shared references, in post-init state.
         This function is equivalent to returning sklearn.clone of self.
-        Equal in value to `type(self)(**self.get_params(deep=False))`.
+        Equal in value to ``type(self)(**self.get_params(deep=False))``.
 
         Returns
         -------
-        instance of type(self), clone of self (see above)
+        instance of ``type(self)``, clone of self (see above)
         """
         return clone(self)
 
@@ -148,7 +148,7 @@ class BaseObject(_BaseEstimator):
 
         Raises
         ------
-        RuntimeError if cls has varargs in __init__
+        RuntimeError if cls has varargs in ``__init__``
         """
         # fetch the constructor or the original constructor before
         # deprecation wrapping if any
@@ -982,7 +982,7 @@ class BaseEstimator(BaseObject):
 
     @property
     def is_fitted(self):
-        """Whether `fit` has been called."""
+        """Whether ``fit`` has been called."""
         return self._is_fitted
 
     def check_is_fitted(self):
@@ -1023,13 +1023,13 @@ class BaseEstimator(BaseObject):
             Dictionary of fitted parameters, paramname : paramvalue
             keys-value pairs include:
 
-            * always: all fitted parameters of this object, as via `get_param_names`
+            * always: all fitted parameters of this object, as via ``get_param_names``
               values are fitted parameter value for that key, of this object
-            * if `deep=True`, also contains keys/value pairs of component parameters
-              parameters of components are indexed as `[componentname]__[paramname]`
-              all parameters of `componentname` appear as `paramname` with its value
-            * if `deep=True`, also contains arbitrary levels of component recursion,
-              e.g., `[componentname]__[componentcomponentname]__[paramname]`, etc.
+            * if ``deep=True``, also contains keys/value pairs of component parameters
+              parameters of components are indexed as ``[componentname]__[paramname]``
+              all parameters of ``componentname`` appear as ``paramname`` with its value
+            * if ``deep=True``, also contains arbitrary levels of component recursion,
+              e.g., ``[componentname]__[componentcomponentname]__[paramname]``, etc.
         """
         if not self.is_fitted:
             raise NotFittedError(
diff --git a/aeon/classification/distance_based/_time_series_neighbors.py b/aeon/classification/distance_based/_time_series_neighbors.py
index 8af7927a..45f35033 100644
--- a/aeon/classification/distance_based/_time_series_neighbors.py
+++ b/aeon/classification/distance_based/_time_series_neighbors.py
@@ -1,8 +1,8 @@
 """KNN time series classification.
 
 A KNN classifier which supports time series distance measures.
-The class has hardcoded string references to numba based distances in aeon.distances.
-It can also be used with callable distance functions.
+The class can take callables or uses string references to utilise the numba based
+distances in aeon.distances.
 """
 
 __author__ = ["TonyBagnall", "GuiArcencio"]
@@ -21,8 +21,8 @@ class KNeighborsTimeSeriesClassifier(BaseClassifier):
     K-Nearest Neighbour Time Series Classifier.
 
     A KNN classifier which supports time series distance measures.
-    It has hardcoded string references to numba based distances in aeon.distances,
-    and can also be used with callables, or aeon (pairwise transformer) estimators.
+    It determines distance function through string references to numba
+    based distances in aeon.distances, and can also be used with callables.
 
     Parameters
     ----------
@@ -33,22 +33,20 @@ class KNeighborsTimeSeriesClassifier(BaseClassifier):
         function.
     distance : str or callable, default ='dtw'
         Distance measure between time series.
-        if str, must be one of the following strings:
-            'euclidean', 'squared', 'dtw', 'ddtw', 'wdtw', 'wddtw',
-            'lcss', 'edr', 'erp', 'msm', 'twe', 'mpdist'
-        this will substitute a hard-coded distance metric from aeon.distances
-        When mpdist is used, the subsequence length (parameter m) must be set
-            Example: knn_mpdist = KNeighborsTimeSeriesClassifier(
-                                distance='mpdist', distance_params={'m':30})
-        if callable, must be of signature (X: np.ndarray, X2: np.ndarray) -> np.ndarray
+        Distance metric to compute similarity between time series. A list of valid
+        strings for metrics can be found in the documentation for
+        :func:`aeon.distances.get_distance_function` or through calling
+        :func:`aeon.distances.get_distance_function_names`. If a
+        callable is passed it must be
+        a function that takes two 2d numpy arrays of shape ``(n_channels,
+        n_timepoints)`` as input and returns a float.
     distance_params : dict, default = None
         Dictionary for metric parameters for the case that distance is a str.
     n_jobs : int, default = None
         The number of parallel jobs to run for neighbors search.
         ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
         ``-1`` means using all processors. See :term:`Glossary <n_jobs>`
-        for more details.
-        Parameter for compatibility purposes, still unimplemented.
+        for more details. Parameter for compatibility purposes, still unimplemented.
 
     Examples
     --------
@@ -179,11 +177,8 @@ class KNeighborsTimeSeriesClassifier(BaseClassifier):
 
         Parameters
         ----------
-        X : 3D np.ndarray of shape = (n_cases, n_channels, n_timepoints) or list of
-        shape[n_cases] of 2D arrays shape (n_channels,n_timepoints_i)
-                If the series are all equal length, a numpy3D will be passed. If
-                unequal, a list of 2D numpy arrays is passed, which may have
-                different lengths.
+        X : np.ndarray
+            A single time series instance if shape = (n_channels, n_timepoints)
 
         Returns
         -------
diff --git a/aeon/datatypes/_alignment/__init__.py b/aeon/datatypes/_alignment/__init__.py
deleted file mode 100644
index 19e0292d..00000000
--- a/aeon/datatypes/_alignment/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""Module exports: Alignment type checkers and mtype inference."""
-
-from aeon.datatypes._alignment._check import check_dict as check_dict_Alignment
-from aeon.datatypes._alignment._examples import example_dict as example_dict_Alignment
-from aeon.datatypes._alignment._registry import (
-    MTYPE_LIST_ALIGNMENT,
-    MTYPE_REGISTER_ALIGNMENT,
-)
-
-__all__ = [
-    "check_dict_Alignment",
-    "example_dict_Alignment",
-    "MTYPE_LIST_ALIGNMENT",
-    "MTYPE_REGISTER_ALIGNMENT",
-]
diff --git a/aeon/datatypes/_alignment/_check.py b/aeon/datatypes/_alignment/_check.py
deleted file mode 100644
index bceaf42f..00000000
--- a/aeon/datatypes/_alignment/_check.py
+++ /dev/null
@@ -1,129 +0,0 @@
-"""Machine type checkers for Alignment scitype.
-
-Exports checkers for Alignment scitype:
-
-check_dict: dict indexed by pairs of str
-  1st element = mtype - str
-  2nd element = scitype - str
-elements are checker/validation functions for mtype
-
-Function signature of all elements
-check_dict[(mtype, scitype)]
-
-Parameters
-----------
-obj - object to check
-return_metadata - bool, optional, default=False
-    if False, returns only "valid" return
-    if True, returns all three return objects
-var_name: str, optional, default="obj" - name of input in error messages
-
-Returns
--------
-valid: bool - whether obj is a valid object of mtype/scitype
-msg: str - error message if object is not valid, otherwise None
-        returned only if return_metadata is True
-metadata: dict - metadata about obj if valid, otherwise None
-        returned only if return_metadata is True
-    fields:
-        currently none, placeholder
-"""
-
-__author__ = ["fkiraly"]
-
-__all__ = ["check_dict"]
-
-import numpy as np
-import pandas as pd
-
-check_dict = dict()
-
-
-def check_align(align_df, name="align_df", index="iloc"):
-    """Check whether the object is a data frame in alignment format.
-
-    Parameters
-    ----------
-    align_df : any object
-        check passes if it follows alignment format, as follows:
-        pandas.DataFrame with column names 'ind'+str(i) for integers i, as follows
-            all integers i between 0 and some natural number n must be present
-    name : string, optional, default="align_df"
-        variable name that is printed in ValueError-s
-    index : string, optional, one of "iloc" (default), "loc", "either"
-        whether alignment to check is "loc" or "iloc"
-
-    Returns
-    -------
-    valid : boolean, whether align_df is a valid alignment data frame
-    msg : error message if align_df is invalid
-    """
-    if not isinstance(align_df, pd.DataFrame):
-        msg = f"{name} is not a pandas DataFrame"
-        return False, msg
-
-    cols = align_df.columns
-    n = len(cols)
-
-    correctcols = set([f"ind{i}" for i in range(n)])
-
-    if not set(cols) == set(correctcols):
-        msg = f"{name} index columns must be named 'ind0', 'ind1', ... 'ind{n}'"
-        return False, msg
-
-    if index == "iloc":
-        # checks whether df columns are of integer (numpy or pandas nullable) type
-        dtypearr = np.array([str(x) for x in align_df[cols].dtypes])
-        allowedtypes = np.array(
-            [
-                "int",
-                "int8",
-                "int16",
-                "int32",
-                "int64",
-                "uint8",
-                "uint16",
-                "uint32",
-                "uint64",
-                "Int8",
-                "Int16",
-                "Int32",
-                "Int64",
-                "UInt8",
-                "UInt16",
-                "UInt32",
-                "UInt64",
-            ]
-        )
-        if not np.all(np.isin(dtypearr, allowedtypes)):
-            msg = f"columns of {name} must have dtype intX, uintX, IntX, or UIntX"
-            return False, msg
-    # no additional restrictions apply if loc or either, so no elif
-
-    return True, ""
-
-
-def check_alignment_alignment(obj, return_metadata=False, var_name="obj"):
-    """Check whether object has mtype `alignment` for scitype `Alignment`."""
-    valid, msg = check_align(obj, name=var_name, index="iloc")
-
-    if return_metadata:
-        return valid, msg, dict()
-    else:
-        return valid
-
-
-check_dict[("alignment", "Alignment")] = check_alignment_alignment
-
-
-def check_alignment_loc_alignment(obj, return_metadata=False, var_name="obj"):
-    """Check whether object has mtype `alignment_loc` for scitype `Alignment`."""
-    valid, msg = check_align(obj, name=var_name, index="loc")
-
-    if return_metadata:
-        return valid, msg, dict()
-    else:
-        return valid
-
-
-check_dict[("alignment_loc", "Alignment")] = check_alignment_loc_alignment
diff --git a/aeon/datatypes/_alignment/_examples.py b/aeon/datatypes/_alignment/_examples.py
deleted file mode 100644
index 4c797d13..00000000
--- a/aeon/datatypes/_alignment/_examples.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""Example generation for testing.
-
-Exports dict of examples, useful for testing as fixtures.
-
-example_dict: dict indexed by triple
-  1st element = mtype - str
-  2nd element = considered as this scitype - str
-  3rd element = int - index of example
-elements are data objects, considered examples for the mtype
-    all examples with same index are considered "same" on scitype content
-    if None, indicates that representation is not possible
-"""
-
-import pandas as pd
-
-example_dict = dict()
-
-###
-
-align = pd.DataFrame({"ind0": [1, 2, 2, 3], "ind1": [0, 0, 1, 1]})
-
-example_dict[("alignment", "Alignment", 0)] = align
-
-
-align = pd.DataFrame({"ind0": [2, 2.5, 2.5, 100], "ind1": [-1, -1, 2, 2]})
-
-example_dict[("alignment_loc", "Alignment", 0)] = align
diff --git a/aeon/datatypes/_alignment/_registry.py b/aeon/datatypes/_alignment/_registry.py
deleted file mode 100644
index 5082caad..00000000
--- a/aeon/datatypes/_alignment/_registry.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import pandas as pd
-
-__all__ = [
-    "MTYPE_REGISTER_ALIGNMENT",
-    "MTYPE_LIST_ALIGNMENT",
-]
-
-
-MTYPE_REGISTER_ALIGNMENT = [
-    (
-        "alignment",
-        "Alignment",
-        "pd.DataFrame in alignment format, values are iloc index references",
-    ),
-    (
-        "alignment_loc",
-        "Alignment",
-        "pd.DataFrame in alignment format, values are loc index references",
-    ),
-]
-
-MTYPE_LIST_ALIGNMENT = pd.DataFrame(MTYPE_REGISTER_ALIGNMENT)[0].values
diff --git a/aeon/datatypes/_check.py b/aeon/datatypes/_check.py
index 082517e0..9ff98b61 100644
--- a/aeon/datatypes/_check.py
+++ b/aeon/datatypes/_check.py
@@ -26,7 +26,6 @@ from typing import List, Union
 
 import numpy as np
 
-from aeon.datatypes._alignment import check_dict_Alignment
 from aeon.datatypes._hierarchical import check_dict_Hierarchical
 from aeon.datatypes._panel import check_dict_Panel
 from aeon.datatypes._proba import check_dict_Proba
@@ -39,7 +38,6 @@ check_dict = dict()
 check_dict.update(check_dict_Series)
 check_dict.update(check_dict_Panel)
 check_dict.update(check_dict_Hierarchical)
-check_dict.update(check_dict_Alignment)
 check_dict.update(check_dict_Table)
 check_dict.update(check_dict_Proba)
 
@@ -143,8 +141,6 @@ def check_is_mtype(
             "is_empty": bool, True iff table has no variables or no instances
             "has_nans": bool, True iff the panel contains NaN values
             "n_instances": int, number of instances/rows in the table
-        For scitype "Alignment":
-            currently none
 
     Raises
     ------
@@ -387,8 +383,7 @@ def check_is_scitype(
             "is_univariate": bool, True iff table has one variable
             "is_empty": bool, True iff table has no variables or no instances
             "has_nans": bool, True iff the panel contains NaN values
-        For scitype "Alignment":
-            currently none
+
     Raises
     ------
     TypeError if scitype input argument is not of expected type
diff --git a/aeon/datatypes/_examples.py b/aeon/datatypes/_examples.py
index f08c6616..396c262f 100644
--- a/aeon/datatypes/_examples.py
+++ b/aeon/datatypes/_examples.py
@@ -20,7 +20,6 @@ __all__ = [
     "get_examples",
 ]
 
-from aeon.datatypes._alignment import example_dict_Alignment
 from aeon.datatypes._hierarchical import (
     example_dict_Hierarchical,
     example_dict_lossy_Hierarchical,
@@ -49,7 +48,6 @@ from aeon.datatypes._table import (
 
 # pool example_dict-s
 example_dict = dict()
-example_dict.update(example_dict_Alignment)
 example_dict.update(example_dict_Series)
 example_dict.update(example_dict_Panel)
 example_dict.update(example_dict_Hierarchical)
diff --git a/aeon/datatypes/_registry.py b/aeon/datatypes/_registry.py
index 6c984c48..ffbd2767 100644
--- a/aeon/datatypes/_registry.py
+++ b/aeon/datatypes/_registry.py
@@ -37,10 +37,6 @@ mtype_to_scitype(mtype: str) - convenience function that returns scitype for an
 ---
 """
 
-from aeon.datatypes._alignment._registry import (
-    MTYPE_LIST_ALIGNMENT,
-    MTYPE_REGISTER_ALIGNMENT,
-)
 from aeon.datatypes._hierarchical._registry import (
     MTYPE_LIST_HIERARCHICAL,
     MTYPE_REGISTER_HIERARCHICAL,
@@ -63,7 +59,6 @@ MTYPE_REGISTER = []
 MTYPE_REGISTER += MTYPE_REGISTER_SERIES
 MTYPE_REGISTER += MTYPE_REGISTER_PANEL
 MTYPE_REGISTER += MTYPE_REGISTER_HIERARCHICAL
-MTYPE_REGISTER += MTYPE_REGISTER_ALIGNMENT
 MTYPE_REGISTER += MTYPE_REGISTER_TABLE
 MTYPE_REGISTER += MTYPE_REGISTER_PROBA
 
@@ -74,7 +69,7 @@ MTYPE_SOFT_DEPS.update(MTYPE_SOFT_DEPS_HIERARCHICAL)
 
 
 # mtypes to exclude in checking since they are ambiguous and rare
-AMBIGUOUS_MTYPES = ["numpy2D", "alignment_loc"]
+AMBIGUOUS_MTYPES = ["numpy2D"]
 
 # all time series mtypes excluding ambiguous ones
 ALL_TIME_SERIES_MTYPES = (
@@ -88,7 +83,6 @@ __all__ = [
     "MTYPE_LIST_HIERARCHICAL",
     "MTYPE_LIST_PANEL",
     "MTYPE_LIST_SERIES",
-    "MTYPE_LIST_ALIGNMENT",
     "MTYPE_LIST_TABLE",
     "MTYPE_LIST_PROBA",
     "MTYPE_SOFT_DEPS",
@@ -100,7 +94,6 @@ DATATYPE_REGISTER = [
     ("Series", "uni- or multivariate time series"),
     ("Panel", "collection of uni- or multivariate time series"),
     ("Hierarchical", "hierarchical panel of time series with 3 or more levels"),
-    ("Alignment", "series or sequence alignment"),
     ("Table", "data table with primitive column types"),
     ("Proba", "probability distribution or distribution statistics, return types"),
 ]
diff --git a/aeon/distances/__init__.py b/aeon/distances/__init__.py
index a917d870..a28a49a9 100644
--- a/aeon/distances/__init__.py
+++ b/aeon/distances/__init__.py
@@ -56,6 +56,7 @@ __all__ = [
     "cost_matrix",
     "get_cost_matrix_function",
     "get_distance_function",
+    "get_distance_function_names",
     "get_pairwise_distance_function",
     "get_alignment_path_function",
     "shape_dtw_distance",
@@ -85,6 +86,7 @@ from aeon.distances._distance import (
     get_alignment_path_function,
     get_cost_matrix_function,
     get_distance_function,
+    get_distance_function_names,
     get_pairwise_distance_function,
     pairwise_distance,
 )
diff --git a/aeon/distances/_adtw.py b/aeon/distances/_adtw.py
index b5b5234b..b34b497b 100644
--- a/aeon/distances/_adtw.py
+++ b/aeon/distances/_adtw.py
@@ -40,10 +40,9 @@ def adtw_distance(
         The window to use for the bounding matrix. If None, no bounding matrix
         is used. window is a percentage deviation, so if ``window = 0.1`` then
         10% of the series length is the max warping allowed.
-        is used.
     itakura_max_slope : float, default=None
         Maximum slope as a proportion of the number of time points used to create
-        Itakura parallelogram on the bounding matrix. Must be between 0. and 1.
+        Itakura parallelogram on the bounding matrix. Must be between 0.0 and 1.0
     warp_penalty: float, default=1.0
         Penalty for warping. A high value will mean less warping.
 
@@ -59,9 +58,8 @@ def adtw_distance(
 
     References
     ----------
-    .. [1] Matthieu Herrmann, Geoffrey I. Webb: Amercing: An intuitive and
-    effective constraint for dynamic time warping, Pattern Recognition,
-    Volume 137, 2023.
+    .. [1] Matthieu Herrmann, Geoffrey I. Webb: Amercing: An intuitive and effective
+    constraint for dynamic time warping, Pattern Recognition, Volume 137, 2023.
 
     Examples
     --------
@@ -214,6 +212,10 @@ def adtw_pairwise_distance(
     y : np.ndarray or None, default=None
         A single series or a collection of time series of shape ``(m_timepoints,)`` or
         ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``.
+        If None, then the adtw pairwise distance between the instances of X is
+        calculated.
+
+
     window : float or None, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
diff --git a/aeon/distances/_distance.py b/aeon/distances/_distance.py
index 93626321..320d395c 100644
--- a/aeon/distances/_distance.py
+++ b/aeon/distances/_distance.py
@@ -103,7 +103,8 @@ def distance(
     metric : str or Callable
         The distance metric to use.
         A list of valid distance metrics can be found in the documentation for
-        :func:`aeon.distances.get_distance_function`.
+        :func:`aeon.distances.get_distance_function` or by calling  the function
+        :func:`aeon.distances.get_distance_function_names`.
     kwargs : Any
         Arguments for metric. Refer to each metrics documentation for a list of
         possible arguments.
@@ -242,8 +243,9 @@ def pairwise_distance(
        ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``
     metric : str or Callable
         The distance metric to use.
-        A list of valid pairwise distance metrics can be found in the documentation for
-        :func:`aeon.distances.get_pairwise_distance_function`.
+        A list of valid distance metrics can be found in the documentation for
+        :func:`aeon.distances.get_distance_function` or by calling  the function
+        :func:`aeon.distances.get_distance_function_names`.
     kwargs : Any
         Extra arguments for metric. Refer to each metric documentation for a list of
         possible arguments.
@@ -445,9 +447,10 @@ def alignment_path(
         Second time series.
     metric : str
         The distance metric to use.
-        A list of valid alignment path metrics can be found in the documentation for
-        :func:`aeon.distances.get_alignment_path_function`.
-    kwargs : Any
+        A list of valid distance metrics can be found in the documentation for
+        :func:`aeon.distances.get_distance_function` or by calling  the function
+        :func:`aeon.distances.get_distance_function_names`.
+    kwargs : any
         Arguments for metric. Refer to each metrics documentation for a list of
         possible arguments.
 
@@ -579,8 +582,9 @@ def cost_matrix(
         Second time series.
     metric : str or Callable
         The distance metric to use.
-        A list of valid alignment path metrics can be found in the documentation for
-        :func:`aeon.distances.get_cost_matrix_function`.
+        A list of valid distance metrics can be found in the documentation for
+        :func:`aeon.distances.get_distance_function` or by calling  the function
+        :func:`aeon.distances.get_distance_function_names`.
     kwargs : Any
         Arguments for metric. Refer to each metrics documentation for a list of
         possible arguments.
@@ -702,6 +706,32 @@ def cost_matrix(
         raise ValueError("Metric must be one of the supported strings")
 
 
+def get_distance_function_names() -> List[str]:
+    """Get a list of distance function names in aeon.
+
+    All distance function names have two associated functions:
+        name_distance
+        name_pairwise_distance
+    Elastic distances have two additional functions associated with them:
+        name_alignment_path
+        name_cost_matrix
+
+    Returns
+    -------
+    List[str]
+        List of distance function names in aeon.
+
+    Examples
+    --------
+    >>> from aeon.distances import get_distance_function_names
+    >>> names = get_distance_function_names()
+    >>> names[0]
+    'adtw'
+
+    """
+    return sorted(DISTANCES_DICT.keys())
+
+
 def get_distance_function(metric: Union[str, DistanceFunction]) -> DistanceFunction:
     """Get the distance function for a given metric string or callable.
 
@@ -721,6 +751,7 @@ def get_distance_function(metric: Union[str, DistanceFunction]) -> DistanceFunct
     'lcss'          distance.lcss_distance
     'euclidean'     distance.euclidean_distance
     'squared'       distance.squared_distance
+    'manhattan'     manhattan.squared_distance
     =============== ========================================
 
     Parameters
@@ -775,6 +806,7 @@ def get_pairwise_distance_function(
     'lcss'          distance.lcss_pairwise_distance
     'euclidean'     distance.euclidean_pairwise_distance
     'squared'       distance.squared_pairwise_distance
+    'manhattan'     distance.manhattan_pairwise_distance
     =============== ========================================
 
     Parameters
@@ -936,6 +968,11 @@ DISTANCES = [
         "distance": squared_distance,
         "pairwise_distance": squared_pairwise_distance,
     },
+    {
+        "name": "manhattan",
+        "distance": manhattan_distance,
+        "pairwise_distance": manhattan_pairwise_distance,
+    },
     {
         "name": "dtw",
         "distance": dtw_distance,
diff --git a/aeon/distances/_dtw.py b/aeon/distances/_dtw.py
index af1bae1b..cbb58c43 100644
--- a/aeon/distances/_dtw.py
+++ b/aeon/distances/_dtw.py
@@ -252,6 +252,8 @@ def dtw_pairwise_distance(
     y : np.ndarray or None, default=None
         A single series or a collection of time series of shape ``(m_timepoints,)`` or
         ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``.
+        If None, then the dtw pairwise distance between the instances of X is
+        calculated.
     window : float or None, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
diff --git a/aeon/distances/_edr.py b/aeon/distances/_edr.py
index 0a084734..e4013ac4 100644
--- a/aeon/distances/_edr.py
+++ b/aeon/distances/_edr.py
@@ -227,12 +227,14 @@ def edr_pairwise_distance(
 
     Parameters
     ----------
-    X : np.ndarray, of shape (n_instances, n_channels, n_timepoints) or
-            (n_instances, n_timepoints)
-        A collection of time series instances.
-    y : np.ndarray, of shape (m_instances, m_channels, m_timepoints) or
-            (m_instances, m_timepoints) or (m_timepoints,), default=None
-        A collection of time series instances.
+    X : np.ndarray
+        A collection of time series instances  of shape ``(n_instances, n_timepoints)``
+        or ``(n_instances, n_channels, n_timepoints)``.
+    y : np.ndarray or None, default=None
+        A single series or a collection of time series of shape ``(m_timepoints,)`` or
+        ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``.
+        If None, then the edr pairwise distance between the instances of X is
+        calculated.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
@@ -346,10 +348,10 @@ def edr_alignment_path(
 
     Parameters
     ----------
-    x : np.ndarray, of shape (n_channels, n_timepoints) or (n_timepoints,)
-        First time series.
-    y : np.ndarray, of shape (m_channels, m_timepoints) or (m_timepoints,)
-        Second time series.
+    x : np.ndarray
+        First time series, shape ``(n_channels, n_timepoints)`` or ``(n_timepoints,)``.
+    y : np.ndarray
+        Second time series, shape ``(m_channels, m_timepoints)`` or ``(m_timepoints,)``.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
diff --git a/aeon/distances/_erp.py b/aeon/distances/_erp.py
index 6fbc40b0..97e44c8b 100644
--- a/aeon/distances/_erp.py
+++ b/aeon/distances/_erp.py
@@ -266,12 +266,14 @@ def erp_pairwise_distance(
 
     Parameters
     ----------
-    X : np.ndarray, of shape (n_instances, n_channels, n_timepoints) or
-            (n_instances, n_timepoints)
-        A collection of time series instances.
-    y : np.ndarray, of shape (m_instances, m_channels, m_timepoints) or
-            (m_instances, m_timepoints) or (m_timepoints,), default=None
-        A collection of time series instances.
+    X : np.ndarray
+        A collection of time series instances  of shape ``(n_instances, n_timepoints)``
+        or ``(n_instances, n_channels, n_timepoints)``.
+    y : np.ndarray or None, default=None
+        A single series or a collection of time series of shape ``(m_timepoints,)`` or
+        ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``.
+        If None, then the erp pairwise distance between the instances of X is
+        calculated.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
@@ -390,6 +392,7 @@ def erp_alignment_path(
 ) -> Tuple[List[Tuple[int, int]], float]:
     """Compute the ERP alignment path between two time series.
 
+    The optimal value of g is selected from the range [σ/5, σ], where σ is the
     The optimal value of g is selected from the range [σ/5, σ], where σ is the
     standard deviation of the training data. When there is > 1 channel, g should
     be a np.ndarray where the nth value is the standard deviation of the nth
@@ -397,10 +400,10 @@ def erp_alignment_path(
 
     Parameters
     ----------
-    x : np.ndarray, of shape (n_channels, n_timepoints) or (n_timepoints,)
-        First time series.
-    y : np.ndarray, of shape (m_channels, m_timepoints) or (m_timepoints,)
-        Second time series.
+    x : np.ndarray
+        First time series, shape ``(n_channels, n_timepoints)`` or ``(n_timepoints,)``.
+    y : np.ndarray
+        Second time series, shape ``(m_channels, m_timepoints)`` or ``(m_timepoints,)``.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
diff --git a/aeon/distances/_euclidean.py b/aeon/distances/_euclidean.py
index 210b34ff..f75a0fa5 100644
--- a/aeon/distances/_euclidean.py
+++ b/aeon/distances/_euclidean.py
@@ -9,10 +9,11 @@ from aeon.distances._utils import reshape_pairwise_to_multiple
 
 @njit(cache=True, fastmath=True)
 def euclidean_distance(x: np.ndarray, y: np.ndarray) -> float:
-    r"""Compute the euclidean distance between two time series.
+    r"""Compute the Euclidean distance between two time series.
 
     The Euclidean distance between two time series of length m is the square root of
     the squared distance and is defined as:
+
     .. math::
         ed(x, y) = \sqrt{\sum_{i=1}^{n} (x_i - y_i)^2}
 
@@ -63,17 +64,18 @@ def _univariate_euclidean_distance(x: np.ndarray, y: np.ndarray) -> float:
 
 @njit(cache=True, fastmath=True)
 def euclidean_pairwise_distance(X: np.ndarray, y: np.ndarray = None) -> np.ndarray:
-    """Compute the euclidean pairwise distance between a set of time series.
+    """Compute the Euclidean pairwise distance between a set of time series.
 
     Parameters
     ----------
-    X : np.ndarray, of shape (n_instances, n_channels, n_timepoints) or
-            (n_instances, n_timepoints) or (n_timepoints,)
-        A collection of time series instances.
-    y : np.ndarray, of shape (m_instances, m_channels, m_timepoints) or
-            (m_instances, m_timepoints) or (m_timepoints,), default=None
-        A collection of time series instances.
-
+    X : np.ndarray
+        A collection of time series instances  of shape ``(n_instances, n_timepoints)``
+        or ``(n_instances, n_channels, n_timepoints)``.
+    y : np.ndarray or None, default=None
+        A single series or a collection of time series of shape ``(m_timepoints,)`` or
+        ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``.
+        If None, then the Euclidean pairwise distance between the instances of X is
+        calculated.
 
     Returns
     -------
diff --git a/aeon/distances/_lcss.py b/aeon/distances/_lcss.py
index 76d57e19..d6209da0 100644
--- a/aeon/distances/_lcss.py
+++ b/aeon/distances/_lcss.py
@@ -228,12 +228,14 @@ def lcss_pairwise_distance(
 
     Parameters
     ----------
-    X : np.ndarray, of shape (n_instances, n_channels, n_timepoints) or
-            (n_instances, n_timepoints)
-        A collection of time series instances.
-    y : np.ndarray, of shape (m_instances, m_channels, m_timepoints) or
-            (m_instances, m_timepoints) or (m_timepoints,), default=None
-        A collection of time series instances
+    X : np.ndarray
+        A collection of time series instances  of shape ``(n_instances, n_timepoints)``
+        or ``(n_instances, n_channels, n_timepoints)``.
+    y : np.ndarray or None, default=None
+        A single series or a collection of time series of shape ``(m_timepoints,)`` or
+        ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``.
+        If None, then the lcss pairwise distance between the instances of X is
+        calculated.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
@@ -346,10 +348,10 @@ def lcss_alignment_path(
 
     Parameters
     ----------
-    x : np.ndarray, of shape (n_channels, n_timepoints) or (n_timepoints,)
-        First time series.
-    y : np.ndarray, of shape (m_channels, m_timepoints) or (m_timepoints,)
-        Second time series.
+    x : np.ndarray
+        First time series, shape ``(n_channels, n_timepoints)`` or ``(n_timepoints,)``.
+    y : np.ndarray
+        Second time series, shape ``(m_channels, m_timepoints)`` or ``(m_timepoints,)``.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
diff --git a/aeon/distances/_manhattan.py b/aeon/distances/_manhattan.py
index 95261e00..13c06b3b 100644
--- a/aeon/distances/_manhattan.py
+++ b/aeon/distances/_manhattan.py
@@ -11,6 +11,7 @@ def manhattan_distance(x: np.ndarray, y: np.ndarray) -> float:
     r"""Compute the manhattan distance between two time series.
 
     The manhattan distance between two time series is defined as:
+
     .. math::
         manhattan(x, y) = \sum_{i=1}^{n} |x_i - y_i|
 
@@ -23,6 +24,7 @@ def manhattan_distance(x: np.ndarray, y: np.ndarray) -> float:
         Second time series, either univariate, shape ``(n_timepoints,)``, or
         multivariate, shape ``(n_channels, n_timepoints)``.
 
+
     Returns
     -------
     float
@@ -74,12 +76,14 @@ def manhattan_pairwise_distance(X: np.ndarray, y: np.ndarray = None) -> np.ndarr
 
     Parameters
     ----------
-    X : np.ndarray, of shape (n_instances, n_channels, n_timepoints) or
-            (n_instances, n_timepoints) or (n_timepoints,)
-        A collection of time series instances.
-    y : np.ndarray, of shape (m_instances, m_channels, m_timepoints) or
-            (m_instances, m_timepoints) or (m_timepoints,), default=None
-        A collection of time series instances.
+    X : np.ndarray
+        A collection of time series instances  of shape ``(n_instances, n_timepoints)``
+        or ``(n_instances, n_channels, n_timepoints)``.
+    y : np.ndarray or None, default=None
+        A single series or a collection of time series of shape ``(m_timepoints,)`` or
+        ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``.
+        If None, then the manhattan pairwise distance between the instances of X is
+        calculated.
 
 
     Returns
diff --git a/aeon/distances/_msm.py b/aeon/distances/_msm.py
index 8bd5093c..1771e1fc 100644
--- a/aeon/distances/_msm.py
+++ b/aeon/distances/_msm.py
@@ -35,7 +35,7 @@ def msm_distance(
 
     For two series, possibly of unequal length, :math:`\mathbf{x}=\{x_1,x_2,\ldots,
     x_n\}` and :math:`\mathbf{y}=\{y_1,y_2, \ldots,y_m\}` MSM works by iterating over
-    series lengths math:`i = 1 \ldots n` and math:`j = 1 \ldote m` to find the cost
+    series lengths :math:`i = 1 \ldots n` and :math:`j = 1 \ldote m` to find the cost
     matrix $D$ as follows.
 
     .. math::
@@ -59,9 +59,10 @@ def msm_distance(
                       &= c &  if\;\; & y \geq x \geq z \\
                       &= c+min(|x-y|,|x-z|) & & otherwise\\
 
-    If $x$ and $y$ are multivariate, then there are two ways of calculating the MSM
-    distance. The independent approach is to find the distance for each channel
-    independently, then return the sum. The dependent approach adopts the adaptation
+    If :math:`\mathbf{x}` and :math:`\mathbf{y$}` are multivariate, then there are two
+    ways of calculating the MSM distance. The independent approach is to find the
+    distance for each channel independently, then return the sum. The dependent
+    approach adopts the adaptation
     described in [2]_ for computing the pointwise MSM distance over channels.
     MSM satisfies triangular inequality and is a metric.
 
@@ -355,12 +356,14 @@ def msm_pairwise_distance(
 
     Parameters
     ----------
-    X : np.ndarray, of shape (n_instances, n_channels, n_timepoints) or
-            (n_instances, n_timepoints)
-        A collection of time series instances.
-    y : np.ndarray, of shape (m_instances, m_channels, m_timepoints) or
-            (m_instances, m_timepoints) or (m_timepoints,), default=None
-        A collection of time series instances.
+    X : np.ndarray
+        A collection of time series instances  of shape ``(n_instances, n_timepoints)``
+        or ``(n_instances, n_channels, n_timepoints)``.
+    y : np.ndarray or None, default=None
+        A single series or a collection of time series of shape ``(m_timepoints,)`` or
+        ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``.
+        If None, then the msm pairwise distance between the instances of X is
+        calculated.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
@@ -501,10 +504,10 @@ def msm_alignment_path(
 
     Parameters
     ----------
-    x : np.ndarray, of shape (n_channels, n_timepoints) or (n_timepoints,)
-        First time series.
-    y : np.ndarray, of shape (m_channels, m_timepoints) or (m_timepoints,)
-        Second time series.
+    x : np.ndarray
+        First time series, shape ``(n_channels, n_timepoints)`` or ``(n_timepoints,)``.
+    y : np.ndarray
+        Second time series, shape ``(m_channels, m_timepoints)`` or ``(m_timepoints,)``.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
diff --git a/aeon/distances/_squared.py b/aeon/distances/_squared.py
index d7d556c4..f9566c25 100644
--- a/aeon/distances/_squared.py
+++ b/aeon/distances/_squared.py
@@ -75,13 +75,14 @@ def squared_pairwise_distance(X: np.ndarray, y: np.ndarray = None) -> np.ndarray
 
     Parameters
     ----------
-    X : np.ndarray, of shape (n_instances, n_channels, n_timepoints) or
-            (n_instances, n_timepoints) or (n_timepoints,)
-        A collection of time series instances.
-    y : np.ndarray, of shape (m_instances, m_channels, m_timepoints) or
-            (m_instances, m_timepoints) or (m_timepoints,), default=None
-        A collection of time series instances.
-
+    x : np.ndarray
+        First time series, either univariate, shape ``(n_timepoints,)``, or
+        multivariate, shape ``(n_channels, n_timepoints)``.
+    y : np.ndarray
+        Second time series, either univariate, shape ``(n_timepoints,)``, or
+        multivariate, shape ``(n_channels, n_timepoints)``.
+        If None, then the squared pairwise distance between the instances of X is
+        calculated.
 
     Returns
     -------
diff --git a/aeon/distances/_twe.py b/aeon/distances/_twe.py
index 2397d8b7..7e5c4c85 100644
--- a/aeon/distances/_twe.py
+++ b/aeon/distances/_twe.py
@@ -257,12 +257,14 @@ def twe_pairwise_distance(
 
     Parameters
     ----------
-    X : np.ndarray, of shape (n_instances, n_channels, n_timepoints) or
-            (n_instances, n_timepoints)
-        A collection of time series instances.
-    y : np.ndarray, of shape (m_instances, m_channels, m_timepoints) or
-            (m_instances, m_timepoints) or (m_timepoints,), default=None
-        A collection of time series instances.
+    X : np.ndarray
+        A collection of time series instances  of shape ``(n_instances, n_timepoints)``
+        or ``(n_instances, n_channels, n_timepoints)``.
+    y : np.ndarray or None, default=None
+        A single series or a collection of time series of shape ``(m_timepoints,)`` or
+        ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``.
+        If None, then the twe pairwise distance between the instances of X is
+        calculated.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
diff --git a/aeon/distances/_utils.py b/aeon/distances/_utils.py
index 31e5fd94..bae30245 100644
--- a/aeon/distances/_utils.py
+++ b/aeon/distances/_utils.py
@@ -2,6 +2,7 @@ from typing import Tuple
 
 import numpy as np
 from numba import njit
+from sklearn.utils import check_random_state
 
 
 @njit(cache=True, fastmath=True)
@@ -12,12 +13,13 @@ def reshape_pairwise_to_multiple(
 
     Parameters
     ----------
-    x : np.ndarray, of shape (n_instances, n_channels, n_timepoints) or
-            (n_instances, n_timepoints) or (n_timepoints,)
-        A collection of time series instances.
-    y : np.ndarray, of shape (m_instances, m_channels, m_timepoints) or
+    x : np.ndarray
+        One or more time series of shape (n_instances, n_channels,
+        n_timepoints) or
+            (n_instances, n_timepoints) or (n_timepoints,).
+    y : np.ndarray
+        One or more time series of shape (m_instances, m_channels, m_timepoints) or
             (m_instances, m_timepoints) or (m_timepoints,)
-        A collection of time series instances.
 
     Returns
     -------
@@ -59,3 +61,76 @@ def reshape_pairwise_to_multiple(
             _y = y.reshape((y.shape[0], 1, y.shape[1]))
             return _x, _y
         raise ValueError("x and y must be 2D or 3D arrays")
+
+
+def _create_test_distance_numpy(
+    n_instance: int,
+    n_channels: int = None,
+    n_timepoints: int = None,
+    random_state: int = 1,
+):
+    """Create a test numpy distance.
+
+    Parameters
+    ----------
+    n_instance: int
+        Number of instances to create.
+    n_channels: int
+        Number of channels to create.
+    n_timepoints: int, default=None
+        Number of timepoints to create in each channel.
+    random_state: int, default=1
+        Random state to initialise with.
+
+    Returns
+    -------
+    np.ndarray 2D or 3D numpy
+        Numpy array of shape specific. If 1 instance then 2D array returned,
+        if > 1 instance then 3D array returned.
+    """
+    rng = check_random_state(random_state)
+    # Generate data as 3d numpy array
+    if n_timepoints is None and n_channels is None:
+        return rng.normal(scale=0.5, size=(1, n_instance))
+    if n_timepoints is None:
+        return rng.normal(scale=0.5, size=(n_instance, n_channels))
+    return rng.normal(scale=0.5, size=(n_instance, n_channels, n_timepoints))
+
+
+def _make_3d_series(x: np.ndarray) -> np.ndarray:
+    """Check a series being passed into pairwise is 3d.
+
+    Pairwise assumes it has been passed two sets of series, if passed a single
+    series this function reshapes.
+
+    If given a 1d array the time series is reshaped to (m, 1, 1). This is so when
+    looped over x[i] = (1, m).
+
+    If given a 2d array then the time series is reshaped to (d, 1, m). The dimensions
+    are put to the start so the ts can be looped through correctly. When looped over
+    the time series x[i] = (d, m).
+
+    Parameters
+    ----------
+    x: np.ndarray, 2d or 3d
+
+    Returns
+    -------
+    np.ndarray, 3d
+    """
+    num_dims = x.ndim
+    if num_dims == 1:
+        shape = x.shape
+        _x = np.reshape(x, (1, 1, shape[0]))
+    elif num_dims == 2:
+        shape = x.shape
+        _x = np.reshape(x, (shape[0], 1, shape[1]))
+    elif num_dims > 3:
+        raise ValueError(
+            "The matrix provided has more than 3 dimensions. This is not"
+            "supported. Please provide a matrix with less than "
+            "3 dimensions"
+        )
+    else:
+        _x = x
+    return _x
diff --git a/aeon/distances/_wddtw.py b/aeon/distances/_wddtw.py
index 9c7c4215..ba85549f 100644
--- a/aeon/distances/_wddtw.py
+++ b/aeon/distances/_wddtw.py
@@ -180,12 +180,14 @@ def wddtw_pairwise_distance(
 
     Parameters
     ----------
-    X : np.ndarray, of shape (n_instances, n_channels, n_timepoints) or
-            (n_instances, n_timepoints)
-        A collection of time series instances.
-    y : np.ndarray, of shape (m_instances, m_channels, m_timepoints) or
-            (m_instances, m_timepoints) or (m_timepoints,), default=None
-        A collection of time series instances.
+    X : np.ndarray
+        A collection of time series instances  of shape ``(n_instances, n_timepoints)``
+        or ``(n_instances, n_channels, n_timepoints)``.
+    y : np.ndarray or None, default=None
+        A single series or a collection of time series of shape ``(m_timepoints,)`` or
+        ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``.
+        If None, then the wddtw pairwise distance between the instances of X is
+        calculated.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
@@ -311,10 +313,10 @@ def wddtw_alignment_path(
 
     Parameters
     ----------
-    x : np.ndarray, of shape (n_channels, n_timepoints) or (n_timepoints,)
-        First time series.
-    y : np.ndarray, of shape (m_channels, m_timepoints) or (m_timepoints,)
-        Second time series.
+    x : np.ndarray
+        First time series, shape ``(n_channels, n_timepoints)`` or ``(n_timepoints,)``.
+    y : np.ndarray
+        Second time series, shape ``(m_channels, m_timepoints)`` or ``(m_timepoints,)``.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
diff --git a/aeon/distances/_wdtw.py b/aeon/distances/_wdtw.py
index 93d81b71..4ad0a5ad 100644
--- a/aeon/distances/_wdtw.py
+++ b/aeon/distances/_wdtw.py
@@ -47,12 +47,14 @@ def wdtw_distance(
 
     Parameters
     ----------
-    x : np.ndarray
-        First time series, either univariate, shape ``(n_timepoints,)``, or
-        multivariate, shape ``(n_channels, n_timepoints)``.
-    y : np.ndarray
-        Second time series, either univariate, shape ``(n_timepoints,)``, or
-        multivariate, shape ``(n_channels, n_timepoints)``.
+    X : np.ndarray
+        A collection of time series instances  of shape ``(n_instances, n_timepoints)``
+        or ``(n_instances, n_channels, n_timepoints)``.
+    y : np.ndarray or None, default=None
+        A single series or a collection of time series of shape ``(m_timepoints,)`` or
+        ``(m_instances, m_timepoints)`` or ``(m_instances, m_channels, m_timepoints)``.
+        If None, then the wdtw pairwise distance between the instances of X is
+        calculated.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
@@ -360,10 +362,10 @@ def wdtw_alignment_path(
 
     Parameters
     ----------
-    x : np.ndarray, of shape (n_channels, n_timepoints) or (n_timepoints,)
-        First time series.
-    y : np.ndarray, of shape (m_channels, m_timepoints) or (m_timepoints,)
-        Second time series.
+    x : np.ndarray
+        First time series, shape ``(n_channels, n_timepoints)`` or ``(n_timepoints,)``.
+    y : np.ndarray
+        Second time series, shape ``(m_channels, m_timepoints)`` or ``(m_timepoints,)``.
     window : float, default=None
         The window to use for the bounding matrix. If None, no bounding matrix
         is used.
diff --git a/aeon/distances/mpdist.py b/aeon/distances/mpdist.py
index ed4dea79..40d685a2 100644
--- a/aeon/distances/mpdist.py
+++ b/aeon/distances/mpdist.py
@@ -2,7 +2,7 @@
 import numpy as np
 
 
-def sliding_dot_products(q, t, q_len, t_len):
+def _sliding_dot_products(q, t, q_len, t_len):
     """
     Compute the sliding dot products between a query and a time series.
 
@@ -42,7 +42,9 @@ def sliding_dot_products(q, t, q_len, t_len):
     return dot_prod
 
 
-def calculate_distance_profile(dot_prod, q_mean, q_std, t_mean, t_std, q_len, n_t_subs):
+def _calculate_distance_profile(
+    dot_prod, q_mean, q_std, t_mean, t_std, q_len, n_t_subs
+):
     """
     Calculate the distance profile for the given query.
 
@@ -85,7 +87,7 @@ def calculate_distance_profile(dot_prod, q_mean, q_std, t_mean, t_std, q_len, n_
     return d
 
 
-def stomp_ab(ts1, ts2, m):
+def _stomp_ab(ts1, ts2, m):
     """
     STOMP implementation for AB similarity join.
 
@@ -109,9 +111,6 @@ def stomp_ab(ts1, ts2, m):
     len1 = len(ts1)
     len2 = len(ts2)
 
-    ts1 = ts1.flatten()
-    ts2 = ts2.flatten()
-
     # Number of subsequences
     n_ts1_subs = len1 - m + 1
     n_ts2_subs = len2 - m + 1
@@ -125,7 +124,7 @@ def stomp_ab(ts1, ts2, m):
 
     # Compute the dot products between the first ts2 subsequence and every
     # ts1 subsequence
-    dot_prod = sliding_dot_products(ts2[0:m], ts1, m, len1)
+    dot_prod = _sliding_dot_products(ts2[0:m], ts1, m, len1)
     first_dot_prod = np.copy(dot_prod)
 
     # Initialization
@@ -133,8 +132,8 @@ def stomp_ab(ts1, ts2, m):
     ip = np.zeros(n_ts1_subs)  # index profile
 
     # Compute the distance profile for the first ts1 subsequence
-    dot_prod = sliding_dot_products(ts1[0:m], ts2, m, len2)
-    dp = calculate_distance_profile(
+    dot_prod = _sliding_dot_products(ts1[0:m], ts2, m, len2)
+    dp = _calculate_distance_profile(
         dot_prod, ts1_mean[0], ts1_std[0], ts2_mean, ts2_std, m, n_ts2_subs
     )
 
@@ -151,7 +150,7 @@ def stomp_ab(ts1, ts2, m):
             )  # compute the next dot products
             # using the previous ones
         dot_prod[0] = first_dot_prod[i]
-        dp = calculate_distance_profile(
+        dp = _calculate_distance_profile(
             dot_prod, ts1_mean[i], ts1_std[i], ts2_mean, ts2_std, m, n_ts2_subs
         )
         mp[i] = np.amin(dp)
@@ -178,6 +177,13 @@ def mpdist(ts1, ts2, m=0):
         mpdist: float
             Distance between the two time series.
     """
+    if ts1.ndim == 2 and ts1.shape[0] > 1:
+        raise ValueError("ts1 must be a 1D array or shape (1,n)")
+    if ts2.ndim == 2 and ts2.shape[0] > 1:
+        raise ValueError("ts2 must be a 1D array or shape (1,n)")
+    ts1 = ts1.squeeze()
+    ts2 = ts2.squeeze()
+
     len1 = len(ts1)
     len2 = len(ts2)
 
@@ -186,10 +192,10 @@ def mpdist(ts1, ts2, m=0):
             m = len1 / 4
         else:
             m = len2 / 4
-
+    m = int(m)
     threshold = 0.05
-    mp_ab, ip_ab = stomp_ab(ts1, ts2, m)  # compute the AB matrix profile
-    mp_ba, ip_ba = stomp_ab(ts2, ts1, m)  # compute the BA matrix profile
+    mp_ab, ip_ab = _stomp_ab(ts1, ts2, m)  # compute the AB matrix profile
+    mp_ba, ip_ba = _stomp_ab(ts2, ts1, m)  # compute the BA matrix profile
 
     join_mp = np.concatenate([mp_ab, mp_ba])
 
diff --git a/docs/api_reference/distances.rst b/docs/api_reference/distances.rst
index 4e24ed99..418fc679 100644
--- a/docs/api_reference/distances.rst
+++ b/docs/api_reference/distances.rst
@@ -211,5 +211,6 @@ General methods to recover distance functions
 
     get_distance_function
     get_pairwise_distance_function
+    get_distance_function_names
     get_cost_matrix_function
     get_alignment_path_function


Test patch:
diff --git a/.github/workflows/periodic_tests.yml b/.github/workflows/periodic_tests.yml
index acc9eb63..d3bba0bc 100644
--- a/.github/workflows/periodic_tests.yml
+++ b/.github/workflows/periodic_tests.yml
@@ -17,7 +17,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
@@ -31,7 +31,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
@@ -49,7 +49,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
@@ -70,7 +70,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
@@ -96,7 +96,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
@@ -125,7 +125,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
 
@@ -148,7 +148,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
diff --git a/.github/workflows/pr_pytest.yml b/.github/workflows/pr_pytest.yml
index 782af04a..fe12a0d4 100644
--- a/.github/workflows/pr_pytest.yml
+++ b/.github/workflows/pr_pytest.yml
@@ -23,7 +23,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
@@ -50,7 +50,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
@@ -80,7 +80,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
 
@@ -108,7 +108,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.10"
 
diff --git a/aeon/clustering/averaging/tests/test_dba.py b/aeon/clustering/averaging/tests/test_dba.py
index f0430aad..f9fd7d83 100644
--- a/aeon/clustering/averaging/tests/test_dba.py
+++ b/aeon/clustering/averaging/tests/test_dba.py
@@ -2,7 +2,7 @@
 import numpy as np
 
 from aeon.clustering.averaging import elastic_barycenter_average
-from aeon.distances.tests.test_utils import create_test_distance_numpy
+from aeon.distances.tests.test_utils import _create_test_distance_numpy
 
 expected_dba = np.array(
     [
@@ -132,7 +132,7 @@ expected_dba = np.array(
 
 def test_dba():
     """Test dba functionality."""
-    X_train = create_test_distance_numpy(10, 10, 10)
+    X_train = _create_test_distance_numpy(10, 10, 10)
 
     average_ts = elastic_barycenter_average(X_train)
 
@@ -155,7 +155,7 @@ def test_elastic_dba_variations():
         "adtw",
     ]
     for dist in distances:
-        X_train = create_test_distance_numpy(4, 2, 10)
+        X_train = _create_test_distance_numpy(4, 2, 10)
 
         average_ts = elastic_barycenter_average(
             X_train, distance=dist, window=0.2, independent=False
diff --git a/aeon/datatypes/tests/test_check.py b/aeon/datatypes/tests/test_check.py
index 0e3904eb..57cf9bb0 100644
--- a/aeon/datatypes/tests/test_check.py
+++ b/aeon/datatypes/tests/test_check.py
@@ -12,11 +12,6 @@ from aeon.datatypes._registry import SCITYPE_LIST, scitype_to_mtype
 
 SCITYPES = SCITYPE_LIST
 
-# scitypes where mtype inference is not unique
-# alignment is excluded since mtypes can be ambiguous
-#   (indices could be both loc or iloc when integers)
-SCITYPES_AMBIGUOUS_MTYPE = ["Alignment"]
-
 
 def _generate_scitype_mtype_combinations():
     """Return scitype/mtype tuples for pytest_generate_tests.
@@ -191,10 +186,6 @@ def test_check_negative(scitype, mtype):
     AssertionError if a examples are correctly identified as incompatible
     error if check itself raises an error
     """
-    # if the scitype is ambiguous, we can't assume that other mtypes are negative
-    if scitype in SCITYPES_AMBIGUOUS_MTYPE:
-        return None
-
     mtypes = scitype_to_mtype(scitype)
     fixtures = dict()
 
@@ -238,10 +229,6 @@ def test_mtype_infer(scitype, mtype, fixture_index):
     AssertionError if mtype of examples is not correctly identified
     error if check itself raises an error
     """
-    # if mtypes are ambiguous, then this test should be skipped
-    if scitype in SCITYPES_AMBIGUOUS_MTYPE:
-        return None
-
     # retrieve fixture for checking
     fixture = get_examples(mtype=mtype, as_scitype=scitype).get(fixture_index)
 
@@ -257,7 +244,7 @@ def test_mtype_infer(scitype, mtype, fixture_index):
 
 # exclude these scitypes in inference of scitype test
 #  would lead to ambiguous results
-SKIP_SCITYPES = ["Alignment", "Table", "Proba"]
+SKIP_SCITYPES = ["Table", "Proba"]
 SCITYPES_FOR_INFER_TEST = list(set(SCITYPE_LIST).difference(SKIP_SCITYPES))
 
 
diff --git a/aeon/datatypes/tests/test_convert.py b/aeon/datatypes/tests/test_convert.py
index 37fd676d..36be4259 100644
--- a/aeon/datatypes/tests/test_convert.py
+++ b/aeon/datatypes/tests/test_convert.py
@@ -9,10 +9,6 @@ from aeon.utils._testing.deep_equals import deep_equals
 
 SCITYPES = [sci[0] for sci in DATATYPE_REGISTER]
 
-# scitypes which have no conversions defined
-# should be listed here to avoid false positive test errors
-SCITYPES_NO_CONVERSIONS = ["Alignment"]
-
 
 def _generate_fixture_tuples():
     """Return fixture tuples for pytest_generate_tests."""
@@ -20,12 +16,7 @@ def _generate_fixture_tuples():
     fixture_tuples = []
 
     for scitype in SCITYPES:
-        # if we know there are no conversions defined, skip this scitype
-        if scitype in SCITYPES_NO_CONVERSIONS:
-            continue
-
         conv_mat = _conversions_defined(scitype)
-
         mtypes = scitype_to_mtype(scitype, softdeps="exclude")
 
         if len(mtypes) == 0:
diff --git a/aeon/distances/tests/test_alignment_path.py b/aeon/distances/tests/test_alignment_path.py
index 5b2b38c5..bf37cb66 100644
--- a/aeon/distances/tests/test_alignment_path.py
+++ b/aeon/distances/tests/test_alignment_path.py
@@ -4,7 +4,7 @@ from numpy.testing import assert_almost_equal
 
 from aeon.distances import alignment_path as compute_alignment_path
 from aeon.distances._distance import DISTANCES
-from aeon.distances.tests.test_utils import create_test_distance_numpy
+from aeon.distances.tests.test_utils import _create_test_distance_numpy
 
 
 def _validate_cost_matrix_result(
@@ -41,8 +41,8 @@ def test_cost_matrix(dist):
         )
 
     _validate_cost_matrix_result(
-        create_test_distance_numpy(10),
-        create_test_distance_numpy(10, random_state=2),
+        _create_test_distance_numpy(10),
+        _create_test_distance_numpy(10, random_state=2),
         dist["name"],
         dist["distance"],
         dist["alignment_path"],
@@ -50,8 +50,8 @@ def test_cost_matrix(dist):
 
     # Test multivariate
     _validate_cost_matrix_result(
-        create_test_distance_numpy(10, 10),
-        create_test_distance_numpy(10, 10, random_state=2),
+        _create_test_distance_numpy(10, 10),
+        _create_test_distance_numpy(10, 10, random_state=2),
         dist["name"],
         dist["distance"],
         dist["alignment_path"],
@@ -59,16 +59,16 @@ def test_cost_matrix(dist):
 
     # Test unequal length
     _validate_cost_matrix_result(
-        create_test_distance_numpy(5),
-        create_test_distance_numpy(10, random_state=2),
+        _create_test_distance_numpy(5),
+        _create_test_distance_numpy(10, random_state=2),
         dist["name"],
         dist["distance"],
         dist["alignment_path"],
     )
 
     _validate_cost_matrix_result(
-        create_test_distance_numpy(10, 5),
-        create_test_distance_numpy(10, 10, random_state=2),
+        _create_test_distance_numpy(10, 5),
+        _create_test_distance_numpy(10, 10, random_state=2),
         dist["name"],
         dist["distance"],
         dist["alignment_path"],
diff --git a/aeon/distances/tests/test_cost_matrix.py b/aeon/distances/tests/test_cost_matrix.py
index 0fbd56c6..f7e5e7a9 100644
--- a/aeon/distances/tests/test_cost_matrix.py
+++ b/aeon/distances/tests/test_cost_matrix.py
@@ -4,7 +4,7 @@ from numpy.testing import assert_almost_equal
 
 from aeon.distances import cost_matrix as compute_cost_matrix
 from aeon.distances._distance import DISTANCES
-from aeon.distances.tests.test_utils import create_test_distance_numpy
+from aeon.distances.tests.test_utils import _create_test_distance_numpy
 
 
 def _validate_cost_matrix_result(
@@ -61,8 +61,8 @@ def test_cost_matrix(dist):
         )
 
     _validate_cost_matrix_result(
-        create_test_distance_numpy(10),
-        create_test_distance_numpy(10, random_state=2),
+        _create_test_distance_numpy(10),
+        _create_test_distance_numpy(10, random_state=2),
         dist["name"],
         dist["distance"],
         dist["cost_matrix"],
@@ -70,8 +70,8 @@ def test_cost_matrix(dist):
 
     # Test multivariate
     _validate_cost_matrix_result(
-        create_test_distance_numpy(10, 10),
-        create_test_distance_numpy(10, 10, random_state=2),
+        _create_test_distance_numpy(10, 10),
+        _create_test_distance_numpy(10, 10, random_state=2),
         dist["name"],
         dist["distance"],
         dist["cost_matrix"],
@@ -79,16 +79,16 @@ def test_cost_matrix(dist):
 
     # Test unequal length
     _validate_cost_matrix_result(
-        create_test_distance_numpy(5),
-        create_test_distance_numpy(10, random_state=2),
+        _create_test_distance_numpy(5),
+        _create_test_distance_numpy(10, random_state=2),
         dist["name"],
         dist["distance"],
         dist["cost_matrix"],
     )
 
     _validate_cost_matrix_result(
-        create_test_distance_numpy(10, 5),
-        create_test_distance_numpy(10, 10, random_state=2),
+        _create_test_distance_numpy(10, 5),
+        _create_test_distance_numpy(10, 10, random_state=2),
         dist["name"],
         dist["distance"],
         dist["cost_matrix"],
diff --git a/aeon/distances/tests/test_distances.py b/aeon/distances/tests/test_distances.py
index 673deb16..13a5282b 100644
--- a/aeon/distances/tests/test_distances.py
+++ b/aeon/distances/tests/test_distances.py
@@ -2,10 +2,17 @@ import numpy as np
 import pytest
 from numpy.testing import assert_almost_equal
 
+from aeon.distances import alignment_path, cost_matrix
+from aeon.distances import distance
 from aeon.distances import distance as compute_distance
-from aeon.distances._distance import DISTANCES
+from aeon.distances import get_distance_function_names, pairwise_distance
+from aeon.distances._distance import (
+    DISTANCES,
+    _custom_func_pairwise,
+    _resolve_key_from_distance,
+)
 from aeon.distances.tests.test_expected_results import _expected_distance_results
-from aeon.distances.tests.test_utils import create_test_distance_numpy
+from aeon.distances.tests.test_utils import _create_test_distance_numpy
 
 
 def _validate_distance_result(x, y, name, distance, expected_result=10):
@@ -36,8 +43,8 @@ def test_distances(dist):
         )
 
     _validate_distance_result(
-        create_test_distance_numpy(10),
-        create_test_distance_numpy(10, random_state=2),
+        _create_test_distance_numpy(10),
+        _create_test_distance_numpy(10, random_state=2),
         dist["name"],
         dist["distance"],
         _expected_distance_results[dist["name"]][1],
@@ -45,8 +52,8 @@ def test_distances(dist):
 
     # Test multivariate
     _validate_distance_result(
-        create_test_distance_numpy(10, 10),
-        create_test_distance_numpy(10, 10, random_state=2),
+        _create_test_distance_numpy(10, 10),
+        _create_test_distance_numpy(10, 10, random_state=2),
         dist["name"],
         dist["distance"],
         _expected_distance_results[dist["name"]][2],
@@ -54,17 +61,54 @@ def test_distances(dist):
 
     # Test unequal length
     _validate_distance_result(
-        create_test_distance_numpy(5),
-        create_test_distance_numpy(10, random_state=2),
+        _create_test_distance_numpy(5),
+        _create_test_distance_numpy(10, random_state=2),
         dist["name"],
         dist["distance"],
         _expected_distance_results[dist["name"]][3],
     )
 
     _validate_distance_result(
-        create_test_distance_numpy(10, 5),
-        create_test_distance_numpy(10, 10, random_state=2),
+        _create_test_distance_numpy(10, 5),
+        _create_test_distance_numpy(10, 10, random_state=2),
         dist["name"],
         dist["distance"],
         _expected_distance_results[dist["name"]][4],
     )
+
+
+def test_get_distance_function_names():
+    assert get_distance_function_names() == sorted([dist["name"] for dist in DISTANCES])
+
+
+def test__resolve_key_from_distance():
+    with pytest.raises(ValueError, match="Unknown metric"):
+        _resolve_key_from_distance(metric="FOO", key="cost_matrix")
+    with pytest.raises(ValueError):
+        _resolve_key_from_distance(metric="dtw", key="FOO")
+
+    def foo(x, y):
+        return 0
+
+    assert callable(_resolve_key_from_distance(foo, key="FOO"))
+
+
+def test_incorrect_inputs():
+    x = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
+    y = np.array([[11, 12, 13, 14, 15, 16, 17, 18, 19, 20]])
+    with pytest.raises(
+        ValueError, match="Metric must be one of the supported strings or a " "callable"
+    ):
+        distance(x, y, metric="FOO")
+    with pytest.raises(
+        ValueError, match="Metric must be one of the supported strings or a " "callable"
+    ):
+        pairwise_distance(x, y, metric="FOO")
+    with pytest.raises(ValueError, match="Metric must be one of the supported strings"):
+        alignment_path(x, y, metric="FOO")
+    with pytest.raises(ValueError, match="Metric must be one of the supported strings"):
+        cost_matrix(x, y, metric="FOO")
+
+    x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+    with pytest.raises(ValueError, match="x and y must be 2D or 3D arrays"):
+        _custom_func_pairwise(x)
diff --git a/aeon/distances/tests/test_expected_results.py b/aeon/distances/tests/test_expected_results.py
index bba6b6b4..f3fc856b 100644
--- a/aeon/distances/tests/test_expected_results.py
+++ b/aeon/distances/tests/test_expected_results.py
@@ -83,6 +83,13 @@ _expected_distance_results = {
         9.360373075383167,
         49.86527164702194,
     ],
+    "manhattan": [
+        5.0,
+        7.399565602170839,
+        56.41541036195164,
+        4.788349892989078,
+        28.544576071968976,
+    ],
 }
 
 _expected_distance_results_params = {
diff --git a/aeon/distances/tests/test_mpdist.py b/aeon/distances/tests/test_mpdist.py
new file mode 100644
index 00000000..37d27776
--- /dev/null
+++ b/aeon/distances/tests/test_mpdist.py
@@ -0,0 +1,19 @@
+"""Test MPDist function."""
+
+import numpy as np
+import pytest
+
+from aeon.distances.mpdist import mpdist
+
+
+def test_mpdist():
+    """Minimal test for MPDist prior to redesign."""
+    x = np.random.rand(1, 10)
+    y = np.random.rand(2, 10)
+    with pytest.raises(ValueError, match="ts1 must be a 1D array"):
+        mpdist(y, y)
+    with pytest.raises(ValueError, match="ts2 must be a 1D array"):
+        mpdist(x, y)
+    y = np.random.rand(1, 10)
+    d = mpdist(x, y)
+    assert d >= 0
diff --git a/aeon/distances/tests/test_numba_distance_parameters.py b/aeon/distances/tests/test_numba_distance_parameters.py
index dda159ee..a145702c 100644
--- a/aeon/distances/tests/test_numba_distance_parameters.py
+++ b/aeon/distances/tests/test_numba_distance_parameters.py
@@ -7,17 +7,17 @@ import pytest
 from aeon.distances import distance
 from aeon.distances._distance import DISTANCES
 from aeon.distances.tests.test_expected_results import _expected_distance_results_params
-from aeon.distances.tests.test_utils import create_test_distance_numpy
+from aeon.distances.tests.test_utils import _create_test_distance_numpy
 
 
 def _test_distance_params(
     param_list: List[Dict], distance_func: Callable, distance_str: str
 ):
-    x_univ = create_test_distance_numpy(10, 1).reshape((1, 10))
-    y_univ = create_test_distance_numpy(10, 1, random_state=2).reshape((1, 10))
+    x_univ = _create_test_distance_numpy(10, 1).reshape((1, 10))
+    y_univ = _create_test_distance_numpy(10, 1, random_state=2).reshape((1, 10))
 
-    x_multi = create_test_distance_numpy(10, 10)
-    y_multi = create_test_distance_numpy(10, 10, random_state=2)
+    x_multi = _create_test_distance_numpy(10, 10)
+    y_multi = _create_test_distance_numpy(10, 10, random_state=2)
 
     test_ts = [[x_univ, y_univ], [x_multi, y_multi]]
     results_to_fill = []
diff --git a/aeon/distances/tests/test_pairwise.py b/aeon/distances/tests/test_pairwise.py
index 66d54d32..c01ae3d2 100644
--- a/aeon/distances/tests/test_pairwise.py
+++ b/aeon/distances/tests/test_pairwise.py
@@ -4,7 +4,7 @@ from numpy.testing import assert_almost_equal
 
 from aeon.distances import pairwise_distance as compute_pairwise_distance
 from aeon.distances._distance import DISTANCES
-from aeon.distances.tests.test_utils import _make_3d_series, create_test_distance_numpy
+from aeon.distances.tests.test_utils import _create_test_distance_numpy, _make_3d_series
 
 
 def _validate_pairwise_result(
@@ -107,21 +107,21 @@ def _validate_single_to_multiple_result(
 def test_pairwise_distance(dist):
     """Test pairwise distance function."""
     _validate_pairwise_result(
-        create_test_distance_numpy(5, 5),
+        _create_test_distance_numpy(5, 5),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
     )
 
     _validate_pairwise_result(
-        create_test_distance_numpy(5, 1, 5),
+        _create_test_distance_numpy(5, 1, 5),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
     )
 
     _validate_pairwise_result(
-        create_test_distance_numpy(5, 5, 5),
+        _create_test_distance_numpy(5, 5, 5),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
@@ -142,8 +142,8 @@ def test_multiple_to_multiple_distances(dist):
         )
 
     _validate_multiple_to_multiple_result(
-        create_test_distance_numpy(5),
-        create_test_distance_numpy(5, random_state=2),
+        _create_test_distance_numpy(5),
+        _create_test_distance_numpy(5, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
@@ -151,8 +151,8 @@ def test_multiple_to_multiple_distances(dist):
 
     # Multivariate tests
     _validate_multiple_to_multiple_result(
-        create_test_distance_numpy(5, 5),
-        create_test_distance_numpy(5, 5, random_state=2),
+        _create_test_distance_numpy(5, 5),
+        _create_test_distance_numpy(5, 5, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
@@ -160,16 +160,16 @@ def test_multiple_to_multiple_distances(dist):
 
     # Dataset tests
     _validate_multiple_to_multiple_result(
-        create_test_distance_numpy(5, 1, 5),
-        create_test_distance_numpy(5, 1, 5, random_state=2),
+        _create_test_distance_numpy(5, 1, 5),
+        _create_test_distance_numpy(5, 1, 5, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
     )
 
     _validate_multiple_to_multiple_result(
-        create_test_distance_numpy(5, 5, 5),
-        create_test_distance_numpy(5, 5, 5, random_state=2),
+        _create_test_distance_numpy(5, 5, 5),
+        _create_test_distance_numpy(5, 5, 5, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
@@ -177,24 +177,24 @@ def test_multiple_to_multiple_distances(dist):
 
     # Unequal length tests
     _validate_multiple_to_multiple_result(
-        create_test_distance_numpy(5),
-        create_test_distance_numpy(3, random_state=2),
+        _create_test_distance_numpy(5),
+        _create_test_distance_numpy(3, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
     )
 
     _validate_multiple_to_multiple_result(
-        create_test_distance_numpy(5, 5),
-        create_test_distance_numpy(5, 3, random_state=2),
+        _create_test_distance_numpy(5, 5),
+        _create_test_distance_numpy(5, 3, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
     )
 
     _validate_multiple_to_multiple_result(
-        create_test_distance_numpy(5, 5, 5),
-        create_test_distance_numpy(5, 5, 3, random_state=2),
+        _create_test_distance_numpy(5, 5, 5),
+        _create_test_distance_numpy(5, 5, 3, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
@@ -218,16 +218,16 @@ def test_new_single_to_multiple_distances(dist):
         )
 
     _validate_single_to_multiple_result(
-        create_test_distance_numpy(5),
-        create_test_distance_numpy(3, 1, 5, random_state=2),
+        _create_test_distance_numpy(5),
+        _create_test_distance_numpy(3, 1, 5, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
     )
 
     _validate_single_to_multiple_result(
-        create_test_distance_numpy(3, 1, 5)[0],
-        create_test_distance_numpy(5, 1, 5, random_state=2),
+        _create_test_distance_numpy(3, 1, 5)[0],
+        _create_test_distance_numpy(5, 1, 5, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
@@ -235,8 +235,8 @@ def test_new_single_to_multiple_distances(dist):
 
     # Multivariate tests
     _validate_single_to_multiple_result(
-        create_test_distance_numpy(5, 5),
-        create_test_distance_numpy(5, 5, 5, random_state=2),
+        _create_test_distance_numpy(5, 5),
+        _create_test_distance_numpy(5, 5, 5, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
@@ -244,32 +244,32 @@ def test_new_single_to_multiple_distances(dist):
 
     # Unequal length tests
     _validate_single_to_multiple_result(
-        create_test_distance_numpy(3),
-        create_test_distance_numpy(3, 1, 5, random_state=2),
+        _create_test_distance_numpy(3),
+        _create_test_distance_numpy(3, 1, 5, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
     )
 
     _validate_single_to_multiple_result(
-        create_test_distance_numpy(5),
-        create_test_distance_numpy(3, 1, 3, random_state=2),
+        _create_test_distance_numpy(5),
+        _create_test_distance_numpy(3, 1, 3, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
     )
 
     _validate_single_to_multiple_result(
-        create_test_distance_numpy(5, 3),
-        create_test_distance_numpy(5, 5, 5, random_state=2),
+        _create_test_distance_numpy(5, 3),
+        _create_test_distance_numpy(5, 5, 5, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
     )
 
     _validate_single_to_multiple_result(
-        create_test_distance_numpy(5, 5),
-        create_test_distance_numpy(5, 5, 3, random_state=2),
+        _create_test_distance_numpy(5, 5),
+        _create_test_distance_numpy(5, 5, 3, random_state=2),
         dist["name"],
         dist["distance"],
         dist["pairwise_distance"],
diff --git a/aeon/distances/tests/test_utils.py b/aeon/distances/tests/test_utils.py
index 3bb2dda6..a2e7af7c 100644
--- a/aeon/distances/tests/test_utils.py
+++ b/aeon/distances/tests/test_utils.py
@@ -1,91 +1,49 @@
-import time
-from typing import Callable
-
+"""Tests for distance utility function."""
 import numpy as np
-from sklearn.utils.validation import check_random_state
-
-
-def create_test_distance_numpy(
-    n_instance: int,
-    n_channels: int = None,
-    n_timepoints: int = None,
-    random_state: int = 1,
-):
-    """Create a test numpy distance.
-
-    Parameters
-    ----------
-    n_instance: int
-        Number of instances to create.
-    n_channels: int
-        Number of channels to create.
-    n_timepoints: int, default=None
-        Number of timepoints to create in each channel.
-    random_state: int, default=1
-        Random state to initialise with.
-
-    Returns
-    -------
-    np.ndarray 2D or 3D numpy
-        Numpy array of shape specific. If 1 instance then 2D array returned,
-        if > 1 instance then 3D array returned.
-    """
-    rng = check_random_state(random_state)
-    # Generate data as 3d numpy array
-    if n_timepoints is None and n_channels is None:
-        return rng.normal(scale=0.5, size=(1, n_instance))
-    if n_timepoints is None:
-        return rng.normal(scale=0.5, size=(n_instance, n_channels))
-    return rng.normal(scale=0.5, size=(n_instance, n_channels, n_timepoints))
-
-
-def _time_distance(callable: Callable, average: int = 30, **kwargs):
-    for _ in range(3):
-        callable(**kwargs)
-
-    total = 0
-    for _ in range(average):
-        start = time.time()
-        callable(**kwargs)
-        total += time.time() - start
-
-    return total / average
-
-
-def _make_3d_series(x: np.ndarray) -> np.ndarray:
-    """Check a series being passed into pairwise is 3d.
-
-    Pairwise assumes it has been passed two sets of series, if passed a single
-    series this function reshapes.
-
-    If given a 1d array the time series is reshaped to (m, 1, 1). This is so when
-    looped over x[i] = (1, m).
-
-    If given a 2d array then the time series is reshaped to (d, 1, m). The dimensions
-    are put to the start so the ts can be looped through correctly. When looped over
-    the time series x[i] = (d, m).
-
-    Parameters
-    ----------
-    x: np.ndarray, 2d or 3d
-
-    Returns
-    -------
-    np.ndarray, 3d
-    """
-    num_dims = x.ndim
-    if num_dims == 1:
-        shape = x.shape
-        _x = np.reshape(x, (1, 1, shape[0]))
-    elif num_dims == 2:
-        shape = x.shape
-        _x = np.reshape(x, (shape[0], 1, shape[1]))
-    elif num_dims > 3:
-        raise ValueError(
-            "The matrix provided has more than 3 dimensions. This is not"
-            "supported. Please provide a matrix with less than "
-            "3 dimensions"
-        )
-    else:
-        _x = x
-    return _x
+import pytest
+
+from aeon.distances._utils import (
+    _create_test_distance_numpy,
+    _make_3d_series,
+    reshape_pairwise_to_multiple,
+)
+
+
+def test_create_test_distance_numpy():
+    """Test function to create distance test instance."""
+    x = _create_test_distance_numpy(10)
+    assert isinstance(x, np.ndarray)
+    assert x.shape == (1, 10)
+    x = _create_test_distance_numpy(10, n_channels=4)
+    assert isinstance(x, np.ndarray)
+    assert x.shape == (10, 4)
+    x = _create_test_distance_numpy(10, n_channels=4, n_timepoints=20)
+    assert isinstance(x, np.ndarray)
+    assert x.shape == (10, 4, 20)
+
+
+def test_incorrect_input():
+    """Test util function incorrect input."""
+    x = np.random.rand(10, 2, 2, 10)
+    y = np.random.rand(10, 2, 10)
+    with pytest.raises(
+        ValueError, match="The matrix provided has more than 3 " "dimensions"
+    ):
+        _make_3d_series(x)
+    with pytest.raises(ValueError, match="x and y must be 1D, 2D, or 3D arrays"):
+        reshape_pairwise_to_multiple(x, x)
+    with pytest.raises(ValueError, match="x and y must be 2D or 3D arrays"):
+        reshape_pairwise_to_multiple(x, y)
+
+
+def test_reshape_pairwise_to_multiple():
+    x = np.random.rand(5, 2, 10)
+    y = np.random.rand(5, 2, 10)
+    x2, y2 = reshape_pairwise_to_multiple(x, y)
+    assert x2.shape == y2.shape == (5, 2, 10)
+    x = np.random.rand(5, 10)
+    y = np.random.rand(5, 10)
+    x2, y2 = reshape_pairwise_to_multiple(x, y)
+    assert x2.shape == y2.shape == (5, 1, 10)
+    y = np.random.rand(5)
+    assert x2.shape == y2.shape == (5, 1, 10)