Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dev/pulseplot_harness/main.js
1 change: 1 addition & 0 deletions packages/extension/.vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
src/**
test/**
scripts/**
dev/**
node_modules/**
esbuild.config.mjs
tsconfig.json
Expand Down
31 changes: 27 additions & 4 deletions packages/extension/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,22 @@ script, running with cwd = the run dir, must emit:
- `AMICODE_ITER iter=<n> f=<obj> inf_pr=<…> inf_du=<…>` to stdout, flushed,
once per Ipopt iteration (drives the live stats row). This stays on the raw
Ipopt callback — it needs the rich IPM state the agnostic callback can't carry.
- `iter_<N>.png` every few iterations (the live plot the Inspector shows). See
- `AMICODE_PULSE_META` (once, before the solve) and `AMICODE_PULSE` (once per
iteration) to stdout, flushed — **this is what the Inspector's live pulse
plot renders**. Prototype-grade line shapes (candidate GA format):

```
AMICODE_PULSE_META drives=<n> knots=<N> labels="a_1","a_2" bounds=<lo>:<hi>,<lo>:<hi>
AMICODE_PULSE iter=<n> dt=<dt> a=<values comma-sep; drives semicolon-sep>
```

Use the template's `PulseEmitCallback` idiom (see below): a small
`AbstractIntermediateCallback` that delegates to the PNG callback, syncs the
trajectory from the primal, and prints the lines. **A script that skips these
lines gets a dead live plot** — the Inspector sits on "warming up" until
completion, then shows a no-pulse-data hint.
- `iter_<N>.png` every few iterations — **archival/publication artifact**
(`plot_pulse` is canonical there); the Inspector no longer displays PNGs. See
the per-iter plotting idiom below — **`LivePulsePlotCallback`** once the bundled
Julia project pins DirectTrajOpt ≥ 0.9.7, else the hand-rolled Ipopt-callback
path (the only one that runs on 0.9.6).
Expand All @@ -88,18 +103,26 @@ primal each iteration and writes `iter_<N>.png` — the same object would instal
on MadNLP via `MadNLPOptions(intermediate_callback = …)`:

```julia
live_plot = LivePulsePlotCallback(qtraj, prob.trajectory; every = 6, save_dir = ".")
live_plot = LivePulsePlotCallback(qtraj, prob.trajectory; every = 6, save_dir = ".")
pulse_emit = PulseEmitCallback(live_plot, prob.trajectory) # wraps live_plot; adds AMICODE_PULSE lines
solve!(qcp; max_iter = max_iter,
options = IpoptOptions(intermediate_callback = live_plot), # → iter_<N>.png
options = IpoptOptions(intermediate_callback = pulse_emit), # → iter_<N>.png + AMICODE_PULSE
callback = CB.callback_factory(cb_log)) # → AMICODE_ITER text
```

`PulseEmitCallback` is defined in the template — copy it verbatim (it qualifies
`update!` against the Makie name collision and resolves the drive component
`:u`-then-`:a`). It delegates to the PNG callback first, so archival frames and
pulse telemetry ride one hook.

**Fallback on DirectTrajOpt 0.9.6 (no Ipopt `intermediate_callback` field yet):
hand-roll the PNG from the raw Ipopt callback** — `IpoptOptions(intermediate_callback=…)`
throws at construction on 0.9.6, so if you author a script against a project still
pinned to 0.9.6, use the text-callback path instead: in `cb_log`, every few iters
call `plot_pulse(qcp; bounds = true, title = …)` and `CairoMakie.save` the figure
(alongside `callback_update_trajectory_factory` to keep the iterate in sync).
(alongside `callback_update_trajectory_factory` to keep the iterate in sync) —
and still print the `AMICODE_PULSE_META`/`AMICODE_PULSE` lines from that same
callback (the synced trajectory has the drive values), or the live plot is dead.

The bundled template uses the preferred `LivePulsePlotCallback` path; it lands
together with the DirectTrajOpt ≥ 0.9.7 `Manifest.toml` bump (lockstep), so the
Expand Down
Binary file removed packages/extension/demo/run/iter_0000.png
Binary file not shown.
Binary file removed packages/extension/demo/run/iter_0010.png
Binary file not shown.
Binary file removed packages/extension/demo/run/iter_0020.png
Binary file not shown.
Binary file removed packages/extension/demo/run/iter_0030.png
Binary file not shown.
Binary file removed packages/extension/demo/run/iter_0040.png
Binary file not shown.
Binary file removed packages/extension/demo/run/iter_0050.png
Binary file not shown.
Binary file removed packages/extension/demo/run/iter_0060.png
Binary file not shown.
14 changes: 11 additions & 3 deletions packages/extension/demo/run/result.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
schema_version = "1"
iterations = 60
fidelity = 0.9999788203047787
wall_seconds = 101.5023238658905
fidelity = 0.9998472204856121
schema_version = "1"
wall_seconds = 109.01594400405884

[params]
levels = 3
drive_max = 0.2
T = 10.0
max_iter = 60
N = 50
delta = 0.2
204 changes: 133 additions & 71 deletions packages/extension/demo/run/run.log

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions packages/extension/demo/run/run.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
schema_version = "1"
run_id = "r20260617-161814Z-e8cb"
script_path = "/Users/raghavchari/amicode/packages/extension/templates/solve_template.jl"
lab = "default"
lab_id = "default"
created_at = "2026-06-17T16:18:14.159Z"
run_id = "r20260702-232128Z-a0b4"
script_path = "/Users/katebonner/Desktop/harmoniqs_products/amicode/packages/extension/templates/solve_template.jl"
lab = "devlab"
lab_id = "devlab"
created_at = "2026-07-02T23:21:28.731Z"
orchestrator_version = "0.1.0"

[julia]
binary = "julia"
project = "/Users/raghavchari/.amico/julia"
project = "/Users/katebonner/Desktop/harmoniqs_products/amicode/packages/extension/julia"
40 changes: 40 additions & 0 deletions packages/extension/dev/pulseplot_harness/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>pulseplot harness (#66)</title>
<link rel="stylesheet" href="../../media/brand.css" />
<link rel="stylesheet" href="../../media/layout.css" />
<style>
/* Stand-ins for the VS Code theme variables the webview provides.
Light values validated (dataviz six checks) against the light surface;
brand.css's --series-* fallbacks cover dark. */
body { --vscode-foreground: #333; --vscode-editor-background: #fcfcfb;
--vscode-descriptionForeground: #717171;
--vscode-charts-blue: #1a85ff; --vscode-charts-orange: #a05a00;
--vscode-charts-purple: #8b47b7; --vscode-charts-green: #2e7d32;
background: var(--vscode-editor-background); color: var(--vscode-foreground);
font-family: system-ui, sans-serif; font-size: 13px;
margin: 0; padding: 16px; height: 100vh; box-sizing: border-box;
display: flex; flex-direction: column; gap: 12px; }
body.dark { --vscode-foreground: #ccc; --vscode-editor-background: #1a1a19;
--vscode-descriptionForeground: #9d9d9d;
--vscode-charts-blue: #3794ff; --vscode-charts-orange: #c17800;
--vscode-charts-purple: #9b6bc4; --vscode-charts-green: #4d9e51; }
#controls { display: flex; gap: 8px; align-items: center; }
#plot-host { flex: 1; display: flex; min-height: 0; }
#bench-out { font-family: monospace; font-size: 12px; }
</style>
</head>
<body>
<div id="controls">
<button id="play">▶ play</button>
<button id="clear">clear</button>
<button id="theme">☀/☾ theme</button>
<button id="bench">⏱ bench (AC8)</button>
<span id="bench-out"></span>
</div>
<div id="plot-host"></div>
<script src="./main.js"></script>
</body>
</html>
92 changes: 92 additions & 0 deletions packages/extension/dev/pulseplot_harness/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Pulseplot dev harness (#66) — standalone browser page for iterating on the
// component's visuals and measuring the AC8 render budget. NOT shipped in the
// VSIX (dev/ is excluded; see .vscodeignore). Run: `pnpm run dev:pulseplot`.
//
// Feeds the component synthetic solve-like data by default; drop a recorded
// `pulse-events.json` (array of pulsemeta/pulse message objects, produced from
// a real run.log) next to index.html to replay a real solve.

import { pulseplot } from "../../media/ui/components/pulseplot";

const HARNESS_META = { drives: 2, knots: 50, labels: ["a_1", "a_2"], bounds: [[-0.2, 0.2], [-0.2, 0.2]] as [number, number][] };

const root = document.getElementById("plot-host")!;
const plot = pulseplot("Harness idle — press play.");
root.append(plot.el);

// --- synthetic data: smooth random-walk pulses converging toward a waveform
function syntheticRecord(iter: number): { iter: number; dt: number; values: number[][] } {
const { knots, drives, bounds } = HARNESS_META;
const dt = 10.0 / knots;
const values = Array.from({ length: drives }, (_, d) => {
const [lo, hi] = bounds[d];
const amp = (hi - lo) / 2;
return Array.from({ length: knots }, (_, k) => {
const t = k / knots;
const target = amp * 0.85 * Math.sin((d + 1) * Math.PI * t * 2) * Math.sin(Math.PI * t);
const noise = amp * Math.max(0, 1 - iter / 40) * (Math.random() - 0.5);
return target + noise;
});
});
return { iter, dt, values };
}

// --- controls
const themeBtn = document.getElementById("theme")!;
themeBtn.addEventListener("click", () => document.body.classList.toggle("dark"));

let timer: ReturnType<typeof setInterval> | undefined;
let iter = 0;
const playBtn = document.getElementById("play")!;
playBtn.addEventListener("click", () => {
if (timer) { clearInterval(timer); timer = undefined; playBtn.textContent = "▶ play"; return; }
plot.meta(HARNESS_META);
playBtn.textContent = "⏸ pause";
timer = setInterval(() => {
plot.update(syntheticRecord(iter++));
if (iter > 60) iter = 0;
}, 200); // the host's 5 Hz cadence
});

document.getElementById("clear")!.addEventListener("click", () => { plot.clear(); iter = 0; });

// --- AC8 budget: median + p95 of component update at fixture scale
document.getElementById("bench")!.addEventListener("click", () => {
plot.meta(HARNESS_META);
const times: number[] = [];
for (let i = 0; i < 300; i++) {
const rec = syntheticRecord(i % 60);
const t0 = performance.now();
plot.update(rec);
times.push(performance.now() - t0);
}
times.sort((a, b) => a - b);
const med = times[150].toFixed(3), p95 = times[285].toFixed(3), max = times[299].toFixed(3);
const verdict = times[285] <= 16 ? "PASS (≤16ms)" : "FAIL (>16ms)";
document.getElementById("bench-out")!.textContent =
`update() over 300 frames @ 2×50: median ${med}ms · p95 ${p95}ms · max ${max}ms → ${verdict}`;
console.log("[bench]", { med, p95, max, verdict });
});

// --- optional recorded replay
fetch("./pulse-events.json").then((r) => (r.ok ? r.json() : undefined)).then((events?: Array<Record<string, unknown>>) => {
if (!events) return;
const btn = document.createElement("button");
btn.textContent = "▶ replay recording";
btn.addEventListener("click", () => {
if (timer) { clearInterval(timer); timer = undefined; }
let i = 0;
timer = setInterval(() => {
const e = events[i++];
if (!e) { clearInterval(timer!); timer = undefined; return; }
if (e.type === "pulsemeta") plot.meta(e as never);
else if (e.type === "pulse") plot.update(e as never);
}, 200);
});
document.getElementById("controls")!.append(btn);
});

// --- URL-hash automation for headless-ish eyeballing: #autoplay #bench #dark
if (location.hash.includes("dark")) document.body.classList.add("dark");
if (location.hash.includes("autoplay")) (document.getElementById("play") as HTMLButtonElement).click();
if (location.hash.includes("bench")) setTimeout(() => (document.getElementById("bench") as HTMLButtonElement).click(), 800);
10 changes: 10 additions & 0 deletions packages/extension/media/brand.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,14 @@
/* background */
--bg-box: var(--vscode-editorWidget-background, transparent);
--bg-plot: var(--vscode-editor-background);

/* series — categorical, FIXED order (drive 1..4), never cycled. Primary
* values ride VS Code's theme-aware chart tokens; the hex fallbacks are the
* dev-harness set, validated (dataviz six checks) against the dark surface. */
--series-1: var(--vscode-charts-blue, #3794ff);
--series-2: var(--vscode-charts-orange, #c17800);
--series-3: var(--vscode-charts-purple, #9b6bc4);
--series-4: var(--vscode-charts-green, #4d9e51);
--plot-grid: var(--vscode-charts-lines, color-mix(in srgb, var(--vscode-foreground, #ccc) 18%, transparent));
--plot-band: color-mix(in srgb, var(--vscode-foreground, #ccc) 6%, transparent);
}
74 changes: 0 additions & 74 deletions packages/extension/media/ui/components/preview.ts

This file was deleted.

Loading
Loading