ptis / clear.js
Asmitha-28's picture
Harden PTIS reviewer boundaries
1aa5b7f verified
Raw
History Blame Contribute Delete
16.2 kB
const DATA = {
latest: "./evidence/latest_run.json",
suite: "./evidence/suite_report.json",
extreme: "./evidence/extreme_batch_report.json",
cctv: "./evidence/cctv_bmd45_report.json",
route: "./data/orr_silk_board_whitefield_route_osrm.geojson",
official: "./data/official_reference_bengaluru_cmp_2020.json"
};
const fmt = new Intl.NumberFormat("en-IN");
const pct = value => `${Math.round(Number(value || 0) * 100)}%`;
const km = metres => `${(Number(metres || 0) / 1000).toFixed(1)} km`;
const q = selector => document.querySelector(selector);
const setText = (selector, value) => {
const node = q(selector);
if (node) node.textContent = value;
};
async function loadJson(url) {
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) throw new Error(`${url} returned ${response.status}`);
const text = await response.text();
return JSON.parse(text.replace(/^\uFEFF/, ""));
}
function findAssertion(report, name) {
return (report.assertions || []).find(item => item.name === name) || { details: {} };
}
function label(id, fallback = "checkpoint") {
return String(id || fallback)
.replace(/_/g, " ")
.replace(/\b\w/g, char => char.toUpperCase());
}
function chooseDecision(latest, extreme) {
const cycles = latest.cycles || [];
const activatedCycle = cycles.find(cycle => (cycle.decisions || []).some(decision => decision.activate)) || cycles[cycles.length - 1] || {};
const cycleDecision = (activatedCycle.decisions || []).find(decision => decision.activate) || (activatedCycle.decisions || [])[0];
const aggregateDecision = ((extreme.aggregate_decisions || [])[0] || {}).decision;
const decision = cycleDecision || aggregateDecision || {};
return {
cycle: activatedCycle,
decision,
checkpoint: activatedCycle.observation?.junction_id || "marathahalli",
destination: activatedCycle.best_destination_id || decision.destination_id || "whitefield",
confidence: activatedCycle.best_confidence || decision.confidence || 0,
activate: Boolean(decision.activate)
};
}
function renderNumbers({ latest, suite, extreme, cctv, route, official }) {
const chosen = chooseDecision(latest, extreme);
const stress = extreme.metrics || {};
const images = findAssertion(cctv, "metadata_rows_match_coco_images").details.coco_images || 0;
const annotations = findAssertion(cctv, "coco_annotations_present").details.annotation_count || 0;
const silkBoard = (official.junction_counts || []).find(item => item.name === "Silk Board Junction") || {};
const doddanakundi = (official.junction_counts || []).find(item => item.name === "Doddanakundi Junction") || {};
const confidenceText = pct(chosen.confidence);
const routePointText = fmt.format(route.coordinate_count || 0);
setText("#route-distance", `${km(route.distance_m)} / ${routePointText} route points`);
setText("#belief-percent", confidenceText);
setText("#belief-destination", label(chosen.destination));
const bar = q("#belief-bar");
if (bar) bar.style.width = `${Math.max(0, Math.min(100, Math.round(Number(chosen.confidence || 0) * 100)))}%`;
setText(
"#hero-action",
chosen.activate
? `At ${label(chosen.checkpoint)}, PTIS increases confidence toward ${label(chosen.destination)} based on continued route movement.`
: `At ${label(chosen.checkpoint)}, PTIS keeps the capacity gate on standby.`
);
setText("#route-points-metric", routePointText);
setText("#events-metric", fmt.format(stress.observation_count || 0));
setText("#confidence-metric", confidenceText);
setText("#safety-metric", fmt.format(stress.capacity_violation_count || 0));
const calibrationError = Number(stress.mean_synthetic_od_abs_rate_error || 0) * 100;
setText("#synthetic-calibration", `Synthetic OD calibration rate error: ${calibrationError.toFixed(2)}% in stress replay. This is synthetic calibration, not field OD accuracy.`);
setText("#suite-pass", `${fmt.format(suite.passed_count || 0)}/${fmt.format(suite.scenario_count || 0)}`);
setText("#stress-load", fmt.format(stress.vehicle_count || 0));
setText("#observation-count", fmt.format(stress.observation_count || 0));
setText("#safety-count", fmt.format(stress.capacity_violation_count || 0));
setText("#cctv-count", `${fmt.format(images)} / ${fmt.format(annotations)}`);
setText("#official-count", `${fmt.format(silkBoard.peak_hour_pcu || 0)} / ${fmt.format(doddanakundi.peak_hour_pcu || 0)} PCU`);
}
function routeCoordinates(route) {
return (((route.features || [])[0] || {}).geometry || {}).coordinates || [];
}
function boundsFor(coords, waypoints) {
const all = coords.concat((waypoints || []).map(point => [Number(point.lon), Number(point.lat)]));
const lons = all.map(point => Number(point[0]));
const lats = all.map(point => Number(point[1]));
return {
minLon: Math.min(...lons),
maxLon: Math.max(...lons),
minLat: Math.min(...lats),
maxLat: Math.max(...lats)
};
}
function renderFallback(route, latest, extreme) {
const svg = q("#fallback-map");
const coords = routeCoordinates(route);
if (!svg || coords.length === 0) return;
const chosen = chooseDecision(latest, extreme);
const width = 980;
const height = 620;
const pad = 64;
const bounds = boundsFor(coords, route.waypoints || []);
const scaleX = (width - pad * 2) / Math.max(0.000001, bounds.maxLon - bounds.minLon);
const scaleY = (height - pad * 2) / Math.max(0.000001, bounds.maxLat - bounds.minLat);
const scale = Math.min(scaleX, scaleY);
const offsetX = (width - (bounds.maxLon - bounds.minLon) * scale) / 2;
const offsetY = (height - (bounds.maxLat - bounds.minLat) * scale) / 2;
const project = coord => {
const lon = Number(Array.isArray(coord) ? coord[0] : coord.lon);
const lat = Number(Array.isArray(coord) ? coord[1] : coord.lat);
return {
x: offsetX + (lon - bounds.minLon) * scale,
y: height - offsetY - (lat - bounds.minLat) * scale
};
};
const d = coords.map((coord, index) => {
const point = project(coord);
return `${index === 0 ? "M" : "L"}${point.x.toFixed(1)} ${point.y.toFixed(1)}`;
}).join(" ");
const nodes = (route.waypoints || []).map(point => {
const projected = project(point);
const className = ["fallback-node"];
if (point.id === chosen.checkpoint) className.push("active");
if (point.id === chosen.destination) className.push("target");
const anchor = point.order > 4 ? "end" : "start";
const dx = anchor === "end" ? -12 : 12;
return `
<circle class="${className.join(" ")}" cx="${projected.x.toFixed(1)}" cy="${projected.y.toFixed(1)}" r="8"></circle>
<text class="fallback-label" x="${(projected.x + dx).toFixed(1)}" y="${(projected.y - 12).toFixed(1)}" text-anchor="${anchor}">${point.name}</text>
`;
}).join("");
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
svg.innerHTML = `
<path class="fallback-route-shadow" d="${d}"></path>
<path class="fallback-route-line" d="${d}"></path>
${nodes}
`;
}
function initMapLibre(route, latest, extreme) {
const frame = q(".map-frame");
const coords = routeCoordinates(route);
if (!frame || !window.maplibregl || coords.length === 0) return;
const chosen = chooseDecision(latest, extreme);
const routeGeoJson = {
type: "FeatureCollection",
features: [{
type: "Feature",
properties: { name: "Silk Board to Whitefield" },
geometry: { type: "LineString", coordinates: coords }
}]
};
const pointGeoJson = {
type: "FeatureCollection",
features: (route.waypoints || []).map(point => ({
type: "Feature",
properties: {
id: point.id,
name: point.name,
state: point.id === chosen.destination ? "target" : (point.id === chosen.checkpoint ? "checkpoint" : "route")
},
geometry: { type: "Point", coordinates: [Number(point.lon), Number(point.lat)] }
}))
};
const bounds = boundsFor(coords, route.waypoints || []);
const map = new maplibregl.Map({
container: "map",
style: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
center: [(bounds.minLon + bounds.maxLon) / 2, (bounds.minLat + bounds.maxLat) / 2],
zoom: 10.7,
pitch: 0,
bearing: 0,
attributionControl: true
});
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "top-right");
map.dragRotate.disable();
map.touchZoomRotate.disableRotation();
map.on("load", () => {
map.addSource("ptis-route", { type: "geojson", data: routeGeoJson });
map.addLayer({
id: "ptis-route-shadow",
type: "line",
source: "ptis-route",
paint: { "line-color": "#0f172a", "line-width": 13, "line-opacity": .18, "line-blur": 1.4 }
});
map.addLayer({
id: "ptis-route-line",
type: "line",
source: "ptis-route",
paint: { "line-color": "#13c8bd", "line-width": 6, "line-opacity": .98 }
});
map.addSource("ptis-points", { type: "geojson", data: pointGeoJson });
map.addLayer({
id: "ptis-point-halo",
type: "circle",
source: "ptis-points",
paint: {
"circle-radius": ["match", ["get", "state"], "target", 18, "checkpoint", 16, 11],
"circle-color": "#ffffff",
"circle-opacity": .95,
"circle-stroke-color": "#cbd5e1",
"circle-stroke-width": 1
}
});
map.addLayer({
id: "ptis-point-dot",
type: "circle",
source: "ptis-points",
paint: {
"circle-radius": ["match", ["get", "state"], "target", 10, "checkpoint", 9, 6],
"circle-color": ["match", ["get", "state"], "target", "#13c8bd", "checkpoint", "#13c8bd", "#071739"],
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 2
}
});
map.addLayer({
id: "ptis-point-label",
type: "symbol",
source: "ptis-points",
layout: {
"text-field": ["get", "name"],
"text-size": 12,
"text-offset": [0, 1.8],
"text-anchor": "top",
"text-allow-overlap": false
},
paint: { "text-color": "#0f172a", "text-halo-color": "#ffffff", "text-halo-width": 1.5 }
});
map.fitBounds([[bounds.minLon, bounds.minLat], [bounds.maxLon, bounds.maxLat]], {
padding: { top: 74, right: 62, bottom: 98, left: 62 },
duration: 0,
bearing: 0,
pitch: 0
});
frame.classList.add("map-ready");
});
}
const WALK_STEPS = [
{
code: "SB",
label: "Silk Board",
confidence: 18,
title: "Vehicle enters at Silk Board",
body: "PTIS starts from a prior belief. Six possible destinations remain, so the system observes without acting."
},
{
code: "HSR",
label: "HSR Layout",
confidence: 22,
title: "Passed Silk Board without turning",
body: "The vehicle did not exit immediately. One local possibility becomes less likely, and Whitefield belief rises to 22%."
},
{
code: "SW",
label: "Sony World",
confidence: 34,
title: "Passed HSR Layout without turning",
body: "Another junction is eliminated from the destination set. PTIS now has stronger evidence that the flow is continuing downstream."
},
{
code: "MH",
label: "Marathahalli",
confidence: 67,
title: "Belief crosses the action threshold",
body: "At Marathahalli, Whitefield belief reaches 67%. PTIS marks the connector decision as eligible, then checks receiving-road capacity before any command is allowed.",
eligible: true
},
{
code: "DK",
label: "Doddanekkundi",
confidence: 82,
title: "Downstream evidence keeps increasing",
body: "Continued movement after Marathahalli strengthens the destination belief. The capacity gate still prevents over-commanding the receiving road."
},
{
code: "WF",
label: "Whitefield",
confidence: 100,
title: "Replay destination observed",
body: "In this software replay, the trace reaches Whitefield. That confirms the replay path for this example, not a live field-deployment result.",
final: true
}
];
let walkIndex = 0;
function renderWalkthrough(route) {
const track = q("#walk-track");
if (!track) return;
const routeKm = route?.distance_m ? km(route.distance_m) : "route grounded";
setText("#walk-route-summary", `6 junctions / ${routeKm}`);
track.innerHTML = WALK_STEPS.map((step, index) => `
<button class="walk-node" type="button" role="tab" data-walk-index="${index}" aria-selected="false">
<span class="walk-node-dot"><span class="walk-node-code">${step.code}</span></span>
<span class="walk-node-label">${step.label}</span>
<span class="walk-node-prob"></span>
</button>
`).join("");
track.addEventListener("click", event => {
const button = event.target.closest("[data-walk-index]");
if (!button) return;
walkIndex = Number(button.dataset.walkIndex || 0);
updateWalkthrough();
});
q("#walk-prev")?.addEventListener("click", () => {
walkIndex = Math.max(0, walkIndex - 1);
updateWalkthrough();
});
q("#walk-next")?.addEventListener("click", () => {
walkIndex = Math.min(WALK_STEPS.length - 1, walkIndex + 1);
updateWalkthrough();
});
q("#walk-reset")?.addEventListener("click", () => {
walkIndex = 0;
updateWalkthrough();
});
updateWalkthrough();
}
function updateWalkthrough() {
const step = WALK_STEPS[walkIndex] || WALK_STEPS[0];
const track = q("#walk-track");
if (track) {
const progress = WALK_STEPS.length <= 1 ? 0 : (walkIndex / (WALK_STEPS.length - 1)) * 100;
track.style.setProperty("--progress-ratio", String(progress / 100));
track.querySelectorAll(".walk-node").forEach((node, index) => {
const item = WALK_STEPS[index];
node.classList.toggle("is-passed", index < walkIndex);
node.classList.toggle("is-active", index === walkIndex && !item.eligible && !item.final);
node.classList.toggle("is-eligible", index === walkIndex && Boolean(item.eligible));
node.classList.toggle("is-final", index === walkIndex && Boolean(item.final));
node.setAttribute("aria-selected", String(index === walkIndex));
const prob = node.querySelector(".walk-node-prob");
if (prob) prob.textContent = index < walkIndex ? `${item.confidence}%` : "";
});
}
setText("#walk-confidence", `${step.confidence}%`);
const bar = q("#walk-progress-bar");
if (bar) bar.style.width = `${step.confidence}%`;
const progress = q(".walk-progress");
if (progress) progress.classList.toggle("is-eligible", step.confidence >= 65);
const card = q("#walk-card");
if (card) {
card.classList.toggle("is-eligible", Boolean(step.eligible));
card.classList.toggle("is-final", Boolean(step.final));
card.innerHTML = `<h3>${step.title}</h3><p>${step.body}</p>`;
}
setText("#walk-position", `Junction ${walkIndex + 1} / ${WALK_STEPS.length}`);
const prev = q("#walk-prev");
const next = q("#walk-next");
if (prev) prev.disabled = walkIndex === 0;
if (next) {
next.disabled = walkIndex === WALK_STEPS.length - 1;
next.textContent = walkIndex === WALK_STEPS.length - 2 ? "Finish replay" : "Next junction";
}
}
async function init() {
try {
const [latest, suite, extreme, cctv, route, official] = await Promise.all([
loadJson(DATA.latest),
loadJson(DATA.suite),
loadJson(DATA.extreme),
loadJson(DATA.cctv),
loadJson(DATA.route),
loadJson(DATA.official)
]);
renderNumbers({ latest, suite, extreme, cctv, route, official });
renderWalkthrough(route);
renderFallback(route, latest, extreme);
initMapLibre(route, latest, extreme);
} catch (error) {
console.error(error);
setText("#hero-action", "Could not load local evidence files. Check the frontend server path.");
setText("#route-distance", "Evidence load failed");
}
}
init();