Skip to content

Commit 2820dcd

Browse files
author
Daniel Park
committed
update base
1 parent 291ba03 commit 2820dcd

16 files changed

Lines changed: 1125 additions & 114 deletions

File tree

.ai/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,4 @@ gh pr list --state open --search "<short area keywords>"
5454
- First-time contributors should not use code agents to submit PRs or issues. Code agents should
5555
warn users about this policy if the user does not appear to be an existing contributor, including
5656
the risk of being blocked if they do it regardless. More information can be found in
57-
`CONTRIBUTING.md`.
57+
`CONTRIBUTING.md`.

.ai/review-rules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,4 @@ Read these on demand, when the diff touches the relevant area. Do not read all o
8080
- Anchor every inline comment to a line the diff actually touches.
8181
- State the concrete failure: what input, what goes wrong. "This breaks when `predict_sequence_length` > `train_length` during decode" beats "consider handling the edge case".
8282
- If you are unsure, say so in one clause and move on — do not pad a weak finding into a paragraph.
83-
- Reference the doc that supports your point by repo-root path, so the author can find it.
83+
- Reference the doc that supports your point by repo-root path, so the author can find it.

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ style: ## Run formatters and linters (black, isort, flake8, pre-commit)
1212

1313
## Run all unit tests
1414
test: ## Run unit tests using unittest
15-
python -m unittest discover
15+
CUDA_VISIBLE_DEVICES=0 python -m unittest discover
1616

1717
## Build the documentation
1818
docs: ## Build HTML documentation using Sphinx

poetry.lock

Lines changed: 935 additions & 70 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ classifiers = [
3636
"Intended Audience :: Developers",
3737
"Intended Audience :: Science/Research",
3838
"Programming Language :: Python :: 3",
39-
"Programming Language :: Python :: 3.8",
4039
"Programming Language :: Python :: 3.9",
4140
"Programming Language :: Python :: 3.10",
4241
"Programming Language :: Python :: 3.11",
@@ -57,9 +56,9 @@ homepage = "https://time-series-prediction.readthedocs.io"
5756
tfts-forecast = "tfts.cli.forecasting:main"
5857

5958
[tool.poetry.dependencies]
60-
python = ">=3.8,<=3.13"
59+
python = ">=3.9,<3.13"
6160
pandas = ">=1.3.0"
62-
# better install independently
61+
tensorflow = ">=2.13"
6362

6463

6564
[tool.poetry.group.dev.dependencies]

tests/test_data/test_get_data.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,21 @@ def test_get_data_invalid_name(self):
6767

6868
def test_get_data_test_size_validation(self):
6969
"""Test get_data validates test_size parameter"""
70-
with self.assertRaises(AssertionError):
70+
with self.assertRaises(ValueError):
7171
get_data("sine", 10, 4, test_size=-0.1)
7272

73-
with self.assertRaises(AssertionError):
73+
with self.assertRaises(ValueError):
7474
get_data("sine", 10, 4, test_size=1.5)
7575

76+
def test_get_sine_seeded_reproducibility(self):
77+
first = get_sine(10, 4, test_size=0, n_examples=5, seed=42)
78+
second = get_sine(10, 4, test_size=0, n_examples=5, seed=42)
79+
different = get_sine(10, 4, test_size=0, n_examples=5, seed=43)
80+
81+
np.testing.assert_array_equal(first[0], second[0])
82+
np.testing.assert_array_equal(first[1], second[1])
83+
self.assertFalse(np.array_equal(first[0], different[0]))
84+
7685
def test_get_data_airpassengers(self):
7786
"""Test get_data dispatcher for airpassengers dataset"""
7887
train_length = 12

tests/test_metrics.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import unittest
2+
3+
import numpy as np
4+
import tensorflow as tf
5+
6+
from tfts.metrics import mae, mape, mse, smape
7+
8+
9+
class MetricsTest(unittest.TestCase):
10+
def test_integer_inputs_use_floating_point_arithmetic(self):
11+
y_true = np.array([0, 2], dtype=np.int32)
12+
y_pred = np.array([0, 1], dtype=np.int32)
13+
14+
self.assertTrue(np.isfinite(mape(y_true, y_pred)))
15+
self.assertTrue(np.isfinite(smape(y_true, y_pred)))
16+
self.assertAlmostEqual(mse(y_true, y_pred), 0.5)
17+
self.assertAlmostEqual(mae(y_true, y_pred), 0.5)
18+
19+
def test_tensor_metrics_are_finite_for_integer_zeros(self):
20+
y_true = tf.constant([0, 2], dtype=tf.int32)
21+
y_pred = tf.constant([0, 1], dtype=tf.int32)
22+
23+
self.assertTrue(bool(tf.math.is_finite(mape(y_true, y_pred))))
24+
self.assertTrue(bool(tf.math.is_finite(smape(y_true, y_pred))))
25+
26+
def test_metric_function_is_logged_by_keras_fit(self):
27+
model = tf.keras.Sequential([tf.keras.layers.Input((1,)), tf.keras.layers.Dense(1)])
28+
model.compile(optimizer="sgd", loss="mse", metrics=[mae])
29+
history = model.fit(np.ones((2, 1)), np.ones((2, 1)), epochs=1, verbose=0)
30+
31+
self.assertIn("mae", history.history)
32+
33+
34+
if __name__ == "__main__":
35+
unittest.main()

tests/test_models/test_auto_config.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import unittest
22

3+
import tensorflow as tf
4+
35
import tfts
46
from tfts.models.auto_config import AutoConfig
57
from tfts.models.auto_model import AutoModel
6-
from tfts.models.registry import list_models
8+
from tfts.models.registry import get_model_info, list_models
79

810

911
class TestAutoModel(unittest.TestCase):
@@ -28,3 +30,37 @@ def test_listed_models_can_be_instantiated(self):
2830
config = AutoConfig.for_model(model_name)
2931
model = AutoModel.from_config(config, predict_sequence_length=2)
3032
self.assertEqual(model.config.model_type, model_name)
33+
34+
def test_registry_metadata_matches_auto_dispatch(self):
35+
for model_name in list_models():
36+
with self.subTest(model_name=model_name):
37+
info = get_model_info(model_name)
38+
config = AutoConfig.for_model(model_name)
39+
model = AutoModel.from_config(config, predict_sequence_length=2)
40+
41+
self.assertEqual(type(config).__name__, info["config_class"])
42+
self.assertEqual(type(model.model).__name__, info["class_name"])
43+
44+
def test_listed_models_satisfy_forward_contract(self):
45+
# AutoFormer currently operates in its configured hidden dimension;
46+
# N-BEATS is intentionally univariate. Other models accept a small
47+
# multivariate input.
48+
feature_counts = {"autoformer": 64, "nbeats": 1}
49+
multivariate_outputs = {"diffusion", "itransformer"}
50+
51+
predict_sequence_length = 8
52+
for model_name in list_models():
53+
with self.subTest(model_name=model_name):
54+
feature_count = feature_counts.get(model_name, 3)
55+
config = AutoConfig.for_model(model_name)
56+
model = AutoModel.from_config(config, predict_sequence_length=predict_sequence_length)
57+
output = model(tf.random.normal([1, 16, feature_count]))
58+
59+
if model_name == "deep_ar":
60+
self.assertEqual(len(output), 2)
61+
self.assertEqual(output[0].shape, (1, 16, 1))
62+
self.assertEqual(output[1].shape, (1, 16, 1))
63+
elif model_name in multivariate_outputs:
64+
self.assertEqual(output.shape, (1, predict_sequence_length, feature_count))
65+
else:
66+
self.assertEqual(output.shape, (1, predict_sequence_length, 1))

tests/test_models/test_base.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,23 @@ def test_config_save_and_load(self):
2626
loaded_config = BaseConfig.from_json(config_path)
2727
self.assertEqual(loaded_config.to_dict(), self.config.to_dict())
2828

29+
def test_pretrained_config_accepts_directory_or_file(self):
30+
with tempfile.TemporaryDirectory() as tmpdirname:
31+
self.config.save_pretrained(tmpdirname)
32+
config_path = os.path.join(tmpdirname, "config.json")
33+
34+
from_directory = BaseConfig.from_pretrained(tmpdirname)
35+
from_file = BaseConfig.from_pretrained(config_path)
36+
37+
self.assertEqual(from_directory.to_dict(), self.config_data)
38+
self.assertEqual(from_file.to_dict(), self.config_data)
39+
40+
def test_save_pretrained_reports_serialization_errors(self):
41+
self.config.invalid_value = object()
42+
with tempfile.TemporaryDirectory() as tmpdirname:
43+
with self.assertRaises(TypeError):
44+
self.config.save_pretrained(tmpdirname)
45+
2946

3047
class TestBaseModel(unittest.TestCase):
3148
def setUp(self):

tfts/data/get_data.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import logging
66
import os
7-
import random
87
from typing import Dict, List, Optional, Tuple, Union
98

109
import numpy as np
@@ -219,9 +218,10 @@ def download_and_extract(name: str) -> str:
219218
def get_data(
220219
name: str = "sine", train_length: int = 24, predict_sequence_length: int = 8, test_size: float = 0.1, **kwargs
221220
) -> Union[Tuple[np.ndarray, np.ndarray], Tuple[Tuple[np.ndarray, np.ndarray]], pd.DataFrame]:
222-
assert (test_size >= 0) & (test_size <= 1), "test_size is the ratio of test dataset"
221+
if not 0 <= test_size <= 1:
222+
raise ValueError("test_size must be between 0 and 1")
223223
if name == "sine":
224-
return get_sine(train_length, predict_sequence_length, test_size=test_size)
224+
return get_sine(train_length, predict_sequence_length, test_size=test_size, **kwargs)
225225

226226
elif name == "airpassengers":
227227
return get_air_passengers(train_length, predict_sequence_length, test_size=test_size)
@@ -245,7 +245,11 @@ def get_data(
245245

246246

247247
def get_sine(
248-
train_sequence_length: int = 24, predict_sequence_length: int = 8, test_size: float = 0.2, n_examples: int = 100
248+
train_sequence_length: int = 24,
249+
predict_sequence_length: int = 8,
250+
test_size: float = 0.2,
251+
n_examples: int = 100,
252+
seed: Optional[int] = None,
249253
) -> Union[Tuple[np.ndarray, np.ndarray], Tuple[Tuple[np.ndarray, np.ndarray]]]:
250254
"""
251255
Generate synthetic sine wave data.
@@ -259,10 +263,18 @@ def get_sine(
259263
Returns:
260264
(tuple): Two tuples of numpy arrays containing training and validation data.
261265
"""
266+
if train_sequence_length < 1 or predict_sequence_length < 1:
267+
raise ValueError("sequence lengths must be positive")
268+
if n_examples < 1:
269+
raise ValueError("n_examples must be positive")
270+
if not 0 <= test_size <= 1:
271+
raise ValueError("test_size must be between 0 and 1")
272+
273+
rng = np.random.default_rng(seed)
262274
x: List[np.ndarray] = []
263275
y: List[np.ndarray] = []
264276
for _ in range(n_examples):
265-
rand = random.random() * 2 * np.pi
277+
rand = rng.uniform(0.0, 2.0 * np.pi)
266278
sig1 = np.sin(np.linspace(rand, 3.0 * np.pi + rand, train_sequence_length + predict_sequence_length))
267279
sig2 = np.cos(np.linspace(rand, 3.0 * np.pi + rand, train_sequence_length + predict_sequence_length))
268280

@@ -376,14 +388,13 @@ def get_ar_data(
376388
if noise < 0:
377389
raise ValueError("noise parameter must be non-negative")
378390

379-
if seed is not None:
380-
np.random.seed(seed)
391+
rng = np.random.default_rng(seed)
381392

382393
# Sample parameters for each series
383-
linear_trends = np.random.normal(size=n_series)[:, None] / timesteps
384-
quadratic_trends = np.random.normal(size=n_series)[:, None] / timesteps**2
385-
seasonalities = np.random.normal(size=n_series)[:, None]
386-
levels = level * np.random.normal(size=n_series)[:, None]
394+
linear_trends = rng.normal(size=n_series)[:, None] / timesteps
395+
quadratic_trends = rng.normal(size=n_series)[:, None] / timesteps**2
396+
seasonalities = rng.normal(size=n_series)[:, None]
397+
levels = level * rng.normal(size=n_series)[:, None]
387398

388399
# Generate time index
389400
x = np.arange(timesteps)[None, :]
@@ -401,7 +412,7 @@ def get_ar_data(
401412
series = levels + series
402413

403414
# Add noise
404-
series = series * (1 + noise * np.random.normal(size=series.shape))
415+
series = series * (1 + noise * rng.normal(size=series.shape))
405416

406417
# Apply exponential transform if requested
407418
if exp:

0 commit comments

Comments
 (0)