Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cd931ac
[ENH] softs_v2 Model added
Muhammad-Rebaal Mar 25, 2026
c90e494
fix: ruff issue fixed
Muhammad-Rebaal Mar 25, 2026
6026e0e
fix: ruff issue fixed
Muhammad-Rebaal Mar 25, 2026
fcb7cad
Author and ref added
Muhammad-Rebaal Mar 25, 2026
2db264b
Merge branch 'main' into softs_model
Muhammad-Rebaal Apr 8, 2026
5026e82
fix : Pytest failure
Muhammad-Rebaal Apr 9, 2026
4d19fa8
Merge branch 'softs_model' of https://github.com/Muhammad-Rebaal/pyto…
Muhammad-Rebaal Apr 9, 2026
a44150c
fix : Code Quality
Muhammad-Rebaal Apr 9, 2026
927c50e
Merge branch 'main' into softs_model
Muhammad-Rebaal Apr 29, 2026
b2aa1f6
Merge branch 'main' into softs_model
Muhammad-Rebaal May 11, 2026
a18a499
Merge branch 'main' into softs_model
phoeenniixx Jun 1, 2026
0087cb8
Merge branch 'main' into softs_model
Muhammad-Rebaal Jun 3, 2026
8fb4616
Added Docstring, removed v1 method, add params
Muhammad-Rebaal Jun 4, 2026
0afece8
Merge branch 'main' into softs_model
Muhammad-Rebaal Jun 15, 2026
b3f75a9
Merge branch 'main' into softs_model
Muhammad-Rebaal Jun 19, 2026
ed02f22
fix:pytest error
Muhammad-Rebaal Jun 19, 2026
7a33e11
Merge branch 'main' into softs_model
Muhammad-Rebaal Jun 19, 2026
b460ae8
Merge branch 'main' into softs_model
Muhammad-Rebaal Jun 20, 2026
d7b34b9
Merge branch 'main' into softs_model
phoeenniixx Jun 22, 2026
dae93e8
Updated the api ref & added loss func
Muhammad-Rebaal Jun 23, 2026
c0f6f17
Added reference, code refactor and use base_model class
Muhammad-Rebaal Jun 24, 2026
c8ca76f
Added pytest fix
Muhammad-Rebaal Jun 24, 2026
f1a2614
Merge branch 'main' into softs_model
Muhammad-Rebaal Jun 24, 2026
d35353c
ENH: Migrated from TslibDataModule to EncoderDecoderTimeSeriesDataModule
Muhammad-Rebaal Jun 26, 2026
5c75c71
Merge branch 'main' into softs_model
Muhammad-Rebaal Jun 30, 2026
9a71c1d
fix : removed predict_step method
Muhammad-Rebaal Jul 2, 2026
f35af9a
Merge branch 'main' into softs_model
Muhammad-Rebaal Jul 14, 2026
c8528ed
Merge branch 'main' into softs_model
Muhammad-Rebaal Jul 29, 2026
844d82b
Fix: Moved the SoftsEncoder to _encoder directory
Muhammad-Rebaal Jul 29, 2026
371fca3
Merge branch 'main' of https://github.com/Muhammad-Rebaal/pytorch-for…
Muhammad-Rebaal Aug 3, 2026
aa653fa
Fix: Code Quality
Muhammad-Rebaal Aug 3, 2026
dd94e8b
Merge branch 'main' into softs_model
phoeenniixx Aug 8, 2026
55a1bc5
Merge branch 'main' into softs_model
Muhammad-Rebaal Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion pytorch_forecasting/layers/_blocks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
from pytorch_forecasting.layers._blocks._residual_block_dsipts import ResidualBlock
from pytorch_forecasting.layers._blocks._softs_block import (
SoftsEncoderLayer,
STADModule,
)

__all__ = ["ResidualBlock"]
__all__ = [
"ResidualBlock",
"STADModule",
"SoftsEncoderLayer",
]
114 changes: 114 additions & 0 deletions pytorch_forecasting/layers/_blocks/_softs_block.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""
SOFTS Blocks for Star Aggregate-Dispatch Network.
"""

import torch
import torch.nn as nn


class STADModule(nn.Module):
"""
Star Aggregate-Dispatch (STAD) Module for capturing inter-series dependencies.

Uses a star-topology to aggregate all channels into a central node,
process it via an MLP, and dispatch back — achieving O(C) cross-channel
mixing instead of O(C²) self-attention.

Parameters
----------
d_model : int
Embedding dimension per channel per time step.
d_core : int
Dimension of the central star node (information bottleneck).
dropout : float, default=0.0
Dropout probability inside the channel-mixing MLP.
"""

def __init__(self, d_model: int, d_core: int, dropout: float = 0.0):
super().__init__()
self.channel_mixing = nn.Sequential(
nn.Linear(d_model, d_model),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(d_model, d_model),
)
self.gen_weight = nn.Linear(d_model, d_core)

def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Aggregate channel features into a star node and dispatch back.

Parameters
----------
x : torch.Tensor
Shape ``(batch_size, n_channels, seq_len, d_model)``.

Returns
-------
torch.Tensor
Same shape as input, enriched with cross-channel context.
"""

B, C, L, D = x.shape

w = self.gen_weight(x).mean(dim=2)
w = torch.softmax(w, dim=1)

x_pooled = x.mean(dim=2)
core_node = torch.einsum("bcd,bce->bed", x_pooled, w)
core_node = self.channel_mixing(core_node)
dispatch_out = torch.einsum("bed,bce->bcd", core_node, w)

dispatch_out = dispatch_out.unsqueeze(2).repeat(1, 1, L, 1)
return x + dispatch_out


class SoftsEncoderLayer(nn.Module):
"""
Single encoder layer for SOFTS, combining STAD and a Feed-Forward Network.

Applies Pre-LayerNorm STAD (cross-channel) then FFN (within-channel)
with residual connections, following the Pre-LN Transformer convention.

Parameters
----------
d_model : int
Embedding dimension per channel per time step.
d_core : int
Dimension of the central star node in the STAD sub-layer.
d_ff : int
Hidden dimension of the feed-forward network (typically 4 x d_model).
dropout : float, default=0.0
Dropout probability applied after the STAD and FFN sub-layers.
"""

def __init__(self, d_model: int, d_core: int, d_ff: int, dropout: float = 0.0):
super().__init__()
self.stad = STADModule(d_model=d_model, d_core=d_core, dropout=dropout)
self.ffn = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(d_ff, d_model),
)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)

def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Apply one SOFTS encoder layer: STAD sub-layer then FFN sub-layer.

Parameters
----------
x : torch.Tensor
Input tensor of shape ``(batch_size, n_channels, seq_len, d_model)``.

Returns
-------
torch.Tensor
Output tensor of shape ``(batch_size, n_channels, seq_len, d_model)``.
"""
x = x + self.dropout(self.stad(self.norm1(x)))
x = x + self.dropout(self.ffn(self.norm2(x)))

return x
3 changes: 3 additions & 0 deletions pytorch_forecasting/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from pytorch_forecasting.models.nhits import NHiTS
from pytorch_forecasting.models.nn import GRU, LSTM, MultiEmbedding, get_rnn
from pytorch_forecasting.models.rnn import RecurrentNetwork
from pytorch_forecasting.models.softs import Softs, Softs_pkg_v2
from pytorch_forecasting.models.temporal_fusion_transformer import (
TemporalFusionTransformer,
)
Expand Down Expand Up @@ -42,4 +43,6 @@
"TiDEModel",
"TimeXer",
"xLSTMTime",
"Softs",
"Softs_pkg_v2",
]
8 changes: 8 additions & 0 deletions pytorch_forecasting/models/softs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
SOFTS Model for Multivariate Time Series Forecasting.
"""

from pytorch_forecasting.models.softs._softs_pkg_v2 import Softs_pkg_v2
from pytorch_forecasting.models.softs._softs_v2 import Softs

__all__ = ["Softs", "Softs_pkg_v2"]
87 changes: 87 additions & 0 deletions pytorch_forecasting/models/softs/_softs_pkg_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
Packages container for SOFTS model.
"""

from pytorch_forecasting.base._base_pkg import Base_pkg


class Softs_pkg_v2(Base_pkg):
"""
SOFTS package container.
Reference : https://arxiv.org/abs/2404.14197
"""

_tags = {
"info:name": "SOFTS",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the name should be same as the class here: Softs in place of SOFTS?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be SOFTS all over the place as the actual model name is SOFTS not Softs and also the convention also follows that across other models.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then you would have to update the class name. The tag name should be exactly same as the class name

"info:y_type": ["numeric"],
"info:compute": 2,
"authors": ["Secilia-Cxy", "Muhammad-Rebaal"],
"capability:exogenous": True,
"capability:multivariate": True,
"capability:pred_int": True,
"capability:flexible_history_length": True,
"capability:cold_start": False,
}

@classmethod
def get_cls(cls):
"""Get model class."""
from pytorch_forecasting.models.softs._softs_v2 import Softs

return Softs

@classmethod
def get_datamodule_cls(cls):
"""Get the underlying DataModule class."""
from pytorch_forecasting.data.data_module import TslibDataModule

return TslibDataModule

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using this datamodule? I think this data module is mainly for tslib models, I have no issue with using this, but pls have a look at EncoderDecoderDataModule as well, maybe that would also be helpful.
I have not looked at the architecture, so I have a question: is the model encoder-decoder based model?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using TslibDataModule because SOFTS is a direct-projection MLP model. It only consumes historical input sequences and maps them directly to the forecast window, using EncoderDecoderModule felt unnecessary.

From the perspective of data input we can call it encoder-only model.

Code Ref : Code

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be seen as a encoder-decoder model with decoder being a identity layer?
I think we should use the TslibDataModule only if we are interfacing the model from the tslib package. Otherwise if it fits encoder-decoder model type, we should use EncoderDecoderDataModule

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to update this data module as well then


@classmethod
def get_test_train_params(cls):
"""Return testing parameter settings for the trainer.

Returns
-------
list of dict
Each dict is a valid set of constructor arguments for ``Softs``.
The key ``datamodule_cfg`` is passed to the DataModule, not the model.
"""
from pytorch_forecasting.metrics import SMAPE, QuantileLoss

params = [
{},
dict(hidden_size=64, d_core=64, d_ff=256, n_layers=1),
dict(hidden_size=128, n_layers=1, use_revin=False),
dict(
hidden_size=64,
n_layers=1,
loss=QuantileLoss(quantiles=[0.1, 0.5, 0.9]),
),
dict(
hidden_size=64,
n_layers=1,
use_revin=False,
loss=QuantileLoss(quantiles=[0.1, 0.5, 0.9]),
),
dict(hidden_size=64, dropout=0.0, n_layers=1),
dict(datamodule_cfg=dict(context_length=16, prediction_length=4)),
dict(
optimizer="adamw",
lr_scheduler="cosine_annealing",
lr_scheduler_params={"T_max": 5},
),
dict(
optimizer="adagrad",
optimizer_params={"lr": 1e-3},
),
dict(hidden_size=64, n_layers=1, logging_metrics=[SMAPE()]),
]

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
Loading
Loading