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 numberDiff line numberDiff line change
@@ -0,0 +1 @@
dev/pulseplot_harness/main.js
1 change: 1 addition & 0 deletions packages/extension/.vscodeignore
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 removedpackages/extension/demo/run/iter_0000.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0010.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0020.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0030.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0040.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0050.png
Binary file not shown.
Binary file removedpackages/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1 @@
dev/pulseplot_harness/main.js
1 change: 1 addition & 0 deletions packages/extension/.vscodeignore
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 removedpackages/extension/demo/run/iter_0000.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0010.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0020.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0030.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0040.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0050.png
Binary file not shown.
Binary file removedpackages/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1 @@
dev/pulseplot_harness/main.js
1 change: 1 addition & 0 deletions packages/extension/.vscodeignore
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 removedpackages/extension/demo/run/iter_0000.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0010.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0020.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0030.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0040.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0050.png
Binary file not shown.
Binary file removedpackages/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1 @@
dev/pulseplot_harness/main.js
1 change: 1 addition & 0 deletions packages/extension/.vscodeignore
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 removedpackages/extension/demo/run/iter_0000.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0010.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0020.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0030.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0040.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0050.png
Binary file not shown.
Binary file removedpackages/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1 @@
dev/pulseplot_harness/main.js
1 change: 1 addition & 0 deletions packages/extension/.vscodeignore
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 removedpackages/extension/demo/run/iter_0000.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0010.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0020.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0030.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0040.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0050.png
Binary file not shown.
Binary file removedpackages/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1 @@
dev/pulseplot_harness/main.js
1 change: 1 addition & 0 deletions packages/extension/.vscodeignore
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 removedpackages/extension/demo/run/iter_0000.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0010.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0020.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0030.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0040.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0050.png
Binary file not shown.
Binary file removedpackages/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1 @@
dev/pulseplot_harness/main.js
1 change: 1 addition & 0 deletions packages/extension/.vscodeignore
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 removedpackages/extension/demo/run/iter_0000.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0010.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0020.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0030.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0040.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0050.png
Binary file not shown.
Binary file removedpackages/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff line change
@@ -0,0 +1 @@
dev/pulseplot_harness/main.js
1 change: 1 addition & 0 deletions packages/extension/.vscodeignore
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 removedpackages/extension/demo/run/iter_0000.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0010.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0020.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0030.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0040.png
Binary file not shown.
Binary file removedpackages/extension/demo/run/iter_0050.png
Binary file not shown.
Binary file removedpackages/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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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