Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
11 changes: 11 additions & 0 deletions pytorch_forecasting/layers/_units/__init__.py

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.

what is the use of this folder?

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.

The model file already imports directly from the canonical locations. I'll remove the _units layer folder entirely.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
UniTS layer abstractions.
"""

from pytorch_forecasting.layers._units._units import (
_PatchEmbedding,
_PositionalEncoding,
_TransformerBlock,
)

__all__ = ["_PatchEmbedding", "_PositionalEncoding", "_TransformerBlock"]
127 changes: 127 additions & 0 deletions pytorch_forecasting/layers/_units/_units.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""
Core Neural Network Layers for the UniTS architecture.
"""

import math

import torch
import torch.nn as nn


class _PatchEmbedding(nn.Module):

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.

would it make sense to add it to _embeddings?

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.

Yes I think it would make complete sense if we'd place it there in a file called _patch_embedding.py, as the _embeddings directory is already the designated home for embedding abstractions (like _data_embedding.py and _en_embedding.py).

"""
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)


class _PositionalEncoding(nn.Module):

@phoeenniixx phoeenniixx Mar 22, 2026

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.

it could go to layers/_encoders?

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.

Actually its just a misinterpretation, it is an embedding not an encoder. We already have a PositionalEmbedding class in _embeddings/_positional_embedding.py doing the exact same math. Instead of duplicating that logic in an _encoder.py file, I created a _PositionalEmbedding child class inside the existing _positional_embedding.py file. It inherits the fixed sinusoidal buffer from the parent and adds the specific dropout and additive forward logic required for UniTS.

"""
Sinusoidal positional encoding.

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__()
self.drop = nn.Dropout(dropout)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
half = d_model // 2
div_term = torch.exp(
torch.arange(0, half, dtype=torch.float) * (-math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term[: pe[:, 0::2].size(1)])
pe[:, 1::2] = torch.cos(position * div_term[: pe[:, 1::2].size(1)])
self.register_buffer("pe", pe.unsqueeze(0))

def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.drop(x + self.pe[:, : x.size(1), :])


class _TransformerBlock(nn.Module):

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.

should we add it to layer/_transforms or something?

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 creating a new _transforms folder wouldn't be necessary here because it would be confusing for new contributors as transform and transformers are 2 different terminologies. Furthermore, we already have an established layers/_blocks/ directory. A transformer block is standard neural network block logic, so keeping it grouped with other blocks in layers/_blocks/_transformer_block.py works perfectly entirely to our existing layout.

"""
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
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"]
74 changes: 74 additions & 0 deletions pytorch_forecasting/models/units/_units_pkg_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""
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
"""

_tags = {
"info:name": "UniTS",
"authors": ["Muhammad-Rebaal", "sohamukute"],
"capability:exogenous": True,
"capability:multivariate": True,
"capability:pred_int": False,
"capability:flexible_history_length": False,

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 some tags are missing here. PLease look at the extension-templates

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.

Updated

}

@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._tslib_data_module import 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.

the path for the data modules have changed. Please use pytorch_forecasting.data.data_module instead


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.

Is this a tslib model? Why not use EncoderDecoderDataModule?

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.

Yes, this is a tslib model. However, we are not using the EncoderDecoderDataModule because UniTS is an encoder-only architecture. Since it doesn't use a decoder, wrapping it in that specific module is unnecessary.


@classmethod
def get_test_train_params(cls):
"""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``.
"""
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": {"context_length": 16, "prediction_length": 4},
},
]

base_dm_cfg = {"context_length": 16, "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