Upload folder using huggingface_hub
Browse files- README.md +30 -6
- app.py +264 -0
- example_daily_sales.csv +366 -0
- example_hourly_energy.csv +1025 -0
- example_monthly_temp.csv +121 -0
- requirements.txt +4 -0
- tinycast/__init__.py +121 -0
- tinycast/backbone.py +1070 -0
- tinycast/checkpoint.py +45 -0
- tinycast/config.py +49 -0
- tinycast/corpus.py +661 -0
- tinycast/downsample.py +148 -0
- tinycast/encoding.py +123 -0
- tinycast/eval.py +540 -0
- tinycast/export.py +517 -0
- tinycast/losses.py +253 -0
- tinycast/model.py +118 -0
- tinycast/normalization.py +58 -0
- tinycast/periodogram.py +104 -0
- tinycast/predictor.py +518 -0
- tinycast/quant.py +81 -0
- tinycast/reference/dataset_properties.json +1 -0
- tinycast/reference/gift_eval_tinycast.csv +98 -0
- tinycast/reference/seasonal_naive.csv +98 -0
- tinycast/scale.py +52 -0
- tinycast/synth.py +338 -0
- tinycast/train.py +714 -0
README.md
CHANGED
|
@@ -1,13 +1,37 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.25.0
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: TinyCast Forecaster
|
| 3 |
+
emoji: 📈
|
| 4 |
+
colorFrom: gray
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.25.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: TinyCast zero-shot probabilistic time-series forecasting
|
| 10 |
+
python_version: "3.12"
|
| 11 |
+
startup_duration_timeout: 30m
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# TinyCast Forecaster
|
| 15 |
+
|
| 16 |
+
A Gradio demo for [TinyCast](https://huggingface.co/raws-labs/tinycast), an attention-free,
|
| 17 |
+
146,505-parameter time-series foundation model that forecasts unseen series zero-shot and
|
| 18 |
+
returns nine quantile forecasts.
|
| 19 |
+
|
| 20 |
+
## How it works
|
| 21 |
+
|
| 22 |
+
1. Upload a CSV file with a value column (or paste numeric values directly)
|
| 23 |
+
2. Select the frequency/domain of your time series
|
| 24 |
+
3. Choose a forecast horizon (1–512 steps)
|
| 25 |
+
4. Get a probabilistic forecast plot with quantile confidence bands
|
| 26 |
+
|
| 27 |
+
The model replaces self-attention with dilated causal convolutions and a zero-parameter
|
| 28 |
+
normalized-periodogram phase prior, so periodicity is computed from the context instead
|
| 29 |
+
of learned. Every learned operation is a convolution, a matrix multiplication or a
|
| 30 |
+
normalization, so the model streams in constant memory and runs on CPU.
|
| 31 |
+
|
| 32 |
+
## Model
|
| 33 |
+
|
| 34 |
+
- **Weights**: [raws-labs/tinycast](https://huggingface.co/raws-labs/tinycast)
|
| 35 |
+
- **Code**: [github.com/raws-labs/tinycast](https://github.com/raws-labs/tinycast)
|
| 36 |
+
- **Paper**: [arXiv:2608.15767](https://arxiv.org/abs/2608.15767)
|
| 37 |
+
- **License**: Apache-2.0
|
app.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TinyCast: Probabilistic Zero-Shot Forecasting — Gradio demo.
|
| 2 |
+
|
| 3 |
+
TinyCast is a 146K-parameter attention-free time-series foundation model that
|
| 4 |
+
forecasts unseen series zero-shot and returns nine quantile forecasts.
|
| 5 |
+
https://huggingface.co/raws-labs/tinycast
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import io
|
| 10 |
+
import time
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
import pandas as pd
|
| 15 |
+
import gradio as gr
|
| 16 |
+
import matplotlib
|
| 17 |
+
|
| 18 |
+
matplotlib.use("Agg")
|
| 19 |
+
import matplotlib.pyplot as plt
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
|
| 23 |
+
# ---------------------------------------------------------------------------
|
| 24 |
+
# Model loading — at module scope, CPU (the model is 146K params, ~0.6 MB).
|
| 25 |
+
# ---------------------------------------------------------------------------
|
| 26 |
+
from huggingface_hub import hf_hub_download
|
| 27 |
+
|
| 28 |
+
HF_REPO = "raws-labs/tinycast"
|
| 29 |
+
|
| 30 |
+
# Download config.json and model.safetensors (config.json lands beside weights)
|
| 31 |
+
_hf_config = hf_hub_download(HF_REPO, "config.json")
|
| 32 |
+
WEIGHTS_PATH = hf_hub_download(HF_REPO, "model.safetensors")
|
| 33 |
+
|
| 34 |
+
# The tinycast package ships on GitHub; we vendor it into the Space repo.
|
| 35 |
+
import sys
|
| 36 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 37 |
+
|
| 38 |
+
from tinycast import TinyCastPredictor
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
# Frequency → (freq_label, domain) mapping for the seasonal scale factor.
|
| 43 |
+
# The model needs a pandas frequency string and a domain for the scale factor.
|
| 44 |
+
# ---------------------------------------------------------------------------
|
| 45 |
+
FREQ_MAP = {
|
| 46 |
+
"Hourly (H)": ("H", "Energy"),
|
| 47 |
+
"Daily (D)": ("D", "Sales"),
|
| 48 |
+
"Weekly (W)": ("W", "Sales"),
|
| 49 |
+
"Monthly (M)": ("M", "Sales"),
|
| 50 |
+
"Quarterly (Q)": ("Q", "Economic"),
|
| 51 |
+
"4-hourly (4H)": ("4H", "Energy"),
|
| 52 |
+
"10-minute (10T)": ("10T", "Energy"),
|
| 53 |
+
"Minute (T)": ("T", "Energy"),
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def parse_csv(file_obj):
|
| 58 |
+
"""Parse an uploaded CSV file into a numpy array of values."""
|
| 59 |
+
if file_obj is None:
|
| 60 |
+
return None, None
|
| 61 |
+
try:
|
| 62 |
+
df = pd.read_csv(file_obj.name)
|
| 63 |
+
except Exception:
|
| 64 |
+
df = pd.read_csv(file_obj)
|
| 65 |
+
|
| 66 |
+
# Try to find a value column: look for common names, else take last column
|
| 67 |
+
value_cols = [c for c in df.columns if c.lower() in ("value", "y", "target", "v", "measurement")]
|
| 68 |
+
if value_cols:
|
| 69 |
+
values = df[value_cols[0]].values
|
| 70 |
+
else:
|
| 71 |
+
values = df.iloc[:, -1].values
|
| 72 |
+
|
| 73 |
+
values = pd.to_numeric(pd.Series(values), errors="coerce").values.astype("float32")
|
| 74 |
+
return values, df
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def parse_text_input(text):
|
| 78 |
+
"""Parse pasted numeric values (comma or newline separated)."""
|
| 79 |
+
if not text or not text.strip():
|
| 80 |
+
return None
|
| 81 |
+
parts = text.replace(",", " ").replace("\n", " ").split()
|
| 82 |
+
vals = []
|
| 83 |
+
for p in parts:
|
| 84 |
+
try:
|
| 85 |
+
vals.append(float(p))
|
| 86 |
+
except ValueError:
|
| 87 |
+
pass
|
| 88 |
+
if not vals:
|
| 89 |
+
return None
|
| 90 |
+
return np.array(vals, dtype="float32")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def forecast(
|
| 94 |
+
csv_file,
|
| 95 |
+
text_input,
|
| 96 |
+
freq_choice,
|
| 97 |
+
prediction_length,
|
| 98 |
+
flip_invariance,
|
| 99 |
+
):
|
| 100 |
+
"""Run TinyCast zero-shot forecast and return a quantile plot."""
|
| 101 |
+
# Get the time series values
|
| 102 |
+
if csv_file is not None:
|
| 103 |
+
values, _ = parse_csv(csv_file)
|
| 104 |
+
else:
|
| 105 |
+
values = parse_text_input(text_input)
|
| 106 |
+
|
| 107 |
+
if values is None or len(values) < 10:
|
| 108 |
+
return None, "Please provide at least 10 data points (via CSV upload or text input)."
|
| 109 |
+
|
| 110 |
+
freq_str, domain = FREQ_MAP.get(freq_choice, ("H", "Energy"))
|
| 111 |
+
|
| 112 |
+
# Build gluonts-style entry
|
| 113 |
+
try:
|
| 114 |
+
start = pd.Period("2020-01-01 00", freq=freq_str.lower())
|
| 115 |
+
except Exception:
|
| 116 |
+
start = pd.Period("2020-01-01", freq="D")
|
| 117 |
+
|
| 118 |
+
entry = {"target": values, "start": start, "item_id": "input"}
|
| 119 |
+
|
| 120 |
+
# Build the predictor
|
| 121 |
+
t0 = time.perf_counter()
|
| 122 |
+
predictor = TinyCastPredictor(
|
| 123 |
+
prediction_length=int(prediction_length),
|
| 124 |
+
checkpoint_path=WEIGHTS_PATH,
|
| 125 |
+
freq=freq_str,
|
| 126 |
+
domain=domain,
|
| 127 |
+
device="cpu",
|
| 128 |
+
force_flip_invariance=bool(flip_invariance),
|
| 129 |
+
)
|
| 130 |
+
forecasts = predictor.predict([entry])
|
| 131 |
+
elapsed = time.perf_counter() - t0
|
| 132 |
+
|
| 133 |
+
fc = forecasts[0]
|
| 134 |
+
# forecast_array is (Q, prediction_length) with quantile levels ascending
|
| 135 |
+
arr = fc.forecast_array # (Q, pl)
|
| 136 |
+
quantile_levels = [float(k) for k in fc.forecast_keys]
|
| 137 |
+
pl = arr.shape[1]
|
| 138 |
+
|
| 139 |
+
# Build the plot
|
| 140 |
+
fig, ax = plt.subplots(figsize=(12, 5))
|
| 141 |
+
|
| 142 |
+
# Plot context (last 200 points for readability)
|
| 143 |
+
context_show = min(200, len(values))
|
| 144 |
+
ctx_x = np.arange(-context_show, 0)
|
| 145 |
+
ax.plot(ctx_x, values[-context_show:], color="black", linewidth=1.0, label="Context (observed)")
|
| 146 |
+
|
| 147 |
+
# Plot forecast quantiles
|
| 148 |
+
fut_x = np.arange(0, pl)
|
| 149 |
+
q_colors = plt.cm.viridis(np.linspace(0.15, 0.85, len(quantile_levels)))
|
| 150 |
+
|
| 151 |
+
# Fill between outer quantiles for the confidence band
|
| 152 |
+
if len(quantile_levels) >= 2:
|
| 153 |
+
ax.fill_between(
|
| 154 |
+
fut_x, arr[0, :], arr[-1, :],
|
| 155 |
+
color="steelblue", alpha=0.15, label=f"{int(float(quantile_levels[0])*100)}%–{int(float(quantile_levels[-1])*100)}% interval",
|
| 156 |
+
)
|
| 157 |
+
if len(quantile_levels) >= 4:
|
| 158 |
+
mid_lo = len(quantile_levels) // 4
|
| 159 |
+
mid_hi = 3 * len(quantile_levels) // 4
|
| 160 |
+
ax.fill_between(
|
| 161 |
+
fut_x, arr[mid_lo, :], arr[mid_hi, :],
|
| 162 |
+
color="steelblue", alpha=0.25, label=f"{int(float(quantile_levels[mid_lo])*100)}%–{int(float(quantile_levels[mid_hi])*100)}% interval",
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
# Median line
|
| 166 |
+
qm = len(quantile_levels) // 2
|
| 167 |
+
ax.plot(fut_x, arr[qm, :], color="crimson", linewidth=2.0, label="Median forecast")
|
| 168 |
+
|
| 169 |
+
# Outer quantile lines
|
| 170 |
+
for i in [0, -1]:
|
| 171 |
+
ax.plot(fut_x, arr[i, :], color=q_colors[i], linewidth=0.8, linestyle="--", alpha=0.5)
|
| 172 |
+
|
| 173 |
+
ax.axvline(x=0, color="gray", linestyle=":", linewidth=0.8)
|
| 174 |
+
ax.set_xlabel("Time (relative)")
|
| 175 |
+
ax.set_ylabel("Value")
|
| 176 |
+
ax.set_title(f"TinyCast {int(prediction_length)}-step probabilistic forecast ({freq_choice})")
|
| 177 |
+
ax.legend(loc="upper left", fontsize=8)
|
| 178 |
+
fig.tight_layout()
|
| 179 |
+
|
| 180 |
+
# Summary text
|
| 181 |
+
info = (
|
| 182 |
+
f"Context: {len(values)} points | Frequency: {freq_str} | Domain: {domain} | "
|
| 183 |
+
f"Horizon: {pl} steps | Quantiles: {len(quantile_levels)} | "
|
| 184 |
+
f"Flip-invariance: {'on' if flip_invariance else 'off'} | "
|
| 185 |
+
f"Inference: {elapsed:.2f}s"
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
plt.close(fig)
|
| 189 |
+
return fig, info
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# ---------------------------------------------------------------------------
|
| 193 |
+
# Build the Gradio UI
|
| 194 |
+
# ---------------------------------------------------------------------------
|
| 195 |
+
CSS = """
|
| 196 |
+
#main-col { max-width: 1200px; margin: 0 auto; }
|
| 197 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 198 |
+
"""
|
| 199 |
+
|
| 200 |
+
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="TinyCast Forecaster") as demo:
|
| 201 |
+
gr.Markdown(
|
| 202 |
+
"# TinyCast: Probabilistic Zero-Shot Forecasting\n\n"
|
| 203 |
+
"An attention-free, 146K-parameter time-series foundation model that forecasts "
|
| 204 |
+
"unseen series zero-shot and returns nine quantile forecasts. "
|
| 205 |
+
"[Model](https://huggingface.co/raws-labs/tinycast) · "
|
| 206 |
+
"[Paper](https://arxiv.org/abs/2608.15767) · "
|
| 207 |
+
"[Code](https://github.com/raws-labs/tinycast)"
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
with gr.Column(elem_id="main-col"):
|
| 211 |
+
with gr.Row():
|
| 212 |
+
with gr.Column(scale=1):
|
| 213 |
+
gr.Markdown("### Input")
|
| 214 |
+
csv_input = gr.File(label="Upload CSV (value column or last column used)", file_types=[".csv"])
|
| 215 |
+
gr.Markdown("**— or —**")
|
| 216 |
+
text_input = gr.Textbox(
|
| 217 |
+
label="Paste values (comma or newline separated)",
|
| 218 |
+
placeholder="10.0, 12.3, 9.8, 11.1, ...",
|
| 219 |
+
lines=3,
|
| 220 |
+
)
|
| 221 |
+
freq_choice = gr.Dropdown(
|
| 222 |
+
choices=list(FREQ_MAP.keys()),
|
| 223 |
+
value="Hourly (H)",
|
| 224 |
+
label="Frequency / Domain",
|
| 225 |
+
)
|
| 226 |
+
prediction_length = gr.Slider(
|
| 227 |
+
minimum=1, maximum=512, value=48, step=1,
|
| 228 |
+
label="Forecast horizon (steps)",
|
| 229 |
+
)
|
| 230 |
+
flip_inv = gr.Checkbox(
|
| 231 |
+
value=True,
|
| 232 |
+
label="Flip-invariance symmetrization (improves accuracy)",
|
| 233 |
+
)
|
| 234 |
+
run_btn = gr.Button("Forecast", variant="primary")
|
| 235 |
+
|
| 236 |
+
with gr.Column(scale=2):
|
| 237 |
+
gr.Markdown("### Forecast")
|
| 238 |
+
plot_output = gr.Plot(label="Probabilistic forecast")
|
| 239 |
+
info_output = gr.Textbox(label="Summary", interactive=False)
|
| 240 |
+
|
| 241 |
+
gr.Markdown("### Examples")
|
| 242 |
+
gr.Examples(
|
| 243 |
+
examples=[
|
| 244 |
+
["example_hourly_energy.csv", None, "Hourly (H)", 48, True],
|
| 245 |
+
["example_daily_sales.csv", None, "Daily (D)", 30, True],
|
| 246 |
+
["example_monthly_temp.csv", None, "Monthly (M)", 24, True],
|
| 247 |
+
],
|
| 248 |
+
inputs=[csv_input, text_input, freq_choice, prediction_length, flip_inv],
|
| 249 |
+
outputs=[plot_output, info_output],
|
| 250 |
+
fn=forecast,
|
| 251 |
+
cache_examples=True,
|
| 252 |
+
cache_mode="lazy",
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
gr.Markdown(
|
| 256 |
+
"\n\n---\n"
|
| 257 |
+
"TinyCast replaces self-attention with dilated causal convolutions and a "
|
| 258 |
+
"zero-parameter normalized-periodogram phase prior. The model is 146,505 "
|
| 259 |
+
"parameters (~0.6 MB) and runs on CPU. License: Apache-2.0."
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
if __name__ == "__main__":
|
| 264 |
+
demo.launch(mcp_server=True)
|
example_daily_sales.csv
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
timestamp,value
|
| 2 |
+
2023-01-01,50.91415
|
| 3 |
+
2023-01-02,54.748363
|
| 4 |
+
2023-01-03,62.10063
|
| 5 |
+
2023-01-04,57.31053
|
| 6 |
+
2023-01-05,40.008057
|
| 7 |
+
2023-01-06,36.59418
|
| 8 |
+
2023-01-07,42.865208
|
| 9 |
+
2023-01-08,49.40127
|
| 10 |
+
2023-01-09,58.16791
|
| 11 |
+
2023-01-10,57.64015
|
| 12 |
+
2023-01-11,57.47703
|
| 13 |
+
2023-01-12,48.544537
|
| 14 |
+
2023-01-13,41.048813
|
| 15 |
+
2023-01-14,46.21341
|
| 16 |
+
2023-01-15,52.102528
|
| 17 |
+
2023-01-16,55.990437
|
| 18 |
+
2023-01-17,61.655533
|
| 19 |
+
2023-01-18,52.31219
|
| 20 |
+
2023-01-19,49.196514
|
| 21 |
+
2023-01-20,41.05094
|
| 22 |
+
2023-01-21,42.627098
|
| 23 |
+
2023-01-22,49.00721
|
| 24 |
+
2023-01-23,62.585938
|
| 25 |
+
2023-01-24,60.43569
|
| 26 |
+
2023-01-25,54.253853
|
| 27 |
+
2023-01-26,45.854763
|
| 28 |
+
2023-01-27,43.147648
|
| 29 |
+
2023-01-28,44.628017
|
| 30 |
+
2023-01-29,52.6382
|
| 31 |
+
2023-01-30,60.56078
|
| 32 |
+
2023-01-31,67.674225
|
| 33 |
+
2023-02-01,54.669594
|
| 34 |
+
2023-02-02,45.724434
|
| 35 |
+
2023-02-03,39.459404
|
| 36 |
+
2023-02-04,45.72962
|
| 37 |
+
2023-02-05,55.136917
|
| 38 |
+
2023-02-06,59.276474
|
| 39 |
+
2023-02-07,59.078808
|
| 40 |
+
2023-02-08,53.765392
|
| 41 |
+
2023-02-09,49.562943
|
| 42 |
+
2023-02-10,44.480484
|
| 43 |
+
2023-02-11,45.86115
|
| 44 |
+
2023-02-12,50.10347
|
| 45 |
+
2023-02-13,60.6648
|
| 46 |
+
2023-02-14,62.299335
|
| 47 |
+
2023-02-15,57.244904
|
| 48 |
+
2023-02-16,50.575447
|
| 49 |
+
2023-02-17,43.271507
|
| 50 |
+
2023-02-18,46.618427
|
| 51 |
+
2023-02-19,52.652737
|
| 52 |
+
2023-02-20,61.185673
|
| 53 |
+
2023-02-21,64.193146
|
| 54 |
+
2023-02-22,52.56737
|
| 55 |
+
2023-02-23,47.35215
|
| 56 |
+
2023-02-24,41.539604
|
| 57 |
+
2023-02-25,43.015053
|
| 58 |
+
2023-02-26,51.974575
|
| 59 |
+
2023-02-27,65.15314
|
| 60 |
+
2023-02-28,60.051785
|
| 61 |
+
2023-03-01,60.193672
|
| 62 |
+
2023-03-02,43.612553
|
| 63 |
+
2023-03-03,42.296066
|
| 64 |
+
2023-03-04,45.769943
|
| 65 |
+
2023-03-05,54.90867
|
| 66 |
+
2023-03-06,63.151993
|
| 67 |
+
2023-03-07,65.37932
|
| 68 |
+
2023-03-08,56.592663
|
| 69 |
+
2023-03-09,47.624107
|
| 70 |
+
2023-03-10,46.224648
|
| 71 |
+
2023-03-11,45.057774
|
| 72 |
+
2023-03-12,49.67294
|
| 73 |
+
2023-03-13,57.968452
|
| 74 |
+
2023-03-14,60.590923
|
| 75 |
+
2023-03-15,59.48032
|
| 76 |
+
2023-03-16,49.78844
|
| 77 |
+
2023-03-17,46.072178
|
| 78 |
+
2023-03-18,44.69993
|
| 79 |
+
2023-03-19,54.32562
|
| 80 |
+
2023-03-20,63.595085
|
| 81 |
+
2023-03-21,62.77124
|
| 82 |
+
2023-03-22,59.709164
|
| 83 |
+
2023-03-23,47.725384
|
| 84 |
+
2023-03-24,43.26156
|
| 85 |
+
2023-03-25,45.18647
|
| 86 |
+
2023-03-26,50.61248
|
| 87 |
+
2023-03-27,63.529232
|
| 88 |
+
2023-03-28,62.64107
|
| 89 |
+
2023-03-29,58.72632
|
| 90 |
+
2023-03-30,51.503403
|
| 91 |
+
2023-03-31,46.040314
|
| 92 |
+
2023-04-01,48.67784
|
| 93 |
+
2023-04-02,54.254543
|
| 94 |
+
2023-04-03,61.14842
|
| 95 |
+
2023-04-04,64.160126
|
| 96 |
+
2023-04-05,53.976833
|
| 97 |
+
2023-04-06,46.069824
|
| 98 |
+
2023-04-07,41.082623
|
| 99 |
+
2023-04-08,44.039944
|
| 100 |
+
2023-04-09,56.099323
|
| 101 |
+
2023-04-10,60.051876
|
| 102 |
+
2023-04-11,63.61479
|
| 103 |
+
2023-04-12,63.286522
|
| 104 |
+
2023-04-13,49.69237
|
| 105 |
+
2023-04-14,47.613266
|
| 106 |
+
2023-04-15,44.580833
|
| 107 |
+
2023-04-16,54.633686
|
| 108 |
+
2023-04-17,60.26825
|
| 109 |
+
2023-04-18,64.08218
|
| 110 |
+
2023-04-19,62.25976
|
| 111 |
+
2023-04-20,45.929203
|
| 112 |
+
2023-04-21,47.053993
|
| 113 |
+
2023-04-22,48.444893
|
| 114 |
+
2023-04-23,53.81755
|
| 115 |
+
2023-04-24,59.130142
|
| 116 |
+
2023-04-25,65.665665
|
| 117 |
+
2023-04-26,58.50036
|
| 118 |
+
2023-04-27,52.15919
|
| 119 |
+
2023-04-28,46.16628
|
| 120 |
+
2023-04-29,52.88702
|
| 121 |
+
2023-04-30,55.231934
|
| 122 |
+
2023-05-01,60.74782
|
| 123 |
+
2023-05-02,66.337105
|
| 124 |
+
2023-05-03,61.098827
|
| 125 |
+
2023-05-04,55.888725
|
| 126 |
+
2023-05-05,48.956055
|
| 127 |
+
2023-05-06,49.502296
|
| 128 |
+
2023-05-07,60.689907
|
| 129 |
+
2023-05-08,60.602024
|
| 130 |
+
2023-05-09,64.23003
|
| 131 |
+
2023-05-10,58.00911
|
| 132 |
+
2023-05-11,50.991734
|
| 133 |
+
2023-05-12,42.670662
|
| 134 |
+
2023-05-13,50.687138
|
| 135 |
+
2023-05-14,55.983334
|
| 136 |
+
2023-05-15,60.105896
|
| 137 |
+
2023-05-16,63.45254
|
| 138 |
+
2023-05-17,62.07938
|
| 139 |
+
2023-05-18,55.025543
|
| 140 |
+
2023-05-19,53.140915
|
| 141 |
+
2023-05-20,57.873272
|
| 142 |
+
2023-05-21,58.24323
|
| 143 |
+
2023-05-22,61.8997
|
| 144 |
+
2023-05-23,60.45314
|
| 145 |
+
2023-05-24,62.291973
|
| 146 |
+
2023-05-25,50.42234
|
| 147 |
+
2023-05-26,46.25465
|
| 148 |
+
2023-05-27,47.645393
|
| 149 |
+
2023-05-28,56.927628
|
| 150 |
+
2023-05-29,68.41625
|
| 151 |
+
2023-05-30,67.670425
|
| 152 |
+
2023-05-31,61.362934
|
| 153 |
+
2023-06-01,50.104202
|
| 154 |
+
2023-06-02,42.82667
|
| 155 |
+
2023-06-03,48.37276
|
| 156 |
+
2023-06-04,57.53865
|
| 157 |
+
2023-06-05,70.8721
|
| 158 |
+
2023-06-06,67.9401
|
| 159 |
+
2023-06-07,65.137054
|
| 160 |
+
2023-06-08,52.063274
|
| 161 |
+
2023-06-09,44.64589
|
| 162 |
+
2023-06-10,47.286335
|
| 163 |
+
2023-06-11,55.87432
|
| 164 |
+
2023-06-12,72.30373
|
| 165 |
+
2023-06-13,65.43512
|
| 166 |
+
2023-06-14,65.054306
|
| 167 |
+
2023-06-15,51.20238
|
| 168 |
+
2023-06-16,51.34544
|
| 169 |
+
2023-06-17,51.68654
|
| 170 |
+
2023-06-18,57.930088
|
| 171 |
+
2023-06-19,66.14603
|
| 172 |
+
2023-06-20,66.28492
|
| 173 |
+
2023-06-21,64.22705
|
| 174 |
+
2023-06-22,52.896214
|
| 175 |
+
2023-06-23,45.223904
|
| 176 |
+
2023-06-24,47.04787
|
| 177 |
+
2023-06-25,59.267765
|
| 178 |
+
2023-06-26,71.35559
|
| 179 |
+
2023-06-27,69.079254
|
| 180 |
+
2023-06-28,62.882923
|
| 181 |
+
2023-06-29,55.46864
|
| 182 |
+
2023-06-30,53.168728
|
| 183 |
+
2023-07-01,51.88983
|
| 184 |
+
2023-07-02,57.867218
|
| 185 |
+
2023-07-03,70.28718
|
| 186 |
+
2023-07-04,70.23555
|
| 187 |
+
2023-07-05,68.196106
|
| 188 |
+
2023-07-06,55.510864
|
| 189 |
+
2023-07-07,45.927315
|
| 190 |
+
2023-07-08,47.477207
|
| 191 |
+
2023-07-09,64.40279
|
| 192 |
+
2023-07-10,72.48931
|
| 193 |
+
2023-07-11,68.76072
|
| 194 |
+
2023-07-12,62.789276
|
| 195 |
+
2023-07-13,59.695496
|
| 196 |
+
2023-07-14,46.629585
|
| 197 |
+
2023-07-15,49.247505
|
| 198 |
+
2023-07-16,61.72998
|
| 199 |
+
2023-07-17,66.4845
|
| 200 |
+
2023-07-18,69.63391
|
| 201 |
+
2023-07-19,63.798508
|
| 202 |
+
2023-07-20,56.673885
|
| 203 |
+
2023-07-21,54.523167
|
| 204 |
+
2023-07-22,52.55344
|
| 205 |
+
2023-07-23,62.081818
|
| 206 |
+
2023-07-24,61.867798
|
| 207 |
+
2023-07-25,69.85313
|
| 208 |
+
2023-07-26,62.109146
|
| 209 |
+
2023-07-27,52.354725
|
| 210 |
+
2023-07-28,48.016262
|
| 211 |
+
2023-07-29,51.629314
|
| 212 |
+
2023-07-30,63.247707
|
| 213 |
+
2023-07-31,64.38914
|
| 214 |
+
2023-08-01,70.44118
|
| 215 |
+
2023-08-02,63.536327
|
| 216 |
+
2023-08-03,55.378143
|
| 217 |
+
2023-08-04,54.008995
|
| 218 |
+
2023-08-05,54.59603
|
| 219 |
+
2023-08-06,64.8622
|
| 220 |
+
2023-08-07,68.2548
|
| 221 |
+
2023-08-08,68.61145
|
| 222 |
+
2023-08-09,64.66726
|
| 223 |
+
2023-08-10,57.438652
|
| 224 |
+
2023-08-11,51.88044
|
| 225 |
+
2023-08-12,50.07852
|
| 226 |
+
2023-08-13,61.47147
|
| 227 |
+
2023-08-14,69.753
|
| 228 |
+
2023-08-15,78.6017
|
| 229 |
+
2023-08-16,71.319374
|
| 230 |
+
2023-08-17,54.501434
|
| 231 |
+
2023-08-18,50.83857
|
| 232 |
+
2023-08-19,49.29136
|
| 233 |
+
2023-08-20,59.777878
|
| 234 |
+
2023-08-21,70.36513
|
| 235 |
+
2023-08-22,75.01684
|
| 236 |
+
2023-08-23,63.851585
|
| 237 |
+
2023-08-24,55.448723
|
| 238 |
+
2023-08-25,45.608852
|
| 239 |
+
2023-08-26,53.543686
|
| 240 |
+
2023-08-27,58.712757
|
| 241 |
+
2023-08-28,68.18
|
| 242 |
+
2023-08-29,69.1187
|
| 243 |
+
2023-08-30,66.10605
|
| 244 |
+
2023-08-31,52.487976
|
| 245 |
+
2023-09-01,47.999584
|
| 246 |
+
2023-09-02,60.76943
|
| 247 |
+
2023-09-03,58.387733
|
| 248 |
+
2023-09-04,66.82796
|
| 249 |
+
2023-09-05,77.61002
|
| 250 |
+
2023-09-06,75.45404
|
| 251 |
+
2023-09-07,54.596462
|
| 252 |
+
2023-09-08,51.645973
|
| 253 |
+
2023-09-09,55.75635
|
| 254 |
+
2023-09-10,67.786095
|
| 255 |
+
2023-09-11,67.507744
|
| 256 |
+
2023-09-12,71.71345
|
| 257 |
+
2023-09-13,69.42085
|
| 258 |
+
2023-09-14,59.76546
|
| 259 |
+
2023-09-15,51.97225
|
| 260 |
+
2023-09-16,54.680218
|
| 261 |
+
2023-09-17,58.825314
|
| 262 |
+
2023-09-18,70.10379
|
| 263 |
+
2023-09-19,72.000114
|
| 264 |
+
2023-09-20,68.135345
|
| 265 |
+
2023-09-21,57.14518
|
| 266 |
+
2023-09-22,54.865337
|
| 267 |
+
2023-09-23,58.469833
|
| 268 |
+
2023-09-24,63.76629
|
| 269 |
+
2023-09-25,72.22359
|
| 270 |
+
2023-09-26,73.30875
|
| 271 |
+
2023-09-27,67.78909
|
| 272 |
+
2023-09-28,56.996487
|
| 273 |
+
2023-09-29,54.750202
|
| 274 |
+
2023-09-30,55.489826
|
| 275 |
+
2023-10-01,69.929504
|
| 276 |
+
2023-10-02,76.23838
|
| 277 |
+
2023-10-03,74.65682
|
| 278 |
+
2023-10-04,65.84966
|
| 279 |
+
2023-10-05,56.173927
|
| 280 |
+
2023-10-06,57.72415
|
| 281 |
+
2023-10-07,56.919933
|
| 282 |
+
2023-10-08,65.44043
|
| 283 |
+
2023-10-09,66.63456
|
| 284 |
+
2023-10-10,76.63159
|
| 285 |
+
2023-10-11,69.8521
|
| 286 |
+
2023-10-12,56.52987
|
| 287 |
+
2023-10-13,53.086147
|
| 288 |
+
2023-10-14,57.27284
|
| 289 |
+
2023-10-15,64.5074
|
| 290 |
+
2023-10-16,71.341805
|
| 291 |
+
2023-10-17,73.88882
|
| 292 |
+
2023-10-18,68.08291
|
| 293 |
+
2023-10-19,60.66885
|
| 294 |
+
2023-10-20,59.265198
|
| 295 |
+
2023-10-21,49.13171
|
| 296 |
+
2023-10-22,63.98945
|
| 297 |
+
2023-10-23,73.097855
|
| 298 |
+
2023-10-24,75.43726
|
| 299 |
+
2023-10-25,68.0731
|
| 300 |
+
2023-10-26,55.290997
|
| 301 |
+
2023-10-27,56.184708
|
| 302 |
+
2023-10-28,62.363735
|
| 303 |
+
2023-10-29,60.448418
|
| 304 |
+
2023-10-30,75.509796
|
| 305 |
+
2023-10-31,73.913704
|
| 306 |
+
2023-11-01,69.354866
|
| 307 |
+
2023-11-02,57.75247
|
| 308 |
+
2023-11-03,54.54735
|
| 309 |
+
2023-11-04,61.43182
|
| 310 |
+
2023-11-05,67.147964
|
| 311 |
+
2023-11-06,78.46525
|
| 312 |
+
2023-11-07,78.78152
|
| 313 |
+
2023-11-08,71.2061
|
| 314 |
+
2023-11-09,66.492966
|
| 315 |
+
2023-11-10,57.2177
|
| 316 |
+
2023-11-11,60.36565
|
| 317 |
+
2023-11-12,64.86029
|
| 318 |
+
2023-11-13,73.817955
|
| 319 |
+
2023-11-14,73.50701
|
| 320 |
+
2023-11-15,73.20759
|
| 321 |
+
2023-11-16,58.076252
|
| 322 |
+
2023-11-17,58.59777
|
| 323 |
+
2023-11-18,57.659733
|
| 324 |
+
2023-11-19,69.61374
|
| 325 |
+
2023-11-20,76.220924
|
| 326 |
+
2023-11-21,81.41122
|
| 327 |
+
2023-11-22,72.78116
|
| 328 |
+
2023-11-23,57.24504
|
| 329 |
+
2023-11-24,56.39986
|
| 330 |
+
2023-11-25,55.065662
|
| 331 |
+
2023-11-26,64.89516
|
| 332 |
+
2023-11-27,78.852
|
| 333 |
+
2023-11-28,78.21188
|
| 334 |
+
2023-11-29,68.84205
|
| 335 |
+
2023-11-30,59.27001
|
| 336 |
+
2023-12-01,57.04907
|
| 337 |
+
2023-12-02,55.282005
|
| 338 |
+
2023-12-03,64.78658
|
| 339 |
+
2023-12-04,75.60434
|
| 340 |
+
2023-12-05,80.11521
|
| 341 |
+
2023-12-06,73.11512
|
| 342 |
+
2023-12-07,55.787292
|
| 343 |
+
2023-12-08,58.21382
|
| 344 |
+
2023-12-09,59.497787
|
| 345 |
+
2023-12-10,68.39167
|
| 346 |
+
2023-12-11,79.86694
|
| 347 |
+
2023-12-12,70.80956
|
| 348 |
+
2023-12-13,69.865524
|
| 349 |
+
2023-12-14,64.78388
|
| 350 |
+
2023-12-15,52.905937
|
| 351 |
+
2023-12-16,64.05953
|
| 352 |
+
2023-12-17,68.60507
|
| 353 |
+
2023-12-18,77.908066
|
| 354 |
+
2023-12-19,75.63645
|
| 355 |
+
2023-12-20,74.43013
|
| 356 |
+
2023-12-21,66.566574
|
| 357 |
+
2023-12-22,58.699356
|
| 358 |
+
2023-12-23,60.684887
|
| 359 |
+
2023-12-24,68.661026
|
| 360 |
+
2023-12-25,73.12828
|
| 361 |
+
2023-12-26,77.25669
|
| 362 |
+
2023-12-27,71.88127
|
| 363 |
+
2023-12-28,64.86134
|
| 364 |
+
2023-12-29,61.350193
|
| 365 |
+
2023-12-30,57.15608
|
| 366 |
+
2023-12-31,67.824974
|
example_hourly_energy.csv
ADDED
|
@@ -0,0 +1,1025 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
timestamp,value
|
| 2 |
+
2020-01-01 00:00:00,10.0251465
|
| 3 |
+
2020-01-01 01:00:00,10.768732
|
| 4 |
+
2020-01-01 02:00:00,11.665449
|
| 5 |
+
2020-01-01 03:00:00,12.198282
|
| 6 |
+
2020-01-01 04:00:00,12.565463
|
| 7 |
+
2020-01-01 05:00:00,13.063052
|
| 8 |
+
2020-01-01 06:00:00,13.372061
|
| 9 |
+
2020-01-01 07:00:00,13.216603
|
| 10 |
+
2020-01-01 08:00:00,12.604707
|
| 11 |
+
2020-01-01 09:00:00,12.033376
|
| 12 |
+
2020-01-01 10:00:00,11.558016
|
| 13 |
+
2020-01-01 11:00:00,10.984669
|
| 14 |
+
2020-01-01 12:00:00,9.751936
|
| 15 |
+
2020-01-01 13:00:00,9.413419
|
| 16 |
+
2020-01-01 14:00:00,8.500818
|
| 17 |
+
2020-01-01 15:00:00,7.9982424
|
| 18 |
+
2020-01-01 16:00:00,7.574732
|
| 19 |
+
2020-01-01 17:00:00,7.3358727
|
| 20 |
+
2020-01-01 18:00:00,7.394071
|
| 21 |
+
2020-01-01 19:00:00,7.636869
|
| 22 |
+
2020-01-01 20:00:00,7.7163033
|
| 23 |
+
2020-01-01 21:00:00,8.505526
|
| 24 |
+
2020-01-01 22:00:00,8.733487
|
| 25 |
+
2020-01-01 23:00:00,9.672831
|
| 26 |
+
2020-01-02 00:00:00,10.5716095
|
| 27 |
+
2020-01-02 01:00:00,11.197558
|
| 28 |
+
2020-01-02 02:00:00,11.76442
|
| 29 |
+
2020-01-02 03:00:00,12.360337
|
| 30 |
+
2020-01-02 04:00:00,12.939544
|
| 31 |
+
2020-01-02 05:00:00,13.383874
|
| 32 |
+
2020-01-02 06:00:00,13.248561
|
| 33 |
+
2020-01-02 07:00:00,13.314223
|
| 34 |
+
2020-01-02 08:00:00,13.031668
|
| 35 |
+
2020-01-02 09:00:00,12.701431
|
| 36 |
+
2020-01-02 10:00:00,12.020719
|
| 37 |
+
2020-01-02 11:00:00,11.330495
|
| 38 |
+
2020-01-02 12:00:00,10.356698
|
| 39 |
+
2020-01-02 13:00:00,9.688904
|
| 40 |
+
2020-01-02 14:00:00,9.151211
|
| 41 |
+
2020-01-02 15:00:00,8.674222
|
| 42 |
+
2020-01-02 16:00:00,7.6487126
|
| 43 |
+
2020-01-02 17:00:00,7.904658
|
| 44 |
+
2020-01-02 18:00:00,7.769175
|
| 45 |
+
2020-01-02 19:00:00,7.7581353
|
| 46 |
+
2020-01-02 20:00:00,7.953417
|
| 47 |
+
2020-01-02 21:00:00,8.312751
|
| 48 |
+
2020-01-02 22:00:00,9.286019
|
| 49 |
+
2020-01-02 23:00:00,10.106877
|
| 50 |
+
2020-01-03 00:00:00,10.847791
|
| 51 |
+
2020-01-03 01:00:00,11.522441
|
| 52 |
+
2020-01-03 02:00:00,12.049262
|
| 53 |
+
2020-01-03 03:00:00,12.351599
|
| 54 |
+
2020-01-03 04:00:00,13.062622
|
| 55 |
+
2020-01-03 05:00:00,13.487353
|
| 56 |
+
2020-01-03 06:00:00,13.192812
|
| 57 |
+
2020-01-03 07:00:00,13.4188595
|
| 58 |
+
2020-01-03 08:00:00,13.117062
|
| 59 |
+
2020-01-03 09:00:00,12.683891
|
| 60 |
+
2020-01-03 10:00:00,11.676296
|
| 61 |
+
2020-01-03 11:00:00,11.046415
|
| 62 |
+
2020-01-03 12:00:00,10.303629
|
| 63 |
+
2020-01-03 13:00:00,9.368568
|
| 64 |
+
2020-01-03 14:00:00,9.214399
|
| 65 |
+
2020-01-03 15:00:00,8.133051
|
| 66 |
+
2020-01-03 16:00:00,7.807804
|
| 67 |
+
2020-01-03 17:00:00,7.376652
|
| 68 |
+
2020-01-03 18:00:00,7.6284394
|
| 69 |
+
2020-01-03 19:00:00,7.6632047
|
| 70 |
+
2020-01-03 20:00:00,7.8102546
|
| 71 |
+
2020-01-03 21:00:00,7.703994
|
| 72 |
+
2020-01-03 22:00:00,8.760406
|
| 73 |
+
2020-01-03 23:00:00,9.593914
|
| 74 |
+
2020-01-04 00:00:00,10.417734
|
| 75 |
+
2020-01-04 01:00:00,10.852821
|
| 76 |
+
2020-01-04 02:00:00,12.047072
|
| 77 |
+
2020-01-04 03:00:00,12.022374
|
| 78 |
+
2020-01-04 04:00:00,12.613148
|
| 79 |
+
2020-01-04 05:00:00,13.214197
|
| 80 |
+
2020-01-04 06:00:00,13.121072
|
| 81 |
+
2020-01-04 07:00:00,13.3912115
|
| 82 |
+
2020-01-04 08:00:00,12.710301
|
| 83 |
+
2020-01-04 09:00:00,12.050664
|
| 84 |
+
2020-01-04 10:00:00,11.461852
|
| 85 |
+
2020-01-04 11:00:00,10.576923
|
| 86 |
+
2020-01-04 12:00:00,9.744464
|
| 87 |
+
2020-01-04 13:00:00,9.33093
|
| 88 |
+
2020-01-04 14:00:00,8.578868
|
| 89 |
+
2020-01-04 15:00:00,8.081609
|
| 90 |
+
2020-01-04 16:00:00,7.1764817
|
| 91 |
+
2020-01-04 17:00:00,7.3470883
|
| 92 |
+
2020-01-04 18:00:00,6.831262
|
| 93 |
+
2020-01-04 19:00:00,7.2876945
|
| 94 |
+
2020-01-04 20:00:00,7.1679893
|
| 95 |
+
2020-01-04 21:00:00,7.5664434
|
| 96 |
+
2020-01-04 22:00:00,8.367287
|
| 97 |
+
2020-01-04 23:00:00,9.229887
|
| 98 |
+
2020-01-05 00:00:00,9.81526
|
| 99 |
+
2020-01-05 01:00:00,10.425717
|
| 100 |
+
2020-01-05 02:00:00,10.981756
|
| 101 |
+
2020-01-05 03:00:00,11.575
|
| 102 |
+
2020-01-05 04:00:00,12.416953
|
| 103 |
+
2020-01-05 05:00:00,12.79881
|
| 104 |
+
2020-01-05 06:00:00,12.655396
|
| 105 |
+
2020-01-05 07:00:00,12.356761
|
| 106 |
+
2020-01-05 08:00:00,12.432598
|
| 107 |
+
2020-01-05 09:00:00,11.511688
|
| 108 |
+
2020-01-05 10:00:00,10.99086
|
| 109 |
+
2020-01-05 11:00:00,10.521675
|
| 110 |
+
2020-01-05 12:00:00,9.159056
|
| 111 |
+
2020-01-05 13:00:00,8.898518
|
| 112 |
+
2020-01-05 14:00:00,7.9705524
|
| 113 |
+
2020-01-05 15:00:00,7.4771733
|
| 114 |
+
2020-01-05 16:00:00,6.9537706
|
| 115 |
+
2020-01-05 17:00:00,6.7005877
|
| 116 |
+
2020-01-05 18:00:00,6.6883497
|
| 117 |
+
2020-01-05 19:00:00,6.4922676
|
| 118 |
+
2020-01-05 20:00:00,7.220683
|
| 119 |
+
2020-01-05 21:00:00,7.5519567
|
| 120 |
+
2020-01-05 22:00:00,8.19096
|
| 121 |
+
2020-01-05 23:00:00,8.973553
|
| 122 |
+
2020-01-06 00:00:00,9.6700535
|
| 123 |
+
2020-01-06 01:00:00,10.45399
|
| 124 |
+
2020-01-06 02:00:00,11.020703
|
| 125 |
+
2020-01-06 03:00:00,11.339109
|
| 126 |
+
2020-01-06 04:00:00,12.072465
|
| 127 |
+
2020-01-06 05:00:00,12.244225
|
| 128 |
+
2020-01-06 06:00:00,12.215451
|
| 129 |
+
2020-01-06 07:00:00,12.449818
|
| 130 |
+
2020-01-06 08:00:00,11.9857645
|
| 131 |
+
2020-01-06 09:00:00,11.418504
|
| 132 |
+
2020-01-06 10:00:00,10.796985
|
| 133 |
+
2020-01-06 11:00:00,10.338858
|
| 134 |
+
2020-01-06 12:00:00,9.5842705
|
| 135 |
+
2020-01-06 13:00:00,9.005072
|
| 136 |
+
2020-01-06 14:00:00,8.019431
|
| 137 |
+
2020-01-06 15:00:00,7.615106
|
| 138 |
+
2020-01-06 16:00:00,7.21694
|
| 139 |
+
2020-01-06 17:00:00,6.8739743
|
| 140 |
+
2020-01-06 18:00:00,6.0764546
|
| 141 |
+
2020-01-06 19:00:00,6.9059014
|
| 142 |
+
2020-01-06 20:00:00,7.036835
|
| 143 |
+
2020-01-06 21:00:00,7.540072
|
| 144 |
+
2020-01-06 22:00:00,8.161126
|
| 145 |
+
2020-01-06 23:00:00,8.897796
|
| 146 |
+
2020-01-07 00:00:00,9.672967
|
| 147 |
+
2020-01-07 01:00:00,10.325688
|
| 148 |
+
2020-01-07 02:00:00,10.753147
|
| 149 |
+
2020-01-07 03:00:00,11.745984
|
| 150 |
+
2020-01-07 04:00:00,12.097243
|
| 151 |
+
2020-01-07 05:00:00,12.787666
|
| 152 |
+
2020-01-07 06:00:00,12.630502
|
| 153 |
+
2020-01-07 07:00:00,12.617562
|
| 154 |
+
2020-01-07 08:00:00,12.146495
|
| 155 |
+
2020-01-07 09:00:00,11.75318
|
| 156 |
+
2020-01-07 10:00:00,11.247693
|
| 157 |
+
2020-01-07 11:00:00,10.245748
|
| 158 |
+
2020-01-07 12:00:00,9.843195
|
| 159 |
+
2020-01-07 13:00:00,9.002382
|
| 160 |
+
2020-01-07 14:00:00,8.080186
|
| 161 |
+
2020-01-07 15:00:00,7.2338934
|
| 162 |
+
2020-01-07 16:00:00,7.3571568
|
| 163 |
+
2020-01-07 17:00:00,6.913296
|
| 164 |
+
2020-01-07 18:00:00,6.7827377
|
| 165 |
+
2020-01-07 19:00:00,6.9620357
|
| 166 |
+
2020-01-07 20:00:00,7.6906977
|
| 167 |
+
2020-01-07 21:00:00,7.812737
|
| 168 |
+
2020-01-07 22:00:00,8.479959
|
| 169 |
+
2020-01-07 23:00:00,8.907433
|
| 170 |
+
2020-01-08 00:00:00,10.329468
|
| 171 |
+
2020-01-08 01:00:00,10.97865
|
| 172 |
+
2020-01-08 02:00:00,11.750752
|
| 173 |
+
2020-01-08 03:00:00,12.186837
|
| 174 |
+
2020-01-08 04:00:00,12.855928
|
| 175 |
+
2020-01-08 05:00:00,13.064922
|
| 176 |
+
2020-01-08 06:00:00,13.233898
|
| 177 |
+
2020-01-08 07:00:00,12.996748
|
| 178 |
+
2020-01-08 08:00:00,12.450676
|
| 179 |
+
2020-01-08 09:00:00,12.49223
|
| 180 |
+
2020-01-08 10:00:00,11.295678
|
| 181 |
+
2020-01-08 11:00:00,10.928416
|
| 182 |
+
2020-01-08 12:00:00,10.176038
|
| 183 |
+
2020-01-08 13:00:00,9.248605
|
| 184 |
+
2020-01-08 14:00:00,8.872624
|
| 185 |
+
2020-01-08 15:00:00,8.1046295
|
| 186 |
+
2020-01-08 16:00:00,7.59621
|
| 187 |
+
2020-01-08 17:00:00,7.503101
|
| 188 |
+
2020-01-08 18:00:00,7.216429
|
| 189 |
+
2020-01-08 19:00:00,7.7061625
|
| 190 |
+
2020-01-08 20:00:00,7.812301
|
| 191 |
+
2020-01-08 21:00:00,8.137366
|
| 192 |
+
2020-01-08 22:00:00,8.477673
|
| 193 |
+
2020-01-08 23:00:00,9.340978
|
| 194 |
+
2020-01-09 00:00:00,10.608282
|
| 195 |
+
2020-01-09 01:00:00,11.168635
|
| 196 |
+
2020-01-09 02:00:00,11.856494
|
| 197 |
+
2020-01-09 03:00:00,12.873333
|
| 198 |
+
2020-01-09 04:00:00,12.774559
|
| 199 |
+
2020-01-09 05:00:00,13.222704
|
| 200 |
+
2020-01-09 06:00:00,13.355967
|
| 201 |
+
2020-01-09 07:00:00,13.473326
|
| 202 |
+
2020-01-09 08:00:00,12.930806
|
| 203 |
+
2020-01-09 09:00:00,12.470578
|
| 204 |
+
2020-01-09 10:00:00,11.656756
|
| 205 |
+
2020-01-09 11:00:00,11.40529
|
| 206 |
+
2020-01-09 12:00:00,10.648692
|
| 207 |
+
2020-01-09 13:00:00,9.619551
|
| 208 |
+
2020-01-09 14:00:00,9.027083
|
| 209 |
+
2020-01-09 15:00:00,8.117006
|
| 210 |
+
2020-01-09 16:00:00,7.806163
|
| 211 |
+
2020-01-09 17:00:00,7.877463
|
| 212 |
+
2020-01-09 18:00:00,7.5271463
|
| 213 |
+
2020-01-09 19:00:00,8.063946
|
| 214 |
+
2020-01-09 20:00:00,7.7430873
|
| 215 |
+
2020-01-09 21:00:00,8.491592
|
| 216 |
+
2020-01-09 22:00:00,8.955315
|
| 217 |
+
2020-01-09 23:00:00,9.82799
|
| 218 |
+
2020-01-10 00:00:00,10.486022
|
| 219 |
+
2020-01-10 01:00:00,11.147181
|
| 220 |
+
2020-01-10 02:00:00,11.804263
|
| 221 |
+
2020-01-10 03:00:00,13.20647
|
| 222 |
+
2020-01-10 04:00:00,13.048044
|
| 223 |
+
2020-01-10 05:00:00,12.952726
|
| 224 |
+
2020-01-10 06:00:00,13.320765
|
| 225 |
+
2020-01-10 07:00:00,13.475443
|
| 226 |
+
2020-01-10 08:00:00,12.9310875
|
| 227 |
+
2020-01-10 09:00:00,12.8167715
|
| 228 |
+
2020-01-10 10:00:00,12.113599
|
| 229 |
+
2020-01-10 11:00:00,11.148289
|
| 230 |
+
2020-01-10 12:00:00,10.296473
|
| 231 |
+
2020-01-10 13:00:00,9.401568
|
| 232 |
+
2020-01-10 14:00:00,8.726533
|
| 233 |
+
2020-01-10 15:00:00,7.9376044
|
| 234 |
+
2020-01-10 16:00:00,7.9828897
|
| 235 |
+
2020-01-10 17:00:00,7.746506
|
| 236 |
+
2020-01-10 18:00:00,7.0605173
|
| 237 |
+
2020-01-10 19:00:00,7.162796
|
| 238 |
+
2020-01-10 20:00:00,7.3298817
|
| 239 |
+
2020-01-10 21:00:00,7.951925
|
| 240 |
+
2020-01-10 22:00:00,8.128733
|
| 241 |
+
2020-01-10 23:00:00,9.228722
|
| 242 |
+
2020-01-11 00:00:00,10.476325
|
| 243 |
+
2020-01-11 01:00:00,10.907269
|
| 244 |
+
2020-01-11 02:00:00,11.853587
|
| 245 |
+
2020-01-11 03:00:00,12.188666
|
| 246 |
+
2020-01-11 04:00:00,13.097588
|
| 247 |
+
2020-01-11 05:00:00,13.067031
|
| 248 |
+
2020-01-11 06:00:00,13.03486
|
| 249 |
+
2020-01-11 07:00:00,13.501218
|
| 250 |
+
2020-01-11 08:00:00,12.607703
|
| 251 |
+
2020-01-11 09:00:00,11.933058
|
| 252 |
+
2020-01-11 10:00:00,11.577747
|
| 253 |
+
2020-01-11 11:00:00,10.787386
|
| 254 |
+
2020-01-11 12:00:00,10.213264
|
| 255 |
+
2020-01-11 13:00:00,9.02052
|
| 256 |
+
2020-01-11 14:00:00,8.623578
|
| 257 |
+
2020-01-11 15:00:00,7.993247
|
| 258 |
+
2020-01-11 16:00:00,7.1938653
|
| 259 |
+
2020-01-11 17:00:00,7.0419154
|
| 260 |
+
2020-01-11 18:00:00,6.722589
|
| 261 |
+
2020-01-11 19:00:00,7.4419746
|
| 262 |
+
2020-01-11 20:00:00,7.1137185
|
| 263 |
+
2020-01-11 21:00:00,7.6229253
|
| 264 |
+
2020-01-11 22:00:00,8.104162
|
| 265 |
+
2020-01-11 23:00:00,8.954372
|
| 266 |
+
2020-01-12 00:00:00,9.781883
|
| 267 |
+
2020-01-12 01:00:00,10.696381
|
| 268 |
+
2020-01-12 02:00:00,11.127903
|
| 269 |
+
2020-01-12 03:00:00,11.81815
|
| 270 |
+
2020-01-12 04:00:00,12.033118
|
| 271 |
+
2020-01-12 05:00:00,12.435387
|
| 272 |
+
2020-01-12 06:00:00,13.239417
|
| 273 |
+
2020-01-12 07:00:00,12.779882
|
| 274 |
+
2020-01-12 08:00:00,12.101705
|
| 275 |
+
2020-01-12 09:00:00,11.500287
|
| 276 |
+
2020-01-12 10:00:00,10.938357
|
| 277 |
+
2020-01-12 11:00:00,10.393133
|
| 278 |
+
2020-01-12 12:00:00,9.61603
|
| 279 |
+
2020-01-12 13:00:00,8.672372
|
| 280 |
+
2020-01-12 14:00:00,7.8295655
|
| 281 |
+
2020-01-12 15:00:00,7.7397933
|
| 282 |
+
2020-01-12 16:00:00,7.059248
|
| 283 |
+
2020-01-12 17:00:00,6.5852513
|
| 284 |
+
2020-01-12 18:00:00,6.5053835
|
| 285 |
+
2020-01-12 19:00:00,6.5380354
|
| 286 |
+
2020-01-12 20:00:00,6.349278
|
| 287 |
+
2020-01-12 21:00:00,7.42987
|
| 288 |
+
2020-01-12 22:00:00,7.8081045
|
| 289 |
+
2020-01-12 23:00:00,8.540043
|
| 290 |
+
2020-01-13 00:00:00,9.384483
|
| 291 |
+
2020-01-13 01:00:00,10.431634
|
| 292 |
+
2020-01-13 02:00:00,10.771479
|
| 293 |
+
2020-01-13 03:00:00,11.337608
|
| 294 |
+
2020-01-13 04:00:00,12.227445
|
| 295 |
+
2020-01-13 05:00:00,12.549001
|
| 296 |
+
2020-01-13 06:00:00,12.308213
|
| 297 |
+
2020-01-13 07:00:00,12.510607
|
| 298 |
+
2020-01-13 08:00:00,12.041148
|
| 299 |
+
2020-01-13 09:00:00,11.684723
|
| 300 |
+
2020-01-13 10:00:00,10.753392
|
| 301 |
+
2020-01-13 11:00:00,10.451753
|
| 302 |
+
2020-01-13 12:00:00,9.753188
|
| 303 |
+
2020-01-13 13:00:00,8.867994
|
| 304 |
+
2020-01-13 14:00:00,8.133882
|
| 305 |
+
2020-01-13 15:00:00,6.652283
|
| 306 |
+
2020-01-13 16:00:00,6.9886127
|
| 307 |
+
2020-01-13 17:00:00,6.638852
|
| 308 |
+
2020-01-13 18:00:00,6.5201063
|
| 309 |
+
2020-01-13 19:00:00,6.534049
|
| 310 |
+
2020-01-13 20:00:00,6.9799843
|
| 311 |
+
2020-01-13 21:00:00,7.537741
|
| 312 |
+
2020-01-13 22:00:00,8.034123
|
| 313 |
+
2020-01-13 23:00:00,8.7285795
|
| 314 |
+
2020-01-14 00:00:00,9.855035
|
| 315 |
+
2020-01-14 01:00:00,10.176398
|
| 316 |
+
2020-01-14 02:00:00,11.339505
|
| 317 |
+
2020-01-14 03:00:00,11.803129
|
| 318 |
+
2020-01-14 04:00:00,12.097129
|
| 319 |
+
2020-01-14 05:00:00,12.513638
|
| 320 |
+
2020-01-14 06:00:00,12.504256
|
| 321 |
+
2020-01-14 07:00:00,12.73588
|
| 322 |
+
2020-01-14 08:00:00,12.385997
|
| 323 |
+
2020-01-14 09:00:00,11.743945
|
| 324 |
+
2020-01-14 10:00:00,11.029556
|
| 325 |
+
2020-01-14 11:00:00,10.603166
|
| 326 |
+
2020-01-14 12:00:00,9.974535
|
| 327 |
+
2020-01-14 13:00:00,9.00083
|
| 328 |
+
2020-01-14 14:00:00,8.401
|
| 329 |
+
2020-01-14 15:00:00,7.6383348
|
| 330 |
+
2020-01-14 16:00:00,7.26806
|
| 331 |
+
2020-01-14 17:00:00,6.9145565
|
| 332 |
+
2020-01-14 18:00:00,6.947548
|
| 333 |
+
2020-01-14 19:00:00,6.707312
|
| 334 |
+
2020-01-14 20:00:00,7.4562407
|
| 335 |
+
2020-01-14 21:00:00,7.776711
|
| 336 |
+
2020-01-14 22:00:00,8.534356
|
| 337 |
+
2020-01-14 23:00:00,9.1367855
|
| 338 |
+
2020-01-15 00:00:00,10.064073
|
| 339 |
+
2020-01-15 01:00:00,10.58064
|
| 340 |
+
2020-01-15 02:00:00,11.775196
|
| 341 |
+
2020-01-15 03:00:00,11.836594
|
| 342 |
+
2020-01-15 04:00:00,12.464749
|
| 343 |
+
2020-01-15 05:00:00,13.037867
|
| 344 |
+
2020-01-15 06:00:00,13.403829
|
| 345 |
+
2020-01-15 07:00:00,13.082812
|
| 346 |
+
2020-01-15 08:00:00,12.695872
|
| 347 |
+
2020-01-15 09:00:00,12.001442
|
| 348 |
+
2020-01-15 10:00:00,11.644415
|
| 349 |
+
2020-01-15 11:00:00,10.972427
|
| 350 |
+
2020-01-15 12:00:00,10.555056
|
| 351 |
+
2020-01-15 13:00:00,9.581602
|
| 352 |
+
2020-01-15 14:00:00,8.444181
|
| 353 |
+
2020-01-15 15:00:00,8.550052
|
| 354 |
+
2020-01-15 16:00:00,7.604582
|
| 355 |
+
2020-01-15 17:00:00,7.223241
|
| 356 |
+
2020-01-15 18:00:00,7.6067095
|
| 357 |
+
2020-01-15 19:00:00,7.418415
|
| 358 |
+
2020-01-15 20:00:00,7.6685295
|
| 359 |
+
2020-01-15 21:00:00,8.2759905
|
| 360 |
+
2020-01-15 22:00:00,9.035503
|
| 361 |
+
2020-01-15 23:00:00,9.801196
|
| 362 |
+
2020-01-16 00:00:00,10.115875
|
| 363 |
+
2020-01-16 01:00:00,11.578452
|
| 364 |
+
2020-01-16 02:00:00,12.102491
|
| 365 |
+
2020-01-16 03:00:00,12.4688425
|
| 366 |
+
2020-01-16 04:00:00,12.867357
|
| 367 |
+
2020-01-16 05:00:00,13.146032
|
| 368 |
+
2020-01-16 06:00:00,13.47516
|
| 369 |
+
2020-01-16 07:00:00,13.226456
|
| 370 |
+
2020-01-16 08:00:00,12.910539
|
| 371 |
+
2020-01-16 09:00:00,12.755513
|
| 372 |
+
2020-01-16 10:00:00,12.0507
|
| 373 |
+
2020-01-16 11:00:00,11.180506
|
| 374 |
+
2020-01-16 12:00:00,10.634314
|
| 375 |
+
2020-01-16 13:00:00,9.988302
|
| 376 |
+
2020-01-16 14:00:00,8.7755165
|
| 377 |
+
2020-01-16 15:00:00,8.254877
|
| 378 |
+
2020-01-16 16:00:00,8.08905
|
| 379 |
+
2020-01-16 17:00:00,7.745661
|
| 380 |
+
2020-01-16 18:00:00,7.5453396
|
| 381 |
+
2020-01-16 19:00:00,7.8343573
|
| 382 |
+
2020-01-16 20:00:00,7.6828833
|
| 383 |
+
2020-01-16 21:00:00,8.079708
|
| 384 |
+
2020-01-16 22:00:00,8.821114
|
| 385 |
+
2020-01-16 23:00:00,9.739335
|
| 386 |
+
2020-01-17 00:00:00,10.328247
|
| 387 |
+
2020-01-17 01:00:00,11.161977
|
| 388 |
+
2020-01-17 02:00:00,11.782786
|
| 389 |
+
2020-01-17 03:00:00,12.469176
|
| 390 |
+
2020-01-17 04:00:00,12.8625555
|
| 391 |
+
2020-01-17 05:00:00,13.429552
|
| 392 |
+
2020-01-17 06:00:00,13.609463
|
| 393 |
+
2020-01-17 07:00:00,13.243746
|
| 394 |
+
2020-01-17 08:00:00,12.989635
|
| 395 |
+
2020-01-17 09:00:00,12.428476
|
| 396 |
+
2020-01-17 10:00:00,12.01937
|
| 397 |
+
2020-01-17 11:00:00,11.196568
|
| 398 |
+
2020-01-17 12:00:00,10.709747
|
| 399 |
+
2020-01-17 13:00:00,9.383434
|
| 400 |
+
2020-01-17 14:00:00,8.939027
|
| 401 |
+
2020-01-17 15:00:00,8.321032
|
| 402 |
+
2020-01-17 16:00:00,7.6699224
|
| 403 |
+
2020-01-17 17:00:00,7.545073
|
| 404 |
+
2020-01-17 18:00:00,7.02404
|
| 405 |
+
2020-01-17 19:00:00,7.822893
|
| 406 |
+
2020-01-17 20:00:00,7.415175
|
| 407 |
+
2020-01-17 21:00:00,8.328657
|
| 408 |
+
2020-01-17 22:00:00,8.525776
|
| 409 |
+
2020-01-17 23:00:00,9.687354
|
| 410 |
+
2020-01-18 00:00:00,10.139987
|
| 411 |
+
2020-01-18 01:00:00,11.008088
|
| 412 |
+
2020-01-18 02:00:00,11.693339
|
| 413 |
+
2020-01-18 03:00:00,12.506607
|
| 414 |
+
2020-01-18 04:00:00,12.681145
|
| 415 |
+
2020-01-18 05:00:00,12.43375
|
| 416 |
+
2020-01-18 06:00:00,12.959249
|
| 417 |
+
2020-01-18 07:00:00,13.027475
|
| 418 |
+
2020-01-18 08:00:00,12.584504
|
| 419 |
+
2020-01-18 09:00:00,12.331266
|
| 420 |
+
2020-01-18 10:00:00,11.740431
|
| 421 |
+
2020-01-18 11:00:00,10.765598
|
| 422 |
+
2020-01-18 12:00:00,9.702071
|
| 423 |
+
2020-01-18 13:00:00,9.48168
|
| 424 |
+
2020-01-18 14:00:00,8.679246
|
| 425 |
+
2020-01-18 15:00:00,7.7628593
|
| 426 |
+
2020-01-18 16:00:00,7.74906
|
| 427 |
+
2020-01-18 17:00:00,6.9394917
|
| 428 |
+
2020-01-18 18:00:00,6.6613154
|
| 429 |
+
2020-01-18 19:00:00,6.9416385
|
| 430 |
+
2020-01-18 20:00:00,7.470113
|
| 431 |
+
2020-01-18 21:00:00,7.5260496
|
| 432 |
+
2020-01-18 22:00:00,8.707575
|
| 433 |
+
2020-01-18 23:00:00,8.844292
|
| 434 |
+
2020-01-19 00:00:00,9.973943
|
| 435 |
+
2020-01-19 01:00:00,10.651715
|
| 436 |
+
2020-01-19 02:00:00,11.219189
|
| 437 |
+
2020-01-19 03:00:00,12.071411
|
| 438 |
+
2020-01-19 04:00:00,12.016457
|
| 439 |
+
2020-01-19 05:00:00,12.872382
|
| 440 |
+
2020-01-19 06:00:00,12.675518
|
| 441 |
+
2020-01-19 07:00:00,12.463147
|
| 442 |
+
2020-01-19 08:00:00,12.4078245
|
| 443 |
+
2020-01-19 09:00:00,11.979747
|
| 444 |
+
2020-01-19 10:00:00,11.287434
|
| 445 |
+
2020-01-19 11:00:00,10.797372
|
| 446 |
+
2020-01-19 12:00:00,9.82523
|
| 447 |
+
2020-01-19 13:00:00,9.077946
|
| 448 |
+
2020-01-19 14:00:00,7.978908
|
| 449 |
+
2020-01-19 15:00:00,7.476622
|
| 450 |
+
2020-01-19 16:00:00,7.0815077
|
| 451 |
+
2020-01-19 17:00:00,6.656616
|
| 452 |
+
2020-01-19 18:00:00,6.609843
|
| 453 |
+
2020-01-19 19:00:00,6.729162
|
| 454 |
+
2020-01-19 20:00:00,7.1053357
|
| 455 |
+
2020-01-19 21:00:00,7.3864164
|
| 456 |
+
2020-01-19 22:00:00,7.952254
|
| 457 |
+
2020-01-19 23:00:00,8.574917
|
| 458 |
+
2020-01-20 00:00:00,9.334189
|
| 459 |
+
2020-01-20 01:00:00,10.519664
|
| 460 |
+
2020-01-20 02:00:00,10.98868
|
| 461 |
+
2020-01-20 03:00:00,11.781853
|
| 462 |
+
2020-01-20 04:00:00,11.840001
|
| 463 |
+
2020-01-20 05:00:00,12.010493
|
| 464 |
+
2020-01-20 06:00:00,12.290182
|
| 465 |
+
2020-01-20 07:00:00,12.627453
|
| 466 |
+
2020-01-20 08:00:00,12.313089
|
| 467 |
+
2020-01-20 09:00:00,11.690872
|
| 468 |
+
2020-01-20 10:00:00,10.845151
|
| 469 |
+
2020-01-20 11:00:00,10.259013
|
| 470 |
+
2020-01-20 12:00:00,9.452984
|
| 471 |
+
2020-01-20 13:00:00,8.671585
|
| 472 |
+
2020-01-20 14:00:00,7.5209994
|
| 473 |
+
2020-01-20 15:00:00,7.2351527
|
| 474 |
+
2020-01-20 16:00:00,6.898465
|
| 475 |
+
2020-01-20 17:00:00,6.947227
|
| 476 |
+
2020-01-20 18:00:00,6.581664
|
| 477 |
+
2020-01-20 19:00:00,6.9403424
|
| 478 |
+
2020-01-20 20:00:00,6.8901696
|
| 479 |
+
2020-01-20 21:00:00,7.40478
|
| 480 |
+
2020-01-20 22:00:00,7.3069963
|
| 481 |
+
2020-01-20 23:00:00,8.91391
|
| 482 |
+
2020-01-21 00:00:00,9.718503
|
| 483 |
+
2020-01-21 01:00:00,10.750223
|
| 484 |
+
2020-01-21 02:00:00,11.036121
|
| 485 |
+
2020-01-21 03:00:00,11.786598
|
| 486 |
+
2020-01-21 04:00:00,12.116884
|
| 487 |
+
2020-01-21 05:00:00,12.336382
|
| 488 |
+
2020-01-21 06:00:00,12.545633
|
| 489 |
+
2020-01-21 07:00:00,12.531868
|
| 490 |
+
2020-01-21 08:00:00,12.587503
|
| 491 |
+
2020-01-21 09:00:00,11.855746
|
| 492 |
+
2020-01-21 10:00:00,11.091891
|
| 493 |
+
2020-01-21 11:00:00,10.571198
|
| 494 |
+
2020-01-21 12:00:00,9.826572
|
| 495 |
+
2020-01-21 13:00:00,8.8883505
|
| 496 |
+
2020-01-21 14:00:00,8.545977
|
| 497 |
+
2020-01-21 15:00:00,7.3358703
|
| 498 |
+
2020-01-21 16:00:00,7.2118373
|
| 499 |
+
2020-01-21 17:00:00,7.1058264
|
| 500 |
+
2020-01-21 18:00:00,6.621053
|
| 501 |
+
2020-01-21 19:00:00,7.0815177
|
| 502 |
+
2020-01-21 20:00:00,7.5859814
|
| 503 |
+
2020-01-21 21:00:00,7.9134316
|
| 504 |
+
2020-01-21 22:00:00,8.124603
|
| 505 |
+
2020-01-21 23:00:00,9.059208
|
| 506 |
+
2020-01-22 00:00:00,10.246461
|
| 507 |
+
2020-01-22 01:00:00,10.85482
|
| 508 |
+
2020-01-22 02:00:00,11.535394
|
| 509 |
+
2020-01-22 03:00:00,12.265543
|
| 510 |
+
2020-01-22 04:00:00,12.816807
|
| 511 |
+
2020-01-22 05:00:00,12.84904
|
| 512 |
+
2020-01-22 06:00:00,13.053181
|
| 513 |
+
2020-01-22 07:00:00,13.05577
|
| 514 |
+
2020-01-22 08:00:00,12.6366625
|
| 515 |
+
2020-01-22 09:00:00,12.259769
|
| 516 |
+
2020-01-22 10:00:00,11.942245
|
| 517 |
+
2020-01-22 11:00:00,10.782839
|
| 518 |
+
2020-01-22 12:00:00,10.602274
|
| 519 |
+
2020-01-22 13:00:00,9.833046
|
| 520 |
+
2020-01-22 14:00:00,8.407312
|
| 521 |
+
2020-01-22 15:00:00,8.116491
|
| 522 |
+
2020-01-22 16:00:00,7.752122
|
| 523 |
+
2020-01-22 17:00:00,7.2469583
|
| 524 |
+
2020-01-22 18:00:00,7.163529
|
| 525 |
+
2020-01-22 19:00:00,7.380883
|
| 526 |
+
2020-01-22 20:00:00,7.889846
|
| 527 |
+
2020-01-22 21:00:00,8.129967
|
| 528 |
+
2020-01-22 22:00:00,9.231865
|
| 529 |
+
2020-01-22 23:00:00,9.660523
|
| 530 |
+
2020-01-23 00:00:00,10.3704
|
| 531 |
+
2020-01-23 01:00:00,11.468608
|
| 532 |
+
2020-01-23 02:00:00,12.038376
|
| 533 |
+
2020-01-23 03:00:00,12.61854
|
| 534 |
+
2020-01-23 04:00:00,12.964861
|
| 535 |
+
2020-01-23 05:00:00,13.702632
|
| 536 |
+
2020-01-23 06:00:00,13.612848
|
| 537 |
+
2020-01-23 07:00:00,13.315376
|
| 538 |
+
2020-01-23 08:00:00,12.748074
|
| 539 |
+
2020-01-23 09:00:00,12.667852
|
| 540 |
+
2020-01-23 10:00:00,11.749039
|
| 541 |
+
2020-01-23 11:00:00,10.916231
|
| 542 |
+
2020-01-23 12:00:00,10.431634
|
| 543 |
+
2020-01-23 13:00:00,9.77112
|
| 544 |
+
2020-01-23 14:00:00,9.250912
|
| 545 |
+
2020-01-23 15:00:00,8.432026
|
| 546 |
+
2020-01-23 16:00:00,8.061729
|
| 547 |
+
2020-01-23 17:00:00,7.356696
|
| 548 |
+
2020-01-23 18:00:00,7.4954934
|
| 549 |
+
2020-01-23 19:00:00,7.626715
|
| 550 |
+
2020-01-23 20:00:00,8.072975
|
| 551 |
+
2020-01-23 21:00:00,8.398747
|
| 552 |
+
2020-01-23 22:00:00,9.155231
|
| 553 |
+
2020-01-23 23:00:00,9.613986
|
| 554 |
+
2020-01-24 00:00:00,10.559055
|
| 555 |
+
2020-01-24 01:00:00,11.342393
|
| 556 |
+
2020-01-24 02:00:00,11.727831
|
| 557 |
+
2020-01-24 03:00:00,12.628369
|
| 558 |
+
2020-01-24 04:00:00,12.99944
|
| 559 |
+
2020-01-24 05:00:00,12.975241
|
| 560 |
+
2020-01-24 06:00:00,13.642166
|
| 561 |
+
2020-01-24 07:00:00,13.267464
|
| 562 |
+
2020-01-24 08:00:00,12.860611
|
| 563 |
+
2020-01-24 09:00:00,12.469219
|
| 564 |
+
2020-01-24 10:00:00,11.94076
|
| 565 |
+
2020-01-24 11:00:00,11.480338
|
| 566 |
+
2020-01-24 12:00:00,10.357729
|
| 567 |
+
2020-01-24 13:00:00,9.69701
|
| 568 |
+
2020-01-24 14:00:00,9.141242
|
| 569 |
+
2020-01-24 15:00:00,8.3389435
|
| 570 |
+
2020-01-24 16:00:00,7.955723
|
| 571 |
+
2020-01-24 17:00:00,7.333064
|
| 572 |
+
2020-01-24 18:00:00,7.465955
|
| 573 |
+
2020-01-24 19:00:00,7.3875375
|
| 574 |
+
2020-01-24 20:00:00,7.898466
|
| 575 |
+
2020-01-24 21:00:00,7.9439836
|
| 576 |
+
2020-01-24 22:00:00,8.594089
|
| 577 |
+
2020-01-24 23:00:00,9.710923
|
| 578 |
+
2020-01-25 00:00:00,10.177725
|
| 579 |
+
2020-01-25 01:00:00,10.904611
|
| 580 |
+
2020-01-25 02:00:00,11.698185
|
| 581 |
+
2020-01-25 03:00:00,12.148561
|
| 582 |
+
2020-01-25 04:00:00,13.011842
|
| 583 |
+
2020-01-25 05:00:00,12.777354
|
| 584 |
+
2020-01-25 06:00:00,13.0811405
|
| 585 |
+
2020-01-25 07:00:00,13.060123
|
| 586 |
+
2020-01-25 08:00:00,12.651708
|
| 587 |
+
2020-01-25 09:00:00,12.016845
|
| 588 |
+
2020-01-25 10:00:00,11.3636875
|
| 589 |
+
2020-01-25 11:00:00,10.880232
|
| 590 |
+
2020-01-25 12:00:00,9.793904
|
| 591 |
+
2020-01-25 13:00:00,9.334096
|
| 592 |
+
2020-01-25 14:00:00,8.157805
|
| 593 |
+
2020-01-25 15:00:00,7.711701
|
| 594 |
+
2020-01-25 16:00:00,7.334664
|
| 595 |
+
2020-01-25 17:00:00,6.7588363
|
| 596 |
+
2020-01-25 18:00:00,7.0191417
|
| 597 |
+
2020-01-25 19:00:00,6.9691052
|
| 598 |
+
2020-01-25 20:00:00,7.0473313
|
| 599 |
+
2020-01-25 21:00:00,7.4097624
|
| 600 |
+
2020-01-25 22:00:00,8.004211
|
| 601 |
+
2020-01-25 23:00:00,9.033804
|
| 602 |
+
2020-01-26 00:00:00,9.551692
|
| 603 |
+
2020-01-26 01:00:00,10.269847
|
| 604 |
+
2020-01-26 02:00:00,11.203754
|
| 605 |
+
2020-01-26 03:00:00,12.310804
|
| 606 |
+
2020-01-26 04:00:00,12.371924
|
| 607 |
+
2020-01-26 05:00:00,12.753451
|
| 608 |
+
2020-01-26 06:00:00,12.730799
|
| 609 |
+
2020-01-26 07:00:00,12.72823
|
| 610 |
+
2020-01-26 08:00:00,11.989372
|
| 611 |
+
2020-01-26 09:00:00,11.681664
|
| 612 |
+
2020-01-26 10:00:00,11.185744
|
| 613 |
+
2020-01-26 11:00:00,10.3938875
|
| 614 |
+
2020-01-26 12:00:00,9.570685
|
| 615 |
+
2020-01-26 13:00:00,8.688068
|
| 616 |
+
2020-01-26 14:00:00,8.035204
|
| 617 |
+
2020-01-26 15:00:00,7.300478
|
| 618 |
+
2020-01-26 16:00:00,6.4845443
|
| 619 |
+
2020-01-26 17:00:00,6.421263
|
| 620 |
+
2020-01-26 18:00:00,6.644646
|
| 621 |
+
2020-01-26 19:00:00,6.955357
|
| 622 |
+
2020-01-26 20:00:00,7.299203
|
| 623 |
+
2020-01-26 21:00:00,7.4260883
|
| 624 |
+
2020-01-26 22:00:00,8.200891
|
| 625 |
+
2020-01-26 23:00:00,8.922175
|
| 626 |
+
2020-01-27 00:00:00,9.374084
|
| 627 |
+
2020-01-27 01:00:00,9.945586
|
| 628 |
+
2020-01-27 02:00:00,11.011762
|
| 629 |
+
2020-01-27 03:00:00,11.272024
|
| 630 |
+
2020-01-27 04:00:00,12.035488
|
| 631 |
+
2020-01-27 05:00:00,12.519751
|
| 632 |
+
2020-01-27 06:00:00,12.215986
|
| 633 |
+
2020-01-27 07:00:00,12.404586
|
| 634 |
+
2020-01-27 08:00:00,12.347478
|
| 635 |
+
2020-01-27 09:00:00,11.696767
|
| 636 |
+
2020-01-27 10:00:00,11.110049
|
| 637 |
+
2020-01-27 11:00:00,10.466522
|
| 638 |
+
2020-01-27 12:00:00,9.859129
|
| 639 |
+
2020-01-27 13:00:00,8.7708645
|
| 640 |
+
2020-01-27 14:00:00,8.268114
|
| 641 |
+
2020-01-27 15:00:00,7.393904
|
| 642 |
+
2020-01-27 16:00:00,6.8272533
|
| 643 |
+
2020-01-27 17:00:00,6.7069054
|
| 644 |
+
2020-01-27 18:00:00,6.428353
|
| 645 |
+
2020-01-27 19:00:00,6.545539
|
| 646 |
+
2020-01-27 20:00:00,6.847363
|
| 647 |
+
2020-01-27 21:00:00,6.9962325
|
| 648 |
+
2020-01-27 22:00:00,8.107981
|
| 649 |
+
2020-01-27 23:00:00,8.568477
|
| 650 |
+
2020-01-28 00:00:00,9.58763
|
| 651 |
+
2020-01-28 01:00:00,10.687465
|
| 652 |
+
2020-01-28 02:00:00,11.029274
|
| 653 |
+
2020-01-28 03:00:00,11.659365
|
| 654 |
+
2020-01-28 04:00:00,12.530771
|
| 655 |
+
2020-01-28 05:00:00,12.680895
|
| 656 |
+
2020-01-28 06:00:00,12.88356
|
| 657 |
+
2020-01-28 07:00:00,12.529809
|
| 658 |
+
2020-01-28 08:00:00,12.466033
|
| 659 |
+
2020-01-28 09:00:00,11.718071
|
| 660 |
+
2020-01-28 10:00:00,11.1147995
|
| 661 |
+
2020-01-28 11:00:00,10.662071
|
| 662 |
+
2020-01-28 12:00:00,9.6633835
|
| 663 |
+
2020-01-28 13:00:00,9.177021
|
| 664 |
+
2020-01-28 14:00:00,8.795667
|
| 665 |
+
2020-01-28 15:00:00,7.3763094
|
| 666 |
+
2020-01-28 16:00:00,7.1041055
|
| 667 |
+
2020-01-28 17:00:00,7.196842
|
| 668 |
+
2020-01-28 18:00:00,6.859724
|
| 669 |
+
2020-01-28 19:00:00,7.2414846
|
| 670 |
+
2020-01-28 20:00:00,7.1253896
|
| 671 |
+
2020-01-28 21:00:00,7.888966
|
| 672 |
+
2020-01-28 22:00:00,8.432524
|
| 673 |
+
2020-01-28 23:00:00,9.232922
|
| 674 |
+
2020-01-29 00:00:00,10.066223
|
| 675 |
+
2020-01-29 01:00:00,10.551151
|
| 676 |
+
2020-01-29 02:00:00,11.322537
|
| 677 |
+
2020-01-29 03:00:00,12.457151
|
| 678 |
+
2020-01-29 04:00:00,12.73124
|
| 679 |
+
2020-01-29 05:00:00,13.012003
|
| 680 |
+
2020-01-29 06:00:00,13.102446
|
| 681 |
+
2020-01-29 07:00:00,13.0984955
|
| 682 |
+
2020-01-29 08:00:00,12.514266
|
| 683 |
+
2020-01-29 09:00:00,12.086655
|
| 684 |
+
2020-01-29 10:00:00,11.943572
|
| 685 |
+
2020-01-29 11:00:00,11.006659
|
| 686 |
+
2020-01-29 12:00:00,10.387002
|
| 687 |
+
2020-01-29 13:00:00,9.336044
|
| 688 |
+
2020-01-29 14:00:00,9.025345
|
| 689 |
+
2020-01-29 15:00:00,8.213752
|
| 690 |
+
2020-01-29 16:00:00,7.779833
|
| 691 |
+
2020-01-29 17:00:00,7.5088787
|
| 692 |
+
2020-01-29 18:00:00,7.1523433
|
| 693 |
+
2020-01-29 19:00:00,7.055222
|
| 694 |
+
2020-01-29 20:00:00,7.5270495
|
| 695 |
+
2020-01-29 21:00:00,8.558337
|
| 696 |
+
2020-01-29 22:00:00,9.126662
|
| 697 |
+
2020-01-29 23:00:00,9.533144
|
| 698 |
+
2020-01-30 00:00:00,10.330581
|
| 699 |
+
2020-01-30 01:00:00,11.386077
|
| 700 |
+
2020-01-30 02:00:00,11.879437
|
| 701 |
+
2020-01-30 03:00:00,12.28483
|
| 702 |
+
2020-01-30 04:00:00,13.284198
|
| 703 |
+
2020-01-30 05:00:00,13.435271
|
| 704 |
+
2020-01-30 06:00:00,12.947216
|
| 705 |
+
2020-01-30 07:00:00,13.293421
|
| 706 |
+
2020-01-30 08:00:00,13.092247
|
| 707 |
+
2020-01-30 09:00:00,12.689511
|
| 708 |
+
2020-01-30 10:00:00,12.008217
|
| 709 |
+
2020-01-30 11:00:00,11.132272
|
| 710 |
+
2020-01-30 12:00:00,10.464313
|
| 711 |
+
2020-01-30 13:00:00,9.773787
|
| 712 |
+
2020-01-30 14:00:00,8.940815
|
| 713 |
+
2020-01-30 15:00:00,8.301156
|
| 714 |
+
2020-01-30 16:00:00,8.150943
|
| 715 |
+
2020-01-30 17:00:00,7.4125514
|
| 716 |
+
2020-01-30 18:00:00,7.430107
|
| 717 |
+
2020-01-30 19:00:00,7.195619
|
| 718 |
+
2020-01-30 20:00:00,8.008719
|
| 719 |
+
2020-01-30 21:00:00,8.541188
|
| 720 |
+
2020-01-30 22:00:00,9.104113
|
| 721 |
+
2020-01-30 23:00:00,9.89836
|
| 722 |
+
2020-01-31 00:00:00,10.575605
|
| 723 |
+
2020-01-31 01:00:00,11.327923
|
| 724 |
+
2020-01-31 02:00:00,12.072576
|
| 725 |
+
2020-01-31 03:00:00,12.539801
|
| 726 |
+
2020-01-31 04:00:00,13.301179
|
| 727 |
+
2020-01-31 05:00:00,13.286345
|
| 728 |
+
2020-01-31 06:00:00,13.158014
|
| 729 |
+
2020-01-31 07:00:00,13.509792
|
| 730 |
+
2020-01-31 08:00:00,13.40123
|
| 731 |
+
2020-01-31 09:00:00,12.352653
|
| 732 |
+
2020-01-31 10:00:00,11.892793
|
| 733 |
+
2020-01-31 11:00:00,11.041668
|
| 734 |
+
2020-01-31 12:00:00,10.314799
|
| 735 |
+
2020-01-31 13:00:00,9.611748
|
| 736 |
+
2020-01-31 14:00:00,8.618164
|
| 737 |
+
2020-01-31 15:00:00,8.1767025
|
| 738 |
+
2020-01-31 16:00:00,7.448817
|
| 739 |
+
2020-01-31 17:00:00,7.3147206
|
| 740 |
+
2020-01-31 18:00:00,7.074531
|
| 741 |
+
2020-01-31 19:00:00,7.1873207
|
| 742 |
+
2020-01-31 20:00:00,7.3396015
|
| 743 |
+
2020-01-31 21:00:00,8.388567
|
| 744 |
+
2020-01-31 22:00:00,8.851811
|
| 745 |
+
2020-01-31 23:00:00,9.073685
|
| 746 |
+
2020-02-01 00:00:00,10.097576
|
| 747 |
+
2020-02-01 01:00:00,10.842316
|
| 748 |
+
2020-02-01 02:00:00,11.544481
|
| 749 |
+
2020-02-01 03:00:00,11.997083
|
| 750 |
+
2020-02-01 04:00:00,12.896331
|
| 751 |
+
2020-02-01 05:00:00,12.948014
|
| 752 |
+
2020-02-01 06:00:00,13.20489
|
| 753 |
+
2020-02-01 07:00:00,13.096085
|
| 754 |
+
2020-02-01 08:00:00,12.947686
|
| 755 |
+
2020-02-01 09:00:00,11.814328
|
| 756 |
+
2020-02-01 10:00:00,11.885085
|
| 757 |
+
2020-02-01 11:00:00,11.048916
|
| 758 |
+
2020-02-01 12:00:00,10.114614
|
| 759 |
+
2020-02-01 13:00:00,9.681565
|
| 760 |
+
2020-02-01 14:00:00,8.503631
|
| 761 |
+
2020-02-01 15:00:00,7.9869933
|
| 762 |
+
2020-02-01 16:00:00,7.17972
|
| 763 |
+
2020-02-01 17:00:00,7.2361383
|
| 764 |
+
2020-02-01 18:00:00,6.9223046
|
| 765 |
+
2020-02-01 19:00:00,6.882573
|
| 766 |
+
2020-02-01 20:00:00,7.677934
|
| 767 |
+
2020-02-01 21:00:00,7.6525803
|
| 768 |
+
2020-02-01 22:00:00,8.319101
|
| 769 |
+
2020-02-01 23:00:00,8.98414
|
| 770 |
+
2020-02-02 00:00:00,9.631924
|
| 771 |
+
2020-02-02 01:00:00,10.649077
|
| 772 |
+
2020-02-02 02:00:00,11.397682
|
| 773 |
+
2020-02-02 03:00:00,11.926179
|
| 774 |
+
2020-02-02 04:00:00,11.8443
|
| 775 |
+
2020-02-02 05:00:00,12.802466
|
| 776 |
+
2020-02-02 06:00:00,12.618207
|
| 777 |
+
2020-02-02 07:00:00,12.328369
|
| 778 |
+
2020-02-02 08:00:00,12.378646
|
| 779 |
+
2020-02-02 09:00:00,11.880337
|
| 780 |
+
2020-02-02 10:00:00,10.924859
|
| 781 |
+
2020-02-02 11:00:00,10.891958
|
| 782 |
+
2020-02-02 12:00:00,9.367159
|
| 783 |
+
2020-02-02 13:00:00,8.474644
|
| 784 |
+
2020-02-02 14:00:00,7.856046
|
| 785 |
+
2020-02-02 15:00:00,7.7394776
|
| 786 |
+
2020-02-02 16:00:00,6.9337397
|
| 787 |
+
2020-02-02 17:00:00,6.5857534
|
| 788 |
+
2020-02-02 18:00:00,6.537084
|
| 789 |
+
2020-02-02 19:00:00,6.5246186
|
| 790 |
+
2020-02-02 20:00:00,6.7984343
|
| 791 |
+
2020-02-02 21:00:00,7.278495
|
| 792 |
+
2020-02-02 22:00:00,8.163805
|
| 793 |
+
2020-02-02 23:00:00,8.944671
|
| 794 |
+
2020-02-03 00:00:00,9.3015175
|
| 795 |
+
2020-02-03 01:00:00,10.333019
|
| 796 |
+
2020-02-03 02:00:00,11.163226
|
| 797 |
+
2020-02-03 03:00:00,11.4081135
|
| 798 |
+
2020-02-03 04:00:00,11.998961
|
| 799 |
+
2020-02-03 05:00:00,12.190515
|
| 800 |
+
2020-02-03 06:00:00,12.241795
|
| 801 |
+
2020-02-03 07:00:00,12.4185
|
| 802 |
+
2020-02-03 08:00:00,11.952248
|
| 803 |
+
2020-02-03 09:00:00,11.75069
|
| 804 |
+
2020-02-03 10:00:00,10.999701
|
| 805 |
+
2020-02-03 11:00:00,10.3677
|
| 806 |
+
2020-02-03 12:00:00,9.45408
|
| 807 |
+
2020-02-03 13:00:00,8.613581
|
| 808 |
+
2020-02-03 14:00:00,8.004079
|
| 809 |
+
2020-02-03 15:00:00,7.405739
|
| 810 |
+
2020-02-03 16:00:00,6.7953157
|
| 811 |
+
2020-02-03 17:00:00,6.7292604
|
| 812 |
+
2020-02-03 18:00:00,6.6988344
|
| 813 |
+
2020-02-03 19:00:00,6.6917815
|
| 814 |
+
2020-02-03 20:00:00,7.3116093
|
| 815 |
+
2020-02-03 21:00:00,7.32885
|
| 816 |
+
2020-02-03 22:00:00,8.191
|
| 817 |
+
2020-02-03 23:00:00,8.739497
|
| 818 |
+
2020-02-04 00:00:00,9.656011
|
| 819 |
+
2020-02-04 01:00:00,10.231482
|
| 820 |
+
2020-02-04 02:00:00,11.356499
|
| 821 |
+
2020-02-04 03:00:00,11.802971
|
| 822 |
+
2020-02-04 04:00:00,12.496439
|
| 823 |
+
2020-02-04 05:00:00,12.318415
|
| 824 |
+
2020-02-04 06:00:00,12.589552
|
| 825 |
+
2020-02-04 07:00:00,12.422355
|
| 826 |
+
2020-02-04 08:00:00,12.210028
|
| 827 |
+
2020-02-04 09:00:00,11.716783
|
| 828 |
+
2020-02-04 10:00:00,11.225892
|
| 829 |
+
2020-02-04 11:00:00,10.539373
|
| 830 |
+
2020-02-04 12:00:00,9.774861
|
| 831 |
+
2020-02-04 13:00:00,8.912492
|
| 832 |
+
2020-02-04 14:00:00,8.354815
|
| 833 |
+
2020-02-04 15:00:00,7.8886633
|
| 834 |
+
2020-02-04 16:00:00,7.074091
|
| 835 |
+
2020-02-04 17:00:00,6.9729905
|
| 836 |
+
2020-02-04 18:00:00,6.873925
|
| 837 |
+
2020-02-04 19:00:00,7.1029377
|
| 838 |
+
2020-02-04 20:00:00,7.314631
|
| 839 |
+
2020-02-04 21:00:00,7.8195643
|
| 840 |
+
2020-02-04 22:00:00,8.264416
|
| 841 |
+
2020-02-04 23:00:00,9.209188
|
| 842 |
+
2020-02-05 00:00:00,9.815623
|
| 843 |
+
2020-02-05 01:00:00,10.8982935
|
| 844 |
+
2020-02-05 02:00:00,11.516722
|
| 845 |
+
2020-02-05 03:00:00,12.185274
|
| 846 |
+
2020-02-05 04:00:00,12.494359
|
| 847 |
+
2020-02-05 05:00:00,13.1514845
|
| 848 |
+
2020-02-05 06:00:00,13.249723
|
| 849 |
+
2020-02-05 07:00:00,13.1994915
|
| 850 |
+
2020-02-05 08:00:00,13.190931
|
| 851 |
+
2020-02-05 09:00:00,12.275956
|
| 852 |
+
2020-02-05 10:00:00,11.923351
|
| 853 |
+
2020-02-05 11:00:00,10.951637
|
| 854 |
+
2020-02-05 12:00:00,10.295759
|
| 855 |
+
2020-02-05 13:00:00,9.530245
|
| 856 |
+
2020-02-05 14:00:00,8.802918
|
| 857 |
+
2020-02-05 15:00:00,8.276347
|
| 858 |
+
2020-02-05 16:00:00,7.6227965
|
| 859 |
+
2020-02-05 17:00:00,7.407319
|
| 860 |
+
2020-02-05 18:00:00,7.418807
|
| 861 |
+
2020-02-05 19:00:00,7.777937
|
| 862 |
+
2020-02-05 20:00:00,7.573539
|
| 863 |
+
2020-02-05 21:00:00,7.8710065
|
| 864 |
+
2020-02-05 22:00:00,8.769709
|
| 865 |
+
2020-02-05 23:00:00,9.620515
|
| 866 |
+
2020-02-06 00:00:00,10.425911
|
| 867 |
+
2020-02-06 01:00:00,11.1972
|
| 868 |
+
2020-02-06 02:00:00,12.149677
|
| 869 |
+
2020-02-06 03:00:00,12.739892
|
| 870 |
+
2020-02-06 04:00:00,13.021235
|
| 871 |
+
2020-02-06 05:00:00,13.245926
|
| 872 |
+
2020-02-06 06:00:00,13.381283
|
| 873 |
+
2020-02-06 07:00:00,13.284798
|
| 874 |
+
2020-02-06 08:00:00,13.0183935
|
| 875 |
+
2020-02-06 09:00:00,12.278732
|
| 876 |
+
2020-02-06 10:00:00,11.885578
|
| 877 |
+
2020-02-06 11:00:00,11.174444
|
| 878 |
+
2020-02-06 12:00:00,10.449688
|
| 879 |
+
2020-02-06 13:00:00,9.663966
|
| 880 |
+
2020-02-06 14:00:00,9.128817
|
| 881 |
+
2020-02-06 15:00:00,8.269147
|
| 882 |
+
2020-02-06 16:00:00,7.81281
|
| 883 |
+
2020-02-06 17:00:00,7.7102604
|
| 884 |
+
2020-02-06 18:00:00,7.452937
|
| 885 |
+
2020-02-06 19:00:00,7.645164
|
| 886 |
+
2020-02-06 20:00:00,8.034392
|
| 887 |
+
2020-02-06 21:00:00,8.459929
|
| 888 |
+
2020-02-06 22:00:00,9.044448
|
| 889 |
+
2020-02-06 23:00:00,9.675538
|
| 890 |
+
2020-02-07 00:00:00,10.623843
|
| 891 |
+
2020-02-07 01:00:00,11.29453
|
| 892 |
+
2020-02-07 02:00:00,11.876816
|
| 893 |
+
2020-02-07 03:00:00,12.5631
|
| 894 |
+
2020-02-07 04:00:00,12.819887
|
| 895 |
+
2020-02-07 05:00:00,13.163748
|
| 896 |
+
2020-02-07 06:00:00,13.073903
|
| 897 |
+
2020-02-07 07:00:00,13.203849
|
| 898 |
+
2020-02-07 08:00:00,13.298198
|
| 899 |
+
2020-02-07 09:00:00,12.433385
|
| 900 |
+
2020-02-07 10:00:00,12.070627
|
| 901 |
+
2020-02-07 11:00:00,11.178066
|
| 902 |
+
2020-02-07 12:00:00,10.250806
|
| 903 |
+
2020-02-07 13:00:00,9.870185
|
| 904 |
+
2020-02-07 14:00:00,8.982967
|
| 905 |
+
2020-02-07 15:00:00,7.881873
|
| 906 |
+
2020-02-07 16:00:00,7.9502907
|
| 907 |
+
2020-02-07 17:00:00,7.213408
|
| 908 |
+
2020-02-07 18:00:00,7.276162
|
| 909 |
+
2020-02-07 19:00:00,7.5327554
|
| 910 |
+
2020-02-07 20:00:00,7.6236134
|
| 911 |
+
2020-02-07 21:00:00,8.3684845
|
| 912 |
+
2020-02-07 22:00:00,8.902253
|
| 913 |
+
2020-02-07 23:00:00,9.141802
|
| 914 |
+
2020-02-08 00:00:00,10.122455
|
| 915 |
+
2020-02-08 01:00:00,11.032818
|
| 916 |
+
2020-02-08 02:00:00,11.567671
|
| 917 |
+
2020-02-08 03:00:00,12.243204
|
| 918 |
+
2020-02-08 04:00:00,12.905411
|
| 919 |
+
2020-02-08 05:00:00,13.090543
|
| 920 |
+
2020-02-08 06:00:00,12.927136
|
| 921 |
+
2020-02-08 07:00:00,13.025655
|
| 922 |
+
2020-02-08 08:00:00,12.550143
|
| 923 |
+
2020-02-08 09:00:00,11.934264
|
| 924 |
+
2020-02-08 10:00:00,11.311354
|
| 925 |
+
2020-02-08 11:00:00,10.852544
|
| 926 |
+
2020-02-08 12:00:00,9.994377
|
| 927 |
+
2020-02-08 13:00:00,9.205934
|
| 928 |
+
2020-02-08 14:00:00,8.233393
|
| 929 |
+
2020-02-08 15:00:00,7.7859116
|
| 930 |
+
2020-02-08 16:00:00,7.1199694
|
| 931 |
+
2020-02-08 17:00:00,6.823988
|
| 932 |
+
2020-02-08 18:00:00,6.8557143
|
| 933 |
+
2020-02-08 19:00:00,6.696135
|
| 934 |
+
2020-02-08 20:00:00,7.3883996
|
| 935 |
+
2020-02-08 21:00:00,8.2109165
|
| 936 |
+
2020-02-08 22:00:00,8.409094
|
| 937 |
+
2020-02-08 23:00:00,8.815016
|
| 938 |
+
2020-02-09 00:00:00,9.728821
|
| 939 |
+
2020-02-09 01:00:00,10.231735
|
| 940 |
+
2020-02-09 02:00:00,11.174477
|
| 941 |
+
2020-02-09 03:00:00,11.956767
|
| 942 |
+
2020-02-09 04:00:00,12.434293
|
| 943 |
+
2020-02-09 05:00:00,12.394519
|
| 944 |
+
2020-02-09 06:00:00,12.748084
|
| 945 |
+
2020-02-09 07:00:00,12.802916
|
| 946 |
+
2020-02-09 08:00:00,12.609008
|
| 947 |
+
2020-02-09 09:00:00,11.627608
|
| 948 |
+
2020-02-09 10:00:00,10.960634
|
| 949 |
+
2020-02-09 11:00:00,10.408355
|
| 950 |
+
2020-02-09 12:00:00,9.023466
|
| 951 |
+
2020-02-09 13:00:00,8.715037
|
| 952 |
+
2020-02-09 14:00:00,8.032685
|
| 953 |
+
2020-02-09 15:00:00,7.363449
|
| 954 |
+
2020-02-09 16:00:00,6.6521363
|
| 955 |
+
2020-02-09 17:00:00,6.6110544
|
| 956 |
+
2020-02-09 18:00:00,6.3941936
|
| 957 |
+
2020-02-09 19:00:00,6.795438
|
| 958 |
+
2020-02-09 20:00:00,6.752792
|
| 959 |
+
2020-02-09 21:00:00,7.350269
|
| 960 |
+
2020-02-09 22:00:00,7.975624
|
| 961 |
+
2020-02-09 23:00:00,8.8540945
|
| 962 |
+
2020-02-10 00:00:00,9.003355
|
| 963 |
+
2020-02-10 01:00:00,10.217049
|
| 964 |
+
2020-02-10 02:00:00,11.157508
|
| 965 |
+
2020-02-10 03:00:00,11.552127
|
| 966 |
+
2020-02-10 04:00:00,11.794086
|
| 967 |
+
2020-02-10 05:00:00,12.463447
|
| 968 |
+
2020-02-10 06:00:00,12.567337
|
| 969 |
+
2020-02-10 07:00:00,12.344029
|
| 970 |
+
2020-02-10 08:00:00,11.867516
|
| 971 |
+
2020-02-10 09:00:00,11.476329
|
| 972 |
+
2020-02-10 10:00:00,10.942833
|
| 973 |
+
2020-02-10 11:00:00,10.110067
|
| 974 |
+
2020-02-10 12:00:00,9.1285
|
| 975 |
+
2020-02-10 13:00:00,8.586722
|
| 976 |
+
2020-02-10 14:00:00,8.009865
|
| 977 |
+
2020-02-10 15:00:00,7.305326
|
| 978 |
+
2020-02-10 16:00:00,6.9213724
|
| 979 |
+
2020-02-10 17:00:00,6.660673
|
| 980 |
+
2020-02-10 18:00:00,6.728831
|
| 981 |
+
2020-02-10 19:00:00,7.1009297
|
| 982 |
+
2020-02-10 20:00:00,7.115267
|
| 983 |
+
2020-02-10 21:00:00,7.174452
|
| 984 |
+
2020-02-10 22:00:00,7.5554914
|
| 985 |
+
2020-02-10 23:00:00,8.802352
|
| 986 |
+
2020-02-11 00:00:00,9.623362
|
| 987 |
+
2020-02-11 01:00:00,10.165096
|
| 988 |
+
2020-02-11 02:00:00,11.1878805
|
| 989 |
+
2020-02-11 03:00:00,11.614379
|
| 990 |
+
2020-02-11 04:00:00,12.338524
|
| 991 |
+
2020-02-11 05:00:00,12.50653
|
| 992 |
+
2020-02-11 06:00:00,12.767748
|
| 993 |
+
2020-02-11 07:00:00,12.252229
|
| 994 |
+
2020-02-11 08:00:00,12.228723
|
| 995 |
+
2020-02-11 09:00:00,11.825585
|
| 996 |
+
2020-02-11 10:00:00,10.965025
|
| 997 |
+
2020-02-11 11:00:00,10.919291
|
| 998 |
+
2020-02-11 12:00:00,9.674904
|
| 999 |
+
2020-02-11 13:00:00,9.301591
|
| 1000 |
+
2020-02-11 14:00:00,8.184461
|
| 1001 |
+
2020-02-11 15:00:00,7.667546
|
| 1002 |
+
2020-02-11 16:00:00,7.491327
|
| 1003 |
+
2020-02-11 17:00:00,7.0335784
|
| 1004 |
+
2020-02-11 18:00:00,6.9271502
|
| 1005 |
+
2020-02-11 19:00:00,7.062455
|
| 1006 |
+
2020-02-11 20:00:00,7.054171
|
| 1007 |
+
2020-02-11 21:00:00,7.7447867
|
| 1008 |
+
2020-02-11 22:00:00,8.271346
|
| 1009 |
+
2020-02-11 23:00:00,9.24433
|
| 1010 |
+
2020-02-12 00:00:00,9.8912
|
| 1011 |
+
2020-02-12 01:00:00,10.786341
|
| 1012 |
+
2020-02-12 02:00:00,11.521907
|
| 1013 |
+
2020-02-12 03:00:00,12.170029
|
| 1014 |
+
2020-02-12 04:00:00,12.6656475
|
| 1015 |
+
2020-02-12 05:00:00,12.860258
|
| 1016 |
+
2020-02-12 06:00:00,12.90046
|
| 1017 |
+
2020-02-12 07:00:00,12.894316
|
| 1018 |
+
2020-02-12 08:00:00,12.959762
|
| 1019 |
+
2020-02-12 09:00:00,12.361292
|
| 1020 |
+
2020-02-12 10:00:00,11.800061
|
| 1021 |
+
2020-02-12 11:00:00,11.252397
|
| 1022 |
+
2020-02-12 12:00:00,9.981055
|
| 1023 |
+
2020-02-12 13:00:00,9.559168
|
| 1024 |
+
2020-02-12 14:00:00,8.534986
|
| 1025 |
+
2020-02-12 15:00:00,8.077829
|
example_monthly_temp.csv
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
timestamp,value
|
| 2 |
+
2015-01-01,13.516318
|
| 3 |
+
2015-02-01,19.44832
|
| 4 |
+
2015-03-01,25.592142
|
| 5 |
+
2015-04-01,25.290962
|
| 6 |
+
2015-05-01,25.0406
|
| 7 |
+
2015-06-01,20.865656
|
| 8 |
+
2015-07-01,14.045304
|
| 9 |
+
2015-08-01,10.812928
|
| 10 |
+
2015-09-01,5.864853
|
| 11 |
+
2015-10-01,4.5164165
|
| 12 |
+
2015-11-01,6.485497
|
| 13 |
+
2015-12-01,7.7111044
|
| 14 |
+
2016-01-01,16.78825
|
| 15 |
+
2016-02-01,18.993366
|
| 16 |
+
2016-03-01,25.160658
|
| 17 |
+
2016-04-01,25.204481
|
| 18 |
+
2016-05-01,25.958303
|
| 19 |
+
2016-06-01,19.010046
|
| 20 |
+
2016-07-01,14.532308
|
| 21 |
+
2016-08-01,10.506654
|
| 22 |
+
2016-09-01,3.0285394
|
| 23 |
+
2016-10-01,6.2418823
|
| 24 |
+
2016-11-01,8.652191
|
| 25 |
+
2016-12-01,11.69021
|
| 26 |
+
2017-01-01,16.132154
|
| 27 |
+
2017-02-01,19.781033
|
| 28 |
+
2017-03-01,25.583107
|
| 29 |
+
2017-04-01,26.611046
|
| 30 |
+
2017-05-01,24.249186
|
| 31 |
+
2017-06-01,20.007671
|
| 32 |
+
2017-07-01,14.45735
|
| 33 |
+
2017-08-01,8.154652
|
| 34 |
+
2017-09-01,8.17909
|
| 35 |
+
2017-10-01,1.7419342
|
| 36 |
+
2017-11-01,5.784525
|
| 37 |
+
2017-12-01,10.24657
|
| 38 |
+
2018-01-01,16.289822
|
| 39 |
+
2018-02-01,22.642492
|
| 40 |
+
2018-03-01,25.15024
|
| 41 |
+
2018-04-01,24.562717
|
| 42 |
+
2018-05-01,24.752445
|
| 43 |
+
2018-06-01,18.1076
|
| 44 |
+
2018-07-01,17.144907
|
| 45 |
+
2018-08-01,9.765287
|
| 46 |
+
2018-09-01,5.3291073
|
| 47 |
+
2018-10-01,4.04141
|
| 48 |
+
2018-11-01,6.247704
|
| 49 |
+
2018-12-01,9.410823
|
| 50 |
+
2019-01-01,18.434864
|
| 51 |
+
2019-02-01,18.922728
|
| 52 |
+
2019-03-01,23.709166
|
| 53 |
+
2019-04-01,25.042074
|
| 54 |
+
2019-05-01,23.702662
|
| 55 |
+
2019-06-01,20.08302
|
| 56 |
+
2019-07-01,14.277656
|
| 57 |
+
2019-08-01,9.124888
|
| 58 |
+
2019-09-01,5.046505
|
| 59 |
+
2019-10-01,2.767738
|
| 60 |
+
2019-11-01,6.664206
|
| 61 |
+
2019-12-01,11.476564
|
| 62 |
+
2020-01-01,14.185374
|
| 63 |
+
2020-02-01,19.162077
|
| 64 |
+
2020-03-01,23.18553
|
| 65 |
+
2020-04-01,24.30904
|
| 66 |
+
2020-05-01,21.50585
|
| 67 |
+
2020-06-01,22.047663
|
| 68 |
+
2020-07-01,15.6585
|
| 69 |
+
2020-08-01,8.932458
|
| 70 |
+
2020-09-01,6.7855034
|
| 71 |
+
2020-10-01,4.3423142
|
| 72 |
+
2020-11-01,6.0222898
|
| 73 |
+
2020-12-01,10.545946
|
| 74 |
+
2021-01-01,16.429447
|
| 75 |
+
2021-02-01,22.279285
|
| 76 |
+
2021-03-01,26.216118
|
| 77 |
+
2021-04-01,24.626713
|
| 78 |
+
2021-05-01,22.910631
|
| 79 |
+
2021-06-01,20.149397
|
| 80 |
+
2021-07-01,15.192514
|
| 81 |
+
2021-08-01,8.898667
|
| 82 |
+
2021-09-01,5.409033
|
| 83 |
+
2021-10-01,6.2199106
|
| 84 |
+
2021-11-01,8.802447
|
| 85 |
+
2021-12-01,9.660249
|
| 86 |
+
2022-01-01,14.028052
|
| 87 |
+
2022-02-01,19.574944
|
| 88 |
+
2022-03-01,22.167557
|
| 89 |
+
2022-04-01,24.590693
|
| 90 |
+
2022-05-01,24.29392
|
| 91 |
+
2022-06-01,19.877985
|
| 92 |
+
2022-07-01,16.851866
|
| 93 |
+
2022-08-01,10.226332
|
| 94 |
+
2022-09-01,7.061425
|
| 95 |
+
2022-10-01,4.7768636
|
| 96 |
+
2022-11-01,8.313245
|
| 97 |
+
2022-12-01,8.166482
|
| 98 |
+
2023-01-01,14.544613
|
| 99 |
+
2023-02-01,18.239468
|
| 100 |
+
2023-03-01,24.899664
|
| 101 |
+
2023-04-01,26.275484
|
| 102 |
+
2023-05-01,22.886602
|
| 103 |
+
2023-06-01,22.48717
|
| 104 |
+
2023-07-01,14.554106
|
| 105 |
+
2023-08-01,7.9249344
|
| 106 |
+
2023-09-01,5.917939
|
| 107 |
+
2023-10-01,5.540031
|
| 108 |
+
2023-11-01,5.9881577
|
| 109 |
+
2023-12-01,13.398281
|
| 110 |
+
2024-01-01,16.28308
|
| 111 |
+
2024-02-01,22.59692
|
| 112 |
+
2024-03-01,25.739082
|
| 113 |
+
2024-04-01,22.471323
|
| 114 |
+
2024-05-01,23.093616
|
| 115 |
+
2024-06-01,15.907271
|
| 116 |
+
2024-07-01,14.030405
|
| 117 |
+
2024-08-01,11.672656
|
| 118 |
+
2024-09-01,5.074929
|
| 119 |
+
2024-10-01,4.044967
|
| 120 |
+
2024-11-01,6.828947
|
| 121 |
+
2024-12-01,11.180975
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy
|
| 2 |
+
pandas
|
| 3 |
+
matplotlib
|
| 4 |
+
gluonts
|
tinycast/__init__.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TinyCast: an attention-free, 146,505-parameter dilated-convolution
|
| 2 |
+
time-series foundation model.
|
| 3 |
+
|
| 4 |
+
Forecast:
|
| 5 |
+
>>> from tinycast import TinyCastPredictor, load_checkpoint
|
| 6 |
+
>>> predictor = TinyCastPredictor(
|
| 7 |
+
... prediction_length=48,
|
| 8 |
+
... checkpoint_path="model.safetensors",
|
| 9 |
+
... freq="H", domain="Energy", device="cpu",
|
| 10 |
+
... force_flip_invariance=True,
|
| 11 |
+
... )
|
| 12 |
+
>>> forecasts = predictor.predict(gluonts_test_input)
|
| 13 |
+
|
| 14 |
+
Evaluate:
|
| 15 |
+
>>> from tinycast import summarize_by_freq_bin
|
| 16 |
+
>>> summarize_by_freq_bin("all_results.csv")["overall"]["ncrps"]
|
| 17 |
+
|
| 18 |
+
Train, then release:
|
| 19 |
+
>>> from tinycast import (TinyCastConfig, train, average_checkpoints,
|
| 20 |
+
... export_safetensors)
|
| 21 |
+
>>> result = train(TinyCastConfig(), data=windows, max_steps=1000,
|
| 22 |
+
... output_dir="run/", batch_size=32, checkpoint_every=100)
|
| 23 |
+
>>> weights = average_checkpoints(result.checkpoints[-8:])
|
| 24 |
+
>>> export_safetensors(weights, "release/", expect_parameters=146_505)
|
| 25 |
+
|
| 26 |
+
Rebuild the synthetic corpus (needs CUDA):
|
| 27 |
+
>>> from tinycast import build_shard, verify_shard
|
| 28 |
+
>>> build_shard() # published shard 0
|
| 29 |
+
>>> verify_shard("synth4096_0", shard=0)
|
| 30 |
+
|
| 31 |
+
``tinycast.train`` is the training function, not the module: the two share a
|
| 32 |
+
name and the function wins. Module-level recipe constants are reachable as
|
| 33 |
+
``from tinycast.train import AR_CHUNKS``. A submodule the package does not
|
| 34 |
+
import itself, ``tinycast.backbone`` and ``tinycast.periodogram`` among them,
|
| 35 |
+
becomes an attribute only after ``import tinycast.backbone``.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
from .config import TinyCastConfig
|
| 39 |
+
from .model import TinyCastForPrediction, TinyCastBackbone, PredictionOutput
|
| 40 |
+
from .checkpoint import load_checkpoint, load_model
|
| 41 |
+
from .predictor import TinyCastPredictor, ARRolloutPredictor
|
| 42 |
+
|
| 43 |
+
# Training and the objectives it optimizes.
|
| 44 |
+
from .losses import committing_loss, pinball_loss, seasonal_copy_baseline
|
| 45 |
+
from .train import TrainResult, train, training_window_width
|
| 46 |
+
|
| 47 |
+
# The synthetic pretraining corpus generators.
|
| 48 |
+
from .synth import generate_gp, generate_spikes, generate_tsi
|
| 49 |
+
|
| 50 |
+
# eval, export and corpus each carry a ``python -m`` entry point, so the package
|
| 51 |
+
# must not import them eagerly: that puts them in sys.modules before runpy runs
|
| 52 |
+
# them as __main__, which warns and executes the module body twice. Resolving
|
| 53 |
+
# them on first attribute access (PEP 562) keeps ``from tinycast import
|
| 54 |
+
# evaluate`` working and leaves the command line quiet.
|
| 55 |
+
_LAZY_MODULES = ("eval", "export", "corpus")
|
| 56 |
+
_LAZY_EXPORTS = {
|
| 57 |
+
"evaluate": "eval",
|
| 58 |
+
"summarize_by_freq_bin": "eval",
|
| 59 |
+
"export_safetensors": "export",
|
| 60 |
+
"average_checkpoints": "export",
|
| 61 |
+
"check_export_roundtrip": "export",
|
| 62 |
+
"ExportError": "export",
|
| 63 |
+
"build_shard": "corpus",
|
| 64 |
+
"verify_shard": "corpus",
|
| 65 |
+
"iter_shard_series": "corpus",
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def __getattr__(name: str):
|
| 70 |
+
from importlib import import_module
|
| 71 |
+
|
| 72 |
+
if name in _LAZY_MODULES:
|
| 73 |
+
value = import_module(f".{name}", __name__)
|
| 74 |
+
else:
|
| 75 |
+
module = _LAZY_EXPORTS.get(name)
|
| 76 |
+
if module is None:
|
| 77 |
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
| 78 |
+
value = getattr(import_module(f".{module}", __name__), name)
|
| 79 |
+
globals()[name] = value # resolve once, then it is a plain global
|
| 80 |
+
return value
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def __dir__() -> list:
|
| 84 |
+
return sorted(set(globals()) | set(_LAZY_EXPORTS) | set(_LAZY_MODULES))
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
__all__ = [
|
| 88 |
+
# model and inference
|
| 89 |
+
"TinyCastConfig",
|
| 90 |
+
"TinyCastForPrediction",
|
| 91 |
+
"TinyCastBackbone",
|
| 92 |
+
"PredictionOutput",
|
| 93 |
+
"load_checkpoint",
|
| 94 |
+
"load_model",
|
| 95 |
+
"TinyCastPredictor",
|
| 96 |
+
"ARRolloutPredictor",
|
| 97 |
+
# training
|
| 98 |
+
"train",
|
| 99 |
+
"TrainResult",
|
| 100 |
+
"training_window_width",
|
| 101 |
+
"pinball_loss",
|
| 102 |
+
"committing_loss",
|
| 103 |
+
"seasonal_copy_baseline",
|
| 104 |
+
# export
|
| 105 |
+
"export_safetensors",
|
| 106 |
+
"average_checkpoints",
|
| 107 |
+
"check_export_roundtrip",
|
| 108 |
+
"ExportError",
|
| 109 |
+
# evaluation
|
| 110 |
+
"evaluate",
|
| 111 |
+
"summarize_by_freq_bin",
|
| 112 |
+
# synthetic corpus
|
| 113 |
+
"build_shard",
|
| 114 |
+
"verify_shard",
|
| 115 |
+
"iter_shard_series",
|
| 116 |
+
"generate_gp",
|
| 117 |
+
"generate_spikes",
|
| 118 |
+
"generate_tsi",
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
+
__version__ = "1.0.0"
|
tinycast/backbone.py
ADDED
|
@@ -0,0 +1,1070 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dilated-convolution forecasting backbone.
|
| 2 |
+
|
| 3 |
+
An attention-free encoder: a stack of causal dilated convolutions. With
|
| 4 |
+
kernel K=3 and dilations d_i = 2^(i-1) for i=1..N the receptive field is
|
| 5 |
+
|
| 6 |
+
RF = 1 + 2 * sum_i d_i = 1 + 2 * (2^N - 1)
|
| 7 |
+
|
| 8 |
+
so N=10 layers cover RF=2047, sufficient for the L=2048 context with zero
|
| 9 |
+
downsampling and no information loss at any time scale. Native multi-scale
|
| 10 |
+
via the dilation schedule; deployment-friendly (no L^2 attention matrix, no
|
| 11 |
+
softmax, pure matmul + element-wise; quantizes cleanly to INT8); streaming-
|
| 12 |
+
friendly (left-only causal padding).
|
| 13 |
+
|
| 14 |
+
Structural priors (zero-parameter):
|
| 15 |
+
- a normalized-periodogram period detector driving a phase encoding
|
| 16 |
+
- bounded recency basis (signed-linear/log, multi-scale exp decay)
|
| 17 |
+
- position-parameterized decoder queries (single-shot arbitrary horizon)
|
| 18 |
+
- no autoregressive rollout inside the backbone
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import math
|
| 24 |
+
|
| 25 |
+
import torch
|
| 26 |
+
import torch.nn as nn
|
| 27 |
+
import torch.nn.functional as F
|
| 28 |
+
|
| 29 |
+
from .periodogram import significant_periods
|
| 30 |
+
from .encoding import (
|
| 31 |
+
N_RECENCY_CHANNELS,
|
| 32 |
+
_norm_fp32,
|
| 33 |
+
_phase_encoding,
|
| 34 |
+
_positional_encoding,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class _SwiGLU(nn.Module):
|
| 39 |
+
"""Standard SwiGLU FFN."""
|
| 40 |
+
|
| 41 |
+
def __init__(self, d: int, d_hidden: int) -> None:
|
| 42 |
+
super().__init__()
|
| 43 |
+
self.up = nn.Linear(d, 2 * d_hidden)
|
| 44 |
+
self.down = nn.Linear(d_hidden, d)
|
| 45 |
+
|
| 46 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 47 |
+
gate, val = self.up(x).chunk(2, dim=-1)
|
| 48 |
+
return self.down(F.silu(gate) * val)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class _DilatedConvBlock(nn.Module):
|
| 52 |
+
"""Dilated Conv1d → RMSNorm → SwiGLU → RMSNorm with residuals.
|
| 53 |
+
|
| 54 |
+
``causal=True``, which is what the released config sets, pads all (K-1)*d
|
| 55 |
+
timesteps on the left, so each output position sees only its own past. With
|
| 56 |
+
``causal=False`` the padding is centered instead: length is still preserved
|
| 57 |
+
and each position gets a symmetric view of (K-1)*d/2 timesteps on either
|
| 58 |
+
side, at the cost of future-side context. Under both, the dilation scales
|
| 59 |
+
the per-layer receptive field without adding parameters.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
def __init__(
|
| 63 |
+
self, d: int, kernel: int = 3, dilation: int = 1,
|
| 64 |
+
ffn_mult: float = 1.5, causal: bool = False, gated: bool = False,
|
| 65 |
+
separable: bool = False,
|
| 66 |
+
) -> None:
|
| 67 |
+
super().__init__()
|
| 68 |
+
self.k = int(kernel)
|
| 69 |
+
self.dilation = int(dilation)
|
| 70 |
+
self.causal = bool(causal)
|
| 71 |
+
self.gated = bool(gated)
|
| 72 |
+
self.separable = bool(separable)
|
| 73 |
+
# Conv with dilation; padding handled in forward. Centered padding
|
| 74 |
+
# gives each position a symmetric view but feeds the right edge
|
| 75 |
+
# FUTURE-side zeros: at the deepest layer the last (most recent)
|
| 76 |
+
# position's representation is dominated by padding standing in for
|
| 77 |
+
# the unknown forecast, and the model learns a train/inference
|
| 78 |
+
# mismatch. Causal (all-left) padding removes both pathologies and
|
| 79 |
+
# is a prerequisite for honest streaming inference.
|
| 80 |
+
if self.separable:
|
| 81 |
+
# Depthwise-separable factorization: depthwise (per-channel, dilated,
|
| 82 |
+
# padding consumed in forward) + pointwise 1x1 (channel mix). Params
|
| 83 |
+
# D*K + D*D against a full conv's D*D*K: the receptive field is
|
| 84 |
+
# preserved at a fraction of the parameters per block. Padding
|
| 85 |
+
# before self.conv feeds the depthwise stage.
|
| 86 |
+
self.conv = nn.Sequential(
|
| 87 |
+
nn.Conv1d(d, d, kernel_size=self.k, dilation=self.dilation, groups=d),
|
| 88 |
+
nn.Conv1d(d, d, kernel_size=1),
|
| 89 |
+
)
|
| 90 |
+
else:
|
| 91 |
+
self.conv = nn.Conv1d(d, d, kernel_size=self.k, dilation=self.dilation)
|
| 92 |
+
# Lightweight gated conv (multiplicative gating): a cheap
|
| 93 |
+
# DEPTHWISE gate conv produces a sigmoid mask over the main conv output,
|
| 94 |
+
# conv_out * σ(gate). Adds data-dependent gating to the encoder at ~D·k
|
| 95 |
+
# params/block (vs doubling the full conv). Off by default.
|
| 96 |
+
self.gate = (
|
| 97 |
+
nn.Conv1d(d, d, kernel_size=self.k, dilation=self.dilation, groups=d)
|
| 98 |
+
if self.gated else None
|
| 99 |
+
)
|
| 100 |
+
self.norm1 = nn.RMSNorm(d)
|
| 101 |
+
d_hidden = int(d * ffn_mult)
|
| 102 |
+
self.ffn = _SwiGLU(d, d_hidden)
|
| 103 |
+
self.norm2 = nn.RMSNorm(d)
|
| 104 |
+
|
| 105 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 106 |
+
# x: (B, L, D). Conv1d takes (B, D, L).
|
| 107 |
+
x_t = x.transpose(1, 2)
|
| 108 |
+
# Effective kernel span = (K-1)*d + 1.
|
| 109 |
+
pad_total = (self.k - 1) * self.dilation
|
| 110 |
+
if self.causal:
|
| 111 |
+
left, right = pad_total, 0
|
| 112 |
+
else:
|
| 113 |
+
left = pad_total // 2
|
| 114 |
+
right = pad_total - left
|
| 115 |
+
x_t = F.pad(x_t, (left, right))
|
| 116 |
+
if self.separable and self.gate is None:
|
| 117 |
+
# Depthwise (B,D,L); then the pointwise 1x1 conv == a per-position
|
| 118 |
+
# Linear, run as F.linear (cublas GEMM) instead of an im2col/cudnn
|
| 119 |
+
# 1x1-conv kernel. Bit-identical channel contraction; params stay as
|
| 120 |
+
# conv.0/conv.1 so existing checkpoints load unchanged.
|
| 121 |
+
dw = self.conv[0](x_t).transpose(1, 2) # (B, L, D)
|
| 122 |
+
pw = self.conv[1]
|
| 123 |
+
conv_out = F.linear(dw, pw.weight.squeeze(-1), pw.bias) # (B, L, D)
|
| 124 |
+
else:
|
| 125 |
+
conv_out = self.conv(x_t)
|
| 126 |
+
if self.gate is not None:
|
| 127 |
+
conv_out = conv_out * torch.sigmoid(self.gate(x_t))
|
| 128 |
+
conv_out = conv_out.transpose(1, 2)
|
| 129 |
+
x = _norm_fp32(self.norm1, x + conv_out)
|
| 130 |
+
x = _norm_fp32(self.norm2, x + self.ffn(x))
|
| 131 |
+
return x
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class DilatedConvBackbone(nn.Module):
|
| 135 |
+
"""Dilated-conv encoder with a phase-conditioned, position-parameterized decoder.
|
| 136 |
+
|
| 137 |
+
Args:
|
| 138 |
+
seq_len: L, the context length.
|
| 139 |
+
p_out: H, the single-shot output length.
|
| 140 |
+
n_quantiles: Q, the number of output channels.
|
| 141 |
+
d: channel dimension.
|
| 142 |
+
n_layers: number of dilated-conv blocks (10 → RF 2047 ≥ L=2048).
|
| 143 |
+
kernel: conv kernel size (default 3).
|
| 144 |
+
ffn_mult: SwiGLU hidden multiplier (default 1.5).
|
| 145 |
+
dilations: explicit dilation schedule; if None, uses 2^(i-1).
|
| 146 |
+
top_k_periods: the periodogram detector's top-K (default 4).
|
| 147 |
+
significance_alpha: the periodogram detector's Bonferroni alpha (default 0.05).
|
| 148 |
+
n_harmonics: Fourier harmonics per detected period (default 1).
|
| 149 |
+
pool_kind: context-summary pooling: "mean_last" (default),
|
| 150 |
+
"mean", "last". Concat the chosen pool(s) into the
|
| 151 |
+
per-horizon query before query_proj.
|
| 152 |
+
causal: if True, all conv padding is left-only (no future
|
| 153 |
+
leakage, clean right edge, streaming-honest).
|
| 154 |
+
phase_bins: if > 0, augment the global pool with a period-folded
|
| 155 |
+
seasonal profile: for each period the periodogram
|
| 156 |
+
detector returns, fold the encoder output into this many
|
| 157 |
+
phase bins and let each decoder query gather the bin
|
| 158 |
+
matching its own phase. 0 disables phase folding and
|
| 159 |
+
leaves the plain global pool, which is the control the
|
| 160 |
+
paper's phase-folding ablation is measured against.
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
def __init__(
|
| 164 |
+
self,
|
| 165 |
+
seq_len: int,
|
| 166 |
+
p_out: int,
|
| 167 |
+
n_quantiles: int = 1,
|
| 168 |
+
d: int = 80,
|
| 169 |
+
n_layers: int = 10,
|
| 170 |
+
kernel: int = 3,
|
| 171 |
+
ffn_mult: float = 1.5,
|
| 172 |
+
dilations: list[int] | None = None,
|
| 173 |
+
top_k_periods: int = 4,
|
| 174 |
+
significance_alpha: float = 0.05,
|
| 175 |
+
n_harmonics: int = 1,
|
| 176 |
+
pool_kind: str = "mean_last",
|
| 177 |
+
causal: bool = False,
|
| 178 |
+
phase_bins: int = 0,
|
| 179 |
+
phase_stats: str = "mean",
|
| 180 |
+
phase_recency_tau: float = 0.0,
|
| 181 |
+
recency_bins: int = 0,
|
| 182 |
+
sig_gate: bool = False,
|
| 183 |
+
cross_cycle: bool = False,
|
| 184 |
+
decoder_depth: int = 1,
|
| 185 |
+
horizon_kernel: int = 0,
|
| 186 |
+
horizon_recurrence: bool = False,
|
| 187 |
+
min_cycles: int = 0,
|
| 188 |
+
period_trust: str = "off",
|
| 189 |
+
gated_conv: bool = False,
|
| 190 |
+
residual_naive: bool = False,
|
| 191 |
+
residual_multi: bool = False,
|
| 192 |
+
residual_trend: bool = False,
|
| 193 |
+
decompose_kernel: int = 0,
|
| 194 |
+
periodogram_off: bool = False,
|
| 195 |
+
res_adaptive: bool = False,
|
| 196 |
+
res_period_target: int = 64,
|
| 197 |
+
res_r_max: float = 32.0,
|
| 198 |
+
with_missing: bool = False,
|
| 199 |
+
missing_channel: bool = False,
|
| 200 |
+
separable_conv: bool = False,
|
| 201 |
+
share_ffn: bool = False,
|
| 202 |
+
future_conv: bool = False,
|
| 203 |
+
future_conv_layers: int = 6,
|
| 204 |
+
future_conv_seed: int = 128,
|
| 205 |
+
base_seasonality: float = 24.0,
|
| 206 |
+
local_anchor: bool = False,
|
| 207 |
+
) -> None:
|
| 208 |
+
super().__init__()
|
| 209 |
+
self.L = int(seq_len)
|
| 210 |
+
self.p_out = int(p_out)
|
| 211 |
+
self.n_quantiles = int(n_quantiles)
|
| 212 |
+
self.D = int(d)
|
| 213 |
+
self.K = int(top_k_periods)
|
| 214 |
+
self.significance_alpha = float(significance_alpha)
|
| 215 |
+
self.n_harmonics = int(n_harmonics)
|
| 216 |
+
self.pool_kind = str(pool_kind)
|
| 217 |
+
self.causal = bool(causal)
|
| 218 |
+
self.phase_bins = int(phase_bins)
|
| 219 |
+
if phase_stats not in ("mean", "mean_var"):
|
| 220 |
+
raise ValueError(f"phase_stats={phase_stats!r}; expected 'mean'|'mean_var'.")
|
| 221 |
+
self.phase_stats = str(phase_stats)
|
| 222 |
+
self.stat_mult = 2 if self.phase_stats == "mean_var" else 1
|
| 223 |
+
self.phase_recency_tau = float(phase_recency_tau)
|
| 224 |
+
self.recency_bins = int(recency_bins)
|
| 225 |
+
self.sig_gate = bool(sig_gate)
|
| 226 |
+
self.cross_cycle = bool(cross_cycle)
|
| 227 |
+
self.decoder_depth = max(1, int(decoder_depth))
|
| 228 |
+
self.horizon_kernel = int(horizon_kernel)
|
| 229 |
+
self.horizon_recurrence = bool(horizon_recurrence)
|
| 230 |
+
self.min_cycles = int(min_cycles)
|
| 231 |
+
if period_trust not in ("off", "coverage", "full"):
|
| 232 |
+
raise ValueError(f"period_trust={period_trust!r}; expected off|coverage|full")
|
| 233 |
+
self.period_trust = str(period_trust)
|
| 234 |
+
# Per-period reliability weight w_k = sigmoid(linear([margin, ln
|
| 235 |
+
# coverage])): a continuous down-weighting of long or weakly-supported
|
| 236 |
+
# periods, whose crossover is learned rather than set. It covers the
|
| 237 |
+
# same ground as the integer min_cycles cutoff, which stays available
|
| 238 |
+
# and independent; both are off in the released config. "coverage" uses
|
| 239 |
+
# ln(L/period) alone (min_cycles is its hard-threshold limit); "full"
|
| 240 |
+
# adds the significance margin ln(s_k/t_alpha) from the detector's
|
| 241 |
+
# periodogram scores.
|
| 242 |
+
if self.period_trust != "off":
|
| 243 |
+
n_feat = 1 if self.period_trust == "coverage" else 2
|
| 244 |
+
self.pt = nn.Linear(n_feat, 1)
|
| 245 |
+
with torch.no_grad():
|
| 246 |
+
self.pt.weight.zero_(); self.pt.weight[0, 0] = 1.0 # cov (or margin) coeff = 1
|
| 247 |
+
self.pt.bias.zero_()
|
| 248 |
+
# data-determined Bonferroni threshold t_alpha (no tuned knob)
|
| 249 |
+
n_fft = 1 << int(math.ceil(math.log2(max(2, self.L))))
|
| 250 |
+
self._n_bins = max(2, n_fft // 2)
|
| 251 |
+
self._t_alpha = math.log(self._n_bins / max(self.significance_alpha, 1e-12)) / self._n_bins
|
| 252 |
+
else:
|
| 253 |
+
self.pt = None
|
| 254 |
+
self.gated_conv = bool(gated_conv)
|
| 255 |
+
self.residual_naive = bool(residual_naive)
|
| 256 |
+
self.residual_multi = bool(residual_multi)
|
| 257 |
+
self.residual_trend = bool(residual_trend)
|
| 258 |
+
self.periodogram_off = bool(periodogram_off)
|
| 259 |
+
self.res_adaptive = bool(res_adaptive)
|
| 260 |
+
self.res_period_target = int(res_period_target)
|
| 261 |
+
self.res_r_max = float(res_r_max)
|
| 262 |
+
# Series decomposition: moving-avg trend / seasonal split fed
|
| 263 |
+
# as two input channels. 0 disables. Even kernel → +1 for centered.
|
| 264 |
+
self.decompose_kernel = int(decompose_kernel)
|
| 265 |
+
self.with_missing = bool(with_missing)
|
| 266 |
+
# Missing-value channel: feed the encoder a binary observed-mask so it
|
| 267 |
+
# can distinguish a genuinely-unobserved position from a real value (the
|
| 268 |
+
# faithful treatment, vs mean-fill which conflates the two).
|
| 269 |
+
self.missing_channel = bool(missing_channel)
|
| 270 |
+
if self.missing_channel and bool(res_adaptive):
|
| 271 |
+
raise NotImplementedError(
|
| 272 |
+
"missing_channel + res_adaptive: the observed-mask is not warped "
|
| 273 |
+
"through _resolution_adapt; not supported together."
|
| 274 |
+
)
|
| 275 |
+
# Recent-anchor channels: gradient-connected causal local level/scale so the
|
| 276 |
+
# encoder can re-anchor amplitude under non-stationarity (the denorm origin
|
| 277 |
+
# x_min is a frozen detached GLOBAL min, with no learned path to the
|
| 278 |
+
# recent regime).
|
| 279 |
+
self.local_anchor = bool(local_anchor)
|
| 280 |
+
|
| 281 |
+
if self.n_harmonics < 1:
|
| 282 |
+
raise ValueError(f"n_harmonics must be >= 1; got {self.n_harmonics}")
|
| 283 |
+
if pool_kind not in ("mean_last", "mean", "last"):
|
| 284 |
+
raise ValueError(
|
| 285 |
+
f"pool_kind={pool_kind!r}; expected 'mean_last' | 'mean' | 'last'."
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
# Positional encoding: phase channels + bounded recency channels.
|
| 289 |
+
n_phase = 2 * self.K * self.n_harmonics
|
| 290 |
+
n_pe = n_phase + N_RECENCY_CHANNELS
|
| 291 |
+
# Input channels: raw value, or [trend, seasonal] if decomposing.
|
| 292 |
+
n_value_ch = 2 if self.decompose_kernel > 0 else 1
|
| 293 |
+
if self.missing_channel:
|
| 294 |
+
n_value_ch += 1 # +observed-mask
|
| 295 |
+
if self.local_anchor:
|
| 296 |
+
n_value_ch += 2 # +[local-scale residual, log local-scale]
|
| 297 |
+
in_channels = n_value_ch + n_pe
|
| 298 |
+
self.in_proj = nn.Linear(in_channels, self.D)
|
| 299 |
+
if self.local_anchor:
|
| 300 |
+
# zero-init the 2 anchor columns (last of the value channels) so the
|
| 301 |
+
# model is baseline-equivalent at init and learns the anchor from zero.
|
| 302 |
+
with torch.no_grad():
|
| 303 |
+
self.in_proj.weight[:, n_value_ch - 2:n_value_ch].zero_()
|
| 304 |
+
|
| 305 |
+
# Dilation schedule.
|
| 306 |
+
if dilations is None:
|
| 307 |
+
dilations = [2**i for i in range(int(n_layers))]
|
| 308 |
+
if len(dilations) != int(n_layers):
|
| 309 |
+
raise ValueError(
|
| 310 |
+
f"dilations length {len(dilations)} != n_layers {n_layers}"
|
| 311 |
+
)
|
| 312 |
+
self.dilations = list(dilations)
|
| 313 |
+
|
| 314 |
+
# Receptive field sanity (informational only; fails soft if RF < L).
|
| 315 |
+
rf = 1 + (kernel - 1) * sum(self.dilations)
|
| 316 |
+
self.receptive_field = rf
|
| 317 |
+
|
| 318 |
+
self.encoder = nn.ModuleList([
|
| 319 |
+
_DilatedConvBlock(
|
| 320 |
+
self.D, kernel=int(kernel), dilation=int(d_i),
|
| 321 |
+
ffn_mult=float(ffn_mult), causal=self.causal,
|
| 322 |
+
gated=self.gated_conv, separable=bool(separable_conv),
|
| 323 |
+
)
|
| 324 |
+
for d_i in self.dilations
|
| 325 |
+
])
|
| 326 |
+
# Cross-layer FFN weight sharing (weight-tied): the SwiGLU FFN is the
|
| 327 |
+
# largest param bucket and is dilation-independent, so one shared FFN
|
| 328 |
+
# across all blocks recovers ~(n_layers-1)/n_layers of FFN params. The
|
| 329 |
+
# per-block dilated convs (which carry the receptive field) stay distinct.
|
| 330 |
+
self.share_ffn = bool(share_ffn)
|
| 331 |
+
if self.share_ffn and len(self.encoder) > 1:
|
| 332 |
+
shared_ffn = self.encoder[0].ffn
|
| 333 |
+
for blk in self.encoder[1:]:
|
| 334 |
+
blk.ffn = shared_ffn
|
| 335 |
+
|
| 336 |
+
# Pooled context summary dim depends on pool_kind.
|
| 337 |
+
pool_dim = {"mean_last": 2 * self.D, "mean": self.D, "last": self.D}[
|
| 338 |
+
self.pool_kind
|
| 339 |
+
]
|
| 340 |
+
|
| 341 |
+
# Phase-binned seasonal profile: K period-folded profiles, each
|
| 342 |
+
# gathered by the decoder query's own phase, then mixed to D. A global
|
| 343 |
+
# mean pool averages every phase of a cycle into one vector, so nothing
|
| 344 |
+
# that varies with phase survives it; folding by phase keeps the
|
| 345 |
+
# per-cycle waveform and hands each query the part of the cycle it is
|
| 346 |
+
# forecasting.
|
| 347 |
+
# Phase profile mixer: K periods × n_bins × (mean[,var]) → D.
|
| 348 |
+
if self.phase_bins > 0:
|
| 349 |
+
self.phase_mix = nn.Linear(self.K * self.stat_mult * self.D, self.D)
|
| 350 |
+
else:
|
| 351 |
+
self.phase_mix = None
|
| 352 |
+
|
| 353 |
+
# Recency profile mixer: rb log-distance bins × (mean[,var])
|
| 354 |
+
# → D. Always-valid aperiodic content path. Flattened (not gathered).
|
| 355 |
+
if self.recency_bins > 0:
|
| 356 |
+
self.recency_mix = nn.Linear(
|
| 357 |
+
self.recency_bins * self.stat_mult * self.D, self.D,
|
| 358 |
+
)
|
| 359 |
+
else:
|
| 360 |
+
self.recency_mix = None
|
| 361 |
+
|
| 362 |
+
# Cross-cycle conv branch: a depthwise conv across cycles
|
| 363 |
+
# at fixed phase, applied to the dominant period's [n_cycles × n_bins]
|
| 364 |
+
# fold. Adds one D-dim feature to the query. See _cross_cycle_profile.
|
| 365 |
+
if self.cross_cycle:
|
| 366 |
+
self.cc_bins = self.phase_bins if self.phase_bins > 0 else 16
|
| 367 |
+
self.cc_cycles = 8 # most-recent N cycles folded; older clamped
|
| 368 |
+
# Depthwise conv ACROSS the cycle axis (length cc_cycles) at fixed
|
| 369 |
+
# phase: models how each phase evolves cycle-to-cycle.
|
| 370 |
+
self.cc_conv = nn.Conv1d(
|
| 371 |
+
self.D, self.D, kernel_size=3, padding=1, groups=self.D,
|
| 372 |
+
)
|
| 373 |
+
self.cc_mix = nn.Linear(self.D, self.D)
|
| 374 |
+
else:
|
| 375 |
+
self.cc_conv = None
|
| 376 |
+
|
| 377 |
+
# Decoder query input: PE + pool [+ phase D] [+ recency D] [+ cc D].
|
| 378 |
+
# With sig_gate, phase & recency are blended into a single D (not
|
| 379 |
+
# concatenated), so they contribute D once, not 2·D.
|
| 380 |
+
query_in = n_pe + pool_dim
|
| 381 |
+
if self.sig_gate and self.phase_mix is not None and self.recency_mix is not None:
|
| 382 |
+
query_in += self.D
|
| 383 |
+
else:
|
| 384 |
+
query_in += self.D if self.phase_mix is not None else 0
|
| 385 |
+
query_in += self.D if self.recency_mix is not None else 0
|
| 386 |
+
query_in += self.D if self.cross_cycle else 0
|
| 387 |
+
self.query_proj = nn.Linear(query_in, self.D)
|
| 388 |
+
d_hidden = int(self.D * float(ffn_mult))
|
| 389 |
+
# Decoder: `decoder_depth` residual SwiGLU blocks (depth 1 is a single
|
| 390 |
+
# block). The decoder's inputs are rich (phase/recency profiles), and
|
| 391 |
+
# depth lets it process them.
|
| 392 |
+
self.decoder_ffns = nn.ModuleList(
|
| 393 |
+
[_SwiGLU(self.D, d_hidden) for _ in range(self.decoder_depth)]
|
| 394 |
+
)
|
| 395 |
+
self.decoder_norms = nn.ModuleList(
|
| 396 |
+
[nn.RMSNorm(self.D) for _ in range(self.decoder_depth)]
|
| 397 |
+
)
|
| 398 |
+
# Cross-horizon coherence: a causal depthwise conv across the
|
| 399 |
+
# horizon axis couples adjacent forecast steps (the cross-step mixing
|
| 400 |
+
# lost when attention was dropped). Causal + fixed kernel preserves the
|
| 401 |
+
# single-shot arbitrary-horizon property. horizon_kernel=0 disables.
|
| 402 |
+
if self.horizon_kernel > 0:
|
| 403 |
+
self.horizon_conv = nn.Conv1d(
|
| 404 |
+
self.D, self.D, kernel_size=self.horizon_kernel, groups=self.D,
|
| 405 |
+
)
|
| 406 |
+
self.horizon_norm = nn.RMSNorm(self.D)
|
| 407 |
+
else:
|
| 408 |
+
self.horizon_conv = None
|
| 409 |
+
# Horizon-recurrent decode-state: a gated diagonal recurrence over the
|
| 410 |
+
# SHORT horizon axis, scanning the precomputed query features. It never
|
| 411 |
+
# re-feeds predicted values, so the decoder stays single-shot rather
|
| 412 |
+
# than autoregressive. An unbounded carried state couples step h to ALL
|
| 413 |
+
# earlier steps (vs the fixed-span horizon_conv), the property that
|
| 414 |
+
# makes a recurrent decoder horizon-invariant. hr_o is zero-init so the
|
| 415 |
+
# block is identity at start and cannot regress the baseline.
|
| 416 |
+
if self.horizon_recurrence:
|
| 417 |
+
self.hr_z = nn.Linear(self.D, self.D) # update gate
|
| 418 |
+
self.hr_c = nn.Linear(self.D, self.D) # candidate
|
| 419 |
+
self.hr_o = nn.Linear(self.D, self.D) # output proj (zero-init)
|
| 420 |
+
nn.init.zeros_(self.hr_o.weight); nn.init.zeros_(self.hr_o.bias)
|
| 421 |
+
self.hr_norm = nn.RMSNorm(self.D)
|
| 422 |
+
else:
|
| 423 |
+
self.hr_z = None
|
| 424 |
+
self.out_proj = nn.Linear(self.D, self.n_quantiles)
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
# Future-conv decoder (horizon-axis state evolution; the conv-native
|
| 428 |
+
# analog of a missing-token decoder). The rest of the decoder queries a
|
| 429 |
+
# STATIC pooled summary at every horizon position, which is why error
|
| 430 |
+
# grows with horizon. future_conv runs a CAUSAL dilated conv over
|
| 431 |
+
# [context-tail seed ++ seasonal-naive future fill], producing
|
| 432 |
+
# per-future-position hidden states that EVOLVE along the horizon (each
|
| 433 |
+
# future position is a causal-conv function of recent context + earlier
|
| 434 |
+
# future), and injects them additively into the decoder query. The fill
|
| 435 |
+
# is the dominant-period seasonal-naive continuation (it carries
|
| 436 |
+
# periodic structure, so the conv evolves a real waveform forward
|
| 437 |
+
# rather than zeros), which leaves the decoder predicting the residual
|
| 438 |
+
# over a copy. ``fc_out`` is zero-init => EXACT baseline at start
|
| 439 |
+
# (zero-init additive idiom), and disabling it restores that baseline.
|
| 440 |
+
# This differs from the two cheaper readouts in the same position: a
|
| 441 |
+
# recurrence over queries derived from the static summary adds no new
|
| 442 |
+
# dynamics, and a phase gather only COPIES context profiles, whereas
|
| 443 |
+
# this path RUNS the conv forward.
|
| 444 |
+
self.future_conv = bool(future_conv)
|
| 445 |
+
if self.future_conv:
|
| 446 |
+
if res_adaptive:
|
| 447 |
+
raise ValueError("future_conv is incompatible with res_adaptive")
|
| 448 |
+
self.fc_seed = int(future_conv_seed)
|
| 449 |
+
fc_in = 1 + n_pe # fill value + the same PE layout
|
| 450 |
+
self.fc_in_proj = nn.Linear(fc_in, self.D)
|
| 451 |
+
fc_dils = [2 ** i for i in range(int(future_conv_layers))]
|
| 452 |
+
self.fc_blocks = nn.ModuleList([
|
| 453 |
+
_DilatedConvBlock(
|
| 454 |
+
self.D, kernel=int(kernel), dilation=int(d_i),
|
| 455 |
+
ffn_mult=float(ffn_mult), causal=True,
|
| 456 |
+
separable=True, # auxiliary module: keep it light (~51K add)
|
| 457 |
+
)
|
| 458 |
+
for d_i in fc_dils
|
| 459 |
+
])
|
| 460 |
+
# Weight-tie the FFN across fc blocks: the convs carry the horizon
|
| 461 |
+
# dynamics, and one shared FFN keeps the param add modest.
|
| 462 |
+
shared = self.fc_blocks[0].ffn
|
| 463 |
+
for blk in self.fc_blocks[1:]:
|
| 464 |
+
blk.ffn = shared
|
| 465 |
+
self.fc_out = nn.Linear(self.D, self.D) # zero-init => exact baseline
|
| 466 |
+
nn.init.zeros_(self.fc_out.weight)
|
| 467 |
+
nn.init.zeros_(self.fc_out.bias)
|
| 468 |
+
|
| 469 |
+
self.base_seasonality = float(base_seasonality)
|
| 470 |
+
|
| 471 |
+
# ---- helpers ----------------------------------------------------------
|
| 472 |
+
|
| 473 |
+
def _future_conv_states(
|
| 474 |
+
self, h: torch.Tensor, fut_pe: torch.Tensor, fill: torch.Tensor,
|
| 475 |
+
) -> torch.Tensor:
|
| 476 |
+
"""Causal-conv continuation states at the H future positions.
|
| 477 |
+
|
| 478 |
+
h: (B, L, D) encoder output; fut_pe: (B, H, n_pe); fill: (B, H) the
|
| 479 |
+
seasonal-naive future continuation. Returns (B, H, D).
|
| 480 |
+
"""
|
| 481 |
+
# self.L is static (asserted == L in forward), so using it instead of
|
| 482 |
+
# the traced h.shape[1] keeps dynamo from graph-splitting on a symint
|
| 483 |
+
# bound.
|
| 484 |
+
ft = self.fc_in_proj(torch.cat([fill.unsqueeze(-1), fut_pe], dim=-1)) # (B,H,D)
|
| 485 |
+
seed = h[:, -min(self.fc_seed, self.L):, :] # (B, seed, D)
|
| 486 |
+
z = torch.cat([seed, ft], dim=1) # (B, seed+H, D)
|
| 487 |
+
for blk in self.fc_blocks: # causal: no future leak
|
| 488 |
+
z = blk(z)
|
| 489 |
+
return z[:, -ft.shape[1]:, :] # (B, H, D)
|
| 490 |
+
|
| 491 |
+
@torch.compiler.disable()
|
| 492 |
+
def _detect_periods(
|
| 493 |
+
self, x_fp32: torch.Tensor,
|
| 494 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 495 |
+
"""Run the significance-filtered periodogram in fp32 (no grad).
|
| 496 |
+
|
| 497 |
+
@torch.compiler.disable: the periodogram uses a complex rfft that
|
| 498 |
+
Inductor cannot codegen: left inside the compiled graph it forces an
|
| 499 |
+
eager fallback + graph break every forward, early in `forward`, blocking
|
| 500 |
+
fusion of the whole conv encoder/decoder downstream. Disabling compile on
|
| 501 |
+
this (no_grad, fp32, produces integer periods the rest only reads) makes a
|
| 502 |
+
clean eager boundary: the FFT runs eager, everything after fuses. Output
|
| 503 |
+
bit-identical (only where it compiles changes).
|
| 504 |
+
|
| 505 |
+
Returns ``(periods, n_valid, scores)``: the integer periods (0 =
|
| 506 |
+
rejected), the count of significant periods per sample, and the
|
| 507 |
+
per-period periodogram scores. ``n_valid`` is the periodicity-strength
|
| 508 |
+
signal the significance gate reads.
|
| 509 |
+
"""
|
| 510 |
+
B, L = x_fp32.shape
|
| 511 |
+
if self.periodogram_off:
|
| 512 |
+
# Control: no period detection → phase encoding zeros out, phase
|
| 513 |
+
# machinery is inert. Tests whether the conv backbone matches with
|
| 514 |
+
# a phase-free (recency-only) decoder.
|
| 515 |
+
z = torch.zeros(B, self.K, dtype=torch.long, device=x_fp32.device)
|
| 516 |
+
return (z, torch.zeros(B, dtype=torch.long, device=x_fp32.device),
|
| 517 |
+
torch.zeros(B, self.K, device=x_fp32.device))
|
| 518 |
+
with torch.no_grad():
|
| 519 |
+
periods, scores, n_valid = significant_periods(
|
| 520 |
+
x_fp32,
|
| 521 |
+
min_period=2,
|
| 522 |
+
max_period=L // 2,
|
| 523 |
+
top_k=self.K,
|
| 524 |
+
significance_alpha=self.significance_alpha,
|
| 525 |
+
)
|
| 526 |
+
periods = periods.long()
|
| 527 |
+
if self.min_cycles > 0:
|
| 528 |
+
# "Do no harm": only TRUST a period with >= min_cycles full
|
| 529 |
+
# cycles in the window (period <= L/min_cycles). Periods too long
|
| 530 |
+
# to be reliably estimated (e.g. an 8640-sample daily cycle in a
|
| 531 |
+
# 2048 window) are zeroed → the significance gate routes those
|
| 532 |
+
# series to the recency/local path instead of mis-locking phase.
|
| 533 |
+
max_p = L // self.min_cycles
|
| 534 |
+
keep = (periods > 0) & (periods <= max_p)
|
| 535 |
+
periods = torch.where(keep, periods, torch.zeros_like(periods))
|
| 536 |
+
n_valid = keep.sum(dim=1).long()
|
| 537 |
+
return periods, n_valid.long(), scores.float()
|
| 538 |
+
|
| 539 |
+
def _pool(self, h: torch.Tensor) -> torch.Tensor:
|
| 540 |
+
"""Pool encoder output to a fixed-size summary.
|
| 541 |
+
|
| 542 |
+
h: (B, L, D)
|
| 543 |
+
Returns: (B, pool_dim)
|
| 544 |
+
"""
|
| 545 |
+
if self.pool_kind == "mean":
|
| 546 |
+
return h.mean(dim=1)
|
| 547 |
+
if self.pool_kind == "last":
|
| 548 |
+
return h[:, -1, :]
|
| 549 |
+
# mean_last:
|
| 550 |
+
return torch.cat([h.mean(dim=1), h[:, -1, :]], dim=-1)
|
| 551 |
+
|
| 552 |
+
def _scatter_profile(
|
| 553 |
+
self, h: torch.Tensor, bins: torch.Tensor, nb: int,
|
| 554 |
+
weight: torch.Tensor | None = None,
|
| 555 |
+
) -> torch.Tensor:
|
| 556 |
+
"""Weighted scatter-mean (+ optional per-bin variance) of h into nb bins.
|
| 557 |
+
|
| 558 |
+
h: (B, L, D)
|
| 559 |
+
bins: (B, L) int in [0, nb)
|
| 560 |
+
weight: (B, L) non-negative, or None for uniform.
|
| 561 |
+
Returns (B, nb, stat_mult·D): per-bin mean, then per-bin variance if
|
| 562 |
+
``phase_stats == 'mean_var'``. Empty bins → global mean
|
| 563 |
+
(and zero variance).
|
| 564 |
+
"""
|
| 565 |
+
B, L, D = h.shape
|
| 566 |
+
oh = F.one_hot(bins, nb).to(h.dtype) # (B,L,nb)
|
| 567 |
+
if weight is not None:
|
| 568 |
+
oh = oh * weight.unsqueeze(-1)
|
| 569 |
+
cnt = oh.sum(dim=1).unsqueeze(-1) # (B,nb,1)
|
| 570 |
+
denom = cnt.clamp(min=1e-6)
|
| 571 |
+
mean = torch.bmm(oh.transpose(1, 2), h) / denom # (B,nb,D)
|
| 572 |
+
gmean = h.mean(dim=1, keepdim=True) # (B,1,D)
|
| 573 |
+
empty = cnt <= 0
|
| 574 |
+
mean = torch.where(empty, gmean.expand(B, nb, D), mean)
|
| 575 |
+
if self.phase_stats == "mean_var":
|
| 576 |
+
sq = torch.bmm(oh.transpose(1, 2), h * h) / denom # E[h²]
|
| 577 |
+
var = (sq - mean * mean).clamp(min=0.0)
|
| 578 |
+
var = torch.where(empty, torch.zeros_like(var), var)
|
| 579 |
+
return torch.cat([mean, var], dim=-1) # (B,nb,2D)
|
| 580 |
+
return mean # (B,nb,D)
|
| 581 |
+
|
| 582 |
+
def _recency_weight(self, L: int, device) -> torch.Tensor | None:
|
| 583 |
+
"""exp recency weight over context positions: recent positions weigh
|
| 584 |
+
more, so the folded profile tracks the CURRENT regime's waveform rather
|
| 585 |
+
than the window average. None if tau<=0 (uniform)."""
|
| 586 |
+
if self.phase_recency_tau <= 0.0:
|
| 587 |
+
return None
|
| 588 |
+
t = torch.arange(L, device=device).float()
|
| 589 |
+
dist = (L - 1 - t) / L # 0 at now
|
| 590 |
+
return torch.exp(-dist / self.phase_recency_tau).view(1, L)
|
| 591 |
+
|
| 592 |
+
def _phase_profile(
|
| 593 |
+
self, h: torch.Tensor, periods: torch.Tensor,
|
| 594 |
+
) -> torch.Tensor:
|
| 595 |
+
"""Period-fold the encoder output into per-phase profiles.
|
| 596 |
+
|
| 597 |
+
Returns (B, K, n_bins, stat_mult·D). Each detected period p_k folds
|
| 598 |
+
the sequence into n_bins phase bins; per bin we keep the (recency-
|
| 599 |
+
weighted) mean [and variance]. Empty bins → global mean.
|
| 600 |
+
"""
|
| 601 |
+
B, L, D = h.shape
|
| 602 |
+
K, nb = self.K, self.phase_bins
|
| 603 |
+
t = torch.arange(L, device=h.device).view(1, L).float() # (1,L)
|
| 604 |
+
p_safe = periods.clamp(min=1).float() # (B,K)
|
| 605 |
+
weight = self._recency_weight(L, h.device)
|
| 606 |
+
if weight is not None:
|
| 607 |
+
weight = weight.expand(B, L)
|
| 608 |
+
profs = []
|
| 609 |
+
for k in range(K):
|
| 610 |
+
frac = (t % p_safe[:, k:k + 1]) / p_safe[:, k:k + 1] # (B,L)
|
| 611 |
+
bins = torch.clamp((frac * nb).long(), max=nb - 1) # (B,L)
|
| 612 |
+
profs.append(self._scatter_profile(h, bins, nb, weight))
|
| 613 |
+
return torch.stack(profs, dim=1) # (B,K,nb,S·D)
|
| 614 |
+
|
| 615 |
+
def _gather_phase(
|
| 616 |
+
self, prof: torch.Tensor, fut_pos: torch.Tensor,
|
| 617 |
+
periods: torch.Tensor, weight: torch.Tensor | None = None,
|
| 618 |
+
) -> torch.Tensor:
|
| 619 |
+
"""Gather each future query's matching phase bin per period, mix → D.
|
| 620 |
+
|
| 621 |
+
prof: (B, K, n_bins, S·D); fut_pos: (B, H); periods: (B, K).
|
| 622 |
+
weight: optional (B,K) per-period reliability weight (period_trust).
|
| 623 |
+
Returns (B, H, D).
|
| 624 |
+
"""
|
| 625 |
+
B, K, nb, SD = prof.shape
|
| 626 |
+
H = fut_pos.shape[1]
|
| 627 |
+
p_safe = periods.clamp(min=1).float().view(B, 1, K) # (B,1,K)
|
| 628 |
+
frac = (fut_pos.unsqueeze(-1).float() % p_safe) / p_safe # (B,H,K)
|
| 629 |
+
fbins = torch.clamp((frac * nb).long(), max=nb - 1) # (B,H,K)
|
| 630 |
+
idx = fbins.permute(0, 2, 1).unsqueeze(-1).expand(B, K, H, SD)
|
| 631 |
+
gathered = torch.gather(prof, 2, idx) # (B,K,H,S·D)
|
| 632 |
+
if weight is not None:
|
| 633 |
+
gathered = gathered * weight.view(B, K, 1, 1).to(gathered.dtype)
|
| 634 |
+
gathered = gathered.permute(0, 2, 1, 3).reshape(B, H, K * SD)
|
| 635 |
+
return self.phase_mix(gathered) # (B,H,D)
|
| 636 |
+
|
| 637 |
+
def _period_trust_weights(
|
| 638 |
+
self, periods: torch.Tensor, scores: torch.Tensor,
|
| 639 |
+
) -> torch.Tensor:
|
| 640 |
+
"""Hyperparameter-free per-period reliability weight w_k∈[0,1] (B,K).
|
| 641 |
+
|
| 642 |
+
ln-coverage = ln(L/period) (data/structure-determined); for 'full' also
|
| 643 |
+
the significance margin ln(s_k/t_alpha) (s_k = the periodogram score,
|
| 644 |
+
t_alpha = data-determined Bonferroni threshold). The sigmoid crossover
|
| 645 |
+
is LEARNED (the linear layer's weights and bias), not a hand-set
|
| 646 |
+
threshold. 0 on rejected slots.
|
| 647 |
+
"""
|
| 648 |
+
valid = periods > 0
|
| 649 |
+
logcov = torch.log(float(self.L) / periods.clamp(min=1).float()) # (B,K)
|
| 650 |
+
if self.period_trust == "coverage":
|
| 651 |
+
feat = logcov.unsqueeze(-1) # (B,K,1)
|
| 652 |
+
else:
|
| 653 |
+
margin = torch.log(scores.clamp(min=1e-12) / self._t_alpha) # >=0 for survivors
|
| 654 |
+
feat = torch.stack([margin, logcov], dim=-1) # (B,K,2)
|
| 655 |
+
w = torch.sigmoid(self.pt(feat.to(self.pt.weight.dtype))).squeeze(-1)
|
| 656 |
+
return torch.where(valid, w, torch.zeros_like(w))
|
| 657 |
+
|
| 658 |
+
def _recency_feat(self, h: torch.Tensor) -> torch.Tensor:
|
| 659 |
+
"""Recency-binned profile: bin context positions by
|
| 660 |
+
log-distance-from-now and pool. Always valid (no period needed);
|
| 661 |
+
the aperiodic content path. Flattened to a single (B, D) descriptor
|
| 662 |
+
(broadcast to all horizons; the query's own PE carries how-far-ahead).
|
| 663 |
+
"""
|
| 664 |
+
B, L, D = h.shape
|
| 665 |
+
rb = self.recency_bins
|
| 666 |
+
t = torch.arange(L, device=h.device).view(1, L).float().expand(B, L)
|
| 667 |
+
dist = (L - 1 - t).clamp(min=0.0) # 0=now
|
| 668 |
+
frac = torch.log1p(dist) / math.log1p(float(L - 1) + 1e-9)
|
| 669 |
+
bins = torch.clamp((frac * rb).long(), max=rb - 1) # (B,L)
|
| 670 |
+
prof = self._scatter_profile(h, bins, rb, None) # (B,rb,S·D)
|
| 671 |
+
return self.recency_mix(prof.reshape(B, rb * self.stat_mult * D))
|
| 672 |
+
|
| 673 |
+
def _cross_cycle_profile(
|
| 674 |
+
self, h: torch.Tensor, periods: torch.Tensor,
|
| 675 |
+
) -> torch.Tensor:
|
| 676 |
+
"""Cross-cycle conv, true ragged form.
|
| 677 |
+
|
| 678 |
+
For the dominant period p0, fold the sequence into a
|
| 679 |
+
[cycles-back-from-now × phase] grid (B, nc, nb, D) by scatter-mean,
|
| 680 |
+
then convolve ACROSS the cycle axis at fixed phase (depthwise conv1d
|
| 681 |
+
over nc), modelling how each phase evolves cycle-to-cycle ("every
|
| 682 |
+
Monday 9am, trending up"). Read out the most-recent cycle (post-conv,
|
| 683 |
+
so it has seen the trend). Returns a (B, nb, D) phase profile the
|
| 684 |
+
decoder gathers by its own phase.
|
| 685 |
+
|
| 686 |
+
Per-sample period handled like phase-binning: phase resampled to nb
|
| 687 |
+
fixed bins; cycles-back clamped to nc (older cycles fold into the
|
| 688 |
+
oldest slot). Attention-free, fixed-shape, batchable.
|
| 689 |
+
"""
|
| 690 |
+
B, L, D = h.shape
|
| 691 |
+
nb, nc = self.cc_bins, self.cc_cycles
|
| 692 |
+
t = torch.arange(L, device=h.device).view(1, L).float() # (1,L)
|
| 693 |
+
p0 = periods[:, :1].clamp(min=1).float() # (B,1)
|
| 694 |
+
pbin = torch.clamp(((t % p0) / p0 * nb).long(), max=nb - 1) # (B,L)
|
| 695 |
+
cyc = torch.clamp(((L - 1 - t) // p0).long(), max=nc - 1) # (B,L) 0=now
|
| 696 |
+
comb = (cyc * nb + pbin).clamp(min=0, max=nc * nb - 1) # (B,L)
|
| 697 |
+
oh = F.one_hot(comb, nc * nb).to(h.dtype) # (B,L,nc·nb)
|
| 698 |
+
cnt = oh.sum(dim=1).unsqueeze(-1).clamp(min=1e-6)
|
| 699 |
+
grid = (torch.bmm(oh.transpose(1, 2), h) / cnt).view(B, nc, nb, D)
|
| 700 |
+
# conv across cycles (nc) at fixed phase, per channel.
|
| 701 |
+
x = grid.permute(0, 2, 3, 1).reshape(B * nb, D, nc) # (B·nb, D, nc)
|
| 702 |
+
x = self.cc_conv(x).reshape(B, nb, D, nc)
|
| 703 |
+
return x[..., 0] # most-recent cycle (B,nb,D)
|
| 704 |
+
|
| 705 |
+
@staticmethod
|
| 706 |
+
def _prefix_integral(
|
| 707 |
+
f: torch.Tensor, Csum: torch.Tensor, xpad: torch.Tensor, L: int,
|
| 708 |
+
) -> torch.Tensor:
|
| 709 |
+
"""Integral of piecewise-constant x from 0 to fractional position f.
|
| 710 |
+
f: (B,M) in native units. Csum: (B,L+1) prefix sums; xpad: (B,L+1)."""
|
| 711 |
+
fc = f.clamp(0.0, float(L))
|
| 712 |
+
k = torch.floor(fc).long()
|
| 713 |
+
rem = (fc - k.float()).to(Csum.dtype)
|
| 714 |
+
return torch.gather(Csum, 1, k) + rem * torch.gather(xpad, 1, k)
|
| 715 |
+
|
| 716 |
+
def _resolution_adapt(
|
| 717 |
+
self, x: torch.Tensor, periods: torch.Tensor, H: int,
|
| 718 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 719 |
+
"""Resolution adaptation (the TinyCast premise; zero params).
|
| 720 |
+
|
| 721 |
+
Resample the context onto a canonical-period grid so the fixed dilation
|
| 722 |
+
schedule spans consistent CYCLE-fractions across all sampling rates: the
|
| 723 |
+
dominant detected period is warped to ``res_period_target`` samples/cycle.
|
| 724 |
+
All detected periods scale by the same ratio; the future native horizon
|
| 725 |
+
is queried at its canonical-mapped position (decoder is position-
|
| 726 |
+
parameterized, so outputs are native values and no output resampling
|
| 727 |
+
is needed).
|
| 728 |
+
|
| 729 |
+
Returns (x_canon (B,L), periods_canon (B,K), fut_pos_canon (B,H) float).
|
| 730 |
+
Aperiodic series (dominant period 0) pass through unchanged (r=1).
|
| 731 |
+
"""
|
| 732 |
+
B, L = x.shape
|
| 733 |
+
pt = float(self.res_period_target)
|
| 734 |
+
p0 = periods[:, 0].float() # (B,) dominant
|
| 735 |
+
r = torch.where(p0 > 0, pt / p0.clamp(min=1.0), torch.ones_like(p0))
|
| 736 |
+
# res_r_max=1.0 → downsample-only (high-freq squeezed to canonical;
|
| 737 |
+
# low-freq left at native, no history truncation / no Δ blow-up).
|
| 738 |
+
r = r.clamp(0.05, self.res_r_max).view(B, 1) # canonical per native
|
| 739 |
+
j = torch.arange(L, device=x.device).view(1, L).float() # canonical idx 0..L-1
|
| 740 |
+
# native time (center) for canonical index j; now = most recent canonical.
|
| 741 |
+
t = (L - 1) - ((L - 1) - j) / r # (B,L), <0 = pre-context
|
| 742 |
+
# Upsample/identity (r>=1): linear interpolation (true pass-through at
|
| 743 |
+
# r=1). Downsample (r<1): area-average over the native window w=1/r,
|
| 744 |
+
# ANTI-ALIASED (averages the whole window, not 2 endpoints). Zero params.
|
| 745 |
+
t0 = torch.floor(t)
|
| 746 |
+
frac = (t - t0).to(x.dtype)
|
| 747 |
+
x_lin = (torch.gather(x, 1, t0.clamp(0, L - 1).long()) * (1.0 - frac)
|
| 748 |
+
+ torch.gather(x, 1, (t0 + 1).clamp(0, L - 1).long()) * frac)
|
| 749 |
+
w = 1.0 / r # (B,1) native window width
|
| 750 |
+
lo, hi = t - w / 2.0, t + w / 2.0
|
| 751 |
+
Csum = F.pad(x.cumsum(dim=1), (1, 0)) # (B,L+1): Csum[k]=Σ x[:k]
|
| 752 |
+
xpad = F.pad(x, (0, 1)) # (B,L+1): x[L]=0
|
| 753 |
+
denom = (hi.clamp(0.0, L) - lo.clamp(0.0, L)).clamp(min=1e-6)
|
| 754 |
+
x_area = (self._prefix_integral(hi, Csum, xpad, L)
|
| 755 |
+
- self._prefix_integral(lo, Csum, xpad, L)) / denom
|
| 756 |
+
x_canon = torch.where(r < 1.0, x_area, x_lin) # anti-alias only on downsample
|
| 757 |
+
x_canon = x_canon * (t >= 0).to(x.dtype) # mask pre-context → 0
|
| 758 |
+
periods_canon = torch.round(periods.float() * r).long()
|
| 759 |
+
periods_canon = torch.where(
|
| 760 |
+
periods > 0, periods_canon.clamp(min=2), torch.zeros_like(periods),
|
| 761 |
+
)
|
| 762 |
+
h_steps = torch.arange(1, H + 1, device=x.device).view(1, H).float()
|
| 763 |
+
fut_pos_canon = (L - 1) + h_steps * r # (B,H) canonical
|
| 764 |
+
return x_canon, periods_canon, fut_pos_canon
|
| 765 |
+
|
| 766 |
+
def _seasonal_naive(
|
| 767 |
+
self, x: torch.Tensor, fut_pos: torch.Tensor, periods: torch.Tensor,
|
| 768 |
+
) -> torch.Tensor:
|
| 769 |
+
"""Value-space seasonal-naive baseline (zero params).
|
| 770 |
+
|
| 771 |
+
Fold the (normalized) input x by the dominant period into phase bins,
|
| 772 |
+
take the per-phase mean VALUE, and gather the bin matching each future
|
| 773 |
+
query's phase. The network then learns only the residual on top of this
|
| 774 |
+
baseline: a target reframe, not added model complexity. Aperiodic /
|
| 775 |
+
empty-bin → fall back to the context mean (persistence-of-level).
|
| 776 |
+
|
| 777 |
+
x: (B, L) fut_pos: (B, H) periods: (B, K) → (B, H)
|
| 778 |
+
"""
|
| 779 |
+
B, L = x.shape
|
| 780 |
+
nb = self.phase_bins if self.phase_bins > 0 else 16
|
| 781 |
+
t = torch.arange(L, device=x.device).view(1, L).float()
|
| 782 |
+
p0 = periods[:, :1].clamp(min=1).float() # (B,1) dominant
|
| 783 |
+
pbin = torch.clamp(((t % p0) / p0 * nb).long(), max=nb - 1) # (B,L)
|
| 784 |
+
oh = F.one_hot(pbin, nb).to(x.dtype) # (B,L,nb)
|
| 785 |
+
cnt = oh.sum(dim=1) # (B,nb)
|
| 786 |
+
base = torch.bmm(oh.transpose(1, 2), x.unsqueeze(-1)).squeeze(-1) # (B,nb)
|
| 787 |
+
gmean = x.mean(dim=1, keepdim=True) # (B,1)
|
| 788 |
+
base = torch.where(cnt > 0, base / cnt.clamp(min=1.0), gmean.expand(B, nb))
|
| 789 |
+
fb = torch.clamp((fut_pos.float() % p0) / p0 * nb, max=nb - 1).long() # (B,H)
|
| 790 |
+
return torch.gather(base, 1, fb) # (B,H)
|
| 791 |
+
|
| 792 |
+
def _super_naive(
|
| 793 |
+
self, x: torch.Tensor, fut_pos: torch.Tensor, periods: torch.Tensor,
|
| 794 |
+
) -> torch.Tensor:
|
| 795 |
+
"""Multi-seasonal "super-naive" baseline (zero params).
|
| 796 |
+
|
| 797 |
+
Greedy additive decomposition over ALL significant periods: start from
|
| 798 |
+
the context mean, then for each significant period (strongest first)
|
| 799 |
+
fold the running residual into per-phase means, subtract it (deflate),
|
| 800 |
+
and accumulate that component's value at the future phase. Result:
|
| 801 |
+
baseline(L+h) = mean + Σ_k s_k[phase_k(L+h)], the genuine multi-period
|
| 802 |
+
seasonal-naive forecast. Non-significant periods (p=0) contribute zero.
|
| 803 |
+
|
| 804 |
+
x: (B, L) fut_pos: (B, H) periods: (B, K) → (B, H)
|
| 805 |
+
"""
|
| 806 |
+
B, L = x.shape
|
| 807 |
+
H = fut_pos.shape[1]
|
| 808 |
+
nb = self.phase_bins if self.phase_bins > 0 else 16
|
| 809 |
+
t = torch.arange(L, device=x.device).view(1, L).float()
|
| 810 |
+
if self.residual_trend:
|
| 811 |
+
# Level term = linear trend (closed-form LS), extrapolated forward.
|
| 812 |
+
# baseline = trend + seasonal, the classical decomposition.
|
| 813 |
+
tc = t - t.mean() # centered (1,L)
|
| 814 |
+
xc = x - x.mean(dim=1, keepdim=True) # (B,L)
|
| 815 |
+
slope = (tc * xc).sum(1, keepdim=True) / (tc * tc).sum().clamp(min=1.0)
|
| 816 |
+
intercept = x.mean(dim=1, keepdim=True) # value at centered t=0
|
| 817 |
+
tmean = t.mean()
|
| 818 |
+
trend_ctx = intercept + slope * (t - tmean) # (B,L)
|
| 819 |
+
r = x - trend_ctx # de-trended residual
|
| 820 |
+
baseline = intercept + slope * (fut_pos.float() - tmean) # (B,H) trend extrap
|
| 821 |
+
else:
|
| 822 |
+
mean = x.mean(dim=1, keepdim=True) # (B,1)
|
| 823 |
+
r = x - mean # residual
|
| 824 |
+
baseline = mean.expand(B, H).clone() # (B,H)
|
| 825 |
+
for k in range(self.K):
|
| 826 |
+
pk = periods[:, k:k + 1].float() # (B,1), 0 if not sig
|
| 827 |
+
sig = (pk > 0).to(x.dtype) # (B,1)
|
| 828 |
+
pks = pk.clamp(min=1.0)
|
| 829 |
+
pbin = torch.clamp((t % pks) / pks * nb, max=nb - 1).long() # (B,L)
|
| 830 |
+
oh = F.one_hot(pbin, nb).to(x.dtype) # (B,L,nb)
|
| 831 |
+
cnt = oh.sum(dim=1).clamp(min=1.0) # (B,nb)
|
| 832 |
+
s_k = torch.bmm(oh.transpose(1, 2), r.unsqueeze(-1)).squeeze(-1) / cnt
|
| 833 |
+
s_k = s_k * sig # (B,nb), zero if not sig
|
| 834 |
+
r = r - torch.gather(s_k, 1, pbin) # deflate
|
| 835 |
+
fb = torch.clamp((fut_pos.float() % pks) / pks * nb, max=nb - 1).long()
|
| 836 |
+
baseline = baseline + torch.gather(s_k, 1, fb) # (B,H)
|
| 837 |
+
return baseline
|
| 838 |
+
|
| 839 |
+
def _gather_cc(
|
| 840 |
+
self, cc_prof: torch.Tensor, fut_pos: torch.Tensor,
|
| 841 |
+
periods: torch.Tensor,
|
| 842 |
+
) -> torch.Tensor:
|
| 843 |
+
"""Gather each future query's matching phase bin from the cross-cycle
|
| 844 |
+
profile (dominant period), mix → D. cc_prof: (B,nb,D)."""
|
| 845 |
+
B, nb, D = cc_prof.shape
|
| 846 |
+
H = fut_pos.shape[1]
|
| 847 |
+
p0 = periods[:, :1].clamp(min=1).float() # (B,1)
|
| 848 |
+
fb = torch.clamp((fut_pos.float() % p0) / p0 * nb, max=nb - 1).long()
|
| 849 |
+
gathered = torch.gather(cc_prof, 1, fb.unsqueeze(-1).expand(B, H, D))
|
| 850 |
+
return self.cc_mix(gathered) # (B,H,D)
|
| 851 |
+
|
| 852 |
+
# ---- forward ----------------------------------------------------------
|
| 853 |
+
|
| 854 |
+
def _local_anchor_channels(
|
| 855 |
+
self, x: torch.Tensor, scale_factor: torch.Tensor | float | None,
|
| 856 |
+
) -> torch.Tensor:
|
| 857 |
+
"""Two causal local-statistics channels exposing the recent level/scale to
|
| 858 |
+
the encoder (the gradient-connected re-anchoring signal WindowMinMax lacks):
|
| 859 |
+
ch1 = (x_t - m_t) / (s_t + eps) local-scale residual (a causal z-score)
|
| 860 |
+
ch2 = log(s_t + eps) log local scale (global normed range ~= 1)
|
| 861 |
+
m_t, s_t = causal boxcar mean / std over a trailing window w ~ one canonical
|
| 862 |
+
period round(base_seasonality / scale_factor), clamped [8, L//4], fallback 64.
|
| 863 |
+
Vectorized via cumsum + per-sample-window gather (O(L), no python loop).
|
| 864 |
+
"""
|
| 865 |
+
B, L = x.shape
|
| 866 |
+
device = x.device
|
| 867 |
+
if scale_factor is not None:
|
| 868 |
+
sf = (scale_factor if torch.is_tensor(scale_factor)
|
| 869 |
+
else x.new_tensor(scale_factor)).reshape(-1).float()
|
| 870 |
+
if sf.numel() == 1:
|
| 871 |
+
sf = sf.expand(B)
|
| 872 |
+
w = (self.base_seasonality / sf.clamp(min=1e-3)).round().long()
|
| 873 |
+
w = w.clamp(min=8, max=max(8, L // 4))
|
| 874 |
+
else:
|
| 875 |
+
w = torch.full((B,), 64, device=device, dtype=torch.long)
|
| 876 |
+
# Center by the per-series mean before a FP32 cumsum. The two-pass variance
|
| 877 |
+
# (E[x^2]-E[x]^2) over a length-L cumsum otherwise suffers catastrophic
|
| 878 |
+
# cancellation on long flat/sparse regions; variance is shift-invariant, so
|
| 879 |
+
# centering changes nothing but keeps the cumsum magnitudes small enough that
|
| 880 |
+
# fp32 stays accurate there, at no extra memory.
|
| 881 |
+
xf = x.float()
|
| 882 |
+
xc = xf - xf.mean(dim=1, keepdim=True)
|
| 883 |
+
cs = F.pad(torch.cumsum(xc, dim=1), (1, 0)) # (B, L+1), cs[:,0]=0
|
| 884 |
+
cs2 = F.pad(torch.cumsum(xc * xc, dim=1), (1, 0))
|
| 885 |
+
t = torch.arange(L, device=device).view(1, L).expand(B, L)
|
| 886 |
+
lo = (t - w.view(B, 1) + 1).clamp(min=0) # trailing-window start
|
| 887 |
+
cnt = (t - lo + 1).float() # window length (>= 1)
|
| 888 |
+
sum_x = cs.gather(1, t + 1) - cs.gather(1, lo)
|
| 889 |
+
sum_x2 = cs2.gather(1, t + 1) - cs2.gather(1, lo)
|
| 890 |
+
m = sum_x / cnt # centered local mean
|
| 891 |
+
s = (sum_x2 / cnt - m * m).clamp(min=0.0).sqrt() # local std (shift-invariant)
|
| 892 |
+
eps = 1e-4 # floor vs the unit normed range -> flat regions give ch1 ~ 0, no blowup
|
| 893 |
+
ch1 = (xc - m) / (s + eps) # = (x - local mean)/(s+eps)
|
| 894 |
+
ch2 = torch.log(s + eps)
|
| 895 |
+
return torch.stack([ch1, ch2], dim=-1).to(x.dtype) # (B, L, 2)
|
| 896 |
+
|
| 897 |
+
def forward(
|
| 898 |
+
self,
|
| 899 |
+
x_normed: torch.Tensor,
|
| 900 |
+
nan_mask: torch.Tensor | None = None,
|
| 901 |
+
scale_factor: torch.Tensor | float | None = None,
|
| 902 |
+
horizon: int | None = None,
|
| 903 |
+
) -> torch.Tensor:
|
| 904 |
+
# observed-mask (1=observed, 0=missing) for the missing-value channel.
|
| 905 |
+
# res_adaptive is rejected with missing_channel (see __init__), so this
|
| 906 |
+
# mask stays aligned with x throughout.
|
| 907 |
+
obs_mask = None
|
| 908 |
+
if self.missing_channel and nan_mask is not None:
|
| 909 |
+
obs_mask = nan_mask[..., 0] if nan_mask.dim() == 3 else nan_mask # (B,L)
|
| 910 |
+
|
| 911 |
+
if x_normed.dim() == 3 and x_normed.shape[-1] > 1:
|
| 912 |
+
x = x_normed[..., 0]
|
| 913 |
+
elif x_normed.dim() == 3:
|
| 914 |
+
x = x_normed.squeeze(-1)
|
| 915 |
+
else:
|
| 916 |
+
x = x_normed # (B, L)
|
| 917 |
+
|
| 918 |
+
x = torch.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)
|
| 919 |
+
B, L = x.shape
|
| 920 |
+
assert L == self.L, f"context length mismatch: got {L}, expected {self.L}"
|
| 921 |
+
H = self.p_out if horizon is None else int(horizon)
|
| 922 |
+
device = x.device
|
| 923 |
+
|
| 924 |
+
# Period detection in fp32 (no grad).
|
| 925 |
+
periods, n_valid, scores = self._detect_periods(x.float()) # (B,K),(B,),(B,K)
|
| 926 |
+
|
| 927 |
+
# Resolution adaptation: warp context to a canonical cycle-resolution so
|
| 928 |
+
# the fixed dilations span consistent cycle-fractions across rates.
|
| 929 |
+
if self.res_adaptive:
|
| 930 |
+
x, periods, fut_pos = self._resolution_adapt(x, periods, H)
|
| 931 |
+
ctx_pos = torch.arange(L, device=device).view(1, L).expand(B, L)
|
| 932 |
+
else:
|
| 933 |
+
ctx_pos = torch.arange(L, device=device).view(1, L).expand(B, L)
|
| 934 |
+
fut_pos = torch.arange(L, L + H, device=device).view(1, H).expand(B, H)
|
| 935 |
+
with torch.amp.autocast(
|
| 936 |
+
device_type=device.type if x.is_cuda else "cpu", enabled=False,
|
| 937 |
+
):
|
| 938 |
+
ctx_pe = _positional_encoding(
|
| 939 |
+
ctx_pos, periods, L, n_harmonics=self.n_harmonics,
|
| 940 |
+
)
|
| 941 |
+
fut_pe = _positional_encoding(
|
| 942 |
+
fut_pos, periods, L, n_harmonics=self.n_harmonics,
|
| 943 |
+
)
|
| 944 |
+
ctx_pe = ctx_pe.to(x.dtype)
|
| 945 |
+
fut_pe = fut_pe.to(x.dtype)
|
| 946 |
+
|
| 947 |
+
# Per-period reliability weight (period_trust; off in the released config).
|
| 948 |
+
# Down-weight unreliable/spurious periods continuously. Applied to the
|
| 949 |
+
# phase-encoding channels here, and to the phase-binning gather + gate below.
|
| 950 |
+
ptw = None
|
| 951 |
+
if self.pt is not None:
|
| 952 |
+
ptw = self._period_trust_weights(periods, scores).to(x.dtype) # (B,K)
|
| 953 |
+
# phase channels are the first n_phase cols, laid out per period as
|
| 954 |
+
# n_harmonics*2 consecutive channels → repeat each w_k that many times.
|
| 955 |
+
rep = self.n_harmonics * 2
|
| 956 |
+
chan_w = ptw.repeat_interleave(rep, dim=1).view(B, 1, -1) # (B,1,n_phase)
|
| 957 |
+
np_ = chan_w.shape[-1]
|
| 958 |
+
ctx_pe = torch.cat([ctx_pe[..., :np_] * chan_w, ctx_pe[..., np_:]], dim=-1)
|
| 959 |
+
fut_pe = torch.cat([fut_pe[..., :np_] * chan_w, fut_pe[..., np_:]], dim=-1)
|
| 960 |
+
|
| 961 |
+
# Embed context.
|
| 962 |
+
if self.decompose_kernel > 0:
|
| 963 |
+
# moving-average series decomposition: moving-average trend +
|
| 964 |
+
# seasonal residual, fed as two channels.
|
| 965 |
+
k = self.decompose_kernel
|
| 966 |
+
pad = k // 2
|
| 967 |
+
xp = F.pad(x.unsqueeze(1), (pad, pad), mode="replicate") # (B,1,L+2pad)
|
| 968 |
+
trend = F.avg_pool1d(xp, kernel_size=k, stride=1)[..., :L].squeeze(1)
|
| 969 |
+
seasonal = x - trend
|
| 970 |
+
value_ch = torch.stack([trend, seasonal], dim=-1) # (B,L,2)
|
| 971 |
+
else:
|
| 972 |
+
value_ch = x.unsqueeze(-1) # (B,L,1)
|
| 973 |
+
if self.missing_channel and obs_mask is not None:
|
| 974 |
+
value_ch = torch.cat(
|
| 975 |
+
[value_ch, obs_mask.unsqueeze(-1).to(value_ch.dtype)], dim=-1
|
| 976 |
+
) # (B,L,nv+1)
|
| 977 |
+
if self.local_anchor:
|
| 978 |
+
value_ch = torch.cat(
|
| 979 |
+
[value_ch, self._local_anchor_channels(x, scale_factor)], dim=-1
|
| 980 |
+
) # (B,L,nv+2)
|
| 981 |
+
ctx_in = torch.cat([value_ch, ctx_pe], dim=-1) # (B,L,nv+n_pe)
|
| 982 |
+
h = self.in_proj(ctx_in) # (B, L, D)
|
| 983 |
+
|
| 984 |
+
# Dilated-conv encoder.
|
| 985 |
+
for block in self.encoder:
|
| 986 |
+
h = block(h)
|
| 987 |
+
|
| 988 |
+
# Per-horizon context feature: static pooled summary, broadcast to all H.
|
| 989 |
+
summary = self._pool(h) # (B, pool_dim)
|
| 990 |
+
ctx_feat = summary.unsqueeze(1).expand(B, H, -1) # (B, H, pool_dim)
|
| 991 |
+
|
| 992 |
+
# Decoder: per-horizon query from PE(L+h) + context feature
|
| 993 |
+
# [+ phase profile] [+ recency profile] [+ cross-cycle feat].
|
| 994 |
+
q_parts = [fut_pe, ctx_feat]
|
| 995 |
+
|
| 996 |
+
phase_feat = None
|
| 997 |
+
if self.phase_mix is not None:
|
| 998 |
+
prof = self._phase_profile(h, periods) # (B,K,nb,S·D)
|
| 999 |
+
phase_feat = self._gather_phase(prof, fut_pos, periods, weight=ptw) # (B,H,D)
|
| 1000 |
+
|
| 1001 |
+
rec_feat = None
|
| 1002 |
+
if self.recency_mix is not None:
|
| 1003 |
+
rec_feat = self._recency_feat(h).unsqueeze(1).expand(B, H, self.D)
|
| 1004 |
+
|
| 1005 |
+
if self.sig_gate and phase_feat is not None and rec_feat is not None:
|
| 1006 |
+
# Blend by periodicity strength: many significant periods → trust
|
| 1007 |
+
# the phase profile; few/none → lean on the recency profile. With
|
| 1008 |
+
# period_trust, use the soft Σw instead of the integer n_valid.
|
| 1009 |
+
strength = ptw.sum(dim=1) if ptw is not None else n_valid.float()
|
| 1010 |
+
g = (strength / float(self.K)).clamp(0.0, 1.0).view(B, 1, 1)
|
| 1011 |
+
q_parts.append(g * phase_feat + (1.0 - g) * rec_feat)
|
| 1012 |
+
else:
|
| 1013 |
+
if phase_feat is not None:
|
| 1014 |
+
q_parts.append(phase_feat)
|
| 1015 |
+
if rec_feat is not None:
|
| 1016 |
+
q_parts.append(rec_feat)
|
| 1017 |
+
|
| 1018 |
+
if self.cross_cycle:
|
| 1019 |
+
cc_prof = self._cross_cycle_profile(h, periods) # (B,nb,D)
|
| 1020 |
+
q_parts.append(self._gather_cc(cc_prof, fut_pos, periods)) # (B,H,D)
|
| 1021 |
+
|
| 1022 |
+
q_in = torch.cat(q_parts, dim=-1)
|
| 1023 |
+
q = self.query_proj(q_in) # (B, H, D)
|
| 1024 |
+
|
| 1025 |
+
if self.future_conv:
|
| 1026 |
+
# Horizon-axis evolving states from a causal conv over context-tail
|
| 1027 |
+
# + seasonal-naive fill, injected additively (fc_out zero-init =>
|
| 1028 |
+
# exact baseline at init). Gives the static-summary decoder the
|
| 1029 |
+
# per-horizon dynamics it lacks.
|
| 1030 |
+
fill = self._seasonal_naive(x, fut_pos, periods) # (B, H)
|
| 1031 |
+
fut_states = self._future_conv_states(h, fut_pe, fill) # (B, H, D)
|
| 1032 |
+
q = q + self.fc_out(fut_states)
|
| 1033 |
+
|
| 1034 |
+
for ffn, norm in zip(self.decoder_ffns, self.decoder_norms):
|
| 1035 |
+
q = _norm_fp32(norm, q + ffn(q))
|
| 1036 |
+
|
| 1037 |
+
if self.horizon_conv is not None:
|
| 1038 |
+
# Causal conv over the horizon axis: pad (k-1) on the left so step
|
| 1039 |
+
# h sees only h, h-1, …, h-(k-1): no future leakage, any H.
|
| 1040 |
+
qt = F.pad(q.transpose(1, 2), (self.horizon_kernel - 1, 0))
|
| 1041 |
+
hc = self.horizon_conv(qt).transpose(1, 2) # (B,H,D)
|
| 1042 |
+
q = _norm_fp32(self.horizon_norm, q + hc)
|
| 1043 |
+
|
| 1044 |
+
if self.hr_z is not None:
|
| 1045 |
+
# gated-recurrence decode-state: gated diagonal recurrence over the H axis.
|
| 1046 |
+
# Scans the query FEATURES only (no value feedback). Sequential
|
| 1047 |
+
# scan: the horizon is short, so this is cheap and stable.
|
| 1048 |
+
z = torch.sigmoid(self.hr_z(q)) # (B,H,D) update gate
|
| 1049 |
+
c = torch.tanh(self.hr_c(q)) # (B,H,D) candidate
|
| 1050 |
+
s = torch.zeros(B, self.D, dtype=q.dtype, device=q.device)
|
| 1051 |
+
states = []
|
| 1052 |
+
for t in range(q.shape[1]):
|
| 1053 |
+
s = (1.0 - z[:, t]) * s + z[:, t] * c[:, t]
|
| 1054 |
+
states.append(s)
|
| 1055 |
+
hstate = torch.stack(states, dim=1) # (B,H,D)
|
| 1056 |
+
q = _norm_fp32(self.hr_norm, q + self.hr_o(hstate)) # hr_o zero-init
|
| 1057 |
+
|
| 1058 |
+
y = self.out_proj(q) # (B, H, Q)
|
| 1059 |
+
|
| 1060 |
+
|
| 1061 |
+
if self.residual_naive:
|
| 1062 |
+
# Learn the residual over a (multi-)seasonal-naive baseline.
|
| 1063 |
+
if self.residual_multi:
|
| 1064 |
+
baseline = self._super_naive(x, fut_pos, periods) # (B, H)
|
| 1065 |
+
else:
|
| 1066 |
+
baseline = self._seasonal_naive(x, fut_pos, periods) # (B, H)
|
| 1067 |
+
y = y + baseline.unsqueeze(-1)
|
| 1068 |
+
|
| 1069 |
+
return y
|
| 1070 |
+
|
tinycast/checkpoint.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Weight loading for TinyCast.
|
| 2 |
+
|
| 3 |
+
The released weights are a ``model.safetensors`` + a ``config.json`` (the
|
| 4 |
+
architecture config). ``load_checkpoint`` (aliased ``load_model``) handles the weight-tied
|
| 5 |
+
FFN sharing: the file stores each unique parameter storage once (the true
|
| 6 |
+
146,505-parameter footprint, ~0.6 MB) and restores the sharing on load.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Optional, Tuple
|
| 14 |
+
|
| 15 |
+
from .config import TinyCastConfig
|
| 16 |
+
from .model import TinyCastForPrediction
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def load_checkpoint(
|
| 20 |
+
weights_path: str,
|
| 21 |
+
config_path: Optional[str] = None,
|
| 22 |
+
) -> Tuple[TinyCastForPrediction, TinyCastConfig]:
|
| 23 |
+
"""Load TinyCastForPrediction from ``model.safetensors`` + ``config.json``.
|
| 24 |
+
|
| 25 |
+
``config_path`` defaults to a sibling ``config.json`` next to the weights.
|
| 26 |
+
"""
|
| 27 |
+
from safetensors.torch import load_model
|
| 28 |
+
|
| 29 |
+
st = Path(weights_path)
|
| 30 |
+
if config_path is None:
|
| 31 |
+
config_path = st.parent / "config.json"
|
| 32 |
+
with open(config_path) as f:
|
| 33 |
+
cfg_dict = json.load(f)
|
| 34 |
+
cfg = TinyCastConfig(**{
|
| 35 |
+
k: v for k, v in cfg_dict.items()
|
| 36 |
+
if k in TinyCastConfig.__dataclass_fields__
|
| 37 |
+
})
|
| 38 |
+
model = TinyCastForPrediction(cfg)
|
| 39 |
+
load_model(model, str(st))
|
| 40 |
+
model.eval()
|
| 41 |
+
return model, cfg
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# Explicit alias used by the notebook / README examples.
|
| 45 |
+
load_model = load_checkpoint
|
tinycast/config.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TinyCast model configuration.
|
| 2 |
+
|
| 3 |
+
Only the fields that shape the deployed model's construction / forward are
|
| 4 |
+
kept. Field defaults are the released model's values, so ``TinyCastConfig()`` alone
|
| 5 |
+
reconstructs the deployed architecture; loading from ``config.json`` overrides
|
| 6 |
+
them.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from dataclasses import asdict, dataclass, field
|
| 10 |
+
from typing import List
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class TinyCastConfig:
|
| 15 |
+
"""Configuration for the deployed TinyCast (dilated-conv) model."""
|
| 16 |
+
|
| 17 |
+
# --- input / output geometry -------------------------------------------
|
| 18 |
+
quantiles: List[float] = field(
|
| 19 |
+
default_factory=lambda: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
|
| 20 |
+
)
|
| 21 |
+
seq_len: int = 2048 # L: encoder context window
|
| 22 |
+
output_token_len: int = 48 # p: single-shot / AR-chunk horizon
|
| 23 |
+
|
| 24 |
+
# --- period detector (shared structural prior) -------------------------
|
| 25 |
+
top_k_periods: int = 4
|
| 26 |
+
significance_alpha: float = 0.05
|
| 27 |
+
n_harmonics: int = 1
|
| 28 |
+
|
| 29 |
+
# --- dilated-conv backbone ---------------------------------------------
|
| 30 |
+
conv_dim: int = 64
|
| 31 |
+
n_layers: int = 10
|
| 32 |
+
kernel_size: int = 3
|
| 33 |
+
ffn_mult: float = 1.0
|
| 34 |
+
pool_kind: str = "mean_last"
|
| 35 |
+
causal: bool = True
|
| 36 |
+
phase_bins: int = 16
|
| 37 |
+
decoder_depth: int = 1
|
| 38 |
+
separable_conv: bool = True
|
| 39 |
+
share_ffn: bool = True
|
| 40 |
+
future_conv: bool = True
|
| 41 |
+
future_conv_layers: int = 6
|
| 42 |
+
future_conv_seed: int = 128
|
| 43 |
+
|
| 44 |
+
@property
|
| 45 |
+
def num_quantiles(self) -> int:
|
| 46 |
+
return len(self.quantiles)
|
| 47 |
+
|
| 48 |
+
def to_dict(self) -> dict:
|
| 49 |
+
return asdict(self)
|
tinycast/corpus.py
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The canonical synthetic corpus: recipe, shard builder and verifier.
|
| 2 |
+
|
| 3 |
+
TinyCast's pretraining mix is GIFT-Eval-Pretrain and Chronos KernelSynth, taken
|
| 4 |
+
from their publishers, plus four synthetic shards. This module is the record of
|
| 5 |
+
how those four shards were made: four shards of 62,500 series at length 4,096,
|
| 6 |
+
families mixed 70/15/15 over the generators in :mod:`tinycast.synth`.
|
| 7 |
+
|
| 8 |
+
Everything that changes the generated bytes is a default in
|
| 9 |
+
:data:`CANONICAL_RECIPE` rather than an argument, so ``build_shard()`` with no
|
| 10 |
+
arguments reproduces published shard 0 and there is nothing to tune. Treat the
|
| 11 |
+
values in that dictionary as versioned data: changing one produces a different
|
| 12 |
+
corpus, not a differently configured build of the same one.
|
| 13 |
+
|
| 14 |
+
WHAT REPRODUCIBILITY MEANS HERE. The shards are published as files, so the
|
| 15 |
+
corpus can be downloaded and read without regenerating anything. Regeneration
|
| 16 |
+
reproduces the published float16 payload exactly on the stack it was generated
|
| 17 |
+
on: 0 of 327,680 values differ across 4 shards by 20 rows on an RTX 3090 with
|
| 18 |
+
torch 2.10.0+cu128 and driver 580.159.03. Reproduction on other GPUs, CUDA
|
| 19 |
+
versions or torch builds is untested, and the GP family runs through cuSOLVER,
|
| 20 |
+
so it is not safe to assume it carries over. :func:`verify_shard` is how you
|
| 21 |
+
find out for a given stack: it regenerates a prefix and reports how many
|
| 22 |
+
float16 values differ.
|
| 23 |
+
|
| 24 |
+
Verify against ``series.f16``, which is the payload, and not against
|
| 25 |
+
``series_mean.f32``. That sidecar is a float32 sum with heavy cancellation on
|
| 26 |
+
near-zero-mean rows, so its last bit moves with the numpy build while the f16
|
| 27 |
+
payload it feeds is unaffected. ``series_stdev.f32`` does not cancel.
|
| 28 |
+
|
| 29 |
+
CUDA IS REQUIRED, and is enforced rather than detected. The GP family samples
|
| 30 |
+
by dense Cholesky, and off CUDA that factorization and the normal draws feeding
|
| 31 |
+
it come from different generators, so the same seed gives different series: the
|
| 32 |
+
CPU path moves up to 182 of the 4,096 float32 values in a row. The published
|
| 33 |
+
shards came from the CUDA branch. A build with no GPU therefore fails here
|
| 34 |
+
instead of falling back and quietly producing a different corpus.
|
| 35 |
+
"""
|
| 36 |
+
from __future__ import annotations
|
| 37 |
+
|
| 38 |
+
import io
|
| 39 |
+
import json
|
| 40 |
+
import math
|
| 41 |
+
import os
|
| 42 |
+
import time
|
| 43 |
+
from pathlib import Path
|
| 44 |
+
from typing import Iterator, Optional, Tuple, Union
|
| 45 |
+
|
| 46 |
+
import numpy as np
|
| 47 |
+
|
| 48 |
+
from .synth import generate_gp, generate_spikes, generate_tsi
|
| 49 |
+
|
| 50 |
+
_F16_BYTES = 2
|
| 51 |
+
|
| 52 |
+
# ---------------------------------------------------------------------------
|
| 53 |
+
# The canonical recipe.
|
| 54 |
+
#
|
| 55 |
+
# Every entry is a default. A reviewer runs the build with no flags and gets the
|
| 56 |
+
# published corpus.
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
+
CANONICAL_RECIPE = {
|
| 59 |
+
"version": "tinycast-synth-4096/v1",
|
| 60 |
+
"n_shards": 4,
|
| 61 |
+
"series_per_shard": 62_500,
|
| 62 |
+
"length": 4096,
|
| 63 |
+
"mix": (0.70, 0.15, 0.15), # gp, spikes, tsi
|
| 64 |
+
"chunk": 2048, # series generated per call
|
| 65 |
+
"device": "cuda",
|
| 66 |
+
# Part of the data, not a memory knob. cuSOLVER factorizes a batch of one on
|
| 67 |
+
# a different code path from a real batch, and these covariances are close
|
| 68 |
+
# to singular (periodic and linear kernels with only 1e-6 jitter), so the
|
| 69 |
+
# factors differ by around 1e-8 and the published rows do not reproduce at
|
| 70 |
+
# batch=1. Batches of 2, 4, 8 and 32 all agree with each other and with the
|
| 71 |
+
# published data. Lowering this to fit a smaller GPU changes the corpus.
|
| 72 |
+
"gp_batch": 32,
|
| 73 |
+
"scale_factor_range": (0.05, 6.0), # log-uniform, per series
|
| 74 |
+
"verified_stack": "RTX 3090 / torch 2.10.0+cu128 / driver 580.159.03",
|
| 75 |
+
# Per-shard seed and family salts. The seed is 50,000 x shard, confirmed by
|
| 76 |
+
# reproducing all 62,500 scale factors of each shard bit for bit.
|
| 77 |
+
#
|
| 78 |
+
# The salts cannot be computed, which is why they are carried as data. They
|
| 79 |
+
# were a per-family hash of the family name taken under CPython's randomized
|
| 80 |
+
# string hashing, so every build process drew its own set and no two shards
|
| 81 |
+
# share a triple. Replacing them with a stable hash would make future builds
|
| 82 |
+
# reproducible while making the published shards unreproducible. They were
|
| 83 |
+
# recovered by brute force against the published bytes (997 candidates per
|
| 84 |
+
# family) and frozen here.
|
| 85 |
+
"shards": {
|
| 86 |
+
0: {"seed": 0, "salt": {"gp": 724, "spikes": 351, "tsi": 900}},
|
| 87 |
+
1: {"seed": 50_000, "salt": {"gp": 23, "spikes": 431, "tsi": 840}},
|
| 88 |
+
2: {"seed": 100_000, "salt": {"gp": 762, "spikes": 505, "tsi": 641}},
|
| 89 |
+
3: {"seed": 150_000, "salt": {"gp": 691, "spikes": 491, "tsi": 745}},
|
| 90 |
+
},
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
FAMILIES = ("gp", "spikes", "tsi")
|
| 94 |
+
|
| 95 |
+
# Files a shard directory holds. The first six are the payload and its indices;
|
| 96 |
+
# the last two attribute each row to a family.
|
| 97 |
+
CACHE_FILES = (
|
| 98 |
+
"series.f16", "offsets.npy", "lengths.npy", "scale_factors.f32",
|
| 99 |
+
"series_mean.f32", "series_stdev.f32",
|
| 100 |
+
)
|
| 101 |
+
FAMILY_FILES = ("dataset_id.u16", "dataset_names.json")
|
| 102 |
+
|
| 103 |
+
DEFAULT_OUT_DIR = "synth4096_{shard}"
|
| 104 |
+
|
| 105 |
+
# Rows compared by verify_shard. Twenty is what the published fidelity statement
|
| 106 |
+
# was measured over, and at 4,096 samples a row it is already 81,920 values.
|
| 107 |
+
DEFAULT_VERIFY_ROWS = 20
|
| 108 |
+
DEFAULT_VERIFY_SCALE_FACTORS = 4096
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# ---------------------------------------------------------------------------
|
| 112 |
+
# Recipe lookups
|
| 113 |
+
# ---------------------------------------------------------------------------
|
| 114 |
+
|
| 115 |
+
def _shard_record(shard: int) -> dict:
|
| 116 |
+
try:
|
| 117 |
+
return CANONICAL_RECIPE["shards"][int(shard)]
|
| 118 |
+
except KeyError:
|
| 119 |
+
raise KeyError(
|
| 120 |
+
f"shard {shard} is not in the published corpus, which covers shards "
|
| 121 |
+
f"{sorted(CANONICAL_RECIPE['shards'])}."
|
| 122 |
+
) from None
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def shard_seed(shard: int) -> int:
|
| 126 |
+
"""Base seed for a shard. The scale-factor stream is drawn from it."""
|
| 127 |
+
return int(_shard_record(shard)["seed"])
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def family_salt(family: str, shard: int) -> int:
|
| 131 |
+
"""Per-shard, per-family seed offset, looked up rather than computed.
|
| 132 |
+
|
| 133 |
+
See :data:`CANONICAL_RECIPE` for why these are data. A missing entry means
|
| 134 |
+
the salt has not been recovered for that shard, and generation refuses
|
| 135 |
+
rather than substituting a computed one: a computed salt would produce a
|
| 136 |
+
different corpus while appearing to succeed.
|
| 137 |
+
"""
|
| 138 |
+
salt = _shard_record(shard)["salt"].get(family)
|
| 139 |
+
if salt is None:
|
| 140 |
+
raise RuntimeError(
|
| 141 |
+
f"the salt for family {family!r} on shard {shard} is not recorded, so "
|
| 142 |
+
f"that shard cannot be reproduced. It is a single integer in [0, 997) "
|
| 143 |
+
f"and is recoverable by brute force against the published bytes. "
|
| 144 |
+
f"Refusing to substitute a computed value."
|
| 145 |
+
)
|
| 146 |
+
return int(salt)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def plan_counts(
|
| 150 |
+
n_series: Optional[int] = None, mix: Optional[tuple] = None,
|
| 151 |
+
) -> Tuple[int, int, int]:
|
| 152 |
+
"""Series per family, in emission order (gp, spikes, tsi).
|
| 153 |
+
|
| 154 |
+
This is also how a published shard's family boundaries are read. The
|
| 155 |
+
per-row family sidecars of the published shards were overwritten by a
|
| 156 |
+
post-processing step that collapsed them to a single label with all ids
|
| 157 |
+
zero, so the attribution cannot be recovered from the files themselves; the
|
| 158 |
+
payload, indices and scale factors were not touched. A shard that ends up
|
| 159 |
+
one row short lost a GP row to a covariance that failed to factorize (see
|
| 160 |
+
:func:`iter_shard_series`), which shifts the boundaries down by that many.
|
| 161 |
+
"""
|
| 162 |
+
n_series = CANONICAL_RECIPE["series_per_shard"] if n_series is None else int(n_series)
|
| 163 |
+
mix = CANONICAL_RECIPE["mix"] if mix is None else tuple(mix)
|
| 164 |
+
n_gp = int(round(n_series * mix[0]))
|
| 165 |
+
n_spikes = int(round(n_series * mix[1]))
|
| 166 |
+
return n_gp, n_spikes, n_series - n_gp - n_spikes
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def _sample_scale_factor(rng: np.random.Generator) -> float:
|
| 170 |
+
"""Log-uniform scale factor, one draw per emitted series.
|
| 171 |
+
|
| 172 |
+
Synthetic series carry no sampling frequency, so each gets a scale factor
|
| 173 |
+
drawn from the range that the real corpora's frequency-derived factors span.
|
| 174 |
+
"""
|
| 175 |
+
low, high = CANONICAL_RECIPE["scale_factor_range"]
|
| 176 |
+
return float(np.exp(rng.uniform(math.log(low), math.log(high))))
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def _require_cuda(device: Optional[str]) -> str:
|
| 180 |
+
"""Resolve and check the device, refusing anything but the canonical one."""
|
| 181 |
+
canonical = CANONICAL_RECIPE["device"]
|
| 182 |
+
device = canonical if device is None else str(device)
|
| 183 |
+
if device != canonical:
|
| 184 |
+
raise ValueError(
|
| 185 |
+
f"device={device!r} is not the device this corpus was generated on "
|
| 186 |
+
f"({canonical!r}). The GP family draws from a different generator on "
|
| 187 |
+
f"each branch, so the same seed gives different series and overriding "
|
| 188 |
+
f"the device does not reproduce the published shards."
|
| 189 |
+
)
|
| 190 |
+
try:
|
| 191 |
+
import torch
|
| 192 |
+
except ImportError as exc:
|
| 193 |
+
raise RuntimeError(
|
| 194 |
+
"torch is required: this corpus was generated on the CUDA branch."
|
| 195 |
+
) from exc
|
| 196 |
+
if not torch.cuda.is_available():
|
| 197 |
+
raise RuntimeError(
|
| 198 |
+
"no CUDA device is available. This corpus was generated on the CUDA "
|
| 199 |
+
"branch, and a CPU run would produce different data rather than "
|
| 200 |
+
"reproducing it, so this fails instead of falling back."
|
| 201 |
+
)
|
| 202 |
+
return device
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
# ---------------------------------------------------------------------------
|
| 206 |
+
# Generation
|
| 207 |
+
# ---------------------------------------------------------------------------
|
| 208 |
+
|
| 209 |
+
def iter_shard_series(
|
| 210 |
+
shard: int = 0,
|
| 211 |
+
*,
|
| 212 |
+
max_series: Optional[int] = None,
|
| 213 |
+
device: Optional[str] = None,
|
| 214 |
+
) -> Iterator[Tuple[np.ndarray, float, str]]:
|
| 215 |
+
"""Yield ``(series, scale_factor, family_label)`` for one published shard.
|
| 216 |
+
|
| 217 |
+
Families are emitted in order, GP first, each generated in chunks of
|
| 218 |
+
``CANONICAL_RECIPE["chunk"]`` series under a seed of
|
| 219 |
+
``shard_seed + 1000 * chunk_index + family_salt``. GP rows whose covariance
|
| 220 |
+
failed to factorize come back non-finite and are dropped here, which is the
|
| 221 |
+
only way a shard ends up holding fewer rows than planned.
|
| 222 |
+
|
| 223 |
+
``max_series`` truncates the emitted stream and leaves the plan alone, so
|
| 224 |
+
what it yields is the published prefix. Generation still runs in canonical
|
| 225 |
+
chunks, so the first chunk is produced in full however early the truncation
|
| 226 |
+
falls; :func:`verify_shard` is the cheap way to check a stack.
|
| 227 |
+
``device`` exists only so that asking for a non-canonical one fails loudly.
|
| 228 |
+
|
| 229 |
+
Validation runs before the first row is generated. The refusals above are
|
| 230 |
+
worth nothing if they wait for the caller to start iterating, which is what
|
| 231 |
+
happens when a generator function does its own argument checking.
|
| 232 |
+
"""
|
| 233 |
+
device = _require_cuda(device)
|
| 234 |
+
seed = shard_seed(shard)
|
| 235 |
+
salts = {f: family_salt(f, shard) for f in FAMILIES}
|
| 236 |
+
if max_series is not None and int(max_series) < 0:
|
| 237 |
+
raise ValueError("max_series must be non-negative")
|
| 238 |
+
return _iter_shard_series(shard, seed, salts, max_series, device)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _iter_shard_series(
|
| 242 |
+
shard: int,
|
| 243 |
+
seed: int,
|
| 244 |
+
salts: dict,
|
| 245 |
+
max_series: Optional[int],
|
| 246 |
+
device: str,
|
| 247 |
+
) -> Iterator[Tuple[np.ndarray, float, str]]:
|
| 248 |
+
"""Generator body for :func:`iter_shard_series`, validation already done."""
|
| 249 |
+
length = CANONICAL_RECIPE["length"]
|
| 250 |
+
chunk = CANONICAL_RECIPE["chunk"]
|
| 251 |
+
gp_batch = CANONICAL_RECIPE["gp_batch"]
|
| 252 |
+
counts = dict(zip(FAMILIES, plan_counts()))
|
| 253 |
+
generators = {
|
| 254 |
+
"gp": lambda n, s: generate_gp(n, length, seed=s, device=device,
|
| 255 |
+
batch=gp_batch),
|
| 256 |
+
"spikes": lambda n, s: generate_spikes(n, length, seed=s),
|
| 257 |
+
"tsi": lambda n, s: generate_tsi(n, length, seed=s),
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
rng = np.random.default_rng(seed)
|
| 261 |
+
emitted_total = 0
|
| 262 |
+
dropped = 0
|
| 263 |
+
for family in FAMILIES:
|
| 264 |
+
total = counts[family]
|
| 265 |
+
label = f"{family}_{length}"
|
| 266 |
+
generated = 0
|
| 267 |
+
chunk_index = 0
|
| 268 |
+
while generated < total:
|
| 269 |
+
cur = min(chunk, total - generated)
|
| 270 |
+
block = generators[family](cur, seed + 1000 * chunk_index + salts[family])
|
| 271 |
+
chunk_index += 1
|
| 272 |
+
generated += cur
|
| 273 |
+
for row in block:
|
| 274 |
+
if not np.isfinite(row).all():
|
| 275 |
+
dropped += 1
|
| 276 |
+
continue
|
| 277 |
+
yield row, _sample_scale_factor(rng), label
|
| 278 |
+
emitted_total += 1
|
| 279 |
+
if max_series is not None and emitted_total >= int(max_series):
|
| 280 |
+
return
|
| 281 |
+
print(f" {family}: {total} generated, {dropped} dropped so far", flush=True)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
# ---------------------------------------------------------------------------
|
| 285 |
+
# Writing a shard
|
| 286 |
+
# ---------------------------------------------------------------------------
|
| 287 |
+
|
| 288 |
+
def normalize_for_f16(series: np.ndarray) -> Tuple[np.ndarray, float, float]:
|
| 289 |
+
"""Per-series z-normalization ahead of the float16 cast.
|
| 290 |
+
|
| 291 |
+
float16 stores the payload at half the size of float32 but caps magnitudes
|
| 292 |
+
at 65,504, which raw series exceed. Storing a z-normalized payload with the
|
| 293 |
+
mean and standard deviation alongside it keeps any realistic magnitude in
|
| 294 |
+
range, and the read path is ``x = payload * stdev + mean`` with no
|
| 295 |
+
branching: a constant series is stored with ``stdev = 1`` so its payload is
|
| 296 |
+
identically zero and denormalizes exactly.
|
| 297 |
+
"""
|
| 298 |
+
series = np.asarray(series, dtype=np.float32)
|
| 299 |
+
clean = np.where(np.isfinite(series), series, np.nan)
|
| 300 |
+
finite = clean[~np.isnan(clean)]
|
| 301 |
+
if finite.size == 0:
|
| 302 |
+
return clean, 0.0, 1.0
|
| 303 |
+
mu = float(finite.mean())
|
| 304 |
+
sd = float(finite.std())
|
| 305 |
+
sd_safe = sd if sd > 0.0 else 1.0
|
| 306 |
+
return ((clean - mu) / sd_safe).astype(np.float32, copy=False), mu, sd_safe
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def build_shard(
|
| 310 |
+
out_dir: Union[str, Path, None] = None,
|
| 311 |
+
shard: int = 0,
|
| 312 |
+
*,
|
| 313 |
+
max_series: Optional[int] = None,
|
| 314 |
+
overwrite: bool = False,
|
| 315 |
+
) -> int:
|
| 316 |
+
"""Generate one published shard and write it as a cache directory.
|
| 317 |
+
|
| 318 |
+
Called with no arguments this reproduces shard 0 into ``synth4096_0``. The
|
| 319 |
+
written files are the six payload and index files, the two family sidecars,
|
| 320 |
+
and ``recipe.json``, which records the recipe and the stack that produced
|
| 321 |
+
the directory. Returns the number of series written.
|
| 322 |
+
|
| 323 |
+
The payload is streamed to a temporary file and renamed on success, so an
|
| 324 |
+
interrupted build leaves no directory that looks complete.
|
| 325 |
+
"""
|
| 326 |
+
_require_cuda(None)
|
| 327 |
+
for family in FAMILIES: # refuse an unreproducible shard first
|
| 328 |
+
family_salt(family, shard)
|
| 329 |
+
out_dir = Path(DEFAULT_OUT_DIR.format(shard=shard) if out_dir is None else out_dir)
|
| 330 |
+
if all((out_dir / f).exists() for f in CACHE_FILES) and not overwrite:
|
| 331 |
+
raise RuntimeError(
|
| 332 |
+
f"{out_dir} already holds a shard. Pass overwrite=True to rebuild it, "
|
| 333 |
+
f"or build into a new directory."
|
| 334 |
+
)
|
| 335 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 336 |
+
|
| 337 |
+
length = CANONICAL_RECIPE["length"]
|
| 338 |
+
n_gp, n_spikes, n_tsi = plan_counts()
|
| 339 |
+
print(f"building shard {shard} into {out_dir}: gp={n_gp} spikes={n_spikes} "
|
| 340 |
+
f"tsi={n_tsi} length={length} device={CANONICAL_RECIPE['device']}",
|
| 341 |
+
flush=True)
|
| 342 |
+
|
| 343 |
+
offsets: list[int] = []
|
| 344 |
+
lengths: list[int] = []
|
| 345 |
+
scale_factors: list[float] = []
|
| 346 |
+
means: list[float] = []
|
| 347 |
+
stdevs: list[float] = []
|
| 348 |
+
labels: list[str] = []
|
| 349 |
+
label_ids: dict[str, int] = {}
|
| 350 |
+
ids: list[int] = []
|
| 351 |
+
|
| 352 |
+
tmp_payload = out_dir / "series.f16.tmp"
|
| 353 |
+
cursor = 0
|
| 354 |
+
n_written = 0
|
| 355 |
+
t0 = time.time()
|
| 356 |
+
try:
|
| 357 |
+
with open(tmp_payload, "wb") as fp:
|
| 358 |
+
for row, sf, label in iter_shard_series(shard, max_series=max_series):
|
| 359 |
+
normalized, mu, sd = normalize_for_f16(row)
|
| 360 |
+
payload = normalized.astype(np.float16, copy=False)
|
| 361 |
+
fp.write(payload.tobytes())
|
| 362 |
+
offsets.append(cursor)
|
| 363 |
+
lengths.append(int(payload.size))
|
| 364 |
+
scale_factors.append(float(sf))
|
| 365 |
+
means.append(mu)
|
| 366 |
+
stdevs.append(sd)
|
| 367 |
+
if label not in label_ids:
|
| 368 |
+
label_ids[label] = len(labels)
|
| 369 |
+
labels.append(label)
|
| 370 |
+
ids.append(label_ids[label])
|
| 371 |
+
cursor += int(payload.size) * _F16_BYTES
|
| 372 |
+
n_written += 1
|
| 373 |
+
if n_written % 10_000 == 0:
|
| 374 |
+
rate = n_written / max(time.time() - t0, 1e-6)
|
| 375 |
+
print(f" {n_written:,} written, {rate:.0f} series/s, "
|
| 376 |
+
f"{cursor / 1e9:.2f} GB", flush=True)
|
| 377 |
+
fp.flush()
|
| 378 |
+
os.fsync(fp.fileno())
|
| 379 |
+
except BaseException:
|
| 380 |
+
tmp_payload.unlink(missing_ok=True)
|
| 381 |
+
raise
|
| 382 |
+
|
| 383 |
+
if n_written == 0:
|
| 384 |
+
tmp_payload.unlink(missing_ok=True)
|
| 385 |
+
raise RuntimeError("no series were generated, so nothing was written")
|
| 386 |
+
|
| 387 |
+
os.replace(tmp_payload, out_dir / "series.f16")
|
| 388 |
+
np.save(out_dir / "offsets.npy", np.asarray(offsets, dtype=np.int64))
|
| 389 |
+
np.save(out_dir / "lengths.npy", np.asarray(lengths, dtype=np.int32))
|
| 390 |
+
_write_bytes(out_dir / "scale_factors.f32",
|
| 391 |
+
np.asarray(scale_factors, dtype=np.float32).tobytes())
|
| 392 |
+
_write_bytes(out_dir / "series_mean.f32",
|
| 393 |
+
np.asarray(means, dtype=np.float32).tobytes())
|
| 394 |
+
_write_bytes(out_dir / "series_stdev.f32",
|
| 395 |
+
np.asarray(stdevs, dtype=np.float32).tobytes())
|
| 396 |
+
_write_bytes(out_dir / "dataset_id.u16",
|
| 397 |
+
np.asarray(ids, dtype=np.uint16).tobytes())
|
| 398 |
+
_write_bytes(out_dir / "dataset_names.json",
|
| 399 |
+
json.dumps({"version": 2, "datasets": labels}).encode("utf-8"))
|
| 400 |
+
_write_bytes(out_dir / "recipe.json",
|
| 401 |
+
json.dumps(_provenance(shard, n_written), indent=2).encode("utf-8"))
|
| 402 |
+
|
| 403 |
+
print(f"wrote {n_written:,} series in {time.time() - t0:.0f}s "
|
| 404 |
+
f"({cursor / 1e9:.2f} GB) to {out_dir}", flush=True)
|
| 405 |
+
return n_written
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def _write_bytes(path: Path, data: bytes) -> None:
|
| 409 |
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
| 410 |
+
with open(tmp, "wb") as fp:
|
| 411 |
+
fp.write(data)
|
| 412 |
+
fp.flush()
|
| 413 |
+
os.fsync(fp.fileno())
|
| 414 |
+
os.replace(tmp, path)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def _provenance(shard: int, n_written: int) -> dict:
|
| 418 |
+
record = {
|
| 419 |
+
"version": CANONICAL_RECIPE["version"],
|
| 420 |
+
"shard": int(shard),
|
| 421 |
+
"seed": shard_seed(shard),
|
| 422 |
+
"salt": dict(_shard_record(shard)["salt"]),
|
| 423 |
+
"series_written": int(n_written),
|
| 424 |
+
"series_planned": CANONICAL_RECIPE["series_per_shard"],
|
| 425 |
+
"length": CANONICAL_RECIPE["length"],
|
| 426 |
+
"mix": list(CANONICAL_RECIPE["mix"]),
|
| 427 |
+
"chunk": CANONICAL_RECIPE["chunk"],
|
| 428 |
+
"gp_batch": CANONICAL_RECIPE["gp_batch"],
|
| 429 |
+
"device": CANONICAL_RECIPE["device"],
|
| 430 |
+
"verified_stack": CANONICAL_RECIPE["verified_stack"],
|
| 431 |
+
}
|
| 432 |
+
try:
|
| 433 |
+
import torch
|
| 434 |
+
record["torch"] = torch.__version__
|
| 435 |
+
if torch.cuda.is_available():
|
| 436 |
+
record["gpu"] = torch.cuda.get_device_name(0)
|
| 437 |
+
except Exception: # provenance is best effort
|
| 438 |
+
pass
|
| 439 |
+
record["numpy"] = np.__version__
|
| 440 |
+
return record
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
# ---------------------------------------------------------------------------
|
| 444 |
+
# Verifying a shard against the published one
|
| 445 |
+
# ---------------------------------------------------------------------------
|
| 446 |
+
|
| 447 |
+
def _resolve_source(path_or_url: Union[str, Path]) -> Union[Path, str]:
|
| 448 |
+
"""A local directory, or an https prefix ending in a slash.
|
| 449 |
+
|
| 450 |
+
``hf://<owner>/<repo>[/<subdir>]`` is rewritten to the dataset's resolve
|
| 451 |
+
URL, which serves byte ranges. That matters: checking twenty rows pulls
|
| 452 |
+
about 160 KB rather than the half gigabyte a shard payload occupies.
|
| 453 |
+
"""
|
| 454 |
+
text = str(path_or_url)
|
| 455 |
+
if text.startswith("hf://"):
|
| 456 |
+
parts = [p for p in text[len("hf://"):].split("/") if p]
|
| 457 |
+
if len(parts) < 2:
|
| 458 |
+
raise ValueError("hf:// source must be hf://<owner>/<repo>[/<subdir>]")
|
| 459 |
+
repo = "/".join(parts[:2])
|
| 460 |
+
subdir = "/".join(parts[2:])
|
| 461 |
+
url = f"https://huggingface.co/datasets/{repo}/resolve/main/"
|
| 462 |
+
return url + (subdir + "/" if subdir else "")
|
| 463 |
+
if text.startswith("http://") or text.startswith("https://"):
|
| 464 |
+
return text if text.endswith("/") else text + "/"
|
| 465 |
+
return Path(text)
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
def _read_file(source: Union[Path, str], name: str) -> bytes:
|
| 469 |
+
if isinstance(source, Path):
|
| 470 |
+
return (source / name).read_bytes()
|
| 471 |
+
import urllib.request
|
| 472 |
+
with urllib.request.urlopen(source + name) as response:
|
| 473 |
+
return response.read()
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
def _read_range(source: Union[Path, str], name: str, start: int, count: int) -> bytes:
|
| 477 |
+
if isinstance(source, Path):
|
| 478 |
+
with open(source / name, "rb") as fp:
|
| 479 |
+
fp.seek(start)
|
| 480 |
+
return fp.read(count)
|
| 481 |
+
import urllib.request
|
| 482 |
+
request = urllib.request.Request(
|
| 483 |
+
source + name, headers={"Range": f"bytes={start}-{start + count - 1}"})
|
| 484 |
+
with urllib.request.urlopen(request) as response:
|
| 485 |
+
data = response.read()
|
| 486 |
+
if len(data) != count: # server ignored the range header
|
| 487 |
+
data = data[start:start + count]
|
| 488 |
+
return data
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
def verify_shard(
|
| 492 |
+
path_or_url: Union[str, Path],
|
| 493 |
+
shard: int = 0,
|
| 494 |
+
*,
|
| 495 |
+
n_rows: int = DEFAULT_VERIFY_ROWS,
|
| 496 |
+
n_scale_factors: int = DEFAULT_VERIFY_SCALE_FACTORS,
|
| 497 |
+
verbose: bool = True,
|
| 498 |
+
) -> dict:
|
| 499 |
+
"""Regenerate a prefix of a published shard and report how far it agrees.
|
| 500 |
+
|
| 501 |
+
``path_or_url`` is a shard directory, an https prefix, or
|
| 502 |
+
``hf://<owner>/<repo>[/<subdir>]``. The comparison is against
|
| 503 |
+
``series.f16``, the payload, cast the same way the builder casts it. The
|
| 504 |
+
returned report gives ``values_differing`` out of ``values_compared``, which
|
| 505 |
+
is the number this module's fidelity claim is stated in.
|
| 506 |
+
|
| 507 |
+
``n_rows`` rows are regenerated from the GP family, which is what a shard
|
| 508 |
+
opens with, rounded up to a whole number of GP batches so the batch shape
|
| 509 |
+
matches the published run. It must not exceed one chunk. Set it to 0 to skip
|
| 510 |
+
the payload check, which is the only part that needs a GPU; the scale-factor
|
| 511 |
+
check runs anywhere and still exercises the seed scheme.
|
| 512 |
+
|
| 513 |
+
A shard that holds fewer rows than planned lost GP rows to covariances that
|
| 514 |
+
failed to factorize. Those rows are dropped here too, so the compared prefix
|
| 515 |
+
stays aligned, but the scale-factor stream shifts by one draw per drop, and
|
| 516 |
+
a mismatch after the drop index is expected rather than a fidelity failure.
|
| 517 |
+
"""
|
| 518 |
+
source = _resolve_source(path_or_url)
|
| 519 |
+
length = CANONICAL_RECIPE["length"]
|
| 520 |
+
chunk = CANONICAL_RECIPE["chunk"]
|
| 521 |
+
gp_batch = CANONICAL_RECIPE["gp_batch"]
|
| 522 |
+
n_rows = int(n_rows)
|
| 523 |
+
if n_rows > chunk:
|
| 524 |
+
raise ValueError(
|
| 525 |
+
f"n_rows={n_rows} exceeds one chunk ({chunk}); rows beyond the first "
|
| 526 |
+
f"chunk are generated under a different seed and would need the whole "
|
| 527 |
+
f"shard regenerated."
|
| 528 |
+
)
|
| 529 |
+
|
| 530 |
+
published_lengths = np.load(io.BytesIO(_read_file(source, "lengths.npy")))
|
| 531 |
+
published_offsets = np.load(io.BytesIO(_read_file(source, "offsets.npy")))
|
| 532 |
+
published_rows = int(published_lengths.size)
|
| 533 |
+
planned_rows = CANONICAL_RECIPE["series_per_shard"]
|
| 534 |
+
|
| 535 |
+
report = {
|
| 536 |
+
"source": str(path_or_url),
|
| 537 |
+
"shard": int(shard),
|
| 538 |
+
"published_rows": published_rows,
|
| 539 |
+
"planned_rows": planned_rows,
|
| 540 |
+
"rows_short_of_plan": max(planned_rows - published_rows, 0),
|
| 541 |
+
"rows_compared": 0,
|
| 542 |
+
"values_compared": 0,
|
| 543 |
+
"values_differing": None,
|
| 544 |
+
"scale_factors_compared": 0,
|
| 545 |
+
"scale_factors_differing": None,
|
| 546 |
+
"payload_checked": False,
|
| 547 |
+
"verified_stack": CANONICAL_RECIPE["verified_stack"],
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
if n_scale_factors:
|
| 551 |
+
published_sf = np.frombuffer(
|
| 552 |
+
_read_file(source, "scale_factors.f32"), dtype=np.float32)
|
| 553 |
+
n_sf = min(int(n_scale_factors), published_sf.size)
|
| 554 |
+
rng = np.random.default_rng(shard_seed(shard))
|
| 555 |
+
ours = np.asarray([_sample_scale_factor(rng) for _ in range(n_sf)],
|
| 556 |
+
dtype=np.float32)
|
| 557 |
+
differing = int(np.count_nonzero(ours != published_sf[:n_sf]))
|
| 558 |
+
report["scale_factors_compared"] = n_sf
|
| 559 |
+
report["scale_factors_differing"] = differing
|
| 560 |
+
|
| 561 |
+
if n_rows > 0:
|
| 562 |
+
_require_cuda(None)
|
| 563 |
+
n_generate = int(math.ceil(n_rows / gp_batch) * gp_batch)
|
| 564 |
+
block = generate_gp(
|
| 565 |
+
n_generate, length,
|
| 566 |
+
seed=shard_seed(shard) + family_salt("gp", shard),
|
| 567 |
+
device=CANONICAL_RECIPE["device"], batch=gp_batch,
|
| 568 |
+
)
|
| 569 |
+
kept = [row for row in block if np.isfinite(row).all()]
|
| 570 |
+
if len(kept) < n_rows:
|
| 571 |
+
raise RuntimeError(
|
| 572 |
+
f"regeneration produced only {len(kept)} usable rows of the "
|
| 573 |
+
f"{n_rows} requested: {n_generate - len(kept)} covariances failed "
|
| 574 |
+
f"to factorize. Investigate that before reading anything into a "
|
| 575 |
+
f"comparison, since it is far above the published failure rate."
|
| 576 |
+
)
|
| 577 |
+
differing = 0
|
| 578 |
+
compared = 0
|
| 579 |
+
max_abs_diff = 0.0
|
| 580 |
+
for i in range(n_rows):
|
| 581 |
+
n = int(published_lengths[i])
|
| 582 |
+
if n != length:
|
| 583 |
+
raise RuntimeError(
|
| 584 |
+
f"published row {i} has length {n}, not {length}: this is not "
|
| 585 |
+
f"a shard of this corpus."
|
| 586 |
+
)
|
| 587 |
+
raw = _read_range(source, "series.f16",
|
| 588 |
+
int(published_offsets[i]), n * _F16_BYTES)
|
| 589 |
+
theirs = np.frombuffer(raw, dtype=np.float16, count=n)
|
| 590 |
+
normalized, _, _ = normalize_for_f16(kept[i])
|
| 591 |
+
ours = normalized.astype(np.float16, copy=False)
|
| 592 |
+
delta = ours.astype(np.float32) - theirs.astype(np.float32)
|
| 593 |
+
differing += int(np.count_nonzero(delta))
|
| 594 |
+
max_abs_diff = max(max_abs_diff, float(np.abs(delta).max()))
|
| 595 |
+
compared += n
|
| 596 |
+
report.update({
|
| 597 |
+
"rows_compared": n_rows,
|
| 598 |
+
"values_compared": compared,
|
| 599 |
+
"values_differing": differing,
|
| 600 |
+
"max_abs_diff": max_abs_diff,
|
| 601 |
+
"payload_checked": True,
|
| 602 |
+
})
|
| 603 |
+
|
| 604 |
+
if verbose:
|
| 605 |
+
print(f"shard {shard} at {path_or_url}")
|
| 606 |
+
print(f" published rows: {published_rows:,} of {planned_rows:,} planned")
|
| 607 |
+
if report["scale_factors_compared"]:
|
| 608 |
+
print(f" scale factors: {report['scale_factors_differing']} of "
|
| 609 |
+
f"{report['scale_factors_compared']:,} differ")
|
| 610 |
+
if report["payload_checked"]:
|
| 611 |
+
print(f" payload: {report['values_differing']} of "
|
| 612 |
+
f"{report['values_compared']:,} float16 values differ "
|
| 613 |
+
f"across {report['rows_compared']} rows "
|
| 614 |
+
f"(max abs difference {report['max_abs_diff']:g})")
|
| 615 |
+
else:
|
| 616 |
+
print(" payload: not checked (n_rows=0)")
|
| 617 |
+
print(f" the published fidelity statement was measured on "
|
| 618 |
+
f"{CANONICAL_RECIPE['verified_stack']}")
|
| 619 |
+
return report
|
| 620 |
+
|
| 621 |
+
|
| 622 |
+
# ---------------------------------------------------------------------------
|
| 623 |
+
# Command line
|
| 624 |
+
# ---------------------------------------------------------------------------
|
| 625 |
+
|
| 626 |
+
def main(argv: Optional[list] = None) -> None:
|
| 627 |
+
import argparse
|
| 628 |
+
|
| 629 |
+
parser = argparse.ArgumentParser(
|
| 630 |
+
prog="python -m tinycast.corpus",
|
| 631 |
+
description="Build or verify a shard of TinyCast's synthetic corpus.")
|
| 632 |
+
sub = parser.add_subparsers(dest="command", required=True)
|
| 633 |
+
|
| 634 |
+
build = sub.add_parser("build", help="generate a shard (needs a CUDA GPU)")
|
| 635 |
+
build.add_argument("--shard", type=int, default=0)
|
| 636 |
+
build.add_argument("--out", default=None,
|
| 637 |
+
help=f"output directory (default {DEFAULT_OUT_DIR})")
|
| 638 |
+
build.add_argument("--max-series", type=int, default=None,
|
| 639 |
+
help="stop after this many series; the prefix is unchanged")
|
| 640 |
+
build.add_argument("--overwrite", action="store_true")
|
| 641 |
+
|
| 642 |
+
verify = sub.add_parser("verify", help="compare a published shard to a rebuild")
|
| 643 |
+
verify.add_argument("source",
|
| 644 |
+
help="shard directory, https prefix, or hf://<owner>/<repo>")
|
| 645 |
+
verify.add_argument("--shard", type=int, default=0)
|
| 646 |
+
verify.add_argument("--rows", type=int, default=DEFAULT_VERIFY_ROWS,
|
| 647 |
+
help="rows to regenerate and compare; 0 skips the payload")
|
| 648 |
+
verify.add_argument("--scale-factors", type=int,
|
| 649 |
+
default=DEFAULT_VERIFY_SCALE_FACTORS)
|
| 650 |
+
|
| 651 |
+
args = parser.parse_args(argv)
|
| 652 |
+
if args.command == "build":
|
| 653 |
+
build_shard(args.out, args.shard, max_series=args.max_series,
|
| 654 |
+
overwrite=args.overwrite)
|
| 655 |
+
else:
|
| 656 |
+
verify_shard(args.source, args.shard, n_rows=args.rows,
|
| 657 |
+
n_scale_factors=args.scale_factors)
|
| 658 |
+
|
| 659 |
+
|
| 660 |
+
if __name__ == "__main__":
|
| 661 |
+
main()
|
tinycast/downsample.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Eval-time period-alignment downsampling rule.
|
| 2 |
+
|
| 3 |
+
Some datasets carry their dominant seasonality at a clean integer multiple of
|
| 4 |
+
the canonical (samples-per-day) period for their sampling frequency, with the
|
| 5 |
+
canonical period itself absent. For example, bizitobs_l2c at 5T has no daily
|
| 6 |
+
cycle at all, only weekly (2016 samples = 7x the canonical 288). Models calibrated
|
| 7 |
+
around the canonical period grid systematically mis-handle such series;
|
| 8 |
+
downsampling the context by the multiple aliases the dominant period back onto
|
| 9 |
+
the grid (weekly-at-5T becomes daily-at-35T) and restores in-distribution
|
| 10 |
+
behaviour. This module derives the factor from the CONTEXT data (a k=7 alias
|
| 11 |
+
on bizitobs_l2c/5T medium+long), so no per-dataset hand-tuning and no
|
| 12 |
+
information beyond the model's own inputs is used.
|
| 13 |
+
|
| 14 |
+
Fire conditions (all required, and deliberately narrow: downsampling a config
|
| 15 |
+
whose canonical peak is intact makes MASE strictly worse, m4_hourly's strong
|
| 16 |
+
weekly harmonic included):
|
| 17 |
+
1. sub-daily frequency (canonical period >= MIN_CANONICAL samples/day);
|
| 18 |
+
2. a dominant spectral peak at period P with P/canonical within
|
| 19 |
+
``REL_TOL`` of an integer k in [2, MAX_K];
|
| 20 |
+
3. the canonical-period peak is ABSENT: power near the canonical period is
|
| 21 |
+
below ``CANONICAL_ABSENT`` x the dominant peak's power;
|
| 22 |
+
4. the dominant peak is significant: >= ``PEAK_SIG`` x median spectral power;
|
| 23 |
+
5. a >= ``QUORUM`` fraction of sampled series agree on the same k;
|
| 24 |
+
6. the downsampled context still fills the model window
|
| 25 |
+
(median len / k >= ``context_window``, else the aliasing starves the
|
| 26 |
+
encoder: on bizitobs_l2c/H, where T/k ~ 358, k=7 turns from a win into a
|
| 27 |
+
loss at long horizons);
|
| 28 |
+
7. the horizon spans at least one canonical day (H >= canonical, else the
|
| 29 |
+
coarse forecast's interpolation loses more than the aliasing gains: on
|
| 30 |
+
bizitobs_l2c/5T/short, H=48 against a canonical 288, by +0.05 MASE).
|
| 31 |
+
"""
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
|
| 34 |
+
from typing import Iterable, Optional
|
| 35 |
+
|
| 36 |
+
import numpy as np
|
| 37 |
+
|
| 38 |
+
MIN_CANONICAL = 8 # rule inactive for daily-or-coarser frequencies
|
| 39 |
+
MAX_K = 16
|
| 40 |
+
REL_TOL = 0.06 # |P/canonical - k| <= REL_TOL * k (after sub-bin refinement)
|
| 41 |
+
CANONICAL_ABSENT = 0.10 # canonical peak power < 10% of dominant peak power
|
| 42 |
+
PEAK_SIG = 20.0 # dominant peak >= 20x median spectral power
|
| 43 |
+
QUORUM = 0.7
|
| 44 |
+
MIN_CYCLES = 3 # dominant period must repeat >= 3x in the analysed tail
|
| 45 |
+
TAIL = 16384 # analyse at most this many trailing samples
|
| 46 |
+
N_SAMPLE = 64 # series sampled per config
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _series_factor(x: np.ndarray, canonical: int) -> int:
|
| 50 |
+
x = np.asarray(x, dtype=np.float64)
|
| 51 |
+
x = x[np.isfinite(x)]
|
| 52 |
+
if x.size < MIN_CYCLES * 2 * canonical:
|
| 53 |
+
return 1
|
| 54 |
+
x = x[-TAIL:]
|
| 55 |
+
n = x.size
|
| 56 |
+
x = x - x.mean()
|
| 57 |
+
p = np.abs(np.fft.rfft(x)) ** 2
|
| 58 |
+
p[0] = 0.0
|
| 59 |
+
med = np.median(p[1:])
|
| 60 |
+
if med <= 0.0:
|
| 61 |
+
return 1
|
| 62 |
+
|
| 63 |
+
periods = np.full(p.shape, np.inf)
|
| 64 |
+
periods[1:] = n / np.arange(1, p.shape[0], dtype=np.float64)
|
| 65 |
+
|
| 66 |
+
# Power near the canonical period (max over a +-10% band).
|
| 67 |
+
canon_band = (periods >= 0.9 * canonical) & (periods <= 1.1 * canonical)
|
| 68 |
+
canon_pow = p[canon_band].max() if canon_band.any() else 0.0
|
| 69 |
+
|
| 70 |
+
# Dominant peak among periods that are >= 1.5x canonical and repeat
|
| 71 |
+
# >= MIN_CYCLES times in the analysed tail.
|
| 72 |
+
cand = (periods >= 1.5 * canonical) & (periods <= n / MIN_CYCLES)
|
| 73 |
+
if not cand.any():
|
| 74 |
+
return 1
|
| 75 |
+
idx = int(np.flatnonzero(cand)[np.argmax(p[cand])])
|
| 76 |
+
peak_pow = p[idx]
|
| 77 |
+
if peak_pow < PEAK_SIG * med:
|
| 78 |
+
return 1
|
| 79 |
+
if canon_pow > CANONICAL_ABSENT * peak_pow:
|
| 80 |
+
return 1 # canonical period present: do not alias
|
| 81 |
+
|
| 82 |
+
# Sub-bin peak refinement (parabolic on log-power): the raw frequency
|
| 83 |
+
# grid is coarse at long periods (spacing ~P^2/n, i.e. ~12% of P for
|
| 84 |
+
# weekly-at-5T in a 16k tail), which would let non-integer multiples
|
| 85 |
+
# masquerade as clean ones under any workable tolerance.
|
| 86 |
+
delta = 0.0
|
| 87 |
+
if 1 <= idx < p.shape[0] - 1 and p[idx - 1] > 0 and p[idx + 1] > 0:
|
| 88 |
+
lp = np.log(p[idx - 1:idx + 2])
|
| 89 |
+
denom = lp[0] - 2.0 * lp[1] + lp[2]
|
| 90 |
+
if denom < 0:
|
| 91 |
+
delta = float(np.clip(0.5 * (lp[0] - lp[2]) / denom, -0.5, 0.5))
|
| 92 |
+
refined_period = n / (idx + delta)
|
| 93 |
+
|
| 94 |
+
r = refined_period / canonical
|
| 95 |
+
k = int(round(r))
|
| 96 |
+
if k < 2 or k > MAX_K or abs(r - k) > REL_TOL * k:
|
| 97 |
+
return 1
|
| 98 |
+
return k
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def period_alignment_factor(
|
| 102 |
+
contexts: Iterable[np.ndarray],
|
| 103 |
+
freq_seconds: float,
|
| 104 |
+
horizon: int,
|
| 105 |
+
context_window: int = 2048,
|
| 106 |
+
n_sample: int = N_SAMPLE,
|
| 107 |
+
) -> int:
|
| 108 |
+
"""Downsample factor for one eval config, from context windows only.
|
| 109 |
+
|
| 110 |
+
``contexts`` are the test INPUT series (the model's own inputs);
|
| 111 |
+
``freq_seconds`` is the sampling interval; ``horizon`` the prediction
|
| 112 |
+
length; ``context_window`` the model's encoder window. Returns 1 unless
|
| 113 |
+
the config's series agree (>= QUORUM) on the same aliasing multiple k >= 2
|
| 114 |
+
and the horizon/window guards pass.
|
| 115 |
+
"""
|
| 116 |
+
canonical = int(round(86400.0 / float(freq_seconds)))
|
| 117 |
+
if canonical < MIN_CANONICAL:
|
| 118 |
+
return 1
|
| 119 |
+
if int(horizon) < canonical: # guard 7: >= one canonical day
|
| 120 |
+
return 1
|
| 121 |
+
ks = []
|
| 122 |
+
lens = []
|
| 123 |
+
for x in contexts:
|
| 124 |
+
x = np.asarray(x, dtype=np.float64)
|
| 125 |
+
ks.append(_series_factor(x, canonical))
|
| 126 |
+
lens.append(x.size)
|
| 127 |
+
if len(ks) >= n_sample:
|
| 128 |
+
break
|
| 129 |
+
if not ks:
|
| 130 |
+
return 1
|
| 131 |
+
vals, counts = np.unique(ks, return_counts=True)
|
| 132 |
+
best = int(vals[np.argmax(counts)])
|
| 133 |
+
if best == 1 or counts.max() / len(ks) < QUORUM:
|
| 134 |
+
return 1
|
| 135 |
+
if float(np.median(lens)) / best < context_window: # guard 6
|
| 136 |
+
return 1
|
| 137 |
+
return best
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def freq_to_seconds(freq: str) -> Optional[float]:
|
| 141 |
+
"""Sampling interval in seconds for a pandas-style freq string, or None."""
|
| 142 |
+
import pandas as pd
|
| 143 |
+
|
| 144 |
+
try:
|
| 145 |
+
off = pd.tseries.frequencies.to_offset(freq)
|
| 146 |
+
return pd.Timedelta(off).total_seconds()
|
| 147 |
+
except (ValueError, TypeError):
|
| 148 |
+
return None
|
tinycast/encoding.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared structural positional-encoding helpers (phase + bounded recency).
|
| 2 |
+
|
| 3 |
+
A normalized-periodogram detector supplies top-K periods per sample. Those
|
| 4 |
+
periods drive a structural positional encoding (sin/cos of phase) shared
|
| 5 |
+
between context tokens and decoder horizon queries, plus a bounded
|
| 6 |
+
recency/trend basis. Also holds the fp32 RMSNorm helper.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import math
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _phase_encoding(
|
| 18 |
+
positions: torch.Tensor, periods: torch.Tensor,
|
| 19 |
+
n_harmonics: int = 1,
|
| 20 |
+
) -> torch.Tensor:
|
| 21 |
+
"""Compute sin/cos phase encoding for each position under each period.
|
| 22 |
+
|
| 23 |
+
For each detected period ``p_k``, emit Fourier-series channels up to
|
| 24 |
+
``n_harmonics``: at harmonic m, the channels are sin(2π·m·t/p_k) and
|
| 25 |
+
cos(2π·m·t/p_k). The fundamental (m=1) is the base behavior; m=2,3,...
|
| 26 |
+
let the model represent non-sinusoidal periodic shapes (square-wave
|
| 27 |
+
traffic, sawtooth load) that the fundamental alone cannot.
|
| 28 |
+
|
| 29 |
+
Output channel ordering, per (period, harmonic):
|
| 30 |
+
[sin(1·φ_1), cos(1·φ_1), ..., sin(H·φ_1), cos(H·φ_1),
|
| 31 |
+
sin(1·φ_2), cos(1·φ_2), ..., sin(H·φ_K), cos(H·φ_K)]
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
positions: (B, T) int tensor of absolute positions.
|
| 35 |
+
periods: (B, K) int tensor of per-sample detected periods. Zero
|
| 36 |
+
means "rejected by significance test" → all harmonics
|
| 37 |
+
of that period come back zeroed.
|
| 38 |
+
n_harmonics: number of Fourier harmonics per period (default 1).
|
| 39 |
+
Returns:
|
| 40 |
+
(B, T, 2·K·n_harmonics) fp32 tensor.
|
| 41 |
+
"""
|
| 42 |
+
B, T = positions.shape
|
| 43 |
+
K = periods.shape[1]
|
| 44 |
+
H = int(n_harmonics)
|
| 45 |
+
if H < 1:
|
| 46 |
+
raise ValueError(f"n_harmonics must be >= 1; got {H}")
|
| 47 |
+
valid = (periods > 0).view(B, 1, K).float() # (B,1,K)
|
| 48 |
+
p_safe = periods.clamp(min=1).view(B, 1, K).float()
|
| 49 |
+
pos = positions.view(B, T, 1).float()
|
| 50 |
+
phase_base = 2.0 * math.pi * pos / p_safe # (B,T,K)
|
| 51 |
+
# Build (B,T,K,H,2): for each (period k, harmonic m), [sin(m·φ_k), cos(m·φ_k)]
|
| 52 |
+
multipliers = torch.arange(1, H + 1, device=positions.device, dtype=phase_base.dtype)
|
| 53 |
+
phase_m = phase_base.unsqueeze(-1) * multipliers # (B,T,K,H)
|
| 54 |
+
sin_m = torch.sin(phase_m) * valid.unsqueeze(-1) # (B,T,K,H)
|
| 55 |
+
cos_m = torch.cos(phase_m) * valid.unsqueeze(-1)
|
| 56 |
+
pair = torch.stack([sin_m, cos_m], dim=-1) # (B,T,K,H,2)
|
| 57 |
+
return pair.reshape(B, T, K * H * 2)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# Number of recency/trend channels added by the bounded-basis extension.
|
| 61 |
+
N_RECENCY_CHANNELS = 5
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _recency_encoding(
|
| 65 |
+
positions: torch.Tensor, L: int,
|
| 66 |
+
) -> torch.Tensor:
|
| 67 |
+
"""Bounded recency/trend channels for the shared positional encoding.
|
| 68 |
+
|
| 69 |
+
"Now" is anchored at position ``L-1`` (end of context). ``Δ = (t - (L-1)) / L``
|
| 70 |
+
is signed: negative for past, zero at "now", positive for future. All
|
| 71 |
+
channels are bounded so they're safe to evaluate at arbitrary future
|
| 72 |
+
horizons (the parameterized-query path goes well beyond training H).
|
| 73 |
+
|
| 74 |
+
Channels (5):
|
| 75 |
+
rec_lin: Δ (signed linear distance from now)
|
| 76 |
+
rec_log: sign(Δ) · log1p(|Δ|)/log(2) (signed log-compressed distance)
|
| 77 |
+
rec_e05: exp(-0.5 · |Δ|) (long-memory decay)
|
| 78 |
+
rec_e2: exp(-2.0 · |Δ|) (medium-memory decay)
|
| 79 |
+
rec_e8: exp(-8.0 · |Δ|) (short-memory / locality kernel)
|
| 80 |
+
"""
|
| 81 |
+
B, T = positions.shape
|
| 82 |
+
delta = (positions.float() - float(L - 1)) / float(L) # (B, T)
|
| 83 |
+
abs_d = delta.abs()
|
| 84 |
+
rec_lin = delta
|
| 85 |
+
rec_log = torch.sign(delta) * torch.log1p(abs_d) / math.log(2.0)
|
| 86 |
+
rec_e05 = torch.exp(-0.5 * abs_d)
|
| 87 |
+
rec_e2 = torch.exp(-2.0 * abs_d)
|
| 88 |
+
rec_e8 = torch.exp(-8.0 * abs_d)
|
| 89 |
+
return torch.stack([rec_lin, rec_log, rec_e05, rec_e2, rec_e8], dim=-1)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _positional_encoding(
|
| 93 |
+
positions: torch.Tensor, periods: torch.Tensor, L: int,
|
| 94 |
+
n_harmonics: int = 1,
|
| 95 |
+
) -> torch.Tensor:
|
| 96 |
+
"""Full shared positional encoding (phase + bounded recency basis).
|
| 97 |
+
|
| 98 |
+
Returns (B, T, 2·K·n_harmonics + 5).
|
| 99 |
+
"""
|
| 100 |
+
return torch.cat(
|
| 101 |
+
[
|
| 102 |
+
_phase_encoding(positions, periods, n_harmonics=n_harmonics),
|
| 103 |
+
_recency_encoding(positions, L),
|
| 104 |
+
],
|
| 105 |
+
dim=-1,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _norm_fp32(norm: nn.Module, x: torch.Tensor) -> torch.Tensor:
|
| 110 |
+
"""Apply RMSNorm in fp32 and cast the result back to the input's dtype.
|
| 111 |
+
|
| 112 |
+
Under bf16-mixed AMP, RMSNorm receives bf16 input but holds fp32 weights.
|
| 113 |
+
PyTorch's fused RMSNorm kernel falls back to a slow non-fused path on
|
| 114 |
+
dtype mismatch. Explicit fp32 promotion matches the weight dtype and lets
|
| 115 |
+
the fused kernel engage. This matters most under compile, where the unfused
|
| 116 |
+
dispatch breaks the graph and prevents downstream fusions.
|
| 117 |
+
"""
|
| 118 |
+
if x.dtype == torch.float32:
|
| 119 |
+
return norm(x)
|
| 120 |
+
with torch.amp.autocast(
|
| 121 |
+
device_type=x.device.type if x.is_cuda else "cpu", enabled=False,
|
| 122 |
+
):
|
| 123 |
+
return norm(x.float()).to(x.dtype)
|
tinycast/eval.py
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TinyCast GIFT-Eval driver.
|
| 2 |
+
|
| 3 |
+
Runs the deployed TinyCast predictor against the GIFT-Eval benchmark and writes
|
| 4 |
+
a results CSV in the leaderboard column order (98 lines incl. header, 15
|
| 5 |
+
columns), plus a per-frequency-bin summary sidecar carrying the three
|
| 6 |
+
normalized aggregates (nGMASE, nCRPS, nMSIS).
|
| 7 |
+
|
| 8 |
+
``summarize_by_freq_bin`` also runs standalone on any leaderboard-format CSV, so
|
| 9 |
+
the published aggregates can be re-derived offline from the pinned per-config
|
| 10 |
+
results in ``reference/gift_eval_tinycast.csv`` without a GPU or benchmark data.
|
| 11 |
+
Called that way it only returns and prints the summary; pass
|
| 12 |
+
``write_sidecar=True`` to also write it beside the CSV.
|
| 13 |
+
|
| 14 |
+
Usage:
|
| 15 |
+
python -m tinycast.eval --ckpt model.safetensors --flip \\
|
| 16 |
+
--output all_results.csv # full 97 configs
|
| 17 |
+
python -m tinycast.eval --ckpt model.safetensors --flip \\
|
| 18 |
+
--configs "m4_hourly/H/short" --output smoke.csv
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import argparse
|
| 23 |
+
import csv
|
| 24 |
+
import json
|
| 25 |
+
import math
|
| 26 |
+
import os
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from typing import Iterable, List, Optional, Sequence
|
| 29 |
+
|
| 30 |
+
import numpy as np
|
| 31 |
+
|
| 32 |
+
MODEL_NAME_DEFAULT = "TinyCast"
|
| 33 |
+
|
| 34 |
+
# ---------------------------------------------------------------------------
|
| 35 |
+
# Dataset constants: the GIFT-Eval protocol's dataset / term layout.
|
| 36 |
+
# ---------------------------------------------------------------------------
|
| 37 |
+
PRETTY_NAMES = {
|
| 38 |
+
"saugeenday": "saugeen",
|
| 39 |
+
"temperature_rain_with_missing": "temperature_rain",
|
| 40 |
+
"kdd_cup_2018_with_missing": "kdd_cup_2018",
|
| 41 |
+
"car_parts_with_missing": "car_parts",
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
SHORT_DATASETS = (
|
| 45 |
+
"m4_yearly m4_quarterly m4_monthly m4_weekly m4_daily m4_hourly "
|
| 46 |
+
"electricity/15T electricity/H electricity/D electricity/W "
|
| 47 |
+
"solar/10T solar/H solar/D solar/W "
|
| 48 |
+
"hospital covid_deaths "
|
| 49 |
+
"us_births/D us_births/M us_births/W "
|
| 50 |
+
"saugeenday/D saugeenday/M saugeenday/W "
|
| 51 |
+
"temperature_rain_with_missing "
|
| 52 |
+
"kdd_cup_2018_with_missing/H kdd_cup_2018_with_missing/D "
|
| 53 |
+
"car_parts_with_missing restaurant "
|
| 54 |
+
"hierarchical_sales/D hierarchical_sales/W "
|
| 55 |
+
"LOOP_SEATTLE/5T LOOP_SEATTLE/H LOOP_SEATTLE/D "
|
| 56 |
+
"SZ_TAXI/15T SZ_TAXI/H "
|
| 57 |
+
"M_DENSE/H M_DENSE/D "
|
| 58 |
+
"ett1/15T ett1/H ett1/D ett1/W ett2/W ett2/D "
|
| 59 |
+
"jena_weather/10T jena_weather/H jena_weather/D "
|
| 60 |
+
"bitbrains_fast_storage/5T bitbrains_fast_storage/H "
|
| 61 |
+
"bitbrains_rnd/5T bitbrains_rnd/H "
|
| 62 |
+
"bizitobs_application bizitobs_service "
|
| 63 |
+
"bizitobs_l2c/5T bizitobs_l2c/H"
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
MED_LONG_DATASETS = (
|
| 67 |
+
"electricity/15T electricity/H "
|
| 68 |
+
"solar/10T solar/H "
|
| 69 |
+
"kdd_cup_2018_with_missing/H "
|
| 70 |
+
"LOOP_SEATTLE/5T LOOP_SEATTLE/H "
|
| 71 |
+
"SZ_TAXI/15T M_DENSE/H "
|
| 72 |
+
"ett1/15T ett1/H ett2/15T ett2/H "
|
| 73 |
+
"jena_weather/10T jena_weather/H "
|
| 74 |
+
"bitbrains_fast_storage/5T bitbrains_rnd/5T "
|
| 75 |
+
"bizitobs_application bizitobs_service "
|
| 76 |
+
"bizitobs_l2c/5T bizitobs_l2c/H"
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# Leaderboard CSV column order (byte-identical for leaderboard submission).
|
| 80 |
+
LEADERBOARD_COLUMNS = (
|
| 81 |
+
"dataset", "model",
|
| 82 |
+
"eval_metrics/MSE[mean]", "eval_metrics/MSE[0.5]",
|
| 83 |
+
"eval_metrics/MAE[0.5]", "eval_metrics/MASE[0.5]",
|
| 84 |
+
"eval_metrics/MAPE[0.5]", "eval_metrics/sMAPE[0.5]",
|
| 85 |
+
"eval_metrics/MSIS", "eval_metrics/RMSE[mean]",
|
| 86 |
+
"eval_metrics/NRMSE[mean]", "eval_metrics/ND[0.5]",
|
| 87 |
+
"eval_metrics/mean_weighted_sum_quantile_loss",
|
| 88 |
+
"domain", "num_variates",
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
REFERENCE_DIR = Path(__file__).parent / "reference"
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _build_metrics():
|
| 95 |
+
from gluonts.ev.metrics import (
|
| 96 |
+
MAE, MAPE, MASE, MSE, MSIS, ND, NRMSE, RMSE, SMAPE,
|
| 97 |
+
MeanWeightedSumQuantileLoss,
|
| 98 |
+
)
|
| 99 |
+
return [
|
| 100 |
+
MSE(forecast_type="mean"),
|
| 101 |
+
MSE(forecast_type=0.5),
|
| 102 |
+
MAE(),
|
| 103 |
+
MASE(),
|
| 104 |
+
MAPE(),
|
| 105 |
+
SMAPE(),
|
| 106 |
+
MSIS(),
|
| 107 |
+
RMSE(),
|
| 108 |
+
NRMSE(),
|
| 109 |
+
ND(),
|
| 110 |
+
MeanWeightedSumQuantileLoss(
|
| 111 |
+
quantile_levels=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
|
| 112 |
+
),
|
| 113 |
+
]
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _load_dataset_properties() -> dict:
|
| 117 |
+
with open(REFERENCE_DIR / "dataset_properties.json") as f:
|
| 118 |
+
return json.load(f)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def iter_configs(
|
| 122 |
+
configs_filter: Optional[Sequence[str]] = None,
|
| 123 |
+
term_filter: Optional[str] = None,
|
| 124 |
+
) -> Iterable[tuple]:
|
| 125 |
+
"""Yield ``(ds_name, term, ds_config_key, ds_key, ds_freq)`` per (dataset, term).
|
| 126 |
+
|
| 127 |
+
``configs_filter`` matches by full ``ds_config_key`` (e.g.
|
| 128 |
+
``ett1/15T/long``); if ``None`` (default), all 97 configs are yielded.
|
| 129 |
+
"""
|
| 130 |
+
dataset_properties = _load_dataset_properties()
|
| 131 |
+
all_datasets = sorted(set(SHORT_DATASETS.split() + MED_LONG_DATASETS.split()))
|
| 132 |
+
med_long_set = set(MED_LONG_DATASETS.split())
|
| 133 |
+
all_terms = ["short", "medium", "long"] if term_filter is None else [term_filter]
|
| 134 |
+
wanted = set(configs_filter) if configs_filter is not None else None
|
| 135 |
+
|
| 136 |
+
for ds_name in all_datasets:
|
| 137 |
+
if "/" in ds_name:
|
| 138 |
+
ds_key = PRETTY_NAMES.get(ds_name.split("/")[0].lower(), ds_name.split("/")[0].lower())
|
| 139 |
+
ds_freq = ds_name.split("/")[1]
|
| 140 |
+
else:
|
| 141 |
+
ds_key = PRETTY_NAMES.get(ds_name.lower(), ds_name.lower())
|
| 142 |
+
ds_freq = dataset_properties[ds_key]["frequency"]
|
| 143 |
+
|
| 144 |
+
for term in all_terms:
|
| 145 |
+
if term in ("medium", "long") and ds_name not in med_long_set:
|
| 146 |
+
continue
|
| 147 |
+
ds_config_key = f"{ds_key}/{ds_freq}/{term}"
|
| 148 |
+
if wanted is not None and ds_config_key not in wanted:
|
| 149 |
+
continue
|
| 150 |
+
yield ds_name, term, ds_config_key, ds_key, ds_freq
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def evaluate(
|
| 154 |
+
ckpt_path: str,
|
| 155 |
+
output_csv: os.PathLike,
|
| 156 |
+
*,
|
| 157 |
+
model_name: str = MODEL_NAME_DEFAULT,
|
| 158 |
+
configs_filter: Optional[Sequence[str]] = None,
|
| 159 |
+
term_filter: Optional[str] = None,
|
| 160 |
+
force_flip_invariance: bool = True,
|
| 161 |
+
device: Optional[str] = None,
|
| 162 |
+
use_amp: int = 1,
|
| 163 |
+
period_align: bool = True,
|
| 164 |
+
batch_size: int = 64,
|
| 165 |
+
predictor_batch_size: Optional[int] = None,
|
| 166 |
+
strict_summary: bool = True,
|
| 167 |
+
) -> dict:
|
| 168 |
+
"""Run GIFT-Eval for TinyCast; stream rows to ``output_csv``.
|
| 169 |
+
|
| 170 |
+
``device`` defaults to ``None``, which is the predictor's request to choose:
|
| 171 |
+
CUDA when it is present, CPU otherwise. Naming a device instead makes it a
|
| 172 |
+
requirement, and an absent one raises rather than being substituted.
|
| 173 |
+
|
| 174 |
+
Returns the ``summarize_by_freq_bin`` summary for the run it just wrote, so
|
| 175 |
+
a caller can assert on the aggregates without re-reading the CSV. The row
|
| 176 |
+
CSV is complete on disk before the summary runs, so a ``strict_summary``
|
| 177 |
+
failure costs no evaluation work: fix the reference and re-summarize.
|
| 178 |
+
"""
|
| 179 |
+
from gluonts.model import evaluate_model
|
| 180 |
+
from gluonts.time_feature import get_seasonality
|
| 181 |
+
from gift_eval.data import Dataset
|
| 182 |
+
|
| 183 |
+
from .predictor import TinyCastPredictor
|
| 184 |
+
|
| 185 |
+
metrics = _build_metrics()
|
| 186 |
+
dataset_properties = _load_dataset_properties()
|
| 187 |
+
|
| 188 |
+
output_csv = Path(output_csv)
|
| 189 |
+
output_csv.parent.mkdir(parents=True, exist_ok=True)
|
| 190 |
+
with open(output_csv, "w", newline="") as f:
|
| 191 |
+
csv.writer(f).writerow(LEADERBOARD_COLUMNS)
|
| 192 |
+
|
| 193 |
+
configs = list(iter_configs(configs_filter, term_filter))
|
| 194 |
+
print(f"Evaluating {model_name} on {len(configs)} configs.")
|
| 195 |
+
print(f"Output: {output_csv}")
|
| 196 |
+
|
| 197 |
+
for i, (ds_name, term, ds_config, ds_key, ds_freq) in enumerate(configs):
|
| 198 |
+
print(f"[{i + 1}/{len(configs)}] {ds_config}", flush=True)
|
| 199 |
+
|
| 200 |
+
probe = Dataset(name=ds_name, term=term, to_univariate=False)
|
| 201 |
+
to_uni = probe.target_dim != 1
|
| 202 |
+
dataset = Dataset(name=ds_name, term=term, to_univariate=to_uni)
|
| 203 |
+
season_length = get_seasonality(dataset.freq)
|
| 204 |
+
|
| 205 |
+
per_cfg_kwargs = dict(
|
| 206 |
+
device=device,
|
| 207 |
+
force_flip_invariance=bool(force_flip_invariance),
|
| 208 |
+
use_amp=use_amp,
|
| 209 |
+
)
|
| 210 |
+
if predictor_batch_size is not None:
|
| 211 |
+
per_cfg_kwargs["batch_size"] = predictor_batch_size
|
| 212 |
+
# bizitobs_l2c has no daily cycle, so scale_factor /= 7.
|
| 213 |
+
if "l2c" in ds_name.lower():
|
| 214 |
+
per_cfg_kwargs["no_daily"] = True
|
| 215 |
+
# Period-alignment downsampling: derive the aliasing factor from the
|
| 216 |
+
# test INPUT contexts only (fires k=7 on bizitobs_l2c/5T med+long).
|
| 217 |
+
if period_align:
|
| 218 |
+
from .downsample import freq_to_seconds, period_alignment_factor
|
| 219 |
+
fs = freq_to_seconds(ds_freq)
|
| 220 |
+
if fs is not None:
|
| 221 |
+
k_align = period_alignment_factor(
|
| 222 |
+
(np.asarray(e["target"], dtype=np.float64)
|
| 223 |
+
for e in dataset.test_data.input),
|
| 224 |
+
fs,
|
| 225 |
+
horizon=dataset.prediction_length,
|
| 226 |
+
)
|
| 227 |
+
if k_align > 1:
|
| 228 |
+
per_cfg_kwargs["downsample_factor"] = k_align
|
| 229 |
+
print(f" period-align: downsample_factor={k_align}", flush=True)
|
| 230 |
+
|
| 231 |
+
predictor = TinyCastPredictor(
|
| 232 |
+
prediction_length=dataset.prediction_length,
|
| 233 |
+
checkpoint_path=ckpt_path,
|
| 234 |
+
freq=ds_freq,
|
| 235 |
+
domain=dataset_properties[ds_key]["domain"],
|
| 236 |
+
**per_cfg_kwargs,
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
res = evaluate_model(
|
| 240 |
+
predictor,
|
| 241 |
+
test_data=dataset.test_data,
|
| 242 |
+
metrics=metrics,
|
| 243 |
+
batch_size=batch_size,
|
| 244 |
+
axis=None,
|
| 245 |
+
mask_invalid_label=True,
|
| 246 |
+
allow_nan_forecast=False,
|
| 247 |
+
seasonality=season_length,
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
with open(output_csv, "a", newline="") as f:
|
| 251 |
+
csv.writer(f).writerow([
|
| 252 |
+
ds_config, model_name,
|
| 253 |
+
res["MSE[mean]"].iloc[0], res["MSE[0.5]"].iloc[0],
|
| 254 |
+
res["MAE[0.5]"].iloc[0], res["MASE[0.5]"].iloc[0],
|
| 255 |
+
res["MAPE[0.5]"].iloc[0], res["sMAPE[0.5]"].iloc[0],
|
| 256 |
+
res["MSIS"].iloc[0], res["RMSE[mean]"].iloc[0],
|
| 257 |
+
res["NRMSE[mean]"].iloc[0], res["ND[0.5]"].iloc[0],
|
| 258 |
+
res["mean_weighted_sum_quantile_loss"].iloc[0],
|
| 259 |
+
dataset_properties[ds_key]["domain"],
|
| 260 |
+
dataset_properties[ds_key]["num_variates"],
|
| 261 |
+
])
|
| 262 |
+
|
| 263 |
+
import gc
|
| 264 |
+
del predictor, res, dataset, probe
|
| 265 |
+
gc.collect()
|
| 266 |
+
|
| 267 |
+
print(f"Done. Results: {output_csv}")
|
| 268 |
+
# A partial run (--configs / --term) covers a subset of the 97 leaderboard
|
| 269 |
+
# configurations, but every configuration it does cover must be scored, so
|
| 270 |
+
# the strict reference check stays on.
|
| 271 |
+
return summarize_by_freq_bin(
|
| 272 |
+
output_csv, strict=strict_summary, write_sidecar=True,
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
# ---------------------------------------------------------------------------
|
| 277 |
+
# Per-frequency-bin normalized summary (sidecar to the leaderboard CSV)
|
| 278 |
+
# ---------------------------------------------------------------------------
|
| 279 |
+
_FREQ_BIN_ORDER = ("sub_hourly", "hourly", "daily_or_coarser")
|
| 280 |
+
|
| 281 |
+
# The three reported GIFT-Eval aggregates, each the geometric mean of the
|
| 282 |
+
# per-configuration ratio of the column below to the same column of the
|
| 283 |
+
# seasonal-naive reference. One construction, three columns.
|
| 284 |
+
_NORMALIZED_METRICS = (
|
| 285 |
+
("ngmase", "eval_metrics/MASE[0.5]"),
|
| 286 |
+
("ncrps", "eval_metrics/mean_weighted_sum_quantile_loss"),
|
| 287 |
+
("nmsis", "eval_metrics/MSIS"),
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
# The GIFT-Eval leaderboard calls the quantile-loss aggregate nWQL; the paper
|
| 291 |
+
# calls it nCRPS. Same column, so the summary carries both spellings.
|
| 292 |
+
_METRIC_ALIASES = (("nwql", "ncrps"),)
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def _freq_bin(freq: str) -> str:
|
| 296 |
+
f = freq.strip().upper()
|
| 297 |
+
if f == "H":
|
| 298 |
+
return "hourly"
|
| 299 |
+
if f.endswith("S") or f.endswith("T") or f == "MIN":
|
| 300 |
+
return "sub_hourly"
|
| 301 |
+
return "daily_or_coarser"
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def _positive_float(value) -> Optional[float]:
|
| 305 |
+
"""Parse ``value``, returning ``None`` unless it is finite and positive.
|
| 306 |
+
|
| 307 |
+
Every aggregate here is a geometric mean, so a zero, a negative or a NaN is
|
| 308 |
+
not a small contribution: it is undefined.
|
| 309 |
+
"""
|
| 310 |
+
try:
|
| 311 |
+
out = float(value)
|
| 312 |
+
except (ValueError, TypeError):
|
| 313 |
+
return None
|
| 314 |
+
return out if math.isfinite(out) and out > 0 else None
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def _read_reference_metrics(reference_csv: os.PathLike) -> dict:
|
| 318 |
+
"""Map ``dataset`` to its seasonal-naive denominator per normalized metric."""
|
| 319 |
+
out: dict = {}
|
| 320 |
+
with open(reference_csv, newline="") as f:
|
| 321 |
+
for row in csv.DictReader(f):
|
| 322 |
+
ds = row.get("dataset")
|
| 323 |
+
if not ds:
|
| 324 |
+
continue
|
| 325 |
+
denominators = {}
|
| 326 |
+
for _, column in _NORMALIZED_METRICS:
|
| 327 |
+
den = _positive_float(row.get(column))
|
| 328 |
+
if den is not None:
|
| 329 |
+
denominators[column] = den
|
| 330 |
+
out[ds] = denominators
|
| 331 |
+
return out
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def _geometric_mean(values: Sequence[float]) -> Optional[float]:
|
| 335 |
+
if not values:
|
| 336 |
+
return None
|
| 337 |
+
return math.exp(sum(math.log(v) for v in values) / len(values))
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def summarize_by_freq_bin(
|
| 341 |
+
output_csv: os.PathLike,
|
| 342 |
+
*,
|
| 343 |
+
seasonal_naive_csv: "os.PathLike | None" = None,
|
| 344 |
+
strict: bool = True,
|
| 345 |
+
write_sidecar: bool = False,
|
| 346 |
+
) -> dict:
|
| 347 |
+
"""Read ``output_csv``, print the per-frequency-bin summary and return it.
|
| 348 |
+
|
| 349 |
+
Three aggregates are reported, all built the same way: the geometric mean
|
| 350 |
+
over configurations of the ratio between a column and the same column of the
|
| 351 |
+
seasonal-naive reference. ``ngmase`` normalizes MASE (point accuracy),
|
| 352 |
+
``ncrps`` normalizes the mean weighted sum quantile loss (probabilistic
|
| 353 |
+
accuracy, the leaderboard's CRPS column, carried under both keys),
|
| 354 |
+
and ``nmsis`` normalizes MSIS (interval score).
|
| 355 |
+
|
| 356 |
+
``write_sidecar`` (default off) additionally writes the returned summary to
|
| 357 |
+
``<stem>.bins.json`` beside ``output_csv``. It is off by default because the
|
| 358 |
+
common call re-derives the published aggregates from the pinned results
|
| 359 |
+
inside the installed package, where the sidecar path lands in
|
| 360 |
+
``site-packages``: reading a file must not write one. ``evaluate`` turns it
|
| 361 |
+
on, since there the CSV is the caller's own output path.
|
| 362 |
+
|
| 363 |
+
``strict`` (default on) raises when any configuration in ``output_csv`` is
|
| 364 |
+
left out of an aggregate, whether because its dataset key is unparseable,
|
| 365 |
+
because the reference file does not cover it, or because a value is missing
|
| 366 |
+
or non-positive. A geometric mean says nothing about how many terms it has,
|
| 367 |
+
so a reference file that has drifted away from the run would otherwise
|
| 368 |
+
quietly shrink the sample and return a number that looks publishable. The
|
| 369 |
+
dropped configurations are recorded under ``summary["dropped"]`` either way,
|
| 370 |
+
so a caller running with ``strict=False`` can still assert on the count.
|
| 371 |
+
"""
|
| 372 |
+
output_csv = Path(output_csv)
|
| 373 |
+
if seasonal_naive_csv is None:
|
| 374 |
+
seasonal_naive_csv = REFERENCE_DIR / "seasonal_naive.csv"
|
| 375 |
+
seasonal_naive_csv = Path(seasonal_naive_csv)
|
| 376 |
+
reference = (
|
| 377 |
+
_read_reference_metrics(seasonal_naive_csv)
|
| 378 |
+
if seasonal_naive_csv.exists()
|
| 379 |
+
else {}
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
mase_per_bin = {b: [] for b in _FREQ_BIN_ORDER}
|
| 383 |
+
ratios = {key: {b: [] for b in _FREQ_BIN_ORDER}
|
| 384 |
+
for key, _ in _NORMALIZED_METRICS}
|
| 385 |
+
rows_per_bin = {b: 0 for b in _FREQ_BIN_ORDER}
|
| 386 |
+
dropped_reasons: dict = {}
|
| 387 |
+
n_rows = 0
|
| 388 |
+
|
| 389 |
+
def drop(dataset: str, reason: str) -> None:
|
| 390 |
+
dropped_reasons.setdefault(reason, []).append(dataset)
|
| 391 |
+
|
| 392 |
+
with open(output_csv, newline="") as f:
|
| 393 |
+
for row in csv.DictReader(f):
|
| 394 |
+
n_rows += 1
|
| 395 |
+
ds = row.get("dataset") or ""
|
| 396 |
+
parts = ds.split("/")
|
| 397 |
+
if len(parts) < 3:
|
| 398 |
+
drop(ds, "unparseable_dataset_key")
|
| 399 |
+
continue
|
| 400 |
+
bin_name = _freq_bin(parts[1])
|
| 401 |
+
rows_per_bin[bin_name] += 1
|
| 402 |
+
|
| 403 |
+
mase = _positive_float(row.get("eval_metrics/MASE[0.5]"))
|
| 404 |
+
if mase is not None:
|
| 405 |
+
mase_per_bin[bin_name].append(mase)
|
| 406 |
+
|
| 407 |
+
denominators = reference.get(ds)
|
| 408 |
+
if denominators is None:
|
| 409 |
+
drop(ds, "no_seasonal_naive_reference")
|
| 410 |
+
continue
|
| 411 |
+
for key, column in _NORMALIZED_METRICS:
|
| 412 |
+
numerator = _positive_float(row.get(column))
|
| 413 |
+
if numerator is None:
|
| 414 |
+
drop(ds, f"no_value_{key}")
|
| 415 |
+
elif column not in denominators:
|
| 416 |
+
drop(ds, f"no_reference_value_{key}")
|
| 417 |
+
else:
|
| 418 |
+
ratios[key][bin_name].append(numerator / denominators[column])
|
| 419 |
+
|
| 420 |
+
summary: dict = {}
|
| 421 |
+
for bin_name in list(_FREQ_BIN_ORDER) + ["overall"]:
|
| 422 |
+
bins = _FREQ_BIN_ORDER if bin_name == "overall" else (bin_name,)
|
| 423 |
+
mase_vals = [v for b in bins for v in mase_per_bin[b]]
|
| 424 |
+
entry = {
|
| 425 |
+
"n": sum(rows_per_bin[b] for b in bins),
|
| 426 |
+
"gmean_mase": _geometric_mean(mase_vals),
|
| 427 |
+
}
|
| 428 |
+
for key, _ in _NORMALIZED_METRICS:
|
| 429 |
+
vals = [v for b in bins for v in ratios[key][b]]
|
| 430 |
+
entry[key] = _geometric_mean(vals)
|
| 431 |
+
entry[f"n_{key}"] = len(vals)
|
| 432 |
+
for alias, key in _METRIC_ALIASES:
|
| 433 |
+
entry[alias] = entry[key]
|
| 434 |
+
entry[f"n_{alias}"] = entry[f"n_{key}"]
|
| 435 |
+
summary[bin_name] = entry
|
| 436 |
+
|
| 437 |
+
n_dropped = sum(len(v) for v in dropped_reasons.values())
|
| 438 |
+
summary["dropped"] = {
|
| 439 |
+
"n_rows": n_rows,
|
| 440 |
+
"n_dropped": n_dropped,
|
| 441 |
+
"by_reason": {k: sorted(set(v)) for k, v in sorted(dropped_reasons.items())},
|
| 442 |
+
}
|
| 443 |
+
summary["reference"] = str(seasonal_naive_csv)
|
| 444 |
+
|
| 445 |
+
if write_sidecar:
|
| 446 |
+
sidecar = output_csv.with_suffix(".bins.json")
|
| 447 |
+
sidecar.write_text(json.dumps(summary, indent=2))
|
| 448 |
+
|
| 449 |
+
def fmt(value) -> str:
|
| 450 |
+
return f"{value:.4f}" if value is not None else "n/a"
|
| 451 |
+
|
| 452 |
+
header = "Per-frequency-bin GIFT-Eval aggregates"
|
| 453 |
+
if write_sidecar:
|
| 454 |
+
header += f" (sidecar: {sidecar.name})"
|
| 455 |
+
print(header)
|
| 456 |
+
print(f" {'bin':<18} {'n':>3} {'nGMASE':>8} {'nCRPS':>8} {'nMSIS':>8}"
|
| 457 |
+
f" {'gmean MASE':>11}")
|
| 458 |
+
for bin_name in list(_FREQ_BIN_ORDER) + ["overall"]:
|
| 459 |
+
s = summary[bin_name]
|
| 460 |
+
print(f" {bin_name:<18} {s['n']:>3} {fmt(s['ngmase']):>8}"
|
| 461 |
+
f" {fmt(s['ncrps']):>8} {fmt(s['nmsis']):>8}"
|
| 462 |
+
f" {fmt(s['gmean_mase']):>11}")
|
| 463 |
+
|
| 464 |
+
if n_dropped:
|
| 465 |
+
detail = ", ".join(f"{reason}: {len(set(datasets))}"
|
| 466 |
+
for reason, datasets in sorted(dropped_reasons.items()))
|
| 467 |
+
message = (
|
| 468 |
+
f"{n_dropped} of {n_rows} configurations in {output_csv} were left "
|
| 469 |
+
f"out of an aggregate ({detail}). The reference "
|
| 470 |
+
f"{seasonal_naive_csv} does not match this run, so the aggregates "
|
| 471 |
+
f"above are geometric means over an incomplete sample."
|
| 472 |
+
)
|
| 473 |
+
if strict:
|
| 474 |
+
raise ValueError(message)
|
| 475 |
+
print(f" WARNING: {message}")
|
| 476 |
+
|
| 477 |
+
return summary
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def _parse_configs_arg(value: Optional[str]) -> Optional[List[str]]:
|
| 481 |
+
if value is None:
|
| 482 |
+
return None
|
| 483 |
+
return [c.strip() for c in value.split(",") if c.strip()]
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def main(argv: Optional[Sequence[str]] = None) -> None:
|
| 487 |
+
parser = argparse.ArgumentParser(
|
| 488 |
+
description="Run GIFT-Eval against the deployed TinyCast model."
|
| 489 |
+
)
|
| 490 |
+
parser.add_argument("--ckpt", required=True,
|
| 491 |
+
help="Path to model.safetensors (config.json sibling).")
|
| 492 |
+
parser.add_argument("--configs", default=None,
|
| 493 |
+
help="Comma-separated ds_config_keys (e.g. "
|
| 494 |
+
"'ett1/15T/long,m4_hourly/H/short'). Default: all 97.")
|
| 495 |
+
parser.add_argument("--term", default=None, choices=["short", "medium", "long"])
|
| 496 |
+
parser.add_argument("--flip", dest="force_flip_invariance",
|
| 497 |
+
action="store_true", default=True,
|
| 498 |
+
help="Flip-invariance symmetrization (deployed default ON).")
|
| 499 |
+
parser.add_argument("--no-flip", dest="force_flip_invariance",
|
| 500 |
+
action="store_false")
|
| 501 |
+
parser.add_argument("--no-period-align", action="store_true",
|
| 502 |
+
help="Disable period-alignment downsampling (default ON).")
|
| 503 |
+
parser.add_argument("--dtype", default="bf16", choices=["bf16", "fp32-strict"],
|
| 504 |
+
help="bf16 autocast at compute (CUDA only) or strict fp32.")
|
| 505 |
+
parser.add_argument("--device", default=None,
|
| 506 |
+
help="cuda | cpu. Default: whichever is present "
|
| 507 |
+
"(CUDA when available, else CPU). Naming one "
|
| 508 |
+
"makes it a requirement: an absent device is an "
|
| 509 |
+
"error, never a silent substitution.")
|
| 510 |
+
parser.add_argument("--batch-size", type=int, default=64,
|
| 511 |
+
help="gluonts.evaluate_model aggregation batch size.")
|
| 512 |
+
parser.add_argument("--predictor-batch-size", type=int, default=None,
|
| 513 |
+
help="Internal predictor batch size (default 256).")
|
| 514 |
+
parser.add_argument("--model-name", default=MODEL_NAME_DEFAULT,
|
| 515 |
+
help="Value written to the 'model' CSV column.")
|
| 516 |
+
parser.add_argument("--output", default="all_results.csv",
|
| 517 |
+
help="Output CSV path (leaderboard format).")
|
| 518 |
+
parser.add_argument("--no-strict-summary", action="store_true",
|
| 519 |
+
help="Warn instead of failing when the seasonal-naive "
|
| 520 |
+
"reference does not cover every evaluated config.")
|
| 521 |
+
args = parser.parse_args(argv)
|
| 522 |
+
|
| 523 |
+
evaluate(
|
| 524 |
+
ckpt_path=args.ckpt,
|
| 525 |
+
output_csv=args.output,
|
| 526 |
+
model_name=args.model_name,
|
| 527 |
+
configs_filter=_parse_configs_arg(args.configs),
|
| 528 |
+
term_filter=args.term,
|
| 529 |
+
force_flip_invariance=bool(args.force_flip_invariance),
|
| 530 |
+
device=args.device,
|
| 531 |
+
use_amp=0 if args.dtype == "fp32-strict" else 1,
|
| 532 |
+
period_align=not args.no_period_align,
|
| 533 |
+
batch_size=args.batch_size,
|
| 534 |
+
predictor_batch_size=args.predictor_batch_size,
|
| 535 |
+
strict_summary=not args.no_strict_summary,
|
| 536 |
+
)
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
if __name__ == "__main__":
|
| 540 |
+
main()
|
tinycast/export.py
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training checkpoint to released artifact.
|
| 2 |
+
|
| 3 |
+
The released model is a ``model.safetensors`` + ``config.json`` pair; this module
|
| 4 |
+
is the bridge that produces it from whatever a training run left on disk.
|
| 5 |
+
|
| 6 |
+
Two things make that bridge non-trivial:
|
| 7 |
+
|
| 8 |
+
* **Weight tying.** Both FFN stacks are shared, so a naive ``state_dict()`` has 177
|
| 9 |
+
entries summing to 321,225 values while the model has only 121 distinct
|
| 10 |
+
parameter tensors holding 146,505 values. The export writes each storage once and
|
| 11 |
+
records the aliases in the safetensors metadata, which is what
|
| 12 |
+
:func:`tinycast.checkpoint.load_checkpoint` reads back. A parameter count is only
|
| 13 |
+
meaningful when taken from a model instantiated from its config; never sum a
|
| 14 |
+
checkpoint's tensors.
|
| 15 |
+
* **Checkpoint averaging.** The released weights are the uniform mean of the last
|
| 16 |
+
eight periodic checkpoints of the training run, so exporting the final checkpoint
|
| 17 |
+
alone does not reproduce them. :func:`average_checkpoints` is that mean.
|
| 18 |
+
|
| 19 |
+
Loading is deliberately restricted: torch-serialized files are read with
|
| 20 |
+
``weights_only=True`` and safetensors files with the safetensors reader, so no path
|
| 21 |
+
handed to this module can execute code.
|
| 22 |
+
|
| 23 |
+
Usage:
|
| 24 |
+
# the last eight periodic checkpoints, in training order
|
| 25 |
+
python -m tinycast.export --out-dir release/ --average CKPT [CKPT ...]
|
| 26 |
+
|
| 27 |
+
>>> from tinycast.export import export_safetensors, check_export_roundtrip
|
| 28 |
+
>>> export_safetensors(model, "release/")
|
| 29 |
+
>>> check_export_roundtrip(model, expect_parameters=146_505)
|
| 30 |
+
"""
|
| 31 |
+
from __future__ import annotations
|
| 32 |
+
|
| 33 |
+
import json
|
| 34 |
+
import math
|
| 35 |
+
import tempfile
|
| 36 |
+
from contextlib import nullcontext
|
| 37 |
+
from pathlib import Path
|
| 38 |
+
from typing import Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
|
| 39 |
+
|
| 40 |
+
import torch
|
| 41 |
+
import torch.nn as nn
|
| 42 |
+
|
| 43 |
+
from .config import TinyCastConfig
|
| 44 |
+
from .model import TinyCastForPrediction
|
| 45 |
+
|
| 46 |
+
WEIGHTS_NAME = "model.safetensors"
|
| 47 |
+
CONFIG_NAME = "config.json"
|
| 48 |
+
|
| 49 |
+
#: Number of periodic checkpoints averaged to produce the released weights.
|
| 50 |
+
RELEASE_AVERAGE_N = 8
|
| 51 |
+
|
| 52 |
+
#: Keys a training checkpoint may nest its tensors under, most specific first.
|
| 53 |
+
_STATE_DICT_KEYS = ("state_dict", "model_state_dict", "model", "weights")
|
| 54 |
+
|
| 55 |
+
StateDict = Dict[str, torch.Tensor]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class ExportError(RuntimeError):
|
| 59 |
+
"""A checkpoint could not be read, averaged, or exported faithfully."""
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# ---------------------------------------------------------------------------
|
| 63 |
+
# Safe loading
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
+
def load_state_dict_safely(path: Union[str, Path]) -> StateDict:
|
| 66 |
+
"""Read a state dict from ``path`` without ever executing code from it.
|
| 67 |
+
|
| 68 |
+
``.safetensors`` goes through the safetensors reader; everything else through
|
| 69 |
+
``torch.load(..., weights_only=True)``. A training checkpoint that nests its
|
| 70 |
+
tensors under ``state_dict`` (or ``model_state_dict`` / ``model`` / ``weights``)
|
| 71 |
+
is unwrapped; the optimizer state, schedulers and step counters around it are
|
| 72 |
+
discarded, since none of them survive into the released artifact.
|
| 73 |
+
"""
|
| 74 |
+
p = Path(path)
|
| 75 |
+
if not p.is_file():
|
| 76 |
+
raise ExportError(f"no such checkpoint: {p}")
|
| 77 |
+
|
| 78 |
+
if p.suffix == ".safetensors":
|
| 79 |
+
from safetensors.torch import load_file
|
| 80 |
+
|
| 81 |
+
return dict(load_file(str(p), device="cpu"))
|
| 82 |
+
|
| 83 |
+
obj = torch.load(str(p), map_location="cpu", weights_only=True)
|
| 84 |
+
return _unwrap_state_dict(obj, p)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _unwrap_state_dict(obj: object, origin: Path) -> StateDict:
|
| 88 |
+
if not isinstance(obj, Mapping):
|
| 89 |
+
raise ExportError(
|
| 90 |
+
f"{origin}: expected a state dict or a checkpoint mapping, got "
|
| 91 |
+
f"{type(obj).__name__}"
|
| 92 |
+
)
|
| 93 |
+
if _is_tensor_mapping(obj):
|
| 94 |
+
return dict(obj)
|
| 95 |
+
for key in _STATE_DICT_KEYS:
|
| 96 |
+
inner = obj.get(key)
|
| 97 |
+
if isinstance(inner, Mapping) and _is_tensor_mapping(inner):
|
| 98 |
+
return dict(inner)
|
| 99 |
+
raise ExportError(
|
| 100 |
+
f"{origin}: no tensor mapping found at the top level or under any of "
|
| 101 |
+
f"{_STATE_DICT_KEYS}"
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _is_tensor_mapping(obj: Mapping) -> bool:
|
| 106 |
+
return bool(obj) and all(
|
| 107 |
+
isinstance(k, str) and isinstance(v, torch.Tensor) for k, v in obj.items()
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# ---------------------------------------------------------------------------
|
| 112 |
+
# Checkpoint averaging
|
| 113 |
+
# ---------------------------------------------------------------------------
|
| 114 |
+
def average_checkpoints(paths: Sequence[Union[str, Path]]) -> StateDict:
|
| 115 |
+
"""Uniform mean of several checkpoints, returned as a state dict.
|
| 116 |
+
|
| 117 |
+
This is how the released weights were made: the last
|
| 118 |
+
:data:`RELEASE_AVERAGE_N` periodic checkpoints of the training run, averaged
|
| 119 |
+
with equal weight. Floating-point tensors are accumulated in float32 in the
|
| 120 |
+
order given and cast back to the source dtype; integer and boolean entries (step
|
| 121 |
+
counters and the like) are taken from the last checkpoint in ``paths``, since a
|
| 122 |
+
mean of those is meaningless. Pass the checkpoints in training order so "last"
|
| 123 |
+
is the most recent one, and keep that order to reproduce the released weights
|
| 124 |
+
exactly: an eight-term float32 sum is not associative, so reordering ``paths``
|
| 125 |
+
moves the result by about one ulp.
|
| 126 |
+
|
| 127 |
+
All checkpoints must carry the same keys with the same shapes; a mismatch is an
|
| 128 |
+
error rather than a silent intersection, because a quietly dropped tensor would
|
| 129 |
+
export as a randomly initialized one.
|
| 130 |
+
"""
|
| 131 |
+
paths = [Path(p) for p in paths]
|
| 132 |
+
if not paths:
|
| 133 |
+
raise ExportError("average_checkpoints needs at least one checkpoint")
|
| 134 |
+
|
| 135 |
+
states = [load_state_dict_safely(p) for p in paths]
|
| 136 |
+
reference = states[0]
|
| 137 |
+
for path, state in zip(paths[1:], states[1:]):
|
| 138 |
+
_assert_same_keys(reference, state, paths[0], path)
|
| 139 |
+
|
| 140 |
+
n = len(states)
|
| 141 |
+
averaged: StateDict = {}
|
| 142 |
+
for key, ref in reference.items():
|
| 143 |
+
tensors = [state[key] for state in states]
|
| 144 |
+
if ref.is_floating_point():
|
| 145 |
+
acc = tensors[0].to(torch.float32).clone()
|
| 146 |
+
for t in tensors[1:]:
|
| 147 |
+
acc += t.to(torch.float32)
|
| 148 |
+
averaged[key] = (acc / n).to(ref.dtype)
|
| 149 |
+
else:
|
| 150 |
+
averaged[key] = tensors[-1].clone()
|
| 151 |
+
return averaged
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _assert_same_keys(a: StateDict, b: StateDict, path_a: Path, path_b: Path) -> None:
|
| 155 |
+
if a.keys() != b.keys():
|
| 156 |
+
only_a = sorted(set(a) - set(b))[:5]
|
| 157 |
+
only_b = sorted(set(b) - set(a))[:5]
|
| 158 |
+
raise ExportError(
|
| 159 |
+
f"checkpoint key mismatch between {path_a.name} and {path_b.name}: "
|
| 160 |
+
f"only in the first {only_a}, only in the second {only_b}"
|
| 161 |
+
)
|
| 162 |
+
for key in a:
|
| 163 |
+
if a[key].shape != b[key].shape:
|
| 164 |
+
raise ExportError(
|
| 165 |
+
f"shape mismatch for {key!r}: {tuple(a[key].shape)} in "
|
| 166 |
+
f"{path_a.name} vs {tuple(b[key].shape)} in {path_b.name}"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# ---------------------------------------------------------------------------
|
| 171 |
+
# Weight tying
|
| 172 |
+
# ---------------------------------------------------------------------------
|
| 173 |
+
def tied_parameter_groups(model: nn.Module) -> Dict[str, List[str]]:
|
| 174 |
+
"""Map each shared parameter's canonical name to the names aliasing it.
|
| 175 |
+
|
| 176 |
+
Two names alias when they resolve to the same ``nn.Parameter`` object, which is
|
| 177 |
+
what ``share_ffn`` produces. The canonical name is the one ``named_parameters``
|
| 178 |
+
keeps, i.e. the first in registration order; only that one is written to the
|
| 179 |
+
safetensors file.
|
| 180 |
+
"""
|
| 181 |
+
canonical = {id(p): name for name, p in model.named_parameters()}
|
| 182 |
+
groups: Dict[str, List[str]] = {name: [] for name in canonical.values()}
|
| 183 |
+
for name, param in model.named_parameters(remove_duplicate=False):
|
| 184 |
+
head = canonical[id(param)]
|
| 185 |
+
if name != head:
|
| 186 |
+
groups[head].append(name)
|
| 187 |
+
return {head: alias for head, alias in groups.items() if alias}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _assert_tied_values_agree(model: nn.Module, state: Mapping[str, torch.Tensor]) -> None:
|
| 191 |
+
"""Refuse a state dict whose tied entries disagree.
|
| 192 |
+
|
| 193 |
+
Loading such a dict would keep whichever copy happened to be written last and
|
| 194 |
+
discard the others without a word.
|
| 195 |
+
"""
|
| 196 |
+
for head, aliases in tied_parameter_groups(model).items():
|
| 197 |
+
if head not in state:
|
| 198 |
+
continue
|
| 199 |
+
for alias in aliases:
|
| 200 |
+
if alias in state and not torch.equal(state[head], state[alias]):
|
| 201 |
+
raise ExportError(
|
| 202 |
+
f"tied parameters disagree in the checkpoint: {alias!r} differs "
|
| 203 |
+
f"from {head!r}; the checkpoint does not come from this "
|
| 204 |
+
f"architecture"
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# ---------------------------------------------------------------------------
|
| 209 |
+
# Export
|
| 210 |
+
# ---------------------------------------------------------------------------
|
| 211 |
+
def export_safetensors(
|
| 212 |
+
model_or_state_dict: Union[nn.Module, Mapping[str, torch.Tensor]],
|
| 213 |
+
out_dir: Union[str, Path],
|
| 214 |
+
config: Optional[TinyCastConfig] = None,
|
| 215 |
+
expect_parameters: Optional[int] = None,
|
| 216 |
+
) -> Dict[str, object]:
|
| 217 |
+
"""Write the released ``model.safetensors`` + ``config.json`` pair to ``out_dir``.
|
| 218 |
+
|
| 219 |
+
Accepts either a live :class:`~tinycast.model.TinyCastForPrediction` or a state
|
| 220 |
+
dict from a training run (in which case ``config`` says which architecture to
|
| 221 |
+
rebuild; it defaults to the released one). Tied FFN weights are written once
|
| 222 |
+
each and their aliases recorded in the safetensors metadata, so the file holds
|
| 223 |
+
the model's true 146,505-value footprint rather than the 321,225 values a
|
| 224 |
+
``state_dict`` reports.
|
| 225 |
+
|
| 226 |
+
Pass ``expect_parameters`` to pin the count (146,505 for the released model);
|
| 227 |
+
the check is against a model instantiated from ``config``, never against a sum
|
| 228 |
+
of checkpoint tensors.
|
| 229 |
+
|
| 230 |
+
Re-exporting the released weights reproduces the published ``config.json`` and
|
| 231 |
+
the published ``model.safetensors`` tensor payload and tensor index byte for
|
| 232 |
+
byte. The header's metadata block is the one exception: the safetensors writer
|
| 233 |
+
emits those keys in an order of its own that varies between runs. The mapping
|
| 234 |
+
they encode is stable and is what the loader reads, so a byte diff of the header
|
| 235 |
+
is not a defect.
|
| 236 |
+
|
| 237 |
+
Returns a report with the two paths, the tensor and parameter counts, and the
|
| 238 |
+
number of aliases carried in the metadata.
|
| 239 |
+
"""
|
| 240 |
+
from safetensors.torch import save_model
|
| 241 |
+
|
| 242 |
+
model, config = _as_model(model_or_state_dict, config)
|
| 243 |
+
model.eval()
|
| 244 |
+
|
| 245 |
+
n_parameters = sum(p.numel() for p in model.parameters())
|
| 246 |
+
if expect_parameters is not None and n_parameters != expect_parameters:
|
| 247 |
+
raise ExportError(
|
| 248 |
+
f"the model instantiated from this config has {n_parameters:,} "
|
| 249 |
+
f"parameters, not the expected {expect_parameters:,}"
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
out = Path(out_dir)
|
| 253 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 254 |
+
weights_path = out / WEIGHTS_NAME
|
| 255 |
+
config_path = out / CONFIG_NAME
|
| 256 |
+
|
| 257 |
+
# save_model can derive the alias map itself, but its choice of which name in a
|
| 258 |
+
# sharing group to keep is its own. Seeding the map (save_model preserves
|
| 259 |
+
# entries already present) makes the canonical name the one named_parameters
|
| 260 |
+
# reports, and the map is verified against the written file below.
|
| 261 |
+
alias_map = {
|
| 262 |
+
alias: head
|
| 263 |
+
for head, aliases in tied_parameter_groups(model).items()
|
| 264 |
+
for alias in aliases
|
| 265 |
+
}
|
| 266 |
+
save_model(model, str(weights_path), metadata=dict(sorted(alias_map.items())))
|
| 267 |
+
# No trailing newline: the released config.json has none, and a byte-for-byte
|
| 268 |
+
# match lets a reader diff this export against the published artifact.
|
| 269 |
+
with open(config_path, "w") as f:
|
| 270 |
+
json.dump(config.to_dict(), f, indent=2)
|
| 271 |
+
|
| 272 |
+
n_tensors, file_sum, written_aliases = _inspect_safetensors(weights_path)
|
| 273 |
+
if file_sum != n_parameters:
|
| 274 |
+
raise ExportError(
|
| 275 |
+
f"{weights_path.name} holds {file_sum:,} values but the model has "
|
| 276 |
+
f"{n_parameters:,} parameters; the tie deduplication is wrong"
|
| 277 |
+
)
|
| 278 |
+
if written_aliases != alias_map:
|
| 279 |
+
raise ExportError(
|
| 280 |
+
f"{weights_path.name} records {len(written_aliases)} tied aliases, "
|
| 281 |
+
f"expected the {len(alias_map)} sharing groups of the model; loading it "
|
| 282 |
+
f"would leave weights uninitialized"
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
return {
|
| 286 |
+
"weights_path": weights_path,
|
| 287 |
+
"config_path": config_path,
|
| 288 |
+
"num_tensors": n_tensors,
|
| 289 |
+
"num_parameters": n_parameters,
|
| 290 |
+
"num_tied_aliases": len(written_aliases),
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def _as_model(
|
| 295 |
+
model_or_state_dict: Union[nn.Module, Mapping[str, torch.Tensor]],
|
| 296 |
+
config: Optional[TinyCastConfig],
|
| 297 |
+
) -> Tuple[TinyCastForPrediction, TinyCastConfig]:
|
| 298 |
+
if isinstance(model_or_state_dict, nn.Module):
|
| 299 |
+
model = model_or_state_dict
|
| 300 |
+
config = config or getattr(model, "config", None)
|
| 301 |
+
if config is None:
|
| 302 |
+
raise ExportError("the model carries no config; pass config=...")
|
| 303 |
+
return model, config
|
| 304 |
+
|
| 305 |
+
if not isinstance(model_or_state_dict, Mapping):
|
| 306 |
+
raise ExportError(
|
| 307 |
+
"expected a TinyCast model or a state dict, got "
|
| 308 |
+
f"{type(model_or_state_dict).__name__}"
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
config = config or TinyCastConfig()
|
| 312 |
+
model = TinyCastForPrediction(config)
|
| 313 |
+
state = dict(model_or_state_dict)
|
| 314 |
+
_assert_tied_values_agree(model, state)
|
| 315 |
+
missing, unexpected = model.load_state_dict(state, strict=False)
|
| 316 |
+
if missing or unexpected:
|
| 317 |
+
raise ExportError(
|
| 318 |
+
f"state dict does not match the architecture: {len(missing)} missing "
|
| 319 |
+
f"{sorted(missing)[:5]}, {len(unexpected)} unexpected "
|
| 320 |
+
f"{sorted(unexpected)[:5]}"
|
| 321 |
+
)
|
| 322 |
+
return model, config
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def _inspect_safetensors(path: Path) -> Tuple[int, int, Dict[str, str]]:
|
| 326 |
+
"""Return (tensor count, summed values, alias map) read back from the header."""
|
| 327 |
+
import struct
|
| 328 |
+
|
| 329 |
+
with open(path, "rb") as f:
|
| 330 |
+
header_len = struct.unpack("<Q", f.read(8))[0]
|
| 331 |
+
header = json.loads(f.read(header_len))
|
| 332 |
+
metadata = header.pop("__metadata__", {}) or {}
|
| 333 |
+
total = 0
|
| 334 |
+
for spec in header.values():
|
| 335 |
+
n = 1
|
| 336 |
+
for dim in spec["shape"]:
|
| 337 |
+
n *= dim
|
| 338 |
+
total += n
|
| 339 |
+
return len(header), total, metadata
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
# ---------------------------------------------------------------------------
|
| 343 |
+
# Round-trip verification
|
| 344 |
+
# ---------------------------------------------------------------------------
|
| 345 |
+
def fixed_batch(
|
| 346 |
+
batch_size: int = 2, seq_len: int = 2048, seed: int = 0
|
| 347 |
+
) -> torch.Tensor:
|
| 348 |
+
"""A deterministic context batch: two seasonal components plus fixed noise.
|
| 349 |
+
|
| 350 |
+
Seasonality is present on purpose, so the round trip exercises the period
|
| 351 |
+
detector and the phase-folding path rather than the convolutions alone.
|
| 352 |
+
"""
|
| 353 |
+
generator = torch.Generator().manual_seed(seed)
|
| 354 |
+
t = torch.arange(seq_len, dtype=torch.float32).unsqueeze(0)
|
| 355 |
+
offsets = torch.arange(batch_size, dtype=torch.float32).unsqueeze(1)
|
| 356 |
+
daily = torch.sin(2 * math.pi * (t + 7 * offsets) / 24.0)
|
| 357 |
+
weekly = 0.3 * torch.sin(2 * math.pi * t / 168.0)
|
| 358 |
+
noise = 0.05 * torch.randn(batch_size, seq_len, generator=generator)
|
| 359 |
+
return 10.0 + 3.0 * daily + weekly + noise
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
@torch.no_grad()
|
| 363 |
+
def check_export_roundtrip(
|
| 364 |
+
model: nn.Module,
|
| 365 |
+
out_dir: Optional[Union[str, Path]] = None,
|
| 366 |
+
config: Optional[TinyCastConfig] = None,
|
| 367 |
+
expect_parameters: Optional[int] = None,
|
| 368 |
+
prediction_length: int = 48,
|
| 369 |
+
) -> Dict[str, object]:
|
| 370 |
+
"""Export ``model``, load it back, and verify nothing was lost.
|
| 371 |
+
|
| 372 |
+
Three things are checked. The reloaded model must produce bitwise-identical
|
| 373 |
+
quantiles on a fixed batch. The FFN tie must be restored as object identity and
|
| 374 |
+
not merely as equal values, since equal-but-separate tensors would triple the
|
| 375 |
+
deployed footprint. And the file's tensors must sum to the instantiated
|
| 376 |
+
parameter count rather than to the inflated ``state_dict`` sum.
|
| 377 |
+
|
| 378 |
+
``out_dir`` defaults to a temporary directory. Pass ``expect_parameters``
|
| 379 |
+
(146,505 for the released model) to pin the count. Returns a report; every
|
| 380 |
+
failure raises :class:`ExportError`.
|
| 381 |
+
"""
|
| 382 |
+
from .checkpoint import load_checkpoint
|
| 383 |
+
|
| 384 |
+
scratch = tempfile.TemporaryDirectory() if out_dir is None else nullcontext(out_dir)
|
| 385 |
+
with scratch as tmp:
|
| 386 |
+
target = Path(tmp)
|
| 387 |
+
report = export_safetensors(
|
| 388 |
+
model, target, config=config, expect_parameters=expect_parameters
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
model.eval()
|
| 392 |
+
x = fixed_batch(seq_len=int(model.config.seq_len))
|
| 393 |
+
reference = model(
|
| 394 |
+
past_values=x, scale_factor=1.0, prediction_length=prediction_length,
|
| 395 |
+
batch_first=True,
|
| 396 |
+
).quantile_outputs
|
| 397 |
+
|
| 398 |
+
reloaded, _ = load_checkpoint(str(report["weights_path"]))
|
| 399 |
+
reloaded.eval()
|
| 400 |
+
replayed = reloaded(
|
| 401 |
+
past_values=x, scale_factor=1.0, prediction_length=prediction_length,
|
| 402 |
+
batch_first=True,
|
| 403 |
+
).quantile_outputs
|
| 404 |
+
|
| 405 |
+
if replayed.shape != reference.shape:
|
| 406 |
+
raise ExportError(
|
| 407 |
+
f"reloaded model returns {tuple(replayed.shape)}, expected "
|
| 408 |
+
f"{tuple(reference.shape)}"
|
| 409 |
+
)
|
| 410 |
+
if not torch.equal(reference, replayed):
|
| 411 |
+
gap = (reference - replayed).abs().max().item()
|
| 412 |
+
raise ExportError(
|
| 413 |
+
f"reloaded model is not bitwise identical: max absolute difference {gap}"
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
expected_groups = tied_parameter_groups(model)
|
| 417 |
+
restored_groups = tied_parameter_groups(reloaded)
|
| 418 |
+
if restored_groups != expected_groups:
|
| 419 |
+
raise ExportError(
|
| 420 |
+
"the FFN weight tie was not restored on load: "
|
| 421 |
+
f"{sum(len(a) for a in restored_groups.values())} aliases share storage, "
|
| 422 |
+
f"expected {sum(len(a) for a in expected_groups.values())}"
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
naive_sum = sum(t.numel() for t in reloaded.state_dict().values())
|
| 426 |
+
if report["num_parameters"] >= naive_sum:
|
| 427 |
+
raise ExportError(
|
| 428 |
+
f"the export did not deduplicate: {report['num_parameters']:,} values "
|
| 429 |
+
f"written against a state dict sum of {naive_sum:,}"
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
report = dict(report)
|
| 433 |
+
report["state_dict_sum"] = naive_sum
|
| 434 |
+
report["bitwise_identical"] = True
|
| 435 |
+
return report
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
# ---------------------------------------------------------------------------
|
| 439 |
+
# CLI
|
| 440 |
+
# ---------------------------------------------------------------------------
|
| 441 |
+
def main(argv: Optional[Iterable[str]] = None) -> int:
|
| 442 |
+
import argparse
|
| 443 |
+
|
| 444 |
+
parser = argparse.ArgumentParser(
|
| 445 |
+
prog="python -m tinycast.export",
|
| 446 |
+
description="Convert training checkpoints into the released "
|
| 447 |
+
"model.safetensors + config.json pair.",
|
| 448 |
+
)
|
| 449 |
+
parser.add_argument(
|
| 450 |
+
"--average", nargs="+", metavar="CKPT", required=True,
|
| 451 |
+
help=f"checkpoints to average, in training order (the release used "
|
| 452 |
+
f"the last {RELEASE_AVERAGE_N})",
|
| 453 |
+
)
|
| 454 |
+
parser.add_argument("--out-dir", required=True, help="directory to write into")
|
| 455 |
+
parser.add_argument(
|
| 456 |
+
"--config", default=None,
|
| 457 |
+
help="config.json to build the architecture from; defaults to the "
|
| 458 |
+
"released configuration",
|
| 459 |
+
)
|
| 460 |
+
parser.add_argument(
|
| 461 |
+
"--expect-parameters", type=int, default=None,
|
| 462 |
+
help="fail unless the instantiated model has this many parameters "
|
| 463 |
+
"(146505 for the released model)",
|
| 464 |
+
)
|
| 465 |
+
parser.add_argument(
|
| 466 |
+
"--no-check", action="store_true",
|
| 467 |
+
help="skip the reload / bitwise-identity round trip",
|
| 468 |
+
)
|
| 469 |
+
args = parser.parse_args(list(argv) if argv is not None else None)
|
| 470 |
+
|
| 471 |
+
config = TinyCastConfig()
|
| 472 |
+
if args.config:
|
| 473 |
+
with open(args.config) as f:
|
| 474 |
+
raw = json.load(f)
|
| 475 |
+
config = TinyCastConfig(**{
|
| 476 |
+
k: v for k, v in raw.items() if k in TinyCastConfig.__dataclass_fields__
|
| 477 |
+
})
|
| 478 |
+
|
| 479 |
+
if len(args.average) != RELEASE_AVERAGE_N:
|
| 480 |
+
print(
|
| 481 |
+
f"[export] averaging {len(args.average)} checkpoints; the released "
|
| 482 |
+
f"weights used {RELEASE_AVERAGE_N}",
|
| 483 |
+
flush=True,
|
| 484 |
+
)
|
| 485 |
+
state = average_checkpoints(args.average)
|
| 486 |
+
model, config = _as_model(state, config)
|
| 487 |
+
|
| 488 |
+
if args.no_check:
|
| 489 |
+
report = export_safetensors(
|
| 490 |
+
model, args.out_dir, config=config,
|
| 491 |
+
expect_parameters=args.expect_parameters,
|
| 492 |
+
)
|
| 493 |
+
else:
|
| 494 |
+
report = check_export_roundtrip(
|
| 495 |
+
model, args.out_dir, config=config,
|
| 496 |
+
expect_parameters=args.expect_parameters,
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
print(
|
| 500 |
+
f"[export] wrote {report['weights_path']} "
|
| 501 |
+
f"({report['num_tensors']} tensors, {report['num_parameters']:,} "
|
| 502 |
+
f"parameters, {report['num_tied_aliases']} tied aliases)",
|
| 503 |
+
flush=True,
|
| 504 |
+
)
|
| 505 |
+
print(f"[export] wrote {report['config_path']}", flush=True)
|
| 506 |
+
if not args.no_check:
|
| 507 |
+
print(
|
| 508 |
+
f"[export] round trip: bitwise identical, tie restored, "
|
| 509 |
+
f"{report['num_parameters']:,} values written against a state dict "
|
| 510 |
+
f"sum of {report['state_dict_sum']:,}",
|
| 511 |
+
flush=True,
|
| 512 |
+
)
|
| 513 |
+
return 0
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
if __name__ == "__main__": # pragma: no cover
|
| 517 |
+
raise SystemExit(main())
|
tinycast/losses.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training objectives for TinyCast.
|
| 2 |
+
|
| 3 |
+
Two objectives supervise the model. The nine-quantile pinball loss trains the
|
| 4 |
+
whole predictive distribution; the gated committing term acts on the median
|
| 5 |
+
alone and is the paper's own addition. ``seasonal_copy_baseline`` builds the
|
| 6 |
+
reference forecast the committing term measures the median against.
|
| 7 |
+
|
| 8 |
+
All three work in the model's normalized output space. ``TinyCastBackbone.encode``
|
| 9 |
+
returns ``(y_norm, x_min, x_range)``; normalize the target with those same
|
| 10 |
+
statistics before calling anything here, so predictions, targets and the
|
| 11 |
+
seasonal copy sit on one scale.
|
| 12 |
+
|
| 13 |
+
Each function is self-contained and takes plain tensors, so it can be exercised
|
| 14 |
+
without a model or a training loop.
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
from typing import Optional, Sequence, Union
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
# Cycles per canonical day, the constant the scale factor is expressed against:
|
| 23 |
+
# a sample's seasonal lag is BASE_SEASONALITY divided by its scale factor. It is
|
| 24 |
+
# defined once, in tinycast.scale, and re-exported here so the lag this module
|
| 25 |
+
# folds at and the factor the model is conditioned on cannot drift apart.
|
| 26 |
+
from .scale import BASE_SEASONALITY
|
| 27 |
+
|
| 28 |
+
# Weight of the committing term in the shipped recipe.
|
| 29 |
+
COMMIT_WEIGHT = 0.3
|
| 30 |
+
|
| 31 |
+
_Reduction = str
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _reduce(per_sample: torch.Tensor, reduction: _Reduction) -> torch.Tensor:
|
| 35 |
+
if reduction == "none":
|
| 36 |
+
return per_sample
|
| 37 |
+
if reduction == "mean":
|
| 38 |
+
return per_sample.mean()
|
| 39 |
+
if reduction == "sum":
|
| 40 |
+
return per_sample.sum()
|
| 41 |
+
raise ValueError(
|
| 42 |
+
f"Unknown reduction: {reduction!r}; expected 'mean', 'sum' or 'none'."
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _as_bh(target: torch.Tensor, name: str) -> torch.Tensor:
|
| 47 |
+
"""Accept ``(B, H)`` or ``(B, H, 1)`` and return ``(B, H)``."""
|
| 48 |
+
if target.dim() == 3 and target.shape[-1] == 1:
|
| 49 |
+
return target.squeeze(-1)
|
| 50 |
+
if target.dim() != 2:
|
| 51 |
+
raise ValueError(
|
| 52 |
+
f"{name} must be (B, H) or (B, H, 1), got {tuple(target.shape)}."
|
| 53 |
+
)
|
| 54 |
+
return target
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _mask_like(mask: Optional[torch.Tensor], ref: torch.Tensor) -> torch.Tensor:
|
| 58 |
+
if mask is None:
|
| 59 |
+
return torch.ones_like(ref)
|
| 60 |
+
mask = _as_bh(mask, "mask").to(ref.dtype)
|
| 61 |
+
if mask.shape != ref.shape:
|
| 62 |
+
raise ValueError(
|
| 63 |
+
f"mask shape {tuple(mask.shape)} does not match "
|
| 64 |
+
f"{tuple(ref.shape)}."
|
| 65 |
+
)
|
| 66 |
+
return mask
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def pinball_loss(
|
| 70 |
+
pred: torch.Tensor,
|
| 71 |
+
target: torch.Tensor,
|
| 72 |
+
quantiles: Union[Sequence[float], torch.Tensor],
|
| 73 |
+
mask: Optional[torch.Tensor] = None,
|
| 74 |
+
*,
|
| 75 |
+
reduction: _Reduction = "mean",
|
| 76 |
+
) -> torch.Tensor:
|
| 77 |
+
"""Quantile (pinball) loss over all quantile levels.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
pred: ``(B, H, Q)`` normalized quantile forecasts, ordered to match
|
| 81 |
+
``quantiles``.
|
| 82 |
+
target: ``(B, H)`` or ``(B, H, 1)`` normalized targets.
|
| 83 |
+
quantiles: the ``Q`` levels, e.g. ``config.quantiles``.
|
| 84 |
+
mask: ``(B, H)`` observedness, 1 for an observed target position and 0
|
| 85 |
+
otherwise. ``None`` treats every position as observed.
|
| 86 |
+
reduction: ``"mean"`` (default) averages over the batch, ``"sum"`` adds,
|
| 87 |
+
``"none"`` returns the ``(B,)`` per-sample losses.
|
| 88 |
+
|
| 89 |
+
The per-sample loss divides by the observed count times ``Q``, so a sample
|
| 90 |
+
with few observed positions is not down-weighted against a full one, and a
|
| 91 |
+
sample with none contributes zero rather than a division by zero.
|
| 92 |
+
|
| 93 |
+
A non-finite target position is replaced by the detached median forecast, so
|
| 94 |
+
it contributes no loss and no gradient. Mask such positions out as well: the
|
| 95 |
+
substitution is a guard, not the mechanism.
|
| 96 |
+
"""
|
| 97 |
+
if pred.dim() != 3:
|
| 98 |
+
raise ValueError(f"pred must be (B, H, Q), got {tuple(pred.shape)}.")
|
| 99 |
+
target = _as_bh(target, "target")
|
| 100 |
+
if pred.shape[:2] != target.shape:
|
| 101 |
+
raise ValueError(
|
| 102 |
+
f"pred {tuple(pred.shape)} and target {tuple(target.shape)} "
|
| 103 |
+
"disagree on batch or horizon."
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
q = torch.as_tensor(quantiles, dtype=pred.dtype, device=pred.device)
|
| 107 |
+
q = q.reshape(-1)
|
| 108 |
+
n_q = pred.shape[-1]
|
| 109 |
+
if q.numel() != n_q:
|
| 110 |
+
raise ValueError(
|
| 111 |
+
f"pred carries {n_q} quantile channels but {q.numel()} levels "
|
| 112 |
+
"were given."
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
q_mid = n_q // 2
|
| 116 |
+
target_b = target.unsqueeze(-1)
|
| 117 |
+
fill = pred[..., q_mid: q_mid + 1].detach()
|
| 118 |
+
target_safe = torch.where(torch.isfinite(target_b), target_b, fill)
|
| 119 |
+
|
| 120 |
+
err = target_safe - pred
|
| 121 |
+
q = q.view(1, 1, -1)
|
| 122 |
+
loss = torch.maximum(q * err, (q - 1.0) * err) # (B, H, Q)
|
| 123 |
+
|
| 124 |
+
obs = _mask_like(mask, target)
|
| 125 |
+
denom = obs.sum(dim=1).clamp(min=1.0) * float(n_q)
|
| 126 |
+
per_sample = (loss * obs.unsqueeze(-1)).sum(dim=(1, 2)) / denom
|
| 127 |
+
return _reduce(per_sample, reduction)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def seasonal_copy_baseline(
|
| 131 |
+
context: torch.Tensor,
|
| 132 |
+
horizon: int,
|
| 133 |
+
scale_factor: Union[torch.Tensor, float],
|
| 134 |
+
*,
|
| 135 |
+
base_seasonality: float = BASE_SEASONALITY,
|
| 136 |
+
) -> torch.Tensor:
|
| 137 |
+
"""Repeat the last seasonal cycle of the context over the horizon.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
context: ``(B, L)`` or ``(B, L, 1)`` context values, in whatever space
|
| 141 |
+
the caller wants the copy in (raw or normalized).
|
| 142 |
+
horizon: ``H``, how many steps to emit.
|
| 143 |
+
scale_factor: per-sample or scalar seasonal scale factor ``s``, the
|
| 144 |
+
value ``tinycast.scale.seasonal_scale_factor`` returns for the
|
| 145 |
+
sample's frequency.
|
| 146 |
+
base_seasonality: numerator of the lag, 24 by convention.
|
| 147 |
+
|
| 148 |
+
The lag is ``round(base_seasonality / s)`` clipped to ``[2, L // 2]``, and
|
| 149 |
+
position ``h`` of the copy is context position ``L - lag + (h mod lag)``.
|
| 150 |
+
Returns ``(B, H)``.
|
| 151 |
+
"""
|
| 152 |
+
if context.dim() == 3 and context.shape[-1] == 1:
|
| 153 |
+
context = context.squeeze(-1)
|
| 154 |
+
if context.dim() != 2:
|
| 155 |
+
raise ValueError(
|
| 156 |
+
f"context must be (B, L) or (B, L, 1), got {tuple(context.shape)}."
|
| 157 |
+
)
|
| 158 |
+
b, length = context.shape
|
| 159 |
+
horizon = int(horizon)
|
| 160 |
+
if horizon < 1:
|
| 161 |
+
raise ValueError(f"horizon must be positive, got {horizon}.")
|
| 162 |
+
if length < 4:
|
| 163 |
+
raise ValueError(f"context is too short to fold a cycle: L={length}.")
|
| 164 |
+
|
| 165 |
+
sf = torch.as_tensor(scale_factor, dtype=torch.float32, device=context.device)
|
| 166 |
+
sf = sf.reshape(-1)
|
| 167 |
+
if sf.numel() == 1:
|
| 168 |
+
sf = sf.expand(b)
|
| 169 |
+
elif sf.numel() != b:
|
| 170 |
+
raise ValueError(
|
| 171 |
+
f"scale_factor carries {sf.numel()} entries for a batch of {b}."
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
lag = (base_seasonality / sf.clamp(min=1e-3)).round().long()
|
| 175 |
+
lag = lag.clamp(2, max(2, length // 2)).view(-1, 1) # (B, 1)
|
| 176 |
+
|
| 177 |
+
h = torch.arange(horizon, device=context.device).view(1, horizon)
|
| 178 |
+
src = (length - lag + (h % lag)).clamp(0, length - 1) # (B, H)
|
| 179 |
+
return torch.gather(context, 1, src)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def committing_loss(
|
| 183 |
+
median: torch.Tensor,
|
| 184 |
+
target: torch.Tensor,
|
| 185 |
+
seasonal_copy: torch.Tensor,
|
| 186 |
+
mask: Optional[torch.Tensor] = None,
|
| 187 |
+
*,
|
| 188 |
+
weight: float = COMMIT_WEIGHT,
|
| 189 |
+
gated: bool = True,
|
| 190 |
+
reduction: _Reduction = "mean",
|
| 191 |
+
) -> torch.Tensor:
|
| 192 |
+
"""Gated committing term: penalize a median that hedges below the copy.
|
| 193 |
+
|
| 194 |
+
Args:
|
| 195 |
+
median: ``(B, H)`` normalized median forecast.
|
| 196 |
+
target: ``(B, H)`` normalized targets.
|
| 197 |
+
seasonal_copy: ``(B, H)`` the reference from
|
| 198 |
+
``seasonal_copy_baseline``, in the same space as ``median``.
|
| 199 |
+
mask: ``(B, H)`` observedness; ``None`` treats every position as
|
| 200 |
+
observed.
|
| 201 |
+
weight: the multiplier the trainer applies, 0.3 in the shipped recipe.
|
| 202 |
+
gated: apply the window-level gate. With ``False`` the hinge is scored
|
| 203 |
+
on every window.
|
| 204 |
+
reduction: as in :func:`pinball_loss`.
|
| 205 |
+
|
| 206 |
+
The hinge ``relu(|median - target| - |copy - target|)`` is zero wherever the
|
| 207 |
+
median is already at least as close as the copy, so the term stops as soon
|
| 208 |
+
as the median reaches it. The gate multiplies the whole window by zero
|
| 209 |
+
unless the copy beats the median summed over the observed positions, which
|
| 210 |
+
keeps the term silent on windows the model already wins.
|
| 211 |
+
|
| 212 |
+
Positions where any of the three inputs is non-finite contribute nothing,
|
| 213 |
+
to the hinge and to the gate alike. The average is still taken over the
|
| 214 |
+
observed count, so masking is what sets the denominator.
|
| 215 |
+
"""
|
| 216 |
+
median = _as_bh(median, "median")
|
| 217 |
+
target = _as_bh(target, "target")
|
| 218 |
+
seasonal_copy = _as_bh(seasonal_copy, "seasonal_copy")
|
| 219 |
+
if median.shape != target.shape or median.shape != seasonal_copy.shape:
|
| 220 |
+
raise ValueError(
|
| 221 |
+
f"median {tuple(median.shape)}, target {tuple(target.shape)} and "
|
| 222 |
+
f"seasonal_copy {tuple(seasonal_copy.shape)} must agree."
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
obs = _mask_like(mask, target)
|
| 226 |
+
med_err = (median - target).abs()
|
| 227 |
+
copy_err = (seasonal_copy - target).abs()
|
| 228 |
+
|
| 229 |
+
finite = (
|
| 230 |
+
torch.isfinite(med_err) & torch.isfinite(copy_err) & torch.isfinite(target)
|
| 231 |
+
).to(obs.dtype)
|
| 232 |
+
scored = obs * finite
|
| 233 |
+
|
| 234 |
+
hinge = torch.relu(med_err - copy_err)
|
| 235 |
+
hinge = torch.where(torch.isfinite(hinge), hinge, torch.zeros_like(hinge))
|
| 236 |
+
denom = obs.sum(dim=1).clamp(min=1.0)
|
| 237 |
+
per_sample = (hinge * scored).sum(dim=1) / denom
|
| 238 |
+
|
| 239 |
+
if gated:
|
| 240 |
+
med_total = (torch.nan_to_num(med_err, 0.0, 0.0, 0.0) * scored).sum(dim=1)
|
| 241 |
+
copy_total = (torch.nan_to_num(copy_err, 0.0, 0.0, 0.0) * scored).sum(dim=1)
|
| 242 |
+
per_sample = per_sample * (copy_total < med_total).to(per_sample.dtype)
|
| 243 |
+
|
| 244 |
+
return _reduce(per_sample * float(weight), reduction)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
__all__ = [
|
| 248 |
+
"BASE_SEASONALITY",
|
| 249 |
+
"COMMIT_WEIGHT",
|
| 250 |
+
"pinball_loss",
|
| 251 |
+
"seasonal_copy_baseline",
|
| 252 |
+
"committing_loss",
|
| 253 |
+
]
|
tinycast/model.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TinyCast model assembly.
|
| 2 |
+
|
| 3 |
+
model(past_values, scale_factor, prediction_length, batch_first).quantile_outputs
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
|
| 11 |
+
from .config import TinyCastConfig
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class PredictionOutput:
|
| 16 |
+
quantile_outputs: torch.Tensor
|
| 17 |
+
prediction_outputs: torch.Tensor = None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class TinyCastBackbone(nn.Module):
|
| 21 |
+
"""Inner model: per-window min-max norm -> dilated-conv core -> denorm."""
|
| 22 |
+
|
| 23 |
+
def __init__(self, config: TinyCastConfig):
|
| 24 |
+
super().__init__()
|
| 25 |
+
self.config = config
|
| 26 |
+
from .backbone import DilatedConvBackbone
|
| 27 |
+
from .normalization import WindowMinMax
|
| 28 |
+
|
| 29 |
+
self.core = DilatedConvBackbone(
|
| 30 |
+
seq_len=int(config.seq_len),
|
| 31 |
+
p_out=int(config.output_token_len),
|
| 32 |
+
n_quantiles=int(config.num_quantiles),
|
| 33 |
+
d=int(config.conv_dim),
|
| 34 |
+
n_layers=int(config.n_layers),
|
| 35 |
+
kernel=int(config.kernel_size),
|
| 36 |
+
ffn_mult=float(config.ffn_mult),
|
| 37 |
+
top_k_periods=int(config.top_k_periods),
|
| 38 |
+
significance_alpha=float(config.significance_alpha),
|
| 39 |
+
n_harmonics=int(config.n_harmonics),
|
| 40 |
+
pool_kind=str(config.pool_kind),
|
| 41 |
+
causal=bool(config.causal),
|
| 42 |
+
phase_bins=int(config.phase_bins),
|
| 43 |
+
decoder_depth=int(config.decoder_depth),
|
| 44 |
+
separable_conv=bool(config.separable_conv),
|
| 45 |
+
share_ffn=bool(config.share_ffn),
|
| 46 |
+
future_conv=bool(config.future_conv),
|
| 47 |
+
future_conv_layers=int(config.future_conv_layers),
|
| 48 |
+
future_conv_seed=int(config.future_conv_seed),
|
| 49 |
+
)
|
| 50 |
+
self.norm = WindowMinMax(eps_clamp=1e-5)
|
| 51 |
+
|
| 52 |
+
def encode(
|
| 53 |
+
self,
|
| 54 |
+
past_values: torch.Tensor,
|
| 55 |
+
batch_first: bool = True,
|
| 56 |
+
scale_factor: "torch.Tensor | float | None" = None,
|
| 57 |
+
horizon: "int | None" = None,
|
| 58 |
+
) -> "tuple[torch.Tensor, torch.Tensor, torch.Tensor]":
|
| 59 |
+
"""per-window min-max norm -> dilated-conv core -> normalized y."""
|
| 60 |
+
x = past_values
|
| 61 |
+
if not batch_first:
|
| 62 |
+
x = x.transpose(0, 1)
|
| 63 |
+
if x.dim() == 2:
|
| 64 |
+
x = x.unsqueeze(-1)
|
| 65 |
+
|
| 66 |
+
x_normed, x_min, x_range = self.norm.transform(x)
|
| 67 |
+
nan_mask = (~torch.isnan(x)).to(x.dtype)
|
| 68 |
+
y_norm = self.core(
|
| 69 |
+
x_normed, nan_mask=nan_mask, scale_factor=scale_factor,
|
| 70 |
+
horizon=horizon,
|
| 71 |
+
)
|
| 72 |
+
return y_norm, x_min, x_range
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class TinyCastForPrediction(nn.Module):
|
| 76 |
+
"""Top-level model container.
|
| 77 |
+
|
| 78 |
+
Usage:
|
| 79 |
+
model = TinyCastForPrediction(TinyCastConfig())
|
| 80 |
+
out = model(past_values=x, scale_factor=sf, prediction_length=pl,
|
| 81 |
+
batch_first=False)
|
| 82 |
+
quantiles = out.quantile_outputs # (B, Q, pred_len, 1)
|
| 83 |
+
|
| 84 |
+
NOTE: this ``forward`` is the SINGLE-SHOT arbitrary-horizon path. The
|
| 85 |
+
deployed GIFT-Eval inference uses the AR-rollout predictor
|
| 86 |
+
(``tinycast.predictor.TinyCastPredictor``), which calls ``self.model.encode``
|
| 87 |
+
in 48-step chunks. For horizons <= 48 the two paths coincide.
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
def __init__(self, config: TinyCastConfig):
|
| 91 |
+
super().__init__()
|
| 92 |
+
self.config = config
|
| 93 |
+
self.model = TinyCastBackbone(config)
|
| 94 |
+
|
| 95 |
+
def forward(self, past_values, scale_factor=None, prediction_length=None,
|
| 96 |
+
batch_first=None):
|
| 97 |
+
if scale_factor is None:
|
| 98 |
+
scale_factor = 1.0
|
| 99 |
+
if batch_first is None:
|
| 100 |
+
batch_first = True
|
| 101 |
+
ctx = past_values
|
| 102 |
+
if not batch_first:
|
| 103 |
+
ctx = ctx.transpose(0, 1)
|
| 104 |
+
if ctx.dim() == 2:
|
| 105 |
+
ctx = ctx.unsqueeze(-1)
|
| 106 |
+
ctx_len_max = int(self.config.seq_len)
|
| 107 |
+
ctx_in = ctx[:, -ctx_len_max:, :]
|
| 108 |
+
p_out_native = int(self.model.core.p_out)
|
| 109 |
+
pred_len = int(prediction_length) if prediction_length else p_out_native
|
| 110 |
+
y_norm, x_min, x_range = self.model.encode(
|
| 111 |
+
ctx_in, batch_first=True, scale_factor=scale_factor,
|
| 112 |
+
horizon=pred_len,
|
| 113 |
+
)
|
| 114 |
+
if y_norm.dim() == 2:
|
| 115 |
+
y_norm = y_norm.unsqueeze(-1)
|
| 116 |
+
y_pred = y_norm * x_range + x_min
|
| 117 |
+
quantile_outputs = y_pred.permute(0, 2, 1).unsqueeze(-1)
|
| 118 |
+
return PredictionOutput(quantile_outputs=quantile_outputs)
|
tinycast/normalization.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Per-context-window min-max normalization used by the encoder path."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class WindowMinMax(nn.Module):
|
| 9 |
+
"""Per-context-window min-max normalization for the encoder path.
|
| 10 |
+
|
| 11 |
+
Per-window statistics:
|
| 12 |
+
|
| 13 |
+
x_min = x.min(1, keepdim=True)[0].detach()
|
| 14 |
+
x_max = x.max(1, keepdim=True)[0].detach()
|
| 15 |
+
x_range = (x_max - x_min).clamp(min=1e-5).detach()
|
| 16 |
+
x_norm = (x - x_min) / x_range # → [0, 1]
|
| 17 |
+
|
| 18 |
+
Stats are detached: gradients do NOT flow through normalization.
|
| 19 |
+
Uses explicit ``transform`` / ``inverse_transform`` so the backbone can
|
| 20 |
+
hold the (x_min, x_range) tuple across encoder + decoder and apply the
|
| 21 |
+
inverse at loss / forecast time.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(self, eps_clamp: float = 1e-5) -> None:
|
| 25 |
+
super().__init__()
|
| 26 |
+
self.eps_clamp = eps_clamp
|
| 27 |
+
|
| 28 |
+
def transform(
|
| 29 |
+
self, x: torch.Tensor
|
| 30 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 31 |
+
"""Normalize ``x: (B, L, 1)`` to [0, 1] per context window.
|
| 32 |
+
|
| 33 |
+
Returns ``(x_normalized, x_min, x_range)`` with stats detached and
|
| 34 |
+
``x_range >= eps_clamp`` to avoid div-by-zero on constant series.
|
| 35 |
+
|
| 36 |
+
Robust to NaN / +inf / -inf in ``x``: those positions are filled
|
| 37 |
+
with 0 BEFORE computing min/max.
|
| 38 |
+
"""
|
| 39 |
+
x_filled = torch.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)
|
| 40 |
+
x_min = x_filled.min(dim=1, keepdim=True).values.detach()
|
| 41 |
+
x_max = x_filled.max(dim=1, keepdim=True).values.detach()
|
| 42 |
+
x_range = (x_max - x_min).clamp(min=self.eps_clamp).detach()
|
| 43 |
+
x_normalized = (x_filled - x_min) / x_range
|
| 44 |
+
return x_normalized, x_min, x_range
|
| 45 |
+
|
| 46 |
+
@staticmethod
|
| 47 |
+
def inverse_transform(
|
| 48 |
+
y_pred_normalized: torch.Tensor,
|
| 49 |
+
x_min: torch.Tensor,
|
| 50 |
+
x_range: torch.Tensor,
|
| 51 |
+
) -> torch.Tensor:
|
| 52 |
+
"""Un-normalize ``(B, p)`` predictions back to raw magnitude.
|
| 53 |
+
|
| 54 |
+
``x_min`` / ``x_range`` come from a prior ``transform`` call and are
|
| 55 |
+
``(B, 1, 1)``; the trailing dim is squeezed for broadcasting against
|
| 56 |
+
``(B, p)``.
|
| 57 |
+
"""
|
| 58 |
+
return y_pred_normalized * x_range.squeeze(-1) + x_min.squeeze(-1)
|
tinycast/periodogram.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Normalized-periodogram period detector.
|
| 2 |
+
|
| 3 |
+
A zero-parameter structural detector: it identifies the dominant seasonal
|
| 4 |
+
periods of each series via a significance-filtered normalized periodogram.
|
| 5 |
+
The dilated-conv encoder uses it to build its phase positional encoding.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import math
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
import torch.nn.functional as F
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def significant_periods(
|
| 17 |
+
x: torch.Tensor,
|
| 18 |
+
*,
|
| 19 |
+
min_period: int = 2,
|
| 20 |
+
max_period: int | None = None,
|
| 21 |
+
top_k: int = 16,
|
| 22 |
+
significance_alpha: float = 0.05,
|
| 23 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 24 |
+
"""Identify candidate periods via the normalized periodogram.
|
| 25 |
+
|
| 26 |
+
Score per frequency bin k:
|
| 27 |
+
I_norm[k] = |X[k]|² / sum_k' |X[k']|²
|
| 28 |
+
|
| 29 |
+
Under H_0 (white Gaussian noise), max(I_norm) follows an extreme-value
|
| 30 |
+
distribution. Peaks are filtered by a Bonferroni-corrected significance
|
| 31 |
+
threshold:
|
| 32 |
+
|
| 33 |
+
t_α = ln(N_bins / α) / N_bins
|
| 34 |
+
|
| 35 |
+
where N_bins is the number of valid frequency bins (data-determined,
|
| 36 |
+
not a knob) and α is the significance level (default 0.05). Peaks below
|
| 37 |
+
t_α are excluded by setting their score to -inf, so ``n_valid`` reflects
|
| 38 |
+
only periods that pass.
|
| 39 |
+
|
| 40 |
+
α is exposed for completeness but should normally stay at 0.05; smaller
|
| 41 |
+
(e.g. 0.01) is stricter, larger (0.10) laxer. Set α=1.0 to disable
|
| 42 |
+
filtering entirely.
|
| 43 |
+
|
| 44 |
+
Returns ``(periods, scores, n_valid)``: integer periods (0 = rejected),
|
| 45 |
+
per-slot scores, and the count of significant periods per sample.
|
| 46 |
+
"""
|
| 47 |
+
B, L = x.shape
|
| 48 |
+
device = x.device
|
| 49 |
+
if max_period is None:
|
| 50 |
+
max_period = L // 2
|
| 51 |
+
|
| 52 |
+
with torch.amp.autocast(device_type=x.device.type if x.is_cuda else "cpu",
|
| 53 |
+
enabled=False):
|
| 54 |
+
x_f = x.float()
|
| 55 |
+
x_c = x_f - x_f.mean(dim=1, keepdim=True)
|
| 56 |
+
n_fft = 1 << int(math.ceil(math.log2(max(2, L))))
|
| 57 |
+
X = torch.fft.rfft(x_c, n=n_fft)
|
| 58 |
+
power = (X * X.conj()).real
|
| 59 |
+
power = power[:, 1:] # skip DC
|
| 60 |
+
n_bins = power.shape[1]
|
| 61 |
+
total = power.sum(dim=1, keepdim=True).clamp(min=1e-12)
|
| 62 |
+
I_norm = power / total
|
| 63 |
+
|
| 64 |
+
k_lo = max(0, (n_fft // max_period) - 1)
|
| 65 |
+
k_hi = min(n_bins - 1, max(0, (n_fft // max(2, min_period)) - 1))
|
| 66 |
+
|
| 67 |
+
I_left = I_norm[:, :-2]
|
| 68 |
+
I_mid = I_norm[:, 1:-1]
|
| 69 |
+
I_right = I_norm[:, 2:]
|
| 70 |
+
is_local_max = (I_mid > I_left) & (I_mid > I_right)
|
| 71 |
+
is_local_max_padded = F.pad(is_local_max, (1, 1), value=False)
|
| 72 |
+
|
| 73 |
+
valid = torch.zeros(n_bins, device=device, dtype=torch.bool)
|
| 74 |
+
if k_hi > k_lo:
|
| 75 |
+
valid[k_lo:k_hi + 1] = True
|
| 76 |
+
is_peak = is_local_max_padded & valid.unsqueeze(0)
|
| 77 |
+
|
| 78 |
+
neg_inf = torch.full_like(I_norm, float("-inf"))
|
| 79 |
+
scored = torch.where(is_peak, I_norm, neg_inf)
|
| 80 |
+
|
| 81 |
+
# Bonferroni-corrected significance threshold. N_bins = number of
|
| 82 |
+
# frequency bins eligible to compete; approximated by the full
|
| 83 |
+
# periodogram length (the local-max filter doesn't change the
|
| 84 |
+
# multiple-testing count substantively).
|
| 85 |
+
N_bins_eff = max(2, int(n_bins))
|
| 86 |
+
alpha = max(min(float(significance_alpha), 1.0), 1e-12)
|
| 87 |
+
sig_threshold = math.log(N_bins_eff / alpha) / N_bins_eff
|
| 88 |
+
scored = torch.where(
|
| 89 |
+
scored >= sig_threshold, scored, neg_inf,
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
scores, k_top = scored.topk(k=min(top_k, n_bins), dim=1)
|
| 93 |
+
freq_bins = k_top + 1 # undo the DC skip
|
| 94 |
+
# Round, don't truncate. The peak bin k for true period S has
|
| 95 |
+
# k_exact = n_fft / S. Truncating ``n_fft // k`` flips period 7 <-> 6
|
| 96 |
+
# for S=7 at L=2048 (depending on which side of the half-integer the
|
| 97 |
+
# peak lands), and that 14% period error becomes a full-cycle phase
|
| 98 |
+
# drift over a 48-step horizon.
|
| 99 |
+
periods_raw = torch.round(n_fft / freq_bins.clamp(min=1).float()).long()
|
| 100 |
+
finite = torch.isfinite(scores)
|
| 101 |
+
n_valid = finite.sum(dim=1).long()
|
| 102 |
+
periods = torch.where(finite, periods_raw, torch.zeros_like(periods_raw))
|
| 103 |
+
|
| 104 |
+
return periods.long(), scores, n_valid
|
tinycast/predictor.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""GIFT-Eval gluonts predictor for TinyCast.
|
| 2 |
+
|
| 3 |
+
``TinyCastPredictor`` is the deployed predictor. It wraps the model in the
|
| 4 |
+
gluonts Predictor protocol and drives autoregressive-rollout decoding
|
| 5 |
+
(48-step chunks), flip-invariance symmetrization, NaN-imputation and
|
| 6 |
+
optional period-alignment downsampling.
|
| 7 |
+
|
| 8 |
+
Anything that changes what the predictor emits says so:
|
| 9 |
+
|
| 10 |
+
``device`` a named device must exist. ``device=None`` is the only
|
| 11 |
+
request that selects one for you (CUDA when present, else
|
| 12 |
+
CPU); the resolved device is ``predictor.device``.
|
| 13 |
+
``TINYCAST_INT8`` ``w8`` or ``w8a8`` post-training fake quantization, off by
|
| 14 |
+
default. Any other non-empty value is an error, not an
|
| 15 |
+
inert setting.
|
| 16 |
+
``TINYCAST_TILT_K`` an eval-only probe that moves the emitted median off the
|
| 17 |
+
quantile grid, off (``0``) by default, with
|
| 18 |
+
``TINYCAST_TILT_MODE`` in {``adaptive``, ``fixed``}.
|
| 19 |
+
|
| 20 |
+
The environment variables are read once, when the predictor is constructed, and
|
| 21 |
+
an engaged one prints a line naming itself. Reproducing the published numbers
|
| 22 |
+
means leaving them unset.
|
| 23 |
+
"""
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import math
|
| 27 |
+
import os
|
| 28 |
+
from typing import List, Optional, Tuple
|
| 29 |
+
|
| 30 |
+
import numpy as np
|
| 31 |
+
import torch
|
| 32 |
+
import torch.nn as nn
|
| 33 |
+
|
| 34 |
+
from .checkpoint import load_checkpoint
|
| 35 |
+
from .normalization import WindowMinMax
|
| 36 |
+
from .scale import seasonal_scale_factor
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
from torch.amp import autocast as _autocast_fp
|
| 40 |
+
except Exception: # pragma: no cover
|
| 41 |
+
_autocast_fp = None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _resolve_device(requested: Optional[str]) -> torch.device:
|
| 45 |
+
"""Return the requested device, or say why it is unavailable.
|
| 46 |
+
|
| 47 |
+
``None`` is the request to choose: CUDA when it is available, CPU otherwise.
|
| 48 |
+
Every other value is a requirement. Falling back to CPU behind the caller's
|
| 49 |
+
back drops bf16 autocast for fp32 and moves every forecast by roughly 1e-3,
|
| 50 |
+
which is small enough to read as a regression and large enough to fail an
|
| 51 |
+
assertion against the published aggregates, so an absent accelerator is an
|
| 52 |
+
error rather than a substitution.
|
| 53 |
+
"""
|
| 54 |
+
if requested is None:
|
| 55 |
+
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 56 |
+
dev = torch.device(requested)
|
| 57 |
+
if dev.type == "cuda":
|
| 58 |
+
if not torch.cuda.is_available():
|
| 59 |
+
raise RuntimeError(
|
| 60 |
+
f"device={requested!r} was requested but "
|
| 61 |
+
"torch.cuda.is_available() is False. Pass device='cpu' to run "
|
| 62 |
+
"on CPU (fp32, and about 1e-3 off the published numbers), or "
|
| 63 |
+
"device=None to take whichever device is present."
|
| 64 |
+
)
|
| 65 |
+
if dev.index is not None and dev.index >= torch.cuda.device_count():
|
| 66 |
+
raise RuntimeError(
|
| 67 |
+
f"device={requested!r} was requested but only "
|
| 68 |
+
f"{torch.cuda.device_count()} CUDA device(s) are visible."
|
| 69 |
+
)
|
| 70 |
+
if dev.type == "mps" and not torch.backends.mps.is_available():
|
| 71 |
+
raise RuntimeError(
|
| 72 |
+
f"device={requested!r} was requested but MPS is unavailable. "
|
| 73 |
+
"Pass device='cpu' to run on CPU."
|
| 74 |
+
)
|
| 75 |
+
return dev
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
_INT8_MODES = ("w8", "w8a8")
|
| 79 |
+
_INT8_OFF = ("", "0", "off", "no", "false", "none")
|
| 80 |
+
_TILT_MODES = ("adaptive", "fixed")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _resolve_int8() -> Optional[str]:
|
| 84 |
+
"""Read ``TINYCAST_INT8``: the quantization mode, or ``None`` when off.
|
| 85 |
+
|
| 86 |
+
Only ``w8`` and ``w8a8`` name a scheme. A truthy value such as ``1`` names
|
| 87 |
+
no scheme, and picking one for the caller would quantize a run that asked
|
| 88 |
+
for something else, so an unrecognized value raises. The alternative is what
|
| 89 |
+
this used to do: leave the run in fp32 while the caller believes they are
|
| 90 |
+
measuring INT8.
|
| 91 |
+
"""
|
| 92 |
+
raw = os.environ.get("TINYCAST_INT8", "").strip().lower()
|
| 93 |
+
if raw in _INT8_OFF:
|
| 94 |
+
return None
|
| 95 |
+
if raw not in _INT8_MODES:
|
| 96 |
+
raise ValueError(
|
| 97 |
+
f"TINYCAST_INT8={raw!r} is not a quantization mode. Use 'w8' "
|
| 98 |
+
"(per-channel INT8 weights) or 'w8a8' (+ per-tensor dynamic INT8 "
|
| 99 |
+
"activations), or unset it to run in floating point."
|
| 100 |
+
)
|
| 101 |
+
return raw
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _resolve_tilt() -> Tuple[float, str]:
|
| 105 |
+
"""Read the tilt probe's variables, announcing an engaged one.
|
| 106 |
+
|
| 107 |
+
``TINYCAST_INT8`` prints a line when it engages; this does the same, so
|
| 108 |
+
neither switch can invalidate a golden array without appearing in the log.
|
| 109 |
+
``TINYCAST_TILT_MODE`` is checked only when the tilt is on, which keeps it
|
| 110 |
+
inert at ``K=0`` as documented.
|
| 111 |
+
"""
|
| 112 |
+
raw = os.environ.get("TINYCAST_TILT_K", "").strip()
|
| 113 |
+
try:
|
| 114 |
+
k = float(raw) if raw else 0.0
|
| 115 |
+
except ValueError:
|
| 116 |
+
raise ValueError(
|
| 117 |
+
f"TINYCAST_TILT_K={raw!r} is not a number. Unset it, or set 0, to "
|
| 118 |
+
"emit the model's own median."
|
| 119 |
+
) from None
|
| 120 |
+
mode = os.environ.get("TINYCAST_TILT_MODE", "adaptive").strip().lower()
|
| 121 |
+
if k != 0.0:
|
| 122 |
+
if mode not in _TILT_MODES:
|
| 123 |
+
raise ValueError(
|
| 124 |
+
f"TINYCAST_TILT_MODE={mode!r} is not a tilt rule. Use "
|
| 125 |
+
"'adaptive' (skew-following) or 'fixed'."
|
| 126 |
+
)
|
| 127 |
+
print(
|
| 128 |
+
f"[tilt] TINYCAST_TILT_K={k:g} {mode}: the emitted median is "
|
| 129 |
+
"re-interpolated off the quantile grid, so this is not the "
|
| 130 |
+
"published model's forecast (inert below 3 quantiles)",
|
| 131 |
+
flush=True,
|
| 132 |
+
)
|
| 133 |
+
return k, mode
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _numpy_fill(arr: np.ndarray) -> np.ndarray:
|
| 137 |
+
mask = np.isnan(arr)
|
| 138 |
+
idx = np.where(~mask, np.arange(mask.shape[1]), 0)
|
| 139 |
+
np.maximum.accumulate(idx, axis=1, out=idx)
|
| 140 |
+
return arr[np.arange(idx.shape[0])[:, None], idx]
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class ARRolloutPredictor:
|
| 144 |
+
"""Base predictor: AR rollout + flip + NaN-imputation + period downsample.
|
| 145 |
+
|
| 146 |
+
Subclasses install ``self.model`` (a callable ``(x, x_mark, y_mark)``).
|
| 147 |
+
``device=None`` selects one; a named device must exist. Either way the
|
| 148 |
+
device that ran is ``self.device``.
|
| 149 |
+
|
| 150 |
+
Every setting that changes a forecast is a named parameter, so a keyword
|
| 151 |
+
that is not one raises. Absorbing unknown keywords would let a misspelling
|
| 152 |
+
such as ``force_flip_invarience=True`` construct a predictor with flip
|
| 153 |
+
symmetrization off and return a forecast that looks right and cannot be
|
| 154 |
+
reproduced.
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
def __init__(
|
| 158 |
+
self,
|
| 159 |
+
prediction_length: int,
|
| 160 |
+
device: Optional[str] = None,
|
| 161 |
+
seq_len: int = 2048,
|
| 162 |
+
input_token_len: int = 2048,
|
| 163 |
+
output_token_len: int = 48,
|
| 164 |
+
num_samples: int = 100,
|
| 165 |
+
batch_size: int = 256,
|
| 166 |
+
use_amp: int = 1,
|
| 167 |
+
downsample_factor: int = 1,
|
| 168 |
+
force_flip_invariance: bool = False,
|
| 169 |
+
adaptive_ar_rollout: bool = False,
|
| 170 |
+
):
|
| 171 |
+
self.device = _resolve_device(device)
|
| 172 |
+
self.prediction_length = int(prediction_length)
|
| 173 |
+
self.num_samples = int(num_samples)
|
| 174 |
+
self.batch_size = int(batch_size)
|
| 175 |
+
self.seq_len = int(seq_len)
|
| 176 |
+
self.input_token_len = int(input_token_len)
|
| 177 |
+
self.output_token_len = int(output_token_len)
|
| 178 |
+
self.use_amp = int(use_amp)
|
| 179 |
+
self.downsample_factor = int(downsample_factor)
|
| 180 |
+
self.force_flip_invariance = bool(force_flip_invariance)
|
| 181 |
+
self.adaptive_ar_rollout = bool(adaptive_ar_rollout)
|
| 182 |
+
# Quantile levels emitted by the model (set by subclasses from cfg).
|
| 183 |
+
self.quantiles = [0.5]
|
| 184 |
+
self.model = None
|
| 185 |
+
|
| 186 |
+
def _downsample_if_needed(
|
| 187 |
+
self, series: torch.Tensor
|
| 188 |
+
) -> Tuple[torch.Tensor, int]:
|
| 189 |
+
cur = series
|
| 190 |
+
if self.downsample_factor > 1:
|
| 191 |
+
cur = cur[::self.downsample_factor]
|
| 192 |
+
return cur, self.downsample_factor
|
| 193 |
+
|
| 194 |
+
def _left_pad_to_len(
|
| 195 |
+
self, arr: np.ndarray, target_len: int
|
| 196 |
+
) -> Tuple[np.ndarray, int]:
|
| 197 |
+
if arr.shape[0] >= target_len:
|
| 198 |
+
return arr[-target_len:], 0
|
| 199 |
+
pad_len = target_len - arr.shape[0]
|
| 200 |
+
fill_value = arr[0] if arr.shape[0] > 0 else 0.0
|
| 201 |
+
padding = np.full((pad_len,), fill_value, dtype=arr.dtype)
|
| 202 |
+
return np.concatenate([padding, arr], axis=0), pad_len
|
| 203 |
+
|
| 204 |
+
def _prepare_context_matrix(
|
| 205 |
+
self, context: List[torch.Tensor]
|
| 206 |
+
) -> Tuple[torch.Tensor, List[int]]:
|
| 207 |
+
xs = []
|
| 208 |
+
downsample_factors = []
|
| 209 |
+
for c in context:
|
| 210 |
+
cur, df = self._downsample_if_needed(c)
|
| 211 |
+
downsample_factors.append(df)
|
| 212 |
+
|
| 213 |
+
cur_np = cur.detach().cpu().float().numpy()
|
| 214 |
+
cur_np, _ = self._left_pad_to_len(cur_np, self.seq_len)
|
| 215 |
+
|
| 216 |
+
x2d = cur_np[None, :]
|
| 217 |
+
x_interp = np.copy(x2d)
|
| 218 |
+
series = x2d[0]
|
| 219 |
+
if np.any(np.isnan(series)):
|
| 220 |
+
valid_mask = ~np.isnan(series)
|
| 221 |
+
if np.sum(valid_mask) >= 2:
|
| 222 |
+
valid_idx = np.where(valid_mask)[0]
|
| 223 |
+
valid_val = series[valid_mask]
|
| 224 |
+
x_interp[0] = np.interp(
|
| 225 |
+
np.arange(len(series)), valid_idx, valid_val
|
| 226 |
+
)
|
| 227 |
+
else:
|
| 228 |
+
x_interp = _numpy_fill(x2d)
|
| 229 |
+
ff = _numpy_fill(x_interp)
|
| 230 |
+
bf = np.flip(_numpy_fill(np.flip(x_interp, axis=1)), axis=1)
|
| 231 |
+
x_imp = np.where(np.isnan(ff), bf, ff)
|
| 232 |
+
x_imp = np.where(np.isnan(x_imp), 0.0, x_imp)
|
| 233 |
+
xs.append(x_imp[0])
|
| 234 |
+
|
| 235 |
+
x = torch.tensor(
|
| 236 |
+
np.stack(xs), device=self.device, dtype=torch.float32
|
| 237 |
+
).unsqueeze(-1)
|
| 238 |
+
return x, downsample_factors
|
| 239 |
+
|
| 240 |
+
def _decode_autoregressive(
|
| 241 |
+
self,
|
| 242 |
+
init_ctx: torch.Tensor,
|
| 243 |
+
use_bf16: bool,
|
| 244 |
+
downsample_factors: List[int],
|
| 245 |
+
) -> torch.Tensor:
|
| 246 |
+
B, _, C = init_ctx.shape
|
| 247 |
+
roll_len = int(self.output_token_len)
|
| 248 |
+
|
| 249 |
+
if self.adaptive_ar_rollout:
|
| 250 |
+
try:
|
| 251 |
+
from .periodogram import significant_periods
|
| 252 |
+
periods, _s, _nv = significant_periods(
|
| 253 |
+
init_ctx[:, -self.seq_len:, 0].float(),
|
| 254 |
+
min_period=2, max_period=self.seq_len // 2, top_k=1,
|
| 255 |
+
)
|
| 256 |
+
pos = periods[periods > 0]
|
| 257 |
+
if pos.numel() > 0:
|
| 258 |
+
p0 = int(pos.float().median().item())
|
| 259 |
+
if 2 <= p0 < roll_len:
|
| 260 |
+
roll_len = max(4, p0)
|
| 261 |
+
except Exception:
|
| 262 |
+
pass
|
| 263 |
+
|
| 264 |
+
target_pred_lens = [
|
| 265 |
+
int(self.prediction_length) // int(max(1, df))
|
| 266 |
+
for df in downsample_factors
|
| 267 |
+
]
|
| 268 |
+
max_target_pred_len = max(target_pred_lens)
|
| 269 |
+
steps = math.ceil(max_target_pred_len / roll_len)
|
| 270 |
+
preds: List[torch.Tensor] = []
|
| 271 |
+
batch_ctx = init_ctx
|
| 272 |
+
|
| 273 |
+
y_mark = torch.zeros(
|
| 274 |
+
B, self.output_token_len, C,
|
| 275 |
+
device=self.device, dtype=init_ctx.dtype,
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
for _ in range(steps):
|
| 279 |
+
x_in = batch_ctx[:, -self.seq_len:, :]
|
| 280 |
+
x_mark = torch.zeros_like(x_in)
|
| 281 |
+
if _autocast_fp is not None and self.use_amp and use_bf16:
|
| 282 |
+
try:
|
| 283 |
+
with _autocast_fp("cuda", dtype=torch.bfloat16):
|
| 284 |
+
out = self.model(x_in, x_mark, y_mark)
|
| 285 |
+
except Exception:
|
| 286 |
+
out = self.model(x_in, x_mark, y_mark)
|
| 287 |
+
else:
|
| 288 |
+
out = self.model(x_in, x_mark, y_mark)
|
| 289 |
+
chunk = out[:, -self.output_token_len:, :][:, :roll_len, :] # (B, roll, Q)
|
| 290 |
+
preds.append(chunk)
|
| 291 |
+
# Feed only the MEDIAN quantile back into the context (the AR state
|
| 292 |
+
# is a univariate series); keep all Q in preds for the forecast.
|
| 293 |
+
q_mid = chunk.shape[-1] // 2
|
| 294 |
+
batch_ctx = torch.cat([batch_ctx, chunk[:, :, q_mid:q_mid + 1]], dim=1)
|
| 295 |
+
|
| 296 |
+
return torch.cat(preds, dim=1) # (B, pl, Q)
|
| 297 |
+
|
| 298 |
+
@torch.no_grad()
|
| 299 |
+
def predict(self, test_data_input, use_bf16_if_available: bool = True):
|
| 300 |
+
from gluonts.itertools import batcher
|
| 301 |
+
from gluonts.model.forecast import SampleForecast, QuantileForecast
|
| 302 |
+
|
| 303 |
+
forecasts: List = []
|
| 304 |
+
use_bf16 = bool(
|
| 305 |
+
use_bf16_if_available
|
| 306 |
+
and self.device.type == "cuda"
|
| 307 |
+
and torch.cuda.is_available()
|
| 308 |
+
and torch.cuda.is_bf16_supported()
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
for batch in batcher(test_data_input, batch_size=self.batch_size):
|
| 312 |
+
targets = [
|
| 313 |
+
torch.tensor(entry["target"], dtype=torch.float32)
|
| 314 |
+
for entry in batch
|
| 315 |
+
]
|
| 316 |
+
batch_ctx, dfs = self._prepare_context_matrix(targets)
|
| 317 |
+
pred_pos = self._decode_autoregressive(batch_ctx, use_bf16, dfs) # (B,pl,Q)
|
| 318 |
+
if self.force_flip_invariance:
|
| 319 |
+
pred_neg = self._decode_autoregressive(-batch_ctx, use_bf16, dfs)
|
| 320 |
+
# Flip-symmetrize. For quantiles the tau-quantile of -y is
|
| 321 |
+
# -(the (1-tau)-quantile of y), so reverse the quantile axis on
|
| 322 |
+
# the negated branch. Q=1 (median) reverse is a no-op.
|
| 323 |
+
pred = 0.5 * (pred_pos - pred_neg.flip(dims=[-1]))
|
| 324 |
+
else:
|
| 325 |
+
pred = pred_pos
|
| 326 |
+
|
| 327 |
+
Q = pred.shape[-1]
|
| 328 |
+
pred_np = pred.float().detach().cpu().numpy() # (B, pl, Q)
|
| 329 |
+
if not np.isfinite(pred_np).all():
|
| 330 |
+
for qi in range(Q):
|
| 331 |
+
pred_np[:, :, qi] = _numpy_fill(pred_np[:, :, qi])
|
| 332 |
+
|
| 333 |
+
for i, ts in enumerate(batch):
|
| 334 |
+
df = int(max(1, dfs[i]))
|
| 335 |
+
target_pl = int(self.prediction_length) // df
|
| 336 |
+
arr = pred_np[i, :target_pl, :] # (target_pl, Q)
|
| 337 |
+
if df > 1:
|
| 338 |
+
new_len = int(self.prediction_length)
|
| 339 |
+
src = np.linspace(0, 1, arr.shape[0])
|
| 340 |
+
dst = np.linspace(0, 1, new_len)
|
| 341 |
+
arr = np.stack([np.interp(dst, src, arr[:, qi])
|
| 342 |
+
for qi in range(Q)], axis=1) # (new_len, Q)
|
| 343 |
+
start_date = ts["start"] + len(ts["target"])
|
| 344 |
+
if Q > 1:
|
| 345 |
+
# Sort across quantiles to guarantee non-crossing, then a
|
| 346 |
+
# QuantileForecast so gluonts scores WQL/CRPS over the deciles.
|
| 347 |
+
arr = np.sort(arr, axis=1)
|
| 348 |
+
forecasts.append(QuantileForecast(
|
| 349 |
+
forecast_arrays=arr.T, # (Q, pl)
|
| 350 |
+
start_date=start_date,
|
| 351 |
+
forecast_keys=[str(q) for q in self.quantiles],
|
| 352 |
+
))
|
| 353 |
+
else:
|
| 354 |
+
samples = np.repeat(arr[:, 0][None, :], self.num_samples, axis=0)
|
| 355 |
+
forecasts.append(
|
| 356 |
+
SampleForecast(samples=samples, start_date=start_date)
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
return forecasts
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
class _BackboneAdapter(nn.Module):
|
| 363 |
+
"""Adapts the model wrapper to the ``(x, x_mark, y_mark) -> (B, p, Q)`` contract."""
|
| 364 |
+
|
| 365 |
+
def __init__(self, backbone: nn.Module, scale_factor: float = 1.0):
|
| 366 |
+
super().__init__()
|
| 367 |
+
self.backbone = backbone
|
| 368 |
+
self.scale_factor = float(scale_factor)
|
| 369 |
+
# single-shot counterfactual: emit this many steps in ONE forward.
|
| 370 |
+
self.pred_len_override = None
|
| 371 |
+
# Resolved once, and announced when on: a forecast must not change
|
| 372 |
+
# because a variable was exported after the predictor was built.
|
| 373 |
+
self.tilt_k, self.tilt_mode = _resolve_tilt()
|
| 374 |
+
|
| 375 |
+
def forward(self, x, x_mark=None, y_mark=None, **kwargs):
|
| 376 |
+
y_norm, x_min, x_range = self.backbone.encode(
|
| 377 |
+
x, batch_first=True, scale_factor=self.scale_factor,
|
| 378 |
+
horizon=self.pred_len_override,
|
| 379 |
+
)
|
| 380 |
+
# Match training: the chunk loss clamps y_norm to [-5,5] before the
|
| 381 |
+
# pinball loss, so the model is never optimized outside that band.
|
| 382 |
+
# Clamp at inference too: otherwise an un-penalized overshoot feeds
|
| 383 |
+
# back into the AR rollout context and compounds over chunks.
|
| 384 |
+
y_norm = y_norm.clamp(-5.0, 5.0)
|
| 385 |
+
|
| 386 |
+
# num_quantiles > 1 => y_norm is (B, p, Q). Manual inverse since
|
| 387 |
+
# WindowMinMax.inverse_transform squeezes x_min/x_range for the (B, p)
|
| 388 |
+
# point case; x_min/x_range are (B, 1, 1) so they broadcast directly.
|
| 389 |
+
if y_norm.dim() == 3:
|
| 390 |
+
tk = self.tilt_k
|
| 391 |
+
Q = y_norm.shape[-1]
|
| 392 |
+
if tk != 0.0 and Q >= 3:
|
| 393 |
+
# EVAL-ONLY de-hedge probe (no training); default off (tk=0).
|
| 394 |
+
qm = Q // 2
|
| 395 |
+
lo = y_norm[..., 0]; hi = y_norm[..., -1]; med = y_norm[..., qm]
|
| 396 |
+
if self.tilt_mode == "fixed":
|
| 397 |
+
tau = torch.full_like(med, 0.5 + tk)
|
| 398 |
+
else: # skew-adaptive
|
| 399 |
+
asym = (hi + lo - 2 * med) / (hi - lo).abs().clamp(min=1e-6)
|
| 400 |
+
tau = 0.5 + tk * torch.tanh(asym) # >0 right-skew
|
| 401 |
+
tau = tau.clamp(qm / (Q + 1.0), (qm + 2) / (Q + 1.0))
|
| 402 |
+
idxf = (tau * (Q + 1) - 1).clamp(0, Q - 1 - 1e-4)
|
| 403 |
+
ilo = idxf.floor().long().clamp(0, Q - 2)
|
| 404 |
+
frac = (idxf - ilo.to(idxf.dtype)).clamp(0, 1)
|
| 405 |
+
qlo = torch.gather(y_norm, -1, ilo.unsqueeze(-1)).squeeze(-1)
|
| 406 |
+
qhi = torch.gather(y_norm, -1, (ilo + 1).unsqueeze(-1)).squeeze(-1)
|
| 407 |
+
y_norm = y_norm.clone()
|
| 408 |
+
y_norm[..., qm] = qlo * (1 - frac) + qhi * frac
|
| 409 |
+
return y_norm * x_range + x_min # (B, p, Q)
|
| 410 |
+
y_pred = WindowMinMax.inverse_transform(y_norm, x_min, x_range)
|
| 411 |
+
return y_pred.unsqueeze(-1) # (B, p, 1)
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
class TinyCastPredictor(ARRolloutPredictor):
|
| 415 |
+
"""The deployed predictor: AR-rollout decoding around the TinyCast model."""
|
| 416 |
+
|
| 417 |
+
def __init__(
|
| 418 |
+
self,
|
| 419 |
+
prediction_length: int,
|
| 420 |
+
checkpoint_path: str,
|
| 421 |
+
device: Optional[str] = None,
|
| 422 |
+
num_samples: int = 100,
|
| 423 |
+
batch_size: int = 256,
|
| 424 |
+
use_amp: int = 1,
|
| 425 |
+
downsample_factor: int = 1,
|
| 426 |
+
force_flip_invariance: bool = False,
|
| 427 |
+
freq: Optional[str] = None,
|
| 428 |
+
domain: Optional[str] = None,
|
| 429 |
+
no_daily: bool = False,
|
| 430 |
+
single_shot: bool = False,
|
| 431 |
+
adaptive_ar_rollout: bool = False,
|
| 432 |
+
config_path: Optional[str] = None,
|
| 433 |
+
):
|
| 434 |
+
self.single_shot = bool(single_shot)
|
| 435 |
+
# 1. Load the weights (safetensors + config.json).
|
| 436 |
+
model, cfg = load_checkpoint(checkpoint_path, config_path)
|
| 437 |
+
|
| 438 |
+
# 2. Eval-time scale_factor from (freq, domain). bizitobs_l2c has no
|
| 439 |
+
# daily cycle -> /7.
|
| 440 |
+
if freq is not None and domain is not None:
|
| 441 |
+
sf_eval = seasonal_scale_factor(freq, domain)
|
| 442 |
+
if no_daily:
|
| 443 |
+
sf_eval /= 7
|
| 444 |
+
else:
|
| 445 |
+
sf_eval = 1.0
|
| 446 |
+
self._eval_scale_factor = float(sf_eval)
|
| 447 |
+
|
| 448 |
+
# 3. Base predictor bookkeeping (drives AR-rollout / batching / flip).
|
| 449 |
+
super().__init__(
|
| 450 |
+
prediction_length=prediction_length,
|
| 451 |
+
device=device,
|
| 452 |
+
seq_len=int(cfg.seq_len),
|
| 453 |
+
input_token_len=int(cfg.seq_len),
|
| 454 |
+
output_token_len=int(cfg.output_token_len),
|
| 455 |
+
num_samples=num_samples,
|
| 456 |
+
batch_size=batch_size,
|
| 457 |
+
use_amp=use_amp,
|
| 458 |
+
downsample_factor=downsample_factor,
|
| 459 |
+
force_flip_invariance=force_flip_invariance,
|
| 460 |
+
adaptive_ar_rollout=adaptive_ar_rollout,
|
| 461 |
+
)
|
| 462 |
+
self.quantiles = list(getattr(cfg, "quantiles", [0.5])) or [0.5]
|
| 463 |
+
self._missing_aware = bool(getattr(cfg, "missing_channel", False))
|
| 464 |
+
|
| 465 |
+
# 4. Install the model wrapped in the adapter interface.
|
| 466 |
+
backbone = model.model # TinyCastForPrediction -> TinyCastBackbone
|
| 467 |
+
backbone.to(self.device).eval()
|
| 468 |
+
# Optional INT8 post-training fake-quant. Off by default;
|
| 469 |
+
# TINYCAST_INT8=w8 (weights) or w8a8 (+ dynamic activations).
|
| 470 |
+
# quantize_int8_ prints the mode it applied.
|
| 471 |
+
_int8 = _resolve_int8()
|
| 472 |
+
if _int8 is not None:
|
| 473 |
+
from .quant import quantize_int8_
|
| 474 |
+
quantize_int8_(backbone, _int8)
|
| 475 |
+
self.model = _BackboneAdapter(
|
| 476 |
+
backbone, scale_factor=self._eval_scale_factor,
|
| 477 |
+
).to(self.device).eval()
|
| 478 |
+
if self.single_shot:
|
| 479 |
+
self.model.pred_len_override = int(prediction_length)
|
| 480 |
+
|
| 481 |
+
def _decode_autoregressive(self, init_ctx, use_bf16, downsample_factors):
|
| 482 |
+
"""Single-shot override: one forward for the full horizon (no AR loop).
|
| 483 |
+
Default deployed behavior (single_shot=False) falls back to the base
|
| 484 |
+
AR rollout."""
|
| 485 |
+
if not self.single_shot:
|
| 486 |
+
return super()._decode_autoregressive(init_ctx, use_bf16, downsample_factors)
|
| 487 |
+
x_in = init_ctx[:, -self.seq_len:, :]
|
| 488 |
+
x_mark = torch.zeros_like(x_in)
|
| 489 |
+
use_cuda = bool(self.use_amp and use_bf16 and str(self.device).startswith("cuda"))
|
| 490 |
+
if use_cuda:
|
| 491 |
+
with torch.autocast("cuda", dtype=torch.bfloat16):
|
| 492 |
+
out = self.model(x_in, x_mark, None)
|
| 493 |
+
else:
|
| 494 |
+
out = self.model(x_in, x_mark, None)
|
| 495 |
+
return out # (B, prediction_length, 1)
|
| 496 |
+
|
| 497 |
+
def _prepare_context_matrix(self, context):
|
| 498 |
+
"""Missing-aware override: keep genuine gaps as NaN so the observed-mask
|
| 499 |
+
+ observed-only normalization see true missingness. Falls back to the
|
| 500 |
+
base (interpolating) behavior when the model is not missing-aware
|
| 501 |
+
(the deployed model is not)."""
|
| 502 |
+
if not getattr(self, "_missing_aware", False):
|
| 503 |
+
return super()._prepare_context_matrix(context)
|
| 504 |
+
xs, dfs = [], []
|
| 505 |
+
for c in context:
|
| 506 |
+
cur, df = self._downsample_if_needed(c)
|
| 507 |
+
dfs.append(df)
|
| 508 |
+
a = cur.detach().cpu().float().numpy()
|
| 509 |
+
if a.shape[0] >= self.seq_len:
|
| 510 |
+
a = a[-self.seq_len:]
|
| 511 |
+
else:
|
| 512 |
+
pad = np.full((self.seq_len - a.shape[0],), np.nan, dtype=a.dtype)
|
| 513 |
+
a = np.concatenate([pad, a], axis=0)
|
| 514 |
+
xs.append(a)
|
| 515 |
+
x = torch.tensor(
|
| 516 |
+
np.stack(xs), device=self.device, dtype=torch.float32
|
| 517 |
+
).unsqueeze(-1)
|
| 518 |
+
return x, dfs
|
tinycast/quant.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Post-training INT8 quantization (fake-quant) for TinyCast.
|
| 2 |
+
|
| 3 |
+
Measures the GIFT-Eval accuracy of an INT8-deployed model by simulating INT8 arithmetic in
|
| 4 |
+
floating point. Per-output-channel symmetric INT8 weights on every Linear and Conv1d; optional
|
| 5 |
+
per-tensor dynamic INT8 activations. RMSNorm, the SiLU gate, the min-max (de)normalization,
|
| 6 |
+
and the rFFT period detector stay in full precision: they are off the convolutional mixing path
|
| 7 |
+
and run as fp/LUT ops on the target runtime.
|
| 8 |
+
|
| 9 |
+
Modes (env ``TINYCAST_INT8``):
|
| 10 |
+
``w8`` per-channel INT8 weights, fp activations. The ~145 KB weight footprint; isolates the
|
| 11 |
+
weight-quantization error.
|
| 12 |
+
``w8a8`` + per-tensor dynamic INT8 activations (scale from each tensor's own range, so no
|
| 13 |
+
calibration set is needed; an optimistic but faithful estimate of full INT8 compute).
|
| 14 |
+
|
| 15 |
+
The weight quant is applied in-place to the parameter tensors, so it is correct regardless of
|
| 16 |
+
whether a module is invoked via ``__call__`` or functionally (the separable pointwise conv is run
|
| 17 |
+
as ``F.linear(weight.squeeze(-1))``). Activation quant (``w8a8``) uses a forward-pre-hook on every
|
| 18 |
+
Linear/Conv1d to quantize inputs, plus a forward-hook on Conv1d to quantize the depthwise output
|
| 19 |
+
that feeds the functional pointwise, covering every activation site on the mixing path.
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
import torch.nn as nn
|
| 25 |
+
|
| 26 |
+
_QMIN, _QMAX = -128, 127 # int8 symmetric (zero-point 0)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@torch.no_grad()
|
| 30 |
+
def _fq_weight_per_outchannel(w: torch.Tensor) -> torch.Tensor:
|
| 31 |
+
"""Symmetric per-output-channel (axis 0) int8 fake-quant of a weight tensor.
|
| 32 |
+
|
| 33 |
+
Linear weight is (out, in); Conv1d weight is (out, in/groups, k). Axis 0 is the output
|
| 34 |
+
channel in both, so a per-axis-0 scale is the standard per-channel weight scheme.
|
| 35 |
+
"""
|
| 36 |
+
red = tuple(d for d in range(w.dim()) if d != 0)
|
| 37 |
+
amax = w.abs().amax(dim=red, keepdim=True).clamp_(min=1e-12)
|
| 38 |
+
scale = amax / _QMAX
|
| 39 |
+
return (torch.round(w / scale).clamp_(_QMIN, _QMAX) * scale).to(w.dtype)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _fq_act_dynamic(x: torch.Tensor) -> torch.Tensor:
|
| 43 |
+
"""Per-tensor symmetric int8 fake-quant with a dynamic (this-tensor) scale."""
|
| 44 |
+
if not torch.is_floating_point(x):
|
| 45 |
+
return x
|
| 46 |
+
amax = x.detach().abs().amax().clamp(min=1e-12)
|
| 47 |
+
scale = amax / _QMAX
|
| 48 |
+
return torch.round(x / scale).clamp(_QMIN, _QMAX) * scale
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _pre_hook(_mod, inp):
|
| 52 |
+
if not inp:
|
| 53 |
+
return None
|
| 54 |
+
return (_fq_act_dynamic(inp[0]),) + tuple(inp[1:])
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _post_hook(_mod, _inp, out):
|
| 58 |
+
return _fq_act_dynamic(out)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def quantize_int8_(model: nn.Module, mode: str = "w8") -> nn.Module:
|
| 62 |
+
"""In-place INT8 fake-quant of ``model``. ``mode`` in {"w8", "w8a8"}. Returns ``model``."""
|
| 63 |
+
mode = mode.strip().lower()
|
| 64 |
+
if mode not in ("w8", "w8a8"):
|
| 65 |
+
raise ValueError(f"unknown INT8 mode {mode!r} (expected 'w8' or 'w8a8')")
|
| 66 |
+
n_w = 0
|
| 67 |
+
with torch.no_grad():
|
| 68 |
+
for m in model.modules():
|
| 69 |
+
if isinstance(m, (nn.Linear, nn.Conv1d)):
|
| 70 |
+
m.weight.data.copy_(_fq_weight_per_outchannel(m.weight.data))
|
| 71 |
+
n_w += 1
|
| 72 |
+
if mode == "w8a8":
|
| 73 |
+
m.register_forward_pre_hook(_pre_hook)
|
| 74 |
+
if isinstance(m, nn.Conv1d):
|
| 75 |
+
m.register_forward_hook(_post_hook)
|
| 76 |
+
print(
|
| 77 |
+
f"[quant] INT8 {mode}: fake-quantized {n_w} Linear/Conv1d weight tensors"
|
| 78 |
+
+ (" + per-tensor dynamic activation quant" if mode == "w8a8" else ""),
|
| 79 |
+
flush=True,
|
| 80 |
+
)
|
| 81 |
+
return model
|
tinycast/reference/dataset_properties.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"m4_yearly": {"domain": "Econ/Fin", "frequency": "A", "num_variates": 1}, "m4_quarterly": {"domain": "Econ/Fin", "frequency": "Q", "num_variates": 1}, "m4_monthly": {"domain": "Econ/Fin", "frequency": "M", "num_variates": 1}, "m4_weekly": {"domain": "Econ/Fin", "frequency": "W", "num_variates": 1}, "m4_daily": {"domain": "Econ/Fin", "frequency": "D", "num_variates": 1}, "m4_hourly": {"domain": "Econ/Fin", "frequency": "H", "num_variates": 1}, "electricity": {"domain": "Energy", "frequency": "W", "num_variates": 1}, "ett1": {"domain": "Energy", "frequency": "W", "num_variates": 7}, "ett2": {"domain": "Energy", "frequency": "W", "num_variates": 7}, "solar": {"domain": "Energy", "frequency": "W", "num_variates": 1}, "hospital": {"domain": "Healthcare", "frequency": "M", "num_variates": 1}, "covid_deaths": {"domain": "Healthcare", "frequency": "D", "num_variates": 1}, "us_births": {"domain": "Healthcare", "frequency": "M", "num_variates": 1}, "saugeen": {"domain": "Nature", "frequency": "M", "num_variates": 1}, "temperature_rain": {"domain": "Nature", "frequency": "D", "num_variates": 1}, "kdd_cup_2018": {"domain": "Nature", "frequency": "D", "num_variates": 1}, "jena_weather": {"domain": "Nature", "frequency": "D", "num_variates": 21}, "car_parts": {"domain": "Sales", "frequency": "M", "num_variates": 1}, "restaurant": {"domain": "Sales", "frequency": "D", "num_variates": 1}, "hierarchical_sales": {"domain": "Sales", "frequency": "W-WED", "num_variates": 1}, "loop_seattle": {"domain": "Transport", "frequency": "D", "num_variates": 1}, "sz_taxi": {"domain": "Transport", "frequency": "H", "num_variates": 1}, "m_dense": {"domain": "Transport", "frequency": "D", "num_variates": 1}, "bitbrains_fast_storage": {"domain": "Web/CloudOps", "frequency": "H", "num_variates": 2}, "bitbrains_rnd": {"domain": "Web/CloudOps", "frequency": "H", "num_variates": 2}, "bizitobs_application": {"domain": "Web/CloudOps", "frequency": "10S", "num_variates": 2}, "bizitobs_service": {"domain": "Web/CloudOps", "frequency": "10S", "num_variates": 2}, "bizitobs_l2c": {"domain": "Web/CloudOps", "frequency": "H", "num_variates": 7}}
|
tinycast/reference/gift_eval_tinycast.csv
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
dataset,model,eval_metrics/MSE[mean],eval_metrics/MSE[0.5],eval_metrics/MAE[0.5],eval_metrics/MASE[0.5],eval_metrics/MAPE[0.5],eval_metrics/sMAPE[0.5],eval_metrics/MSIS,eval_metrics/RMSE[mean],eval_metrics/NRMSE[mean],eval_metrics/ND[0.5],eval_metrics/mean_weighted_sum_quantile_loss,domain,num_variates
|
| 2 |
+
loop_seattle/5T/short,TinyCast,44.24408692791981,44.24408692791981,3.7410070822826973,0.5934177626553161,0.10333592953204616,0.0790137582765152,4.834083147652098,6.651622879261858,0.11406064670352252,0.06415001194039043,0.05139483459546802,Transport,1
|
| 3 |
+
loop_seattle/5T/medium,TinyCast,98.20614629692338,98.20614629692338,5.956920582994711,0.9436984543863005,0.20160873373718577,0.132715571219461,9.629173740418173,9.909901427205186,0.17631750976206734,0.10598585775643184,0.08725979064788297,Transport,1
|
| 4 |
+
loop_seattle/5T/long,TinyCast,113.45040647216489,113.45040647216489,6.401010942068713,1.0107825740241352,0.21699007191552494,0.14103020212133877,12.857768236014797,10.65131008243422,0.1883173522634331,0.1131711895617032,0.09563315974138765,Transport,1
|
| 5 |
+
loop_seattle/D/short,TinyCast,18.663410946557644,18.663410946557644,2.926714380065477,0.883553479359932,0.054177088301986365,0.05405887580631441,7.380008644327176,4.32011700611889,0.07720626099359222,0.0523042949903881,0.042698347325786395,Transport,1
|
| 6 |
+
loop_seattle/H/short,TinyCast,63.06633140725992,63.06633140725992,4.349558852172275,0.8845625595564358,0.11818351171152795,0.09950442329946693,6.687267308602856,7.941431319810046,0.1405714099768551,0.07699161473599359,0.06068601926596376,Transport,1
|
| 7 |
+
loop_seattle/H/medium,TinyCast,72.35113097073739,72.35113097073739,4.853164616162563,1.0019077460097932,0.13148091333938458,0.10949005741083954,8.615747092001905,8.505946800370749,0.15034165879301567,0.08577914204185315,0.06962011250734562,Transport,1
|
| 8 |
+
loop_seattle/H/long,TinyCast,65.8910875654401,65.8910875654401,4.646866349678578,0.9738467480194081,0.14407944582482157,0.10641190073192427,10.483142999498524,8.11733254003063,0.14506757706093826,0.08304570977590309,0.06843967546853323,Transport,1
|
| 9 |
+
m_dense/D/short,TinyCast,10480.7,10480.7,53.51029947916667,0.7748143251363052,0.1382961343888268,0.11246461091218171,7.494234233556761,102.37528998737928,0.17722676315524236,0.09263423989645812,0.07786557125473008,Transport,1
|
| 10 |
+
m_dense/H/short,TinyCast,51430.0996875,51430.0996875,102.29150770399306,0.8921151875559064,0.3215289556467253,0.24313662423027887,7.9984136474443925,226.78205327472455,0.4022548907535992,0.18143966271724662,0.1437088438495966,Transport,1
|
| 11 |
+
m_dense/H/medium,TinyCast,42422.50555555556,42422.50555555556,98.20399739583333,0.8434814150427642,0.3026363512886733,0.23359158833821614,8.648207997321895,205.96724388978836,0.3583569939963727,0.17086255387302987,0.1376271248660661,Transport,1
|
| 12 |
+
m_dense/H/long,TinyCast,45079.36987654321,45079.36987654321,103.38788966049383,0.8902011815198739,0.3177976149730835,0.24438393675250772,9.89889499669728,212.31902853146067,0.36755564448629796,0.1789797300745008,0.1456864229304492,Transport,1
|
| 13 |
+
sz_taxi/15T/short,TinyCast,17.307643725030733,17.307643725030733,2.8093365743774488,0.56131924208872,1298855927179.874,0.41046200974665864,4.0379503080516095,4.160245632775874,0.3890007772564103,0.2626849968660404,0.206862466845479,Transport,1
|
| 14 |
+
sz_taxi/15T/medium,TinyCast,17.176065872061965,17.176065872061965,2.8675911667000533,0.5599120350379014,11519178918165.262,0.4170552441197583,5.753307664775564,4.144401750803361,0.38570572924704843,0.26687720173847523,0.22147479893996128,Transport,1
|
| 15 |
+
sz_taxi/15T/long,TinyCast,16.577307302795585,16.577307302795585,2.84828851161859,0.537775482581525,8185874683847.322,0.41623861198751333,7.003358529588487,4.0715239533613925,0.376852965628887,0.2636324837740379,0.22561013457503432,Transport,1
|
| 16 |
+
sz_taxi/H/short,TinyCast,7.586486685989249,7.586486685989249,1.9103956956129808,0.5823192162139363,1.3190546361809103,0.3059674409719614,4.111199385015859,2.754357762889427,0.2565854224458191,0.17796514788380788,0.13980384104042456,Transport,1
|
| 17 |
+
bitbrains_fast_storage/5T/short,TinyCast,1900821.8495638215,1900821.8495638215,180.9161150882152,0.8707491108197133,2.5999596170141994,0.6328290414944234,14.628815830083955,1378.7029591481341,4.328786309811298,0.5680318570593322,0.4382953022346899,Web/CloudOps,2
|
| 18 |
+
bitbrains_fast_storage/5T/medium,TinyCast,3698363.988009589,3698363.988009589,306.51702595059857,1.0739030132087344,5.425270140752215,0.7169551569543965,25.616906164417372,1923.1130980807106,5.842948631816825,0.9312833650782235,0.7348087900238709,Web/CloudOps,2
|
| 19 |
+
bitbrains_fast_storage/5T/long,TinyCast,3801015.314638277,3801015.314638277,320.72388129341397,0.9810565674831482,3.724999236613369,0.7443551005073823,22.721673451081248,1949.6192742785133,5.152261640601248,0.8475774591551065,0.7107125865949239,Web/CloudOps,2
|
| 20 |
+
bitbrains_fast_storage/H/short,TinyCast,3350219.742214611,3350219.742214611,342.13923596997927,1.035867798928794,4.369458581754894,0.5601783613841338,18.596723121472916,1830.360549786465,5.217122071810725,0.9752079500509765,0.7268432335817985,Web/CloudOps,2
|
| 21 |
+
bitbrains_rnd/5T/short,TinyCast,1716487.2211356303,1716487.2211356303,127.88738486392651,1.727615235380808,1.348279409098898,0.6111930074239366,54.26260705883576,1310.1477859904319,5.360746534769076,0.5232782611098349,0.4328036309064437,Web/CloudOps,2
|
| 22 |
+
bitbrains_rnd/5T/medium,TinyCast,2263940.9115963113,2263940.9115963113,148.47080556987703,4.488258259100947,0.4486479054342338,0.7040026541179003,164.2567076945487,1504.6397946340219,6.330786610708255,0.6246923624743488,0.5911119097203699,Web/CloudOps,2
|
| 23 |
+
bitbrains_rnd/5T/long,TinyCast,2859365.748948195,2859365.748948195,212.117391142277,3.450989893916647,3.026372416404876,0.6839630832295709,122.2642737201257,1690.9659218766637,6.477709497487906,0.8125739386041303,0.7324299246088727,Web/CloudOps,2
|
| 24 |
+
bitbrains_rnd/H/short,TinyCast,1951804.6616925949,1951804.6616925949,190.79642492003018,5.873774881374325,3.0833461830771283,0.5758552192726804,193.56051832268145,1397.0700274834453,6.114262106732073,0.83501852307995,0.6768112378253903,Web/CloudOps,2
|
| 25 |
+
bizitobs_application/10S/short,TinyCast,1202011.5911111112,1202011.5911111112,511.65680555555554,1.4365212207493263,0.020941043429904514,0.02126424577501085,16.39675337008677,1096.3628920713759,0.04228233445947083,0.019732557839577083,0.015573763327225789,Web/CloudOps,2
|
| 26 |
+
bizitobs_application/10S/medium,TinyCast,3821721.1733333333,3821721.1733333333,1116.08125,2.6158345668367957,0.03760443687438965,0.03825534502665202,41.57801287990864,1954.9222934258366,0.07676404478273681,0.043825225864110855,0.038683657904042684,Web/CloudOps,2
|
| 27 |
+
bizitobs_application/10S/long,TinyCast,6377225.102222222,6377225.102222222,1519.8002777777779,3.2712700300710433,0.04648904588487413,0.047585703531901044,74.82748279411558,2525.3168320474606,0.09739137623221442,0.0586126218985649,0.05436535009286809,Web/CloudOps,2
|
| 28 |
+
bizitobs_l2c/5T/short,TinyCast,20.910473051525297,20.910473051525297,2.7368809836251393,0.2856036998160157,0.14710997590632013,0.21205160390763056,2.9522126094428813,4.572797070888375,0.15704835442944587,0.09399556728286393,0.0764864945634414,Web/CloudOps,7
|
| 29 |
+
bizitobs_l2c/5T/medium,TinyCast,57.513790521775746,57.513790521775746,4.788768947678435,0.4874476313438473,0.3732310604071342,0.6381389022576076,3.4790807741267313,7.583784709614043,0.39901860010963586,0.2519596685450198,0.19883974174097674,Web/CloudOps,7
|
| 30 |
+
bizitobs_l2c/5T/long,TinyCast,58.49988337926971,58.49988337926971,4.703327181899804,0.481824424146925,0.3912872161892713,0.7234287770552564,3.577103266965183,7.648521646649744,0.4294530558989807,0.2640847897769059,0.2079641779499675,Web/CloudOps,7
|
| 31 |
+
bizitobs_l2c/H/short,TinyCast,74.67544022817461,74.67544022817461,5.2198379758804565,0.5344963489050996,0.4788500010538444,0.6654215010385665,3.6846044295181293,8.641495254189207,0.46579872545263284,0.28136263513605253,0.22171674144342682,Web/CloudOps,7
|
| 32 |
+
bizitobs_l2c/H/medium,TinyCast,100.58119419642857,100.58119419642857,5.87127685546875,0.6149229551869348,0.5341694058527017,0.8577732631138393,5.103138751198932,10.029017608740578,0.6072722494122171,0.35551473155605917,0.28786509557482653,Web/CloudOps,7
|
| 33 |
+
bizitobs_l2c/H/long,TinyCast,116.8093501984127,116.8093501984127,6.622200520833333,0.7065034860463142,0.7182541012578003,0.8494622124565973,10.697783398926884,10.807837443189674,0.660173904369807,0.4045031206601936,0.3487624586820504,Web/CloudOps,7
|
| 34 |
+
bizitobs_service/10S/short,TinyCast,4698.85648354828,4698.85648354828,24.49354755704365,0.9619385844153198,0.06322367875033585,0.05459318695875703,9.685720597059765,68.54820554579295,0.050782703545633356,0.018145603586067316,0.015078402238691998,Web/CloudOps,2
|
| 35 |
+
bizitobs_service/10S/medium,TinyCast,28334.413174603174,28334.413174603174,45.6440091765873,1.2868080102589707,0.09811119200691344,0.08665448264470176,15.65923175791177,168.3282898820135,0.126687833856165,0.034352755886404615,0.02937165679843956,Web/CloudOps,2
|
| 36 |
+
bizitobs_service/10S/long,TinyCast,134291.53185185185,134291.53185185185,80.87890873015873,1.5712950980940312,0.12657031508349867,0.1140683257895172,27.175007100786228,366.4580901711024,0.2714838685694251,0.05991768122102279,0.05634743240727026,Web/CloudOps,2
|
| 37 |
+
car_parts/M/short,TinyCast,1.3387428151669534,1.3387428151669534,0.5610735126585066,1.0046851599906685,0.6434972493761926,1.7877842143111389,15.150192550816168,1.1570405417127583,2.774464529299755,1.3453967281875052,1.1563300613220318,Sales,1
|
| 38 |
+
covid_deaths/D/short,TinyCast,717588.6957393484,717588.6957393484,197.66975054824562,42.86037270685185,0.12282604843133045,0.16853826186236212,757.6436637266253,847.1060711264844,0.3187277667256405,0.07437420210868682,0.06930300504974816,Healthcare,1
|
| 39 |
+
electricity/15T/short,TinyCast,165728.66631018836,165728.66631018836,61.24668899398666,1.1455489449171512,0.17541189296660817,0.1813955271685565,11.767695984551851,407.09785839548255,0.8024701351167887,0.12072929832196289,0.09967619680692709,Energy,1
|
| 40 |
+
electricity/15T/medium,TinyCast,265624.58404385735,265624.58404385735,60.785754127296244,0.9553742544827218,0.15863709329530282,0.1492815988739177,8.627838373799541,515.3877996653174,0.8880650716880526,0.10473997469812892,0.08532420164947832,Energy,1
|
| 41 |
+
electricity/15T/long,TinyCast,329539.0969178984,329539.0969178984,65.92105753654045,1.0000069893761259,0.16357317447017727,0.15153064711775138,9.484122201829592,574.0549598408661,0.9056420994568655,0.10399855261656309,0.0851238115030191,Energy,1
|
| 42 |
+
electricity/D/short,TinyCast,1781630419.9057298,1781630419.9057298,4758.490560810811,1.520750366833196,0.45952525278329504,0.10174649267798072,12.724417587455866,42209.364125816086,0.693640034801828,0.0781978034155274,0.060897143284970534,Energy,1
|
| 43 |
+
electricity/H/short,TinyCast,2078730.2604737647,2078730.2604737647,195.37475414929088,1.0434622509358347,0.18948337904655863,0.13323683262732833,10.922461867297288,1441.7802400066955,0.6806232349328453,0.09223083622832991,0.07469545370451673,Energy,1
|
| 44 |
+
electricity/H/medium,TinyCast,6897401.130664415,6897401.130664415,270.0492746164133,1.1544809599512351,0.22157058136251614,0.13467502826597633,12.652023545681958,2626.290374399681,1.0251276580896305,0.10540912884379984,0.08775565669684998,Energy,1
|
| 45 |
+
electricity/H/long,TinyCast,10335719.401981981,10335719.401981981,313.20569266141143,1.311547092732918,0.3145948344492027,0.14959712349266505,17.97017321294264,3214.921367931412,1.26796404037728,0.12352823291340928,0.10533341183862409,Energy,1
|
| 46 |
+
electricity/W/short,TinyCast,128317784322.07567,128317784322.07567,38548.00271677928,1.7224971059223015,0.2566139873716758,0.10849626687195923,13.930672069612124,358214.71818181296,0.8173277752266295,0.08795382127192254,0.06694613568802286,Energy,1
|
| 47 |
+
ett1/15T/short,TinyCast,5.361727387564523,5.361727387564523,1.1755770910353887,0.7562283281562213,0.4924726026124937,0.2589161691211519,5.162023400363836,2.315540409400044,0.4377335221089301,0.2222330037862829,0.17693710721247993,Energy,7
|
| 48 |
+
ett1/15T/medium,TinyCast,9.402190212673611,9.402190212673611,1.6372881014384921,1.0455030489866155,0.7463840016001188,0.4031215413411458,9.76289238797673,3.0662991068507344,0.5844396070118025,0.31206871255055085,0.2555580983731971,Energy,7
|
| 49 |
+
ett1/15T/long,TinyCast,9.244584573412698,9.244584573412698,1.6541104077535962,1.074562293727344,0.846863059927027,0.4056002807617187,12.002302210050333,3.0404908441586693,0.5795205611595534,0.31527507920730885,0.26303683899141084,Energy,7
|
| 50 |
+
ett1/D/short,TinyCast,42389.02222222222,42389.02222222222,136.02098214285715,1.6996648224499773,1.2227787078373016,0.4813387916201637,11.203052479321421,205.88594469322626,0.5407662630433157,0.3572636214602873,0.2856896872972474,Energy,7
|
| 51 |
+
ett1/H/short,TinyCast,103.01610834030878,103.01610834030878,5.070075225830078,0.8571495009030993,0.4806339899698893,0.26966083163306825,6.066873579858822,10.149685135032946,0.4737273357601031,0.23664115653652637,0.18454077882381037,Energy,7
|
| 52 |
+
ett1/H/medium,TinyCast,133.77725074404762,133.77725074404762,6.994163876488095,1.3686390900698875,2738232925694.1333,0.4431784130278088,10.267276553788607,11.56621159861982,0.5539941179660003,0.335003870076789,0.26499877408295125,Energy,7
|
| 53 |
+
ett1/H/long,TinyCast,160.18366402116402,160.18366402116402,7.428542493386243,1.41610917545092,3416827299278.7944,0.47630111452132934,17.1122354162061,12.656368516330584,0.6060860764559786,0.35573681090224185,0.29712143678186426,Energy,7
|
| 54 |
+
ett1/W/short,TinyCast,2011626.0,2011626.0,1035.6922433035713,1.6248616254535604,0.8583596774509975,0.5207679952893939,14.300083010139824,1418.318017935329,0.5643825913948318,0.4121266632529299,0.33461569724082,Energy,7
|
| 55 |
+
ett2/15T/short,TinyCast,8.542245556059338,8.542245556059338,1.828979274204799,0.7977637055045982,0.11675881246605886,0.13770481972467333,5.960209582023153,2.922712020719684,0.13875154087160105,0.08682815505567654,0.06987016453124961,Energy,7
|
| 56 |
+
ett2/15T/medium,TinyCast,13.07299045138889,13.07299045138889,2.295947808159722,0.9498484354639674,0.144159166678213,0.17323478577628967,9.902986035407816,3.6156590618293767,0.19268501622242679,0.12235521466375968,0.10234915555751511,Energy,7
|
| 57 |
+
ett2/15T/long,TinyCast,12.420850694444445,12.420850694444445,2.2458406187996034,0.9552525869265864,0.14638871073557067,0.17425280131990947,12.429491781394304,3.524322728474855,0.18781753148114935,0.11968490788741563,0.10269056593429093,Energy,7
|
| 58 |
+
ett2/D/short,TinyCast,119729.95555555556,119729.95555555556,209.34871031746033,1.391076627845421,0.4334957062251984,0.14021989126054069,14.639593650144755,346.02016640010385,0.19803647066492555,0.11981579039409214,0.09908310643497366,Energy,7
|
| 59 |
+
ett2/H/short,TinyCast,105.86847446986607,105.86847446986607,6.4301946730840776,0.7491574015472658,0.12146225350857989,0.11259141308920724,5.280887719007807,10.289240713962624,0.13049619529847512,0.08155275624247994,0.06474864313547667,Energy,7
|
| 60 |
+
ett2/H/medium,TinyCast,262.78627232142856,262.78627232142856,9.951085844494047,1.0705651377694565,0.18129692458733945,0.17153523763020834,8.362985316753157,16.210683894315764,0.21844939017662102,0.13409728105839946,0.10807732772305327,Energy,7
|
| 61 |
+
ett2/H/long,TinyCast,216.7968253968254,216.7968253968254,9.040730406746032,1.0091326427196337,0.17643990593463876,0.1707823253813244,11.704296859619719,14.724022052307086,0.20163414956203768,0.12380584466037664,0.1061356610020145,Energy,7
|
| 62 |
+
ett2/W/short,TinyCast,7161788.571428572,7161788.571428572,1640.3597935267858,1.036383892254434,0.17148779119764054,0.18681137902396067,14.537499283465852,2676.151821445968,0.22448810991134696,0.13760103842854057,0.12450411098056532,Energy,7
|
| 63 |
+
hierarchical_sales/D/short,TinyCast,29.19706801138125,29.19706801138125,2.3333297571794906,0.7575841824952811,0.6457101283071776,1.0286102534621744,7.07832206053394,5.403431133213529,1.6577128322798675,0.7158397294343857,0.5878786597175552,Sales,1
|
| 64 |
+
hierarchical_sales/W/short,TinyCast,480.91189833818856,480.91189833818856,9.262271881103516,0.7521108764594379,0.5638248297323311,0.4720336764545764,6.837358953125831,21.929703562478643,1.0068034171692506,0.42523543257561824,0.36087737376157075,Sales,1
|
| 65 |
+
hospital/M/short,TinyCast,2635.1076773685354,2635.1076773685354,18.3890812163869,0.7787874084304148,0.19600348739922435,0.1764316086352571,5.629575597124316,51.33329988777787,0.18635179891223488,0.066756635020707,0.05370179299767255,Healthcare,1
|
| 66 |
+
jena_weather/10T/short,TinyCast,737.4657753051273,737.4657753051273,5.181869942801339,0.2830066840085954,0.36903646614834207,0.552891525011214,3.213335159674428,27.156321092981784,0.1686747894840554,0.0321859068738754,0.02797473539661994,Nature,21
|
| 67 |
+
jena_weather/10T/medium,TinyCast,1496.4421852453102,1496.4421852453102,9.826428064123377,0.599973524454999,0.6914869268521763,0.6612252459725604,9.462120068850538,38.68387500296875,0.23740228286464188,0.060304621877954157,0.05215981565067656,Nature,21
|
| 68 |
+
jena_weather/10T/long,TinyCast,1347.9896732390873,1347.9896732390873,9.352307839368386,0.6377870157231208,0.7125753689503994,0.6615759794043485,11.832725243656402,36.714978867474336,0.22466525410274407,0.05722837604681763,0.05001671925210481,Nature,21
|
| 69 |
+
jena_weather/D/short,TinyCast,360.4092757936508,360.4092757936508,10.167755611359127,1.2438429713883608,0.8665065087642766,0.44703078497023807,9.15185872707675,18.98444826150212,0.1143174071761791,0.06122650720635964,0.053621856678926466,Nature,21
|
| 70 |
+
jena_weather/H/short,TinyCast,1122.6431671626983,1122.6431671626983,8.337321486190248,0.5583927058292222,1.16205769443803,0.6162634532254442,4.862676662693756,33.50586765273656,0.20538883687596202,0.05110725024217434,0.042352005732441994,Nature,21
|
| 71 |
+
jena_weather/H/medium,TinyCast,1559.271626984127,1559.271626984127,11.34192863343254,0.82201669463664,2.19126262226046,0.6926077222067212,8.148551897694308,39.48761358937922,0.24163171080387966,0.0694032728341155,0.05921602679031589,Nature,21
|
| 72 |
+
jena_weather/H/long,TinyCast,1451.1585978835978,1451.1585978835978,12.337256117724868,1.2372013048116568,2.376308798348106,0.7116374343791336,21.033421879854583,38.094075627105035,0.22936854067949444,0.0742839505914733,0.06403979708580165,Nature,21
|
| 73 |
+
kdd_cup_2018/D/short,TinyCast,2911.8811459995677,2911.8811459995677,21.00633677958142,1.1915066063457622,0.509937226768345,0.4632225368907377,9.645711885756073,53.96184898610839,1.2088027347454506,0.47056425647458555,0.37499475175037367,Nature,1
|
| 74 |
+
kdd_cup_2018/H/short,TinyCast,4428.40544507181,4428.40544507181,24.484380819274286,1.0194315529770632,1.0062891958892606,0.5246448799650837,7.7420535492651315,66.54626544797094,1.392933853478974,0.5125024326316768,0.3995833092562999,Nature,1
|
| 75 |
+
kdd_cup_2018/H/medium,TinyCast,5057.902963232746,5057.902963232746,25.82511224428789,1.0797880779211075,1.123762793111469,0.5598105568251877,10.435659593711573,71.11893533534332,1.4886481232339814,0.5405663722817384,0.44898669999971397,Nature,1
|
| 76 |
+
kdd_cup_2018/H/long,TinyCast,4177.368478330922,4177.368478330922,24.350837097055326,1.042253138917662,0.9913156556511691,0.6197239900842068,14.638124597812372,64.63256515357348,1.516541164902919,0.5713690423671467,0.4937515620723734,Nature,1
|
| 77 |
+
m4_daily/D/short,TinyCast,386967.5754799081,386967.5754799081,185.35799083511822,3.4841999666237293,0.0395577275268352,0.0319446626195895,35.48001408762702,622.0671792338092,0.09609218717022147,0.028632686859902296,0.023237907721219753,Econ/Fin,1
|
| 78 |
+
m4_hourly/H/short,TinyCast,2435120.026100816,2435120.026100816,284.8097814661293,1.0222185676333535,0.10936638276166194,0.10435770348842209,11.960363456937134,1560.4871118022143,0.21304096324626956,0.038882826860023555,0.028535365688984397,Econ/Fin,1
|
| 79 |
+
m4_monthly/M/short,TinyCast,1975177.037971065,1975177.037971065,595.8976114818432,1.03344105749455,0.1709086054806356,0.14037018246562394,10.415824918873035,1405.409918127471,0.2920924274906528,0.12384798031418648,0.10211410097321605,Econ/Fin,1
|
| 80 |
+
m4_quarterly/Q/short,TinyCast,1998109.2978125,1998109.2978125,628.8970325927735,1.376682755781824,0.12536339755853018,0.11501970611512662,12.067188115733634,1413.54494014605,0.23660253103085468,0.10526628863590991,0.08453829758462092,Econ/Fin,1
|
| 81 |
+
m4_weekly/W/short,TinyCast,347017.66530962073,347017.66530962073,292.05628615813157,2.375093592561412,0.07180270041136153,0.07393041340574759,31.16335772623592,589.0820531213124,0.1073216119717094,0.053208124828930614,0.04581422383855976,Econ/Fin,1
|
| 82 |
+
m4_yearly/A/short,TinyCast,4131320.0302225705,4131320.0302225705,983.0706571491143,3.703042215816235,0.17711929984939676,0.16428084468396,30.828436436413924,2032.564889547827,0.32593119996973857,0.15763993591905975,0.12227082973056771,Econ/Fin,1
|
| 83 |
+
restaurant/D/short,TinyCast,149.795676893621,149.795676893621,7.48103590324233,0.7140542511073307,0.7216503356722676,0.4078460662006675,4.798367435523933,12.23910441550447,0.5632719429215224,0.34429460565324277,0.26898689146401417,Sales,1
|
| 84 |
+
saugeen/D/short,TinyCast,1027.6305208333333,1027.6305208333333,12.28921875,2.729262467797945,0.3095691426595052,0.3312169138590495,24.78162106669607,32.056676696646726,1.0383233962018728,0.3980507234083472,0.32716491124571656,Nature,1
|
| 85 |
+
saugeen/M/short,TinyCast,447.1129092261905,447.1129092261905,12.983006068638392,0.7607681767579312,0.36929300853184294,0.3627051852998279,5.014860167518044,21.145044554840513,0.6348376720758496,0.3897887908339379,0.29732434590218304,Nature,1
|
| 86 |
+
saugeen/W/short,TinyCast,1091.752734375,1091.752734375,16.127630615234374,1.3084226666206766,0.43306527137756345,0.4202584266662598,10.76846426338361,33.041681772800246,0.9973906393072427,0.48682594065418355,0.39494308918125093,Nature,1
|
| 87 |
+
solar/10T/short,TinyCast,35.38759293382185,35.38759293382185,2.7478948774128935,1.2092061910074965,5.3764949077865465,1.5784135191980069,11.732245439375333,5.948747173466178,1.73048987304251,0.7993623058580365,0.6564243312663933,Energy,1
|
| 88 |
+
solar/10T/medium,TinyCast,23.771390239645545,23.771390239645545,2.0070813147585573,0.8818089939306053,3.4706532129371834,1.4721230927952096,12.145282983871581,4.8755912707737865,1.0819130604153002,0.44537931240658873,0.3800340063104551,Energy,1
|
| 89 |
+
solar/10T/long,TinyCast,21.99100210123302,21.99100210123302,1.920834464439027,0.8426367678835721,2.7411416188370343,1.47795006596549,11.225711751477654,4.689456482496987,1.0135510118891753,0.4151576461729174,0.35299910057498773,Energy,1
|
| 90 |
+
solar/D/short,TinyCast,134638.40486618006,134638.40486618006,244.66284215328466,0.9544751702039223,1.096048649326148,0.4143624967032105,5.675164959046651,366.9310628254033,0.5300904365706345,0.3534544930892344,0.28338941469586704,Energy,1
|
| 91 |
+
solar/H/short,TinyCast,737.9774478966577,737.9774478966577,11.11688431729915,0.8602663353314536,3.558273438428383,1.4107715080574847,5.32781661862047,27.16574033404313,1.001338234099665,0.40977205752882306,0.3184018809689805,Energy,1
|
| 92 |
+
solar/H/medium,TinyCast,714.6127889294404,714.6127889294404,11.019490916305505,0.8432656365847866,4.550756358452363,1.4003983546347514,8.402250632335997,26.73224249720626,0.9629577598666192,0.39694777902546247,0.33288960205553647,Energy,1
|
| 93 |
+
solar/H/long,TinyCast,851.7509783049472,851.7509783049472,13.291783252230333,1.0149629411795078,4.150918191030254,1.440622485331762,9.849022779881642,29.18477305556696,1.0118894140680637,0.46085041474919114,0.37578656900544266,Energy,1
|
| 94 |
+
solar/W/short,TinyCast,1670333.9124087591,1670333.9124087591,1046.5890810333028,1.145065110150808,0.23824922185744682,0.2054090378058218,9.42922974300857,1292.4139864643832,0.26381400658172083,0.21363499745728415,0.1759532259048021,Energy,1
|
| 95 |
+
temperature_rain/D/short,TinyCast,181.9258203608153,181.9258203608153,6.207237948262678,1.4557875160071272,62.43454723924855,1.457818859810271,18.557788723998502,13.487988002693927,1.5878927988640386,0.7307560206098415,0.5897024269021723,Nature,1
|
| 96 |
+
us_births/D/short,TinyCast,236075.30666666667,236075.30666666667,317.48578125,0.46719668126881475,0.03038674036661784,0.029927581151326498,4.298056948804611,485.87581403756525,0.04554691491119974,0.02976171574366844,0.024416986371726027,Healthcare,1
|
| 97 |
+
us_births/M/short,TinyCast,84781968.0,84781968.0,8055.583333333333,0.9120966855713007,0.024705658356348675,0.02509211003780365,9.086494946974335,9207.712419488349,0.028599091997540623,0.025020586911060013,0.021844573174920206,Healthcare,1
|
| 98 |
+
us_births/W/short,TinyCast,4135229.1428571427,4135229.1428571427,1572.802734375,1.4293763597578706,0.02104850539139339,0.021356597542762756,9.32450229299691,2033.526282804612,0.027604919892820882,0.021350642898871827,0.015937398238737275,Healthcare,1
|
tinycast/reference/seasonal_naive.csv
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
dataset,model,eval_metrics/MSE[mean],eval_metrics/MSE[0.5],eval_metrics/MAE[0.5],eval_metrics/MASE[0.5],eval_metrics/MAPE[0.5],eval_metrics/sMAPE[0.5],eval_metrics/MSIS,eval_metrics/RMSE[mean],eval_metrics/NRMSE[mean],eval_metrics/ND[0.5],eval_metrics/mean_weighted_sum_quantile_loss,domain,num_variates
|
| 2 |
+
bitbrains_fast_storage/H/short,Seasonal_Naive,4198734.112364942,4198734.112364942,323.91137144521844,1.2985124569638047,4.122732886534151,0.31268171558395724,34.57780677446709,2049.0812849579547,5.840547085975178,0.9232525964178199,1.0222395520114795,Web/CloudOps,2
|
| 3 |
+
solar/H/short,Seasonal_Naive,1009.7531694199001,1009.7531694199001,12.29965959124688,0.9519362458316395,3.8670855712527974,0.6200499536903793,16.15202026601857,31.77661356123242,1.171296544389828,0.4533695432534923,0.5916559631947232,Energy,1
|
| 4 |
+
solar/H/medium,Seasonal_Naive,993.4109489051095,993.4109489051095,12.224457306873479,0.9349685908749716,5.243116882202785,0.568037363492515,29.963349764914504,31.518422373353484,1.1353671382460326,0.4403534842165714,0.9459874672135314,Energy,1
|
| 5 |
+
solar/H/long,Seasonal_Naive,1160.9764801297647,1160.9764801297647,13.995331508515815,1.0711966477927781,5.3301362264546865,0.7115105305381197,36.28762062617156,34.073104938202576,1.1813766446766685,0.48524364916713286,1.0779764182077312,Energy,1
|
| 6 |
+
m_dense/D/short,Seasonal_Naive,39929.671111111114,39929.671111111114,118.38299768518519,1.6693368812171987,0.2532523956185716,0.2557375985604745,23.291872240434824,199.82410042612756,0.3459250756939904,0.20493848013227253,0.22686565739428607,Transport,1
|
| 7 |
+
ett2/H/short,Seasonal_Naive,151.78024553571427,151.78024553571427,7.9655081612723215,0.9231678277377344,0.1445715792364967,0.13716111864362443,9.954323499249957,12.319912562015782,0.1562507702523094,0.10102480674150278,0.08895289185095483,Energy,7
|
| 8 |
+
ett2/H/medium,Seasonal_Naive,338.06856398809526,338.06856398809526,11.509515671502976,1.2384249730532626,0.20117125149055434,0.19754707699730284,25.271323860489556,18.3866409109466,0.24777181028131637,0.15509812516606425,0.1862050814081793,Energy,7
|
| 9 |
+
ett2/H/long,Seasonal_Naive,258.848330026455,258.848330026455,10.011796254960318,1.1284016697207502,0.19021284402568,0.18827246610449735,30.708493118791996,16.08876409257265,0.2203232414204263,0.1371038440641976,0.20790633225768584,Energy,7
|
| 10 |
+
ett2/D/short,Seasonal_Naive,122878.94603174603,122878.94603174603,211.56051587301587,1.3901143157307532,0.4713712661985367,0.14168287004743305,32.70861808020462,350.5409334610525,0.20062382493032205,0.12108166507005774,0.15331100213050436,Energy,7
|
| 11 |
+
electricity/15T/short,Seasonal_Naive,366722.87168918917,366722.87168918917,94.21436127533784,1.7170601412372184,0.30235384997180187,0.23004003312303262,26.97516311288429,605.5764788110492,1.193710612408421,0.18571507783204827,0.16489261500071847,Energy,1
|
| 12 |
+
electricity/15T/medium,Seasonal_Naive,373592.9609594595,373592.9609594595,70.78214646677928,1.1507882041044137,0.21527551573127818,0.162019429539667,12.499327683031675,611.2225134592635,1.0531979612070823,0.12196476848793535,0.11275485280084273,Energy,1
|
| 13 |
+
electricity/15T/long,Seasonal_Naive,432425.0750750751,432425.0750750751,73.83208042417418,1.1635317075319347,0.19999027586553747,0.1567721679648986,14.199582505532762,657.5903550654275,1.0374294291948993,0.11647916132088339,0.11255332426055248,Energy,1
|
| 14 |
+
bitbrains_rnd/H/short,Seasonal_Naive,3710112.576957504,3710112.576957504,254.47180959890883,6.0373618687533925,4.792930625938438,0.36119250186151963,222.24992066836998,1926.1652517262125,8.429841528920612,1.1136931405932016,1.2432170953390527,Web/CloudOps,2
|
| 15 |
+
loop_seattle/H/short,Seasonal_Naive,125.56935646488512,125.56935646488512,6.351615330169464,1.2928412613913995,0.1710389719335216,0.14238265652237242,14.224911524098493,11.205773354163698,0.1983535851600468,0.11243005123144872,0.10423508438727377,Transport,1
|
| 16 |
+
loop_seattle/H/medium,Seasonal_Naive,153.75436661506708,153.75436661506708,7.21920260497291,1.4805126192799751,0.20086050437084785,0.16072294158463138,25.49530583293511,12.399772845301122,0.21916459102759323,0.1275985944423001,0.16208090743299983,Transport,1
|
| 17 |
+
loop_seattle/H/long,Seasonal_Naive,167.0609584623323,167.0609584623323,7.511452824002408,1.5460268753677606,0.23476947145360338,0.16369716459260514,30.628061299733826,12.925206321847721,0.23099074580000042,0.13423972094936726,0.18709450197084374,Transport,1
|
| 18 |
+
solar/W/short,Seasonal_Naive,2813150.8321167883,2813150.8321167883,1341.8134124087592,1.4703506784640206,0.3086572076282362,0.2534797557079009,11.583228430599936,1677.245012547895,0.3423676402558515,0.2738976644635694,0.20974762813186765,Energy,1
|
| 19 |
+
bizitobs_application/10S/short,Seasonal_Naive,3938171.448888889,3938171.448888889,1054.6502083333332,2.2423302386755415,0.0340565554300944,0.03481890360514323,26.67150527524178,1984.4826653031992,0.07653356419683467,0.04067364297805646,0.03483868247005456,Web/CloudOps,2
|
| 20 |
+
bizitobs_application/10S/medium,Seasonal_Naive,5143417.173333333,5143417.173333333,1276.9247916666666,2.6914183385868085,0.03906738917032877,0.03992526690165202,28.10250665325578,2267.9103098079813,0.08905416300729038,0.05014107835453231,0.04268912394422428,Web/CloudOps,2
|
| 21 |
+
bizitobs_application/10S/long,Seasonal_Naive,5953087.1466666665,5953087.1466666665,1470.5443055555556,3.2063450561622804,0.04643516540527344,0.04741138034396701,21.30987046900455,2439.894904840507,0.09409699394104339,0.05671301593170149,0.04573090228155046,Web/CloudOps,2
|
| 22 |
+
bizitobs_service/10S/short,Seasonal_Naive,73535.24306878306,73535.24306878306,55.138688409391534,1.225305042620374,0.08240041742879878,0.07553190362516535,16.350613928029045,271.1738244535838,0.2008942442703866,0.04084850431410673,0.03998233993351736,Web/CloudOps,2
|
| 23 |
+
bizitobs_service/10S/medium,Seasonal_Naive,96480.73142857142,96480.73142857142,63.760550595238094,1.3205790727650968,0.0796314687577505,0.07705034528459821,17.448776648119132,310.6134759288003,0.23377502162466005,0.04798769290234763,0.04755831292581585,Web/CloudOps,2
|
| 24 |
+
bizitobs_service/10S/long,Seasonal_Naive,130947.25079365079,130947.25079365079,75.32497354497355,1.367193469742303,0.08491984049479166,0.08144445348668981,17.936180702599792,361.8663438255218,0.26808215608217806,0.055803148480992645,0.05345299584740217,Web/CloudOps,2
|
| 25 |
+
bizitobs_l2c/5T/short,Seasonal_Naive,174.3096912202381,174.3096912202381,9.685235305059523,0.9860210805885202,0.42617953698061745,0.5068802606491816,7.245192912077175,13.202639555037399,0.45343208257188783,0.3326301832497065,0.26206838623738704,Web/CloudOps,7
|
| 26 |
+
bizitobs_l2c/5T/medium,Seasonal_Naive,294.66615646258504,294.66615646258504,12.864580676020408,1.2435997989335994,0.9360730119589105,1.1668089007914848,7.637297246345627,17.16584272509174,0.9031757619892758,0.6768661254104057,0.5203917750796179,Web/CloudOps,7
|
| 27 |
+
bizitobs_l2c/5T/long,Seasonal_Naive,416.33936507936505,416.33936507936505,15.353609871031747,1.4542830442631411,1.0210265852224123,1.298652753589897,8.166517613570452,20.404395729336485,1.1456763155758307,0.862082240570442,0.6484921267253538,Web/CloudOps,7
|
| 28 |
+
ett2/W/short,Seasonal_Naive,4688738.857142857,4688738.857142857,1323.111607142857,0.7785209533197451,0.12217171703066144,0.1269862311226981,16.565488704288857,2165.3495923621335,0.1816396339666693,0.11098877930205708,0.13367758870767996,Energy,7
|
| 29 |
+
loop_seattle/5T/short,Seasonal_Naive,76.27992181453173,76.27992181453173,4.806023281794214,0.7623397118337274,0.13014939133965933,0.10246611478035911,9.641956549599044,8.733837748351622,0.14976603568936095,0.08241268902447142,0.08083202568463041,Transport,1
|
| 30 |
+
loop_seattle/5T/medium,Seasonal_Naive,149.91421294504644,149.91421294504644,7.272513948013416,1.1532851467900647,0.22437167928437823,0.16228546394529234,12.161708217874148,12.243945971174751,0.217844961125775,0.12939296873911593,0.11727937998585615,Transport,1
|
| 31 |
+
loop_seattle/5T/long,Seasonal_Naive,172.4441060371517,172.4441060371517,7.891544207229676,1.2506857542807588,0.23036973349395495,0.1761461930635284,12.853493858763652,13.131797517367975,0.2321728668857043,0.13952411620149754,0.12716656087996273,Transport,1
|
| 32 |
+
loop_seattle/D/short,Seasonal_Naive,58.65786974329205,58.65786974329205,5.940060831075852,1.7323957056549062,0.11095823557630288,0.10706747427075271,21.844122635807963,7.658842585096788,0.13687375626545378,0.10615682844516516,0.1032547596878987,Transport,1
|
| 33 |
+
bitbrains_rnd/5T/short,Seasonal_Naive,3407833.9580161185,3407833.9580161185,210.5518366560826,1.9708126376716957,3.046095137251105,0.38408369415249266,74.37164472655921,1846.0319493486884,7.553429809324837,0.8615173317922609,1.1015904440824371,Web/CloudOps,2
|
| 34 |
+
bitbrains_rnd/5T/medium,Seasonal_Naive,3267017.897300195,3267017.897300195,204.8128727866788,4.542392314855541,2.750306364851926,0.3918429569214854,178.58186300111112,1807.489390646649,7.605029622998441,0.8617522032352579,1.169291300089368,Web/CloudOps,2
|
| 35 |
+
bitbrains_rnd/5T/long,Seasonal_Naive,3851065.095876048,3851065.095876048,237.4491928177016,3.501257904408163,3.179405138911208,0.40348581728614835,137.13909859136817,1962.413079826989,7.517562382830161,0.909614361068727,1.175220506770106,Web/CloudOps,2
|
| 36 |
+
electricity/W/short,Seasonal_Naive,317380739662.4144,317380739662.4144,56604.72342342342,2.0896995064573947,0.285636731125347,0.1196364761812733,15.938528356933686,563365.5471027798,1.2854142672489681,0.12915329923232946,0.0993032024444414,Energy,1
|
| 37 |
+
solar/D/short,Seasonal_Naive,181869.82773722627,181869.82773722627,295.1889902676399,1.155861190610263,1.1535581565541362,0.49778203291324513,22.3811319807538,426.46198861941525,0.6160923187357285,0.4264475482750424,0.5590651164917229,Energy,1
|
| 38 |
+
ett1/H/short,Seasonal_Naive,141.21642485119048,141.21642485119048,5.822230747767857,0.9773176243434534,0.5700293404715402,0.28737351553780693,9.542335004679739,11.883451722929264,0.5546493091237623,0.271747328730894,0.24036844107833774,Energy,7
|
| 39 |
+
ett1/H/medium,Seasonal_Naive,204.51871279761906,204.51871279761906,8.163152785528274,1.5678007461285395,2345799664153.64,0.45697086879185267,22.421278808802057,14.301003908733787,0.6849841868181539,0.3909956677413786,0.4349118298516912,Energy,7
|
| 40 |
+
ett1/H/long,Seasonal_Naive,199.49484126984126,199.49484126984126,7.749353091931217,1.4787012870437326,2646635889605.55,0.47721551158440806,25.35082572229849,14.124264273576916,0.6763804250290198,0.37109973563904386,0.47106248989542887,Energy,7
|
| 41 |
+
temperature_rain/D/short,Seasonal_Naive,310.64911445557556,310.64911445557556,7.890313322375261,2.0115357653222166,97.02872693676676,1.3339967533230714,47.47823470729249,17.625240833973745,2.0749568489172328,0.9288984940737532,1.2679455200160903,Nature,1
|
| 42 |
+
saugeen/W/short,Seasonal_Naive,1800.4892578125,1800.4892578125,24.552409362792968,1.990658004828632,0.824161148071289,0.557866096496582,23.291068065474015,42.43217243805106,1.2808504084686672,0.7411348925661494,0.7340352210434381,Nature,1
|
| 43 |
+
covid_deaths/D/short,Seasonal_Naive,4435194.482205514,4435194.482205514,353.7093984962406,46.91239825526407,0.132724402667158,0.17781104711702989,1472.7784117419058,2105.990142950701,0.792389040211866,0.13308488253209907,0.12672327831223676,Healthcare,1
|
| 44 |
+
us_births/D/short,Seasonal_Naive,2855120.2133333334,2855120.2133333334,1266.88,1.8648391912491094,0.12472813924153646,0.1233361307779948,22.730582398368462,1689.7100974230264,0.15839661042681866,0.11875971986174946,0.11952686561898423,Healthcare,1
|
| 45 |
+
m4_weekly/W/short,Seasonal_Naive,453525.1459181487,453525.1459181487,347.99148275123207,2.777295047362158,0.08937289522218837,0.09161286714732429,26.631225199626535,673.442756229621,0.1226908336142798,0.06339865521526268,0.06087039452311706,Econ/Fin,1
|
| 46 |
+
car_parts/M/short,Seasonal_Naive,2.5046831406935035,2.5046831406935035,0.6672313006509898,1.2014638390969912,0.8541762945076465,1.6763314144736843,20.368070639282948,1.5826190763078472,3.7949581992256025,1.5999522140809175,1.7217438941460104,Sales,1
|
| 47 |
+
sz_taxi/H/short,Seasonal_Naive,12.029984433426817,12.029984433426817,2.4300792726696048,0.7381672994945998,1.552363729884482,0.38466275859082866,8.057108467274903,3.468426795166191,0.3231053592422672,0.22637687999566908,0.21382494112013678,Transport,1
|
| 48 |
+
ett1/15T/short,Seasonal_Naive,8.776201520647321,8.776201520647321,1.4945350283668155,0.9341549587802876,0.6328330752819759,0.30603092738560267,8.439802226867357,2.962465446321243,0.5600292824430536,0.2825293306160358,0.24136394938567346,Energy,7
|
| 49 |
+
ett1/15T/medium,Seasonal_Naive,12.883658234126985,12.883658234126985,1.8389095052083333,1.188305978388322,0.9080556087099148,0.42800251405065676,12.40964763458674,3.5893813163450585,0.6841396278696877,0.3504979699009272,0.32155116949786394,Energy,7
|
| 50 |
+
ett1/15T/long,Seasonal_Naive,13.023148561507936,13.023148561507936,1.8557530381944445,1.190885705867336,0.9840622608053838,0.4204419926758006,13.893107103740524,3.608759975602137,0.6878332194845934,0.35370836394199906,0.3401793468576481,Energy,7
|
| 51 |
+
saugeen/M/short,Seasonal_Naive,713.7325613839286,713.7325613839286,16.662892659505207,0.9763741730697157,0.4971456073579334,0.4422340847197033,10.586458554994822,26.71577364374703,0.8020876713555654,0.500270025848136,0.4450846286544009,Nature,1
|
| 52 |
+
us_births/M/short,Seasonal_Naive,74172117.33333333,74172117.33333333,6704.75,0.7605310792868888,0.020606640726327896,0.020953829089800518,5.646371868808973,8612.323573422756,0.02674981830097076,0.020824907787592,0.016825596814792485,Healthcare,1
|
| 53 |
+
m4_quarterly/Q/short,Seasonal_Naive,2487884.428,2487884.428,708.8467662760416,1.602247174616002,0.14192582162221273,0.12521366802851358,12.540135359281015,1577.302896719587,0.26401272904907985,0.11864846608179633,0.09805397548466407,Econ/Fin,1
|
| 54 |
+
ett1/W/short,Seasonal_Naive,1571455.142857143,1571455.142857143,970.0256696428571,1.7689207877683926,0.6150428227015904,0.6251754760742188,12.799615221235317,1253.5769393448265,0.4988281842249676,0.38599636628003803,0.3118251468740881,Energy,7
|
| 55 |
+
saugeen/D/short,Seasonal_Naive,994.7777083333333,994.7777083333333,15.3725,3.413049305768806,0.47135218302408854,0.4242374928792318,58.71112805300772,31.540096834558597,1.0215912513857213,0.49791893773514423,0.5850141736335629,Nature,1
|
| 56 |
+
electricity/H/short,Seasonal_Naive,2887718.36009009,2887718.36009009,247.69792300112613,1.3577726051465433,0.3631860614314555,0.16452154722835458,18.287059022037134,1699.3287969342748,0.8022045515456071,0.11693110926995137,0.10566499518474065,Energy,1
|
| 57 |
+
electricity/H/medium,Seasonal_Naive,8353587.978378379,8353587.978378379,308.2015061936937,1.3924537851702354,0.3866618745283321,0.15667597765115268,20.69150990156008,2890.2574242406813,1.128162669425473,0.1203011991361897,0.12738701179892217,Energy,1
|
| 58 |
+
electricity/H/long,Seasonal_Naive,13526606.943327328,13526606.943327328,357.2064234234234,1.5240786163496851,0.36051114996741657,0.16771684918985819,24.628137038027603,3677.8535782882013,1.4505443840804357,0.14088210974822626,0.15341839756227935,Energy,1
|
| 59 |
+
ett1/D/short,Seasonal_Naive,57976.62857142857,57976.62857142857,154.97894345238095,1.7783697877928102,1.1623038155691965,0.5164825923859127,23.47582377746543,240.7833643992636,0.6324254934604606,0.4070573356817528,0.40843357791503654,Energy,7
|
| 60 |
+
bizitobs_l2c/H/short,Seasonal_Naive,281.8430679563492,281.8430679563492,12.53165302579365,1.2140641267600039,1.3605904339028776,1.138373051002047,7.486930567002142,16.788182389894065,0.9049260260934667,0.675488192208351,0.5211675771895117,Web/CloudOps,7
|
| 61 |
+
bizitobs_l2c/H/medium,Seasonal_Naive,456.3732886904762,456.3732886904762,15.667392113095238,1.5102861226255864,1.6912910179529201,1.4024095456148358,18.533653530822832,21.362895138311103,1.293555748999092,0.9486843898499615,0.9042051354742671,Web/CloudOps,7
|
| 62 |
+
bizitobs_l2c/H/long,Seasonal_Naive,309.27222222222224,309.27222222222224,13.635487971230159,1.4260541525613812,2.4383105011700468,0.9168538411458333,22.036198194504,17.586137217201003,1.0742120179443015,0.8328949597245134,0.9410651242377539,Web/CloudOps,7
|
| 63 |
+
hospital/M/short,Seasonal_Naive,3464.0771403737504,3464.0771403737504,20.005975662755322,0.9205278266364826,0.23307140471364352,0.21025354682958836,6.599875528051222,58.85641120875236,0.21366243998041978,0.07262633732682543,0.06248817651596496,Healthcare,1
|
| 64 |
+
m4_daily/D/short,Seasonal_Naive,497624.29605596676,497624.29605596676,180.83018393659805,3.278424296610481,0.04217648847004251,0.03045251709590828,32.19152086932198,705.4249046184624,0.10896865245350737,0.02793326594720746,0.024356147648337902,Econ/Fin,1
|
| 65 |
+
bitbrains_fast_storage/5T/short,Seasonal_Naive,5404398.959598898,5404398.959598898,346.43736615760264,1.1360241737534567,5.932190463200192,0.4761820977820179,31.06038796757978,2324.736320445589,7.299097101583847,1.087727650212227,1.210386425305063,Web/CloudOps,2
|
| 66 |
+
bitbrains_fast_storage/5T/medium,Seasonal_Naive,5191990.375993042,5191990.375993042,332.75293038959035,1.2202713595882653,5.768419487236171,0.4516151765437165,36.13464652226377,2278.593947150971,6.92299777327362,1.0109953109538277,1.1979128978088514,Web/CloudOps,2
|
| 67 |
+
bitbrains_fast_storage/5T/long,Seasonal_Naive,6473012.871295428,6473012.871295428,404.5292260549034,1.1366422318753115,6.462525817893773,0.5033018991430218,32.16149564789198,2544.2116404291974,6.723591850916995,1.0690499817466734,1.1773077032683998,Web/CloudOps,2
|
| 68 |
+
ett2/15T/short,Seasonal_Naive,14.617721121651785,14.617721121651785,2.534710984002976,1.067314800011479,0.15340707960692476,0.17031889997991706,8.528224043296179,3.8233128464267456,0.181506262086668,0.12033174753313121,0.09638438775638664,Energy,7
|
| 69 |
+
ett2/15T/medium,Seasonal_Naive,14.948670634920635,14.948670634920635,2.4787872023809525,1.0512391501712754,0.1650384327009311,0.1899925877490716,13.312453415228259,3.8663510749698657,0.20604484738813625,0.1320990569451144,0.12412877798161984,Energy,7
|
| 70 |
+
ett2/15T/long,Seasonal_Naive,13.674988839285714,13.674988839285714,2.362206101190476,1.0126577751121104,0.1609817984920091,0.1894470412273871,15.633896791936913,3.697970908388236,0.19707156352079597,0.12588623903557683,0.13282444765695306,Energy,7
|
| 71 |
+
jena_weather/10T/short,Seasonal_Naive,1986.203373015873,1986.203373015873,10.556470114087302,0.7428874675800381,0.8774809708937492,0.39362146904346257,20.65580527060839,44.566841631597285,0.2768159431081858,0.06556891005829443,0.15520092375681044,Nature,21
|
| 72 |
+
jena_weather/10T/medium,Seasonal_Naive,2572.8219336219336,2572.8219336219336,12.447531114718615,0.7160100391723763,0.8990536291309329,0.4023956803535614,27.512189615220795,50.72299215959104,0.311286135757249,0.07639028565640756,0.21183609008512827,Nature,21
|
| 73 |
+
jena_weather/10T/long,Seasonal_Naive,2044.5376984126983,2044.5376984126983,11.441627397486773,0.7614923703210493,0.7718160693153647,0.4043349656668781,32.10701219708694,45.21656442513847,0.2766879110680792,0.0700132799578619,0.2371949131202996,Nature,21
|
| 74 |
+
jena_weather/D/short,Seasonal_Naive,633.5864087301587,633.5864087301587,13.136656746031745,1.5734140297468562,1.7707135350571066,0.4854136849221913,31.00640730511845,25.17114238031637,0.15157141745411598,0.07910414448099358,0.2105908566035542,Nature,21
|
| 75 |
+
us_births/W/short,Seasonal_Naive,5288246.857142857,5288246.857142857,1720.3214285714287,1.5634204337929731,0.023174324205943515,0.023229679891041348,13.279190377688378,2299.6188504060533,0.031217100406456557,0.023353194707727513,0.01929679613291714,Healthcare,1
|
| 76 |
+
m_dense/H/short,Seasonal_Naive,145691.3111111111,145691.3111111111,180.18139756944444,1.4875444172240992,0.5399732053067908,0.3908195287297072,14.354127446705878,381.69531187992226,0.6770323708995337,0.31959690096169935,0.27479786933969136,Transport,1
|
| 77 |
+
m_dense/H/medium,Seasonal_Naive,158402.70222222223,158402.70222222223,189.1006423611111,1.569946511898904,0.5428334697930458,0.397995368109809,22.89829990230752,397.99836962256796,0.6924668813918432,0.32901122738058514,0.3773835994986822,Transport,1
|
| 78 |
+
m_dense/H/long,Seasonal_Naive,137584.05530864198,137584.05530864198,175.8238888888889,1.4779178744488217,0.4978060829089136,0.38833718102796383,27.64819460133164,370.92324719359664,0.6421229733186591,0.3043771431636812,0.4189507904607682,Transport,1
|
| 79 |
+
kdd_cup_2018/D/short,Seasonal_Naive,3183.23772462431,3183.23772462431,23.884613481510574,1.4970343738270926,0.5893064466306873,0.5721879846611051,24.580612052672397,56.42018898075679,1.263872161836989,0.5350407118596068,0.674506129145155,Nature,1
|
| 80 |
+
electricity/D/short,Seasonal_Naive,4108036968.189694,4108036968.189694,6964.321117117117,1.987041399842847,0.6052061423957089,0.13582226488305205,24.595303547998157,64093.9698270414,1.0532768308357283,0.11444692995228782,0.10405987098536745,Energy,1
|
| 81 |
+
restaurant/D/short,Seasonal_Naive,269.8283833006444,269.8283833006444,10.66129634818343,1.0060757450046092,0.9434227416570351,0.5561225187815214,20.74904381007386,16.426453765211907,0.7559834619860213,0.49065755991884735,0.6770164746085788,Sales,1
|
| 82 |
+
jena_weather/H/short,Seasonal_Naive,1941.9557226399331,1941.9557226399331,11.035693889672096,0.7229102185557323,1.1349936220434893,0.40077710759656054,17.382581140637207,44.06762669624872,0.2701317147149199,0.06764809310095782,0.15420788787522166,Nature,21
|
| 83 |
+
jena_weather/H/medium,Seasonal_Naive,1951.8111111111111,1951.8111111111111,12.670400080605159,0.8886133410507664,1.7017065286708326,0.45474790719102887,42.286346185217376,44.1793063674738,0.27034101099927343,0.077532425227887,0.3428055217231077,Nature,21
|
| 84 |
+
jena_weather/H/long,Seasonal_Naive,2714.1007936507935,2714.1007936507935,15.67983630952381,1.2677178473920059,1.8661278391921836,0.5020780815368567,51.127769997760254,52.097032484113655,0.31368185519406616,0.09440998667650671,0.41914340130808575,Nature,21
|
| 85 |
+
m4_monthly/M/short,Seasonal_Naive,2652970.8577407408,2652970.8577407408,700.2370131655092,1.259717038974429,0.19222692431343927,0.15988281532570167,10.588031340884587,1628.794295711015,0.33851937015163663,0.14553329004027185,0.12192063664829357,Econ/Fin,1
|
| 86 |
+
hierarchical_sales/D/short,Seasonal_Naive,58.96940884663084,58.96940884663084,3.4495245969408845,1.1348033996861053,1.219942090035255,1.1912773542022284,28.8672586949405,7.679154175209066,2.3558794594517027,1.058275945161004,1.7364572065613715,Sales,1
|
| 87 |
+
m4_hourly/H/short,Seasonal_Naive,3614355.993558776,3614355.993558776,353.8562298711755,1.1932101877276544,0.1561203125786282,0.1391227341312525,9.229226853352637,1901.1459685039379,0.25954838815818215,0.04830918594592099,0.03757255147212195,Econ/Fin,1
|
| 88 |
+
m4_yearly/A/short,Seasonal_Naive,4070174.893125562,4070174.893125562,1007.6444123429384,3.965954931783283,0.17520083696448324,0.16353434447586868,52.037766411775884,2017.4674453694568,0.323510256896443,0.1615804524853844,0.13714565740983498,Econ/Fin,1
|
| 89 |
+
hierarchical_sales/W/short,Seasonal_Naive,1045.4721927966102,1045.4721927966102,13.569120762711865,1.0250133777288337,0.9503837463030324,0.5798107884238987,15.297190661198302,32.33376242871544,1.4844588487218926,0.6229649713667368,0.8322267526416594,Sales,1
|
| 90 |
+
kdd_cup_2018/H/short,Seasonal_Naive,5615.976023835748,5615.976023835748,32.23957254610716,1.340425278558367,1.4218929148819215,0.6653979848821596,11.487358350395635,74.9398160114885,1.5686260624453057,0.6748326381021144,0.547536580422314,Nature,1
|
| 91 |
+
kdd_cup_2018/H/medium,Seasonal_Naive,5785.5976861948375,5785.5976861948375,31.3213270934715,1.4289997072062277,1.6378374272295781,0.6550327560864638,21.848163370875977,76.06311646386071,1.592138770600474,0.6556120959870002,0.7587892026846513,Nature,1
|
| 92 |
+
kdd_cup_2018/H/long,Seasonal_Naive,4863.8870949314805,4863.8870949314805,29.603438898808303,1.3355336658089667,1.3504101339311876,0.8098963463700346,25.694140506460055,69.74157364822995,1.6364191009705393,0.694616286015704,0.9362833347539716,Nature,1
|
| 93 |
+
sz_taxi/15T/short,Seasonal_Naive,31.23822759271978,31.23822759271978,3.8183243114578564,0.7644167735543712,1312692316403.7466,0.538321787329966,7.055930253561324,5.58911688844667,0.5226063327326282,0.3570296534180041,0.3087068505119876,Transport,1
|
| 94 |
+
sz_taxi/15T/medium,Seasonal_Naive,28.930445379273504,28.930445379273504,3.6711446647970085,0.7134840540400068,18917890105834.12,0.5172677325387286,10.816848306969383,5.378702945810774,0.5005780877467839,0.34166128798341444,0.379172158612487,Transport,1
|
| 95 |
+
sz_taxi/15T/long,Seasonal_Naive,28.48211582977208,28.48211582977208,3.6789685719373217,0.6909431344941248,3284113398767.079,0.5217545990251068,12.685376303466118,5.336863857151696,0.4939706119705816,0.3405187776085254,0.4279704063043717,Transport,1
|
| 96 |
+
solar/10T/short,Seasonal_Naive,37.917036857512166,37.917036857512166,2.5271684706936206,1.1057028063509833,4.40491207343912,0.925039670208932,18.785745549933484,6.157681126650857,1.7912687729072838,0.7351530344494263,0.8595386998005952,Energy,1
|
| 97 |
+
solar/10T/medium,Seasonal_Naive,29.228987641008626,29.228987641008626,2.118083017100752,0.9270005404761702,3.219568147534267,0.6587355148950821,15.807425762449464,5.406383970918883,1.1996980737201242,0.4700110331163171,0.6550635536405177,Energy,1
|
| 98 |
+
solar/10T/long,Seasonal_Naive,26.90307906275345,26.90307906275345,1.995840144718167,0.8709317964102903,2.310040346896705,0.6597688060895561,18.202191205836492,5.18681781661487,1.1210476934837867,0.4313689182665159,0.6738494837655387,Energy,1
|
tinycast/scale.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Seasonal discretization scale factor.
|
| 2 |
+
|
| 3 |
+
Maps a pandas frequency string to the model's seasonal scale factor
|
| 4 |
+
``s = base_seasonality / (samples per natural period)``, i.e. how many
|
| 5 |
+
context samples fall in one canonical seasonal cycle at that sampling rate.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from typing import Optional
|
| 10 |
+
|
| 11 |
+
# Cycles per canonical day: the unit every scale factor is expressed against.
|
| 12 |
+
# ``tinycast.losses`` reads this so the seasonal lag the committing term folds
|
| 13 |
+
# at and the scale factor the model is conditioned on cannot drift apart.
|
| 14 |
+
BASE_SEASONALITY = 24.0
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def seasonal_scale_factor(freq: str, domain: Optional[str] = None) -> float:
|
| 18 |
+
"""Seasonal scale factor for a pandas frequency string."""
|
| 19 |
+
has_weekly = domain in ["Transport", "Healthcare", "Sales"]
|
| 20 |
+
|
| 21 |
+
if freq == "4S":
|
| 22 |
+
factor = BASE_SEASONALITY / (3600.0 / 4)
|
| 23 |
+
elif freq == "10S":
|
| 24 |
+
factor = BASE_SEASONALITY / 360
|
| 25 |
+
elif freq == "T":
|
| 26 |
+
factor = BASE_SEASONALITY / (24.0 * 60)
|
| 27 |
+
elif freq[-1] == "T":
|
| 28 |
+
n_min = int(freq[:-1])
|
| 29 |
+
factor = BASE_SEASONALITY / (24 * 60 / n_min)
|
| 30 |
+
elif freq == "H":
|
| 31 |
+
factor = BASE_SEASONALITY / 24
|
| 32 |
+
elif freq == "6H":
|
| 33 |
+
factor = BASE_SEASONALITY / 4
|
| 34 |
+
elif freq == "D":
|
| 35 |
+
factor = BASE_SEASONALITY / 7 if has_weekly else BASE_SEASONALITY / 365
|
| 36 |
+
elif freq[-1] == "D" and "WED" not in freq:
|
| 37 |
+
n = int(freq[:-1])
|
| 38 |
+
factor = BASE_SEASONALITY / 7 if has_weekly else BASE_SEASONALITY / 365
|
| 39 |
+
factor *= n
|
| 40 |
+
elif freq == "W" or "W-" in freq:
|
| 41 |
+
factor = BASE_SEASONALITY / (365.0 / 7)
|
| 42 |
+
elif freq == "M" or "M-" in freq or freq == "MS":
|
| 43 |
+
factor = BASE_SEASONALITY / 12
|
| 44 |
+
elif "Q" in freq:
|
| 45 |
+
factor = BASE_SEASONALITY / 4.0
|
| 46 |
+
elif "A" in freq:
|
| 47 |
+
factor = BASE_SEASONALITY / 4.0
|
| 48 |
+
else:
|
| 49 |
+
raise NotImplementedError(
|
| 50 |
+
f"{freq} not implemented. Add {freq} option to seasonal_scale_factor."
|
| 51 |
+
)
|
| 52 |
+
return factor
|
tinycast/synth.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Synthetic series generators: Gaussian process, spike trains, TSI.
|
| 2 |
+
|
| 3 |
+
These are the three families in TinyCast's synthetic pretraining shards. They
|
| 4 |
+
follow the recipe published by Reverso (arXiv 2602.17634, Appendix A),
|
| 5 |
+
implemented from its Algorithms 1 to 3 and Table 8:
|
| 6 |
+
|
| 7 |
+
* :func:`generate_gp` is Algorithm 1: a Gaussian process over a 38-entry kernel
|
| 8 |
+
bank (Constant; Linear with sigma in {0, 1, 10}; RBF with l in {0.1, 1, 10};
|
| 9 |
+
RationalQuadratic with alpha in {0.1, 1, 10}; Matern with nu in
|
| 10 |
+
{0.5, 1.5, 2.5} crossed with l in {0.1, 1, 10}; Periodic over a 19-period set
|
| 11 |
+
normalized by series length), with J ~ U{1,5} kernels composed by random sum
|
| 12 |
+
or product, and a mean function that is a linear trend with probability 1/2.
|
| 13 |
+
* :func:`generate_spikes` is Algorithm 2: trapezoid pulse trains (a quarter of
|
| 14 |
+
the pulse ramping up, half flat, the remainder ramping down) tiled at a fixed
|
| 15 |
+
period over a baseline, in an upward or downward variant, plus white noise.
|
| 16 |
+
* :func:`generate_tsi` is Algorithm 3, the trend-seasonal-impulse process: an
|
| 17 |
+
optional trend, K ~ U{1,3} seasonal components (sine, sawtooth or square),
|
| 18 |
+
noise, sparse outliers and level shifts.
|
| 19 |
+
|
| 20 |
+
All three return float32 with no normalization applied. The corpus builder
|
| 21 |
+
z-normalizes per series on write, which is why the absolute scale of any
|
| 22 |
+
parameter below does not matter and the shape does.
|
| 23 |
+
|
| 24 |
+
DOCUMENTED ASSUMPTIONS. The source paper leaves the following symbolic, and
|
| 25 |
+
neither Kairos (arXiv 2509.25826) nor Chronos-2 (arXiv 2510.15821), which the
|
| 26 |
+
respective algorithms defer to, publishes them. We record the choices we made
|
| 27 |
+
rather than presenting the families as an exact replication:
|
| 28 |
+
|
| 29 |
+
A1 Algorithm 1 trend units. The slope m ~ U[-0.01, 0.01] is applied per
|
| 30 |
+
index step (mu_t = m*t + c with t = 0..L-1), not per unit of the [0,1]
|
| 31 |
+
kernel grid. On the [0,1] grid the sampled trend would be at most 1% of
|
| 32 |
+
the GP's unit scale, which makes the probability-1/2 branch pointless.
|
| 33 |
+
Index units give trend-dominated series about half the time, which is a
|
| 34 |
+
realistic class, and the per-series z-normalization on write removes the
|
| 35 |
+
magnitude difference.
|
| 36 |
+
A2 Algorithm 2 numeric ranges: baseline U[-1, 1], period U{16, L//8}, pulse
|
| 37 |
+
width U{4, p}, amplitude U[0.5, 3], noise sigma U[0.01, 0.3]. Under
|
| 38 |
+
z-normalization the levels are immaterial; shape, sparsity and period are
|
| 39 |
+
what the model sees.
|
| 40 |
+
A3 Algorithm 3 probabilities and ranges: P_trend = 0.5, P_seasonal = 0.8,
|
| 41 |
+
P_noise = 0.8, P_outlier = 0.2, P_shift = 0.2; trend types linear,
|
| 42 |
+
exponential, quadratic and piecewise linear; periods taken from the
|
| 43 |
+
Algorithm 1 period set in index steps, filtered to at most L/2; amplitude
|
| 44 |
+
U[0.5, 2]; noise sigma U[0.05, 0.5], normal or Laplace; U{1, L//100}
|
| 45 |
+
outliers at +/- U[3, 8] standard deviations; U{1,3} level shifts of
|
| 46 |
+
magnitude +/- U[0.5, 3].
|
| 47 |
+
A4 The family mix. The paper gives a total series count but not the
|
| 48 |
+
proportions, and describes spikes and TSI as additions to a GP majority.
|
| 49 |
+
:mod:`tinycast.corpus` uses 70/15/15.
|
| 50 |
+
|
| 51 |
+
DEVICE. :func:`generate_gp` draws from numpy on the CPU path and from torch on
|
| 52 |
+
the CUDA path, so the same seed produces different series on the two branches.
|
| 53 |
+
The published shards came from the CUDA branch, and :mod:`tinycast.corpus`
|
| 54 |
+
refuses anything else for that reason. The CPU branch is kept here because it
|
| 55 |
+
is useful at small scale and because spikes and TSI are numpy either way.
|
| 56 |
+
"""
|
| 57 |
+
from __future__ import annotations
|
| 58 |
+
|
| 59 |
+
import math
|
| 60 |
+
|
| 61 |
+
import numpy as np
|
| 62 |
+
|
| 63 |
+
# Algorithm 1 / Table 8 period set. Table 8 specifies the periodic kernel's p
|
| 64 |
+
# as a fraction of the series length, so these enter the bank divided by L.
|
| 65 |
+
PERIODS = (24, 48, 96, 168, 336, 672, 7, 14, 30, 60,
|
| 66 |
+
365, 730, 4, 26, 52, 6, 12, 40, 10)
|
| 67 |
+
|
| 68 |
+
# Jitter added to the diagonal before factorization, and the larger retry value
|
| 69 |
+
# the CPU path falls back to. The CUDA path does not retry: see generate_gp.
|
| 70 |
+
GP_JITTER = 1e-6
|
| 71 |
+
GP_JITTER_RETRY = 1e-4
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def kernel_bank(length: int) -> list[tuple[str, tuple]]:
|
| 75 |
+
"""The 38 Table-8 kernels as (tag, params) pairs, for a series length.
|
| 76 |
+
|
| 77 |
+
Only the periodic entries depend on ``length``, and they depend on it
|
| 78 |
+
because Table 8 normalizes the period by the series length.
|
| 79 |
+
"""
|
| 80 |
+
bank: list[tuple[str, tuple]] = [("const", (1.0,))]
|
| 81 |
+
bank += [("linear", (s,)) for s in (0.0, 1.0, 10.0)]
|
| 82 |
+
bank += [("rbf", (l,)) for l in (0.1, 1.0, 10.0)]
|
| 83 |
+
bank += [("rq", (a,)) for a in (0.1, 1.0, 10.0)]
|
| 84 |
+
bank += [("matern", (nu, l)) for nu in (0.5, 1.5, 2.5) for l in (0.1, 1.0, 10.0)]
|
| 85 |
+
bank += [("periodic", (p / length,)) for p in PERIODS]
|
| 86 |
+
return bank
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _matern(d: np.ndarray, nu: float, l: float) -> np.ndarray:
|
| 90 |
+
"""Matern closed forms for nu in {0.5, 1.5, 2.5}, so no Bessel call."""
|
| 91 |
+
if nu == 0.5:
|
| 92 |
+
return np.exp(-d / l)
|
| 93 |
+
if nu == 1.5:
|
| 94 |
+
a = math.sqrt(3.0) * d / l
|
| 95 |
+
return (1.0 + a) * np.exp(-a)
|
| 96 |
+
if nu == 2.5:
|
| 97 |
+
a = math.sqrt(5.0) * d / l
|
| 98 |
+
return (1.0 + a + a * a / 3.0) * np.exp(-a)
|
| 99 |
+
raise ValueError(f"unsupported Matern nu={nu}")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _dense_kernel(tag: str, params: tuple, t: np.ndarray) -> np.ndarray:
|
| 103 |
+
d = np.abs(t[:, None] - t[None, :])
|
| 104 |
+
if tag == "const":
|
| 105 |
+
return np.full_like(d, params[0])
|
| 106 |
+
if tag == "linear": # sigma^2 + x.x'
|
| 107 |
+
return params[0] ** 2 + np.outer(t, t)
|
| 108 |
+
if tag == "rbf":
|
| 109 |
+
return np.exp(-(d ** 2) / (2.0 * params[0] ** 2))
|
| 110 |
+
if tag == "rq": # Table 8's form carries no lengthscale (l=1)
|
| 111 |
+
return (1.0 + d ** 2 / (2.0 * params[0])) ** (-params[0])
|
| 112 |
+
if tag == "matern":
|
| 113 |
+
return _matern(d, *params)
|
| 114 |
+
if tag == "periodic": # Table 8: exp(-2 sin^2(pi d / p)), l=1
|
| 115 |
+
return np.exp(-2.0 * np.sin(np.pi * d / params[0]) ** 2)
|
| 116 |
+
raise ValueError(tag)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def generate_gp(
|
| 120 |
+
num_series: int,
|
| 121 |
+
length: int,
|
| 122 |
+
seed: int | None = None,
|
| 123 |
+
max_kernels: int = 5,
|
| 124 |
+
device: str | None = None,
|
| 125 |
+
batch: int = 32,
|
| 126 |
+
) -> np.ndarray:
|
| 127 |
+
"""Algorithm 1: GP samples with composed Table-8 kernels and a trend mean.
|
| 128 |
+
|
| 129 |
+
Sampling is by dense jittered Cholesky. A circulant or FFT sampler is not
|
| 130 |
+
an option here because the composed bank is not stationary: the linear and
|
| 131 |
+
periodic entries break both stationarity and circularity.
|
| 132 |
+
|
| 133 |
+
``device='cuda'`` batches the covariance construction and the factorization
|
| 134 |
+
on the GPU, which is what production volume at length 4096 needs. Both
|
| 135 |
+
paths work in float64; float32 loses the high-frequency modes of the
|
| 136 |
+
periodic entries outright. The CUDA path uses ``cholesky_ex`` and writes
|
| 137 |
+
NaN for any row whose covariance failed to factorize, leaving the caller to
|
| 138 |
+
drop it, while the CPU path retries once at a larger jitter and raises if
|
| 139 |
+
that also fails.
|
| 140 |
+
|
| 141 |
+
``device=None`` selects the numpy path; any device string selects the torch
|
| 142 |
+
path on that device, so ``'cpu'`` runs the batched code on the CPU and is a
|
| 143 |
+
third result rather than a cheap stand-in for either. The paths do not agree
|
| 144 |
+
row by row. ``batch`` is part of the data on the torch path rather than a
|
| 145 |
+
memory knob: see :mod:`tinycast.corpus`.
|
| 146 |
+
|
| 147 |
+
Returns float32 of shape ``(num_series, length)``, unnormalized.
|
| 148 |
+
"""
|
| 149 |
+
rng = np.random.default_rng(seed)
|
| 150 |
+
bank = kernel_bank(length)
|
| 151 |
+
t = np.linspace(0.0, 1.0, length)
|
| 152 |
+
idx = np.arange(length, dtype=np.float64)
|
| 153 |
+
out = np.empty((num_series, length), dtype=np.float32)
|
| 154 |
+
|
| 155 |
+
def compose_indices():
|
| 156 |
+
n_k = int(rng.integers(1, max_kernels + 1))
|
| 157 |
+
picks = [int(rng.integers(0, len(bank))) for _ in range(n_k)]
|
| 158 |
+
ops = [int(rng.integers(0, 2)) for _ in range(n_k - 1)]
|
| 159 |
+
return picks, ops
|
| 160 |
+
|
| 161 |
+
def mean_fn():
|
| 162 |
+
# ASSUMPTION A1: the trend is in index units.
|
| 163 |
+
if rng.uniform() < 0.5:
|
| 164 |
+
m = rng.uniform(-0.01, 0.01)
|
| 165 |
+
c = rng.uniform(-0.1, 0.1)
|
| 166 |
+
return m * idx + c
|
| 167 |
+
return np.zeros(length)
|
| 168 |
+
|
| 169 |
+
if device is None:
|
| 170 |
+
for i in range(num_series):
|
| 171 |
+
picks, ops = compose_indices()
|
| 172 |
+
K = _dense_kernel(*bank[picks[0]], t)
|
| 173 |
+
for op, pi in zip(ops, picks[1:]):
|
| 174 |
+
Kn = _dense_kernel(*bank[pi], t)
|
| 175 |
+
K = K + Kn if op == 0 else K * Kn
|
| 176 |
+
K = K + GP_JITTER * np.eye(length)
|
| 177 |
+
try:
|
| 178 |
+
Lc = np.linalg.cholesky(K)
|
| 179 |
+
except np.linalg.LinAlgError:
|
| 180 |
+
K += (GP_JITTER_RETRY - GP_JITTER) * np.eye(length)
|
| 181 |
+
Lc = np.linalg.cholesky(K)
|
| 182 |
+
out[i] = (Lc @ rng.standard_normal(length) + mean_fn()).astype(np.float32)
|
| 183 |
+
return out
|
| 184 |
+
|
| 185 |
+
import torch
|
| 186 |
+
|
| 187 |
+
dev = torch.device(device)
|
| 188 |
+
g = torch.Generator(device=dev)
|
| 189 |
+
g.manual_seed(int(seed) if seed is not None else 0)
|
| 190 |
+
tt = torch.linspace(0.0, 1.0, length, device=dev, dtype=torch.float64)
|
| 191 |
+
dd = (tt[:, None] - tt[None, :]).abs()
|
| 192 |
+
d2 = dd * dd
|
| 193 |
+
outer = tt[:, None] * tt[None, :]
|
| 194 |
+
eye = torch.eye(length, device=dev, dtype=torch.float64)
|
| 195 |
+
|
| 196 |
+
def _k(tag: str, params: tuple) -> "torch.Tensor":
|
| 197 |
+
if tag == "const":
|
| 198 |
+
return torch.full_like(dd, params[0])
|
| 199 |
+
if tag == "linear":
|
| 200 |
+
return params[0] ** 2 + outer
|
| 201 |
+
if tag == "rbf":
|
| 202 |
+
return torch.exp(-d2 / (2.0 * params[0] ** 2))
|
| 203 |
+
if tag == "rq":
|
| 204 |
+
return (1.0 + d2 / (2.0 * params[0])).pow(-params[0])
|
| 205 |
+
if tag == "matern":
|
| 206 |
+
nu, l = params
|
| 207 |
+
if nu == 0.5:
|
| 208 |
+
return torch.exp(-dd / l)
|
| 209 |
+
if nu == 1.5:
|
| 210 |
+
a = math.sqrt(3.0) * dd / l
|
| 211 |
+
return (1.0 + a) * torch.exp(-a)
|
| 212 |
+
a = math.sqrt(5.0) * dd / l
|
| 213 |
+
return (1.0 + a + a * a / 3.0) * torch.exp(-a)
|
| 214 |
+
return torch.exp(-2.0 * torch.sin(math.pi * dd / params[0]) ** 2)
|
| 215 |
+
|
| 216 |
+
Ks = torch.empty((batch, length, length), device=dev, dtype=torch.float64)
|
| 217 |
+
done = 0
|
| 218 |
+
while done < num_series:
|
| 219 |
+
b = min(batch, num_series - done)
|
| 220 |
+
means = np.empty((b, length), dtype=np.float64)
|
| 221 |
+
for i in range(b):
|
| 222 |
+
picks, ops = compose_indices()
|
| 223 |
+
K = _k(*bank[picks[0]])
|
| 224 |
+
for op, pi in zip(ops, picks[1:]):
|
| 225 |
+
Kn = _k(*bank[pi])
|
| 226 |
+
K = K + Kn if op == 0 else K * Kn
|
| 227 |
+
Ks[i] = K
|
| 228 |
+
means[i] = mean_fn()
|
| 229 |
+
Kb = Ks[:b] + GP_JITTER * eye
|
| 230 |
+
Lc, info = torch.linalg.cholesky_ex(Kb)
|
| 231 |
+
z = torch.randn(b, length, 1, generator=g, device=dev, dtype=torch.float64)
|
| 232 |
+
s = torch.matmul(Lc, z).squeeze(-1)
|
| 233 |
+
bad = info > 0
|
| 234 |
+
if bool(bad.any()):
|
| 235 |
+
s[bad] = float("nan")
|
| 236 |
+
out[done:done + b] = (s.cpu().numpy() + means).astype(np.float32)
|
| 237 |
+
done += b
|
| 238 |
+
return out
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def generate_spikes(
|
| 242 |
+
num_series: int, length: int, seed: int | None = None,
|
| 243 |
+
) -> np.ndarray:
|
| 244 |
+
"""Algorithm 2: trapezoid pulse trains over a baseline, plus white noise.
|
| 245 |
+
|
| 246 |
+
The numeric ranges are ASSUMPTION A2. Levels wash out under the per-series
|
| 247 |
+
z-normalization the corpus applies on write, so what this family
|
| 248 |
+
contributes is the pulse shape and its spacing.
|
| 249 |
+
"""
|
| 250 |
+
rng = np.random.default_rng(seed)
|
| 251 |
+
out = np.empty((num_series, length), dtype=np.float32)
|
| 252 |
+
for i in range(num_series):
|
| 253 |
+
b = rng.uniform(-1.0, 1.0)
|
| 254 |
+
p = int(rng.integers(16, max(17, length // 8) + 1))
|
| 255 |
+
w = int(rng.integers(4, p + 1))
|
| 256 |
+
a = rng.uniform(0.5, 3.0)
|
| 257 |
+
sigma = rng.uniform(0.01, 0.3)
|
| 258 |
+
sign = -1.0 if rng.integers(0, 2) == 0 else 1.0 # downward or upward
|
| 259 |
+
up = w // 4
|
| 260 |
+
flat = w // 2
|
| 261 |
+
down = w - up - flat
|
| 262 |
+
pulse = np.concatenate([
|
| 263 |
+
np.linspace(0.0, a, max(up, 1)),
|
| 264 |
+
np.full(max(flat, 1), a),
|
| 265 |
+
np.linspace(a, 0.0, max(down, 1)),
|
| 266 |
+
])[:w]
|
| 267 |
+
x = np.full(length, b)
|
| 268 |
+
for start in range(0, length, p):
|
| 269 |
+
seg = min(w, length - start)
|
| 270 |
+
x[start:start + seg] += sign * pulse[:seg]
|
| 271 |
+
x += rng.normal(0.0, sigma, length)
|
| 272 |
+
out[i] = x.astype(np.float32)
|
| 273 |
+
return out
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def generate_tsi(
|
| 277 |
+
num_series: int, length: int, seed: int | None = None,
|
| 278 |
+
p_trend: float = 0.5, p_seas: float = 0.8, p_noise: float = 0.8,
|
| 279 |
+
p_out: float = 0.2, p_shift: float = 0.2,
|
| 280 |
+
) -> np.ndarray:
|
| 281 |
+
"""Algorithm 3: trend, seasonal components, noise, outliers, level shifts.
|
| 282 |
+
|
| 283 |
+
The probabilities and ranges are ASSUMPTION A3: the source paper states the
|
| 284 |
+
structure and defers the numbers, and the work it defers to does not
|
| 285 |
+
publish them either.
|
| 286 |
+
"""
|
| 287 |
+
rng = np.random.default_rng(seed)
|
| 288 |
+
t = np.arange(length, dtype=np.float64)
|
| 289 |
+
tn = t / max(length - 1, 1)
|
| 290 |
+
periods = [p for p in PERIODS if p <= length // 2]
|
| 291 |
+
out = np.empty((num_series, length), dtype=np.float32)
|
| 292 |
+
for i in range(num_series):
|
| 293 |
+
x = np.zeros(length)
|
| 294 |
+
if rng.uniform() < p_trend:
|
| 295 |
+
kind = rng.integers(0, 4)
|
| 296 |
+
if kind == 0: # linear
|
| 297 |
+
x += rng.uniform(-2.0, 2.0) * tn
|
| 298 |
+
elif kind == 1: # exponential
|
| 299 |
+
x += np.exp(rng.uniform(0.5, 2.0) * tn) - 1.0
|
| 300 |
+
elif kind == 2: # quadratic
|
| 301 |
+
x += rng.uniform(-2.0, 2.0) * tn ** 2
|
| 302 |
+
else: # piecewise linear
|
| 303 |
+
cp = int(rng.integers(1, length - 1))
|
| 304 |
+
s1, s2 = rng.uniform(-2.0, 2.0, 2)
|
| 305 |
+
x[:cp] += s1 * tn[:cp]
|
| 306 |
+
x[cp:] += s1 * tn[cp] + s2 * (tn[cp:] - tn[cp])
|
| 307 |
+
if rng.uniform() < p_seas and periods:
|
| 308 |
+
n_comp = int(rng.integers(1, 4))
|
| 309 |
+
chosen = rng.choice(periods, size=min(n_comp, len(periods)), replace=False)
|
| 310 |
+
for p in chosen:
|
| 311 |
+
amp = rng.uniform(0.5, 2.0)
|
| 312 |
+
phi = rng.uniform(0.0, 2.0 * np.pi)
|
| 313 |
+
arg = 2.0 * np.pi * t / p + phi
|
| 314 |
+
wave = rng.integers(0, 3)
|
| 315 |
+
if wave == 0:
|
| 316 |
+
x += amp * np.sin(arg)
|
| 317 |
+
elif wave == 1: # sawtooth
|
| 318 |
+
x += amp * (2.0 * ((arg / (2.0 * np.pi)) % 1.0) - 1.0)
|
| 319 |
+
else: # square
|
| 320 |
+
x += amp * np.sign(np.sin(arg))
|
| 321 |
+
if rng.uniform() < p_noise:
|
| 322 |
+
sigma = rng.uniform(0.05, 0.5)
|
| 323 |
+
if rng.integers(0, 2) == 0:
|
| 324 |
+
x += rng.normal(0.0, sigma, length)
|
| 325 |
+
else:
|
| 326 |
+
x += rng.laplace(0.0, sigma / math.sqrt(2.0), length)
|
| 327 |
+
if rng.uniform() < p_out:
|
| 328 |
+
n = int(rng.integers(1, max(2, length // 100)))
|
| 329 |
+
pos = rng.integers(0, length, n)
|
| 330 |
+
mag = rng.uniform(3.0, 8.0, n) * max(x.std(), 0.1)
|
| 331 |
+
x[pos] += mag * rng.choice([-1.0, 1.0], n)
|
| 332 |
+
if rng.uniform() < p_shift:
|
| 333 |
+
n = int(rng.integers(1, 4))
|
| 334 |
+
for _ in range(n):
|
| 335 |
+
pos = int(rng.integers(1, length))
|
| 336 |
+
x[pos:] += rng.uniform(0.5, 3.0) * (1.0 if rng.integers(0, 2) else -1.0)
|
| 337 |
+
out[i] = x.astype(np.float32)
|
| 338 |
+
return out
|
tinycast/train.py
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training entry point for TinyCast.
|
| 2 |
+
|
| 3 |
+
``train`` runs the shipped recipe: a four-block autoregressive rollout under
|
| 4 |
+
scheduled sampling, the nine-quantile pinball loss plus the gated committing
|
| 5 |
+
term, AdamW, and a warmup-stable-decay learning-rate schedule. It runs
|
| 6 |
+
single-process, and CPU is a first-class device.
|
| 7 |
+
|
| 8 |
+
What a short run does and does not reproduce. The released 146,505-parameter
|
| 9 |
+
checkpoint is 36,621 optimizer steps at an effective batch of 4096, in
|
| 10 |
+
bf16-mixed across eight GPUs, over GIFT-Eval-Pretrain plus Chronos KernelSynth
|
| 11 |
+
plus four synthetic shards, with the final weights the uniform mean of the last
|
| 12 |
+
eight periodic checkpoints. A short run on a small corpus reproduces the
|
| 13 |
+
mechanics: the rollout, the objective, the schedule shape, the optimizer. It
|
| 14 |
+
does not reproduce the checkpoint or its numbers.
|
| 15 |
+
|
| 16 |
+
``max_steps`` reshapes the run rather than truncating it. Warmup, stable and
|
| 17 |
+
decay are fractions of ``max_steps``, and the scheduled-sampling feedback
|
| 18 |
+
probability ramps over its first half, so a 200-step run warms up, holds and
|
| 19 |
+
decays inside 200 steps and reaches full feedback by step 100. A 200-step run
|
| 20 |
+
is therefore not a prefix of a 36,621-step run, and their step-100 states are
|
| 21 |
+
not comparable.
|
| 22 |
+
|
| 23 |
+
Two further departures from the released run, both deliberate. This entry point
|
| 24 |
+
trains in the model's own precision instead of bf16-mixed, and it writes
|
| 25 |
+
periodic checkpoints but does not average them.
|
| 26 |
+
"""
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import collections.abc
|
| 30 |
+
import json
|
| 31 |
+
import math
|
| 32 |
+
import os
|
| 33 |
+
import random
|
| 34 |
+
from dataclasses import dataclass, field
|
| 35 |
+
from pathlib import Path
|
| 36 |
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
| 37 |
+
|
| 38 |
+
import torch
|
| 39 |
+
|
| 40 |
+
from .config import TinyCastConfig
|
| 41 |
+
from .losses import (
|
| 42 |
+
COMMIT_WEIGHT,
|
| 43 |
+
committing_loss,
|
| 44 |
+
pinball_loss,
|
| 45 |
+
seasonal_copy_baseline,
|
| 46 |
+
)
|
| 47 |
+
from .model import TinyCastForPrediction
|
| 48 |
+
|
| 49 |
+
# --- the shipped recipe ----------------------------------------------------
|
| 50 |
+
# Every value below can be overridden by an attribute of the same name on the
|
| 51 |
+
# config object passed to ``train``; the defaults are what the released
|
| 52 |
+
# checkpoint was trained with.
|
| 53 |
+
AR_CHUNKS = 4 # K: blocks rolled out per training window
|
| 54 |
+
SCHEDULED_SAMPLING_MAX = 0.5 # peak probability of feeding our own median
|
| 55 |
+
LEARNING_RATE = 3e-3
|
| 56 |
+
MIN_LEARNING_RATE = 1e-5
|
| 57 |
+
WARMUP_FRACTION = 0.05
|
| 58 |
+
DECAY_FRACTION = 0.35
|
| 59 |
+
WEIGHT_DECAY = 0.01
|
| 60 |
+
GRAD_CLIP = 1.0
|
| 61 |
+
ADAM_BETAS = (0.9, 0.95)
|
| 62 |
+
COMMIT_GATED = True
|
| 63 |
+
|
| 64 |
+
# Guards carried over from the released run: predictions are optimized inside a
|
| 65 |
+
# fixed normalized band, and a window whose context is flat is not scored.
|
| 66 |
+
_PRED_CLAMP = 5.0
|
| 67 |
+
_TARGET_CLAMP = 10.0
|
| 68 |
+
_MIN_RANGE = 1e-4
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@dataclass
|
| 72 |
+
class TrainResult:
|
| 73 |
+
"""What a finished run leaves behind.
|
| 74 |
+
|
| 75 |
+
Attributes:
|
| 76 |
+
checkpoint_path: the final ``model.safetensors``, next to a
|
| 77 |
+
``config.json``, so ``tinycast.load_checkpoint`` reads it directly.
|
| 78 |
+
steps: optimizer steps actually run.
|
| 79 |
+
losses: one training loss per optimizer step, in order.
|
| 80 |
+
learning_rates: the learning rate each of those steps used.
|
| 81 |
+
checkpoints: the periodic checkpoints written along the way, oldest
|
| 82 |
+
first. The released weights are the uniform mean of the last eight
|
| 83 |
+
of these; ``train`` writes them but does not average them.
|
| 84 |
+
window_width: the sample width the run required, ``L + K * p``.
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
checkpoint_path: str
|
| 88 |
+
steps: int
|
| 89 |
+
losses: List[float] = field(default_factory=list)
|
| 90 |
+
learning_rates: List[float] = field(default_factory=list)
|
| 91 |
+
checkpoints: List[str] = field(default_factory=list)
|
| 92 |
+
window_width: int = 0
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def training_window_width(
|
| 96 |
+
config: TinyCastConfig, chunks: Optional[int] = None
|
| 97 |
+
) -> int:
|
| 98 |
+
"""Sample width the rollout consumes: ``L + K * p``.
|
| 99 |
+
|
| 100 |
+
``L`` is the encoder context, ``p`` the horizon unit and ``K`` the number of
|
| 101 |
+
autoregressive blocks. Each block re-normalizes a context that has slid
|
| 102 |
+
forward by ``p``, so the last block still needs a full ``L`` of history
|
| 103 |
+
behind it. Call this instead of hard-coding the width.
|
| 104 |
+
"""
|
| 105 |
+
k = int(_setting(config, "ar_chunks", AR_CHUNKS) if chunks is None else chunks)
|
| 106 |
+
if k < 1:
|
| 107 |
+
raise ValueError(f"ar_chunks must be at least 1, got {k}.")
|
| 108 |
+
return int(config.seq_len) + k * int(config.output_token_len)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _setting(config: Any, name: str, default: Any) -> Any:
|
| 112 |
+
value = getattr(config, name, None)
|
| 113 |
+
return default if value is None else value
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
# --- reproducibility -------------------------------------------------------
|
| 117 |
+
|
| 118 |
+
def _seed_everything(seed: int) -> None:
|
| 119 |
+
random.seed(seed)
|
| 120 |
+
try:
|
| 121 |
+
import numpy as np
|
| 122 |
+
|
| 123 |
+
np.random.seed(seed % (2 ** 32))
|
| 124 |
+
except ImportError:
|
| 125 |
+
pass
|
| 126 |
+
torch.manual_seed(seed)
|
| 127 |
+
if torch.cuda.is_available():
|
| 128 |
+
torch.cuda.manual_seed_all(seed)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
class _Determinism:
|
| 132 |
+
"""Turn deterministic kernels on, then put the process back as it was.
|
| 133 |
+
|
| 134 |
+
The flags are process-global, so a library function has no business leaving
|
| 135 |
+
them changed. Entered only when the caller asks for ``deterministic=True``.
|
| 136 |
+
"""
|
| 137 |
+
|
| 138 |
+
def __init__(self, enabled: bool, device: torch.device) -> None:
|
| 139 |
+
self.enabled = bool(enabled)
|
| 140 |
+
self.device = device
|
| 141 |
+
self._saved: Dict[str, Any] = {}
|
| 142 |
+
|
| 143 |
+
def __enter__(self) -> "_Determinism":
|
| 144 |
+
if not self.enabled:
|
| 145 |
+
return self
|
| 146 |
+
self._saved["algorithms"] = torch.are_deterministic_algorithms_enabled()
|
| 147 |
+
self._saved["cudnn_deterministic"] = torch.backends.cudnn.deterministic
|
| 148 |
+
self._saved["cudnn_benchmark"] = torch.backends.cudnn.benchmark
|
| 149 |
+
self._saved["cublas"] = os.environ.get("CUBLAS_WORKSPACE_CONFIG")
|
| 150 |
+
if self.device.type == "cuda" and self._saved["cublas"] is None:
|
| 151 |
+
# cuBLAS needs this set before its handle is created for the
|
| 152 |
+
# deterministic reduction path to be available.
|
| 153 |
+
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
|
| 154 |
+
torch.backends.cudnn.deterministic = True
|
| 155 |
+
torch.backends.cudnn.benchmark = False
|
| 156 |
+
torch.use_deterministic_algorithms(True, warn_only=True)
|
| 157 |
+
return self
|
| 158 |
+
|
| 159 |
+
def __exit__(self, *exc: Any) -> None:
|
| 160 |
+
if not self.enabled:
|
| 161 |
+
return
|
| 162 |
+
torch.use_deterministic_algorithms(self._saved["algorithms"])
|
| 163 |
+
torch.backends.cudnn.deterministic = self._saved["cudnn_deterministic"]
|
| 164 |
+
torch.backends.cudnn.benchmark = self._saved["cudnn_benchmark"]
|
| 165 |
+
if self._saved["cublas"] is None:
|
| 166 |
+
os.environ.pop("CUBLAS_WORKSPACE_CONFIG", None)
|
| 167 |
+
else:
|
| 168 |
+
os.environ["CUBLAS_WORKSPACE_CONFIG"] = self._saved["cublas"]
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def _resolve_device(device: str) -> torch.device:
|
| 172 |
+
"""Return the requested device, or say why it is unavailable.
|
| 173 |
+
|
| 174 |
+
A silent fall back to CPU would change the numbers with no signal, so a
|
| 175 |
+
request for an absent accelerator is an error.
|
| 176 |
+
"""
|
| 177 |
+
dev = torch.device(device)
|
| 178 |
+
if dev.type == "cuda" and not torch.cuda.is_available():
|
| 179 |
+
raise RuntimeError(
|
| 180 |
+
"device='cuda' was requested but torch.cuda.is_available() is "
|
| 181 |
+
"False. Pass device='cpu' to train on CPU."
|
| 182 |
+
)
|
| 183 |
+
if dev.type == "mps" and not torch.backends.mps.is_available():
|
| 184 |
+
raise RuntimeError(
|
| 185 |
+
"device='mps' was requested but MPS is unavailable. Pass "
|
| 186 |
+
"device='cpu' to train on CPU."
|
| 187 |
+
)
|
| 188 |
+
return dev
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# --- data ------------------------------------------------------------------
|
| 192 |
+
|
| 193 |
+
class _SampleBatcher:
|
| 194 |
+
"""Group a plain iterable of samples into collated batches.
|
| 195 |
+
|
| 196 |
+
Re-iterated once per epoch, so a list, a sequence or any object with a
|
| 197 |
+
fresh ``__iter__`` works. A one-shot generator is exhausted after its first
|
| 198 |
+
pass and ``_epochs`` reports that rather than looping on nothing.
|
| 199 |
+
"""
|
| 200 |
+
|
| 201 |
+
def __init__(self, samples: Iterable[Any], batch_size: int) -> None:
|
| 202 |
+
self.samples = samples
|
| 203 |
+
self.batch_size = int(batch_size)
|
| 204 |
+
|
| 205 |
+
def __iter__(self):
|
| 206 |
+
from torch.utils.data import default_collate
|
| 207 |
+
|
| 208 |
+
buffer: List[Any] = []
|
| 209 |
+
for sample in self.samples:
|
| 210 |
+
buffer.append(sample)
|
| 211 |
+
if len(buffer) == self.batch_size:
|
| 212 |
+
yield default_collate(buffer)
|
| 213 |
+
buffer = []
|
| 214 |
+
# A short tail is dropped, matching drop_last on the DataLoader path:
|
| 215 |
+
# a partial batch changes the effective batch size mid-run.
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def _build_loader(
|
| 219 |
+
data: Any,
|
| 220 |
+
batch_size: int,
|
| 221 |
+
num_workers: int,
|
| 222 |
+
device: torch.device,
|
| 223 |
+
seed: int,
|
| 224 |
+
) -> Iterable[Any]:
|
| 225 |
+
from torch.utils.data import DataLoader, Dataset, IterableDataset
|
| 226 |
+
|
| 227 |
+
if isinstance(data, DataLoader):
|
| 228 |
+
# The caller has already made every batching decision; respect it.
|
| 229 |
+
return data
|
| 230 |
+
|
| 231 |
+
pin_memory = device.type == "cuda"
|
| 232 |
+
|
| 233 |
+
def worker_init(worker_id: int) -> None:
|
| 234 |
+
_seed_everything(seed + 1 + worker_id)
|
| 235 |
+
|
| 236 |
+
if isinstance(data, IterableDataset):
|
| 237 |
+
return DataLoader(
|
| 238 |
+
data,
|
| 239 |
+
batch_size=batch_size,
|
| 240 |
+
num_workers=num_workers,
|
| 241 |
+
pin_memory=pin_memory,
|
| 242 |
+
worker_init_fn=worker_init,
|
| 243 |
+
)
|
| 244 |
+
if isinstance(data, Dataset):
|
| 245 |
+
generator = torch.Generator().manual_seed(seed)
|
| 246 |
+
return DataLoader(
|
| 247 |
+
data,
|
| 248 |
+
batch_size=batch_size,
|
| 249 |
+
shuffle=True,
|
| 250 |
+
drop_last=True,
|
| 251 |
+
num_workers=num_workers,
|
| 252 |
+
pin_memory=pin_memory,
|
| 253 |
+
generator=generator,
|
| 254 |
+
worker_init_fn=worker_init,
|
| 255 |
+
)
|
| 256 |
+
if isinstance(data, collections.abc.Iterable):
|
| 257 |
+
if num_workers:
|
| 258 |
+
raise ValueError(
|
| 259 |
+
"num_workers > 0 needs a Dataset or a DataLoader; a plain "
|
| 260 |
+
"iterable is batched in this process."
|
| 261 |
+
)
|
| 262 |
+
return _SampleBatcher(data, batch_size)
|
| 263 |
+
raise TypeError(
|
| 264 |
+
"data must be a Dataset, an IterableDataset, a DataLoader or an "
|
| 265 |
+
f"iterable of samples, got {type(data).__name__}."
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def _epochs(loader: Iterable[Any]):
|
| 270 |
+
"""Yield batches forever, restarting the loader between passes."""
|
| 271 |
+
while True:
|
| 272 |
+
produced = False
|
| 273 |
+
for batch in loader:
|
| 274 |
+
produced = True
|
| 275 |
+
yield batch
|
| 276 |
+
if not produced:
|
| 277 |
+
raise ValueError(
|
| 278 |
+
"the training data yielded no batches. A one-shot generator is "
|
| 279 |
+
"exhausted after one pass: pass a Dataset, a DataLoader or a "
|
| 280 |
+
"re-iterable sequence, and check that it holds at least one "
|
| 281 |
+
"full batch."
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def _unpack_batch(
|
| 286 |
+
batch: Any, width: int, device: torch.device, dtype: torch.dtype
|
| 287 |
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 288 |
+
"""Normalize a batch to ``(window, mask, scale_factor)`` on ``device``.
|
| 289 |
+
|
| 290 |
+
Accepts a mapping with a ``window`` key (plus optional ``mask`` and
|
| 291 |
+
``scale_factor``), a bare tensor of windows, or a positional
|
| 292 |
+
``(window, mask, scale_factor)`` sequence.
|
| 293 |
+
"""
|
| 294 |
+
mask: Any = None
|
| 295 |
+
scale: Any = None
|
| 296 |
+
if isinstance(batch, dict):
|
| 297 |
+
if "window" not in batch:
|
| 298 |
+
raise KeyError(
|
| 299 |
+
f"batch dict needs a 'window' key, got {sorted(batch)}."
|
| 300 |
+
)
|
| 301 |
+
window = batch["window"]
|
| 302 |
+
mask = batch.get("mask")
|
| 303 |
+
scale = batch.get("scale_factor")
|
| 304 |
+
elif torch.is_tensor(batch):
|
| 305 |
+
window = batch
|
| 306 |
+
elif isinstance(batch, (tuple, list)):
|
| 307 |
+
window = batch[0]
|
| 308 |
+
mask = batch[1] if len(batch) > 1 else None
|
| 309 |
+
scale = batch[2] if len(batch) > 2 else None
|
| 310 |
+
else:
|
| 311 |
+
raise TypeError(
|
| 312 |
+
"a batch must be a dict, a tensor or a sequence, got "
|
| 313 |
+
f"{type(batch).__name__}."
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
window = torch.as_tensor(window).to(device=device, dtype=dtype)
|
| 317 |
+
if window.dim() == 3 and window.shape[-1] == 1:
|
| 318 |
+
window = window.squeeze(-1)
|
| 319 |
+
if window.dim() != 2:
|
| 320 |
+
raise ValueError(
|
| 321 |
+
f"window must be (B, L + K*p), got {tuple(window.shape)}."
|
| 322 |
+
)
|
| 323 |
+
if window.shape[1] != width:
|
| 324 |
+
raise ValueError(
|
| 325 |
+
f"window is {window.shape[1]} long but the rollout needs "
|
| 326 |
+
f"{width} = seq_len + ar_chunks * output_token_len. Use "
|
| 327 |
+
"tinycast.train.training_window_width(config) to size the dataset."
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
if mask is None:
|
| 331 |
+
observed = torch.isfinite(window).to(dtype)
|
| 332 |
+
else:
|
| 333 |
+
observed = torch.as_tensor(mask).to(device=device, dtype=dtype)
|
| 334 |
+
if observed.dim() == 3 and observed.shape[-1] == 1:
|
| 335 |
+
observed = observed.squeeze(-1)
|
| 336 |
+
if observed.shape != window.shape:
|
| 337 |
+
raise ValueError(
|
| 338 |
+
f"mask {tuple(observed.shape)} does not match window "
|
| 339 |
+
f"{tuple(window.shape)}."
|
| 340 |
+
)
|
| 341 |
+
observed = observed * torch.isfinite(window).to(dtype)
|
| 342 |
+
window = torch.nan_to_num(window, nan=0.0, posinf=0.0, neginf=0.0)
|
| 343 |
+
|
| 344 |
+
if scale is None:
|
| 345 |
+
# No metadata: one sample per canonical cycle. Datasets that carry a
|
| 346 |
+
# frequency should supply tinycast.scale.seasonal_scale_factor instead,
|
| 347 |
+
# since the local anchor channels and the seasonal copy both read it.
|
| 348 |
+
scale_factor = torch.ones(
|
| 349 |
+
window.shape[0], device=device, dtype=torch.float32
|
| 350 |
+
)
|
| 351 |
+
else:
|
| 352 |
+
scale_factor = torch.as_tensor(scale).to(
|
| 353 |
+
device=device, dtype=torch.float32
|
| 354 |
+
).reshape(-1)
|
| 355 |
+
if scale_factor.numel() == 1:
|
| 356 |
+
scale_factor = scale_factor.expand(window.shape[0])
|
| 357 |
+
elif scale_factor.numel() != window.shape[0]:
|
| 358 |
+
raise ValueError(
|
| 359 |
+
f"scale_factor carries {scale_factor.numel()} entries for a "
|
| 360 |
+
f"batch of {window.shape[0]}."
|
| 361 |
+
)
|
| 362 |
+
return window, observed, scale_factor
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
# --- schedule --------------------------------------------------------------
|
| 366 |
+
|
| 367 |
+
def _wsd_lambda(
|
| 368 |
+
max_steps: int,
|
| 369 |
+
warmup_fraction: float,
|
| 370 |
+
decay_fraction: float,
|
| 371 |
+
min_ratio: float,
|
| 372 |
+
) -> Callable[[int], float]:
|
| 373 |
+
"""Warmup, stable, decay multiplier.
|
| 374 |
+
|
| 375 |
+
Linear to the peak over the warmup span, held there, then down to the floor
|
| 376 |
+
as ``1 - sqrt(progress)``: a sharp initial drop and a shallow tail. All
|
| 377 |
+
three spans are fractions of ``max_steps``, which is why ``max_steps``
|
| 378 |
+
reshapes a run instead of truncating one.
|
| 379 |
+
"""
|
| 380 |
+
warmup_steps = max(1, int(max_steps * warmup_fraction))
|
| 381 |
+
decay_steps = max(1, int(max_steps * decay_fraction))
|
| 382 |
+
decay_start = max(warmup_steps + 1, max_steps - decay_steps)
|
| 383 |
+
|
| 384 |
+
def lr_lambda(step: int) -> float:
|
| 385 |
+
if step < warmup_steps:
|
| 386 |
+
return step / warmup_steps
|
| 387 |
+
if step < decay_start:
|
| 388 |
+
return 1.0
|
| 389 |
+
span = max(1, max_steps - decay_start)
|
| 390 |
+
progress = min(1.0, (step - decay_start) / span)
|
| 391 |
+
return min_ratio + (1.0 - min_ratio) * (1.0 - math.sqrt(progress))
|
| 392 |
+
|
| 393 |
+
return lr_lambda
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
# --- the rollout -----------------------------------------------------------
|
| 397 |
+
|
| 398 |
+
def _chunk_loss(
|
| 399 |
+
backbone: torch.nn.Module,
|
| 400 |
+
context: torch.Tensor,
|
| 401 |
+
target: torch.Tensor,
|
| 402 |
+
target_mask: torch.Tensor,
|
| 403 |
+
scale_factor: torch.Tensor,
|
| 404 |
+
quantiles: torch.Tensor,
|
| 405 |
+
commit_weight: float,
|
| 406 |
+
commit_gated: bool,
|
| 407 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 408 |
+
"""One block: encode the context, score the block, hand back its median.
|
| 409 |
+
|
| 410 |
+
Returns the scalar block loss and the raw-magnitude median forecast the
|
| 411 |
+
next block feeds on.
|
| 412 |
+
"""
|
| 413 |
+
horizon = target.shape[1]
|
| 414 |
+
y_norm, x_min, x_range = backbone.encode(
|
| 415 |
+
context, batch_first=True, scale_factor=scale_factor, horizon=horizon,
|
| 416 |
+
)
|
| 417 |
+
y_norm = y_norm.clamp(-_PRED_CLAMP, _PRED_CLAMP)
|
| 418 |
+
x_min_b = x_min.squeeze(-1) # (B, 1)
|
| 419 |
+
x_range_b = x_range.squeeze(-1) # (B, 1)
|
| 420 |
+
target_norm = ((target - x_min_b) / x_range_b).clamp(
|
| 421 |
+
-_TARGET_CLAMP, _TARGET_CLAMP
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
# A sample is scored only if its context has range to normalize by, its
|
| 425 |
+
# forecast is finite, and it has an observed target position.
|
| 426 |
+
non_batch = tuple(range(1, y_norm.dim()))
|
| 427 |
+
valid = (
|
| 428 |
+
(x_range_b.squeeze(-1) > _MIN_RANGE)
|
| 429 |
+
& torch.isfinite(y_norm).all(dim=non_batch)
|
| 430 |
+
& (target_mask.sum(dim=1) > 0)
|
| 431 |
+
).to(y_norm.dtype)
|
| 432 |
+
|
| 433 |
+
per_sample = pinball_loss(
|
| 434 |
+
y_norm, target_norm, quantiles, target_mask, reduction="none",
|
| 435 |
+
)
|
| 436 |
+
per_sample = torch.nan_to_num(per_sample, 0.0, 0.0, 0.0) * valid
|
| 437 |
+
|
| 438 |
+
q_mid = y_norm.shape[-1] // 2
|
| 439 |
+
median_norm = y_norm[..., q_mid] # (B, H)
|
| 440 |
+
|
| 441 |
+
if commit_weight > 0.0:
|
| 442 |
+
copy_raw = seasonal_copy_baseline(context, horizon, scale_factor)
|
| 443 |
+
copy_norm = (copy_raw - x_min_b) / x_range_b
|
| 444 |
+
commit = committing_loss(
|
| 445 |
+
median_norm, target_norm, copy_norm, target_mask,
|
| 446 |
+
weight=commit_weight, gated=commit_gated, reduction="none",
|
| 447 |
+
)
|
| 448 |
+
per_sample = per_sample + torch.nan_to_num(commit, 0.0, 0.0, 0.0) * valid
|
| 449 |
+
|
| 450 |
+
n_valid = valid.sum()
|
| 451 |
+
if float(n_valid) < 1.0:
|
| 452 |
+
loss = per_sample.sum() * 0.0
|
| 453 |
+
else:
|
| 454 |
+
loss = per_sample.sum() / n_valid
|
| 455 |
+
if not torch.isfinite(loss):
|
| 456 |
+
loss = per_sample.sum() * 0.0
|
| 457 |
+
|
| 458 |
+
median_raw = median_norm * x_range_b + x_min_b
|
| 459 |
+
return loss, median_raw
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
def _rollout_loss(
|
| 463 |
+
backbone: torch.nn.Module,
|
| 464 |
+
window: torch.Tensor,
|
| 465 |
+
observed: torch.Tensor,
|
| 466 |
+
scale_factor: torch.Tensor,
|
| 467 |
+
*,
|
| 468 |
+
seq_len: int,
|
| 469 |
+
horizon_unit: int,
|
| 470 |
+
chunks: int,
|
| 471 |
+
epsilon: float,
|
| 472 |
+
quantiles: torch.Tensor,
|
| 473 |
+
commit_weight: float,
|
| 474 |
+
commit_gated: bool,
|
| 475 |
+
) -> torch.Tensor:
|
| 476 |
+
"""Roll out ``chunks`` blocks under scheduled sampling and average them.
|
| 477 |
+
|
| 478 |
+
Each block re-normalizes its own context window. Between blocks the model's
|
| 479 |
+
own median replaces the true values with probability ``epsilon``, so the
|
| 480 |
+
context the model sees late in the rollout is built the way it will be at
|
| 481 |
+
inference. Fed values are detached: they are inputs, not a gradient path.
|
| 482 |
+
|
| 483 |
+
An unobserved target position is always replaced by the median, whatever
|
| 484 |
+
``epsilon`` says, rather than by the zero its mask stands for. On gap-free
|
| 485 |
+
data this makes no difference; on gappy data it keeps a filler value out of
|
| 486 |
+
the next context.
|
| 487 |
+
"""
|
| 488 |
+
length, unit = int(seq_len), int(horizon_unit)
|
| 489 |
+
series = window
|
| 490 |
+
total = window.new_zeros(())
|
| 491 |
+
for k in range(chunks):
|
| 492 |
+
context = series[:, k * unit: k * unit + length]
|
| 493 |
+
start = length + k * unit
|
| 494 |
+
stop = start + unit
|
| 495 |
+
target = window[:, start:stop]
|
| 496 |
+
target_mask = observed[:, start:stop]
|
| 497 |
+
loss_k, median_raw = _chunk_loss(
|
| 498 |
+
backbone, context, target, target_mask, scale_factor,
|
| 499 |
+
quantiles, commit_weight, commit_gated,
|
| 500 |
+
)
|
| 501 |
+
total = total + loss_k
|
| 502 |
+
if k < chunks - 1:
|
| 503 |
+
use_pred = (
|
| 504 |
+
torch.rand(window.shape[0], 1, device=window.device) < epsilon
|
| 505 |
+
) | (target_mask < 0.5)
|
| 506 |
+
fed = torch.where(use_pred, median_raw, target)
|
| 507 |
+
series = series.clone()
|
| 508 |
+
series[:, start:stop] = fed.detach()
|
| 509 |
+
return total / float(chunks)
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
# --- checkpoints -----------------------------------------------------------
|
| 513 |
+
|
| 514 |
+
def _save(model: torch.nn.Module, path: Path) -> None:
|
| 515 |
+
"""Write the model in the released format.
|
| 516 |
+
|
| 517 |
+
``safetensors`` stores each unique storage once, so the two weight-tied FFN
|
| 518 |
+
stacks are written once and ``tinycast.load_checkpoint`` restores the
|
| 519 |
+
sharing on load.
|
| 520 |
+
"""
|
| 521 |
+
from safetensors.torch import save_model
|
| 522 |
+
|
| 523 |
+
save_model(model, str(path))
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
# --- entry point -----------------------------------------------------------
|
| 527 |
+
|
| 528 |
+
def train(
|
| 529 |
+
config: TinyCastConfig,
|
| 530 |
+
*,
|
| 531 |
+
data: Any,
|
| 532 |
+
max_steps: int,
|
| 533 |
+
output_dir: str,
|
| 534 |
+
seed: int = 42,
|
| 535 |
+
device: str = "cpu",
|
| 536 |
+
batch_size: int,
|
| 537 |
+
accumulate_grad_batches: int = 1,
|
| 538 |
+
num_workers: int = 0,
|
| 539 |
+
checkpoint_every: Optional[int] = None,
|
| 540 |
+
callbacks: Sequence[Callable[[Dict[str, Any]], None]] = (),
|
| 541 |
+
deterministic: bool = False,
|
| 542 |
+
) -> TrainResult:
|
| 543 |
+
"""Train a TinyCast model and return where it landed.
|
| 544 |
+
|
| 545 |
+
Args:
|
| 546 |
+
config: the architecture. Optional attributes named after the module
|
| 547 |
+
constants (``ar_chunks``, ``learning_rate``, ``commit_weight`` and
|
| 548 |
+
so on) override the shipped recipe; absent ones take it.
|
| 549 |
+
data: a ``Dataset``, an ``IterableDataset``, a ``DataLoader``, or a
|
| 550 |
+
plain iterable of samples. A sample is a mapping with a ``window``
|
| 551 |
+
of ``L + K * p`` values and optionally a ``mask`` and a
|
| 552 |
+
``scale_factor``; a bare tensor or a positional triple works too.
|
| 553 |
+
Use :func:`training_window_width` to size the window.
|
| 554 |
+
max_steps: optimizer steps. This RESHAPES the run: the warmup, stable
|
| 555 |
+
and decay spans of the schedule are fractions of it, and the
|
| 556 |
+
scheduled-sampling probability ramps over its first half. A short
|
| 557 |
+
run is not a prefix of a long one.
|
| 558 |
+
output_dir: created if absent. Receives ``model.safetensors``,
|
| 559 |
+
``config.json`` and any periodic checkpoints.
|
| 560 |
+
seed: seeds Python, NumPy, torch, the sampler and the workers, inside
|
| 561 |
+
this call.
|
| 562 |
+
device: ``"cpu"``, ``"cuda"``, ``"cuda:1"``, ``"mps"``. An unavailable
|
| 563 |
+
accelerator raises rather than falling back.
|
| 564 |
+
batch_size: samples per micro-batch. The effective batch is this times
|
| 565 |
+
``accumulate_grad_batches``.
|
| 566 |
+
accumulate_grad_batches: micro-batches per optimizer step.
|
| 567 |
+
num_workers: DataLoader workers. Zero, single-process, by default.
|
| 568 |
+
checkpoint_every: write a checkpoint every this many optimizer steps.
|
| 569 |
+
callbacks: called after each optimizer step with a dict carrying
|
| 570 |
+
``step``, ``loss``, ``lr``, ``epsilon`` and ``grad_norm``.
|
| 571 |
+
deterministic: request deterministic kernels for the duration of the
|
| 572 |
+
call, then restore the process flags. An op with no deterministic
|
| 573 |
+
implementation warns rather than raising, so this tightens
|
| 574 |
+
reproducibility without ruling a device out.
|
| 575 |
+
|
| 576 |
+
Returns:
|
| 577 |
+
A :class:`TrainResult`.
|
| 578 |
+
|
| 579 |
+
Nothing here reaches the network and nothing is uploaded.
|
| 580 |
+
"""
|
| 581 |
+
if int(max_steps) < 1:
|
| 582 |
+
raise ValueError(f"max_steps must be at least 1, got {max_steps}.")
|
| 583 |
+
if int(batch_size) < 1:
|
| 584 |
+
raise ValueError(f"batch_size must be at least 1, got {batch_size}.")
|
| 585 |
+
if int(accumulate_grad_batches) < 1:
|
| 586 |
+
raise ValueError(
|
| 587 |
+
"accumulate_grad_batches must be at least 1, got "
|
| 588 |
+
f"{accumulate_grad_batches}."
|
| 589 |
+
)
|
| 590 |
+
max_steps = int(max_steps)
|
| 591 |
+
accumulate_grad_batches = int(accumulate_grad_batches)
|
| 592 |
+
|
| 593 |
+
chunks = int(_setting(config, "ar_chunks", AR_CHUNKS))
|
| 594 |
+
width = training_window_width(config, chunks)
|
| 595 |
+
seq_len = int(config.seq_len)
|
| 596 |
+
horizon_unit = int(config.output_token_len)
|
| 597 |
+
eps_max = float(_setting(config, "scheduled_sampling_max", SCHEDULED_SAMPLING_MAX))
|
| 598 |
+
peak_lr = float(_setting(config, "learning_rate", LEARNING_RATE))
|
| 599 |
+
min_lr = float(_setting(config, "min_learning_rate", MIN_LEARNING_RATE))
|
| 600 |
+
warmup_fraction = float(_setting(config, "warmup_fraction", WARMUP_FRACTION))
|
| 601 |
+
decay_fraction = float(_setting(config, "decay_fraction", DECAY_FRACTION))
|
| 602 |
+
weight_decay = float(_setting(config, "weight_decay", WEIGHT_DECAY))
|
| 603 |
+
grad_clip = float(_setting(config, "grad_clip", GRAD_CLIP))
|
| 604 |
+
commit_weight = float(_setting(config, "commit_weight", COMMIT_WEIGHT))
|
| 605 |
+
commit_gated = bool(_setting(config, "commit_gated", COMMIT_GATED))
|
| 606 |
+
if warmup_fraction + decay_fraction > 1.0:
|
| 607 |
+
raise ValueError(
|
| 608 |
+
"warmup_fraction + decay_fraction must not exceed 1.0, got "
|
| 609 |
+
f"{warmup_fraction} + {decay_fraction}."
|
| 610 |
+
)
|
| 611 |
+
|
| 612 |
+
torch_device = _resolve_device(device)
|
| 613 |
+
out = Path(output_dir)
|
| 614 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 615 |
+
|
| 616 |
+
with _Determinism(deterministic, torch_device):
|
| 617 |
+
_seed_everything(int(seed))
|
| 618 |
+
|
| 619 |
+
model = TinyCastForPrediction(config).to(torch_device)
|
| 620 |
+
model.train()
|
| 621 |
+
backbone = model.model
|
| 622 |
+
param_dtype = next(model.parameters()).dtype
|
| 623 |
+
quantiles = torch.tensor(
|
| 624 |
+
list(config.quantiles), dtype=param_dtype, device=torch_device,
|
| 625 |
+
)
|
| 626 |
+
|
| 627 |
+
optimizer = torch.optim.AdamW(
|
| 628 |
+
model.parameters(),
|
| 629 |
+
lr=peak_lr,
|
| 630 |
+
betas=ADAM_BETAS,
|
| 631 |
+
weight_decay=weight_decay,
|
| 632 |
+
)
|
| 633 |
+
scheduler = torch.optim.lr_scheduler.LambdaLR(
|
| 634 |
+
optimizer,
|
| 635 |
+
_wsd_lambda(max_steps, warmup_fraction, decay_fraction,
|
| 636 |
+
min_lr / peak_lr),
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
+
loader = _build_loader(
|
| 640 |
+
data, int(batch_size), int(num_workers), torch_device, int(seed),
|
| 641 |
+
)
|
| 642 |
+
batches = _epochs(loader)
|
| 643 |
+
|
| 644 |
+
losses: List[float] = []
|
| 645 |
+
learning_rates: List[float] = []
|
| 646 |
+
written: List[str] = []
|
| 647 |
+
|
| 648 |
+
for step in range(max_steps):
|
| 649 |
+
# The feedback probability ramps to its maximum over the first half
|
| 650 |
+
# of the run, so it too is a function of max_steps.
|
| 651 |
+
epsilon = eps_max * min(1.0, step / max(1.0, 0.5 * max_steps))
|
| 652 |
+
lr = float(scheduler.get_last_lr()[0])
|
| 653 |
+
|
| 654 |
+
optimizer.zero_grad(set_to_none=True)
|
| 655 |
+
step_loss = 0.0
|
| 656 |
+
for _ in range(accumulate_grad_batches):
|
| 657 |
+
window, observed, scale_factor = _unpack_batch(
|
| 658 |
+
next(batches), width, torch_device, param_dtype,
|
| 659 |
+
)
|
| 660 |
+
loss = _rollout_loss(
|
| 661 |
+
backbone, window, observed, scale_factor,
|
| 662 |
+
seq_len=seq_len,
|
| 663 |
+
horizon_unit=horizon_unit,
|
| 664 |
+
chunks=chunks,
|
| 665 |
+
epsilon=epsilon,
|
| 666 |
+
quantiles=quantiles,
|
| 667 |
+
commit_weight=commit_weight,
|
| 668 |
+
commit_gated=commit_gated,
|
| 669 |
+
)
|
| 670 |
+
(loss / accumulate_grad_batches).backward()
|
| 671 |
+
step_loss += float(loss.detach()) / accumulate_grad_batches
|
| 672 |
+
|
| 673 |
+
if grad_clip > 0.0:
|
| 674 |
+
grad_norm = float(
|
| 675 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip)
|
| 676 |
+
)
|
| 677 |
+
else:
|
| 678 |
+
grad_norm = float("nan")
|
| 679 |
+
optimizer.step()
|
| 680 |
+
scheduler.step()
|
| 681 |
+
|
| 682 |
+
losses.append(step_loss)
|
| 683 |
+
learning_rates.append(lr)
|
| 684 |
+
record = {
|
| 685 |
+
"step": step + 1,
|
| 686 |
+
"loss": step_loss,
|
| 687 |
+
"lr": lr,
|
| 688 |
+
"epsilon": epsilon,
|
| 689 |
+
"grad_norm": grad_norm,
|
| 690 |
+
}
|
| 691 |
+
for callback in callbacks:
|
| 692 |
+
callback(record)
|
| 693 |
+
|
| 694 |
+
if checkpoint_every and (step + 1) % int(checkpoint_every) == 0:
|
| 695 |
+
path = out / f"step-{step + 1:06d}.safetensors"
|
| 696 |
+
_save(model, path)
|
| 697 |
+
written.append(str(path))
|
| 698 |
+
|
| 699 |
+
final = out / "model.safetensors"
|
| 700 |
+
_save(model, final)
|
| 701 |
+
with open(out / "config.json", "w") as handle:
|
| 702 |
+
json.dump(config.to_dict(), handle, indent=2)
|
| 703 |
+
|
| 704 |
+
return TrainResult(
|
| 705 |
+
checkpoint_path=str(final),
|
| 706 |
+
steps=max_steps,
|
| 707 |
+
losses=losses,
|
| 708 |
+
learning_rates=learning_rates,
|
| 709 |
+
checkpoints=written,
|
| 710 |
+
window_width=width,
|
| 711 |
+
)
|
| 712 |
+
|
| 713 |
+
|
| 714 |
+
__all__ = ["TrainResult", "train", "training_window_width"]
|