Repository: sktime/sktime

Issue:
[BUG] Unusual if statement in _lower_bounding_numba.py
**Describe the bug**
<!--
A clear and concise description of what the bug is.
--> If statement with same code in both branches

**To Reproduce**
<!--
Add a Minimal, Complete, and Verifiable example (for more details, see e.g. https://stackoverflow.com/help/mcve

If the code is too long, feel free to put it in a public gist and link it in the issue: https://gist.github.com
--> See def create_shape_on_matrix, specifically lines 63-68 


**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
--> In the else statement, I would expect ceil and floor to be exchanged


**Versions**
0.27.0

<!--
Please run the following code snippet and paste the output here:

from sktime import show_versions; show_versions()
-->
System:
    python: 3.12.0 (v3.12.0:0fb18b02c8, Oct  2 2023, 09:45:56) [Clang 13.0.0 (clang-1300.0.29.30)]
executable: /path
   machine: macOS-14.4-arm64-arm-64bit

Python dependencies:
          pip: 24.0
       sktime: 0.27.0
      sklearn: 1.4.1.post1
       skbase: 0.7.5
        numpy: 1.26.4
        scipy: 1.12.0
       pandas: 2.1.4
   matplotlib: 3.8.3
       joblib: 1.3.2
        numba: 0.59.0
  statsmodels: None
     pmdarima: None
statsforecast: None
      tsfresh: None
      tslearn: None
        torch: None
   tensorflow: None
tensorflow_probability: None
Backend MacOSX is interactive backend. Turning interactive mode on.

</details>

<!-- Thanks for contributing! -->



Discussion:
Thanks for reporting, @albertoazzari!

What I can easily verify is that the if/else statement in its current form is redundant, so there is a bug.

What I don't get - and I also have to say, I do no fully understand the specification of the function (the docstring is incomplete), why floor/ceil should be switched after the halfway point. Explanation would be appreciated.

Also, feel free to open a PR - if you do that, improvement of the docstring would also be appreciated, e.g., saying in math or more precisely what this function is doing to the bounding matrix.
Thanks @fkiraly for the quick answer, my claim for the else branch is somewhat a speculation.  In the last few days, I read the paper for the sakoe-chiba band (see https://ieeexplore.ieee.org/document/1163055) and I did not find any clarification about the change after the halfway point. It could be useful maybe try to understand how different implementations handle or do not handle this problem.
Now this may be just me, but I don't see any indication in the Sakoe/Chiba paper that would even hint at doing the case distinction.

We could simply remove if/else and replace it by one of the identical branches, that also wouldn't change the logic.
I agree.
Why did you then say:

> In the else statement, I would expect ceil and floor to be exchanged 

Could you explain?
As I told you in a previous message, it was just an assumption given by the presence of the if/else statement in the code. Still, as I wrote you in the last message, I agree with you that the best solution is to remove the if/else due to the absence in the paper of an explanation related to the halfway point case.
would you like to make the PR?

Resolving patch:
diff --git a/sktime/distances/_lower_bounding_numba.py b/sktime/distances/_lower_bounding_numba.py
--- a/sktime/distances/_lower_bounding_numba.py
+++ b/sktime/distances/_lower_bounding_numba.py
@@ -55,26 +55,16 @@ def create_shape_on_matrix(
             "values"
         )
 
-    half_way = math.floor(upper_line_y_values / 2)
-
     for i in range(start_val, upper_line_y_values):
         x = i * x_step_size
 
-        if i > half_way:
-            upper_y = max(0, min(y_size - 1, math.ceil(y_upper_line[i])))
-            lower_y = max(0, min(y_size - 1, math.floor(y_lower_line[i])))
-        else:
-            upper_y = max(0, min(y_size - 1, math.ceil(y_upper_line[i])))
-            lower_y = max(0, min(y_size - 1, math.floor(y_lower_line[i])))
-
-        if upper_line_y_values == lower_line_y_values:
-            if upper_y == lower_y:
-                bounding_matrix[upper_y, x] = 0.0
-            else:
-                bounding_matrix[upper_y : (lower_y + 1), x] = 0.0
-        else:
+        upper_y = max(0, min(y_size - 1, math.ceil(y_upper_line[i])))
+        lower_y = max(0, min(y_size - 1, math.floor(y_lower_line[i])))
+
+        if upper_y == lower_y:
             bounding_matrix[upper_y, x] = 0.0
-            bounding_matrix[lower_y, x] = 0.0
+        else:
+            bounding_matrix[upper_y : (lower_y + 1), x] = 0.0
 
     return bounding_matrix
 


Test patch: