-
Notifications
You must be signed in to change notification settings - Fork 1
perf: smoother solve cold-start — warming-up UX + live-frame display fix #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
24b0d88
d98e5c8
0dcb2f2
8388cfb
2287cad
20abb98
e21539c
32438de
afc6020
e83d76a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| constructing SmoothPulseProblem [UnitaryTrajectory] | ||
| ┌ Warning: Trajectory has timestep variable :Δt but no bounds on it. | ||
| │ Adding default lower bound of 0 to prevent negative timesteps. | ||
| │ | ||
| │ Recommended: Add explicit bounds when creating the trajectory: | ||
| │ NamedTrajectory(...; Δt_bounds=(min, max)) | ||
| │ Example: | ||
| │ NamedTrajectory(qtraj, N; Δt_bounds=(1e-3, 0.5)) | ||
| │ | ||
| │ Or use timesteps_all_equal=true in problem options to fix timesteps. | ||
| └ @ DirectTrajOpt.Problems ~/.julia/packages/DirectTrajOpt/TIf6x/src/problems.jl:66 | ||
| QuantumControlProblem | ||
| ├─ UnitaryTrajectory · ZeroOrderPulse · BilinearIntegrator, DerivativeIntegrator, DerivativeIntegrator | ||
| │ | ||
| ├─ System | ||
| │ dim=3 drives=2 | ||
| │ | ||
| ├─ Trajectory | ||
| │ N=50 T=10.000 Δt∈[0, Inf] | ||
| │ Ũ⃗ (18) ±[1.0, 1.0, 1.0, … (18 total)] ✓ state | ||
| │ Δt ( 1) [0.0, Inf] ✓ timestep | ||
| │ t ( 1) · state | ||
| │ u ( 2) ±[0.2, 0.2] ✓ control | ||
| │ du ( 2) · control | ||
| │ ddu ( 2) ±[1.0, 1.0] ✓ control | ||
| │ | ||
| ├─ Goal | ||
| │ EmbeddedOperator on [3], subspace dim 2 | ||
| │ | ||
| ├─ Objective total = 44.95 @ current x | ||
| │ KnotPointObjective w=1 44.25 | ||
| │ QuadraticRegularizer(:u) w=1 2.042e-04 | ||
| │ QuadraticRegularizer(:du) w=1 9.849e-03 | ||
| │ QuadraticRegularizer(:ddu) w=1 0.6964 | ||
| │ NullObjective w=1 0 | ||
| │ | ||
| ├─ Constraints 1/14 violated at x₀ | ||
| │ [dyn] BilinearIntegrator ✗ (‖c‖∞ = 0.09876) | ||
| │ [dyn] DerivativeIntegrator ✓ (‖c‖∞ = 2.776e-17) | ||
| │ [dyn] DerivativeIntegrator ✓ (‖c‖∞ = 2.220e-16) | ||
| │ [ineq] AllEqualConstraint ✓ (no eval) | ||
| │ [eq] EqualityConstraint ✓ (no eval) | ||
| │ [eq] EqualityConstraint ✓ (no eval) | ||
| │ [eq] EqualityConstraint ✓ (no eval) | ||
| │ [bnd] BoundsConstraint ✓ | ||
| │ [bnd] BoundsConstraint ✓ | ||
| │ [bnd] BoundsConstraint ✓ | ||
| │ [bnd] BoundsConstraint ✓ | ||
| │ [bnd] BoundsConstraint ✓ | ||
| │ [eq] TimeConsistencyConstraint ✓ (no eval) | ||
| │ [eq] EqualityConstraint ✓ (no eval) | ||
| │ | ||
| └─ Status | ||
| variables: 1300 (1100 bounded) | ||
| equality: 52827 | ||
| inequality: 1 | ||
| F (raw) = 0.557531 | ||
|
|
||
| Hint: show_problem(qcp; detail=:full) for pulse plot + sparsity | ||
|
|
||
| AMICODE_ITER iter=0 f=4.427415e+01 inf_pr=2.715e+00 inf_du=4.401e+00 | ||
|
|
||
| ****************************************************************************** | ||
| This program contains Ipopt, a library for large-scale nonlinear optimization. | ||
| Ipopt is released as open source code under the Eclipse Public License (EPL). | ||
| For more information visit https://github.com/coin-or/Ipopt | ||
| ****************************************************************************** | ||
|
|
||
| AMICODE_ITER iter=1 f=3.312001e+00 inf_pr=1.962e+00 inf_du=1.835e+02 | ||
| AMICODE_ITER iter=2 f=1.909965e+01 inf_pr=1.305e-01 inf_du=1.641e+02 | ||
| AMICODE_ITER iter=3 f=2.688086e+01 inf_pr=2.734e-03 inf_du=1.741e+02 | ||
| AMICODE_ITER iter=4 f=2.499717e+01 inf_pr=2.649e-04 inf_du=1.925e+02 | ||
| AMICODE_ITER iter=5 f=2.047457e+01 inf_pr=1.168e-03 inf_du=1.436e+00 | ||
| AMICODE_ITER iter=6 f=1.371681e+01 inf_pr=1.334e-03 inf_du=1.135e+00 | ||
| AMICODE_ITER iter=7 f=2.567280e+00 inf_pr=5.849e-03 inf_du=4.532e+00 | ||
| AMICODE_ITER iter=8 f=1.243844e+00 inf_pr=1.448e-02 inf_du=5.058e+03 | ||
| AMICODE_ITER iter=9 f=2.715532e+00 inf_pr=7.488e-03 inf_du=1.266e+05 | ||
| AMICODE_ITER iter=10 f=2.760619e+00 inf_pr=2.607e-04 inf_du=2.939e+04 | ||
| AMICODE_ITER iter=11 f=2.141433e+00 inf_pr=5.075e-05 inf_du=2.069e+00 | ||
| AMICODE_ITER iter=12 f=1.090665e+00 inf_pr=3.007e-04 inf_du=1.943e-01 | ||
| AMICODE_ITER iter=13 f=2.750855e-02 inf_pr=7.233e-04 inf_du=1.921e+02 | ||
| AMICODE_ITER iter=14 f=7.579673e-01 inf_pr=6.602e-04 inf_du=1.915e+02 | ||
| AMICODE_ITER iter=15 f=8.604789e-01 inf_pr=9.254e-07 inf_du=9.379e-01 | ||
| AMICODE_ITER iter=16 f=7.798056e-01 inf_pr=4.902e-06 inf_du=1.591e-01 | ||
| AMICODE_ITER iter=17 f=5.809716e-01 inf_pr=3.379e-05 inf_du=1.330e-01 | ||
| AMICODE_ITER iter=18 f=2.319975e-01 inf_pr=1.465e-04 inf_du=9.073e-02 | ||
| AMICODE_ITER iter=19 f=5.657439e-02 inf_pr=2.357e-04 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=20 f=1.418744e-01 inf_pr=1.465e-04 inf_du=1.923e+02 | ||
| AMICODE_ITER iter=21 f=1.615507e-01 inf_pr=2.529e-07 inf_du=5.215e-01 | ||
| AMICODE_ITER iter=22 f=1.421320e-01 inf_pr=1.716e-06 inf_du=6.246e-02 | ||
| AMICODE_ITER iter=23 f=9.743153e-02 inf_pr=1.068e-05 inf_du=5.106e-02 | ||
| AMICODE_ITER iter=24 f=3.100677e-02 inf_pr=3.676e-05 inf_du=3.206e-02 | ||
| AMICODE_ITER iter=25 f=2.149879e-02 inf_pr=4.050e-05 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=26 f=2.110705e-02 inf_pr=1.111e-05 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=27 f=2.287865e-02 inf_pr=4.976e-08 inf_du=3.663e-01 | ||
| AMICODE_ITER iter=28 f=2.057141e-02 inf_pr=2.819e-07 inf_du=1.757e-02 | ||
| AMICODE_ITER iter=29 f=1.566875e-02 inf_pr=1.540e-06 inf_du=1.382e-02 | ||
| AMICODE_ITER iter=30 f=9.845458e-03 inf_pr=3.980e-06 inf_du=7.545e-03 | ||
| AMICODE_ITER iter=31 f=1.037462e-02 inf_pr=3.020e-06 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=32 f=9.708787e-03 inf_pr=1.299e-06 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=33 f=9.757572e-03 inf_pr=4.887e-08 inf_du=3.527e-01 | ||
| AMICODE_ITER iter=34 f=9.648300e-03 inf_pr=2.990e-08 inf_du=3.208e-03 | ||
| AMICODE_ITER iter=35 f=9.435164e-03 inf_pr=8.478e-08 inf_du=2.684e-03 | ||
| AMICODE_ITER iter=36 f=9.281332e-03 inf_pr=1.850e-07 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=37 f=9.315456e-03 inf_pr=3.351e-07 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=38 f=9.332486e-03 inf_pr=1.542e-10 inf_du=1.467e-02 | ||
| AMICODE_ITER iter=39 f=9.317899e-03 inf_pr=1.125e-09 inf_du=2.186e-03 | ||
| AMICODE_ITER iter=40 f=9.283247e-03 inf_pr=7.451e-09 inf_du=2.047e-03 | ||
| AMICODE_ITER iter=41 f=9.226014e-03 inf_pr=3.074e-08 inf_du=1.767e-03 | ||
| AMICODE_ITER iter=42 f=9.195011e-03 inf_pr=6.195e-08 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=43 f=9.136038e-03 inf_pr=3.153e-07 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=44 f=9.137616e-03 inf_pr=4.528e-11 inf_du=7.841e-03 | ||
| AMICODE_ITER iter=45 f=9.134479e-03 inf_pr=3.146e-10 inf_du=1.453e-03 | ||
| AMICODE_ITER iter=46 f=9.126828e-03 inf_pr=2.038e-09 inf_du=1.389e-03 | ||
| AMICODE_ITER iter=47 f=9.111456e-03 inf_pr=8.248e-09 inf_du=1.377e-03 | ||
| AMICODE_ITER iter=48 f=9.088690e-03 inf_pr=5.431e-08 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=49 f=9.001065e-03 inf_pr=4.205e-07 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=50 f=9.000330e-03 inf_pr=7.382e-10 inf_du=3.516e-02 | ||
| AMICODE_ITER iter=51 f=8.998946e-03 inf_pr=2.533e-10 inf_du=1.200e-03 | ||
| AMICODE_ITER iter=52 f=8.994278e-03 inf_pr=1.160e-09 inf_du=1.212e-03 | ||
| AMICODE_ITER iter=53 f=8.982299e-03 inf_pr=9.944e-09 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=54 f=8.943076e-03 inf_pr=8.767e-08 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=55 f=8.942956e-03 inf_pr=6.080e-12 inf_du=3.079e-03 | ||
| AMICODE_ITER iter=56 f=8.942226e-03 inf_pr=2.578e-11 inf_du=1.137e-03 | ||
| AMICODE_ITER iter=57 f=8.940084e-03 inf_pr=2.319e-10 inf_du=1.141e-03 | ||
| AMICODE_ITER iter=58 f=8.933903e-03 inf_pr=2.064e-09 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=59 f=8.917972e-03 inf_pr=1.428e-08 inf_du=1.924e+02 | ||
| AMICODE_ITER iter=60 f=8.917966e-03 inf_pr=7.926e-13 inf_du=1.106e-03 | ||
| DONE fidelity=0.9999788203047787 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ import { getInspector } from "./run_inspector"; | |
| import type { StatusBarManager } from "./status_bar"; | ||
| import type { RunStatus } from "./types"; | ||
| import { | ||
| AMICODE_ITER_RE, ITER_PNG_RE, ingestRunDir, readTomlSafe, parseAmicoNum, | ||
| AMICODE_ITER_RE, ITER_PNG_RE, ingestRunDir, readTomlSafe, parseAmicoNum, SinkDedup, | ||
| type IterRecord, type RunCompletion, type PromoteInfo, type RunSink, | ||
| } from "./run_dir_reader"; | ||
|
|
||
|
|
@@ -34,7 +34,9 @@ export interface RunsRootWatcherOptions { | |
| /** Live sink: routes to the Inspector + status bar, carrying newest-wins and | ||
| * promote-once guards so replay-then-incremental never double-fires. */ | ||
| class LiveRunSink implements RunSink { | ||
| private latestIter = -1; | ||
| /** Newest-wins guard: frame display vs log-line iters tracked separately so the | ||
| * log high-water mark can't suppress lagging frames (see SinkDedup). */ | ||
| private readonly dedup = new SinkDedup(); | ||
| constructor( | ||
| private readonly opts: RunsRootWatcherOptions, | ||
| private readonly runId: string, | ||
|
|
@@ -44,12 +46,11 @@ class LiveRunSink implements RunSink { | |
| ) {} | ||
|
|
||
| image(fsPath: string, iter: number): void { | ||
| if (iter <= this.latestIter) return; | ||
| this.latestIter = iter; | ||
| if (!this.dedup.acceptFrame(iter)) return; // dedup on FRAMES only — see SinkDedup | ||
| getInspector()?.setImageSource(fsPath, iter); | ||
| } | ||
| iter(rec: IterRecord): void { | ||
| if (rec.iter > this.latestIter) this.latestIter = rec.iter; | ||
| this.dedup.noteIter(rec.iter); | ||
| getInspector()?.postIterationRecord(rec); | ||
| // Live status-bar update — show "running · iter N" as it solves, not only at | ||
| // completion (#5 AC3). | ||
|
|
@@ -65,7 +66,7 @@ class LiveRunSink implements RunSink { | |
| getInspector()?.postCompletion(c.status, c.fidelity); | ||
| this.opts.statusBar?.setRun({ | ||
| runId: c.runId, outputDir: c.runDir, startedAt: 0, | ||
| status: c.status, latestIter: this.latestIter >= 0 ? this.latestIter : undefined, | ||
| status: c.status, latestIter: this.dedup.high >= 0 ? this.dedup.high : undefined, | ||
| fidelity: c.fidelity, | ||
| }); | ||
| this.opts.channel.appendLine(`[runs] ${c.runId} ${c.status}${c.fidelity !== undefined ? ` F=${c.fidelity.toFixed(6)}` : ""}`); | ||
|
|
@@ -100,20 +101,65 @@ export class RunsRootWatcher implements vscode.Disposable { | |
| * promote prompt fires at most once per run, never re-popping on re-switch / | ||
| * launch-follows-latest. */ | ||
| private readonly promotedRuns = new Set<string>(); | ||
| /** Polling backstop. macOS fs.watch (FSEvents) coalesces and silently drops | ||
| * events — especially under load — so the symlink-follow + per-frame watches | ||
| * miss `latest` swings and `iter_*.png` creations, leaving the inspector | ||
| * stuck (no live frames). A cheap periodic rescan guarantees delivery; the | ||
| * fs.watch paths stay for low latency. All sinks are idempotent (frame dedup | ||
| * by iter, finishedSeen, log byte-offset), so double-delivery is harmless. */ | ||
| private poll?: NodeJS.Timeout; | ||
| private static readonly POLL_MS = 700; | ||
|
|
||
| constructor(private readonly opts: RunsRootWatcherOptions) {} | ||
|
|
||
| start(): void { | ||
| fs.mkdirSync(this.opts.runsRoot, { recursive: true }); | ||
| const latest = path.join(this.opts.runsRoot, "latest"); | ||
| if (fs.existsSync(latest)) this.followLatest(); | ||
| if (fs.existsSync(latest)) { | ||
| // On launch, stay IDLE for a previous, already-finished run — don't re-display | ||
| // its last plot. Only resume a still-running run. A run that starts AFTER | ||
| // launch is picked up normally (idle → warming → frames). To baseline a | ||
| // finished run we set activeRunDir WITHOUT a sink, so the poll won't render it. | ||
| try { | ||
| const target = fs.realpathSync(latest); | ||
| if (fs.existsSync(path.join(target, "FINISHED"))) { this.activeRunDir = target; this.finishedSeen = true; } | ||
| else this.followLatest(); | ||
| } catch { /* noop */ } | ||
| } | ||
| this.rootWatcher = fs.watch(this.opts.runsRoot, { persistent: false }, (_e, filename) => { | ||
| if (filename === "latest") this.followLatest(); | ||
| }); | ||
| this.opts.channel.appendLine(`[runs] watching ${this.opts.runsRoot}`); | ||
| this.poll = setInterval(() => this.tick(), RunsRootWatcher.POLL_MS); | ||
| this.opts.channel.appendLine(`[runs] watching ${this.opts.runsRoot} (fs.watch + ${RunsRootWatcher.POLL_MS}ms poll)`); | ||
| } | ||
|
|
||
| /** fs.watch backstop: re-resolve `latest`, then rescan the active run for new | ||
| * frames / FINISHED and drain the log — catching anything FSEvents dropped. */ | ||
| private tick(): void { | ||
| try { | ||
| if (fs.existsSync(path.join(this.opts.runsRoot, "latest"))) this.followLatest(); | ||
| const runDir = this.activeRunDir; | ||
| if (!runDir || !this.sink) return; | ||
| // Deliver only the NEWEST frame this tick — frames produced between two | ||
| // ticks are intentionally skipped. The inspector shows the latest pulse, | ||
| // not an animation, so a coalesced frame is no loss (and the fs.watch path | ||
| // still catches most frames at low latency). Not a dropped-frame bug. | ||
| let newest = -1, newestPath: string | undefined; | ||
| for (const f of fs.readdirSync(runDir)) { | ||
| const m = ITER_PNG_RE.exec(f); | ||
| if (m) { const k = parseInt(m[1], 10); if (k > newest) { newest = k; newestPath = path.join(runDir, f); } } | ||
| } | ||
| if (newestPath) this.sink.image(newestPath, newest); // deduped by lastFrameIter | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [minor] The poll delivers only the newest frame per tick, so frames produced between two ticks are skipped on this path. Correct for "show the latest pulse" (not an animation) — worth a one-line comment saying so, since otherwise it reads like a dropped-frame bug.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call — added a comment in e83d76a: the poll intentionally delivers only the newest frame per tick (latest pulse, not an animation; the fs.watch path still catches most frames at low latency), so a coalesced frame is no loss. The new state-machine test also asserts the newest-wins behavior explicitly (two frames between ticks → only iter_18 delivered). |
||
| if (!this.finishedSeen && fs.existsSync(path.join(runDir, "FINISHED"))) { | ||
| this.finishedSeen = true; this.onFinished(runDir); | ||
| } | ||
| this.logTailer?.poke(); // drain appended AMICODE_ITER lines | ||
| } catch { /* transient fs race — next tick retries */ } | ||
| } | ||
|
|
||
| dispose(): void { | ||
| if (this.poll) clearInterval(this.poll); | ||
| this.poll = undefined; | ||
| try { this.rootWatcher?.close(); } catch { /* noop */ } | ||
| try { this.activeRunWatcher?.close(); } catch { /* noop */ } | ||
| this.logTailer?.dispose(); | ||
|
|
@@ -146,6 +192,7 @@ export class RunsRootWatcher implements vscode.Disposable { | |
|
|
||
| this.sink = new LiveRunSink(this.opts, runId, runDir, this.promotedRuns); | ||
| getInspector()?.reveal(); | ||
| getInspector()?.setRunLabel(runId); | ||
|
|
||
| // Replay everything already on disk (late-join safe). Returns the run.log | ||
| // bytes consumed so the tailer attaches exactly there (no skipped iters). | ||
|
|
@@ -154,6 +201,11 @@ export class RunsRootWatcher implements vscode.Disposable { | |
| catch (err) { this.opts.channel.appendLine(`[runs] replay failed: ${(err as Error).message}`); } | ||
| this.finishedSeen = finishedAtSwitch; | ||
|
|
||
| // Fresh run (manifest but no frames/FINISHED yet) → Julia/Makie warming up; | ||
| // show that instead of an idle panel so the ~minute cold start isn't read as frozen. | ||
| const hasFrame = fs.readdirSync(runDir).some((f) => ITER_PNG_RE.test(f)); | ||
| if (!finishedAtSwitch && !hasFrame) getInspector()?.setWarmingUp(); | ||
|
|
||
| // Incremental: new iter PNGs + FINISHED. | ||
| this.activeRunWatcher = fs.watch(runDir, { persistent: false }, (_e, filename) => { | ||
| if (!filename) return; | ||
|
|
@@ -207,9 +259,16 @@ class LogTailer implements vscode.Disposable { | |
| private buf = ""; | ||
| private pollTimer?: NodeJS.Timeout; | ||
| private disposed = false; | ||
| private attached = false; | ||
|
|
||
| constructor(private readonly opts: LogTailerOptions) {} | ||
|
|
||
| /** Backstop drain (called by the watcher's poll). No-op until attach() has set | ||
| * the start offset, so it never re-reads lines ingestRunDir already replayed. */ | ||
| poke(): void { | ||
| if (this.attached && !this.disposed) this.drain(); | ||
| } | ||
|
|
||
| start(): void { | ||
| const tryAttach = () => { | ||
| if (this.disposed) return; | ||
|
|
@@ -231,6 +290,7 @@ class LogTailer implements vscode.Disposable { | |
| // Start where ingestRunDir stopped reading (startOffset), not at current EOF — | ||
| // otherwise lines appended between the replay read and this attach are lost. | ||
| this.offset = this.opts.startOffset ?? 0; | ||
| this.attached = true; | ||
| try { | ||
| this.watcher = fs.watch(this.opts.path, { persistent: false }, (event) => { | ||
| if (event === "change") this.drain(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[important] This poll backstop — plus the idle-on-finished baseline in
start()and the warming-up transition — is the most intricate new code in the release, and none of it has a test. The newSinkDedupunit test covers the dedup primitive, not this state machine. One test that appendsiter_*.png+AMICODE_ITERlines to a temp run dir over time and asserts the sink/inspector calls (incl. a finished run at launch staying idle, and warming → first frame) would close it. Fine as a tracked follow-up if you don't want to block the unit — but it's the riskiest untested code in the stack.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in e83d76a — added
test/watcher_statemachine.test.tscovering exactly this. It drivesRunsRootWatcherover a temp run dir and asserts the inspector calls: (a) a run already FINISHED at launch stays idle (no stale plot rendered, even after a poll tick); (b) a fresh run shows warming-up, then the poll delivers the newest frame, then completion fires once. Infra to make the vscode-coupled watcher testable: a minimalvscodestub aliased invitest.config.ts, the inspector mocked to capture calls, andtick()driven directly so the poll path is deterministic (no 700ms-timer race). 49 tests green.