| """TinyCast model assembly. |
| |
| model(past_values, scale_factor, prediction_length, batch_first).quantile_outputs |
| """ |
|
|
| from dataclasses import dataclass |
|
|
| import torch |
| import torch.nn as nn |
|
|
| from .config import TinyCastConfig |
|
|
|
|
| @dataclass |
| class PredictionOutput: |
| quantile_outputs: torch.Tensor |
| prediction_outputs: torch.Tensor = None |
|
|
|
|
| class TinyCastBackbone(nn.Module): |
| """Inner model: per-window min-max norm -> dilated-conv core -> denorm.""" |
|
|
| def __init__(self, config: TinyCastConfig): |
| super().__init__() |
| self.config = config |
| from .backbone import DilatedConvBackbone |
| from .normalization import WindowMinMax |
|
|
| self.core = DilatedConvBackbone( |
| seq_len=int(config.seq_len), |
| p_out=int(config.output_token_len), |
| n_quantiles=int(config.num_quantiles), |
| d=int(config.conv_dim), |
| n_layers=int(config.n_layers), |
| kernel=int(config.kernel_size), |
| ffn_mult=float(config.ffn_mult), |
| top_k_periods=int(config.top_k_periods), |
| significance_alpha=float(config.significance_alpha), |
| n_harmonics=int(config.n_harmonics), |
| pool_kind=str(config.pool_kind), |
| causal=bool(config.causal), |
| phase_bins=int(config.phase_bins), |
| decoder_depth=int(config.decoder_depth), |
| separable_conv=bool(config.separable_conv), |
| share_ffn=bool(config.share_ffn), |
| future_conv=bool(config.future_conv), |
| future_conv_layers=int(config.future_conv_layers), |
| future_conv_seed=int(config.future_conv_seed), |
| ) |
| self.norm = WindowMinMax(eps_clamp=1e-5) |
|
|
| def encode( |
| self, |
| past_values: torch.Tensor, |
| batch_first: bool = True, |
| scale_factor: "torch.Tensor | float | None" = None, |
| horizon: "int | None" = None, |
| ) -> "tuple[torch.Tensor, torch.Tensor, torch.Tensor]": |
| """per-window min-max norm -> dilated-conv core -> normalized y.""" |
| x = past_values |
| if not batch_first: |
| x = x.transpose(0, 1) |
| if x.dim() == 2: |
| x = x.unsqueeze(-1) |
|
|
| x_normed, x_min, x_range = self.norm.transform(x) |
| nan_mask = (~torch.isnan(x)).to(x.dtype) |
| y_norm = self.core( |
| x_normed, nan_mask=nan_mask, scale_factor=scale_factor, |
| horizon=horizon, |
| ) |
| return y_norm, x_min, x_range |
|
|
|
|
| class TinyCastForPrediction(nn.Module): |
| """Top-level model container. |
| |
| Usage: |
| model = TinyCastForPrediction(TinyCastConfig()) |
| out = model(past_values=x, scale_factor=sf, prediction_length=pl, |
| batch_first=False) |
| quantiles = out.quantile_outputs # (B, Q, pred_len, 1) |
| |
| NOTE: this ``forward`` is the SINGLE-SHOT arbitrary-horizon path. The |
| deployed GIFT-Eval inference uses the AR-rollout predictor |
| (``tinycast.predictor.TinyCastPredictor``), which calls ``self.model.encode`` |
| in 48-step chunks. For horizons <= 48 the two paths coincide. |
| """ |
|
|
| def __init__(self, config: TinyCastConfig): |
| super().__init__() |
| self.config = config |
| self.model = TinyCastBackbone(config) |
|
|
| def forward(self, past_values, scale_factor=None, prediction_length=None, |
| batch_first=None): |
| if scale_factor is None: |
| scale_factor = 1.0 |
| if batch_first is None: |
| batch_first = True |
| ctx = past_values |
| if not batch_first: |
| ctx = ctx.transpose(0, 1) |
| if ctx.dim() == 2: |
| ctx = ctx.unsqueeze(-1) |
| ctx_len_max = int(self.config.seq_len) |
| ctx_in = ctx[:, -ctx_len_max:, :] |
| p_out_native = int(self.model.core.p_out) |
| pred_len = int(prediction_length) if prediction_length else p_out_native |
| y_norm, x_min, x_range = self.model.encode( |
| ctx_in, batch_first=True, scale_factor=scale_factor, |
| horizon=pred_len, |
| ) |
| if y_norm.dim() == 2: |
| y_norm = y_norm.unsqueeze(-1) |
| y_pred = y_norm * x_range + x_min |
| quantile_outputs = y_pred.permute(0, 2, 1).unsqueeze(-1) |
| return PredictionOutput(quantile_outputs=quantile_outputs) |
|
|