feat(sim): Phase 1d #6 — listCurrentVectors in Worker adapter, no more heuristic parsing

`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) <noreply@anthropic.com>
This commit is contained in:
davidmonterocrespo24 2026-05-15 22:31:08 +02:00
parent f7d3ee95e4
commit 6c6dea3326
4 changed files with 115 additions and 31 deletions

View File

@ -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<string[]> {
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<string, import('../ports/SolverPort').SolveVector>;
rawNames: string[];
}> {
await this.init();
const rawNames = await this.client.listVectors();
const vectors = new Map<string, import('../ports/SolverPort').SolveVector>();
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;

View File

@ -92,6 +92,16 @@ function legacyNameFor(ngName: string): string {
return `v(${l})`;
}
interface AdapterWithRead {
readAllCurrentVectors(): Promise<{
vectors: Map<string, import('./ports/SolverPort').SolveVector>;
rawNames: string[];
}> | {
vectors: Map<string, import('./ports/SolverPort').SolveVector>;
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<SpiceResult> {
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<string>();
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[] => {

View File

@ -159,6 +159,26 @@ export class NgSpiceInteractive {
return this.request<VecResult>('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<string[]> {
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<void> {
await this.init();

View File

@ -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.