# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Tests for TimesFM configuration dataclasses. These tests verify that config dataclasses enforce immutability, compose correctly, and carry the exact default values the model implementation relies on. Catching a silent default-value drift here prevents subtle inference regressions that would otherwise only surface as degraded forecast quality. """ import dataclasses import pytest from timesfm.configs import ( ForecastConfig, RandomFourierFeaturesConfig, ResidualBlockConfig, StackedTransformersConfig, TransformerConfig, ) # --------------------------------------------------------------------------- # ForecastConfig # --------------------------------------------------------------------------- class TestForecastConfig: """Tests for ForecastConfig — the primary user-facing configuration.""" def test_defaults_match_safe_inference_settings(self): """Default config must be conservative: no normalization, no fancy heads. These defaults are what users get when they call ``ForecastConfig()`` without arguments. Changing them silently would break all existing code that relies on the defaults. """ cfg = ForecastConfig() assert cfg.max_context == 0 assert cfg.max_horizon == 0 assert cfg.normalize_inputs is False assert cfg.per_core_batch_size == 1 assert cfg.use_continuous_quantile_head is False assert cfg.force_flip_invariance is True assert cfg.infer_is_positive is True assert cfg.fix_quantile_crossing is False assert cfg.return_backcast is False def test_frozen_prevents_mutation(self): """Configs are frozen dataclasses — mutating them must raise. This is critical because ``compile()`` captures the config object and the compiled decode closure relies on its values never changing. """ cfg = ForecastConfig(max_context=512) with pytest.raises(dataclasses.FrozenInstanceError): cfg.max_context = 1024 def test_replace_creates_independent_copy(self): """``dataclasses.replace`` must yield a new object with updated fields. The compile path uses ``replace`` to adjust context/horizon to valid multiples; the original config must remain untouched. """ original = ForecastConfig(max_context=512, max_horizon=128) replaced = dataclasses.replace(original, max_context=1024) assert replaced.max_context == 1024 assert replaced.max_horizon == 128 # untouched assert original.max_context == 512 # original unchanged def test_equality_is_structural(self): """Two configs with identical fields must be equal (value semantics).""" a = ForecastConfig(max_context=256, normalize_inputs=True) b = ForecastConfig(max_context=256, normalize_inputs=True) assert a == b def test_inequality_on_any_field_difference(self): """A single differing field must break equality.""" a = ForecastConfig(max_context=256) b = ForecastConfig(max_context=512) assert a != b # --------------------------------------------------------------------------- # ResidualBlockConfig # --------------------------------------------------------------------------- class TestResidualBlockConfig: """Tests for ResidualBlockConfig used by tokenizer and output projections.""" def test_frozen_prevents_mutation(self): cfg = ResidualBlockConfig( input_dims=64, hidden_dims=128, output_dims=128, use_bias=True, activation="swish", ) with pytest.raises(dataclasses.FrozenInstanceError): cfg.input_dims = 32 def test_activation_accepts_all_valid_literals(self): """All three activation modes must be constructable without error.""" for act in ("relu", "swish", "none"): cfg = ResidualBlockConfig( input_dims=8, hidden_dims=16, output_dims=8, use_bias=False, activation=act, ) assert cfg.activation == act # --------------------------------------------------------------------------- # TransformerConfig & StackedTransformersConfig # --------------------------------------------------------------------------- class TestTransformerConfig: """Tests for TransformerConfig — architecture-level hyperparameters.""" def test_model_dims_must_be_divisible_by_num_heads(self): """The model instantiation will fail if this invariant is broken. We verify the config at least *carries* the right values that the TimesFM 2.5 definition uses (1280 dims, 16 heads → 80 head_dim). """ cfg = TransformerConfig( model_dims=1280, hidden_dims=1280, num_heads=16, attention_norm="rms", feedforward_norm="rms", qk_norm="rms", use_bias=False, use_rotary_position_embeddings=True, ff_activation="swish", fuse_qkv=True, ) assert cfg.model_dims % cfg.num_heads == 0 assert cfg.model_dims // cfg.num_heads == 80 # head_dim def test_stacked_config_composes_correctly(self): """StackedTransformersConfig must wrap a TransformerConfig cleanly.""" xf = TransformerConfig( model_dims=64, hidden_dims=64, num_heads=4, attention_norm="rms", feedforward_norm="rms", qk_norm="none", use_bias=True, use_rotary_position_embeddings=False, ff_activation="relu", fuse_qkv=False, ) stacked = StackedTransformersConfig(num_layers=6, transformer=xf) assert stacked.num_layers == 6 assert stacked.transformer is xf assert stacked.transformer.model_dims == 64 # --------------------------------------------------------------------------- # RandomFourierFeaturesConfig # --------------------------------------------------------------------------- class TestRandomFourierFeaturesConfig: """Tests for RandomFourierFeaturesConfig.""" def test_frozen_prevents_mutation(self): cfg = RandomFourierFeaturesConfig( input_dims=32, output_dims=64, projection_stddev=1.0, use_bias=True, ) with pytest.raises(dataclasses.FrozenInstanceError): cfg.output_dims = 128