usmar commited on
Commit
c1295a6
·
verified ·
1 Parent(s): 5dc5126

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. README.md +15 -5
  2. index.html +337 -18
  3. meta.json +1 -0
  4. shim.js +183 -0
  5. weights.bin +3 -0
README.md CHANGED
@@ -1,10 +1,20 @@
1
  ---
2
- title: Tinycast
3
- emoji: 🏢
4
- colorFrom: red
5
- colorTo: pink
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: tinycast
3
+ emoji: 📉
4
+ colorFrom: blue
5
+ colorTo: red
6
  sdk: static
7
  pinned: false
8
+ license: mit
9
+ short_description: Tiny models vs transformers on the ETTh1 benchmark
10
  ---
11
 
12
+ # tinycast
13
+
14
+ Do you need a transformer to forecast? In-browser demo of tiny forecasting
15
+ models (NLinear, DLinear + baselines) on the ETTh1 benchmark — inference runs
16
+ as plain JavaScript matrix products, no backend. The ETTh1 CSV is fetched
17
+ directly from [ETDataset](https://github.com/zhouhaoyi/ETDataset) at load time.
18
+
19
+ Source, training code and full evaluation (TCN, LSTM, published transformer
20
+ comparison): https://github.com/UsmarHaider/tinycast
index.html CHANGED
@@ -1,19 +1,338 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>tinycast do you need a transformer to forecast?</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light;
10
+ --surface: #fcfcfb; --plane: #f9f9f7;
11
+ --ink: #0b0b0b; --ink-2: #52514e; --muted: #898781;
12
+ --grid: #e1e0d9; --axis: #c3c2b7; --ring: rgba(11,11,11,.10);
13
+ --s1: #2a78d6; --s2: #eb6834; --s3: #1baf7a; --s4: #eda100; --s5: #e87ba4; --s6: #008300;
14
+ }
15
+ @media (prefers-color-scheme: dark) {
16
+ :root:not([data-theme="light"]) {
17
+ color-scheme: dark;
18
+ --surface: #1a1a19; --plane: #0d0d0d;
19
+ --ink: #ffffff; --ink-2: #c3c2b7; --muted: #898781;
20
+ --grid: #2c2c2a; --axis: #383835; --ring: rgba(255,255,255,.10);
21
+ --s1: #3987e5; --s2: #d95926; --s3: #199e70; --s4: #c98500; --s5: #d55181; --s6: #008300;
22
+ }
23
+ }
24
+ * { box-sizing: border-box; }
25
+ body {
26
+ margin: 0; background: var(--plane); color: var(--ink);
27
+ font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
28
+ }
29
+ .wrap { max-width: 980px; margin: 0 auto; padding: 28px 20px 48px; }
30
+ header h1 { margin: 0; font-size: 26px; letter-spacing: -0.02em; }
31
+ header h1 .accent { color: var(--s1); }
32
+ header p.tag { margin: 6px 0 0; color: var(--ink-2); max-width: 64ch; }
33
+ .card {
34
+ background: var(--surface); border: 1px solid var(--ring); border-radius: 12px;
35
+ padding: 16px 18px; margin-top: 18px;
36
+ }
37
+ .controls { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }
38
+ .seg { display: inline-flex; border: 1px solid var(--ring); border-radius: 8px; overflow: hidden; }
39
+ .seg button {
40
+ border: 0; background: transparent; color: var(--ink-2); padding: 6px 14px;
41
+ font: inherit; cursor: pointer;
42
+ }
43
+ .seg button.on { background: var(--s1); color: #fff; }
44
+ .controls label { color: var(--ink-2); font-size: 13px; }
45
+ input[type=range] { width: 190px; accent-color: var(--s1); vertical-align: middle; }
46
+ .btn {
47
+ border: 1px solid var(--ring); background: transparent; color: var(--ink);
48
+ border-radius: 8px; padding: 6px 14px; font: inherit; cursor: pointer;
49
+ }
50
+ .btn:hover { border-color: var(--s1); color: var(--s1); }
51
+ .chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
52
+ .chip {
53
+ display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--ring);
54
+ border-radius: 999px; padding: 3px 12px; font-size: 13px; color: var(--ink-2);
55
+ background: transparent; cursor: pointer;
56
+ }
57
+ .chip .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--c); }
58
+ .chip.off { opacity: .38; }
59
+ .chip.fixed { cursor: default; }
60
+ .chip .dot.dash { border-radius: 1px; height: 3px; }
61
+ #chartbox { position: relative; margin-top: 6px; }
62
+ svg { display: block; width: 100%; height: auto; }
63
+ .tip {
64
+ position: absolute; pointer-events: none; display: none; background: var(--surface);
65
+ border: 1px solid var(--ring); border-radius: 8px; padding: 8px 10px; font-size: 12px;
66
+ box-shadow: 0 4px 14px rgba(0,0,0,.12); min-width: 150px; z-index: 2;
67
+ }
68
+ .tip .row { display: flex; justify-content: space-between; gap: 12px; }
69
+ .tip .row .val { font-variant-numeric: tabular-nums; color: var(--ink); }
70
+ .tip .row .name { color: var(--ink-2); }
71
+ .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-top: 18px; }
72
+ .tile { background: var(--surface); border: 1px solid var(--ring); border-radius: 12px; padding: 14px 16px; }
73
+ .tile .k { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
74
+ .tile .v { font-size: 26px; margin-top: 2px; }
75
+ .tile .s { font-size: 12px; color: var(--ink-2); }
76
+ .tablecard { overflow-x: auto; }
77
+ table { border-collapse: collapse; width: 100%; font-size: 13.5px; margin-top: 4px; }
78
+ th, td { text-align: right; padding: 6px 10px; border-bottom: 1px solid var(--grid); font-variant-numeric: tabular-nums; }
79
+ th:first-child, td:first-child { text-align: left; }
80
+ th { color: var(--muted); font-weight: 600; }
81
+ td.best { color: var(--s1); font-weight: 700; }
82
+ tr.pub td { color: var(--ink-2); }
83
+ tr.sep td { border-top: 2px solid var(--axis); }
84
+ .note { color: var(--muted); font-size: 12.5px; margin-top: 8px; }
85
+ footer { margin-top: 26px; color: var(--muted); font-size: 13px; }
86
+ footer a { color: var(--s1); text-decoration: none; }
87
+ #err { display: none; margin-top: 18px; border: 1px solid #d03b3b; color: #d03b3b;
88
+ border-radius: 10px; padding: 10px 14px; }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <div class="wrap">
93
+ <header>
94
+ <h1>tiny<span class="accent">cast</span></h1>
95
+ <p class="tag">Do you need a transformer to forecast? Five tiny models — none bigger than
96
+ 80k parameters — against the ETTh1 long-horizon benchmark, evaluated exactly like the papers.
97
+ Pick a test window and see for yourself.</p>
98
+ </header>
99
+
100
+ <div id="err"></div>
101
+
102
+ <div class="card">
103
+ <div class="controls">
104
+ <span class="seg" id="horizon-seg"></span>
105
+ <label>window <input type="range" id="win" min="0" max="100" value="0">
106
+ <span id="winlab" style="font-variant-numeric:tabular-nums"></span></label>
107
+ <button class="btn" id="rand">Try a sample</button>
108
+ <span id="t0" style="color:var(--muted);font-size:13px"></span>
109
+ </div>
110
+ <div class="chips" id="chips"></div>
111
+ <div id="chartbox">
112
+ <svg id="chart" viewBox="0 0 940 400" role="img" aria-label="Forecast chart"></svg>
113
+ <div class="tip" id="tip"></div>
114
+ </div>
115
+ <div class="note">Oil temperature (°C) of electricity transformer 1, hourly. Left of the
116
+ divider: the last week of history the models see. Right: what each predicts vs. what happened.</div>
117
+ </div>
118
+
119
+ <div class="tiles" id="tiles"></div>
120
+
121
+ <div class="card tablecard">
122
+ <strong>Test MSE (standardized, multivariate) — lower is better</strong>
123
+ <table id="results"></table>
124
+ <div class="note">Published rows are the numbers reported in Zeng et&nbsp;al. 2023
125
+ (<em>Are Transformers Effective for Time Series Forecasting?</em>, AAAI). Same dataset,
126
+ same split, same metric. Best per horizon in blue.</div>
127
+ </div>
128
+
129
+ <footer>
130
+ Built by <a href="https://github.com/UsmarHaider">Usmar Haider</a> ·
131
+ <a href="https://github.com/UsmarHaider/tinycast">source &amp; write-up on GitHub</a> ·
132
+ data: <a href="https://github.com/zhouhaoyi/ETDataset">ETDataset</a>
133
+ </footer>
134
+ </div>
135
+
136
+ <script src="shim.js"></script>
137
+ <script>
138
+ "use strict";
139
+ const SERIES = [
140
+ { key: "nlinear", label: "NLinear", css: "--s1", on: true },
141
+ { key: "seasonal_naive", label: "Seasonal naive", css: "--s2", on: true },
142
+ { key: "dlinear", label: "DLinear", css: "--s3", on: false },
143
+ { key: "tcn", label: "TCN", css: "--s4", on: true },
144
+ { key: "lstm", label: "LSTM", css: "--s5", on: false },
145
+ { key: "persistence", label: "Persistence", css: "--s6", on: false },
146
+ ];
147
+ const NICE = { persistence: "Persistence", seasonal_naive: "Seasonal naive", linear: "Linear",
148
+ nlinear: "NLinear", dlinear: "DLinear", tcn: "TCN", lstm: "LSTM" };
149
+ const state = { horizon: 96, index: 0, meta: null, sample: null, hist_show: 168 };
150
+
151
+ const $ = (id) => document.getElementById(id);
152
+ const css = (v) => getComputedStyle(document.documentElement).getPropertyValue(v).trim();
153
+
154
+ async function api(path) {
155
+ const r = await fetch(path);
156
+ if (!r.ok) throw new Error(path + " → " + r.status);
157
+ return r.json();
158
+ }
159
+
160
+ function fail(msg) { const e = $("err"); e.style.display = "block"; e.textContent = msg; }
161
+
162
+ function segButtons() {
163
+ const seg = $("horizon-seg");
164
+ seg.innerHTML = "";
165
+ for (const h of state.meta.horizons) {
166
+ const b = document.createElement("button");
167
+ b.textContent = h + " h";
168
+ b.className = h === state.horizon ? "on" : "";
169
+ b.onclick = () => { state.horizon = h; clampIndex(); refresh(); };
170
+ seg.appendChild(b);
171
+ }
172
+ }
173
+
174
+ function clampIndex() {
175
+ const n = state.meta.n_windows[String(state.horizon)];
176
+ state.index = Math.min(state.index, n - 1);
177
+ $("win").max = n - 1;
178
+ }
179
+
180
+ function chips(available) {
181
+ const box = $("chips");
182
+ box.innerHTML = "";
183
+ const hist = document.createElement("span");
184
+ hist.className = "chip fixed";
185
+ hist.innerHTML = '<span class="dot" style="--c:var(--ink)"></span>history';
186
+ const act = document.createElement("span");
187
+ act.className = "chip fixed";
188
+ act.innerHTML = '<span class="dot dash" style="--c:var(--ink)"></span>actual';
189
+ box.append(hist, act);
190
+ for (const s of SERIES) {
191
+ if (!available.includes(s.key)) continue;
192
+ const b = document.createElement("button");
193
+ b.className = "chip" + (s.on ? "" : " off");
194
+ b.innerHTML = `<span class="dot" style="--c:var(${s.css})"></span>${s.label}`;
195
+ b.onclick = () => { s.on = !s.on; draw(); };
196
+ box.appendChild(b);
197
+ }
198
+ }
199
+
200
+ function draw() {
201
+ const d = state.sample;
202
+ if (!d) return;
203
+ const H = d.horizon, histN = state.hist_show;
204
+ const hist = d.history_ot.slice(-histN);
205
+ const W = 940, Hgt = 400, m = { t: 16, r: 14, b: 30, l: 46 };
206
+ const iw = W - m.l - m.r, ih = Hgt - m.t - m.b;
207
+ const x = (i) => m.l + ((i + histN) / (histN + H - 1)) * iw; // i in [-histN, H-1]
208
+ const active = SERIES.filter((s) => s.on && d.forecasts[s.key]);
209
+ let vals = hist.concat(d.actual_ot);
210
+ for (const s of active) vals = vals.concat(d.forecasts[s.key]);
211
+ const lo = Math.min(...vals), hi = Math.max(...vals);
212
+ const pad = (hi - lo) * 0.08 + 0.01;
213
+ const y = (v) => m.t + (1 - (v - (lo - pad)) / (hi - lo + 2 * pad)) * ih;
214
+
215
+ const line = (pts, color, dash, wdt) => {
216
+ const dstr = pts.map((p, i) => (i ? "L" : "M") + p[0].toFixed(1) + " " + p[1].toFixed(1)).join("");
217
+ return `<path d="${dstr}" fill="none" stroke="${color}" stroke-width="${wdt || 2}"` +
218
+ (dash ? ` stroke-dasharray="${dash}"` : "") + ` stroke-linejoin="round"/>`;
219
+ };
220
+ let g = "";
221
+ // gridlines + y labels
222
+ const ticks = 5;
223
+ for (let i = 0; i <= ticks; i++) {
224
+ const v = lo - pad + ((hi - lo + 2 * pad) * i) / ticks;
225
+ g += `<line x1="${m.l}" x2="${W - m.r}" y1="${y(v)}" y2="${y(v)}" stroke="${css("--grid")}" stroke-width="1"/>`;
226
+ g += `<text x="${m.l - 8}" y="${y(v) + 4}" text-anchor="end" font-size="11" fill="${css("--muted")}">${v.toFixed(1)}°</text>`;
227
+ }
228
+ // x labels every 48h
229
+ for (let t = -histN; t <= H; t += 48) {
230
+ g += `<text x="${x(t)}" y="${Hgt - 8}" text-anchor="middle" font-size="11" fill="${css("--muted")}">${t === 0 ? "now" : (t > 0 ? "+" + t : t) + "h"}</text>`;
231
+ }
232
+ // forecast-start divider
233
+ g += `<line x1="${x(0)}" x2="${x(0)}" y1="${m.t}" y2="${Hgt - m.b}" stroke="${css("--axis")}" stroke-width="1" stroke-dasharray="2 3"/>`;
234
+ // history + actual
235
+ g += line(hist.map((v, i) => [x(i - histN), y(v)]), css("--ink"), null, 2);
236
+ g += line(d.actual_ot.map((v, i) => [x(i), y(v)]), css("--ink"), "5 4", 2);
237
+ // forecasts
238
+ for (const s of active) g += line(d.forecasts[s.key].map((v, i) => [x(i), y(v)]), css(s.css), null, 2);
239
+ $("chart").innerHTML = g + `<rect id="hit" x="${m.l}" y="${m.t}" width="${iw}" height="${ih}" fill="transparent"/>`;
240
+ hover(x, histN, H, hist, active);
241
+ }
242
+
243
+ function hover(xf, histN, H, hist, active) {
244
+ const svg = $("chart"), tip = $("tip"), box = $("chartbox");
245
+ const d = state.sample;
246
+ svg.onmousemove = (ev) => {
247
+ const pt = svg.createSVGPoint(); pt.x = ev.clientX; pt.y = ev.clientY;
248
+ const p = pt.matrixTransform(svg.getScreenCTM().inverse());
249
+ const t = Math.round(((p.x - 46) / (940 - 46 - 14)) * (histN + H - 1) - histN);
250
+ if (t < -histN || t >= H) { tip.style.display = "none"; return; }
251
+ let rows = `<div class="row"><span class="name">${t >= 0 ? "+" + t + "h" : t + "h"}</span></div>`;
252
+ if (t < 0) {
253
+ rows += `<div class="row"><span class="name">history</span><span class="val">${hist[t + histN].toFixed(1)}°C</span></div>`;
254
+ } else {
255
+ rows += `<div class="row"><span class="name">actual</span><span class="val">${d.actual_ot[t].toFixed(1)}°C</span></div>`;
256
+ for (const s of active)
257
+ rows += `<div class="row"><span class="name" style="color:var(${s.css})">${s.label}</span><span class="val">${d.forecasts[s.key][t].toFixed(1)}°C</span></div>`;
258
+ }
259
+ tip.innerHTML = rows;
260
+ tip.style.display = "block";
261
+ const r = box.getBoundingClientRect();
262
+ const tx = ev.clientX - r.left + 14, ty = ev.clientY - r.top - 10;
263
+ tip.style.left = Math.min(tx, r.width - tip.offsetWidth - 6) + "px";
264
+ tip.style.top = ty + "px";
265
+ };
266
+ svg.onmouseleave = () => { tip.style.display = "none"; };
267
+ }
268
+
269
+ function tiles() {
270
+ const res = state.meta.results, box = $("tiles");
271
+ const ours = res.models, pub = res.published || {};
272
+ const h = "96";
273
+ const nl = ours.nlinear?.[h]?.mse, inf = pub["Informer (2021)"]?.[h]?.mse;
274
+ const tls = [];
275
+ if (nl != null && inf != null)
276
+ tls.push(["NLinear vs Informer, h=96", Math.round((1 - nl / inf) * 100) + "% lower MSE",
277
+ `${nl.toFixed(3)} vs ${inf.toFixed(3)}`]);
278
+ tls.push(["Winning model size", "32k params", "one 336×96 matrix + bias"]);
279
+ const otm = res.ot_mae_celsius_h96;
280
+ if (otm) tls.push(["Oil-temp error, 4 days out", otm.mae_c + " °C MAE", NICE[otm.model] + ", horizon 96 h"]);
281
+ const sn = ours.seasonal_naive?.[h]?.mse;
282
+ if (sn != null && inf != null && sn < inf)
283
+ tls.push(["Seasonal naive vs Informer", "baseline wins", `${sn.toFixed(3)} vs ${inf.toFixed(3)} MSE`]);
284
+ box.innerHTML = tls.map(([k, v, s]) =>
285
+ `<div class="tile"><div class="k">${k}</div><div class="v">${v}</div><div class="s">${s}</div></div>`).join("");
286
+ }
287
+
288
+ function table() {
289
+ const res = state.meta.results, horizons = res.horizons;
290
+ const rows = [];
291
+ const ordered = ["persistence", "seasonal_naive", "linear", "nlinear", "dlinear", "tcn", "lstm"];
292
+ for (const k of ordered) if (res.models[k]) rows.push([NICE[k] + " (ours)", res.models[k], false]);
293
+ for (const [k, v] of Object.entries(res.published || {})) rows.push([k, v, true]);
294
+ const best = {};
295
+ for (const h of horizons) {
296
+ best[h] = Math.min(...rows.map(([, v]) => v[String(h)]?.mse ?? Infinity));
297
+ }
298
+ let html = "<tr><th>model</th>" + horizons.map((h) => `<th>h=${h}</th>`).join("") + "</tr>";
299
+ let sep = false;
300
+ for (const [name, v, pub] of rows) {
301
+ const cls = (pub ? "pub" : "") + (pub && !sep ? " sep" : "");
302
+ if (pub) sep = true;
303
+ html += `<tr class="${cls}"><td>${name}</td>` + horizons.map((h) => {
304
+ const c = v[String(h)];
305
+ if (!c) return "<td>–</td>";
306
+ return `<td class="${c.mse === best[h] ? "best" : ""}">${c.mse.toFixed(3)}</td>`;
307
+ }).join("") + "</tr>";
308
+ }
309
+ $("results").innerHTML = html;
310
+ }
311
+
312
+ async function refresh() {
313
+ segButtons();
314
+ const n = state.meta.n_windows[String(state.horizon)];
315
+ $("win").max = n - 1; $("win").value = state.index;
316
+ $("winlab").textContent = state.index + " / " + (n - 1);
317
+ try {
318
+ state.sample = await api(`api/sample?index=${state.index}&horizon=${state.horizon}`);
319
+ } catch (e) { fail("Could not load sample: " + e.message); return; }
320
+ $("t0").textContent = "forecast starts " + state.sample.t0;
321
+ chips(Object.keys(state.sample.forecasts));
322
+ draw();
323
+ }
324
+
325
+ (async function init() {
326
+ try { state.meta = await api("api/meta"); } catch (e) { fail("Could not reach the API: " + e.message); return; }
327
+ const q = new URLSearchParams(location.search);
328
+ const n = state.meta.n_windows[String(state.horizon)];
329
+ state.index = q.has("sample") ? Math.min(+q.get("sample"), n - 1) : 480 % n;
330
+ $("win").oninput = (e) => { state.index = +e.target.value; refresh(); };
331
+ $("rand").onclick = () => { state.index = Math.floor(Math.random() * n); refresh(); };
332
+ tiles(); table();
333
+ await refresh();
334
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", draw);
335
+ })();
336
+ </script>
337
+ </body>
338
  </html>
meta.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"tensors": {"nlinear_96": {"w": {"offset": 0, "shape": [96, 336]}, "b": {"offset": 32256, "shape": [96]}}, "nlinear_336": {"w": {"offset": 32352, "shape": [336, 336]}, "b": {"offset": 145248, "shape": [336]}}, "dlinear_96": {"wt": {"offset": 145584, "shape": [96, 336]}, "bt": {"offset": 177840, "shape": [96]}, "ws": {"offset": 177936, "shape": [96, 336]}, "bs": {"offset": 210192, "shape": [96]}}, "dlinear_336": {"wt": {"offset": 210288, "shape": [336, 336]}, "bt": {"offset": 323184, "shape": [336]}, "ws": {"offset": 323520, "shape": [336, 336]}, "bs": {"offset": 436416, "shape": [336]}}}, "seq_len": 336, "horizons": [96, 336], "moving_avg": 25, "channels": ["HUFL", "HULL", "MUFL", "MULL", "LUFL", "LULL", "OT"], "target_index": 6, "scaler_mean": [7.937742245659508, 2.0210386567335163, 5.079770601157927, 0.7461858799957015, 2.781762386375555, 0.7884531235540096, 17.1282616982271], "scaler_std": [5.812749409143771, 2.0901046504076, 5.518793579036245, 1.926379274132982, 1.0235226594952191, 0.6302366362251923, 9.176491024944333], "test_start_row": 11184, "test_end_row": 14400, "data_url": "https://raw.githubusercontent.com/zhouhaoyi/ETDataset/main/ETT-small/ETTh1.csv", "results": {"seq_len": 336, "horizons": [96, 192, 336, 720], "models": {"persistence": {"96": {"mse": 1.2944, "mae": 0.7132}, "192": {"mse": 1.3249, "mae": 0.7331}, "336": {"mse": 1.3299, "mae": 0.746}, "720": {"mse": 1.3351, "mae": 0.755}}, "seasonal_naive": {"96": {"mse": 0.5122, "mae": 0.4333}, "192": {"mse": 0.5808, "mae": 0.4692}, "336": {"mse": 0.6499, "mae": 0.5008}, "720": {"mse": 0.6554, "mae": 0.5141}}, "linear": {"96": {"mse": 0.3952, "mae": 0.4191}, "192": {"mse": 0.447, "mae": 0.4569}, "336": {"mse": 0.4897, "mae": 0.4894}, "720": {"mse": 0.5275, "mae": 0.5292}}, "nlinear": {"96": {"mse": 0.3998, "mae": 0.4156}, "192": {"mse": 0.4228, "mae": 0.4288}, "336": {"mse": 0.4497, "mae": 0.4437}, "720": {"mse": 0.4552, "mae": 0.4646}}, "dlinear": {"96": {"mse": 0.395, "mae": 0.4184}, "192": {"mse": 0.4352, "mae": 0.4434}, "336": {"mse": 0.4739, "mae": 0.4711}, "720": {"mse": 0.5015, "mae": 0.5108}}, "tcn": {"96": {"mse": 0.3848, "mae": 0.414}, "192": {"mse": 0.4378, "mae": 0.4427}, "336": {"mse": 0.485, "mae": 0.4702}, "720": {"mse": 0.524, "mae": 0.5071}}, "lstm": {"96": {"mse": 0.4097, "mae": 0.4317}, "192": {"mse": 0.4541, "mae": 0.4557}, "336": {"mse": 0.4603, "mae": 0.4608}, "720": {"mse": 0.5274, "mae": 0.5129}}}, "published": {"Informer (2021)": {"96": {"mse": 0.865, "mae": 0.713}, "192": {"mse": 1.008, "mae": 0.792}, "336": {"mse": 1.107, "mae": 0.809}, "720": {"mse": 1.181, "mae": 0.865}}, "Autoformer (2021)": {"96": {"mse": 0.449, "mae": 0.459}, "192": {"mse": 0.5, "mae": 0.482}, "336": {"mse": 0.521, "mae": 0.496}, "720": {"mse": 0.514, "mae": 0.512}}, "FEDformer (2022)": {"96": {"mse": 0.376, "mae": 0.419}, "192": {"mse": 0.42, "mae": 0.448}, "336": {"mse": 0.459, "mae": 0.465}, "720": {"mse": 0.506, "mae": 0.507}}, "DLinear (paper)": {"96": {"mse": 0.375, "mae": 0.399}, "192": {"mse": 0.405, "mae": 0.416}, "336": {"mse": 0.439, "mae": 0.443}, "720": {"mse": 0.472, "mae": 0.49}}, "NLinear (paper)": {"96": {"mse": 0.374, "mae": 0.394}, "192": {"mse": 0.408, "mae": 0.415}, "336": {"mse": 0.429, "mae": 0.427}, "720": {"mse": 0.44, "mae": 0.453}}}, "ot_mae_celsius_h96": {"model": "tcn", "mae_c": 1.575}}}
shim.js ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* tinycast static-Space engine.
2
+ *
3
+ * In the browser it overrides window.fetch for the app's own /api/* routes and
4
+ * answers them client-side: weights come from weights.bin/meta.json in the
5
+ * Space, the raw ETTh1 CSV comes straight from GitHub (CORS: *), and the
6
+ * linear models run as plain matrix-vector products below. Under Node it just
7
+ * exports the math so the Python test suite can check JS/PyTorch parity.
8
+ */
9
+ (function (root) {
10
+ "use strict";
11
+
12
+ // ---------- pure forecasting math (mirrors tinycast/models.py) ----------
13
+
14
+ function movingAverage(col, kernel) {
15
+ const L = col.length;
16
+ const left = Math.floor(kernel / 2);
17
+ const right = kernel - 1 - left;
18
+ const padded = new Float64Array(L + kernel - 1);
19
+ for (let i = 0; i < left; i++) padded[i] = col[0];
20
+ for (let i = 0; i < L; i++) padded[left + i] = col[i];
21
+ for (let i = 0; i < right; i++) padded[left + L + i] = col[L - 1];
22
+ const out = new Float64Array(L);
23
+ let sum = 0;
24
+ for (let i = 0; i < kernel; i++) sum += padded[i];
25
+ out[0] = sum / kernel;
26
+ for (let i = 1; i < L; i++) {
27
+ sum += padded[i + kernel - 1] - padded[i - 1];
28
+ out[i] = sum / kernel;
29
+ }
30
+ return out;
31
+ }
32
+
33
+ function matVec(w, rows, cols, v, b) {
34
+ const out = new Float64Array(rows);
35
+ for (let r = 0; r < rows; r++) {
36
+ let s = b ? b[r] : 0;
37
+ const off = r * cols;
38
+ for (let c = 0; c < cols; c++) s += w[off + c] * v[c];
39
+ out[r] = s;
40
+ }
41
+ return out;
42
+ }
43
+
44
+ function nlinearForward(t, col, H) {
45
+ const L = col.length;
46
+ const last = col[L - 1];
47
+ const centered = new Float64Array(L);
48
+ for (let i = 0; i < L; i++) centered[i] = col[i] - last;
49
+ const out = matVec(t.w.data, H, L, centered, t.b.data);
50
+ for (let h = 0; h < H; h++) out[h] += last;
51
+ return out;
52
+ }
53
+
54
+ function dlinearForward(t, col, H, kernel) {
55
+ const L = col.length;
56
+ const trend = movingAverage(col, kernel);
57
+ const seasonal = new Float64Array(L);
58
+ for (let i = 0; i < L; i++) seasonal[i] = col[i] - trend[i];
59
+ const out = matVec(t.wt.data, H, L, trend, t.bt.data);
60
+ const s = matVec(t.ws.data, H, L, seasonal, t.bs.data);
61
+ for (let h = 0; h < H; h++) out[h] += s[h];
62
+ return out;
63
+ }
64
+
65
+ function persistence(col, H) {
66
+ return new Float64Array(H).fill(col[col.length - 1]);
67
+ }
68
+
69
+ function seasonalNaive(col, H, period) {
70
+ period = period || 24;
71
+ const out = new Float64Array(H);
72
+ const start = col.length - period;
73
+ for (let h = 0; h < H; h++) out[h] = col[start + (h % period)];
74
+ return out;
75
+ }
76
+
77
+ const math = { movingAverage, matVec, nlinearForward, dlinearForward, persistence, seasonalNaive };
78
+
79
+ if (typeof module !== "undefined" && module.exports) {
80
+ module.exports = math; // Node: parity tests only
81
+ return;
82
+ }
83
+
84
+ // ---------- browser: data loading + fetch override ----------
85
+
86
+ const realFetch = root.fetch.bind(root);
87
+ let enginePromise = null;
88
+
89
+ async function loadEngine() {
90
+ const meta = await (await realFetch("meta.json")).json();
91
+ const buf = await (await realFetch("weights.bin")).arrayBuffer();
92
+ const tensors = {};
93
+ for (const [model, parts] of Object.entries(meta.tensors)) {
94
+ tensors[model] = {};
95
+ for (const [name, t] of Object.entries(parts)) {
96
+ const size = t.shape.reduce((a, b) => a * b, 1);
97
+ tensors[model][name] = { data: new Float32Array(buf, t.offset * 4, size), shape: t.shape };
98
+ }
99
+ }
100
+ const csv = await (await realFetch(meta.data_url)).text();
101
+ const lines = csv.trim().split("\n");
102
+ const header = lines[0].split(",");
103
+ const otCol = header.indexOf("OT");
104
+ const mean = meta.scaler_mean[meta.target_index];
105
+ const std = meta.scaler_std[meta.target_index];
106
+ const dates = [];
107
+ const ot = new Float64Array(meta.test_end_row - meta.test_start_row);
108
+ for (let r = meta.test_start_row; r < meta.test_end_row; r++) {
109
+ const cells = lines[r + 1].split(",");
110
+ dates.push(cells[0]);
111
+ ot[r - meta.test_start_row] = (parseFloat(cells[otCol]) - mean) / std;
112
+ }
113
+ return { meta, tensors, ot, dates, mean, std };
114
+ }
115
+
116
+ function engine() {
117
+ if (!enginePromise) enginePromise = loadEngine();
118
+ return enginePromise;
119
+ }
120
+
121
+ async function handle(url) {
122
+ const e = await engine();
123
+ const { meta } = e;
124
+ const L = meta.seq_len;
125
+ const nWindows = (h) => e.ot.length - L - h + 1;
126
+
127
+ const u = new URL(url, location.href);
128
+ if (u.pathname.endsWith("/api/meta") || u.pathname.endsWith("api/meta")) {
129
+ const n = {};
130
+ for (const h of meta.horizons) n[String(h)] = nWindows(h);
131
+ return {
132
+ horizons: meta.horizons,
133
+ n_windows: n,
134
+ models: ["persistence", "seasonal_naive", "nlinear", "dlinear"],
135
+ results: meta.results,
136
+ };
137
+ }
138
+
139
+ const index = parseInt(u.searchParams.get("index") || "0", 10);
140
+ const H = parseInt(u.searchParams.get("horizon") || "96", 10);
141
+ if (!meta.horizons.includes(H)) throw new Error("horizon not exported: " + H);
142
+ if (index < 0 || index >= nWindows(H)) throw new Error("index out of range");
143
+
144
+ const col = e.ot.subarray(index, index + L);
145
+ const actual = e.ot.subarray(index + L, index + L + H);
146
+ const toC = (a) => Array.from(a, (z) => Math.round((z * e.std + e.mean) * 1000) / 1000);
147
+ const forecasts = {
148
+ persistence: persistence(col, H),
149
+ seasonal_naive: seasonalNaive(col, H),
150
+ nlinear: nlinearForward(e.tensors["nlinear_" + H], col, H),
151
+ dlinear: dlinearForward(e.tensors["dlinear_" + H], col, H, meta.moving_avg),
152
+ };
153
+ const mae = {};
154
+ for (const [k, f] of Object.entries(forecasts)) {
155
+ let s = 0;
156
+ for (let h = 0; h < H; h++) s += Math.abs(f[h] - actual[h]);
157
+ mae[k] = Math.round((s / H) * e.std * 1000) / 1000;
158
+ }
159
+ const out = {};
160
+ for (const [k, f] of Object.entries(forecasts)) out[k] = toC(f);
161
+ return {
162
+ index,
163
+ n_windows: nWindows(H),
164
+ horizon: H,
165
+ t0: e.dates[index + L],
166
+ history_ot: toC(col),
167
+ actual_ot: toC(actual),
168
+ forecasts: out,
169
+ window_mae_c: mae,
170
+ };
171
+ }
172
+
173
+ root.fetch = function (url, opts) {
174
+ const u = String(url);
175
+ if (u.startsWith("api/") || u.startsWith("/api/")) {
176
+ return handle(u).then(
177
+ (data) => new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json" } }),
178
+ (err) => new Response(String(err && err.message), { status: 500 })
179
+ );
180
+ }
181
+ return realFetch(url, opts);
182
+ };
183
+ })(typeof window !== "undefined" ? window : globalThis);
weights.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9f63b761d52d915c9c6182e1c9352bd648e71b9b53030a6fea9186dddddd9684
3
+ size 1747008