Skip to content
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
930f855
[ENH] Units_v2 Model added
Muhammad-Rebaal Mar 9, 2026
0f4faf4
Merge branch 'main' into units_v2
Muhammad-Rebaal Mar 10, 2026
43dd89d
Merge branch 'main' into units_v2
Muhammad-Rebaal Mar 16, 2026
2d46f32
Add UniTS model, its package container, and integrate it into the mod…
Muhammad-Rebaal Mar 16, 2026
e9cd22f
[ENH] Added a default fixture
Muhammad-Rebaal Mar 17, 2026
c8c81a9
feat: Add `UniTS_pkg_v2` for UniTS model definition, metadata, and te…
Muhammad-Rebaal Mar 17, 2026
61e4d29
[BUG] Fix default fixture context_length and shared dict
Muhammad-Rebaal Mar 17, 2026
ee65d40
Merge branch 'main' into units_v2
Muhammad-Rebaal Mar 21, 2026
7e13f3b
Merge branch 'main' into units_v2
Muhammad-Rebaal Mar 23, 2026
8900902
fix: Code Refactored
Muhammad-Rebaal Mar 23, 2026
5701122
Merge branch 'main' into units_v2
Muhammad-Rebaal Mar 25, 2026
4152064
Merge branch 'main' into units_v2
Muhammad-Rebaal Apr 8, 2026
9efb9df
Merge branch 'main' into units_v2
Muhammad-Rebaal May 11, 2026
e17fb97
Merge branch 'main' into units_v2
Muhammad-Rebaal Jun 8, 2026
78b7bd2
Merge branch 'main' into units_v2
Muhammad-Rebaal Jun 15, 2026
a7c0172
feat: add UniTS_pkg_v2 remaining tags and update the import
Muhammad-Rebaal Jun 15, 2026
08f46b0
fix : units_v2 test error
Muhammad-Rebaal Jun 16, 2026
49c27f8
fix: pytest error
Muhammad-Rebaal Jun 16, 2026
349c726
fix: pytest error
Muhammad-Rebaal Jun 16, 2026
9f192ed
Merge branch 'main' into units_v2
Muhammad-Rebaal Jun 19, 2026
24f57c9
Merge branch 'main' into units_v2
Muhammad-Rebaal Jun 21, 2026
858a146
fix:pytest hallucination
Muhammad-Rebaal Jun 22, 2026
bc89293
Merge branch 'main' into units_v2
Muhammad-Rebaal Jun 25, 2026
18afcbb
Merge branch 'units_v2' of https://github.com/Muhammad-Rebaal/pytorch…
Muhammad-Rebaal Jun 27, 2026
4327f59
Merge branch 'main' into units_v2
Muhammad-Rebaal Jun 29, 2026
1fa26b3
fix: removed the v1 tags
Muhammad-Rebaal Jun 29, 2026
8e7919f
fix: Performed De-duplication
Muhammad-Rebaal Jul 3, 2026
7eb7fce
Added API reference for the Units model
Muhammad-Rebaal Jul 3, 2026
0dc7a8d
feat: Converted the model on the BaseClass instead of the TslibModel …
Muhammad-Rebaal Jul 6, 2026
26e8290
Merge branch 'main' into units_v2
Muhammad-Rebaal Jul 14, 2026
013bd15
Merge branch 'main' into units_v2
Muhammad-Rebaal Jul 26, 2026
96c229e
remove the non-important tag, losses suport added, and tests added
Muhammad-Rebaal Jul 27, 2026
36f2e6a
fix pytest error
Muhammad-Rebaal Jul 27, 2026
dccfe8c
fix code quality
Muhammad-Rebaal Jul 27, 2026
3c3b662
fix: tag removed and renamed param name
Muhammad-Rebaal Jul 30, 2026
e069a29
remove dist loss as there isn't any implementation in base_model_v2
Muhammad-Rebaal Jul 31, 2026
874e477
Merge branch 'main' of https://github.com/Muhammad-Rebaal/pytorch-for…
Muhammad-Rebaal Aug 3, 2026
0de97fc
fix : Code Quality
Muhammad-Rebaal Aug 3, 2026
038d8c4
fix Code Quality
Muhammad-Rebaal Aug 3, 2026
30b0aab
fix : Remove dead import
Muhammad-Rebaal Aug 8, 2026
aca98b1
Merge branch 'main' into units_v2
Muhammad-Rebaal Aug 8, 2026
81fc45a
Merge branch 'main' into units_v2
Muhammad-Rebaal Aug 9, 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
1 change: 1 addition & 0 deletions docs/source/m_layer_v2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ 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.units._units_v2.UniTS
1 change: 1 addition & 0 deletions docs/source/pkg_v2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,4 @@ 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.units._units_pkg_v2.UniTS_pkg_v2
7 changes: 6 additions & 1 deletion pytorch_forecasting/layers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
FullAttention,
TriangularCausalMask,
)
from pytorch_forecasting.layers._blocks import ResidualBlock
from pytorch_forecasting.layers._blocks import ResidualBlock, _TransformerBlock
from pytorch_forecasting.layers._decomposition import SeriesDecomposition
from pytorch_forecasting.layers._embeddings import (
DataEmbedding_inverted,
EnEmbedding,
PositionalEmbedding,
_PatchEmbedding,
_PositionalEmbedding,
embedding_cat_variables,
)
from pytorch_forecasting.layers._encoders import (
Expand Down Expand Up @@ -41,6 +43,9 @@
"DataEmbedding_inverted",
"EnEmbedding",
"PositionalEmbedding",
"_PatchEmbedding",
"_PositionalEmbedding",
"_TransformerBlock",
"Encoder",
"EncoderLayer",
"FlattenHead",
Expand Down
3 changes: 2 additions & 1 deletion pytorch_forecasting/layers/_blocks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pytorch_forecasting.layers._blocks._residual_block_dsipts import ResidualBlock
from pytorch_forecasting.layers._blocks._transformer_block import _TransformerBlock

__all__ = ["ResidualBlock"]
__all__ = ["ResidualBlock", "_TransformerBlock"]
45 changes: 45 additions & 0 deletions pytorch_forecasting/layers/_blocks/_transformer_block.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
Pre-norm Transformer Encoder Block for PTF.
"""

import torch
import torch.nn as nn


class _TransformerBlock(nn.Module):
"""
Pre-norm transformer encoder block (MHSA + FFN).

Parameters
----------
d_model : int
Model dimension.
n_heads : int
Number of attention heads.
d_ff : int
Feed-forward hidden dimension.
dropout : float
Dropout probability.
"""

def __init__(self, d_model: int, n_heads: int, d_ff: int, dropout: float = 0.1):
super().__init__()
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.attn = nn.MultiheadAttention(
embed_dim=d_model, num_heads=n_heads, dropout=dropout, batch_first=True
)
self.ff = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(d_ff, d_model),
nn.Dropout(dropout),
)

def forward(self, x: torch.Tensor) -> torch.Tensor:
normed = self.norm1(x)
attn_out, _ = self.attn(normed, normed, normed)
x = x + attn_out
x = x + self.ff(self.norm2(x))
return x
4 changes: 4 additions & 0 deletions pytorch_forecasting/layers/_embeddings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
DataEmbedding_inverted,
)
from pytorch_forecasting.layers._embeddings._en_embedding import EnEmbedding
from pytorch_forecasting.layers._embeddings._patch_embedding import _PatchEmbedding
from pytorch_forecasting.layers._embeddings._positional_embedding import (
PositionalEmbedding,
_PositionalEmbedding,
)
from pytorch_forecasting.layers._embeddings._sub_nn import embedding_cat_variables

__all__ = [
"PositionalEmbedding",
"_PositionalEmbedding",
"_PatchEmbedding",
"DataEmbedding_inverted",
"EnEmbedding",
"embedding_cat_variables",
Expand Down
55 changes: 55 additions & 0 deletions pytorch_forecasting/layers/_embeddings/_patch_embedding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
Patch Embedding Layer for PTF.
"""

import torch
import torch.nn as nn


class _PatchEmbedding(nn.Module):
"""
Project strided patches of a multivariate time series into d_model space.

Uses channel-independent patching: each channel's patches are projected
separately with a shared Linear(patch_len, d_model), then averaged across
channels to match the UniTS paper's channel-independent approach.

Parameters
----------
patch_len : int
Length of each patch window in time steps.
stride : int
Stride between consecutive patches.
d_model : int
Output embedding dimension.
dropout : float
Dropout probability.
"""

def __init__(self, patch_len: int, stride: int, d_model: int, dropout: float = 0.1):
super().__init__()
self.patch_len = patch_len
self.stride = stride
self.projection = nn.Linear(patch_len, d_model)
self.drop = nn.Dropout(dropout)

def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Parameters
----------
x : torch.Tensor
Shape (batch, seq_len, n_channels).

Returns
-------
torch.Tensor
Shape (batch, num_patches, d_model).
"""
patches = x.unfold(dimension=1, size=self.patch_len, step=self.stride)
B, num_patches, C, P = patches.shape
patches = patches.permute(0, 2, 1, 3).contiguous().view(B * C, num_patches, P)
emb = self.drop(self.projection(patches))
emb = emb.view(B, C, num_patches, self.projection.out_features)

# Channel independence: average across channels as per UniTS logic
return emb.mean(dim=1)
26 changes: 26 additions & 0 deletions pytorch_forecasting/layers/_embeddings/_positional_embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,29 @@ def __init__(self, d_model, max_len=5000):

def forward(self, x):
return self.pe[:, : x.size(1)]


class _PositionalEmbedding(PositionalEmbedding):
"""
Sinusoidal positional embedding with additive application and dropout.

Inherits the sinusoidal buffer from ``PositionalEmbedding`` and adds:
- Additive application (x + pe) in ``forward``
- Dropout after addition

Parameters
----------
d_model : int
Embedding dimension.
max_len : int
Maximum sequence length.
dropout : float
Dropout probability.
"""

def __init__(self, d_model: int, max_len: int = 512, dropout: float = 0.1):
super().__init__(d_model, max_len)
self.drop = nn.Dropout(dropout)

def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.drop(x + self.pe[:, : x.size(1), :])
2 changes: 2 additions & 0 deletions pytorch_forecasting/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
)
from pytorch_forecasting.models.tide import TiDEModel
from pytorch_forecasting.models.timexer import TimeXer
from pytorch_forecasting.models.units import UniTS
from pytorch_forecasting.models.xlstm import xLSTMTime

__all__ = [
Expand All @@ -41,5 +42,6 @@
"DecoderMLP",
"TiDEModel",
"TimeXer",
"UniTS",
"xLSTMTime",
]
8 changes: 8 additions & 0 deletions pytorch_forecasting/models/units/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
UniTS: Unified Time Series Model for time series forecasting.
"""

from pytorch_forecasting.models.units._units_pkg_v2 import UniTS_pkg_v2
from pytorch_forecasting.models.units._units_v2 import UniTS

__all__ = ["UniTS", "UniTS_pkg_v2"]
96 changes: 96 additions & 0 deletions pytorch_forecasting/models/units/_units_pkg_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""
Packages container for UniTS model.
"""

from pytorch_forecasting.base._base_pkg import Base_pkg


class UniTS_pkg_v2(Base_pkg):
"""
UniTS: Unified Time Series Model.
Reference: https://arxiv.org/abs/2403.00131
Github: https://github.com/mims-harvard/UniTS
"""

_tags = {
"info:name": "UniTS",
"info:compute": 4,
"authors": ["Muhammad-Rebaal", "gasvn", "sohamukute"],
"capability:exogenous": True,
"capability:multivariate": True,
"info:pred_type": ["point", "quantile", "distribution"],
"info:y_type": ["numeric"],
"capability:pred_int": True,
"capability:flexible_history_length": True,
"capability:cold_start": False,
}

@classmethod
def get_cls(cls):
from pytorch_forecasting.models.units._units_v2 import UniTS

return UniTS

@classmethod
def get_datamodule_cls(cls):
from pytorch_forecasting.data.data_module import (
EncoderDecoderTimeSeriesDataModule,
)

return EncoderDecoderTimeSeriesDataModule

@classmethod
def get_base_test_params(cls):

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 method instead of get_test_train_params?
I feel like an AI hallucination? It was correct earlier

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.

As I mentioned earlier, get_base_test_params is not going to work for now for v2
use get_test_train_params instead.
for that you need to remove pred_type, y_type tags

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.

looping over all the losses is not a good idea rn for v2

@Muhammad-Rebaal Muhammad-Rebaal Jul 28, 2026

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.

Aren't these tags pred_type, y_type be removed from here : https://github.com/sktime/pytorch-forecasting/blob/main/extension_templates/v2/model_simple/model_pkg.py if these are part of get_base_test_params ?

looping over all the losses is not a good idea rn for v2

Can you suggest me the preferred ones ? so I'd add those

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.

Aren't these tags pred_type, y_type be removed from here : https://github.com/sktime/pytorch-forecasting/blob/main/extension_templates/v2/model_simple/model_pkg.py if these are part of get_base_test_params ?

I have reservations about this :)
Yes, we can remove this, but we will have to add it again, once the work on updating the test framework is done - which is the next work item on my list. As, we dont have much contributors around at this time, we can manage to ignore this. wdyt?

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.

Hmm, right

"""Return testing parameter settings for the trainer.

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``.
"""
from pytorch_forecasting.metrics import NormalDistributionLoss, QuantileLoss

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 am not sure if DistributionLoss works well with v2 rn, does it?

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.

No I didn't find any implementation as well that's why I asked you above. That's why in my last commit I remove the code implementation as well but forget to remove this dead import sorry for confusion

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.

Is there any open issue regarding the support of Distribution loss in v2 models ?


params = [
{},
{
Comment thread
phoeenniixx marked this conversation as resolved.
"patch_len": 8,
"stride": 4,

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.

please add some loss functions as well here - Is this model only compatible with point prediction losses, or can it also handle quantile and distribution losses?

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.

In the paper the model it is mentioned only about point prediction losses but we can extend it so I extend it to both quantile and distribution losses.

},
{
"d_model": 32,
"n_heads": 4,
"patch_len": 8,
"stride": 4,
},
{
"patch_len": 8,
"stride": 4,
"datamodule_cfg": {
"max_encoder_length": 16,
"max_prediction_length": 4,
},
},
{
"patch_len": 8,
"stride": 4,
"loss": QuantileLoss(quantiles=[0.1, 0.5, 0.9]),
},
{
"patch_len": 8,
"stride": 4,
"loss": NormalDistributionLoss(),
},
]

base_dm_cfg = {"max_encoder_length": 16, "max_prediction_length": 4}

for param in params:
merged = base_dm_cfg.copy()
merged.update(param.get("datamodule_cfg", {}))
param["datamodule_cfg"] = merged

return params
Loading
Loading