| |
| |
| |
| |
| |
| |
| |
| |
| (function (root) { |
| "use strict"; |
|
|
| |
|
|
| 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; |
| return; |
| } |
|
|
| |
|
|
| 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); |
|
|