Repository: sktime/sktime

Issue:
[BUG] Prediction fails for TransformedTargetForecaster with LTSFNLinearForecaster with a non-relative ForecastingHorizon
**Describe the bug**
<!--
A clear and concise description of what the bug is.
-->
When constructing a TransformedTargetForecaster with a transformer and LTSFNLinearForecaster, prediction fails saying that a different fh has been provided even if that is not the case.
```
Traceback (most recent call last):
  File "bug_report/tmp.py", line 29, in <module>
    pipe.predict(fh=fh)
  File "lib/python3.11/site-packages/sktime/forecasting/base/_base.py", line 431, in predict
    y_pred = self._predict(fh=fh, X=X_inner)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.11/site-packages/sktime/forecasting/compose/_pipeline.py", line 1032, in _predict
    y_pred = self.forecaster_.predict(fh=fh, X=X)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.11/site-packages/sktime/forecasting/base/_base.py", line 424, in predict
    fh = self._check_fh(fh)
         ^^^^^^^^^^^^^^^^^^
  File "lib/python3.11/site-packages/sktime/forecasting/base/_base.py", line 1745, in _check_fh
    raise ValueError(
ValueError: A different forecasting horizon `fh` has been provided from the one seen already in `fit`, in this instance of LTSFNLinearForecaster. If you want to change the forecasting horizon, please re-fit the forecaster. This is because fitting of the forecaster LTSFNLinearForecaster depends on `fh`.
```

**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
-->

```python
import numpy as np
import pandas as pd
from sktime.forecasting.base import ForecastingHorizon
from sktime.forecasting.ltsf import LTSFNLinearForecaster
from sktime.split import temporal_train_test_split
from sktime.transformations.series.boxcox import LogTransformer

y = pd.DataFrame(
    np.random.random([100, 3]), index=pd.date_range("2023-01-01", periods=100)
)
y_train, y_test = temporal_train_test_split(y, test_size=30)

# TransformedTargetForecaster works with relative ForecastingHorizon
fh = ForecastingHorizon(np.arange(1, 31), is_relative=True)
pipe = LogTransformer() * LTSFNLinearForecaster(seq_len=20, pred_len=30)
pipe.fit(y_train, fh=fh)
pipe.predict()

# Forecaster works with non-relative ForecastingHorizon
fh = ForecastingHorizon(y_test.index, is_relative=False)
pipe = LTSFNLinearForecaster(seq_len=20, pred_len=30, batch_size=256)
pipe.fit(y_train, fh=fh)
pipe.predict()

# TransformedTargetForecaster fails with non-relative ForecastingHorizon
fh = ForecastingHorizon(y_test.index, is_relative=False)
pipe = LogTransformer() * LTSFNLinearForecaster(seq_len=20, pred_len=30)
pipe.fit(y_train, fh=fh)
pipe.predict()

# TransformedTargetForecaster fails with non-relative ForecastingHorizon even if ForecastingHorizon is provided to predict
fh = ForecastingHorizon(y_test.index, is_relative=False)
pipe = LogTransformer() * LTSFNLinearForecaster(seq_len=20, pred_len=30)
pipe.fit(y_train, fh=fh)
pipe.predict(fh=fh)

```

**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->
I would expect the TransformedTargetForecaster prediction to work with a non-relative ForecastingHorizon same as when using only the LTSFNLinearForecaster without needing to provide the ForecastingHorizon again.

**Additional context**
<!--
Add any other context about the problem here.
-->
I suspect that it has to do with the conversion of the ForecastingHorizon to relative if it's non-relative and then it thinks that it's not the same ForecastingHorizon anymore.
https://github.com/sktime/sktime/blob/d8574257d62a36930203bcb11f532623869fd7d7/sktime/forecasting/base/adapters/_pytorch.py#L64

**Versions**
<details>

<!--
Please run the following code snippet and paste the output here:

from sktime import show_versions; show_versions()
-->

System:
    python: 3.11.7 (main, Dec 17 2023, 18:26:46) [GCC 11.4.0]
executable: /home/usr/.pyenv/versions/bug_report/bin/python
   machine: Linux-6.5.0-15-generic-x86_64-with-glibc2.35

Python dependencies:
          pip: 23.3.2
       sktime: 0.26.0
      sklearn: 1.4.0
       skbase: 0.7.2
        numpy: 1.26.3
        scipy: 1.12.0
       pandas: 2.1.4
   matplotlib: 3.8.2
       joblib: 1.3.2
        numba: 0.59.0
  statsmodels: 0.14.1
     pmdarima: 2.0.4
statsforecast: 1.6.0
      tsfresh: None
      tslearn: None
        torch: 2.2.0+cpu
   tensorflow: None
tensorflow_probability: None


</details>



<!-- Thanks for contributing! -->



Discussion:
Hm, I find it curious that:

* this is not detected by a test
* this seems indeed specific to the LTSFN forecasters

My suspicion is, `fh` is passed directly to the network cass, and by it to `pytoch`, which might be mutating the `fh` internally.
FYI @luca-miniati, any ideas?

Resolving patch:
diff --git a/sktime/forecasting/base/adapters/_pytorch.py b/sktime/forecasting/base/adapters/_pytorch.py
--- a/sktime/forecasting/base/adapters/_pytorch.py
+++ b/sktime/forecasting/base/adapters/_pytorch.py
@@ -57,14 +57,7 @@ def _fit(self, y, fh, X=None):
         """
         from sktime.forecasting.base import ForecastingHorizon
 
-        # save fh and y for prediction later
-        if fh.is_relative:
-            self._fh = fh
-        else:
-            fh = fh.to_relative(self.cutoff)
-            self._fh = fh
-
-        self._y = y
+        fh = fh.to_relative(self.cutoff)
 
         if type(fh) is ForecastingHorizon:
             self.network = self._build_network(fh._values[-1])
@@ -122,6 +115,7 @@ def _predict(self, X=None, fh=None):
 
         if fh is None:
             fh = self._fh
+        fh = fh.to_relative(self.cutoff)
 
         if max(fh._values) > self.network.pred_len or min(fh._values) < 0:
             raise ValueError(
@@ -166,7 +160,7 @@ def build_pytorch_train_dataloader(self, y):
             dataset = PyTorchTrainDataset(
                 y=y,
                 seq_len=self.network.seq_len,
-                fh=self._fh._values[-1],
+                fh=self._fh.to_relative(self.cutoff)._values[-1],
             )
 
         return DataLoader(


Test patch: