File size: 6,508 Bytes
c1295a6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | /* 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);
|