From 33e5906f58212c8b20ea84e9a98c6251d534829a Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 10:26:18 +0100 Subject: [PATCH 01/23] [TSMixer v2] add init --- pytorch_forecasting/models/tsmixer/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 pytorch_forecasting/models/tsmixer/__init__.py diff --git a/pytorch_forecasting/models/tsmixer/__init__.py b/pytorch_forecasting/models/tsmixer/__init__.py new file mode 100644 index 000000000..58cacd2fa --- /dev/null +++ b/pytorch_forecasting/models/tsmixer/__init__.py @@ -0,0 +1,8 @@ +""" +TSMixer model for time series forecasting. +""" + +from pytorch_forecasting.models.tsmixer._tsmixer_v2 import TSMixer +from pytorch_forecasting.models.tsmixer._tsmixer_pkg_v2 import TSMixer_pkg_v2 + +__all__ = ["TSMixer", "TSMixer_pkg_v2"] From beca8112ea252009e08792ba7c4c083a8274449a Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 10:33:59 +0100 Subject: [PATCH 02/23] [TSMixer v2] add TSMixerBlock --- .../models/tsmixer/_tsmixer_v2.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 pytorch_forecasting/models/tsmixer/_tsmixer_v2.py diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py new file mode 100644 index 000000000..24e4ee2e2 --- /dev/null +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -0,0 +1,44 @@ +""" +TSMixer model for PyTorch Forecasting. +------------------------------------------- +""" + +from typing import Any +import warnings + +import torch +import torch.nn as nn +from torch.optim import Optimizer + +from pytorch_forecasting.models.base._tslib_base_model_v2 import TslibBaseModel + + +class TSMixerBlock(nn.Module): + def __init__( + self, + sequence_length: int, + num_features: int, + hidden_dim: int, + dropout: float, + ): + super().__init__() + + self.temporal = nn.Sequential( + nn.Linear(sequence_length, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, sequence_length), + nn.Dropout(dropout), + ) + + self.channel = nn.Sequential( + nn.Linear(num_features, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, num_features), + nn.Dropout(dropout), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = x + self.temporal(x.transpose(1, 2)).transpose(1, 2) + x = x + self.channel(x) + + return x From a8fd17b0b3fd957df806d2a3f3f3568426140a96 Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 10:34:44 +0100 Subject: [PATCH 03/23] [TSMixer v2] add unstable note --- pytorch_forecasting/models/tsmixer/_tsmixer_v2.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py index 24e4ee2e2..99edba18c 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -3,6 +3,12 @@ ------------------------------------------- """ +################################################# +# NOTE: This is an experimental implementation # +# of TSMixer for PyTorch Forecasting v2. # +# It is an unstable API and subject to change. # +################################################# + from typing import Any import warnings From 20ecc8d839aac29cd680d35ab6d209b0f24567cf Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 10:35:41 +0100 Subject: [PATCH 04/23] [TSMixer v2] add TSMixer with init --- .../models/tsmixer/_tsmixer_v2.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py index 99edba18c..8ba579f1b 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -48,3 +48,53 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: x = x + self.channel(x) return x + + +class TSMixer(TslibBaseModel): + + @classmethod + def _pkg(cls): + """Package containing the model.""" + from pytorch_forecasting.models.tsmixer._tsmixer_pkg_v2 import TSMixer_pkg_v2 + + return TSMixer_pkg_v2 + + def __init__( + self, + loss: nn.Module, + d_model: int = 64, + e_layers: int = 2, + dropout: float = 0.1, + logging_metrics: list[nn.Module] | None = None, + optimizer: Optimizer | str | None = "adam", + optimizer_params: dict | None = None, + lr_scheduler: str | None = None, + lr_scheduler_params: dict | None = None, + metadata: dict | None = None, + **kwargs: Any, + ): + super().__init__( + loss=loss, + logging_metrics=logging_metrics, + optimizer=optimizer, + optimizer_params=optimizer_params, + lr_scheduler=lr_scheduler, + lr_scheduler_params=lr_scheduler_params, + metadata=metadata, + ) + + warnings.warn( + "TSMixer is an experimental model implemented on TslibBaseModelV2. " + "It is an unstable version and may be subject to unannounced changes. " + "Please use with caution." + ) + + self.d_model = d_model + self.e_layers = e_layers + self.dropout = dropout + + self.save_hyperparameters( + ignore=["loss", "logging_metrics", "metadata"] + ) + + self._init_network() From bda6af3676410e436a216ba29034c18f44696e93 Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 10:36:18 +0100 Subject: [PATCH 05/23] [TSMixer v2] add docstring for TSMixer --- .../models/tsmixer/_tsmixer_v2.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py index 8ba579f1b..2b1e5a2df 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -51,6 +51,40 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: class TSMixer(TslibBaseModel): + """ + TSMixer: An All-MLP Architecture for Time Series Forecasting. + + TSMixer is a lightweight time series forecasting model made of stacked + multilayer perceptrons. + + Parameters + ---------- + loss : nn.Module + Loss function for training step. + d_model : int, default=64 + Hidden dimension of the temporal and channel mixing MLPs. + e_layers : int, default=2 + Number of stacked TSMixer blocks. + dropout : float, default=0.1 + Dropout probability used within the mixer blocks. + logging_metrics : Optional[list[nn.Module]], default=None + List of metrics to log during training, validation and testing. + optimizer : Optional[Union[Optimizer, str]], default="adam" + Optimizer used for training. + optimizer_params : Optional[dict], default=None + Parameters passed to the optimizer. + lr_scheduler : Optional[str], default=None + Learning rate scheduler utilised. + lr_scheduler_params : Optional[dict], default=None + Parameters passed to the learning rate scheduler. + metadata : Optional[dict], default=None + Metadata for the model from TslibDataModule. + + References + ---------- + [1] TSMixer: An All-MLP Architecture for Time Series Forecasting (https://arxiv.org/abs/2303.06053). + [2] https://github.com/thuml/Time-Series-Library/blob/main/models/TSMixer.py + """ @classmethod def _pkg(cls): From 6c91fcbdb5d7d1aac6d70180636f0c8b6ef5db1d Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 10:49:05 +0100 Subject: [PATCH 06/23] [TSMixer v2] for TSMixer class add _init_network() --- .../models/tsmixer/_tsmixer_v2.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py index 2b1e5a2df..defb57c6c 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -132,3 +132,25 @@ def __init__( ) self._init_network() + + def _init_network(self): + """Initialize the TSMixer network components.""" + + self.enc_in = self.cont_dim + self.target_dim + + self.model = nn.ModuleList( + [ + TSMixerBlock( + sequence_length=self.context_length, + num_features=self.enc_in, + hidden_dim=self.d_model, + dropout=self.dropout, + ) + for _ in range(self.e_layers) + ] + ) + + self.projection = nn.Linear( + self.context_length, + self.prediction_length, + ) From cfcdf35376a6f4d28395544c0b11798be0f75dd7 Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 11:26:24 +0100 Subject: [PATCH 07/23] [TSMixer v2] for TSMixer class add _encoder() --- .../models/tsmixer/_tsmixer_v2.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py index defb57c6c..5064ef956 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -154,3 +154,35 @@ def _init_network(self): self.context_length, self.prediction_length, ) + + def _encoder(self, x: torch.Tensor, target_indices: torch.Tensor | None) -> torch.Tensor: + """ + Encode the input time series through the TSMixer blocks. + + Parameters + ---------- + x : torch.Tensor + Input data to the encoder via tensor of shape + (batch_size, context_length, n_features). + target_indices : torch.Tensor or None + Indices of target features to extract from the output. + If None, all features are returned. + + Returns + ------- + torch.Tensor + Forecast tensor of shape + (batch_size, prediction_length, n_targets). + """ + + for block in self.model: + x = block(x) + + output = self.projection(x.transpose(1, 2)) + + if target_indices is not None: + output = output[:, target_indices, :] + + output = output.transpose(1, 2) + + return output From f62c902d5e96ff01809805a903cfc93c02e04e46 Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 11:35:52 +0100 Subject: [PATCH 08/23] [TSMixer v2] for TSMixer class add _prepare_input_data() --- .../models/tsmixer/_tsmixer_v2.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py index 5064ef956..576cb8b0f 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -186,3 +186,32 @@ def _encoder(self, x: torch.Tensor, target_indices: torch.Tensor | None) -> torc output = output.transpose(1, 2) return output + + def _prepare_input_data(self, x: dict[str, torch.Tensor]): + """Prepare input data and target indices for model input.""" + + available_features = [] + target_indices = [] + current_idx = 0 + + if "history_cont" in x and x["history_cont"].size(-1) > 0: + available_features.append(x["history_cont"]) + current_idx += x["history_cont"].size(-1) + + if "history_target" in x and x["history_target"].size(-1) > 0: + n_targets = x["history_target"].size(-1) + target_indices = list(range(current_idx, current_idx + n_targets)) + available_features.append(x["history_target"]) + + if not available_features: + raise ValueError("No valid input features found in the input dictionary.") + + input_data = torch.cat(available_features, dim=-1) + + target_indices = ( + torch.tensor(target_indices, dtype=torch.long, device=input_data.device) + if target_indices + else None + ) + + return input_data, target_indices From 0f35a8d2ab2f643915dcd2e5c913b07a6718adbc Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 11:39:00 +0100 Subject: [PATCH 09/23] [TSMixer v2] for TSMixer class add forward() --- .../models/tsmixer/_tsmixer_v2.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py index 576cb8b0f..5c20f7ccb 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -215,3 +215,27 @@ def _prepare_input_data(self, x: dict[str, torch.Tensor]): ) return input_data, target_indices + + def forward(self, x: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: + """ + Forward pass of the TSMixer model. + + Parameters + ---------- + x : dict[str, torch.Tensor] + Dictionary containing input tensors. + + Returns + ------- + dict[str, torch.Tensor] + Dictionary containing the model prediction. + """ + + input_data, target_indices = self._prepare_input_data(x) + + prediction = self._encoder(input_data, target_indices) + + if "target_scale" in x and hasattr(self, "transform_output"): + prediction = self.transform_output(prediction, x["target_scale"]) + + return {"prediction": prediction} From 617f1e5992b6e3bc183e8f636c7909a56891eee7 Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 12:26:20 +0100 Subject: [PATCH 10/23] [TSMixer v2] add package configuration --- .../models/tsmixer/_tsmixer_pkg_v2.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 pytorch_forecasting/models/tsmixer/_tsmixer_pkg_v2.py diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_pkg_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_pkg_v2.py new file mode 100644 index 000000000..a9790818a --- /dev/null +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_pkg_v2.py @@ -0,0 +1,84 @@ +""" +Packages container for TSMixer model. +""" + +from pytorch_forecasting.base._base_pkg import Base_pkg + + +class TSMixer_pkg_v2(Base_pkg): + """TSMixer package container.""" + + _tags = { + "info:name": "TSMixer", + "info:compute": 2, + "authors": ["seaic-mac-murchadha"], + "info:y_type": ["numeric"], + "capability:exogenous": True, + "capability:multivariate": True, + "capability:pred_int": False, + "capability:flexible_history_length": False, + "capability:cold_start": False, + } + + @classmethod + def get_cls(cls): + """Get model class.""" + from pytorch_forecasting.models.tsmixer._tsmixer_v2 import TSMixer + + return TSMixer + + @classmethod + def get_datamodule_cls(cls): + """Get the underlying DataModule class.""" + from pytorch_forecasting.data.data_module import TslibDataModule + + return TslibDataModule + + @classmethod + def get_test_train_params(cls): + """ + Return testing parameter settings for the trainer. + + Returns + ------- + list[dict] + Parameter configurations used to create testing instances of the TSMixer class. + """ + + import torch.nn as nn + + from pytorch_forecasting.metrics import SMAPE + + params = [ + {}, + dict( + d_model=64, + e_layers=2, + dropout=0.1, + logging_metrics=[SMAPE()], + ), + dict( + d_model=32, + e_layers=1, + dropout=0.0, + loss=nn.MSELoss(), + ), + dict( + optimizer="adamw", + optimizer_params={"lr": 1e-3}, + ), + ] + + default_dm_cfg = { + "context_length": 8, + "prediction_length": 2, + } + + for param in params: + current_dm_cfg = param.get("datamodule_cfg", {}) + param["datamodule_cfg"] = { + **default_dm_cfg, + **current_dm_cfg, + } + + return params \ No newline at end of file From ac7990a53dca9a83ad3d2218770aec115ffba009 Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 12:48:52 +0100 Subject: [PATCH 11/23] [TSMixer v2] support QuantileLoss --- .../models/tsmixer/_tsmixer_pkg_v2.py | 4 +-- .../models/tsmixer/_tsmixer_v2.py | 29 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_pkg_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_pkg_v2.py index a9790818a..ca68f7674 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_pkg_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_pkg_v2.py @@ -15,7 +15,7 @@ class TSMixer_pkg_v2(Base_pkg): "info:y_type": ["numeric"], "capability:exogenous": True, "capability:multivariate": True, - "capability:pred_int": False, + "capability:pred_int": True, "capability:flexible_history_length": False, "capability:cold_start": False, } @@ -81,4 +81,4 @@ def get_test_train_params(cls): **current_dm_cfg, } - return params \ No newline at end of file + return params diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py index 5c20f7ccb..505d2bf20 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -16,6 +16,7 @@ import torch.nn as nn from torch.optim import Optimizer +from pytorch_forecasting.metrics import QuantileLoss from pytorch_forecasting.models.base._tslib_base_model_v2 import TslibBaseModel @@ -150,9 +151,16 @@ def _init_network(self): ] ) + self.n_quantiles = None + output_dim = self.prediction_length + + if isinstance(self._loss, QuantileLoss): + self.n_quantiles = len(self._loss.quantiles) + output_dim *= self.n_quantiles + self.projection = nn.Linear( self.context_length, - self.prediction_length, + output_dim, ) def _encoder(self, x: torch.Tensor, target_indices: torch.Tensor | None) -> torch.Tensor: @@ -171,8 +179,11 @@ def _encoder(self, x: torch.Tensor, target_indices: torch.Tensor | None) -> torc Returns ------- torch.Tensor - Forecast tensor of shape - (batch_size, prediction_length, n_targets). + Forecast tensor. + For point forecasts, returns shape + (batch_size, prediction_length, n_targets). + For quantile forecasts, returns shape + (batch_size, prediction_length, n_quantiles). """ for block in self.model: @@ -183,9 +194,17 @@ def _encoder(self, x: torch.Tensor, target_indices: torch.Tensor | None) -> torc if target_indices is not None: output = output[:, target_indices, :] - output = output.transpose(1, 2) + if self.n_quantiles is None: + return output.transpose(1, 2) - return output + if output.shape[1] != 1: + raise ValueError("Quantile forecasting currently only supports a single target.") + + return output.squeeze(1).reshape( + output.shape[0], + self.prediction_length, + self.n_quantiles, + ) def _prepare_input_data(self, x: dict[str, torch.Tensor]): """Prepare input data and target indices for model input.""" From 3a17d25a947cb68009eaeca5865c6f3034d28823 Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 13:09:03 +0100 Subject: [PATCH 12/23] [TSMixer v2] for testing add sample dataset --- tests/test_models/test_tsmixer_v2.py | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/test_models/test_tsmixer_v2.py diff --git a/tests/test_models/test_tsmixer_v2.py b/tests/test_models/test_tsmixer_v2.py new file mode 100644 index 000000000..d8d419c74 --- /dev/null +++ b/tests/test_models/test_tsmixer_v2.py @@ -0,0 +1,56 @@ +import numpy as np +import pandas as pd +import pytest +import torch +from torch import nn + +from pytorch_forecasting.data import TimeSeries +from pytorch_forecasting.data.data_module import TslibDataModule +from pytorch_forecasting.metrics import MAE, SMAPE, QuantileLoss +from pytorch_forecasting.models.tsmixer._tsmixer_v2 import TSMixer + + +@pytest.fixture +def sample_dataset(): + """Create a sample dataset for testing using v2.""" + n_samples = 100 + n_series = 3 + + time_idx = np.arange(n_samples) + + series_data = [] + for i in range(n_series): + trend = 0.1 * time_idx + seasonality = 10 * np.sin(2 * np.pi * time_idx / 20) + noise = np.random.normal(0, 1, n_samples) + values = trend + seasonality + noise + + series = pd.DataFrame( + { + "time_idx": time_idx, + "series_id": i, + "value": values, + "feat1": np.random.normal(0, 1, n_samples), + "feat2": np.random.normal(0, 1, n_samples), + } + ) + series_data.append(series) + + data = pd.concat(series_data).reset_index(drop=True) + + ts = TimeSeries( + data, + time="time_idx", + group=["series_id"], + target=["value"], + num=["feat1", "feat2"], + cat=[], + known=["time_idx"], + unknown=["value", "feat1", "feat2"], + ) + + dm = TslibDataModule(ts, context_length=16, prediction_length=4, batch_size=4) + + dm.setup() + + return {"data_module": dm, "time_series": ts} From e2ce3ec8c3475c518187ea168d4e49cc586299fc Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 13:27:43 +0100 Subject: [PATCH 13/23] [TSMixer v2] add model tests --- tests/test_models/test_tsmixer_v2.py | 163 ++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/tests/test_models/test_tsmixer_v2.py b/tests/test_models/test_tsmixer_v2.py index d8d419c74..cefc608d0 100644 --- a/tests/test_models/test_tsmixer_v2.py +++ b/tests/test_models/test_tsmixer_v2.py @@ -6,7 +6,7 @@ from pytorch_forecasting.data import TimeSeries from pytorch_forecasting.data.data_module import TslibDataModule -from pytorch_forecasting.metrics import MAE, SMAPE, QuantileLoss +from pytorch_forecasting.metrics import MAE, QuantileLoss, SMAPE from pytorch_forecasting.models.tsmixer._tsmixer_v2 import TSMixer @@ -54,3 +54,164 @@ def sample_dataset(): dm.setup() return {"data_module": dm, "time_series": ts} + + +@pytest.fixture +def model_with_logging_metrics(sample_dataset): + """TSMixer instance used to test BaseModel logging_metrics registration.""" + dm = sample_dataset["data_module"] + with pytest.warns(UserWarning): + model = TSMixer( + loss=MAE(), + logging_metrics=[SMAPE(), MAE()], + metadata=dm.metadata, + ) + return model + + +@pytest.mark.parametrize( + "d_model, e_layers, dropout", + [ + (32, 1, 0.0), + (64, 2, 0.1), + ], +) +def test_tsmixer_init(d_model, e_layers, dropout, sample_dataset): + """Test TSMixer initialization.""" + + dm = sample_dataset["data_module"] + + model = TSMixer( + loss=MAE(), + d_model=d_model, + e_layers=e_layers, + dropout=dropout, + metadata=dm.metadata, + ) + + assert model.d_model == d_model + assert model.e_layers == e_layers + assert model.dropout == dropout + assert model.n_quantiles is None + + +def test_tsmixer_forward(sample_dataset): + """Test forward pass of TSMixer.""" + + dm = sample_dataset["data_module"] + + train_dataloader = dm.train_dataloader() + batch = next(iter(train_dataloader))[0] + + metadata = dm.metadata + + model = TSMixer( + loss=MAE(), + d_model=32, + e_layers=2, + dropout=0.1, + metadata=metadata, + ) + + with torch.no_grad(): + output = model(batch) + + assert "prediction" in output + assert output["prediction"].shape[0] == dm.batch_size + assert output["prediction"].shape[1] == metadata["prediction_length"] + + +def test_quantile_loss_output(sample_dataset): + """Test TSMixer output shape with quantile loss.""" + + dm = sample_dataset["data_module"] + + train_dataloader = dm.train_dataloader() + batch = next(iter(train_dataloader))[0] + + metadata = dm.metadata + + quantiles = [0.1, 0.5, 0.9] + + model = TSMixer( + loss=QuantileLoss(quantiles=quantiles), + d_model=32, + e_layers=2, + dropout=0.1, + logging_metrics=[SMAPE(), MAE()], + metadata=metadata, + ) + + with torch.no_grad(): + output = model(batch) + + assert "prediction" in output + pred = output["prediction"] + assert pred.shape == ( + dm.batch_size, + metadata["prediction_length"], + len(quantiles), + ) + + +def test_univariate_forecast(): + """Test univariate forecasting with TSMixer.""" + + n_samples = 100 + time_idx = np.arange(n_samples) + values = np.sin(2 * np.pi * time_idx / 20) + np.random.normal(0, 0.1, n_samples) + + series = pd.DataFrame({"time_idx": time_idx, "series_id": 0, "value": values}) + + ts = TimeSeries( + series, + time="time_idx", + group=["series_id"], + target=["value"], + num=[], + cat=[], + known=["time_idx"], + unknown=["value"], + ) + + dm = TslibDataModule(ts, context_length=16, prediction_length=4, batch_size=4) + + dm.setup() + + metadata = dm.metadata + + model = TSMixer( + loss=MAE(), + d_model=32, + e_layers=1, + dropout=0.1, + metadata=metadata, + ) + + train_dataloader = dm.train_dataloader() + batch = next(iter(train_dataloader))[0] + + with torch.no_grad(): + output = model(batch) + + assert "prediction" in output + assert output["prediction"].shape == ( + dm.batch_size, + metadata["prediction_length"], + 1, + ) + + +def test_logging_metrics_is_module_list(model_with_logging_metrics): + """logging_metrics must be registered as nn.ModuleList so .to() propagates.""" + assert isinstance(model_with_logging_metrics.logging_metrics, nn.ModuleList) + + +def test_logging_metrics_device_propagation(model_with_logging_metrics): + """Metric state tensors must follow the model when moved to a different device.""" + model_with_logging_metrics.to("meta") + for metric in model_with_logging_metrics.logging_metrics: + for state_name in metric._defaults: + val = getattr(metric, state_name) + if isinstance(val, torch.Tensor): + assert val.device.type == "meta" From b0338c7db16c740a648ac4d6b33be6c2543041e6 Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 13:36:51 +0100 Subject: [PATCH 14/23] [TSMixer v2] update _prepare_input_data() --- .../models/tsmixer/_tsmixer_v2.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py index 505d2bf20..c050f0941 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -210,27 +210,25 @@ def _prepare_input_data(self, x: dict[str, torch.Tensor]): """Prepare input data and target indices for model input.""" available_features = [] - target_indices = [] current_idx = 0 if "history_cont" in x and x["history_cont"].size(-1) > 0: available_features.append(x["history_cont"]) current_idx += x["history_cont"].size(-1) - if "history_target" in x and x["history_target"].size(-1) > 0: - n_targets = x["history_target"].size(-1) - target_indices = list(range(current_idx, current_idx + n_targets)) - available_features.append(x["history_target"]) + if "history_target" not in x or x["history_target"].size(-1) == 0: + raise ValueError("No target history found in the input dictionary.") - if not available_features: - raise ValueError("No valid input features found in the input dictionary.") + n_targets = x["history_target"].size(-1) + target_indices = list(range(current_idx, current_idx + n_targets)) + available_features.append(x["history_target"]) input_data = torch.cat(available_features, dim=-1) - target_indices = ( - torch.tensor(target_indices, dtype=torch.long, device=input_data.device) - if target_indices - else None + target_indices = torch.tensor( + target_indices, + dtype=torch.long, + device=input_data.device ) return input_data, target_indices From d48bb62337cafe07b6ad2db2c30bb2868f5524bc Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 13:37:11 +0100 Subject: [PATCH 15/23] [TSMixer v2] add testing for _prepare_input_data() --- tests/test_models/test_tsmixer_v2.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_models/test_tsmixer_v2.py b/tests/test_models/test_tsmixer_v2.py index cefc608d0..b6efc20aa 100644 --- a/tests/test_models/test_tsmixer_v2.py +++ b/tests/test_models/test_tsmixer_v2.py @@ -215,3 +215,29 @@ def test_logging_metrics_device_propagation(model_with_logging_metrics): val = getattr(metric, state_name) if isinstance(val, torch.Tensor): assert val.device.type == "meta" + + +def test_prepare_input_data(sample_dataset): + """Test preparation of continuous and target historical data.""" + + dm = sample_dataset["data_module"] + batch = next(iter(dm.train_dataloader()))[0] # One sample batch + + model = TSMixer( + loss=MAE(), + metadata=dm.metadata, + ) + + input_data, target_indices = model._prepare_input_data(batch) + + assert input_data.shape[-1] == ( + batch["history_cont"].shape[-1] + + batch["history_target"].shape[-1] + ) + + assert target_indices.tolist() == [ + batch["history_cont"].shape[-1] + ] + + assert target_indices.dtype == torch.long + assert target_indices.device == input_data.device From 8e0c43702a6c9bfbce6bb96063bae5cbb435d600 Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 13:47:48 +0100 Subject: [PATCH 16/23] [TSMixer v2] add testing for error handling --- tests/test_models/test_tsmixer_v2.py | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_models/test_tsmixer_v2.py b/tests/test_models/test_tsmixer_v2.py index b6efc20aa..928582099 100644 --- a/tests/test_models/test_tsmixer_v2.py +++ b/tests/test_models/test_tsmixer_v2.py @@ -241,3 +241,50 @@ def test_prepare_input_data(sample_dataset): assert target_indices.dtype == torch.long assert target_indices.device == input_data.device + +def test_prepare_input_data_error_for_no_history(sample_dataset): + """Test _prepare_input_data without the required history.""" + + dm = sample_dataset["data_module"] + batch = next(iter(dm.train_dataloader()))[0] + + model = TSMixer( + loss=MAE(), + metadata=dm.metadata, + ) + + batch_without_target = { + key: value + for key, value in batch.items() + if key != "history_target" + } + + with pytest.raises( + ValueError, + match="No target history found in the input dictionary.", + ): + model._prepare_input_data(batch_without_target) + +def test_quantile_loss_error_on_multiple_targets(sample_dataset): + """Test error for quantile forecasting with multiple targets.""" + + dm = sample_dataset["data_module"] + + model = TSMixer( + loss=QuantileLoss(quantiles=[0.1, 0.5, 0.9]), + metadata=dm.metadata, + ) + + batch_size = dm.batch_size + context_length = dm.metadata["context_length"] + n_features = model.enc_in + + x = torch.randn(batch_size, context_length, n_features) + + target_indices = torch.tensor([0, 1], dtype=torch.long) + + with pytest.raises( + ValueError, + match="Quantile forecasting currently only supports a single target.", + ): + model._encoder(x, target_indices) From ba79905dfb0d250773ceef759598d6daec851ebc Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 13:59:27 +0100 Subject: [PATCH 17/23] [TSMixer v2] apply ruff formatting --- pytorch_forecasting/models/tsmixer/__init__.py | 2 +- .../models/tsmixer/_tsmixer_pkg_v2.py | 3 ++- .../models/tsmixer/_tsmixer_v2.py | 18 +++++++++--------- tests/test_models/test_tsmixer_v2.py | 15 ++++++--------- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/pytorch_forecasting/models/tsmixer/__init__.py b/pytorch_forecasting/models/tsmixer/__init__.py index 58cacd2fa..5d2fbdc26 100644 --- a/pytorch_forecasting/models/tsmixer/__init__.py +++ b/pytorch_forecasting/models/tsmixer/__init__.py @@ -2,7 +2,7 @@ TSMixer model for time series forecasting. """ -from pytorch_forecasting.models.tsmixer._tsmixer_v2 import TSMixer from pytorch_forecasting.models.tsmixer._tsmixer_pkg_v2 import TSMixer_pkg_v2 +from pytorch_forecasting.models.tsmixer._tsmixer_v2 import TSMixer __all__ = ["TSMixer", "TSMixer_pkg_v2"] diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_pkg_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_pkg_v2.py index ca68f7674..b6a812715 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_pkg_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_pkg_v2.py @@ -42,7 +42,8 @@ def get_test_train_params(cls): Returns ------- list[dict] - Parameter configurations used to create testing instances of the TSMixer class. + Parameter configurations used to create testing instances of the TSMixer + class. """ import torch.nn as nn diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py index c050f0941..27a6157ee 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -128,9 +128,7 @@ def __init__( self.e_layers = e_layers self.dropout = dropout - self.save_hyperparameters( - ignore=["loss", "logging_metrics", "metadata"] - ) + self.save_hyperparameters(ignore=["loss", "logging_metrics", "metadata"]) self._init_network() @@ -162,8 +160,10 @@ def _init_network(self): self.context_length, output_dim, ) - - def _encoder(self, x: torch.Tensor, target_indices: torch.Tensor | None) -> torch.Tensor: + + def _encoder( + self, x: torch.Tensor, target_indices: torch.Tensor | None + ) -> torch.Tensor: """ Encode the input time series through the TSMixer blocks. @@ -198,7 +198,9 @@ def _encoder(self, x: torch.Tensor, target_indices: torch.Tensor | None) -> torc return output.transpose(1, 2) if output.shape[1] != 1: - raise ValueError("Quantile forecasting currently only supports a single target.") + raise ValueError( + "Quantile forecasting currently only supports a single target." + ) return output.squeeze(1).reshape( output.shape[0], @@ -226,9 +228,7 @@ def _prepare_input_data(self, x: dict[str, torch.Tensor]): input_data = torch.cat(available_features, dim=-1) target_indices = torch.tensor( - target_indices, - dtype=torch.long, - device=input_data.device + target_indices, dtype=torch.long, device=input_data.device ) return input_data, target_indices diff --git a/tests/test_models/test_tsmixer_v2.py b/tests/test_models/test_tsmixer_v2.py index 928582099..e61840513 100644 --- a/tests/test_models/test_tsmixer_v2.py +++ b/tests/test_models/test_tsmixer_v2.py @@ -6,7 +6,7 @@ from pytorch_forecasting.data import TimeSeries from pytorch_forecasting.data.data_module import TslibDataModule -from pytorch_forecasting.metrics import MAE, QuantileLoss, SMAPE +from pytorch_forecasting.metrics import MAE, SMAPE, QuantileLoss from pytorch_forecasting.models.tsmixer._tsmixer_v2 import TSMixer @@ -231,17 +231,15 @@ def test_prepare_input_data(sample_dataset): input_data, target_indices = model._prepare_input_data(batch) assert input_data.shape[-1] == ( - batch["history_cont"].shape[-1] - + batch["history_target"].shape[-1] + batch["history_cont"].shape[-1] + batch["history_target"].shape[-1] ) - assert target_indices.tolist() == [ - batch["history_cont"].shape[-1] - ] + assert target_indices.tolist() == [batch["history_cont"].shape[-1]] assert target_indices.dtype == torch.long assert target_indices.device == input_data.device + def test_prepare_input_data_error_for_no_history(sample_dataset): """Test _prepare_input_data without the required history.""" @@ -254,9 +252,7 @@ def test_prepare_input_data_error_for_no_history(sample_dataset): ) batch_without_target = { - key: value - for key, value in batch.items() - if key != "history_target" + key: value for key, value in batch.items() if key != "history_target" } with pytest.raises( @@ -265,6 +261,7 @@ def test_prepare_input_data_error_for_no_history(sample_dataset): ): model._prepare_input_data(batch_without_target) + def test_quantile_loss_error_on_multiple_targets(sample_dataset): """Test error for quantile forecasting with multiple targets.""" From f9d483760394f50b12c937fe8a658b080b058616 Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Thu, 20 Aug 2026 15:47:52 +0100 Subject: [PATCH 18/23] [TSMixer v2] add test for forward prediction with target scaling provided --- tests/test_models/test_tsmixer_v2.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_models/test_tsmixer_v2.py b/tests/test_models/test_tsmixer_v2.py index e61840513..522f24968 100644 --- a/tests/test_models/test_tsmixer_v2.py +++ b/tests/test_models/test_tsmixer_v2.py @@ -285,3 +285,30 @@ def test_quantile_loss_error_on_multiple_targets(sample_dataset): match="Quantile forecasting currently only supports a single target.", ): model._encoder(x, target_indices) + + +def test_forward_with_target_scale(sample_dataset): + """Test that prediction uses target scaling when target_scale is provided.""" + dm = sample_dataset["data_module"] + batch = next(iter(dm.train_dataloader()))[0] + + model = TSMixer( + loss=MAE(), + metadata=dm.metadata, + ) + + batch["target_scale"] = torch.ones(batch["history_target"].shape[0], 2) + + captured = {} + + def mock_transform_output(prediction, target_scale): + captured["prediction"] = prediction + captured["target_scale"] = target_scale + return prediction + 1 + + model.transform_output = mock_transform_output + + prediction = model(batch)["prediction"] + + assert torch.equal(captured["target_scale"], batch["target_scale"]) + assert torch.allclose(prediction, captured["prediction"] + 1) From d2e947094dbc614bb3ae5c0600d8811a72850a38 Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Sun, 23 Aug 2026 22:47:15 +0100 Subject: [PATCH 19/23] Add docstrings to TSMixerBlock --- .../models/tsmixer/_tsmixer_v2.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py index 27a6157ee..8234e249d 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -21,6 +21,20 @@ class TSMixerBlock(nn.Module): + """ + TSMixer block for applying the time-mixing and feature-mixing MLPs. + + Parameters + ---------- + sequence_length : int + Length of the lookback window containing past time steps. + num_features : int + Number of expected features in the input. + hidden_dim : int + Dimension of the hidden layers. + dropout : float + Probability of an element to be zeroed. + """ def __init__( self, sequence_length: int, @@ -45,6 +59,19 @@ def __init__( ) def forward(self, x: torch.Tensor) -> torch.Tensor: + """ + Apply time-mixing and feature-mixing to the input tensor. + + Parameters + ---------- + x : torch.Tensor + Input tensor. + + Returns + ------- + torch.Tensor + Output tensor with time-mixing and feature-mixing applied. + """ x = x + self.temporal(x.transpose(1, 2)).transpose(1, 2) x = x + self.channel(x) From af66d3f5d1733700b5fb9fa7729da21f2659148b Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Sun, 23 Aug 2026 23:06:48 +0100 Subject: [PATCH 20/23] Remove redundant experimental model warnings --- pytorch_forecasting/models/tsmixer/_tsmixer_v2.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py index 8234e249d..81a2a8306 100644 --- a/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py +++ b/pytorch_forecasting/models/tsmixer/_tsmixer_v2.py @@ -10,7 +10,6 @@ ################################################# from typing import Any -import warnings import torch import torch.nn as nn @@ -35,6 +34,7 @@ class TSMixerBlock(nn.Module): dropout : float Probability of an element to be zeroed. """ + def __init__( self, sequence_length: int, @@ -145,12 +145,6 @@ def __init__( metadata=metadata, ) - warnings.warn( - "TSMixer is an experimental model implemented on TslibBaseModelV2. " - "It is an unstable version and may be subject to unannounced changes. " - "Please use with caution." - ) - self.d_model = d_model self.e_layers = e_layers self.dropout = dropout From d79be744513887970a98e85d0fd574566d5ddf8d Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Sun, 23 Aug 2026 23:18:23 +0100 Subject: [PATCH 21/23] Remove redundant TSMixer forward test --- tests/test_models/test_tsmixer_v2.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/tests/test_models/test_tsmixer_v2.py b/tests/test_models/test_tsmixer_v2.py index 522f24968..0a288d8ff 100644 --- a/tests/test_models/test_tsmixer_v2.py +++ b/tests/test_models/test_tsmixer_v2.py @@ -95,32 +95,6 @@ def test_tsmixer_init(d_model, e_layers, dropout, sample_dataset): assert model.n_quantiles is None -def test_tsmixer_forward(sample_dataset): - """Test forward pass of TSMixer.""" - - dm = sample_dataset["data_module"] - - train_dataloader = dm.train_dataloader() - batch = next(iter(train_dataloader))[0] - - metadata = dm.metadata - - model = TSMixer( - loss=MAE(), - d_model=32, - e_layers=2, - dropout=0.1, - metadata=metadata, - ) - - with torch.no_grad(): - output = model(batch) - - assert "prediction" in output - assert output["prediction"].shape[0] == dm.batch_size - assert output["prediction"].shape[1] == metadata["prediction_length"] - - def test_quantile_loss_output(sample_dataset): """Test TSMixer output shape with quantile loss.""" From 3b8e43c0b738b8e7ac0e2718494e065516e617cf Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Mon, 24 Aug 2026 20:58:01 +0100 Subject: [PATCH 22/23] Add TSMixer v2 API reference for the model and pkg --- docs/source/m_layer_v2.rst | 1 + docs/source/pkg_v2.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/source/m_layer_v2.rst b/docs/source/m_layer_v2.rst index 3b291d4ca..cd36fa93b 100644 --- a/docs/source/m_layer_v2.rst +++ b/docs/source/m_layer_v2.rst @@ -47,6 +47,7 @@ See the detailed API documentation for the V2 base classes and specific model im models.samformer._samformer_v2.Samformer models.tide._tide_dsipts._tide_v2.TIDE models.timexer._timexer_v2.TimeXer + models.tsmixer._tsmixer_v2.TSMixer models.mlp._decodermlp_v2.DecoderMLP_v2 models.softs._softs_v2.SOFTS models.scinet._scinet_v2.SCINet_v2 diff --git a/docs/source/pkg_v2.rst b/docs/source/pkg_v2.rst index cfad5c386..99e8f3d77 100644 --- a/docs/source/pkg_v2.rst +++ b/docs/source/pkg_v2.rst @@ -99,6 +99,7 @@ See the detailed API documentation for the available V2 Package classes below: models.samformer._samformer_v2_pkg.Samformer_pkg_v2 models.tide._tide_dsipts._tide_v2_pkg.TIDE_pkg_v2 models.timexer._timexer_pkg_v2.TimeXer_pkg_v2 + models.tsmixer._tsmixer_pkg_v2.TSMixer_pkg_v2 models.mlp._decodermlp_pkg_v2.DecoderMLP_pkg_v2 models.softs._softs_pkg_v2.SOFTS_pkg_v2 models.scinet._scinet_pkg_v2.SCINet_pkg_v2 From ca2f59de08496bbf68d3c7af00c0821a8894282b Mon Sep 17 00:00:00 2001 From: Seaic Mac Murchadha Date: Mon, 24 Aug 2026 21:04:00 +0100 Subject: [PATCH 23/23] Remove non-model specific tests for TSMixer v2 --- tests/test_models/test_tsmixer_v2.py | 55 ---------------------------- 1 file changed, 55 deletions(-) diff --git a/tests/test_models/test_tsmixer_v2.py b/tests/test_models/test_tsmixer_v2.py index 0a288d8ff..b23ffd228 100644 --- a/tests/test_models/test_tsmixer_v2.py +++ b/tests/test_models/test_tsmixer_v2.py @@ -2,7 +2,6 @@ import pandas as pd import pytest import torch -from torch import nn from pytorch_forecasting.data import TimeSeries from pytorch_forecasting.data.data_module import TslibDataModule @@ -56,45 +55,6 @@ def sample_dataset(): return {"data_module": dm, "time_series": ts} -@pytest.fixture -def model_with_logging_metrics(sample_dataset): - """TSMixer instance used to test BaseModel logging_metrics registration.""" - dm = sample_dataset["data_module"] - with pytest.warns(UserWarning): - model = TSMixer( - loss=MAE(), - logging_metrics=[SMAPE(), MAE()], - metadata=dm.metadata, - ) - return model - - -@pytest.mark.parametrize( - "d_model, e_layers, dropout", - [ - (32, 1, 0.0), - (64, 2, 0.1), - ], -) -def test_tsmixer_init(d_model, e_layers, dropout, sample_dataset): - """Test TSMixer initialization.""" - - dm = sample_dataset["data_module"] - - model = TSMixer( - loss=MAE(), - d_model=d_model, - e_layers=e_layers, - dropout=dropout, - metadata=dm.metadata, - ) - - assert model.d_model == d_model - assert model.e_layers == e_layers - assert model.dropout == dropout - assert model.n_quantiles is None - - def test_quantile_loss_output(sample_dataset): """Test TSMixer output shape with quantile loss.""" @@ -176,21 +136,6 @@ def test_univariate_forecast(): ) -def test_logging_metrics_is_module_list(model_with_logging_metrics): - """logging_metrics must be registered as nn.ModuleList so .to() propagates.""" - assert isinstance(model_with_logging_metrics.logging_metrics, nn.ModuleList) - - -def test_logging_metrics_device_propagation(model_with_logging_metrics): - """Metric state tensors must follow the model when moved to a different device.""" - model_with_logging_metrics.to("meta") - for metric in model_with_logging_metrics.logging_metrics: - for state_name in metric._defaults: - val = getattr(metric, state_name) - if isinstance(val, torch.Tensor): - assert val.device.type == "meta" - - def test_prepare_input_data(sample_dataset): """Test preparation of continuous and target historical data."""