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.