Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,17 @@ jobs:
path: ~/.cache/huggingface/hub/
key: ${{ Runner.os }}-huggingface-${{ hashFiles('**/tests/') }}

- name: Cache TabPFN checkpoints
uses: actions/cache@v4
with:
path: ~/.cache/tabpfn
key: ${{ runner.os }}-tabpfn-v1

- name: Run tests
run: uv run pytest
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
TABPFN_TOKEN: ${{ secrets.TABPFN_TOKEN }}

- name: Test import class
run: uv run -- python -c "from timecopilot import TimeCopilot, TimeCopilotForecaster"
Expand Down
1 change: 1 addition & 0 deletions docs/changelogs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Welcome to the TimeCopilot Changelog. Here, you will find a comprehensive list of all the changes, updates, and improvements made to the TimeCopilot project. This section is designed to keep you informed about the latest features, bug fixes, and enhancements as we continue to develop and refine the TimeCopilot experience. Stay tuned for regular updates and feel free to explore the details of each release below.

- [v0.0.33](v0.0.33.md)
- [v0.0.32](v0.0.32.md)
- [v0.0.31](v0.0.31.md)
- [v0.0.30](v0.0.30.md)
Expand Down
38 changes: 38 additions & 0 deletions docs/changelogs/v0.0.33.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
### Features

* **TabPFN-3 foundation model**: Added support for TabPFN-3 via `foundationforecast>=0.1.6`. Use the existing `TabPFN` class with `model_path`; TabPFN-2 remains the default.

```python
import pandas as pd
from tabpfn_time_series import TabPFNMode
from timecopilot.models.foundation.tabpfn import TABPFN_V2_MODEL, TABPFN_V3_MODEL, TabPFN

df = pd.read_csv(
"https://timecopilot.s3.amazonaws.com/public/data/air_passengers.csv",
parse_dates=["ds"],
)

model = TabPFN(
model_path=TABPFN_V3_MODEL,
mode=TabPFNMode.LOCAL,
context_length=32768,
alias="TabPFN-3",
)
assert model.model_path == TABPFN_V3_MODEL
```

**License note:** TabPFN-2.6+ and TabPFN-3 weights use the TabPFN Non-Commercial license. First LOCAL use requires accepting terms at [ux.priorlabs.ai](https://ux.priorlabs.ai) (`TABPFN_TOKEN`). TabPFN-3 is LOCAL-only today.

* **TabPFN family notebook**: Added [`tabpfn-family` example notebook](../examples/tabpfn-family.ipynb) comparing TabPFN-2 and TabPFN-3 with prediction intervals.

### Dependencies

* Bumped `foundationforecast` from `>=0.1.3` to `>=0.1.6` (TabPFN-3 via `model_path`, `tabpfn-time-series>=1.2.0`).

### CI

* Added `TABPFN_TOKEN` secret and TabPFN checkpoint cache for LOCAL integration tests.

---

**Full Changelog**: https://github.com/TimeCopilot/timecopilot/compare/v0.0.32...v0.0.33
1 change: 1 addition & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ For model API details, see the [Model Hub](../model-hub.md).
| [Compare Foundation Models](ts-foundation-models-comparison-quickstart.ipynb) | Benchmark multiple foundation models side by side | Python 3.10+; GPU optional |
| [Chronos Family](chronos-family.ipynb) | Forecast with Chronos 1.x and 2.x checkpoints | Python 3.10+ |
| [TimesFM Family](timesfm-family.ipynb) | Forecast with TimesFM 1.0, 2.0, 2.5, and 3.0 with prediction intervals | Python 3.10+ |
| [TabPFN Family](tabpfn-family.ipynb) | Forecast with TabPFN-2 and TabPFN-3 with prediction intervals | Python 3.10–3.12; `TABPFN_TOKEN` for LOCAL |
| [TiRex Family](tirex-family.ipynb) | Forecast with TiRex 1.0 and 2.0 | Python 3.11+ |
| [Toto Family](toto-family.ipynb) | Forecast with Toto 1.0 and 2.0 | Python 3.10+ |
| [Finetuning](finetuning.ipynb) | Adapt Chronos 2 and TimeGPT to your data | Python 3.10+; GPU recommended |
Expand Down
858 changes: 858 additions & 0 deletions docs/examples/tabpfn-family.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/model-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ TimeCopilot provides a unified interface to state-of-the-art foundation models f
- [PatchTST-FM](api/models/foundation/models.md#timecopilot.models.foundation.patchtst_fm) ([arXiv:2602.06909](https://arxiv.org/abs/2602.06909))
- [Sundial](api/models/foundation/models.md#timecopilot.models.foundation.sundial) ([arXiv:2502.00816](https://arxiv.org/pdf/2502.00816))
- [T0](api/models/foundation/models.md#timecopilot.models.foundation.t0) ([model card](https://huggingface.co/theforecastingcompany/t0-alpha))
- [TabPFN](api/models/foundation/models.md#timecopilot.models.foundation.tabpfn) ([arXiv:2501.02945](https://arxiv.org/abs/2501.02945))
- [TabPFN](api/models/foundation/models.md#timecopilot.models.foundation.tabpfn) ([arXiv:2501.02945](https://arxiv.org/abs/2501.02945); TabPFN-2 default, TabPFN-3 via `model_path`; [NC license](https://docs.priorlabs.ai/models))
- [TiRex / TiRex-2](api/models/foundation/models.md#timecopilot.models.foundation.tirex) ([arXiv:2505.23719](https://arxiv.org/abs/2505.23719), [arXiv:2607.01204](https://arxiv.org/abs/2607.01204))
- [TimeGPT](api/models/foundation/models.md#timecopilot.models.foundation.timegpt) ([arXiv:2310.03589](https://arxiv.org/abs/2310.03589))
- [TimesFM](api/models/foundation/models.md#timecopilot.models.foundation.timesfm) ([arXiv:2310.10688](https://arxiv.org/abs/2310.10688); [3.0 license](https://huggingface.co/google/timesfm-3.0-pytorch/blob/main/LICENSE))
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ nav:
- Chronos: examples/chronos-family.ipynb
- TimesFM: examples/timesfm-family.ipynb
- TiRex: examples/tirex-family.ipynb
- TabPFN: examples/tabpfn-family.ipynb
- Toto: examples/toto-family.ipynb
- Finetuning: examples/finetuning.ipynb
- Benchmarks and Ensembles:
Expand All @@ -62,6 +63,7 @@ nav:
- experiments/fev.md
- Changelogs:
- changelogs/index.md
- changelogs/v0.0.33.md
- changelogs/v0.0.32.md
- changelogs/v0.0.31.md
- changelogs/v0.0.30.md
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dev = [
"pytest-rerunfailures>=15.1",
"pytest-xdist>=3.8.0",
"pytest>=8.4.2",
"python-dotenv>=1.0",
"s3fs>=2025.3.0",
"sktime>=0.40.1",
"timecopilot-gift-eval>=0.3.0",
Expand Down Expand Up @@ -68,7 +69,7 @@ dependencies = [
"catboost>=1.2.10",
"datasets>=4.1.1",
"fire",
"foundationforecast>=0.1.3",
"foundationforecast>=0.1.6",
"fsspec>=2025.9.0",
"huggingface-hub>=0.36.2,<2.0",
"hydra-core>=1.3.2",
Expand Down Expand Up @@ -107,7 +108,7 @@ license = "MIT"
name = "timecopilot"
readme = "README.md"
requires-python = ">=3.10"
version = "0.0.32"
version = "0.0.33"

[project.optional-dependencies]
distributed = [
Expand Down
22 changes: 20 additions & 2 deletions tests/models/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,26 @@ def disable_mps_session(monkeypatch):
models.append(PatchTSTFM(context_length=2_048))

if sys.version_info < (3, 13):
from tabpfn_time_series import TabPFNMode
from contextlib import contextmanager

import numpy as np
import pandas as pd
from tabpfn_time_series import TimeSeriesDataFrame

from timecopilot.models.foundation.tabpfn import TabPFN

models.append(TabPFN(mode=TabPFNMode.MOCK))
class _MockTabPFNPredictor:
def predict(self, train_tsdf, test_tsdf, quantiles=None):
result = {"target": np.full(len(test_tsdf), 1.0)}
if quantiles is not None:
for q in quantiles:
result[q] = np.full(len(test_tsdf), q)
return TimeSeriesDataFrame(pd.DataFrame(result, index=test_tsdf.index))

@contextmanager
def _mock_get_model(_self):
yield _MockTabPFNPredictor()

tabpfn_model = TabPFN()
tabpfn_model._get_model = lambda: _mock_get_model(tabpfn_model) # type: ignore[method-assign, assignment]
models.append(tabpfn_model)
153 changes: 153 additions & 0 deletions tests/models/foundation/test_tabpfn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
from __future__ import annotations

import os
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from dotenv import load_dotenv

pytest.importorskip("tabpfn_time_series", reason="TabPFN requires Python < 3.13")

from tabpfn.errors import TabPFNLicenseError # noqa: E402
from tabpfn_time_series import TabPFNMode # noqa: E402
from utilsforecast.data import generate_series # noqa: E402

from timecopilot.models.foundation.tabpfn import ( # noqa: E402
TABPFN_V2_MODEL,
TABPFN_V3_MODEL,
TabPFN,
)

load_dotenv(Path(__file__).resolve().parents[3] / ".env")

pytestmark = pytest.mark.models

DEFAULT_QUANTILES = [round(i * 0.1, 1) for i in range(1, 10)]
DEFAULT_LEVEL = [0, 20, 40, 60, 80]

TABPFN_CASES = [
pytest.param(TABPFN_V2_MODEL, 4096, "TabPFN-2", id="v2"),
pytest.param(TABPFN_V3_MODEL, 32768, "TabPFN-3", id="v3"),
]


def _require_tabpfn_token() -> None:
if not os.environ.get("TABPFN_TOKEN"):
pytest.skip("TABPFN_TOKEN not set")


def test_tabpfn_default_model_path() -> None:
model = TabPFN()
assert model.model_path == TABPFN_V2_MODEL


def test_tabpfn_v3_model_path() -> None:
model = TabPFN(
model_path=TABPFN_V3_MODEL,
context_length=32768,
mode=TabPFNMode.LOCAL,
)
assert model.model_path == TABPFN_V3_MODEL
assert model.context_length == 32768


def test_tabpfn_v3_rejects_client_mode() -> None:
with pytest.raises(ValueError, match="LOCAL-only"):
TabPFN(model_path=TABPFN_V3_MODEL, mode=TabPFNMode.CLIENT)


def test_tabpfn_predictor_receives_model_path() -> None:
with patch(
"foundationforecast.models.tabpfn.TabPFNTimeSeriesPredictor"
) as predictor_cls:
predictor_cls.return_value = MagicMock()
model = TabPFN(model_path=TABPFN_V3_MODEL, mode=TabPFNMode.LOCAL)
with model._get_model():
pass
predictor_cls.assert_called_once_with(
tabpfn_mode=TabPFNMode.LOCAL,
tabpfn_config={"model_path": TABPFN_V3_MODEL},
)


@pytest.fixture(scope="module")
def tabpfn_df():
return generate_series(n_series=1, freq="D", min_length=30, max_length=30)


def _make_model(model_path: str, context_length: int, alias: str) -> TabPFN:
return TabPFN(
model_path=model_path,
mode=TabPFNMode.LOCAL,
context_length=context_length,
alias=alias,
)


def _forecast_or_skip_v3(model: TabPFN, *args, **kwargs):
try:
return model.forecast(*args, **kwargs)
except TabPFNLicenseError as exc:
if model.model_path == TABPFN_V3_MODEL:
pytest.skip(
"TabPFN-3 license not accepted; accept at https://ux.priorlabs.ai"
)
raise exc


@pytest.mark.parametrize("model_path,context_length,alias", TABPFN_CASES)
def test_tabpfn_local_point_forecast(
tabpfn_df, model_path: str, context_length: int, alias: str
) -> None:
_require_tabpfn_token()
fcst = _forecast_or_skip_v3(
_make_model(model_path, context_length, alias),
tabpfn_df,
h=3,
freq="D",
)
assert fcst.shape == (3, 3)
assert alias in fcst.columns


@pytest.mark.parametrize("model_path,context_length,alias", TABPFN_CASES)
def test_tabpfn_local_quantile_forecast(
tabpfn_df, model_path: str, context_length: int, alias: str
) -> None:
_require_tabpfn_token()
fcst = _forecast_or_skip_v3(
_make_model(model_path, context_length, alias),
tabpfn_df,
h=3,
freq="D",
quantiles=DEFAULT_QUANTILES,
)
q_cols = [f"{alias}-q-{int(100 * q)}" for q in DEFAULT_QUANTILES]
assert len(fcst.columns) == 3 + len(q_cols)
assert all(col in fcst.columns for col in q_cols)
assert not any("-lo-" in col or "-hi-" in col for col in fcst.columns)
for c1, c2 in zip(q_cols[:-1], q_cols[1:], strict=False):
assert fcst[c1].le(fcst[c2]).mean() >= 0.8


@pytest.mark.parametrize("model_path,context_length,alias", TABPFN_CASES)
def test_tabpfn_local_level_forecast(
tabpfn_df, model_path: str, context_length: int, alias: str
) -> None:
_require_tabpfn_token()
fcst = _forecast_or_skip_v3(
_make_model(model_path, context_length, alias),
tabpfn_df,
h=3,
freq="D",
level=DEFAULT_LEVEL,
)
lv_cols = []
for lv in DEFAULT_LEVEL:
lv_cols.extend([f"{alias}-lo-{lv}", f"{alias}-hi-{lv}"])
assert len(fcst.columns) == 3 + len(lv_cols)
assert all(col in fcst.columns for col in lv_cols)
assert not any("-q-" in col for col in fcst.columns)
for lo, hi in zip(lv_cols[2::2], lv_cols[3::2], strict=False):
assert fcst[lo].le(fcst[hi]).all()
3 changes: 2 additions & 1 deletion timecopilot/models/foundation/tabpfn.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from foundationforecast.models.tabpfn import TABPFN_V2_MODEL, TABPFN_V3_MODEL
from foundationforecast.models.tabpfn import TabPFN as _TabPFN

from ..utils.forecaster import Forecaster
Expand All @@ -7,4 +8,4 @@ class TabPFN(_TabPFN, Forecaster):
pass


__all__ = ["TabPFN"]
__all__ = ["TABPFN_V2_MODEL", "TABPFN_V3_MODEL", "TabPFN"]
Loading
Loading