Download shim.js from usmar/tinycast: direct link, hf CLI and curl.
- Browser
- Download file 6.51 kB
-
https://huggingface.co/spaces/usmar/tinycast/resolve/main/shim.js
- Command line
-
hf download hf://spaces/usmar/tinycast/shim.js
-
curl -L -o shim.js https://huggingface.co/spaces/usmar/tinycast/resolve/main/shim.js
6.51 kB
| /* tinycast static-Space engine. | |
| * | |
| * In the browser it overrides window.fetch for the app's own /api/* routes and | |
| * answers them client-side: weights come from weights.bin/meta.json in the | |
| * Space, the raw ETTh1 CSV comes straight from GitHub (CORS: *), and the | |
| * linear models run as plain matrix-vector products below. Under Node it just | |
| * exports the math so the Python test suite can check JS/PyTorch parity. | |
| */ | |
| (function (root) { | |
| "use strict"; | |
| // ---------- pure forecasting math (mirrors tinycast/models.py) ---------- | |
| function movingAverage(col, kernel) { | |
| const L = col.length; | |
| const left = Math.floor(kernel / 2); | |
| const right = kernel - 1 - left; | |
| const padded = new Float64Array(L + kernel - 1); | |
| for (let i = 0; i < left; i++) padded[i] = col[0]; | |
| for (let i = 0; i < L; i++) padded[left + i] = col[i]; | |
| for (let i = 0; i < right; i++) padded[left + L + i] = col[L - 1]; | |
| const out = new Float64Array(L); | |
| let sum = 0; | |
| for (let i = 0; i < kernel; i++) sum += padded[i]; | |
| out[0] = sum / kernel; | |
| for (let i = 1; i < L; i++) { | |
| sum += padded[i + kernel - 1] - padded[i - 1]; | |
| out[i] = sum / kernel; | |
| } | |
| return out; | |
| } | |
| function matVec(w, rows, cols, v, b) { | |
| const out = new Float64Array(rows); | |
| for (let r = 0; r < rows; r++) { | |
| let s = b ? b[r] : 0; | |
| const off = r * cols; | |
| for (let c = 0; c < cols; c++) s += w[off + c] * v[c]; | |
| out[r] = s; | |
| } | |
| return out; | |
| } | |
| function nlinearForward(t, col, H) { | |
| const L = col.length; | |
| const last = col[L - 1]; | |
| const centered = new Float64Array(L); | |
| for (let i = 0; i < L; i++) centered[i] = col[i] - last; | |
| const out = matVec(t.w.data, H, L, centered, t.b.data); | |
| for (let h = 0; h < H; h++) out[h] += last; | |
| return out; | |
| } | |
| function dlinearForward(t, col, H, kernel) { | |
| const L = col.length; | |
| const trend = movingAverage(col, kernel); | |
| const seasonal = new Float64Array(L); | |
| for (let i = 0; i < L; i++) seasonal[i] = col[i] - trend[i]; | |
| const out = matVec(t.wt.data, H, L, trend, t.bt.data); | |
| const s = matVec(t.ws.data, H, L, seasonal, t.bs.data); | |
| for (let h = 0; h < H; h++) out[h] += s[h]; | |
| return out; | |
| } | |
| function persistence(col, H) { | |
| return new Float64Array(H).fill(col[col.length - 1]); | |
| } | |
| function seasonalNaive(col, H, period) { | |
| period = period || 24; | |
| const out = new Float64Array(H); | |
| const start = col.length - period; | |
| for (let h = 0; h < H; h++) out[h] = col[start + (h % period)]; | |
| return out; | |
| } | |
| const math = { movingAverage, matVec, nlinearForward, dlinearForward, persistence, seasonalNaive }; | |
| if (typeof module !== "undefined" && module.exports) { | |
| module.exports = math; // Node: parity tests only | |
| return; | |
| } | |
| // ---------- browser: data loading + fetch override ---------- | |
| const realFetch = root.fetch.bind(root); | |
| let enginePromise = null; | |
| async function loadEngine() { | |
| const meta = await (await realFetch("meta.json")).json(); | |
| const buf = await (await realFetch("weights.bin")).arrayBuffer(); | |
| const tensors = {}; | |
| for (const [model, parts] of Object.entries(meta.tensors)) { | |
| tensors[model] = {}; | |
| for (const [name, t] of Object.entries(parts)) { | |
| const size = t.shape.reduce((a, b) => a * b, 1); | |
| tensors[model][name] = { data: new Float32Array(buf, t.offset * 4, size), shape: t.shape }; | |
| } | |
| } | |
| const csv = await (await realFetch(meta.data_url)).text(); | |
| const lines = csv.trim().split("\n"); | |
| const header = lines[0].split(","); | |
| const otCol = header.indexOf("OT"); | |
| const mean = meta.scaler_mean[meta.target_index]; | |
| const std = meta.scaler_std[meta.target_index]; | |
| const dates = []; | |
| const ot = new Float64Array(meta.test_end_row - meta.test_start_row); | |
| for (let r = meta.test_start_row; r < meta.test_end_row; r++) { | |
| const cells = lines[r + 1].split(","); | |
| dates.push(cells[0]); | |
| ot[r - meta.test_start_row] = (parseFloat(cells[otCol]) - mean) / std; | |
| } | |
| return { meta, tensors, ot, dates, mean, std }; | |
| } | |
| function engine() { | |
| if (!enginePromise) enginePromise = loadEngine(); | |
| return enginePromise; | |
| } | |
| async function handle(url) { | |
| const e = await engine(); | |
| const { meta } = e; | |
| const L = meta.seq_len; | |
| const nWindows = (h) => e.ot.length - L - h + 1; | |
| const u = new URL(url, location.href); | |
| if (u.pathname.endsWith("/api/meta") || u.pathname.endsWith("api/meta")) { | |
| const n = {}; | |
| for (const h of meta.horizons) n[String(h)] = nWindows(h); | |
| return { | |
| horizons: meta.horizons, | |
| n_windows: n, | |
| models: ["persistence", "seasonal_naive", "nlinear", "dlinear"], | |
| results: meta.results, | |
| }; | |
| } | |
| const index = parseInt(u.searchParams.get("index") || "0", 10); | |
| const H = parseInt(u.searchParams.get("horizon") || "96", 10); | |
| if (!meta.horizons.includes(H)) throw new Error("horizon not exported: " + H); | |
| if (index < 0 || index >= nWindows(H)) throw new Error("index out of range"); | |
| const col = e.ot.subarray(index, index + L); | |
| const actual = e.ot.subarray(index + L, index + L + H); | |
| const toC = (a) => Array.from(a, (z) => Math.round((z * e.std + e.mean) * 1000) / 1000); | |
| const forecasts = { | |
| persistence: persistence(col, H), | |
| seasonal_naive: seasonalNaive(col, H), | |
| nlinear: nlinearForward(e.tensors["nlinear_" + H], col, H), | |
| dlinear: dlinearForward(e.tensors["dlinear_" + H], col, H, meta.moving_avg), | |
| }; | |
| const mae = {}; | |
| for (const [k, f] of Object.entries(forecasts)) { | |
| let s = 0; | |
| for (let h = 0; h < H; h++) s += Math.abs(f[h] - actual[h]); | |
| mae[k] = Math.round((s / H) * e.std * 1000) / 1000; | |
| } | |
| const out = {}; | |
| for (const [k, f] of Object.entries(forecasts)) out[k] = toC(f); | |
| return { | |
| index, | |
| n_windows: nWindows(H), | |
| horizon: H, | |
| t0: e.dates[index + L], | |
| history_ot: toC(col), | |
| actual_ot: toC(actual), | |
| forecasts: out, | |
| window_mae_c: mae, | |
| }; | |
| } | |
| root.fetch = function (url, opts) { | |
| const u = String(url); | |
| if (u.startsWith("api/") || u.startsWith("/api/")) { | |
| return handle(u).then( | |
| (data) => new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json" } }), | |
| (err) => new Response(String(err && err.message), { status: 500 }) | |
| ); | |
| } | |
| return realFetch(url, opts); | |
| }; | |
| })(typeof window !== "undefined" ? window : globalThis); | |