Repository: sktime/sktime

Issue:
[BUG] ClaSPSegmentation sometimes causes segmentation faults
**Describe the bug**
<!--
A clear and concise description of what the bug is.
-->

The `predict_scores` function in the `ClaSPSegmentation` algorithm sometimes causes a segmentation fault.

**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
-->

Suppose we have this python script called `main.py`.
```python
# main.py
import numpy as np
from sktime.annotation import clasp

data = np.array([
    -4.88766644, -4.94650199, -5.0013731,  -5.07699301, -4.8090572,  -5.02243262,
    -5.06190068, -5.036496,   -4.99698149, -4.91650067, -5.037595,   -4.89593437,
    -5.18017945, -5.03147768, -4.96460002, -5.07882177, -4.90128851, -4.89329415,
    -4.93704548, -5.08301928, -4.9478639,  -4.85836931, -4.99318617, -4.93284624,
    -4.95908828, -4.92954853, -4.9177114,  -5.13054821, -4.99285564, -4.9434356,
    -5.05020327, -5.03574802, -4.96580102, -4.90538156, -5.21520763, -5.28234695,
    -4.92255138, -5.1677956,  -4.99630143, -4.98356873, -5.02101731, -5.20573748,
    -5.01009681, -5.10857426, -5.04622259, -5.15655929, -5.0303795,  -5.04891457,
    -4.7889435,  -4.99531652,  4.91859431,  5.07871592,  4.91930235,  4.66571706,
     5.01479832,  4.9631594,   5.0613015,   5.13741069,  5.17634528,  4.84588181,
     5.07359443,  5.14543124,  4.84981414,  5.06021783,  4.85828366,  4.86481576,
     4.9936544,   5.06856781,  4.98766012,  5.00303655,  4.85387857,  5.02492314,
     4.89012454,  5.03577498,  5.0334031,   4.96262505,  5.21961174,  4.98566746,
     5.07385143,  5.17354887,  4.88847377,  4.99087296,  4.95405063,  5.15365623,
     4.92825306,  4.92045917,  4.91300356,  5.0840935,   5.01251463,  5.1022592,
     5.12099528,  5.12232648,  5.0762803,   4.99285947,  5.11243604,  5.03798832,
     5.06287038,  5.03328841,  4.90856316,  4.90418496
])

model_clasp = clasp.ClaSPSegmentation(n_cps=2)
model_clasp.fit(data)
```
Now run the script and call `predict_scores`.
```
$ python3 -i main.py
>>> model_clasp.predict_scores(data)
Segmentation fault 
```
The segmentation fault does not happen if `n_cps=1`. Here is an updated `main.py` script.
```python
# main.py
import numpy as np
from sktime.annotation import clasp

data = np.array([
    -4.88766644, -4.94650199, -5.0013731,  -5.07699301, -4.8090572,  -5.02243262,
    -5.06190068, -5.036496,   -4.99698149, -4.91650067, -5.037595,   -4.89593437,
    -5.18017945, -5.03147768, -4.96460002, -5.07882177, -4.90128851, -4.89329415,
    -4.93704548, -5.08301928, -4.9478639,  -4.85836931, -4.99318617, -4.93284624,
    -4.95908828, -4.92954853, -4.9177114,  -5.13054821, -4.99285564, -4.9434356,
    -5.05020327, -5.03574802, -4.96580102, -4.90538156, -5.21520763, -5.28234695,
    -4.92255138, -5.1677956,  -4.99630143, -4.98356873, -5.02101731, -5.20573748,
    -5.01009681, -5.10857426, -5.04622259, -5.15655929, -5.0303795,  -5.04891457,
    -4.7889435,  -4.99531652,  4.91859431,  5.07871592,  4.91930235,  4.66571706,
     5.01479832,  4.9631594,   5.0613015,   5.13741069,  5.17634528,  4.84588181,
     5.07359443,  5.14543124,  4.84981414,  5.06021783,  4.85828366,  4.86481576,
     4.9936544,   5.06856781,  4.98766012,  5.00303655,  4.85387857,  5.02492314,
     4.89012454,  5.03577498,  5.0334031,   4.96262505,  5.21961174,  4.98566746,
     5.07385143,  5.17354887,  4.88847377,  4.99087296,  4.95405063,  5.15365623,
     4.92825306,  4.92045917,  4.91300356,  5.0840935,   5.01251463,  5.1022592,
     5.12099528,  5.12232648,  5.0762803,   4.99285947,  5.11243604,  5.03798832,
     5.06287038,  5.03328841,  4.90856316,  4.90418496
])

model_clasp = clasp.ClaSPSegmentation(n_cps=1)
model_clasp.fit(data)
model_clasp.predict_scores(data)
print("Ran without a segmentation fault.")
```
This script runs without a segmentation fault.
```
$ python3 main.py
Ran without segmentation fault.
```

**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->

`predict_scores` should not cause a segmentation fault.

**Additional context**
<!--
Add any other context about the problem here.
-->

The function I used the generate `data` was,
```
data = np.concatenate([np.random.normal(-5, 0.1, 50), np.random.normal(5, 0.1, 50)])
```

**Versions**

<!--
Please run the following code snippet and paste the output here:

from sktime import show_versions; show_versions()
-->
Here are my versions.

```python3
>>> from sktime import show_versions; show_versions()

System:
    python: 3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0]
executable: /home/alex/documents/sktime/.venv/bin/python3
   machine: Linux-5.15.90.1-microsoft-standard-WSL2-x86_64-with-glibc2.35

Python dependencies:
          pip: 22.0.2
       sktime: 0.24.0
      sklearn: 1.3.1
       skbase: 0.6.0
        numpy: 1.26.1
        scipy: 1.11.3
       pandas: 2.1.1
   matplotlib: 3.8.0
       joblib: 1.3.2
        numba: 0.58.1
  statsmodels: None
     pmdarima: None
statsforecast: None
      tsfresh: None
      tslearn: None
        torch: None
   tensorflow: None
tensorflow_probability: None
```

<!-- Thanks for contributing! -->



Discussion:
I suppose that's why it is considered a segmentation algorithm.

I think this is related to https://github.com/sktime/sktime/issues/3103, where it was not clear which algorithm to attribute the segmentation faults to. How did you narrow it down to clasp?

My guess is, local testing during interface refactor?
> How did you narrow it down to clasp?

I was looking at the `_predict_scores` function in the `ClaSPSegmentation` class to see what it did and whether something similar should be implemented in the `GreedyGaussianSegmentation` class.

When I was fitting `ClaSPSegmentation` to some data, I noticed that it sometimes caused a segmentation fault and through trial and error found that `predict_scores` seemed to be the problem.
Ah, interesting - and strange.

Design-wise, computational bottlenecks should be located in `fit`, not the `predict` methods.

So, deeper down, this might tell us something about valid API designs for annotation. Of course it could be entirely specific to the implementation.
I think the issues might be due to accessing an index outside the valid range of an array.

Suppose we have the following `main.py` script.

```python
import numpy as np
from sktime.transformations.series import _clasp_numba

knn_mask = np.array([
    [0, 0, 0, 0],
    [1, 1, 1, 1],
    [2, 2, 2, 2],
])
split_idx = 0
m = 10
_clasp_numba._calc_knn_labels(knn_mask=knn_mask, split_idx=split_idx, m=m)
```
Running this script gives a segmentation fault.
```
$ python3 main.py
Segmentation fault
```
Numba seems to cause the segmentation fault. By commenting out the `@njit(fastmath=True, cache=True)` decorator on the `_calc_knn_labels` method we get a more helpful error message.
```python

$ python3 main.py
Traceback (most recent call last):
  File "/home/alex/documents/sktime/main.py", line 37, in <module>
    _clasp_numba._calc_knn_labels(knn_mask=knn_mask, split_idx=split_idx, m=m)
  File "/home/alex/documents/sktime/sktime/transformations/series/_clasp_numba.py", line 166, in _calc_knn_labels
    y_pred[exclusion_zone] = np.ones(m, dtype=np.int64)
IndexError: index -10 is out of bounds for axis 0 with size 4
```
Going a little further, the error happens on this line,
 https://github.com/sktime/sktime/blob/f8c31e7481c634574fe10ed852672f20b42daf57/sktime/transformations/series/_clasp_numba.py#L164

Where `y_pred` is `array([1, 1, 1, 1])` and `exclusion_zone` is `array([-10,  -9,  -8,  -7,  -6,  -5,  -4,  -3,  -2,  -1])`. So, the integers in `exclusion_zone` are out of range for `y_pred`.

I am not familiar with the Clasp algorithm so it is not obvious to me what changes need to be made here.
> I am not familiar with the Clasp algorithm so it is not obvious to me what changes need to be made here.

I think this is simply a detection buffer.

The fix would be, intersect `exclusion_zone` with available indices in `y_pred`.

Resolving patch:
diff --git a/sktime/transformations/series/_clasp_numba.py b/sktime/transformations/series/_clasp_numba.py
--- a/sktime/transformations/series/_clasp_numba.py
+++ b/sktime/transformations/series/_clasp_numba.py
@@ -161,7 +161,13 @@ def _calc_knn_labels(knn_mask, split_idx, m):
 
     # apply exclusion zone at split point
     exclusion_zone = np.arange(split_idx - m, split_idx)
-    y_pred[exclusion_zone] = np.ones(m, dtype=np.int64)
+
+    # Remove indexes outside the range of y_pred
+    exclusion_zone = exclusion_zone[
+        (exclusion_zone >= -len(y_pred)) & (exclusion_zone < len(y_pred))
+    ]
+
+    y_pred[exclusion_zone] = np.ones(len(exclusion_zone), dtype=np.int64)
 
     return y_true, y_pred
 


Test patch: