Skip to content

Commit 28f2dfa

Browse files
committed
refactor(timesfm3): simplify detrending column handling
1 parent fdbba09 commit 28f2dfa

4 files changed

Lines changed: 34 additions & 42 deletions

File tree

‎sdm/models/timesfm3/model.py‎

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from sdm.models.timesfm3.block import ResidualBlock
4343
from sdm.models.timesfm3.ckpt import remap_ckpt
4444
from sdm.models.timesfm3.icl import ICLBlock
45-
from sdm.models.timesfm3.recipe import TIME_COLUMN, default_recipe
45+
from sdm.models.timesfm3.recipe import default_recipe
4646
from sdm.models.timesfm3.util import (
4747
gather_future_patches,
4848
get_running_stats,
@@ -52,7 +52,7 @@
5252
from sdm.tensor.table import TableSchema
5353

5454

55-
def _with_time_column(table: TableTensor, start: int) -> TableTensor:
55+
def _with_id_column(table: TableTensor, start: int) -> TableTensor:
5656
time_steps = torch.arange(
5757
start, start + table.size(-2), device=table.device
5858
).expand(*table.size()[:-2], -1)
@@ -62,7 +62,7 @@ def _with_time_column(table: TableTensor, start: int) -> TableTensor:
6262
(
6363
table,
6464
TableTensor(
65-
columns={Stype.id: [TIME_COLUMN]},
65+
columns={Stype.id: ["__timesfm3_id__"]},
6666
id=ColumnarTensor((time_steps,)),
6767
),
6868
),
@@ -71,9 +71,9 @@ def _with_time_column(table: TableTensor, start: int) -> TableTensor:
7171
)
7272

7373

74-
def _drop_time_column(table: TableTensor) -> TableTensor:
75-
if TIME_COLUMN in table.columns[Stype.id]:
76-
return table.drop_columns(TIME_COLUMN)
74+
def _drop_id_column(table: TableTensor) -> TableTensor:
75+
if "__timesfm3_id__" in table.columns[Stype.id]:
76+
return table.drop_columns("__timesfm3_id__")
7777
return table
7878

7979

@@ -204,13 +204,13 @@ def forward(
204204
x_query = expand_query(x_context.schema, x_query)
205205

206206
out = super().forward(
207-
_with_time_column(x_context, 0),
208-
_with_time_column(y_context, 0),
209-
_with_time_column(x_query, context_length),
207+
_with_id_column(x_context, 0),
208+
_with_id_column(y_context, 0),
209+
_with_id_column(x_query, context_length),
210210
*args,
211211
**kwargs,
212212
)
213-
return _drop_time_column(out)
213+
return _drop_id_column(out)
214214

215215
def fit(
216216
self,
@@ -228,7 +228,7 @@ def fit(
228228
kwargs["_x_context_schema"] = x.schema
229229
kwargs["_context_length"] = x.size(-2)
230230
super().fit(
231-
_with_time_column(x, 0), _with_time_column(y, 0), *args, **kwargs
231+
_with_id_column(x, 0), _with_id_column(y, 0), *args, **kwargs
232232
)
233233

234234
def predict(
@@ -249,18 +249,18 @@ def predict(
249249
cached_kwargs = cast(dict[str, Any], self._cache["kwargs"])
250250
x = expand_query(cached_kwargs["_x_context_schema"], x)
251251
out = super().predict(
252-
_with_time_column(x, cached_kwargs["_context_length"]),
252+
_with_id_column(x, cached_kwargs["_context_length"]),
253253
*args,
254254
**kwargs,
255255
)
256-
return _drop_time_column(out)
256+
return _drop_id_column(out)
257257

258258
def _prepare_context(
259259
self,
260260
context: MemberContext,
261261
callbacks: Sequence[Callback],
262262
) -> MemberContext:
263-
context = context._replace(y=_drop_time_column(context.y))
263+
context = context._replace(y=_drop_id_column(context.y))
264264
return super()._prepare_context(context, callbacks)
265265

266266
def _forward(
@@ -307,7 +307,7 @@ def _forward(
307307
if x_context is not None
308308
else kwargs["_context_length"]
309309
)
310-
return _with_time_column(out, context_length)
310+
return _with_id_column(out, context_length)
311311

312312

313313
class _TimesFM3(torch.nn.Module):

‎sdm/models/timesfm3/recipe.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33

44
import sdm.processing as sp
55

6-
TIME_COLUMN = "__timesfm3_time__"
7-
86

97
def default_recipe() -> sp.Recipe: # noqa: D103
108
return sp.Recipe(
11-
features=sp.LinearDetrend(TIME_COLUMN, threshold=0.5),
12-
target=sp.LinearDetrend(TIME_COLUMN, threshold=0.5),
9+
features=sp.LinearDetrend("__timesfm3_id__", threshold=0.5),
10+
target=sp.LinearDetrend("__timesfm3_id__", threshold=0.5),
1311
output=sp.AverageEstimators(),
1412
)

‎sdm/processing/numerical/linear_detrend.py‎

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# SPDX-License-Identifier: Apache-2.0
33

44
import torch
5-
from torch import Tensor
65

76
from sdm import Stype, TableTensor
87
from sdm.processing import InvertibleMixin, Processor
@@ -11,13 +10,13 @@
1110
class LinearDetrend(Processor, InvertibleMixin):
1211
"""Remove a linear trend fitted to observed numerical values.
1312
14-
The ``time_column`` contains numerical step coordinates stored as an ID
13+
The ``id_column`` contains numerical step coordinates stored as an ID
1514
column. Non-finite values are ignored when fitting and preserved during
1615
transformation. For predictions with multiple outputs per fitted column,
1716
the outputs for each column must be adjacent.
1817
1918
Args:
20-
time_column: Column containing the step coordinate for each row.
19+
id_column: Column containing the step coordinate for each row.
2120
threshold: Detrend only when the residual standard deviation is less
2221
than this fraction of the original standard deviation. ``None``
2322
always applies the fitted trend.
@@ -28,20 +27,17 @@ class LinearDetrend(Processor, InvertibleMixin):
2827

2928
def __init__(
3029
self,
31-
time_column: str,
30+
id_column: str,
3231
*,
3332
threshold: float | None = None,
3433
) -> None:
3534
super().__init__()
36-
self.time_column = time_column
35+
self.id_column = id_column
3736
self.threshold = threshold
3837
self.register_buffer("origin", torch.empty(0, dtype=torch.long))
3938
self.register_buffer("slope", torch.empty(0))
4039
self.register_buffer("intercept", torch.empty(0))
4140

42-
def _time(self, table: TableTensor) -> Tensor:
43-
return table[self.time_column].id[..., 0]
44-
4541
def _fit(
4642
self,
4743
table: TableTensor,
@@ -51,7 +47,7 @@ def _fit(
5147
values = table.numerical.to(
5248
dtype=torch.promote_types(table.numerical.dtype, torch.float32)
5349
)
54-
time = self._time(table)
50+
time = table[self.id_column].id[..., 0]
5551
self.origin = time[..., -1:].clone()
5652
time = (time - self.origin).to(dtype=values.dtype).unsqueeze(-1)
5753
valid = values.isfinite()
@@ -80,24 +76,23 @@ def _fit(
8076
self.slope.masked_fill_(~apply, 0.0)
8177
self.intercept.masked_fill_(~apply, 0.0)
8278

83-
def _trend(self, table: TableTensor) -> Tensor:
84-
time = (
85-
(self._time(table) - self.origin)
86-
.to(dtype=self.slope.dtype)
87-
.unsqueeze(-1)
88-
)
89-
return self.slope * time + self.intercept
90-
9179
def _transform(self, table: TableTensor) -> TableTensor:
92-
time = (self._time(table) - self.origin).to(dtype=self.slope.dtype)
80+
time = (table[self.id_column].id[..., 0] - self.origin).to(
81+
dtype=self.slope.dtype
82+
)
9383
numerical = table.numerical - self.intercept
9484
numerical.addcmul_(self.slope, time.unsqueeze(-1), value=-1)
9585
return table.replace_blocks(
9686
numerical=numerical.to(table.numerical.dtype)
9787
)
9888

9989
def _inverse_transform(self, table: TableTensor) -> TableTensor:
100-
trend = self._trend(table) # [..., F, T]
90+
time = (
91+
(table[self.id_column].id[..., 0] - self.origin)
92+
.to(dtype=self.slope.dtype)
93+
.unsqueeze(-1)
94+
)
95+
trend = self.slope * time + self.intercept # [..., F, T]
10196
values = table.numerical.unflatten(-1, (trend.size(-1), -1))
10297
numerical = (values + trend.unsqueeze(-1)).flatten(-2)
10398
return table.replace_blocks(
@@ -107,5 +102,5 @@ def _inverse_transform(self, table: TableTensor) -> TableTensor:
107102
def __repr__(self, *, indent: int = 0) -> str:
108103
return (
109104
f"{' ' * indent}{self.__class__.__name__}("
110-
f"time_column={self.time_column!r}, threshold={self.threshold})"
105+
f"id_column={self.id_column!r}, threshold={self.threshold})"
111106
)

‎test/models/timesfm3/test_detrending.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from sdm import ColumnarTensor, TableTensor
77
from sdm.models.timesfm3 import TimesFM3
8-
from sdm.models.timesfm3.recipe import TIME_COLUMN
98
from sdm.processing.execution import RecipeExecution
109
from sdm.testing import withCUDA
1110

@@ -15,7 +14,7 @@ def test_restores_each_target_trend_across_quantiles(
1514
device: torch.device,
1615
) -> None:
1716
context = TableTensor(
18-
columns={"numerical": ["a", "b"], "id": [TIME_COLUMN]},
17+
columns={"numerical": ["a", "b"], "id": ["__timesfm3_id__"]},
1918
numerical=torch.tensor(
2019
[[1.0, 10.0], [5.0, 20.0], [9.0, 30.0]], device=device
2120
),
@@ -36,7 +35,7 @@ def test_restores_each_target_trend_across_quantiles(
3635
for name in ("a", "b")
3736
for q in quantiles
3837
],
39-
"id": [TIME_COLUMN],
38+
"id": ["__timesfm3_id__"],
4039
},
4140
numerical=residual.flatten(-2),
4241
id=ColumnarTensor((torch.arange(3, 5, device=device),)),

0 commit comments

Comments
 (0)