patchfm 2.0.1__tar.gz → 2.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchfm
3
- Version: 2.0.1
3
+ Version: 2.1.1
4
4
  Summary: a Foundation Model for Univariate Time Series Forecasting
5
5
  Author-email: Samy-Melwan Vilhes <samy-melwan.vilhes@insa-rouen.fr>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "patchfm"
3
- version = "2.0.1"
3
+ version = "2.1.1"
4
4
  authors = [
5
5
  { name="Samy-Melwan Vilhes", email="samy-melwan.vilhes@insa-rouen.fr" },
6
6
  ]
@@ -0,0 +1,2 @@
1
+ from patchfm.configs.model_config import PatchFMConfig
2
+ from patchfm.inference.forecaster import Forecaster
@@ -1,11 +1,12 @@
1
- from dataclasses import dataclass, field, asdict
1
+ from dataclasses import asdict, dataclass, field
2
+
2
3
 
3
4
  @dataclass
4
5
  class PatchFMConfig:
5
6
  max_seq_len: int = 1024
6
7
  patch_len: int = 32
7
- d_model: int = 2048
8
- n_heads: int = 64
8
+ d_model: int = 1024
9
+ n_heads: int = 32
9
10
  n_layers_encoder: int = 6
10
11
  quantiles: list[float] = field(default_factory=lambda: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
11
12
 
@@ -1,7 +1,10 @@
1
+ import numpy as np
1
2
  import torch
2
3
  import torch.nn as nn
3
4
  from einops import rearrange
4
- from patchfm.inference.modules import CausalRevIN, ResidualBlock, TransformerEncoder, PatchFM, SeqTypeConverter
5
+
6
+ from patchfm.inference.modules import (CausalRevIN, PatchFM, ResidualBlock,
7
+ SeqTypeConverter, TransformerEncoder)
5
8
 
6
9
 
7
10
  # --- Forecaster Model ---
@@ -20,7 +23,7 @@ class Forecaster(nn.Module):
20
23
  self.max_patches = self.max_seq_len // self.patch_len
21
24
 
22
25
  print("Loading base model from HuggingFace Hub...")
23
- base_model = PatchFM.from_pretrained("vilhess/PatchFM")
26
+ base_model = PatchFM.from_pretrained("vilhess/PatchFM-CausalRevIN-asinh")
24
27
  self._init_from_base(base_model)
25
28
 
26
29
  self.eval()
@@ -59,91 +62,13 @@ class Forecaster(nn.Module):
59
62
  self.proj_output = base_model.proj_output
60
63
 
61
64
  @torch.inference_mode()
62
- def forecast(self, x: torch.Tensor, forecast_horizon: int | None = None, quantiles: list[float] | None = None) -> torch.Tensor:
63
- x = self.converter.convert(x)
64
- assert x.ndim in (1, 2), f"Input dimension must be 1D (time) or 2D (batch, time), got {x.ndim}D."
65
-
66
- batch_dim=True
67
- if x.ndim != 2:
68
- x = x.unsqueeze(0)
69
- batch_dim=False
70
- bs, ws = x.size()
71
-
72
- x = x.to(self.device)
73
-
74
- if ws > self.max_seq_len:
75
- print(f"Warning: Input length {ws} exceeds max_seq_len {self.max_seq_len}. Truncating input.")
76
- x = x[:, -self.max_seq_len:]
77
- ws = self.max_seq_len
65
+ def auto_regressive_quantile_decoding(self, x: torch.Tensor, forecast_horizon: int | None = None, quantiles: list[float] | None = None) -> torch.Tensor:
78
66
 
79
- # Pad so length is divisible by patch_len
80
- pad = (self.patch_len - ws % self.patch_len) % self.patch_len
81
- if pad > 0:
82
- x = torch.cat([x[:, :1].repeat(1, pad), x], dim=1)
67
+ q = torch.tensor(self.quantiles, device=self.device)
83
68
 
84
69
  # Default horizon = patch_len
85
70
  forecast_horizon = forecast_horizon or self.patch_len
86
71
 
87
- # Reshape into patches
88
- x = rearrange(x, "b (pn pl) -> b pn pl", pl=self.patch_len)
89
-
90
- rollouts = -(-forecast_horizon // self.patch_len) # ceil division
91
- predictions = []
92
-
93
- for _ in range(rollouts):
94
-
95
- if x.size(1) > self.max_patches:
96
- x = x[:, -self.max_patches:, :]
97
-
98
- # Forward pass
99
- x = self.revin(x, mode="norm")
100
- x = self.proj_embedding(x)
101
- x = self.transformer_encoder(x)
102
- x = x[:, -1:, :] # Keep only the last patch for autoregressive forecasting
103
- forecasting = self.proj_output(x)
104
- forecasting = self.revin(forecasting, mode="denorm")
105
-
106
- # Reshape to (bs, patch_num, patch_len, n_quantiles)
107
- forecasting = rearrange(
108
- forecasting, "b 1 (pl q) -> b 1 pl q",
109
- pl=self.patch_len, q=self.n_quantiles
110
- )
111
-
112
- # Take median quantile (index 4)
113
- patch_median = forecasting[:, -1:, :, 4].detach()
114
- predictions.append(forecasting[:, -1, :, :])
115
-
116
- # Append median patch for next rollout
117
- x = patch_median.clone()
118
-
119
- pred_quantiles = torch.cat(predictions, dim=1)
120
- pred_quantiles = pred_quantiles[:, :forecast_horizon, :]
121
- pred_median = pred_quantiles[:, :, 4]
122
-
123
- pred_quantiles = pred_quantiles[..., [self.quantiles.index(q) for q in quantiles]] if quantiles is not None else pred_quantiles
124
-
125
- self.clear_cache()
126
-
127
- if torch.any(torch.isnan(pred_median)) or torch.any(torch.isinf(pred_median)):
128
- print("Warning: NaN or Inf values detected in predictions. Returning zeros.")
129
- pred_median = torch.zeros_like(pred_median)
130
- pred_quantiles = torch.zeros_like(pred_quantiles)
131
-
132
- if not batch_dim:
133
- pred_median = pred_median.squeeze(0)
134
- pred_quantiles = pred_quantiles.squeeze(0)
135
-
136
- pred_median, pred_quantiles = self.converter.deconvert(pred_median, pred_quantiles)
137
- return pred_median, pred_quantiles
138
-
139
- @torch.inference_mode()
140
- def auto_regressive_quantile_decoding(self, x: torch.Tensor, forecast_horizon: int | None = None, quantiles: list[float] | None = None) -> torch.Tensor:
141
-
142
- if forecast_horizon<=self.patch_len:
143
- print("Warning: forecast_horizon is less than or equal to patch_len. Using standard forecast method instead.")
144
- return self.forecast(x, forecast_horizon, quantiles)
145
-
146
- print("Using Auto-Regressive Quantile Decoding... This may be slower because no KV-Caching.")
147
72
  x = self.converter.convert(x)
148
73
  assert x.ndim in (1, 2), f"Input dimension must be 1D (time) or 2D (batch, time), got {x.ndim}D."
149
74
 
@@ -165,12 +90,8 @@ class Forecaster(nn.Module):
165
90
  if pad > 0:
166
91
  x = torch.cat([x[:, :1].repeat(1, pad), x], dim=1)
167
92
 
168
- # Default horizon = patch_len
169
- forecast_horizon = forecast_horizon or self.patch_len
170
-
171
93
  # Reshape into patches
172
- x = rearrange(x, "b (pn pl) -> b pn pl", pl=self.patch_len)
173
- context = x.clone()
94
+ x = rearrange(x, "b (pn pl) -> b pn pl", pl=self.patch_len)
174
95
 
175
96
  rollouts = -(-forecast_horizon // self.patch_len) # ceil division
176
97
  predictions = []
@@ -188,18 +109,10 @@ class Forecaster(nn.Module):
188
109
  forecasting = rearrange(
189
110
  forecasting, "b 1 (pl q) -> b 1 pl q",
190
111
  pl=self.patch_len, q=self.n_quantiles
191
- )
112
+ )
113
+ x = forecasting.permute(0, 3, 1, 2).reshape(forecasting.size(0)*self.n_quantiles, 1, self.patch_len)
192
114
 
193
- context_expanded = torch.repeat_interleave(context.unsqueeze(-1), repeats=self.n_quantiles, dim=-1) # batch x patch_num x patch_len x n_quantiles
194
- base_context_expanded = torch.cat((context_expanded, forecasting), dim=1)
195
- context_expanded = base_context_expanded.permute(0, 3, 1, 2).reshape(bs*self.n_quantiles, base_context_expanded.size(1), self.patch_len)
196
-
197
- x = context_expanded
198
- q = torch.tensor(self.quantiles, device=self.device)
199
-
200
- predictions.append(forecasting[:, -1, :, :])
201
-
202
- self.clear_cache()
115
+ predictions.append(forecasting[:, -1, :, :].detach())
203
116
 
204
117
  for _ in range(rollouts-1):
205
118
 
@@ -223,15 +136,11 @@ class Forecaster(nn.Module):
223
136
  )
224
137
  forecasting = forecasting.permute(0, 2, 3, 1, 4).flatten(start_dim=-2) # batch x 1 x patch_len x n_quantiles**2
225
138
  forecasting = torch.quantile(forecasting, q, dim=-1) # n_quantiles x batch x 1 x patch_len
226
- forecasting = forecasting.permute(1, 2, 3, 0) # batch x 1 x patch_len x n_quantiles
227
-
228
- base_context_expanded = torch.cat((base_context_expanded, forecasting), dim=1)
229
- context_expanded = base_context_expanded.permute(0, 3, 1, 2).reshape(bs*self.n_quantiles, base_context_expanded.size(1), self.patch_len)
230
-
231
- x = context_expanded
232
- predictions.append(forecasting[:, -1, :, :])
139
+
140
+ x = forecasting.permute(1, 0, 2, 3).reshape(-1, 1, self.patch_len)
141
+ predictions.append(forecasting.permute(1, 2, 3, 0)[:, 0].detach())
233
142
 
234
- self.clear_cache()
143
+ self.clear_cache()
235
144
 
236
145
  pred_quantiles = torch.cat(predictions, dim=1)
237
146
  pred_quantiles = pred_quantiles[:, :forecast_horizon, :]
@@ -251,13 +160,30 @@ class Forecaster(nn.Module):
251
160
  pred_median, pred_quantiles = self.converter.deconvert(pred_median, pred_quantiles)
252
161
  return pred_median, pred_quantiles
253
162
 
254
- def __call__(self, context: torch.Tensor, forecast_horizon: int | None = None, quantiles: list[float] | None = None, quantile_decoding: bool = False) -> torch.Tensor:
255
- if quantile_decoding:
256
- return self.auto_regressive_quantile_decoding(context, forecast_horizon, quantiles)
163
+ def __call__(self, context: torch.Tensor, forecast_horizon: int | None = None, quantiles: list[float] | None = None, flip_equivariance: bool = False) -> torch.Tensor:
164
+ if flip_equivariance:
165
+ print("Flip equivariance enabled: forecast = (f(x) - f(-x)) / 2. This requires multiplying by 2 the batch size (Reverso: Efficient Time Series Foundation Models for Zero-shot Forecasting).")
166
+ bs = context.size(0)
167
+ context_flipped = -context
168
+ concat_context = torch.cat([context, context_flipped], dim=0)
169
+ pred_median_full, pred_quantiles_full = self.auto_regressive_quantile_decoding(concat_context, forecast_horizon, quantiles)
170
+ pred_median, pred_quantiles = pred_median_full[:bs], pred_quantiles_full[:bs]
171
+ pred_median2, pred_quantiles2 = pred_median_full[bs:], pred_quantiles_full[bs:]
172
+ pred_median = (pred_median - pred_median2) / 2
173
+ pred_quantiles = (pred_quantiles - flip_last_dim(pred_quantiles2)) / 2
257
174
  else:
258
- return self.forecast(context, forecast_horizon, quantiles)
175
+ pred_median, pred_quantiles = self.auto_regressive_quantile_decoding(context, forecast_horizon, quantiles)
176
+ return pred_median, pred_quantiles
259
177
 
260
178
  def clear_cache(self):
261
179
  self.revin.clear_cache()
262
180
  for layer in self.transformer_encoder.layers:
263
- layer.attn.clear_cache()
181
+ layer.attn.clear_cache()
182
+
183
+ def flip_last_dim(x):
184
+ if isinstance(x, torch.Tensor):
185
+ return torch.flip(x, dims=[-1])
186
+ elif isinstance(x, np.ndarray):
187
+ return np.flip(x, axis=-1)
188
+ else:
189
+ raise TypeError(f"Unsupported type: {type(x)}")
@@ -1,11 +1,12 @@
1
1
  # Modules efficient for inference with caching
2
2
 
3
+ import numpy as np
3
4
  import torch
4
5
  import torch.nn as nn
5
6
  from einops import rearrange
6
- from rotary_embedding_torch import RotaryEmbedding
7
7
  from huggingface_hub import PyTorchModelHubMixin
8
- import numpy as np
8
+ from rotary_embedding_torch import RotaryEmbedding
9
+
9
10
 
10
11
  class SeqTypeConverter:
11
12
  def __init__(self):
@@ -136,11 +137,14 @@ class CausalRevIN(nn.Module):
136
137
  counts = counts - nan_counts
137
138
 
138
139
  if self.cached_counts is not None:
140
+ factor = B//self.cached_counts.size(0)
141
+ self.cached_counts = self.cached_counts.repeat_interleave(factor, dim=0)
139
142
  counts += self.cached_counts
140
143
  self.cached_counts = counts[:, -1:, :]
141
144
 
142
145
  cumsum_x = torch.cumsum(x.nansum(dim=-1, keepdim=True), dim=1)
143
146
  if self.cached_cumsum_x is not None:
147
+ self.cached_cumsum_x = self.cached_cumsum_x.repeat_interleave(factor, dim=0)
144
148
  cumsum_x += self.cached_cumsum_x
145
149
  self.cached_cumsum_x = cumsum_x[:, -1:, :]
146
150
 
@@ -149,6 +153,7 @@ class CausalRevIN(nn.Module):
149
153
 
150
154
  cumsum_x2 = torch.cumsum((x**2).nansum(dim=-1, keepdim=True), dim=1)
151
155
  if self.cached_cumsum_x2 is not None:
156
+ self.cached_cumsum_x2 = self.cached_cumsum_x2.repeat_interleave(factor, dim=0)
152
157
  cumsum_x2 += self.cached_cumsum_x2
153
158
  self.cached_cumsum_x2 = cumsum_x2[:, -1:, :]
154
159
 
@@ -161,6 +166,8 @@ class CausalRevIN(nn.Module):
161
166
  self.cached_cumsum_x = None
162
167
  self.cached_cumsum_x2 = None
163
168
  self.cached_counts = None
169
+ self.cached_mean = None
170
+ self.cached_std = None
164
171
 
165
172
  class ResidualBlock(nn.Module):
166
173
  def __init__(self, in_dim, hid_dim, out_dim):
@@ -218,6 +225,9 @@ class MultiHeadAttention(nn.Module):
218
225
  if self.k_cache is not None and self.v_cache is not None:
219
226
  offset += self.k_cache.size(2)
220
227
  is_causal = False
228
+ factor = q.size(0) // self.k_cache.size(0)
229
+ self.k_cache = self.k_cache.repeat_interleave(factor, dim=0)
230
+ self.v_cache = self.v_cache.repeat_interleave(factor, dim=0)
221
231
  k = torch.cat([self.k_cache, k], dim=2)
222
232
  v = torch.cat([self.v_cache, v], dim=2)
223
233
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchfm
3
- Version: 2.0.1
3
+ Version: 2.1.1
4
4
  Summary: a Foundation Model for Univariate Time Series Forecasting
5
5
  Author-email: Samy-Melwan Vilhes <samy-melwan.vilhes@insa-rouen.fr>
6
6
  License: MIT License
@@ -1,2 +0,0 @@
1
- from patchfm.inference.forecaster import Forecaster
2
- from patchfm.configs.model_config import PatchFMConfig
File without changes
File without changes
File without changes