Repository: aeon-toolkit/aeon
Pull request #759 (2023-09-23 17:53:52)

[ENH] Adtw 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

<!--
Example: Fixes #1234. See also #3456.

Please use keywords (e.g., Fixes) to create link to the issues or pull requests you resolved, so that they will automatically be closed when your pull request is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->
Implements ADTW mentioned in Issue #749 

#### What does this implement/fix? Explain your changes.
This PR introduces a new distance measure ADTW.
<!--
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!
-->


chrisholder: Should mention I set the default of warp_penalty=1. there is no reasoning for this other than msm uses a cost of 1. In the paper it says this value should be carefully selected.

Patch:
diff --git a/aeon/clustering/metrics/averaging/_barycenter_averaging.py b/aeon/clustering/metrics/averaging/_barycenter_averaging.py
index 75a72a49..c38d0201 100644
--- a/aeon/clustering/metrics/averaging/_barycenter_averaging.py
+++ b/aeon/clustering/metrics/averaging/_barycenter_averaging.py
@@ -8,6 +8,7 @@ from numba import njit
 
 from aeon.clustering.metrics.medoids import medoids
 from aeon.distances import (
+    adtw_alignment_path,
     ddtw_alignment_path,
     dtw_alignment_path,
     edr_alignment_path,
@@ -113,6 +114,7 @@ def _ba_update(
     c: float = 1.0,
     descriptor: str = "identity",
     reach: int = 30,
+    warp_penalty: float = 1.0,
 ) -> Tuple[np.ndarray, float]:
     X_size, X_dims, X_timepoints = X.shape
     sum = np.zeros(X_timepoints)
@@ -142,6 +144,10 @@ def _ba_update(
             curr_alignment, _ = shape_dtw_alignment_path(
                 curr_ts, center, window=window, descriptor=descriptor, reach=reach
             )
+        elif metric == "adtw":
+            curr_alignment, _ = adtw_alignment_path(
+                curr_ts, center, window=window, warp_penalty=warp_penalty
+            )
         else:
             # When numba version > 0.57 add more informative error with what metric
             # was passed.
diff --git a/aeon/distances/__init__.py b/aeon/distances/__init__.py
index a0903985..ef4b3ad2 100644
--- a/aeon/distances/__init__.py
+++ b/aeon/distances/__init__.py
@@ -10,6 +10,10 @@ __all__ = [
     "euclidean_pairwise_distance",
     "manhattan_distance",
     "manhattan_pairwise_distance",
+    "adtw_distance",
+    "adtw_pairwise_distance",
+    "adtw_cost_matrix",
+    "adtw_alignment_path",
     "dtw_distance",
     "dtw_pairwise_distance",
     "dtw_cost_matrix",
@@ -61,6 +65,12 @@ __all__ = [
 ]
 
 
+from aeon.distances._adtw import (
+    adtw_alignment_path,
+    adtw_cost_matrix,
+    adtw_distance,
+    adtw_pairwise_distance,
+)
 from aeon.distances._bounding_matrix import create_bounding_matrix
 from aeon.distances._ddtw import (
     ddtw_alignment_path,
diff --git a/aeon/distances/_adtw.py b/aeon/distances/_adtw.py
new file mode 100644
index 00000000..e5f82f8f
--- /dev/null
+++ b/aeon/distances/_adtw.py
@@ -0,0 +1,372 @@
+# -*- coding: utf-8 -*-
+r"""Amercing dynamic time warping (ADTW) between two time series."""
+__author__ = ["chrisholder", "TonyBagnall"]
+
+from typing import List, Tuple
+
+import numpy as np
+from numba import njit
+
+from aeon.distances._alignment_paths import compute_min_return_path
+from aeon.distances._bounding_matrix import create_bounding_matrix
+from aeon.distances._squared import _univariate_squared_distance
+from aeon.distances._utils import reshape_pairwise_to_multiple
+
+
+@njit(cache=True, fastmath=True)
+def adtw_distance(
+    x: np.ndarray,
+    y: np.ndarray,
+    window: float = None,
+    itakura_max_slope: float = None,
+    warp_penalty: float = 1.0,
+) -> float:
+    r"""Compute the ADTW distance between two time series.
+
+    Amercing Dynamic Time Warping (ADTW) [1]_ is a variant of DTW that uses a
+    explicit warping penalty to encourage or discourage warping. The warping
+    penalty is a constant value that is added to the cost of warping. A high
+    value will encourage the algorithm to warp less and if the value is low warping
+    is more likely.
+
+    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)``.
+    window : float or None, default=None
+        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.
+    warp_penalty: float, default=1.0
+        Penalty for warping. A high value will mean less warping.
+
+    Returns
+    -------
+    float
+        ADTW distance between x and y, minimum value 0.
+
+    Raises
+    ------
+    ValueError
+        If x and y are not 1D or 2D arrays.
+
+    References
+    ----------
+    .. [1] Matthieu Herrmann, Geoffrey I. Webb: Amercing: An intuitive and
+    effective constraint for dynamic time warping, Pattern Recognition,
+    Volume 137, 2023.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from aeon.distances import adtw_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])
+    >>> adtw_distance(x, y) # 1D series
+    783.0
+    >>> x = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [0, 1, 0, 2, 0]])
+    >>> y = np.array([[11, 12, 13, 14],[7, 8, 9, 20],[1, 3, 4, 5]] )
+    >>> adtw_distance(x, y) # 2D series with 3 channels, unequal length
+    565.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, itakura_max_slope
+        )
+        return _adtw_distance(_x, _y, bounding_matrix, warp_penalty)
+    if x.ndim == 2 and y.ndim == 2:
+        bounding_matrix = create_bounding_matrix(
+            x.shape[1], y.shape[1], window, itakura_max_slope
+        )
+        return _adtw_distance(x, y, bounding_matrix, warp_penalty)
+    raise ValueError("x and y must be 1D or 2D")
+
+
+@njit(cache=True, fastmath=True)
+def adtw_cost_matrix(
+    x: np.ndarray,
+    y: np.ndarray,
+    window: float = None,
+    itakura_max_slope: float = None,
+    warp_penalty: float = 1.0,
+) -> np.ndarray:
+    r"""Compute the ADTW cost matrix between two time series.
+
+    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)``.
+    window : float, default=None
+        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``,
+        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.
+    warp_penalty: float, default=1.0
+        Penalty for warping. A high value will mean less warping.
+
+    Returns
+    -------
+    np.ndarray (n_timepoints, m_timepoints)
+        adtw cost matrix between x and y.
+
+    Raises
+    ------
+    ValueError
+        If x and y are not 1D or 2D arrays.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from aeon.distances import adtw_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]])
+    >>> adtw_cost_matrix(x, y)
+    array([[  0.,   2.,   7.,  17.,  34.,  60.,  97., 147., 212., 294.],
+           [  2.,   0.,   2.,   7.,  17.,  34.,  60.,  97., 147., 212.],
+           [  7.,   2.,   0.,   2.,   7.,  17.,  34.,  60.,  97., 147.],
+           [ 17.,   7.,   2.,   0.,   2.,   7.,  17.,  34.,  60.,  97.],
+           [ 34.,  17.,   7.,   2.,   0.,   2.,   7.,  17.,  34.,  60.],
+           [ 60.,  34.,  17.,   7.,   2.,   0.,   2.,   7.,  17.,  34.],
+           [ 97.,  60.,  34.,  17.,   7.,   2.,   0.,   2.,   7.,  17.],
+           [147.,  97.,  60.,  34.,  17.,   7.,   2.,   0.,   2.,   7.],
+           [212., 147.,  97.,  60.,  34.,  17.,   7.,   2.,   0.,   2.],
+           [294., 212., 147.,  97.,  60.,  34.,  17.,   7.,   2.,   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, itakura_max_slope
+        )
+        return _adtw_cost_matrix(_x, _y, bounding_matrix, warp_penalty)
+    if x.ndim == 2 and y.ndim == 2:
+        bounding_matrix = create_bounding_matrix(
+            x.shape[1], y.shape[1], window, itakura_max_slope
+        )
+        return _adtw_cost_matrix(x, y, bounding_matrix, warp_penalty)
+    raise ValueError("x and y must be 1D or 2D")
+
+
+@njit(cache=True, fastmath=True)
+def _adtw_distance(
+    x: np.ndarray, y: np.ndarray, bounding_matrix: np.ndarray, warp_penalty: float
+) -> float:
+    return _adtw_cost_matrix(x, y, bounding_matrix, warp_penalty)[
+        x.shape[1] - 1, y.shape[1] - 1
+    ]
+
+
+@njit(cache=True, fastmath=True)
+def _adtw_cost_matrix(
+    x: np.ndarray, y: np.ndarray, bounding_matrix: np.ndarray, warp_penalty: float
+) -> np.ndarray:
+    x_size = x.shape[1]
+    y_size = y.shape[1]
+    cost_matrix = np.full((x_size + 1, y_size + 1), np.inf)
+    cost_matrix[0, 0] = 0.0
+
+    for i in range(x_size):
+        for j in range(y_size):
+            if bounding_matrix[i, j]:
+                cost_matrix[i + 1, j + 1] = _univariate_squared_distance(
+                    x[:, i], y[:, j]
+                ) + min(
+                    cost_matrix[i, j + 1] + warp_penalty,
+                    cost_matrix[i + 1, j] + warp_penalty,
+                    cost_matrix[i, j],
+                )
+
+    return cost_matrix[1:, 1:]
+
+
+@njit(cache=True, fastmath=True)
+def adtw_pairwise_distance(
+    X: np.ndarray,
+    y: np.ndarray = None,
+    window: float = None,
+    itakura_max_slope: float = None,
+    warp_penalty: float = 1.0,
+) -> np.ndarray:
+    r"""Compute the ADTW pairwise distance between a set of time series.
+
+    Parameters
+    ----------
+    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)``.
+    window : float or None, default=None
+        The window to use for the bounding matrix. If None, no bounding matrix
+        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.
+    warp_penalty: float, default=1.0
+        Penalty for warping. A high value will mean less warping.
+        warp less and if value is low then will encourage algorithm to warp
+        more.
+
+    Returns
+    -------
+    np.ndarray
+        ADTW pairwise matrix between the instances of X of shape
+        ``(n_instances, n_instances)`` or between X and y of shape ``(n_instances,
+        n_instances)``.
+
+    Raises
+    ------
+    ValueError
+        If X is not 2D or 3D array and if y is not 1D, 2D or 3D arrays when passing y.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from aeon.distances import adtw_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]]])
+    >>> adtw_pairwise_distance(X)
+    array([[  0.,  27., 108.],
+           [ 27.,   0.,  27.],
+           [108.,  27.,   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]]])
+    >>> adtw_pairwise_distance(X, y)
+    array([[300., 507., 768.],
+           [147., 300., 507.],
+           [ 48., 147., 300.]])
+
+    >>> 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]])
+    >>> adtw_pairwise_distance(X, y_univariate)
+    array([[300.],
+           [147.],
+           [ 48.]])
+    """
+    if y is None:
+        # To self
+        if X.ndim == 3:
+            return _adtw_pairwise_distance(X, window, itakura_max_slope, warp_penalty)
+        if X.ndim == 2:
+            _X = X.reshape((X.shape[0], 1, X.shape[1]))
+            return _adtw_pairwise_distance(_X, window, itakura_max_slope, warp_penalty)
+        raise ValueError("x and y must be 2D or 3D arrays")
+    _x, _y = reshape_pairwise_to_multiple(X, y)
+    return _adtw_from_multiple_to_multiple_distance(
+        _x, _y, window, itakura_max_slope, warp_penalty
+    )
+
+
+@njit(cache=True, fastmath=True)
+def _adtw_pairwise_distance(
+    X: np.ndarray, window: float, itakura_max_slope: float, warp_penalty: float
+) -> 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, itakura_max_slope
+    )
+
+    for i in range(n_instances):
+        for j in range(i + 1, n_instances):
+            distances[i, j] = _adtw_distance(X[i], X[j], bounding_matrix, warp_penalty)
+            distances[j, i] = distances[i, j]
+
+    return distances
+
+
+@njit(cache=True, fastmath=True)
+def _adtw_from_multiple_to_multiple_distance(
+    x: np.ndarray,
+    y: np.ndarray,
+    window: float,
+    itakura_max_slope: float,
+    warp_penalty: float,
+) -> 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, itakura_max_slope
+    )
+
+    for i in range(n_instances):
+        for j in range(m_instances):
+            distances[i, j] = _adtw_distance(x[i], y[j], bounding_matrix, warp_penalty)
+    return distances
+
+
+@njit(cache=True, fastmath=True)
+def adtw_alignment_path(
+    x: np.ndarray,
+    y: np.ndarray,
+    window: float = None,
+    itakura_max_slope: float = None,
+    warp_penalty: float = 1.0,
+) -> Tuple[List[Tuple[int, int]], float]:
+    """Compute the ADTW alignment path between two time series.
+
+    Parameters
+    ----------
+    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.
+    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.
+    warp_penalty: float, default=1.0
+        Penalty for warping. A high value will mean less warping.
+
+    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 ADTW distance betweeen the two time series.
+
+    Raises
+    ------
+    ValueError
+        If x and y are not 1D or 2D arrays.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from aeon.distances import adtw_alignment_path
+    >>> x = np.array([[1, 2, 3, 6]])
+    >>> y = np.array([[1, 2, 3, 4]])
+    >>> adtw_alignment_path(x, y)
+    ([(0, 0), (1, 1), (2, 2), (3, 3)], 4.0)
+    """
+    cost_matrix = adtw_cost_matrix(x, y, window, itakura_max_slope, warp_penalty)
+    return (
+        compute_min_return_path(cost_matrix),
+        cost_matrix[x.shape[-1] - 1, y.shape[-1] - 1],
+    )
diff --git a/aeon/distances/_distance.py b/aeon/distances/_distance.py
index 37c7e2b6..e879e4c7 100644
--- a/aeon/distances/_distance.py
+++ b/aeon/distances/_distance.py
@@ -5,6 +5,12 @@ from typing import Any, Callable, List, Tuple, Union
 
 import numpy as np
 
+from aeon.distances._adtw import (
+    adtw_alignment_path,
+    adtw_cost_matrix,
+    adtw_distance,
+    adtw_pairwise_distance,
+)
 from aeon.distances._ddtw import (
     ddtw_alignment_path,
     ddtw_cost_matrix,
@@ -205,6 +211,14 @@ def distance(
         )
     elif metric == "mpdist":
         return mpdist(x, y, **kwargs)
+    elif metric == "adtw":
+        return adtw_distance(
+            x,
+            y,
+            kwargs.get("window"),
+            kwargs.get("itakura_max_slope"),
+            kwargs.get("warp_penalty", 1.0),
+        )
     else:
         if isinstance(metric, Callable):
             return metric(x, y, **kwargs)
@@ -357,6 +371,14 @@ def pairwise_distance(
         )
     elif metric == "mpdist":
         return _custom_func_pairwise(x, y, mpdist, **kwargs)
+    elif metric == "adtw":
+        return adtw_pairwise_distance(
+            x,
+            y,
+            kwargs.get("window"),
+            kwargs.get("itakura_max_slope"),
+            kwargs.get("warp_penalty", 1.0),
+        )
     else:
         if isinstance(metric, Callable):
             return _custom_func_pairwise(x, y, metric, **kwargs)
@@ -530,6 +552,14 @@ def alignment_path(
             kwargs.get("c", 1.0),
             kwargs.get("itakura_max_slope"),
         )
+    elif metric == "adtw":
+        return adtw_alignment_path(
+            x,
+            y,
+            kwargs.get("window"),
+            kwargs.get("itakura_max_slope"),
+            kwargs.get("warp_penalty", 1.0),
+        )
     else:
         raise ValueError("Metric must be one of the supported strings")
 
@@ -661,6 +691,14 @@ def cost_matrix(
             kwargs.get("c", 1.0),
             kwargs.get("itakura_max_slope"),
         )
+    elif metric == "adtw":
+        return adtw_cost_matrix(
+            x,
+            y,
+            kwargs.get("window"),
+            kwargs.get("itakura_max_slope"),
+            kwargs.get("warp_penalty", 1.0),
+        )
     else:
         raise ValueError("Metric must be one of the supported strings")
 
@@ -676,6 +714,7 @@ def get_distance_function(metric: Union[str, DistanceFunction]) -> DistanceFunct
     'ddtw'          distance.ddtw_distance
     'wdtw'          distance.wdtw_distance
     'wddtw'         distance.wddtw_distance
+    'adtw'          distance.adtw_distance
     'erp'           distance.erp_distance
     'edr'           distance.edr_distance
     'msm'           distance.msm_distance
@@ -729,6 +768,7 @@ def get_pairwise_distance_function(
     'ddtw'          distance.ddtw_pairwise_distance
     'wdtw'          distance.wdtw_pairwise_distance
     'wddtw'         distance.wddtw_pairwise_distance
+    'adtw'          distance.adtw_pairwise_distance
     'erp'           distance.erp_pairwise_distance
     'edr'           distance.edr_pairwise_distance
     'msm'           distance.msm_pairiwse_distance
@@ -783,6 +823,7 @@ def get_alignment_path_function(metric: str) -> AlignmentPathFunction:
     'ddtw'          distance.ddtw_alignment_path
     'wdtw'          distance.wdtw_alignment_path
     'wddtw'         distance.wddtw_alignment_path
+    'adtw'          distance.adtw_alignment_path
     'erp'           distance.erp_alignment_path
     'edr'           distance.edr_alignment_path
     'msm'           distance.msm_alignment_path
@@ -830,6 +871,7 @@ def get_cost_matrix_function(metric: str) -> CostMatrixFunction:
     'ddtw'          distance.ddtw_cost_matrix
     'wdtw'          distance.wdtw_cost_matrix
     'wddtw'         distance.wddtw_cost_matrix
+    'adtw'          distance.adtw_cost_matrix
     'erp'           distance.erp_cost_matrix
     'edr'           distance.edr_cost_matrix
     'msm'           distance.msm_cost_matrix
@@ -956,6 +998,13 @@ DISTANCES = [
         "cost_matrix": msm_cost_matrix,
         "alignment_path": msm_alignment_path,
     },
+    {
+        "name": "adtw",
+        "distance": adtw_distance,
+        "pairwise_distance": adtw_pairwise_distance,
+        "cost_matrix": adtw_cost_matrix,
+        "alignment_path": adtw_alignment_path,
+    },
     {
         "name": "shape_dtw",
         "distance": shape_dtw_distance,


Test patch:
diff --git a/aeon/clustering/metrics/tests/test_dba.py b/aeon/clustering/metrics/tests/test_dba.py
index 8a42d4cd..3a2fd127 100644
--- a/aeon/clustering/metrics/tests/test_dba.py
+++ b/aeon/clustering/metrics/tests/test_dba.py
@@ -143,7 +143,18 @@ def test_dba():
 
 
 def test_elastic_dba_variations():
-    distances = ["dtw", "ddtw", "wdtw", "wddtw", "erp", "edr", "twe", "msm"]
+    distances = [
+        "dtw",
+        "ddtw",
+        "wdtw",
+        "wddtw",
+        "erp",
+        "edr",
+        "twe",
+        "msm",
+        "shape_dtw",
+        "adtw",
+    ]
     for dist in distances:
         X_train = create_test_distance_numpy(10, 10, 10)
 
diff --git a/aeon/distances/tests/_expected_results.py b/aeon/distances/tests/_expected_results.py
index e9751aa0..40cad787 100644
--- a/aeon/distances/tests/_expected_results.py
+++ b/aeon/distances/tests/_expected_results.py
@@ -77,6 +77,13 @@ _expected_distance_results = {
         12.461233755089522,
         36.0265974253265,
     ],
+    "adtw": [
+        25.0,
+        5.163699519307769,
+        49.59969618998147,
+        9.360373075383167,
+        49.86527164702194,
+    ],
 }
 
 _expected_distance_results_params = {
@@ -128,4 +135,9 @@ _expected_distance_results_params = {
         [6.5373486105638134, 50.88045781961115],
         [3.6922226886554435, 38.923480043064245],
     ],
+    "adtw": [
+        [5.163699519307769, 49.59969618998147],
+        [5.231335579254712, 50.31911284774053],
+        [6.932617853961479, 50.31911284774053],
+    ],
 }
diff --git a/aeon/distances/tests/test_numba_distance_parameters.py b/aeon/distances/tests/test_numba_distance_parameters.py
index ca27910e..648840a7 100644
--- a/aeon/distances/tests/test_numba_distance_parameters.py
+++ b/aeon/distances/tests/test_numba_distance_parameters.py
@@ -67,6 +67,7 @@ DIST_PARAMS = {
     "wddtw": BASIC_BOUNDING_PARAMS + [{"g": 1.0}],
     "twe": BASIC_BOUNDING_PARAMS + [{"lmbda": 0.5}, {"nu": 0.9}],
     "msm": BASIC_BOUNDING_PARAMS + [{"independent": False}, {"c": 0.1}],
+    "adtw": BASIC_BOUNDING_PARAMS + [{"warp_penalty": 5.0}],
 }