Repository: aeon-toolkit/aeon
Pull request #366 (2023-05-02 18:22:52)

[ENH] Update edr distance
<!--
Thanks for contributing a pull request! Please ensure you have taken a look at our contribution guide: https://github.com/aeon-toolkit/aeon/blob/main/CONTRIBUTING.md

Feel free to delete sections of this template if they do not apply to your PR, avoid submitting a blank template or empty sections.
-->

#### Reference Issues/PRs
This pr depends on pr #333

#### What does this implement/fix? Explain your changes.
This pr updates edr to the new format for distances. In addition in doing so it makes optimisations for computing the distances between multiple time series. 

<!--
A clear and concise description of what you have implemented.
-->

#### Does your contribution introduce a new dependency? If yes, which one?

<!--
If your contribution does add a dependency, we may suggest adding it as an optional/soft dependency to keep external dependencies of the core aeon package to a minimum.
-->

#### What should a reviewer concentrate their feedback on?

<!--
This section is particularly useful if you have a pull request that is still in development. You can guide the reviews to focus on the parts that are ready for their comments. We suggest using bullets (indicated by * or -) and filled checkboxes [x] here
-->

#### Did you add any tests for the change?

<!--
This section is useful if you have added a test in addition to the existing ones. This will ensure that further changes to these files won't introduce the same kind of bug. It is considered good practice to add tests with newly added code to enforce the fact that the code actually works. This will reduce the chance of introducing logical bugs.
-->

#### Any other comments?

<!--
Please be aware that we are a loose team of volunteers so patience is necessary; assistance handling other issues is very welcome. We value all user contributions, no matter how minor they are. If we are slow to review, either the pull request needs some benchmarking, tinkering, convincing, etc. or more likely the reviewers are simply busy. In either case, we ask for your understanding during the review process.
-->

### PR checklist

<!--
Please go through the checklist below. Please feel free to remove points if they are not applicable.
-->

##### For all contributions
- [x] I've added myself to the [list of contributors](https://github.com/aeon-toolkit/aeon/blob/main/.all-contributorsrc).
- [x] Optionally, I've updated aeon's [CODEOWNERS](https://github.com/aeon-toolkit/aeon/blob/main/CODEOWNERS) to receive notifications about future changes to these files.
- [x] The PR title starts with either [ENH], [MNT], [DOC], or [BUG] indicating whether the PR topic is related to enhancement, maintenance, documentation, or bug.
- [x] For new estimators, I've added the estimator to the online documentation.


<!--
Thanks for contributing!
-->



Patch:
diff --git a/aeon/distances/__init__.py b/aeon/distances/__init__.py
index 9ae07ae4..ca6ec45d 100644
--- a/aeon/distances/__init__.py
+++ b/aeon/distances/__init__.py
@@ -31,18 +31,20 @@ __all__ = [
     "erp_pairwise_distance",
     "erp_alignment_path",
     "erp_cost_matrix",
+    "edr_distance",
+    "edr_pairwise_distance",
+    "edr_alignment_path",
+    "edr_cost_matrix",
     "distance",
     "distance_factory",
     "pairwise_distance",
     "euclidean_distance",
     "squared_distance",
-    "edr_distance",
     "erp_distance",
     "msm_distance",
     "twe_distance",
     "msm_alignment_path",
     "erp_alignment_path",
-    "edr_alignment_path",
     "distance_alignment_path_factory",
     "distance_alignment_path",
     "twe_alignment_path",
@@ -60,8 +62,6 @@ from aeon.distances._distance import (
     distance_alignment_path,
     distance_alignment_path_factory,
     distance_factory,
-    edr_alignment_path,
-    edr_distance,
     msm_alignment_path,
     msm_distance,
     pairwise_distance,
@@ -74,6 +74,12 @@ from aeon.distances._dtw import (
     dtw_distance,
     dtw_pairwise_distance,
 )
+from aeon.distances._edr import (
+    edr_alignment_path,
+    edr_cost_matrix,
+    edr_distance,
+    edr_pairwise_distance,
+)
 from aeon.distances._erp import (
     erp_alignment_path,
     erp_cost_matrix,
diff --git a/aeon/distances/_distance.py b/aeon/distances/_distance.py
index 5d3f7d6a..653ac895 100644
--- a/aeon/distances/_distance.py
+++ b/aeon/distances/_distance.py
@@ -12,7 +12,7 @@ from aeon.distances._ddtw import (
     ddtw_pairwise_distance,
 )
 from aeon.distances._dtw import dtw_alignment_path, dtw_distance, dtw_pairwise_distance
-from aeon.distances._edr import _EdrDistance
+from aeon.distances._edr import edr_alignment_path, edr_distance, edr_pairwise_distance
 from aeon.distances._erp import erp_alignment_path, erp_distance, erp_pairwise_distance
 from aeon.distances._euclidean import euclidean_distance, euclidean_pairwise_distance
 from aeon.distances._lcss import (
@@ -52,86 +52,6 @@ from aeon.distances.base import (
 )
 
 
-def edr_distance(
-    x: np.ndarray,
-    y: np.ndarray,
-    window: Union[float, None] = None,
-    epsilon: float = None,
-    **kwargs: Any,
-) -> float:
-    """Compute the Edit distance for real sequences (EDR) between two series.
-
-    EDR computes the minimum number of elements (as a percentage) that must be removed
-    from x and y so that the sum of the distance between the remaining signal elements
-    lies within the tolerance (epsilon). EDR was originally proposed in [1]_.
-
-    The value returned will be between 0 and 1 per time series. The value will
-    represent as a percentage of elements that must be removed for the time series to
-    be an exact match.
-
-    Parameters
-    ----------
-    x: np.ndarray (1d or 2d array)
-        First time series.
-    y: np.ndarray (1d or 2d array)
-        Second time series.
-    window: float, defaults = None
-        Float that is the radius of the sakoe chiba window (if using Sakoe-Chiba
-        lower bounding). Value must be between 0. and 1.
-    epsilon : float, defaults = None
-        Matching threshold to determine if two subsequences are considered close
-        enough to be considered 'common'. If not specified as per the original paper
-        epsilon is set to a quarter of the maximum standard deviation.
-    kwargs: Any
-        Extra kwargs.
-
-    Returns
-    -------
-    float
-        Edr distance between the x and y. The value will be between 0.0 and 1.0
-        where 0.0 is an exact match between time series (i.e. they are the same) and
-        1.0 where there are no matching subsequences.
-
-    Raises
-    ------
-    ValueError
-        If the sakoe_chiba_window_radius is not a float.
-        If the value of x or y provided is not a numpy array.
-        If the value of x or y has more than 3 dimensions.
-        If a metric string provided, and is not a defined valid string.
-        If a metric object (instance of class) is provided and doesn't inherit from
-        NumbaDistance.
-        If the metric type cannot be determined
-
-    Examples
-    --------
-    >>> x_1d = np.array([1, 2, 3, 4])  # 1d array
-    >>> y_1d = np.array([5, 6, 7, 8])  # 1d array
-    >>> edr_distance(x_1d, y_1d)
-    1.0
-
-    >>> x_2d = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])  # 2d array
-    >>> y_2d = np.array([[9, 10, 11, 12], [13, 14, 15, 16]])  # 2d array
-    >>> edr_distance(x_2d, y_2d)
-    1.0
-
-    References
-    ----------
-    .. [1] Lei Chen, M. Tamer Özsu, and Vincent Oria. 2005. Robust and fast similarity
-    search for moving object trajectories. In Proceedings of the 2005 ACM SIGMOD
-    international conference on Management of data (SIGMOD '05). Association for
-    Computing Machinery, New York, NY, USA, 491–502.
-    DOI:https://doi.org/10.1145/1066157.1066213
-    """
-    format_kwargs = {
-        "window": window,
-        "epsilon": epsilon,
-    }
-    format_kwargs = {**format_kwargs, **kwargs}
-
-    return distance(x, y, metric="edr", **format_kwargs)
-
-
 def msm_distance(
     x: np.ndarray,
     y: np.ndarray,
@@ -277,83 +197,6 @@ def twe_distance(
     return distance(x, y, metric="twe", **format_kwargs)
 
 
-def edr_alignment_path(
-    x: np.ndarray,
-    y: np.ndarray,
-    return_cost_matrix: bool = False,
-    window: Union[float, None] = None,
-    epsilon: float = None,
-    **kwargs: Any,
-) -> AlignmentPathReturn:
-    """Compute the Edit distance for real sequences (EDR) alignment path.
-
-    EDR computes the minimum number of elements (as a percentage) that must be removed
-    from x and y so that the sum of the distance between the remaining signal elements
-    lies within the tolerance (epsilon). EDR was originally proposed in [1]_.
-
-    The value returned will be between 0 and 1 per time series. The value will
-    represent as a percentage of elements that must be removed for the time series to
-    be an exact match.
-
-    Parameters
-    ----------
-    x: np.ndarray (1d or 2d array)
-        First time series.
-    y: np.ndarray (1d or 2d array)
-        Second time series.
-    return_cost_matrix: bool, defaults = False
-        Boolean that when true will also return the cost matrix.
-    window: float, defaults = None
-        Float that is the radius of the sakoe chiba window (if using Sakoe-Chiba
-        lower bounding). Value must be between 0. and 1.
-    epsilon : float, defaults = None
-        Matching threshold to determine if two subsequences are considered close
-        enough to be considered 'common'. If not specified as per the original paper
-        epsilon is set to a quarter of the maximum standard deviation.
-    kwargs: Any
-        Extra kwargs.
-
-    Returns
-    -------
-    list[tuple]
-        List of tuples containing the edr alignment path.
-    float
-        Edr distance between x and y.
-    np.ndarray (of shape (len(x), len(y)).
-        Optional return only given if return_cost_matrix = True.
-        Cost matrix used to compute the distance.
-
-
-    Raises
-    ------
-    ValueError
-        If the sakoe_chiba_window_radius is not a float.
-        If the value of x or y provided is not a numpy array.
-        If the value of x or y has more than 3 dimensions.
-        If a metric string provided, and is not a defined valid string.
-        If a metric object (instance of class) is provided and doesn't inherit from
-        NumbaDistance.
-        If the metric type cannot be determined
-
-    References
-    ----------
-    .. [1] Lei Chen, M. Tamer Özsu, and Vincent Oria. 2005. Robust and fast similarity
-    search for moving object trajectories. In Proceedings of the 2005 ACM SIGMOD
-    international conference on Management of data (SIGMOD '05). Association for
-    Computing Machinery, New York, NY, USA, 491–502.
-    DOI:https://doi.org/10.1145/1066157.1066213
-    """
-    format_kwargs = {
-        "window": window,
-        "epsilon": epsilon,
-    }
-    format_kwargs = {**format_kwargs, **kwargs}
-
-    return distance_alignment_path(
-        x, y, metric="edr", return_cost_matrix=return_cost_matrix, **format_kwargs
-    )
-
-
 def msm_alignment_path(
     x: np.ndarray,
     y: np.ndarray,
@@ -506,7 +349,17 @@ def twe_alignment_path(
     )
 
 
-NEW_DISTANCES = ["squared", "euclidean", "dtw", "ddtw", "wdtw", "wddtw", "lcss", "erp"]
+NEW_DISTANCES = [
+    "squared",
+    "euclidean",
+    "dtw",
+    "ddtw",
+    "wdtw",
+    "wddtw",
+    "lcss",
+    "erp",
+    "edr",
+]
 
 
 def distance(
@@ -605,6 +458,8 @@ def distance(
             return lcss_distance(x, y, **kwargs)
         elif metric == "erp":
             return erp_distance(x, y, **kwargs)
+        elif metric == "edr":
+            return edr_distance(x, y, **kwargs)
     _x = to_numba_timeseries(x)
     _y = to_numba_timeseries(y)
 
@@ -688,6 +543,8 @@ def distance_factory(
             return lcss_distance
         elif metric == "erp":
             return erp_distance
+        elif metric == "edr":
+            return edr_distance
     global dist_callable
 
     if x is None:
@@ -820,6 +677,8 @@ def pairwise_distance(
             return lcss_pairwise_distance(_x, _y, **kwargs)
         elif metric == "erp":
             return erp_pairwise_distance(_x, _y, **kwargs)
+        elif metric == "edr":
+            return edr_pairwise_distance(_x, _y, **kwargs)
 
     symmetric = np.array_equal(_x, _y)
     _metric_callable = _resolve_metric_to_factory(
@@ -910,6 +769,8 @@ def distance_alignment_path(
             return lcss_alignment_path(x, y, **kwargs)
         elif metric == "erp":
             return erp_alignment_path(x, y, **kwargs)
+        elif metric == "edr":
+            return edr_alignment_path(x, y, **kwargs)
     _x = to_numba_timeseries(x)
     _y = to_numba_timeseries(y)
 
@@ -997,6 +858,8 @@ def distance_alignment_path_factory(
             return lcss_alignment_path
         elif metric == "erp":
             return erp_alignment_path
+        elif metric == "edr":
+            return edr_alignment_path
     if x is None:
         x = np.zeros((1, 10))
     if y is None:
@@ -1019,13 +882,6 @@ def distance_alignment_path_factory(
 
 
 _METRIC_INFOS = [
-    MetricInfo(
-        canonical_name="edr",
-        aka={"edr", "edit distance for real sequences"},
-        dist_func=edr_distance,
-        dist_instance=_EdrDistance(),
-        dist_alignment_path_func=edr_alignment_path,
-    ),
     MetricInfo(
         canonical_name="msm",
         aka={"msm", "move-split-merge"},
@@ -1050,7 +906,6 @@ _METRIC_CALLABLES = dict(
 _METRICS_NAMES = list(_METRICS.keys())
 
 ALL_DISTANCES = (
-    edr_distance,
     msm_distance,
     twe_distance,
 )
diff --git a/aeon/distances/_dtw.py b/aeon/distances/_dtw.py
index 2545f466..ad95cf64 100644
--- a/aeon/distances/_dtw.py
+++ b/aeon/distances/_dtw.py
@@ -61,7 +61,7 @@ def dtw_distance(x: np.ndarray, y: np.ndarray, window: float = None) -> float:
     .. math::
         dtw(x, y) = \sqrt{\sum_{(i, j) \in \pi} \|x_{i} - y_{j}\|^2}
 
-    # Parameters
+    Parameters
     ----------
     x: np.ndarray, of shape (n_channels, n_timepoints) or (n_timepoints,) or
             (n_instances, n_channels, n_timepoints)
diff --git a/aeon/distances/_edr.py b/aeon/distances/_edr.py
index d5c55430..29fd15d8 100644
--- a/aeon/distances/_edr.py
+++ b/aeon/distances/_edr.py
@@ -1,231 +1,200 @@
 # -*- coding: utf-8 -*-
+"""Edit distance for real sequences (EDR) between two time series.
+
+ERP was adapted in [1] specifically for distances between trajectories. Like LCSS,
+EDR uses a distance threshold to define when two elements of a series match.
+However, rather than simply count matches and look for the longest sequence,
+ERP applies a (constant) penalty for non-matching elements
+where gaps are inserted to create an optimal alignment.
+
+References
+----------
+.. [1] Chen L, Ozsu MT, Oria V: Robust and fast similarity search for moving
+object trajectories. In: Proceedings of the ACM SIGMOD International Conference
+on Management of Data, 2005
+"""
 __author__ = ["chrisholder", "TonyBagnall"]
 
-import warnings
-from typing import Any, List, Tuple
+from typing import List, Tuple
 
 import numpy as np
 from numba import njit
-from numba.core.errors import NumbaWarning
 
 from aeon.distances._alignment_paths import (
     _add_inf_to_out_of_bounds_cost_matrix,
     compute_min_return_path,
 )
 from aeon.distances._bounding_matrix import create_bounding_matrix
-from aeon.distances.base import (
-    DistanceAlignmentPathCallable,
-    DistanceCallable,
-    NumbaDistance,
-)
+from aeon.distances._euclidean import _univariate_euclidean_distance
+
 
-# Warning occurs when using large time series (i.e. 1000x1000)
-warnings.simplefilter("ignore", category=NumbaWarning)
+@njit(cache=True, fastmath=True)
+def edr_distance(
+    x: np.ndarray, y: np.ndarray, window: float = None, epsilon: float = None
+) -> float:
+    """Compute the edr distance between two time series.
+
+    EDR computes the minimum number of elements (as a percentage) that must be removed
+    from x and y so that the sum of the distance between the remaining signal elements
+    lies within the tolerance (epsilon). EDR was originally proposed in [1]_.
+
+    The value returned will be between 0 and 1 per time series. The value will
+    represent as a percentage of elements that must be removed for the time series to
+    be an exact match.
+
+    Parameters
+    ----------
+    x: np.ndarray, of shape (n_channels, n_timepoints) or (n_timepoints,) or
+            (n_instances, n_channels, n_timepoints)
+        First time series.
+    y: np.ndarray, of shape (m_channels, m_timepoints) or (m_timepoints,) or
+            (m_instances, m_channels, m_timepoints)
+        Second time series.
+    window: float, default=None
+        The window to use for the bounding matrix. If None, no bounding matrix
+        is used.
+    epsilon : float, defaults = None
+        Matching threshold to determine if two subsequences are considered close
+        enough to be considered 'common'. If not specified as per the original paper
+        epsilon is set to a quarter of the maximum standard deviation.
 
+    Returns
+    -------
+    float
+        edr distance between x and y.
 
-class _EdrDistance(NumbaDistance):
-    """Edit distance for real sequences (EDR) between two time series.
+    Raises
+    ------
+    ValueError
+        If x and y are not 1D, 2D, or 3D arrays.
 
-    ERP was adapted in [1] specifically for distances between trajectories. Like LCSS,
-    EDR uses a distance threshold to define when two elements of a series match.
-    However, rather than simply count matches and look for the longest sequence,
-    ERP applies a (constant) penalty for non-matching elements
-    where gaps are inserted to create an optimal alignment.
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from aeon.distances import edr_distance
+    >>> 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]])
+    >>> dist = edr_distance(x, y)
 
     References
     ----------
-    .. [1] Chen L, Ozsu MT, Oria V: Robust and fast similarity search for moving
-    object trajectories. In: Proceedings of the ACM SIGMOD International Conference
-    on Management of Data, 2005
+    .. [1] Lei Chen, M. Tamer Özsu, and Vincent Oria. 2005. Robust and fast similarity
+    search for moving object trajectories. In Proceedings of the 2005 ACM SIGMOD
+    international conference on Management of data (SIGMOD '05). Association for
+    Computing Machinery, New York, NY, USA, 491–502.
+    DOI:https://doi.org/10.1145/1066157.1066213
     """
+    if x.ndim == 1 and y.ndim == 1:
+        _x = x.reshape((1, x.shape[0]))
+        _y = y.reshape((1, y.shape[0]))
+        bounding_matrix = create_bounding_matrix(_x.shape[1], _y.shape[1], window)
+        return _edr_distance(_x, _y, bounding_matrix, epsilon)
+    if x.ndim == 2 and y.ndim == 2:
+        bounding_matrix = create_bounding_matrix(x.shape[1], y.shape[1], window)
+        return _edr_distance(x, y, bounding_matrix, epsilon)
+    if x.ndim == 3 and y.ndim == 3:
+        distance = 0
+        bounding_matrix = create_bounding_matrix(x.shape[2], y.shape[2], window)
+        for curr_x, curr_y in zip(x, y):
+            distance += _edr_distance(curr_x, curr_y, bounding_matrix, epsilon)
+        return distance
+    raise ValueError("x and y must be 1D, 2D, or 3D arrays")
 
-    def _distance_alignment_path_factory(
-        self,
-        x: np.ndarray,
-        y: np.ndarray,
-        return_cost_matrix: bool = False,
-        window: float = None,
-        epsilon: float = None,
-        **kwargs: Any,
-    ) -> DistanceAlignmentPathCallable:
-        """Create a no_python compiled edr alignment path distance callable.
-
-        Series should be shape (d, m), where d is the number of dimensions, m the series
-        length. Series can be different lengths.
-
-        Parameters
-        ----------
-        x: np.ndarray (2d array of shape (d,m1)).
-            First time series.
-        y: np.ndarray (2d array of shape (d,m2)).
-            Second time series.
-        return_cost_matrix: bool, defaults = False
-            Boolean that when true will also return the cost matrix.
-        window: float, defaults = None
-            Float that is the radius of the sakoe chiba window (if using Sakoe-Chiba
-            lower bounding). Must be between 0 and 1.
-        epsilon : float, defaults = None
-            Matching threshold to determine if two subsequences are considered close
-            enough to be considered 'common'. If not specified as per the original paper
-            epsilon is set to a quarter of the maximum standard deviation.
-        kwargs: Any
-            Extra kwargs.
-
-        Returns
-        -------
-        Callable[[np.ndarray, np.ndarray], tuple[np.ndarray, float]]
-            No_python compiled edr distance path callable.
-
-        Raises
-        ------
-        ValueError
-            If the input time series are not numpy array.
-            If the input time series do not have exactly 2 dimensions.
-        """
-        _bounding_matrix = create_bounding_matrix(x.shape[1], y.shape[1], window)
-
-        if epsilon is not None and not isinstance(epsilon, float):
-            raise ValueError("The value of epsilon must be a float.")
-
-        if return_cost_matrix is True:
-
-            @njit(cache=True)
-            def numba_edr_distance_alignment_path(
-                _x: np.ndarray, _y: np.ndarray
-            ) -> Tuple[List, float, np.ndarray]:
-                if epsilon is None:
-                    _epsilon = max(np.std(_x), np.std(_y)) / 4
-                else:
-                    _epsilon = epsilon
-                cost_matrix = _edr_cost_matrix(_x, _y, _bounding_matrix, _epsilon)
-                temp_cm = _add_inf_to_out_of_bounds_cost_matrix(
-                    cost_matrix, _bounding_matrix
-                )
-                path = compute_min_return_path(temp_cm)
-                distance = float(cost_matrix[-1, -1] / max(_x.shape[1], _y.shape[1]))
-                return path, distance, cost_matrix
-
-        else:
 
-            @njit(cache=True)
-            def numba_edr_distance_alignment_path(
-                _x: np.ndarray, _y: np.ndarray
-            ) -> Tuple[List, float]:
-                if epsilon is None:
-                    _epsilon = max(np.std(_x), np.std(_y)) / 4
-                else:
-                    _epsilon = epsilon
-                cost_matrix = _edr_cost_matrix(_x, _y, _bounding_matrix, _epsilon)
-                temp_cm = _add_inf_to_out_of_bounds_cost_matrix(
-                    cost_matrix, _bounding_matrix
-                )
-                path = compute_min_return_path(temp_cm)
-                distance = float(cost_matrix[-1, -1] / max(_x.shape[1], _y.shape[1]))
-                return path, distance
-
-        return numba_edr_distance_alignment_path
-
-    def _distance_factory(
-        self,
-        x: np.ndarray,
-        y: np.ndarray,
-        window: float = None,
-        epsilon: float = None,
-        **kwargs: Any,
-    ) -> DistanceCallable:
-        """Create a no_python compiled edr distance callable.
-
-        Series should be shape (d, m), where d is the number of dimensions, m the series
-        length. Series can be different lengths.
-
-        Parameters
-        ----------
-        x: np.ndarray (2d array of shape (d,m1)).
-            First time series.
-        y: np.ndarray (2d array of shape (d,m2)).
-            Second time series.
-        window: float, defaults = None
-            Float that is the radius of the sakoe chiba window (if using Sakoe-Chiba
-            lower bounding). Must be between 0 and 1.
-        epsilon : float, defaults = None
-            Matching threshold to determine if two subsequences are considered close
-            enough to be considered 'common'. If not specified as per the original paper
-            epsilon is set to a quarter of the maximum standard deviation.
-        kwargs: Any
-            Extra kwargs.
-
-        Returns
-        -------
-        Callable[[np.ndarray, np.ndarray], float]
-            No_python compiled edr distance callable.
-
-        Raises
-        ------
-        ValueError
-            If the input time series are not numpy array.
-            If the input time series do not have exactly 2 dimensions.
-          If
-        """
-        _bounding_matrix = create_bounding_matrix(x.shape[1], y.shape[1], window)
-
-        if epsilon is not None and not isinstance(epsilon, float):
-            raise ValueError("The value of epsilon must be a float.")
-
-        @njit(cache=True)
-        def numba_edr_distance(_x: np.ndarray, _y: np.ndarray) -> float:
-            if np.array_equal(_x, _y):
-                return 0.0
-            if epsilon is None:
-                _epsilon = max(np.std(_x), np.std(_y)) / 4
-            else:
-                _epsilon = epsilon
-            cost_matrix = _edr_cost_matrix(_x, _y, _bounding_matrix, _epsilon)
-            return float(cost_matrix[-1, -1] / max(_x.shape[1], _y.shape[1]))
-
-        return numba_edr_distance
-
-
-@njit(cache=True)
-def _edr_cost_matrix(
-    x: np.ndarray,
-    y: np.ndarray,
-    bounding_matrix: np.ndarray,
-    epsilon: float,
-):
+@njit(cache=True, fastmath=True)
+def edr_cost_matrix(
+    x: np.ndarray, y: np.ndarray, window: float = None, epsilon: float = None
+) -> np.ndarray:
     """Compute the edr cost matrix between two time series.
 
     Parameters
     ----------
-    x: np.ndarray, 2d shape (d (n_dimensions),m (series_length))
+    x: np.ndarray, of shape (n_channels, n_timepoints) or (n_timepoints,) or
+            (n_instances, n_channels, n_timepoints)
         First time series.
-    y: np.ndarray, 2d array shape (d, m)
+    y: np.ndarray, of shape (m_channels, m_timepoints) or (m_timepoints,) or
+            (m_instances, m_channels, m_timepoints)
         Second time series.
-    bounding_matrix: np.ndarray (2d of size mxn where m is len(x) and n is len(y))
-        Bounding matrix where the values in bound are marked by finite values and
-        outside bound points are infinite values.
-    epsilon : float
-        Matching threshold to determine if distance between two subsequences are
-        considered similar (similar if distance less than the threshold).
+    window: float, default=None
+        The window to use for the bounding matrix. If None, no bounding matrix
+        is used.
+    epsilon : float, defaults = None
+        Matching threshold to determine if two subsequences are considered close
+        enough to be considered 'common'. If not specified as per the original paper
+        epsilon is set to a quarter of the maximum standard deviation.
 
     Returns
     -------
-    np.ndarray (2d of size mxn where m is len(x) and n is len(y))
-        Edr cost matrix between x and y.
+    np.ndarray (n_timepoints, m_timepoints)
+        edr cost matrix between x and y.
+
+    Raises
+    ------
+    ValueError
+        If x and y are not 1D, 2D, or 3D arrays.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from aeon.distances import edr_cost_matrix
+    >>> x = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
+    >>> y = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
+    >>> edr_cost_matrix(x, y)
+    array([[0., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
+           [1., 0., 1., 2., 2., 2., 2., 2., 2., 2.],
+           [1., 1., 0., 1., 2., 3., 3., 3., 3., 3.],
+           [1., 2., 1., 0., 1., 2., 3., 4., 4., 4.],
+           [1., 2., 2., 1., 0., 1., 2., 3., 4., 5.],
+           [1., 2., 3., 2., 1., 0., 1., 2., 3., 4.],
+           [1., 2., 3., 3., 2., 1., 0., 1., 2., 3.],
+           [1., 2., 3., 4., 3., 2., 1., 0., 1., 2.],
+           [1., 2., 3., 4., 4., 3., 2., 1., 0., 1.],
+           [1., 2., 3., 4., 5., 4., 3., 2., 1., 0.]])
     """
-    dimensions = x.shape[0]
+    if x.ndim == 1 and y.ndim == 1:
+        _x = x.reshape((1, x.shape[0]))
+        _y = y.reshape((1, y.shape[0]))
+        bounding_matrix = create_bounding_matrix(_x.shape[1], _y.shape[1], window)
+        return _edr_cost_matrix(_x, _y, bounding_matrix, epsilon)
+    if x.ndim == 2 and y.ndim == 2:
+        bounding_matrix = create_bounding_matrix(x.shape[1], y.shape[1], window)
+        return _edr_cost_matrix(x, y, bounding_matrix, epsilon)
+    if x.ndim == 3 and y.ndim == 3:
+        bounding_matrix = create_bounding_matrix(x.shape[2], y.shape[2], window)
+        cost_matrix = np.zeros((x.shape[2], y.shape[2]))
+        for curr_x, curr_y in zip(x, y):
+            cost_matrix = np.add(
+                cost_matrix, _edr_cost_matrix(curr_x, curr_y, bounding_matrix, epsilon)
+            )
+        return cost_matrix
+    raise ValueError("x and y must be 1D, 2D, or 3D arrays")
+
+
+@njit(cache=True, fastmath=True)
+def _edr_distance(
+    x: np.ndarray, y: np.ndarray, bounding_matrix: np.ndarray, epsilon: float = None
+) -> float:
+    distance = _edr_cost_matrix(x, y, bounding_matrix, epsilon)[
+        x.shape[1] - 1, y.shape[1] - 1
+    ]
+    return float(distance / max(x.shape[1], y.shape[1]))
+
+
+@njit(cache=True, fastmath=True)
+def _edr_cost_matrix(
+    x: np.ndarray, y: np.ndarray, bounding_matrix: np.ndarray, epsilon: float = None
+) -> np.ndarray:
     x_size = x.shape[1]
     y_size = y.shape[1]
+    if epsilon is None:
+        epsilon = max(np.std(x), np.std(y)) / 4
+
     cost_matrix = np.zeros((x_size + 1, y_size + 1))
+
     for i in range(1, x_size + 1):
         for j in range(1, y_size + 1):
             if bounding_matrix[i - 1, j - 1]:
-                curr_dist = 0
-                for k in range(dimensions):
-                    curr_dist += (x[k][i - 1] - y[k][j - 1]) * (
-                        x[k][i - 1] - y[k][j - 1]
-                    )
-                curr_dist = np.sqrt(curr_dist)
-                if curr_dist < epsilon:
+                if _univariate_euclidean_distance(x[:, i - 1], y[:, j - 1]) < epsilon:
                     cost = 0
                 else:
                     cost = 1
@@ -235,3 +204,190 @@ def _edr_cost_matrix(
                     cost_matrix[i, j - 1] + 1,
                 )
     return cost_matrix[1:, 1:]
+
+
+@njit(cache=True, fastmath=True)
+def edr_pairwise_distance(
+    X: np.ndarray, y: np.ndarray = None, window: float = None, epsilon: float = None
+) -> np.ndarray:
+    """Compute the pairwise edr distance between a set of time series.
+
+    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.
+    window: float, default=None
+        The window to use for the bounding matrix. If None, no bounding matrix
+        is used.
+    epsilon : float, defaults = None
+        Matching threshold to determine if two subsequences are considered close
+        enough to be considered 'common'. If not specified as per the original paper
+        epsilon is set to a quarter of the maximum standard deviation.
+
+    Returns
+    -------
+    np.ndarray (n_instances, n_instances)
+        edr pairwise matrix between the instances of X.
+
+    Raises
+    ------
+    ValueError
+        If X is not 2D or 3D array when only passing X.
+        If X and y are not 1D, 2D or 3D arrays when passing both X and y.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from aeon.distances import edr_pairwise_distance
+    >>> # Distance between each time series in a collection of time series
+    >>> X = np.array([[[1, 2, 3]],[[4, 5, 6]], [[7, 8, 9]]])
+    >>> edr_pairwise_distance(X)
+    array([[0., 1., 1.],
+           [1., 0., 1.],
+           [1., 1., 0.]])
+
+    >>> # Distance between two collections of time series
+    >>> X = np.array([[[1, 2, 3]],[[4, 5, 6]], [[7, 8, 9]]])
+    >>> y = np.array([[[11, 12, 13]],[[14, 15, 16]], [[17, 18, 19]]])
+    >>> edr_pairwise_distance(X, y)
+    array([[1., 1., 1.],
+           [1., 1., 1.],
+           [1., 1., 1.]])
+
+    >>> X = np.array([[[1, 2, 3]],[[4, 5, 6]], [[7, 8, 9]]])
+    >>> y_univariate = np.array([[11, 12, 13],[14, 15, 16], [17, 18, 19]])
+    >>> edr_pairwise_distance(X, y_univariate)
+    array([[1.],
+           [1.],
+           [1.]])
+    """
+    if y is None:
+        # To self
+        if X.ndim == 3:
+            return _edr_pairwise_distance(X, window, epsilon)
+        if X.ndim == 2:
+            _X = X.reshape((X.shape[0], 1, X.shape[1]))
+            return _edr_pairwise_distance(_X, window, epsilon)
+        raise ValueError("x and y must be 2D or 3D arrays")
+    elif y.ndim == X.ndim:
+        # Multiple to multiple
+        if y.ndim == 3 and X.ndim == 3:
+            return _edr_from_multiple_to_multiple_distance(X, y, window, epsilon)
+        if y.ndim == 2 and X.ndim == 2:
+            _x = X.reshape((X.shape[0], 1, X.shape[1]))
+            _y = y.reshape((y.shape[0], 1, y.shape[1]))
+            return _edr_from_multiple_to_multiple_distance(_x, _y, window, epsilon)
+        if y.ndim == 1 and X.ndim == 1:
+            _x = X.reshape((1, 1, X.shape[0]))
+            _y = y.reshape((1, 1, y.shape[0]))
+            return _edr_from_multiple_to_multiple_distance(_x, _y, window, epsilon)
+        raise ValueError("x and y must be 1D, 2D, or 3D arrays")
+    else:
+        # Single to multiple
+        if X.ndim == 3 and y.ndim == 2:
+            _y = y.reshape((1, y.shape[0], y.shape[1]))
+            return _edr_from_multiple_to_multiple_distance(X, _y, window, epsilon)
+        if y.ndim == 3 and X.ndim == 2:
+            _x = X.reshape((1, X.shape[0], X.shape[1]))
+            return _edr_from_multiple_to_multiple_distance(_x, y, window, epsilon)
+        if X.ndim == 2 and y.ndim == 1:
+            _x = X.reshape((X.shape[0], 1, X.shape[1]))
+            _y = y.reshape((1, 1, y.shape[0]))
+            return _edr_from_multiple_to_multiple_distance(_x, _y, window, epsilon)
+        if y.ndim == 2 and X.ndim == 1:
+            _x = X.reshape((1, 1, X.shape[0]))
+            _y = y.reshape((y.shape[0], 1, y.shape[1]))
+            return _edr_from_multiple_to_multiple_distance(_x, _y, window, epsilon)
+        else:
+            raise ValueError("x and y must be 2D or 3D arrays")
+
+
+@njit(cache=True, fastmath=True)
+def _edr_pairwise_distance(
+    X: np.ndarray, window: float, epsilon: float = None
+) -> np.ndarray:
+    n_instances = X.shape[0]
+    distances = np.zeros((n_instances, n_instances))
+    bounding_matrix = create_bounding_matrix(X.shape[2], X.shape[2], window)
+
+    for i in range(n_instances):
+        for j in range(i + 1, n_instances):
+            distances[i, j] = _edr_distance(X[i], X[j], bounding_matrix, epsilon)
+            distances[j, i] = distances[i, j]
+
+    return distances
+
+
+@njit(cache=True, fastmath=True)
+def _edr_from_multiple_to_multiple_distance(
+    x: np.ndarray, y: np.ndarray, window: float, epsilon: float = None
+) -> np.ndarray:
+    n_instances = x.shape[0]
+    m_instances = y.shape[0]
+    distances = np.zeros((n_instances, m_instances))
+    bounding_matrix = create_bounding_matrix(x.shape[2], y.shape[2], window)
+
+    for i in range(n_instances):
+        for j in range(m_instances):
+            distances[i, j] = _edr_distance(x[i], y[j], bounding_matrix, epsilon)
+    return distances
+
+
+@njit(cache=True, fastmath=True)
+def edr_alignment_path(
+    x: np.ndarray, y: np.ndarray, window: float = None, epsilon: float = None
+) -> Tuple[List[Tuple[int, int]], float]:
+    """Compute the edr alignment path between two time series.
+
+    Parameters
+    ----------
+    x: np.ndarray, of shape (n_channels, n_timepoints) or (n_timepoints,) or
+            (n_instances, n_channels, n_timepoints)
+        First time series.
+    y: np.ndarray, of shape (m_channels, m_timepoints) or (m_timepoints,) or
+            (m_instances, m_channels, m_timepoints)
+        Second time series.
+    window: float, default=None
+        The window to use for the bounding matrix. If None, no bounding matrix
+        is used.
+    epsilon : float, defaults = None
+        Matching threshold to determine if two subsequences are considered close
+        enough to be considered 'common'. If not specified as per the original paper
+        epsilon is set to a quarter of the maximum standard deviation.
+
+    Returns
+    -------
+    List[Tuple[int, int]]
+        The alignment path between the two time series where each element is a tuple
+        of the index in x and the index in y that have the best alignment according
+        to the cost matrix.
+    float
+        The edr distance between the two time series.
+
+    Raises
+    ------
+    ValueError
+        If x and y are not 1D, 2D, or 3D arrays.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from aeon.distances import edr_alignment_path
+    >>> x = np.array([[1, 2, 3, 6]])
+    >>> y = np.array([[1, 2, 3, 4]])
+    >>> edr_alignment_path(x, y)
+    ([(0, 0), (1, 1), (2, 2), (3, 3)], 0.25)
+    """
+    x_size = x.shape[-1]
+    y_size = y.shape[-1]
+    bounding_matrix = create_bounding_matrix(x_size, y_size, window)
+    cost_matrix = edr_cost_matrix(x, y, window, epsilon)
+    # Need to do this because the cost matrix contains 0s and not inf in out of bounds
+    cost_matrix = _add_inf_to_out_of_bounds_cost_matrix(cost_matrix, bounding_matrix)
+    return compute_min_return_path(cost_matrix), float(
+        cost_matrix[x_size - 1, y_size - 1] / max(x_size, y_size)
+    )


Test patch:
diff --git a/aeon/distances/tests/_expected_results.py b/aeon/distances/tests/_expected_results.py
index 2f40d3b2..a22d5a88 100644
--- a/aeon/distances/tests/_expected_results.py
+++ b/aeon/distances/tests/_expected_results.py
@@ -23,7 +23,7 @@ _expected_distance_results = {
         18.353616712062177,
         171.1112853298397,
     ],
-    "edr": [1.0, 0.6, 1.0],
+    "edr": [1.0, 0.6, 1.0, 10.0, 0.4, 0.5, 5.0],
     "lcss": [1.0, 0.1, 1.0, 9.8, 0.0, 1.0, 9.4],
     "squared": [
         25.0,
diff --git a/aeon/distances/tests/test_cost_matrix.py b/aeon/distances/tests/test_cost_matrix.py
index 290bb238..5c5670f5 100644
--- a/aeon/distances/tests/test_cost_matrix.py
+++ b/aeon/distances/tests/test_cost_matrix.py
@@ -33,6 +33,10 @@ def _validate_cost_matrix_result(
                 float(cost_matrix_result[-1, -1] / min(x.shape[-1], y.shape[-1]))
             )
             assert_almost_equal(distance, distance_result)
+    elif name == "edr":
+        if x.ndim != 3:
+            distance = float(cost_matrix_result[-1, -1] / max(x.shape[-1], y.shape[-1]))
+            assert_almost_equal(distance, distance_result)
     else:
         assert_almost_equal(cost_matrix_result[-1, -1], distance_result)
 
diff --git a/aeon/distances/tests/test_new_distances.py b/aeon/distances/tests/test_new_distances.py
index 3d17e838..23f08bbb 100644
--- a/aeon/distances/tests/test_new_distances.py
+++ b/aeon/distances/tests/test_new_distances.py
@@ -15,6 +15,10 @@ from aeon.distances import (
     dtw_cost_matrix,
     dtw_distance,
     dtw_pairwise_distance,
+    edr_alignment_path,
+    edr_cost_matrix,
+    edr_distance,
+    edr_pairwise_distance,
     erp_alignment_path,
     erp_cost_matrix,
     erp_distance,
@@ -92,6 +96,13 @@ DISTANCES = [
         "cost_matrix": erp_cost_matrix,
         "alignment_path": erp_alignment_path,
     },
+    {
+        "name": "edr",
+        "distance": edr_distance,
+        "pairwise_distance": edr_pairwise_distance,
+        "cost_matrix": edr_cost_matrix,
+        "alignment_path": edr_alignment_path,
+    },
 ]