outliar / shim.js
usmar's picture
Deploy outliar
ef95487 verified
Raw
History Blame Contribute Delete
7.4 kB
/* Static-deployment shim.
*
* Hugging Face static Spaces serve files, not Python, so the page has no
* backend. This intercepts the app's own API routes and answers them in the
* browser: NAB is fetched straight from the upstream GitHub raw endpoint
* (which sends `access-control-allow-origin: *`) and every detector and metric
* is recomputed locally by web/outliar.js — the same code the Python↔Node
* parity test pins to the NumPy implementation.
*
* Two detectors do not survive the trip and the page says so rather than
* quietly serving different numbers: `iforest` needs scikit-learn's fitted
* trees, and `random` uses NumPy's PCG64, which a JS PRNG cannot reproduce
* bit-for-bit (it is still uniform noise, which is the entire point of it).
*/
(function () {
"use strict";
const NAB = "https://raw.githubusercontent.com/numenta/NAB/master";
const REAL_CORPORA = new Set([
"realAWSCloudwatch", "realAdExchange", "realKnownCause", "realTraffic", "realTweets",
]);
const N_GRID = 120;
const cache = { labels: null, series: new Map(), scores: new Map(), sweeps: new Map() };
const SAMPLES = [
{
series: "realKnownCause/nyc_taxi.csv",
detector: "random",
note: "Uniform noise on NYC taxi demand, at its own point-adjusted optimum.",
},
{
series: "realKnownCause/machine_temperature_system_failure.csv",
detector: "window_pca",
note: "A real detector on a real machine failure — the honest case.",
},
{
series: "realAWSCloudwatch/ec2_cpu_utilization_5f5533.csv",
detector: "random",
note: "Noise again, on EC2 CPU utilisation.",
},
{
series: "realTraffic/speed_7578.csv",
detector: "seasonal",
note: "A seasonal-profile detector on highway speeds.",
},
];
async function labels() {
if (!cache.labels) {
const response = await fetch(`${NAB}/labels/combined_windows.json`);
if (!response.ok) throw new Error(`could not reach NAB (${response.status})`);
cache.labels = await response.json();
}
return cache.labels;
}
async function series(key) {
if (cache.series.has(key)) return cache.series.get(key);
const [all, response] = await Promise.all([
labels(), fetch(`${NAB}/data/${key}`),
]);
if (!response.ok) throw new Error(`could not load ${key} (${response.status})`);
const parsed = outliar.parseCsv(await response.text());
const windows = outliar.windowsToIndices(parsed.timestamps, all[key] || []);
const value = {
key,
name: key.split("/")[1].replace(/\.csv$/, ""),
corpus: key.split("/")[0],
timestamps: parsed.timestamps,
values: parsed.values,
windows,
n: parsed.values.length,
samplingMinutes: outliar.samplingMinutes(parsed.timestamps),
};
cache.series.set(key, value);
return value;
}
function scores(s, detector) {
const id = `${s.key}::${detector}`;
if (!cache.scores.has(id)) {
cache.scores.set(id, outliar.DETECTORS[detector](s, 7));
}
return cache.scores.get(id);
}
async function evaluate(key, detector, threshold) {
const s = await series(key);
const raw = scores(s, detector);
const probation = outliar.probationLength(s.n);
const scored = Array.from(raw.slice(probation));
const yTrue = outliar.pointLabels(s.n, s.windows).slice(probation);
const sweepId = `${key}::${detector}`;
if (!cache.sweeps.has(sweepId)) {
cache.sweeps.set(sweepId, outliar.sweep(yTrue, scored, N_GRID));
}
const best = cache.sweeps.get(sweepId);
const paOptimal = best.pa[0];
if (threshold === null || threshold === undefined) threshold = paOptimal;
const yPred = scored.map((v) => v >= threshold);
const protocols = outliar.allProtocols(yTrue, yPred);
const [caught, missed] = outliar.eventRecall(yTrue, yPred);
let alarms = 0, inside = 0;
for (let i = 0; i < yPred.length; i++) {
if (yPred[i]) { alarms++; if (yTrue[i]) inside++; }
}
const credited = outliar.adjustPredictions(yTrue, yPred).reduce((a, b) => a + (b ? 1 : 0), 0);
const step = Math.max(1, Math.floor(s.n / 2400));
const timestamps = [], values = [], drawn = [];
for (let i = 0; i < s.n; i += step) {
timestamps.push(s.timestamps[i]);
values.push(s.values[i]);
drawn.push(raw[i]);
}
const finite = scored.filter(Number.isFinite);
return {
series: key,
name: s.name,
detector,
is_control: outliar.CONTROLS.has(detector),
description: outliar.DESCRIPTIONS[detector],
threshold,
pa_optimal_threshold: paOptimal,
composite_optimal_threshold: best.composite[0],
score_range: [Math.min(...finite), outliar.quantile(finite, 0.999)],
probation,
n: s.n,
timestamps,
values,
scores: drawn,
windows: s.windows,
metrics: protocols,
alarms,
alarms_inside: inside,
pa_credited: credited,
windows_caught: caught,
windows_total: caught + missed,
false_alarms_per_day: outliar.falseAlarmsPerDay(yTrue, yPred, s.samplingMinutes),
};
}
async function catalog() {
const all = await labels();
const keys = Object.keys(all).filter((k) => REAL_CORPORA.has(k.split("/")[0])).sort();
return {
series: keys.map((k) => ({
key: k,
name: k.split("/")[1].replace(/\.csv$/, ""),
corpus: k.split("/")[0],
windows: all[k].length,
})),
detectors: Object.keys(outliar.DETECTORS).map((name) => ({
name,
description: outliar.DESCRIPTIONS[name],
is_control: outliar.CONTROLS.has(name),
})),
n_samples: SAMPLES.length,
};
}
function json(payload) {
return new Response(JSON.stringify(payload), {
status: 200, headers: { "content-type": "application/json" },
});
}
const original = window.fetch.bind(window);
window.fetch = async function (input, init) {
const url = typeof input === "string" ? input : input.url;
// Only the app's own routes are intercepted; NAB requests pass through.
if (!/^\/(api\/|sample|healthz)/.test(url)) return original(input, init);
const parsed = new URL(url, location.origin);
const path = parsed.pathname;
const query = parsed.searchParams;
try {
if (path === "/api/catalog") return json(await catalog());
if (path === "/healthz") return json({ status: "ok", static: true });
if (path === "/api/findings") {
const response = await original("findings.json");
if (!response.ok) throw new Error("findings.json missing");
return json(await response.json());
}
if (path === "/api/evaluate") {
const threshold = query.has("threshold") ? parseFloat(query.get("threshold")) : null;
return json(await evaluate(query.get("series"), query.get("detector"), threshold));
}
if (path === "/sample") {
const choice = SAMPLES[(parseInt(query.get("index") || "0", 10) || 0) % SAMPLES.length];
const payload = await evaluate(choice.series, choice.detector, null);
payload.note = choice.note;
return json(payload);
}
} catch (err) {
return new Response(String(err && err.message ? err.message : err), { status: 502 });
}
return new Response("not found", { status: 404 });
};
})();