tinycast / shim.js
usmar's picture
Upload folder using huggingface_hub
c1295a6 verified
Raw
History Blame Contribute Delete
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);