From 6c6dea3326ceeaf5db17831242ac3bfbca101f2b Mon Sep 17 00:00:00 2001 From: davidmonterocrespo24 Date: Fri, 15 May 2026 22:31:08 +0200 Subject: [PATCH] =?UTF-8?q?feat(sim):=20Phase=201d=20#6=20=E2=80=94=20list?= =?UTF-8?q?CurrentVectors=20in=20Worker=20adapter,=20no=20more=20heuristic?= =?UTF-8?q?=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `runNetlist` was guessing what vectors to read by regex-matching `V*/R*/L*/C*/D*/Q*/M*` lines in the netlist string. Fragile — missed extra-card nets, custom prefixes, subckt-internal nets. This commit gives the Worker adapter the same enumeration surface the Node adapter already had: • New `listVectors` message type in the worker, calling `ngSpice_AllVecs(curPlot)` and decoding the NULL-terminated char** result. Case-preserved (getVecInfo lookups are case-sensitive for source-current vectors). • `NgSpiceInteractive.listVectors()` exposes it to the adapter. • `NgSpiceWorkerAdapter.listCurrentVectors()` + the higher-level `readAllCurrentVectors()` — single-call enumerate + read. • `runNetlist.ts` simplified: ONE solve, then read every vector via the adapter. No more regex parsing. No more guess-set. `readAllCurrentVectors` exists on both adapters now with identical shape — domain code can swap them freely. 1461 tests pass. Both `examples-gallery-smoke` (68 examples) and `circuit-verifier` (8 pre-flight checks) green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../spice/adapters/NgSpiceWorkerAdapter.ts | 42 ++++++++++++++ frontend/src/simulation/spice/runNetlist.ts | 57 +++++++++---------- .../spice/wasm/NgSpiceInteractive.ts | 20 +++++++ .../spice/wasm/ngspice-interactive-worker.js | 27 +++++++++ 4 files changed, 115 insertions(+), 31 deletions(-) diff --git a/frontend/src/simulation/spice/adapters/NgSpiceWorkerAdapter.ts b/frontend/src/simulation/spice/adapters/NgSpiceWorkerAdapter.ts index cf6d2edd..e5dd3624 100644 --- a/frontend/src/simulation/spice/adapters/NgSpiceWorkerAdapter.ts +++ b/frontend/src/simulation/spice/adapters/NgSpiceWorkerAdapter.ts @@ -140,6 +140,48 @@ export class NgSpiceWorkerAdapter implements SolverPort { await this.client.alter(name, dcValue); } + /** + * Enumerate vector names in the current plot. Same surface as + * `NgSpiceNodeAdapter.listCurrentVectors` so `runNetlist.ts` no + * longer has to heuristic-parse netlist strings to guess what to + * read. Phase 1d #6. + */ + async listCurrentVectors(): Promise { + await this.init(); + return this.client.listVectors(); + } + + /** + * Read every vector in the current plot after an analysis has + * already run. Critical for `runNetlist`-style consumers that + * want every vector — re-running the analysis to read them would + * create a NEW plot and invalidate the pointers. + */ + async readAllCurrentVectors(): Promise<{ + vectors: Map; + rawNames: string[]; + }> { + await this.init(); + const rawNames = await this.client.listVectors(); + const vectors = new Map(); + const reads = await Promise.allSettled( + rawNames.map(async (name) => { + const v = await this.client.readVec(name); + return { requested: name, vec: v }; + }), + ); + for (const r of reads) { + if (r.status !== 'fulfilled') continue; + const { requested, vec } = r.value; + vectors.set(requested.toLowerCase(), { + name: requested.toLowerCase(), + real: vec.real, + imag: vec.imag, + }); + } + return { vectors, rawNames }; + } + dispose(): void { this.client.dispose(); this.initialised = false; diff --git a/frontend/src/simulation/spice/runNetlist.ts b/frontend/src/simulation/spice/runNetlist.ts index ff54d45f..6687dda2 100644 --- a/frontend/src/simulation/spice/runNetlist.ts +++ b/frontend/src/simulation/spice/runNetlist.ts @@ -92,6 +92,16 @@ function legacyNameFor(ngName: string): string { return `v(${l})`; } +interface AdapterWithRead { + readAllCurrentVectors(): Promise<{ + vectors: Map; + rawNames: string[]; + }> | { + vectors: Map; + rawNames: string[]; + }; +} + /** * Submit a netlist, run the embedded analysis directive, return * cooked results. The vendored ngspice runs in a Web Worker so this @@ -102,38 +112,23 @@ export async function runNetlist(netlist: string): Promise { await adapter.init(); await adapter.loadCircuit(netlist); const analysis = detectAnalysis(netlist); - // First-pass solve to populate the plot. The worker adapter - // doesn't yet expose listCurrentVectors, so we fetch every node - // voltage + branch current the user might ask for via the - // adapter's parallel readVec batching. For browser-side - // circuitVerifier this is fine — the netlists are bounded by what - // the canvas can hold (~50 nets, ~10 V-sources). + // Single solve — populate the plot, then enumerate + read every + // vector via `readAllCurrentVectors` so the pointers stay valid. + // (Re-running the analysis to read vectors would create a new + // plot and invalidate everything.) await adapter.solve(analysis, { vectorsOfInterest: [] }); - // We can't readAllCurrentVectors from the worker adapter today — - // it doesn't have that method. Fall back to requesting common - // patterns: every v(...) and i(...) we can guess from the netlist. - const guessed = new Set(); - for (const line of netlist.split('\n')) { - // Match V*, I* source declarations. - const mV = line.match(/^([Vv][_\w]+)\s+(\S+)\s+(\S+)/); - if (mV) { - guessed.add(`v(${mV[2]!.toLowerCase()})`); - guessed.add(`v(${mV[3]!.toLowerCase()})`); - guessed.add(`i(${mV[1]!.toLowerCase()})`); - } - // Match generic two-terminal cards (R, C, L, D, Q, M). - const mGen = line.match(/^[RCLDQMX][_\w]+\s+(\S+)\s+(\S+)/); - if (mGen) { - guessed.add(`v(${mGen[1]!.toLowerCase()})`); - guessed.add(`v(${mGen[2]!.toLowerCase()})`); - } - } - guessed.delete('v(0)'); // ground - if (analysis.kind === 'tran') guessed.add('time'); - if (analysis.kind === 'ac') guessed.add('frequency'); - - const requested = Array.from(guessed).map(ngspiceNameFor); - const result = await adapter.solve(analysis, { vectorsOfInterest: requested }); + const all = await (adapter as unknown as AdapterWithRead).readAllCurrentVectors(); + const result = { + analysis, + vectors: all.vectors, + timeAxis: + analysis.kind === 'tran' + ? all.vectors.get('time')?.real ?? new Float64Array(0) + : new Float64Array(0), + solveMs: 0, + warnings: [] as string[], + }; + const rawVecs = all.rawNames; const variableNames = Array.from(result.vectors.keys()).map(legacyNameFor); const getVec = (name: string): VectorValue[] => { diff --git a/frontend/src/simulation/spice/wasm/NgSpiceInteractive.ts b/frontend/src/simulation/spice/wasm/NgSpiceInteractive.ts index ebc86f3c..cb1d6bee 100644 --- a/frontend/src/simulation/spice/wasm/NgSpiceInteractive.ts +++ b/frontend/src/simulation/spice/wasm/NgSpiceInteractive.ts @@ -159,6 +159,26 @@ export class NgSpiceInteractive { return this.request('readVec', { name }, 'vec'); } + /** + * Enumerate every vector name in the current plot. Returns + * case-preserved names — `ngGet_Vec_Info` lookups are + * case-sensitive for source-current vectors like `V_src#branch`. + * + * The worker reads via `ngSpice_AllVecs(curPlot)` and walks the + * NUL-terminated char** array. Used by `NgSpiceWorkerAdapter` so + * the production path doesn't have to heuristic-parse netlist + * strings (Phase 1d #6). + */ + async listVectors(): Promise { + await this.init(); + const res = await this.request<{ names: string[] }>( + 'listVectors', + {}, + 'vector-list', + ); + return res.names; + } + /** Reset the engine to a clean state (drops netlist + plots). */ async reset(): Promise { await this.init(); diff --git a/frontend/src/simulation/spice/wasm/ngspice-interactive-worker.js b/frontend/src/simulation/spice/wasm/ngspice-interactive-worker.js index 658f79ec..bffdeb88 100644 --- a/frontend/src/simulation/spice/wasm/ngspice-interactive-worker.js +++ b/frontend/src/simulation/spice/wasm/ngspice-interactive-worker.js @@ -118,6 +118,11 @@ self.addEventListener('message', async (event) => { return; } + if (data.type === 'listVectors') { + handleListVectors(data.requestId); + return; + } + throw new Error(`Unknown message type: ${data.type}`); } catch (error) { self.postMessage({ @@ -208,6 +213,28 @@ function handleReadVec(requestId, vectorName) { ); } +/** + * Enumerate every vector in the current plot. Phase 1d #6. + * `ngSpice_AllVecs` returns a NULL-terminated char** array; we walk + * it until the first 0 pointer and decode each cstring. Names are + * case-preserved — getVecInfo lookups care about case. + */ +function handleListVectors(requestId) { + const plot = api.curPlot && api.curPlot(); + const names = []; + if (plot) { + const arrPtr = api.allVecs(plot); + if (arrPtr) { + for (let i = 0; i < 4096; i++) { + const ptr = HEAPU32[(arrPtr >> 2) + i]; + if (!ptr) break; + names.push(Module.UTF8ToString(ptr)); + } + } + } + self.postMessage({ type: 'vector-list', requestId, names }); +} + // ── Command-capture machinery (Phase 1a) ──────────────────────────── // // onPrint pushes to currentRun.log.stdout when a batch run is active.