Repository: aeon-toolkit/aeon (python)

Issue:
[ENH] Itakura parallelogram unequal length support
### Describe the feature or idea you want to propose

Currently Itakura parallelogram doesn't support unequal length time series. It would be nice make it so it does.

### Describe your proposed solution

Current a error is being thrown as when you call the function with unequal length it returns a bounding of all false.
```
@njit(cache=True)
def _itakura_parallelogram(x_size: int, y_size: int, max_slope_percent: float):
    """Itakura parallelogram bounding matrix.

    This code was adapted from tslearn. This link to the original code line 974:
    https://github.com/tslearn-team/tslearn/blob/main/tslearn/metrics/dtw_variants.py
    """
    if x_size != y_size:
        raise ValueError(
            """Itakura parallelogram does not support unequal length time series.
Please consider using a full bounding matrix or a sakoe chiba bounding matrix
instead."""
        )
```

### Describe alternatives you've considered, if relevant

_No response_

### Additional context

_No response_

Pull request:
[ENH] Support for unequal length in itakura parallelogram
#### Reference Issues/PRs

Fixes #1300 

#### What does this implement/fix? Explain your changes.
Adds support for unequal length in itakura parallelogram earlier this would lead to `ValueError`
Also added the tests for the same.

#### Does your contribution introduce a new dependency? If yes, which one?


#### Any other comments?
Compared the results with an alternate implementation(pyts **code link**: https://pyts.readthedocs.io/en/latest/_modules/pyts/metrics/dtw.html#itakura_parallelogram)

1st case is (7,5) bounding matrix
2nd case is (5,7) bounding matrix
![image](https://github.com/user-attachments/assets/7c911b02-2317-47f2-bab2-4950bf5fa4e1)


### PR checklist



##### For all contributions
- [x] I've added myself to the [list of contributors](https://github.com/aeon-toolkit/aeon/blob/main/.all-contributorsrc). Alternatively, you can use the [@all-contributors](https://allcontributors.org/docs/en/bot/usage) bot to do this for you after the PR has been merged.
- [x] The PR title starts with either [ENH], [MNT], [DOC], [BUG], [REF], [DEP] or [GOV] indicating whether the PR topic is related to enhancement, maintenance, documentation, bugs, refactoring, deprecation or governance.

##### For new estimators and functions
- [ ] I've added the estimator/function to the online [API documentation](https://www.aeon-toolkit.org/en/latest/api_reference.html).
- [ ] (OPTIONAL) I've added myself as a `__maintainer__` at the top of relevant files and want to be contacted regarding its maintenance. Unmaintained files may be removed. This is for the full file, and you should not add yourself if you are just making minor changes or do not want to help maintain its contents.

##### For developers with write access
- [ ] (OPTIONAL) I've updated aeon's [CODEOWNERS](https://github.com/aeon-toolkit/aeon/blob/main/CODEOWNERS) to receive notifications about future changes to these files.



Patch:
diff --git a/aeon/distances/elastic/_bounding_matrix.py b/aeon/distances/elastic/_bounding_matrix.py
index 2b710b97..509dff6e 100644
--- a/aeon/distances/elastic/_bounding_matrix.py
+++ b/aeon/distances/elastic/_bounding_matrix.py
@@ -63,44 +63,61 @@ def create_bounding_matrix(
 def _itakura_parallelogram(x_size: int, y_size: int, max_slope_percent: float):
     """Itakura parallelogram bounding matrix.
 
-    This code was adapted from tslearn. This link to the original code line 974:
-    https://github.com/tslearn-team/tslearn/blob/main/tslearn/metrics/dtw_variants.py
+    This code was adapted from pyts. This link to the original code:
+    https://pyts.readthedocs.io/en/latest/_modules/pyts/metrics/dtw.html#itakura_parallelogram
     """
-    if x_size != y_size:
-        raise ValueError(
-            """Itakura parallelogram does not support unequal length time series.
-Please consider using a full bounding matrix or a sakoe chiba bounding matrix
-instead."""
-        )
     one_percent = min(x_size, y_size) / 100
     max_slope = math.floor((max_slope_percent * one_percent) * 100)
     min_slope = 1 / float(max_slope)
-    max_slope *= float(x_size) / float(y_size)
-    min_slope *= float(x_size) / float(y_size)
-
-    lower_bound = np.empty((2, y_size))
-    lower_bound[0] = min_slope * np.arange(y_size)
-    lower_bound[1] = (
-        (x_size - 1) - max_slope * (y_size - 1) + max_slope * np.arange(y_size)
-    )
-    lower_bound_ = np.empty(y_size)
-    for i in range(y_size):
-        lower_bound_[i] = max(round(lower_bound[0, i], 2), round(lower_bound[1, i], 2))
-    lower_bound_ = np.ceil(lower_bound_)
-
-    upper_bound = np.empty((2, y_size))
-    upper_bound[0] = max_slope * np.arange(y_size)
-    upper_bound[1] = (
-        (x_size - 1) - min_slope * (y_size - 1) + min_slope * np.arange(y_size)
-    )
-    upper_bound_ = np.empty(y_size)
-    for i in range(y_size):
-        upper_bound_[i] = min(round(upper_bound[0, i], 2), round(upper_bound[1, i], 2))
-    upper_bound_ = np.floor(upper_bound_ + 1)
-
-    bounding_matrix = np.full((x_size, y_size), False)
-    for i in range(y_size):
-        bounding_matrix[int(lower_bound_[i]) : int(upper_bound_[i]), i] = True
+    max_slope *= float(y_size - 1) / float(x_size - 2)
+    max_slope = max(max_slope, 1.0)
+
+    min_slope *= float(y_size - 2) / float(x_size - 1)
+    min_slope = min(min_slope, 1.0)
+
+    centered_scale = np.arange(x_size) - x_size + 1
+
+    lower_bound = np.empty(x_size, dtype=np.float64)
+    upper_bound = np.empty(x_size, dtype=np.float64)
+
+    for i in range(x_size):
+        lb0 = min_slope * i
+        lb1 = max_slope * centered_scale[i] + y_size - 1
+        lower_bound[i] = math.ceil(max(round(lb0, 2), round(lb1, 2)))
+
+        ub0 = max_slope * i + 1
+        ub1 = min_slope * centered_scale[i] + y_size
+        upper_bound[i] = math.floor(min(round(ub0, 2), round(ub1, 2)))
+
+    if max_slope == 1.0:
+        if y_size > x_size:
+            for i in range(x_size - 1):
+                upper_bound[i] = lower_bound[i + 1]
+        else:
+            for i in range(x_size):
+                upper_bound[i] = lower_bound[i] + 1
+
+    for i in range(x_size):
+        if lower_bound[i] < 0:
+            lower_bound[i] = 0
+        if lower_bound[i] > y_size:
+            lower_bound[i] = y_size
+        if upper_bound[i] < 0:
+            upper_bound[i] = 0
+        if upper_bound[i] > y_size:
+            upper_bound[i] = y_size
+
+    bounding_matrix = np.empty((x_size, y_size), dtype=np.bool_)
+    for i in range(x_size):
+        for j in range(y_size):
+            bounding_matrix[i, j] = False
+
+    for i in range(x_size):
+        start = int(lower_bound[i])
+        end = int(upper_bound[i])
+        for j in range(start, end):
+            bounding_matrix[i, j] = True
+
     return bounding_matrix
 
 


Test patch:
diff --git a/aeon/distances/elastic/tests/test_bounding.py b/aeon/distances/elastic/tests/test_bounding.py
index 32ac1987..f0c2d837 100644
--- a/aeon/distances/elastic/tests/test_bounding.py
+++ b/aeon/distances/elastic/tests/test_bounding.py
@@ -1,7 +1,6 @@
 """Test for bounding matrix."""
 
 import numpy as np
-import pytest
 
 from aeon.distances import create_bounding_matrix
 
@@ -37,10 +36,34 @@ def test_itakura_parallelogram():
     matrix = create_bounding_matrix(10, 10, itakura_max_slope=0.2)
     assert isinstance(matrix, np.ndarray)
 
-    with pytest.raises(
-        ValueError,
-        match="""Itakura parallelogram does not support unequal length time series.
-Please consider using a full bounding matrix or a sakoe chiba bounding matrix
-instead.""",
-    ):
-        create_bounding_matrix(5, 10, itakura_max_slope=0.2)
+    expected_result_5_7 = np.array(
+        [
+            [True, False, False, False, False, False, False],
+            [False, True, True, True, True, False, False],
+            [False, False, True, True, True, False, False],
+            [False, False, True, True, True, True, False],
+            [False, False, False, False, False, False, True],
+        ]
+    )
+
+    expected_result_7_5 = np.array(
+        [
+            [True, False, False, False, False],
+            [False, True, False, False, False],
+            [False, True, True, True, False],
+            [False, True, True, True, False],
+            [False, True, True, True, False],
+            [False, False, False, True, False],
+            [False, False, False, False, True],
+        ]
+    )
+
+    matrix = create_bounding_matrix(5, 7, itakura_max_slope=0.5)
+    assert isinstance(matrix, np.ndarray)
+    assert matrix.shape == (5, 7)
+    assert np.array_equal(matrix, expected_result_5_7)
+
+    matrix = create_bounding_matrix(7, 5, itakura_max_slope=0.5)
+    assert isinstance(matrix, np.ndarray)
+    assert matrix.shape == (7, 5)
+    assert np.array_equal(matrix, expected_result_7_5)