diff --git a/packages/app/src/amicode/inspector/device-inspector.tsx b/packages/app/src/amicode/inspector/device-inspector.tsx new file mode 100644 index 000000000..68ef8d656 --- /dev/null +++ b/packages/app/src/amicode/inspector/device-inspector.tsx @@ -0,0 +1,123 @@ +import { For, Show, createMemo } from "solid-js" +import type { createInspectorBridge } from "./inspector-bridge" + +type Props = { bridge: ReturnType } + +function isRecord(v: unknown): v is Record { + return typeof v === "object" && v !== null +} + +export function DeviceInspector(props: Props) { + const devices = () => Array.from(props.bridge.devices().entries()) + const active = createMemo(() => { + const id = props.bridge.activeDevice() + if (id && props.bridge.devices().has(id)) return { id, data: props.bridge.devices().get(id)! } + const first = devices()[0] + return first ? { id: first[0], data: first[1] } : undefined + }) + + const refresh = (device: string) => { + window.parent.postMessage({ source: "amicode", kind: "device:refresh", device }, "*") + // also try direct parent (when not in deck pane iframe indirection the host still listens) + window.postMessage({ source: "amicode", kind: "device:refresh", device }, "*") + } + + return ( +
+
+
Device Inspector
+ 1}> + + +
+ + +
+
{active()!.id}
+ +
+ + ).driveLines}> + {(dl) => ( +
+
Drive lines
+
+ + {(d) => ( + + {d.id} {d.online ? "●" : "○"} + + )} + +
+
+ )} +
+ + ).qubits}> +
+
Qubits
+
+ + {(q) => {q.qubit}: {q.status}} + +
+
+
+ + ).metrics}> +
+
Metrics
+ }).metrics)}> + {([k, m]) => ( +
+ {k} + + {m.value.toFixed(4)} · {m.status} · {Math.round(m.ageSeconds)}s ago + +
+ )} +
+
+
+ + ).calibrationParams}> +
+
Calibration params
+
+ {JSON.stringify((active()!.data.status as { calibrationParams: unknown }).calibrationParams, null, 2)} +
+
+
+ + 0}> +
+
Recommended actions
+ + {(a) => ( +
+
{a.node} → {a.action} {a.locked ? "🔒" : ""}
+ +
{a.reason}
+
+
+ )} +
+
+
+
+ }> +
No device — configure one in settings.
+ + + ) +} diff --git a/packages/app/src/amicode/inspector/inspector-bridge.ts b/packages/app/src/amicode/inspector/inspector-bridge.ts new file mode 100644 index 000000000..736b0be2c --- /dev/null +++ b/packages/app/src/amicode/inspector/inspector-bridge.ts @@ -0,0 +1,122 @@ +import { createSignal, onCleanup } from "solid-js" + +export type RunIteration = { runId: string; iter: number; objective: number; inf_pr: number; inf_du: number } +export type RunPulseMeta = { runId: string; drives: number; knots: number; labels: string[]; bounds: [number, number][]; interp?: string } +export type RunPulse = { runId: string; iter: number; dt: number; values: number[][] } +export type RunCompletion = { runId: string; fidelity: number; iterations: number; status: string } +export type RunBridgeMessage = + | { type: "run:iteration"; runId: string; iter: number; objective: number; inf_pr: number; inf_du: number } + | { type: "run:pulse-meta"; runId: string; drives: number; knots: number; labels: string[]; bounds: [number, number][]; interp?: string } + | { type: "run:pulse"; runId: string; iter: number; dt: number; values: number[][] } + | { type: "run:completion"; runId: string; fidelity: number; iterations: number; status: string } + | { type: "run:activate"; runId: string } + | { type: "run:timing"; runId: string; elapsed: number } + | { type: "run:label"; runId: string; label: string } + +export type DeviceBridgeMessage = + | { type: "device:status"; device: string; status: unknown } + | { type: "device:actions"; device: string; actions: unknown[] } + | { type: "device:activate"; device: string } + +export type InspectorMessage = RunBridgeMessage | DeviceBridgeMessage + +type RunState = { + label?: string + iterations: RunIteration[] + pulseMeta?: RunPulseMeta + pulses: RunPulse[] + completion?: RunCompletion + timing?: number +} + +export function createInspectorBridge() { + const [runs, setRuns] = createSignal>(new Map()) + const [activeRunId, setActiveRunId] = createSignal(undefined) + const [devices, setDevices] = createSignal>(new Map()) + const [activeDevice, setActiveDevice] = createSignal(undefined) + + const onMessage = (e: MessageEvent) => { + const d = e.data as InspectorMessage & { source?: string } + if (!d || d.source !== "amicode") return + switch (d.type) { + case "run:iteration": { + setRuns((m) => { + const n = new Map(m) + const s = n.get(d.runId) ?? { iterations: [], pulses: [] } + n.set(d.runId, { ...s, iterations: [...s.iterations, { runId: d.runId, iter: d.iter, objective: d.objective, inf_pr: d.inf_pr, inf_du: d.inf_du }] }) + return n + }) + if (!activeRunId()) setActiveRunId(d.runId) + break + } + case "run:pulse-meta": + setRuns((m) => { + const n = new Map(m) + const s = n.get(d.runId) ?? { iterations: [], pulses: [] } + n.set(d.runId, { ...s, pulseMeta: d }) + return n + }) + break + case "run:pulse": + setRuns((m) => { + const n = new Map(m) + const s = n.get(d.runId) ?? { iterations: [], pulses: [] } + n.set(d.runId, { ...s, pulses: [...s.pulses.slice(-200), d] }) + return n + }) + break + case "run:completion": + setRuns((m) => { + const n = new Map(m) + const s = n.get(d.runId) ?? { iterations: [], pulses: [] } + n.set(d.runId, { ...s, completion: d }) + return n + }) + break + case "run:activate": + setActiveRunId(d.runId) + break + case "run:label": + setRuns((m) => { + const n = new Map(m) + const s = n.get(d.runId) ?? { iterations: [], pulses: [] } + n.set(d.runId, { ...s, label: d.label }) + return n + }) + break + case "run:timing": + setRuns((m) => { + const n = new Map(m) + const s = n.get(d.runId) ?? { iterations: [], pulses: [] } + n.set(d.runId, { ...s, timing: d.elapsed }) + return n + }) + break + case "device:status": + setDevices((m) => { + const n = new Map(m) + const cur = n.get(d.device) ?? { status: undefined, actions: [] } + n.set(d.device, { ...cur, status: d.status }) + return n + }) + if (!activeDevice()) setActiveDevice(d.device) + break + case "device:actions": + setDevices((m) => { + const n = new Map(m) + const cur = n.get(d.device) ?? { status: undefined, actions: [] } + n.set(d.device, { ...cur, actions: d.actions }) + return n + }) + break + case "device:activate": + setActiveDevice(d.device) + break + } + } + + window.addEventListener("message", onMessage) + onCleanup(() => window.removeEventListener("message", onMessage)) + + return { runs, activeRunId, setActiveRunId, devices, activeDevice, setActiveDevice } +} diff --git a/packages/app/src/amicode/inspector/run-inspector.tsx b/packages/app/src/amicode/inspector/run-inspector.tsx new file mode 100644 index 000000000..030567179 --- /dev/null +++ b/packages/app/src/amicode/inspector/run-inspector.tsx @@ -0,0 +1,108 @@ +import { For, Show, createMemo } from "solid-js" +import type { createInspectorBridge } from "./inspector-bridge" + +type Props = { bridge: ReturnType } + +// Minimal pulse sparkline — renders correctly at 320px column width and responds +// to resize via viewBox (no fixed pixel width). Each drive is a polyline. +function PulseChart(props: { values: number[][]; bounds?: [number, number][] }) { + const paths = createMemo(() => { + const v = props.values + if (v.length === 0 || v[0].length === 0) return [] + return v.map((drive, idx) => { + const b = props.bounds?.[idx] + const lo = b?.[0] ?? Math.min(...drive) + const hi = b?.[1] ?? Math.max(...drive) + const range = hi - lo || 1 + const pts = drive.map((val, i) => { + const x = (i / (drive.length - 1)) * 100 + const y = 50 - ((val - lo) / range) * 40 - 5 + return `${x},${y}` + }) + return { d: `M ${pts.join(" L ")}`, color: idx % 2 === 0 ? "var(--amico-accent, #eab308)" : "var(--amico-accent-2, #38bdf8)" } + }) + }) + return ( + + {(p) => } + + ) +} + +export function RunInspector(props: Props) { + const runs = () => Array.from(props.bridge.runs().entries()) + const active = createMemo(() => { + const id = props.bridge.activeRunId() + if (id && props.bridge.runs().has(id)) return { id, state: props.bridge.runs().get(id)! } + const first = runs()[0] + return first ? { id: first[0], state: first[1] } : undefined + }) + const latestIter = createMemo(() => active()?.state.iterations.at(-1)) + const latestPulse = createMemo(() => active()?.state.pulses.at(-1)) + + return ( +
+
+
Run Inspector
+ 1}> + + +
+ + +
+ {active()!.state.label ?? active()!.id} +
+ + {(meta) => ( +
+ {meta().drives} drive(s) · {meta().knots} knots · {meta().labels.join(", ")} +
+ )} +
+ {(p) => } + +
Waiting for pulse data…
+
+
+
+
iter
+
{latestIter()?.iter ?? "—"}
+
+
+
objective
+
{latestIter() ? latestIter()!.objective.toExponential(2) : "—"}
+
+
+
inf
+
{latestIter() ? `${latestIter()!.inf_pr.toExponential(1)}/${latestIter()!.inf_du.toExponential(1)}` : "—"}
+
+
+ + {(c) => ( +
+
completion
+
+ {c().status} · F={c().fidelity.toFixed(5)} · {c().iterations} iters + · {active()!.state.timing!.toFixed(1)}s +
+
+ )} +
+ +
running · iter {latestIter()!.iter}
+
+
+ }> +
No solve yet — launch one from the chat.
+ + + ) +}