Repository: sktime/sktime (python)

Issue:
[BUG] Clasp Segmentation sometimes raises a ValueError
**Describe the bug**
<!--
A clear and concise description of what the bug is.
-->

The `predict_scores` method for the `ClaSPSegmentation` algorithm sometimes raises a value error.

**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
-->

Consider the following `main.py` script.

```python
# main.py
import numpy as np
from sktime.annotation import clasp

# Causes a value error kth(=3) out of bounds (3)
data = np.array([-4.93826793, -5.10968536, -4.94699538, -5.06644812, -5.09389618,
       -4.97855996, -5.03805906, -4.94288774, -4.93562747, -5.15704812,
       -4.98363671, -4.94730547, -4.95460821, -4.77100651, -4.97104642,
       -5.19168259, -4.86592807, -5.03451162, -4.8818181 , -4.96151397,
       -5.04680535, -5.10203082, -5.09094031, -5.14550955, -4.88810337,
       -4.98522959, -4.89865192, -5.17349344, -4.79173094, -4.98540751,
       -4.95218348, -4.92745787, -5.10828999, -5.018588  , -5.23138986,
       -5.0829429 , -4.95182016, -5.04750463, -5.04910479, -4.96474658,
       -4.87263583, -4.93261922, -4.88780183, -4.99625131, -5.04841316,
       -4.8784347 , -4.92589113, -5.23561638, -5.10026717, -4.98502644,
        4.95563698,  5.00165565,  5.02851673,  5.09061275,  5.03335048,
        5.01256648,  5.21727   ,  4.900725  ,  4.86488069,  5.03619925,
        4.84147019,  4.90967582,  4.94225031,  5.01319431,  4.88011974,
        5.03605379,  4.97238513,  4.9344612 ,  5.18930892,  4.90348493,
        4.83290085,  5.04300367,  4.92500575,  5.06299654,  4.83232756,
        4.94786228,  5.03254223,  4.86344949,  4.97827722,  5.03143399,
        5.1337682 ,  4.88999864,  5.16775253,  5.13377762,  5.09881714,
        5.21716965,  4.90141898,  4.94692338,  4.92881945,  4.99632702,
        5.14217334,  5.04286422,  5.02892189,  4.8142512 ,  4.91298724,
        4.97970406,  5.13247509,  4.97386383,  4.94512011,  4.91927367])

model_clasp = clasp.ClaSPSegmentation(n_cps=2)
model_clasp.fit(data)
```

Executing `predict_scores` using this script raises a value error.

```
python3 -i main.py
>>> model_clasp.predict_scores(data)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/alex/documents/sktime/sktime/annotation/base/_base.py", line 150, in predict_scores
    return self._predict_scores(X)
  File "/home/alex/documents/sktime/sktime/annotation/clasp.py", line 287, in _predict_scores
    self.found_cps, self.profiles, self.scores = self._run_clasp(X)
  File "/home/alex/documents/sktime/sktime/annotation/clasp.py", line 318, in _run_clasp
    self.found_cps, self.profiles, self.scores = _segmentation(
  File "/home/alex/documents/sktime/sktime/annotation/clasp.py", line 154, in _segmentation
    profile = clasp.transform(X[ranges])
  File "/home/alex/documents/sktime/sktime/transformations/base.py", line 583, in transform
    Xt = self._transform(X=X_inner, y=y_inner)
  File "/home/alex/documents/sktime/sktime/transformations/series/clasp.py", line 106, in _transform
    Xt, _ = clasp(
  File "/home/alex/documents/sktime/sktime/transformations/series/_clasp_numba.py", line 334, in clasp
    knn_mask = _compute_distances_iterative(X, m, k_neighbours).T
  File "/home/alex/documents/sktime/sktime/transformations/series/_clasp_numba.py", line 114, in _compute_distances_iterative
    idx = np.argpartition(dist, k)
  File "/home/alex/documents/sktime/.venv/lib/python3.10/site-packages/numpy/core/fromnumeric.py", line 858, in argpartition
    return _wrapfunc(a, 'argpartition', kth, axis=axis, kind=kind, order=order)
  File "/home/alex/documents/sktime/.venv/lib/python3.10/site-packages/numpy/core/fromnumeric.py", line 59, in _wrapfunc
    return bound(*args, **kwds)
ValueError: kth(=3) out of bounds (3)
```
From debugging, the error seems to come from `_compute_distances_iterative` in `_clasp_number.py` on line 53.

https://github.com/sktime/sktime/blob/f48b1f53f58004bb792abf78ce6818034835d3c2/sktime/transformations/series/_clasp_numba.py#L114

In the example, `k` is `3` and `dist` is `array([inf, inf, inf])`. Since `dist` has length 3, then `k` is out of range. I think this could be solved with some simple bound checking where if `k` is greater than or equal to the length of `dist` then set `k` to the length of `dist` minus 1.

**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->

Using `predict_scores` should not raise a value error.

**Additional context**
<!--
Add any other context about the problem here.
-->

This bug happened when I was trying to fit the `ClaSPSegmentation` algorithm to several different data arrays genered using the following code.

```python
n = 50
data = np.concatenate([np.random.normal(-5, 0.1, n), np.random.normal(5, 0.1, n)])
```

**Versions**
<details>

<!--
Please run the following code snippet and paste the output here:

from sktime import show_versions; show_versions()
-->

My versions,

```
>>> 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! -->


Pull request description:
[BUG] in `ClaSPSegmentation`, deal with `k` when it is too large for `np.argpartition`
<!--
Welcome to sktime, and thanks for contributing!
Please have a look at our contribution guide:
https://www.sktime.net/en/latest/get_involved/contributing.html
-->

#### 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.
If no issue exists, you can open one here: https://github.com/sktime/sktime/issues
-->
Closes #5476.

#### What does this implement/fix? Explain your changes.
<!--
A clear and concise description of what you have implemented.
-->

Sometimes the `ClaSPSegmentation` algorithm raises a value error in the `_compute_distances_iterative` method. This happens when `_compute_distances_iterative` attempts to use `np.argpartition` with a `kth` value which is larger than the length of the input array.

This PR checks if `k` is larger than the length of `dist` and if it is, it sets `k` to be the length of `dist` minus 1.

#### Does your contribution introduce a new dependency? If yes, which one?

<!--
Only relevant if you changed pyproject.toml.
We try to minimize dependencies in the core dependency set. There
are also further specific instructions to follow for soft dependencies.
See here for handling dependencies in sktime: https://www.sktime.net/en/latest/developer_guide/dependencies.html
-->

No.

#### 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?
<!--
We value all user contributions, no matter how small or complex they are. If you have any questions, feel free to post
in the dev-chat channel on the sktime discord https://discord.com/invite/54ACzaFsn7. If we are slow to review (>3 working days), likewise feel free to ping us on discord. Thank you 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/sktime/sktime/blob/main/CONTRIBUTORS.md) with any new badges I've earned :-)
  How to: add yourself to the [all-contributors file](https://github.com/sktime/sktime/blob/main/.all-contributorsrc) in the `sktime` root directory (not the `CONTRIBUTORS.md`). Common badges: `code` - fixing a bug, or adding code logic. `doc` - writing or improving documentation or docstrings. `bug` - reporting or diagnosing a bug (get this plus `code` if you also fixed the bug in the PR).`maintenance` - CI, test framework, release.
  See here for [full badge reference](https://allcontributors.org/docs/en/emoji-key)
- [ ] Optionally, I've added myself and possibly others to the [CODEOWNERS](https://github.com/sktime/sktime/blob/main/CODEOWNERS) file - do this if you want to become the owner or maintainer of an estimator you added.
  See here for further details on the [algorithm maintainer role](https://www.sktime.net/en/latest/get_involved/governance.html#algorithm-maintainers).
- [x] The PR title starts with either [ENH], [MNT], [DOC], or [BUG]. [BUG] - bugfix, [MNT] - CI, test framework, [ENH] - adding or improving code, [DOC] - writing or improving documentation or docstrings.

<!--
Thanks for contributing!
-->


Patch:
diff --git a/.github/workflows/update_contributors.yml b/.github/workflows/update_contributors.yml
index 5f4e5b04f..9f9a91c2d 100644
--- a/.github/workflows/update_contributors.yml
+++ b/.github/workflows/update_contributors.yml
@@ -11,7 +11,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Set up Node
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@v4
         with:
           node-version: 18
       - name: Set up tool
diff --git a/CODEOWNERS b/CODEOWNERS
index 65f57f595..0e2122e79 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -84,5 +84,6 @@ sktime/transformations/series/outlier_detection.py @aiwalter
 sktime/transformations/series/scaledlogit.py @ltsaprounis
 sktime/transformations/series/time_since.py @KishManani
 sktime/transformations/series/theta.py @GuzalBulatova
+sktime/transformations/series/scaledasinh.py @ali-parizad
 
 sktime/utils/mlflow_sktime.py @benjaminbluhm
diff --git a/docs/source/api_reference/dists_kernels.rst b/docs/source/api_reference/dists_kernels.rst
index fb07eb8ca..cf5c7f1c7 100644
--- a/docs/source/api_reference/dists_kernels.rst
+++ b/docs/source/api_reference/dists_kernels.rst
@@ -10,10 +10,6 @@ Distances and kernel functions are treated the same, as they have the same forma
 
 Below, we list separately pairwise transformers for time series, and pairwise transformers for tabular data.
 
-.. automodule:: sktime.dists_kernels
-   :no-members:
-   :no-inherited-members:
-
 All time series distances and kernels in ``sktime`` can be listed using the ``sktime.registry.all_estimators`` utility,
 using ``estimator_types="transformer-pairwise-panel"``, optionally filtered by tags.
 Valid tags can be listed using ``sktime.registry.all_tags``.
@@ -24,10 +20,6 @@ Standalone, performant ``numba`` distance functions are available in the :mod:`s
 These are not wrapped in the ``sktime`` ``BaseObject`` interface and can therefore
 be used within other ``numba`` compiled functions for end-to-end compilation.
 
-.. automodule:: sktime.distances
-   :no-members:
-   :no-inherited-members:
-
 Time series distances/kernels
 -----------------------------
 
diff --git a/docs/source/api_reference/transformations.rst b/docs/source/api_reference/transformations.rst
index e41db1059..df9413e10 100644
--- a/docs/source/api_reference/transformations.rst
+++ b/docs/source/api_reference/transformations.rst
@@ -6,10 +6,6 @@ Time series transformations
 The :mod:`sktime.transformations` module contains classes for data
 transformations.
 
-.. automodule:: sktime.transformations
-   :no-members:
-   :no-inherited-members:
-
 All (simple) transformers in ``sktime`` can be listed using the ``sktime.registry.all_estimators`` utility,
 using ``estimator_types="regressor"``, optionally filtered by tags.
 Valid tags can be listed using ``sktime.registry.all_tags``.
@@ -288,6 +284,14 @@ Depending on the transformer, the transformation parameters can be fitted.
     ExponentTransformer
     SqrtTransformer
 
+.. currentmodule:: sktime.transformations.series.scaledasinh
+
+.. autosummary::
+    :toctree: auto_generated/
+    :template: class.rst
+
+    ScaledAsinhTransformer
+
 Detrending and Decomposition
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/pyproject.toml b/pyproject.toml
index fcbb55dd3..b37505598 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -245,7 +245,7 @@ tests = [
   "pytest<7.5,>=7.4",
   "pytest-cov<4.2,>=4.1",
   "pytest-randomly<3.16,>=3.15",
-  "pytest-timeout<2.2,>=2.1",
+  "pytest-timeout>=2.1,<2.3",
   "pytest-xdist<3.4,>=3.3",
 ]
 
diff --git a/sktime/classification/deep_learning/mcdcnn.py b/sktime/classification/deep_learning/mcdcnn.py
index 848e52326..1a9f024a2 100644
--- a/sktime/classification/deep_learning/mcdcnn.py
+++ b/sktime/classification/deep_learning/mcdcnn.py
@@ -215,6 +215,7 @@ class MCDCNNClassifier(BaseDeepClassifier):
         self.history = self.model_.fit(
             X,
             y_onehot,
+            epochs=self.n_epochs,
             batch_size=self.batch_size,
             verbose=self.verbose,
             callbacks=self.callbacks_,
diff --git a/sktime/dists_kernels/dtw/_dtw_tslearn.py b/sktime/dists_kernels/dtw/_dtw_tslearn.py
index 21bf86fa8..229228af1 100644
--- a/sktime/dists_kernels/dtw/_dtw_tslearn.py
+++ b/sktime/dists_kernels/dtw/_dtw_tslearn.py
@@ -101,7 +101,7 @@ class DtwDistTslearn(_TslearnPwTrafoAdapter, BasePairwiseTransformerPanel):
 
 
 class SoftDtwDistTslearn(_TslearnPwTrafoAdapter, BasePairwiseTransformerPanel):
-    """Dynamic time warping distance, from tslearn.
+    """Soft dynamic time warping distance, from tslearn.
 
     Direct interface to ``tslearn.metrics.cdist_soft_dtw`` and
     ``tslearn.metrics.cdist_soft_dtw_normalized``.
diff --git a/sktime/dists_kernels/signature_kernel.py b/sktime/dists_kernels/signature_kernel.py
index 207e8361b..267f4a3f1 100644
--- a/sktime/dists_kernels/signature_kernel.py
+++ b/sktime/dists_kernels/signature_kernel.py
@@ -867,7 +867,7 @@ class SeqKernelizer(BaseEstimator, TransformerMixin):
 
 
 class SignatureKernel(BasePairwiseTransformerPanel):
-    """Compute the sequential kernel matrix row features on collection of series.
+    """Time series signature kernel, including high-order and low-rank variants.
 
     Implements the signature kernel of Kiraly et al, see [1]_ and [2]_,
     including higher-order and low-rank approximation variants described therein.
diff --git a/sktime/forecasting/model_selection/_tune.py b/sktime/forecasting/model_selection/_tune.py
index d7d466088..7b70b6d1e 100644
--- a/sktime/forecasting/model_selection/_tune.py
+++ b/sktime/forecasting/model_selection/_tune.py
@@ -23,7 +23,9 @@ from sktime.forecasting.base._delegate import _DelegatedForecaster
 from sktime.forecasting.model_evaluation import evaluate
 from sktime.performance_metrics.base import BaseMetric
 from sktime.split.base import BaseSplitter
+from sktime.utils.parallel import parallelize
 from sktime.utils.validation.forecasting import check_scoring
+from sktime.utils.warnings import warn
 
 
 class BaseGridSearch(_DelegatedForecaster):
@@ -36,6 +38,7 @@ class BaseGridSearch(_DelegatedForecaster):
         "capability:pred_int:insample": True,
     }
 
+    # todo 0.26.0: remove n_jobs, pre_dispatch parameters and all related logic
     def __init__(
         self,
         forecaster,
@@ -52,6 +55,7 @@ class BaseGridSearch(_DelegatedForecaster):
         error_score=np.nan,
         tune_by_instance=False,
         tune_by_variable=False,
+        backend_params=None,
     ):
         self.forecaster = forecaster
         self.cv = cv
@@ -67,6 +71,7 @@ class BaseGridSearch(_DelegatedForecaster):
         self.error_score = error_score
         self.tune_by_instance = tune_by_instance
         self.tune_by_variable = tune_by_variable
+        self.backend_params = backend_params
 
         super().__init__()
 
@@ -181,11 +186,25 @@ class BaseGridSearch(_DelegatedForecaster):
         scoring = check_scoring(self.scoring, obj=self)
         scoring_name = f"test_{scoring.name}"
 
-        parallel = Parallel(
-            n_jobs=self.n_jobs, pre_dispatch=self.pre_dispatch, backend=self.backend
-        )
+        # todo 0.26.0: remove this logic and only use backend_params
+        backend = self.backend
+        backend_params = self.backend_params if self.backend_params else {}
+        if backend in ["threading", "multiprocessing", "loky"]:
+            n_jobs = self.n_jobs
+            pre_dispatch = self.pre_dispatch
+            backend_params["n_jobs"] = n_jobs
+            backend_params["pre_dispatch"] = pre_dispatch
+            if n_jobs is not None or pre_dispatch is not None:
+                warn(
+                    f"in {self.__class__.__name__}, n_jobs and pre_dispatch "
+                    "parameters are deprecated and will be removed in 0.26.0. "
+                    "Please use n_jobs and pre_dispatch directly in the backend_params "
+                    "argument instead.",
+                    obj=self,
+                    stacklevel=2,
+                )
 
-        def _fit_and_score(params):
+        def _fit_and_score(params, meta):
             # Clone forecaster.
             forecaster = self.forecaster.clone()
 
@@ -228,8 +247,11 @@ class BaseGridSearch(_DelegatedForecaster):
                     )
                 )
 
-            out = parallel(
-                delayed(_fit_and_score)(params) for params in candidate_params
+            out = parallelize(
+                fun=_fit_and_score,
+                iter=candidate_params,
+                backend=backend,
+                backend_params=backend_params,
             )
 
             if len(out) < 1:
@@ -436,9 +458,19 @@ class ForecastingGridSearchCV(BaseGridSearch):
     error_score : numeric value or the str 'raise', optional (default=np.nan)
         The test score returned when a forecaster fails to be fitted.
     return_train_score : bool, optional (default=False)
-    backend : str, optional (default="loky")
-        Specify the parallelisation backend implementation in joblib, where
-        "loky" is used by default.
+
+    backend : {"dask", "loky", "multiprocessing", "threading"}, by default "loky".
+        Runs parallel evaluate if specified and `strategy` is set as "refit".
+
+        - "None": executes loop sequentally, simple list comprehension
+        - "loky", "multiprocessing" and "threading": uses ``joblib.Parallel`` loops
+        - "dask": uses ``dask``, requires ``dask`` package in environment
+
+        Recommendation: Use "dask" or "loky" for parallel evaluate.
+        "threading" is unlikely to see speed ups due to the GIL and the serialization
+        backend (``cloudpickle``) for "dask" and "loky" is generally more robust
+        than the standard ``pickle`` library used in "multiprocessing".
+
     error_score : "raise" or numeric, default=np.nan
         Value to assign to the score if an exception occurs in estimator fitting. If set
         to "raise", the exception is raised. If a numeric value is given,
@@ -460,6 +492,18 @@ class ForecastingGridSearchCV(BaseGridSearch):
         Has the same effect as applying ColumnEnsembleForecaster wrapper to self.
         If False, the same best parameter is selected for all variables.
 
+    backend_params : dict, optional
+        additional parameters passed to the backend as config.
+        Directly passed to ``utils.parallel.parallelize``.
+        Valid keys depend on the value of ``backend``:
+
+        - "None": no additional parameters, ``backend_params`` is ignored
+        - "loky", "multiprocessing" and "threading":
+            any valid keys for ``joblib.Parallel`` can be passed here,
+            e.g., ``n_jobs``, with the exception of ``backend``
+            which is directly controlled by ``backend``
+        - "dask": any valid keys for ``dask.compute`` can be passed, e.g., ``scheduler``
+
     Attributes
     ----------
     best_index_ : int
@@ -569,6 +613,7 @@ class ForecastingGridSearchCV(BaseGridSearch):
         error_score=np.nan,
         tune_by_instance=False,
         tune_by_variable=False,
+        backend_params=None,
     ):
         super().__init__(
             forecaster=forecaster,
@@ -585,6 +630,7 @@ class ForecastingGridSearchCV(BaseGridSearch):
             error_score=error_score,
             tune_by_instance=tune_by_instance,
             tune_by_variable=tune_by_variable,
+            backend_params=backend_params,
         )
         self.param_grid = param_grid
 
@@ -738,16 +784,25 @@ class ForecastingRandomizedSearchCV(BaseGridSearch):
     return_n_best_forecasters: int, default=1
         In case the n best forecaster should be returned, this value can be set
         and the n best forecasters will be assigned to n_best_forecasters_
-    pre_dispatch : str, optional (default='2*n_jobs')
     random_state : int, RandomState instance or None, default=None
         Pseudo random number generator state used for random uniform sampling
         from lists of possible values instead of scipy.stats distributions.
         Pass an int for reproducible output across multiple
         function calls.
     pre_dispatch : str, optional (default='2*n_jobs')
-    backend : str, optional (default="loky")
-        Specify the parallelisation backend implementation in joblib, where
-        "loky" is used by default.
+
+    backend : {"dask", "loky", "multiprocessing", "threading"}, by default "loky".
+        Runs parallel evaluate if specified and `strategy` is set as "refit".
+
+        - "None": executes loop sequentally, simple list comprehension
+        - "loky", "multiprocessing" and "threading": uses ``joblib.Parallel`` loops
+        - "dask": uses ``dask``, requires ``dask`` package in environment
+
+        Recommendation: Use "dask" or "loky" for parallel evaluate.
+        "threading" is unlikely to see speed ups due to the GIL and the serialization
+        backend (``cloudpickle``) for "dask" and "loky" is generally more robust
+        than the standard ``pickle`` library used in "multiprocessing".
+
     error_score : "raise" or numeric, default=np.nan
         Value to assign to the score if an exception occurs in estimator fitting. If set
         to "raise", the exception is raised. If a numeric value is given,
@@ -769,6 +824,18 @@ class ForecastingRandomizedSearchCV(BaseGridSearch):
         Has the same effect as applying ColumnEnsembleForecaster wrapper to self.
         If False, the same best parameter is selected for all variables.
 
+    backend_params : dict, optional
+        additional parameters passed to the backend as config.
+        Directly passed to ``utils.parallel.parallelize``.
+        Valid keys depend on the value of ``backend``:
+
+        - "None": no additional parameters, ``backend_params`` is ignored
+        - "loky", "multiprocessing" and "threading":
+            any valid keys for ``joblib.Parallel`` can be passed here,
+            e.g., ``n_jobs``, with the exception of ``backend``
+            which is directly controlled by ``backend``
+        - "dask": any valid keys for ``dask.compute`` can be passed, e.g., ``scheduler``
+
     Attributes
     ----------
     best_index_ : int
@@ -812,6 +879,7 @@ class ForecastingRandomizedSearchCV(BaseGridSearch):
         error_score=np.nan,
         tune_by_instance=False,
         tune_by_variable=False,
+        backend_params=None,
     ):
         super().__init__(
             forecaster=forecaster,
@@ -828,6 +896,7 @@ class ForecastingRandomizedSearchCV(BaseGridSearch):
             error_score=error_score,
             tune_by_instance=tune_by_instance,
             tune_by_variable=tune_by_variable,
+            backend_params=backend_params,
         )
         self.param_distributions = param_distributions
         self.n_iter = n_iter
@@ -980,9 +1049,19 @@ class ForecastingSkoptSearchCV(BaseGridSearch):
         Number of jobs to run in parallel.
         None means 1 unless in a joblib.parallel_backend context.
         -1 means using all processors.
-    backend : str, optional (default="loky")
-        Specify the parallelisation backend implementation in joblib, where
-        "loky" is used by default.
+
+    backend : {"dask", "loky", "multiprocessing", "threading"}, by default "loky".
+        Runs parallel evaluate if specified and `strategy` is set as "refit".
+
+        - "None": executes loop sequentally, simple list comprehension
+        - "loky", "multiprocessing" and "threading": uses ``joblib.Parallel`` loops
+        - "dask": uses ``dask``, requires ``dask`` package in environment
+
+        Recommendation: Use "dask" or "loky" for parallel evaluate.
+        "threading" is unlikely to see speed ups due to the GIL and the serialization
+        backend (``cloudpickle``) for "dask" and "loky" is generally more robust
+        than the standard ``pickle`` library used in "multiprocessing".
+
     tune_by_instance : bool, optional (default=False)
         Whether to tune parameter by each time series instance separately,
         in case of Panel or Hierarchical data passed to the tuning estimator.
@@ -1000,6 +1079,18 @@ class ForecastingSkoptSearchCV(BaseGridSearch):
         Has the same effect as applying ColumnEnsembleForecaster wrapper to self.
         If False, the same best parameter is selected for all variables.
 
+    backend_params : dict, optional
+        additional parameters passed to the backend as config.
+        Directly passed to ``utils.parallel.parallelize``.
+        Valid keys depend on the value of ``backend``:
+
+        - "None": no additional parameters, ``backend_params`` is ignored
+        - "loky", "multiprocessing" and "threading":
+            any valid keys for ``joblib.Parallel`` can be passed here,
+            e.g., ``n_jobs``, with the exception of ``backend``
+            which is directly controlled by ``backend``
+        - "dask": any valid keys for ``dask.compute`` can be passed, e.g., ``scheduler``
+
     Attributes
     ----------
     best_index_ : int
@@ -1082,6 +1173,7 @@ class ForecastingSkoptSearchCV(BaseGridSearch):
         error_score=np.nan,
         tune_by_instance=False,
         tune_by_variable=False,
+        backend_params=None,
     ):
         self.param_distributions = param_distributions
         self.n_iter = n_iter
@@ -1103,6 +1195,7 @@ class ForecastingSkoptSearchCV(BaseGridSearch):
             error_score=error_score,
             tune_by_instance=tune_by_instance,
             tune_by_variable=tune_by_variable,
+            backend_params=backend_params,
         )
 
     def _fit(self, y, X=None, fh=None):
diff --git a/sktime/split/base/_base_splitter.py b/sktime/split/base/_base_splitter.py
index 3751b6bb5..b1325307e 100644
--- a/sktime/split/base/_base_splitter.py
+++ b/sktime/split/base/_base_splitter.py
@@ -395,7 +395,7 @@ class BaseSplitter(BaseObject):
         n_splits : int
             The number of splits.
         """
-        raise NotImplementedError("abstract method")
+        return len(list(self.split(y)))
 
     def get_cutoffs(self, y: Optional[ACCEPTED_Y_TYPES] = None) -> np.ndarray:
         """Return the cutoff points in .iloc[] context.
diff --git a/sktime/transformations/series/_clasp_numba.py b/sktime/transformations/series/_clasp_numba.py
index 5ca335ccc..da2d023b3 100644
--- a/sktime/transformations/series/_clasp_numba.py
+++ b/sktime/transformations/series/_clasp_numba.py
@@ -111,7 +111,8 @@ def _compute_distances_iterative(X, m, k):
         )
         dist[trivialMatchRange[0] : trivialMatchRange[1]] = np.inf
 
-        idx = np.argpartition(dist, k)
+        _k = min(k, len(dist) - 1)
+        idx = np.argpartition(dist, _k)
 
         knns[order, :] = idx[:k]
         dot_prev = dot_rolled
diff --git a/sktime/transformations/series/detrend/mstl.py b/sktime/transformations/series/detrend/mstl.py
index aa535b3b4..8d19ad6b9 100644
--- a/sktime/transformations/series/detrend/mstl.py
+++ b/sktime/transformations/series/detrend/mstl.py
@@ -14,7 +14,8 @@ from sktime.transformations.base import BaseTransformer
 class MSTL(BaseTransformer):
     """Season-Trend decomposition using LOESS for multiple seasonalities.
 
-    Direct interface for `statsmodels.tsa.seasonal.MSTL`.
+    Direct interface for ``statsmodels.tsa.seasonal.MSTL``.
+    ``MSTL`` can be used to perform deseasonalization or decomposition:
 
     `fit` stores the decomposed values in self.trend_, self.seasonal_, and self.resid_.
     If `return_components=False`, then `transform` returns a pd.Series of the
@@ -24,10 +25,18 @@ class MSTL(BaseTransformer):
     well as three components as variables in the returned multivariate series
     (DataFrame cols)
         "transformed" - the transformed series
-        "seasonal" - the seasonal component(s)
+        "seasonal" - the seasonal component(s), summed up if multiple
         "trend" - the trend component
         "resid" - the residuals after de-trending, de-seasonalizing
 
+    ``MSTL`` performs ``inverse_transform`` by summing any components,
+    and can be used for pipelining in a ``TransformedTargetForecaster``.
+
+    Important: for separate forecasts of trend and seasonalities, and an
+    inverse transform that respects seasonality, ensure
+    that ``return_components=True`` is set, otherwise the inverse will just
+    return the trend component.
+
     Parameters
     ----------
     endog : array_like
@@ -54,7 +63,7 @@ class MSTL(BaseTransformer):
         if True, will return the transformed series, as well as three components
             as variables in the returned multivariate series (DataFrame cols)
             "transformed" - the transformed series
-            "seasonal" - the seasonal component(s)
+            "seasonal" - the seasonal component(s), summed up if multiple
             "trend" - the trend component
             "resid" - the residuals after de-trending, de-seasonalizing
 
@@ -99,6 +108,7 @@ class MSTL(BaseTransformer):
         "univariate-only": True,
         "capability:inverse_transform": True,
         "capability:inverse_transform:exact": False,
+        "skip-inverse-transform": False,
         "fit_is_empty": False,
         "python_dependencies": "statsmodels",
     }
@@ -181,10 +191,11 @@ class MSTL(BaseTransformer):
         return ret_obj
 
     def _inverse_transform(self, X, y=None):
-        if len(self.seasonal_.shape) > 1:
-            return self.seasonal_.sum(axis=1) + self.resid_ + self.trend_
-        else:
-            return self.seasonal_ + self.resid_ + self.trend_
+        # for inverse transform, we sum up the columns
+        # this will be close if return_components=True
+        row_sums = X.sum(axis=1)
+        row_sums.columns = self.fit_column_names
+        return row_sums
 
     def _make_return_object(self, X, mstl):
         if len(mstl.seasonal.shape) > 1:
diff --git a/sktime/transformations/series/scaledasinh.py b/sktime/transformations/series/scaledasinh.py
new file mode 100644
index 000000000..8df1b2653
--- /dev/null
+++ b/sktime/transformations/series/scaledasinh.py
@@ -0,0 +1,193 @@
+# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
+"""Implements the Hyperbolic Sine transformation and its inverse."""
+
+__author__ = ["Ali Parizad"]
+__all__ = ["ScaledAsinhTransformer"]
+
+
+import numpy as np
+from scipy import stats
+
+from sktime.transformations.base import BaseTransformer
+
+
+class ScaledAsinhTransformer(BaseTransformer):
+    r"""Hyperbolic sine transformation and its inverse [1]_.
+
+    Known as variance stabilizing transformation,
+    Combined with an sktime.forecasting.compose.TransformedTargetForecaster,
+    can be usefull in time series that exhibit spikes [1]_, [2]_
+
+    Parameters
+    ----------
+    mad_normalization_factor : float, default = 1.4826
+        The normalization factor used to adjust the median absolute deviation
+        (MAD) for asymptotically normal consistency to the standard deviation.
+        The default value based on [1]_, [2]_ is 1.4826.
+
+    Attributes
+    ----------
+    shift_parameter_asinh_ : float
+        shift parameter, denoted as "a" in [1]_, the median of sample data.
+        It is fitted, based on the data provided in "fit".
+
+    scale_parameter_asinh_ : float
+        scale parameter, denoted as "b" in [1]_, the median absolute deviation
+        (MAD) around the sample median adjusted by a factor for asymptotically
+        normal consistency to the standard deviation (Based on [1]_, [2]_
+        b = median_abs_deviation(sample data) :math:`\times` mad_normalization_factor).
+        It is fitted, based on the data provided in "fit".
+
+    See Also
+    --------
+    sktime.transformations.series.boxcox.LogTransformer :
+        Transformer input data using natural log. Can help normalize data and
+        compress variance of the series.
+    sktime.transformations.series.boxcox.BoxCoxTransformer :
+        Applies Box-Cox power transformation. Can help normalize data and
+        compress variance of the series.
+    sktime.transformations.series.exponent.ExponentTransformer :
+        Transform input data by raising it to an exponent. Can help compress
+        variance of series if a fractional exponent is supplied.
+    sktime.transformations.series.exponent.SqrtTransformer :
+        Transform input data by taking its square root. Can help compress
+        variance of input series.
+
+    Notes
+    -----
+    | The Hyperbolic Sine transformation is applied as:
+    |   :math:`asinh(\frac{x- a}{b})`
+    | The Hyperbolic Sine inverse transformation is applied as:
+    |   :math:`b . sinh(x) + a`
+    | where "a" is the shift parameter and "b" is the scale parameter [1]_.
+    | a = median(sample data)
+    | b = median_abs_deviation(sample data) :math:`\times` mad_normalization_factor
+
+    References
+    ----------
+    .. [1] Ziel F, Weron R. Day-ahead electricity price forecasting with
+        high-dimensional structures: Univariate vs. multivariate modeling
+        frameworks. Energy Economics. 2018 Feb 1;70:396-420.
+    .. [2] Uniejewski, B., Weron, R., Ziel, F., 2017. Variance stabilizing
+        transformations for electricity spot price forecasting.
+        IEEE Transactions on Power Systems, DOI: 10.1109/TPWRS.2017.2734563
+
+    Examples
+    --------
+    >>> from sktime.transformations.series.scaledasinh import ScaledAsinhTransformer
+    >>> from sktime.datasets import load_airline
+    >>> y = load_airline()
+    >>> transformer = ScaledAsinhTransformer()
+    >>> y_hat = transformer.fit_transform(y)
+    """
+
+    _tags = {
+        "scitype:transform-input": "Series",
+        # what is the scitype of X: Series, or Panel
+        "scitype:transform-output": "Series",
+        # what scitype is returned: Primitives, Series, Panel
+        "scitype:instancewise": True,  # is this an instance-wise transform?
+        "X_inner_mtype": "np.ndarray",  # which mtypes do _fit/_predict support for X?
+        "y_inner_mtype": "None",  # which mtypes do _fit/_predict support for y?
+        "transform-returns-same-time-index": True,
+        "fit_is_empty": False,
+        "univariate-only": False,
+        "capability:inverse_transform": True,
+        "skip-inverse-transform": False,
+    }
+
+    def __init__(self, mad_normalization_factor=1.4826):
+        super().__init__()
+        self.mad_normalization_factor = mad_normalization_factor
+
+    def _fit(self, X, y=None):
+        """Fit transformer to X and y.
+
+        private _fit containing the core logic, called from fit
+
+        Parameters
+        ----------
+        X : 2D np.ndarray (n x 1)
+            Data to be transformed
+        y : ignored argument for interface compatibility
+            Additional data, e.g., labels for transformation
+
+        Returns
+        -------
+        self: a fitted instance of the estimator
+        """
+        self.shift_parameter_asinh_ = np.median(X)
+
+        self.scale_parameter_asinh_ = (
+            stats.median_abs_deviation(X) * self.mad_normalization_factor
+        )
+
+        return self
+
+    def _transform(self, X, y=None):
+        """Transform X and return a transformed version.
+
+        private _transform containing core logic, called from transform
+
+        Parameters
+        ----------
+        X : 2D np.ndarray
+            Data to be transformed
+        y : Series or Panel of mtype y_inner_mtype, default=None
+            Ignored argument for interface compatibility
+
+        Returns
+        -------
+        transformed version of X
+        """
+        X_transformed = np.arcsinh(
+            (X - self.shift_parameter_asinh_) / self.scale_parameter_asinh_
+        )
+
+        return X_transformed
+
+    def _inverse_transform(self, X, y=None):
+        """Inverse transform, inverse operation to transform.
+
+        private _inverse_transform containing core logic, called from inverse_transform
+
+        Parameters
+        ----------
+        X : 2D np.ndarray
+            Data to be inverse transformed
+        y : Series or Panel of mtype y_inner_mtype, optional (default=None)
+            Ignored argument for interface compatibility
+
+        Returns
+        -------
+        inverse transformed version of X
+        """
+        X_inv_transformed = (
+            self.scale_parameter_asinh_ * np.sinh(X) + self.shift_parameter_asinh_
+        )
+
+        return X_inv_transformed
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+
+
+        Returns
+        -------
+        params : dict or list of dict, default = {}
+            Parameters to create testing instances of the class
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`
+        """
+        params1 = {}
+        params2 = {"mad_normalization_factor": 1.6}
+
+        return [params1, params2]
diff --git a/sktime/utils/parallel.py b/sktime/utils/parallel.py
index 90da9d066..ab839145c 100644
--- a/sktime/utils/parallel.py
+++ b/sktime/utils/parallel.py
@@ -116,3 +116,30 @@ def _parallelize_dask(fun, iter, meta, backend, backend_params):
 
 
 para_dict["dask"] = _parallelize_dask
+
+
+def _get_parallel_test_fixtures():
+    """Return fixtures for parallelization tests.
+
+    Returns a list of parameter fixtures, where each fixture
+    is a dict with keys "backend" and "backend_params".
+    """
+    from sktime.utils.validation._dependencies import _check_soft_dependencies
+
+    fixtures = []
+
+    # test no parallelization
+    fixtures.append({"backend": "None", "backend_params": {}})
+
+    # test joblib backends
+    for backend in ["loky", "multiprocessing", "threading"]:
+        fixtures.append({"backend": backend, "backend_params": {}})
+        fixtures.append({"backend": backend, "backend_params": {"n_jobs": 2}})
+        fixtures.append({"backend": backend, "backend_params": {"n_jobs": -1}})
+
+    # test dask backends
+    if _check_soft_dependencies("dask", severity="none"):
+        fixtures.append({"backend": "dask", "backend_params": {}})
+        fixtures.append({"backend": "dask", "backend_params": {"scheduler": "sync"}})
+
+    return fixtures
diff --git a/sktime/utils/validation/_dependencies.py b/sktime/utils/validation/_dependencies.py
index aaf37b46e..b927273be 100644
--- a/sktime/utils/validation/_dependencies.py
+++ b/sktime/utils/validation/_dependencies.py
@@ -301,9 +301,14 @@ def _check_python_version(obj, package=None, msg=None, severity="error"):
         return True
     # now we know that est_version is not compatible with sys_version
 
+    if isclass(obj):
+        class_name = obj.__name__
+    else:
+        class_name = type(obj).__name__
+
     if not isinstance(msg, str):
         msg = (
-            f"{type(obj).__name__} requires python version to be {est_specifier},"
+            f"{class_name} requires python version to be {est_specifier},"
             f" but system python version is {sys.version}."
         )
 


Test patch:
diff --git a/.github/workflows/test_all.yml b/.github/workflows/test_all.yml
index 8e22fa20c..c30a1547d 100644
--- a/.github/workflows/test_all.yml
+++ b/.github/workflows/test_all.yml
@@ -4,7 +4,25 @@ on:
     - cron: 0 0 * * 0
   workflow_dispatch:
 jobs:
+  code_quality:
+    name: validate code quality
+    runs-on: ubuntu-latest
+    steps:
+      - name: repository checkout step
+        uses: actions/checkout@v4
+      - name: python environment step
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.8"
+      - name: install pre-commit
+        run: python3 -m pip install pre-commit
+      - name: run pre-commit hooks on all files
+        run: pre-commit run --color always --all-files --show-diff-on-failure
+      - name: check missing __init__ files
+        run: build_tools/fail_on_missing_init_files.sh
+        shell: bash
   test_base:
+    needs: code_quality
     name: test base framework
     strategy:
       fail-fast: false
@@ -30,6 +48,7 @@ jobs:
           sub-sample-estimators: "False"
           test-affected-estimators: "False"
   test_components:
+    needs: code_quality
     name: test individual components
     strategy:
       fail-fast: false
diff --git a/.github/workflows/test_base.yml b/.github/workflows/test_base.yml
index 689d1b9be..921ede719 100644
--- a/.github/workflows/test_base.yml
+++ b/.github/workflows/test_base.yml
@@ -1,8 +1,6 @@
 name: test base workflow
 on:
-  pull_request:
-    branches:
-      - main
+  workflow_call:
 jobs:
   detect:
     name: check for changes in base framework
diff --git a/.github/workflows/test_code_quality.yml b/.github/workflows/test_code_quality.yml
new file mode 100644
index 000000000..20b2eb76b
--- /dev/null
+++ b/.github/workflows/test_code_quality.yml
@@ -0,0 +1,34 @@
+name: validate code quality workflow
+on:
+  pull_request:
+    branches:
+      - main
+jobs:
+  code_quality:
+    name: test code quality
+    runs-on: ubuntu-latest
+    steps:
+      - name: repository checkout step
+        uses: actions/checkout@v4
+      - name: python environment step
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.8"
+      - name: install pre-commit
+        run: python3 -m pip install pre-commit
+      - id: changed-files
+        name: identify modified files
+        uses: tj-actions/changed-files@v40
+      - name: run pre-commit hooks on modified files
+        run: pre-commit run --color always --files ${{ steps.changed-files.outputs.all_changed_files }} --show-diff-on-failure
+      - name: check missing __init__ files
+        run: build_tools/fail_on_missing_init_files.sh
+        shell: bash
+  unit_test_base:
+    needs: code_quality
+    name: run unit tests for base framework
+    uses: ./.github/workflows/test_base.yml
+  unit_test_components:
+    needs: code_quality
+    name: run unit tests for individual components
+    uses: ./.github/workflows/test_components.yml
diff --git a/.github/workflows/test_components.yml b/.github/workflows/test_components.yml
index f90fc5f4e..cf66c2a8e 100644
--- a/.github/workflows/test_components.yml
+++ b/.github/workflows/test_components.yml
@@ -1,8 +1,6 @@
 name: test individual components workflow
 on:
-  pull_request:
-    branches:
-      - main
+  workflow_call:
 jobs:
   detect:
     name: check for changes in individual components
diff --git a/sktime/forecasting/model_selection/tests/test_tune.py b/sktime/forecasting/model_selection/tests/test_tune.py
index 0de49f274..fa52904df 100644
--- a/sktime/forecasting/model_selection/tests/test_tune.py
+++ b/sktime/forecasting/model_selection/tests/test_tune.py
@@ -37,6 +37,7 @@ from sktime.tests.test_switch import run_test_for_class
 from sktime.transformations.series.detrend import Detrender
 from sktime.transformations.series.impute import Imputer
 from sktime.utils._testing.hierarchical import _make_hierarchical
+from sktime.utils.parallel import _get_parallel_test_fixtures
 
 TEST_METRICS = [MeanAbsolutePercentageError(symmetric=True), MeanSquaredError()]
 TEST_METRICS_PROBA = [CRPS(), PinballLoss()]
@@ -322,3 +323,30 @@ def test_skoptcv_multiple_forecaster():
     )
     sscv.fit(y, X)
     assert len(sscv.cv_results_) == 5
+
+
+BACKEND_TEST = _get_parallel_test_fixtures()
+
+
+@pytest.mark.skipif(
+    not run_test_for_class(ForecastingGridSearchCV),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
+@pytest.mark.parametrize("backend_set", BACKEND_TEST)
+def test_gscv_backends(backend_set):
+    """Test ForecastingGridSearchCV."""
+    backend = backend_set["backend"]
+    backend_params = backend_set["backend_params"]
+
+    y, X = load_longley()
+
+    gscv = ForecastingGridSearchCV(
+        PIPE,
+        param_grid=PIPE_GRID,
+        cv=CVs[0],
+        scoring=TEST_METRICS[0],
+        error_score=ERROR_SCORES[0],
+        backend=backend,
+        backend_params=backend_params,
+    )
+    gscv.fit(y, X)