/** * NetlistBuilder — turn a Velxio circuit (components + wires + board pin * state) into a complete ngspice netlist string. * * Algorithm (see plan phase_8_velxio_implementation §5): * 1. Union-Find on wires to identify nets. * 2. Canonicalize known-special nets: GND → "0", VCC/VDD/5V/3V3 → "vcc_rail". * 3. Auto-name remaining nets "n0", "n1", ... deterministically. * 4. Detect floating nodes (no DC path to 0) → add auto pull-down 100 MΩ. * 5. Emit component cards via `componentToSpice`. * 6. Emit board GPIO source cards (digital or PWM quasi-static). * 7. Emit the Vcc rail source. * 8. Append `.model` / `.subckt` cards for every used device. * 9. Append analysis card (`.op` / `.tran` / `.ac`). * 10. `.end`. */ import { UnionFind } from './unionFind'; import { componentToSpice } from './componentToSpice'; import type { BuildNetlistInput, ComponentForSpice, BoardForSpice, WireForSpice, } from './types'; const GROUND_PIN_RE = /^(gnd|vss|vee|ground|gnd\.\d+)$/i; // Deliberately excludes "V+" / "V-" (which are probe terminals) and // "VBB" (non-standard). VCC-like pins on boards are handled via the // board.vccPinNames list, not this regex. const VCC_PIN_RE = /^(vcc|vdd|vcc_rail|5v|3v3|3\.3v)$/i; /** metadataId prefixes of components that must NOT be auto-canonicalized * by the pin-name regex (their pins are just probe labels). */ function skipCanonicalization(metadataId: string): boolean { return metadataId.startsWith('instr-'); } export function buildNetlist(input: BuildNetlistInput): string { const { components, wires, boards, analysis, extraCards = [] } = input; // ── 1. Union-Find over wires ───────────────────────────────────────────── const uf = new UnionFind(); const pinKey = (componentId: string, pinName: string) => `${componentId}:${pinName}`; // Seed every pin referenced by a wire (components pins are added on demand) for (const w of wires) { const a = pinKey(w.start.componentId, w.start.pinName); const b = pinKey(w.end.componentId, w.end.pinName); uf.add(a); uf.add(b); uf.union(a, b); } // ── 2. Canonicalize ground / VCC pins ──────────────────────────────────── for (const board of boards) { for (const pinName of board.groundPinNames ?? []) { uf.setCanonical(pinKey(board.id, pinName), '0'); } for (const pinName of board.vccPinNames ?? []) { uf.setCanonical(pinKey(board.id, pinName), 'vcc_rail'); } } for (const comp of components) { if (skipCanonicalization(comp.metadataId)) continue; for (const pinName of pinsReferencedByWires(comp.id, wires)) { if (GROUND_PIN_RE.test(pinName)) { uf.setCanonical(pinKey(comp.id, pinName), '0'); } else if (VCC_PIN_RE.test(pinName)) { uf.setCanonical(pinKey(comp.id, pinName), 'vcc_rail'); } } } // ── 3. Auto-name remaining nets deterministically ──────────────────────── const netNames = assignDeterministicNetNames(uf); // Helper: pin → net name (null if pin isn't in any net) function netLookup(componentId: string, pinName: string): string | null { const key = pinKey(componentId, pinName); if (!uf.has(key)) return null; return netNames.get(uf.find(key)) ?? null; } // ── 4. Emit component cards ─────────────────────────────────────────────── const cards: string[] = []; const modelLines = new Set(); const dominantVcc = boards[0]?.vcc ?? 5; for (const comp of components) { const localLookup = (pinName: string) => netLookup(comp.id, pinName); const emission = componentToSpice(comp, localLookup, { vcc: dominantVcc }); if (!emission) continue; cards.push(...emission.cards); for (const m of emission.modelsUsed) modelLines.add(m); } // ── 5. Board GPIO sources ───────────────────────────────────────────────── for (const board of boards) { for (const [pinName, state] of Object.entries(board.pins)) { if (state.type === 'input') continue; // don't drive the pin const net = netLookup(board.id, pinName); if (!net) continue; if (net === '0' || net === 'vcc_rail') continue; // already served const v = state.type === 'digital' ? state.v : state.duty * board.vcc; cards.push(`V_${board.id}_${pinName} ${net} 0 DC ${v}`); } } // ── 6. Vcc rail source (if any pin referenced it) ───────────────────────── if (hasNet(netNames, 'vcc_rail')) { cards.unshift(`V_VCC_RAIL vcc_rail 0 DC ${dominantVcc}`); } // ── 7. Auto pull-downs for floating nets ───────────────────────────────── const floating = detectFloatingNets(netNames, cards); for (const net of floating) { cards.push(`R_autopull_${net} ${net} 0 100Meg`); } // ── 8. Compose netlist ──────────────────────────────────────────────────── const lines: string[] = [`* Velxio circuit @ ${new Date().toISOString()}`]; lines.push(...cards); lines.push(...modelLines); lines.push(...extraCards); switch (analysis.kind) { case 'op': lines.push('.op'); break; case 'tran': lines.push(`.tran ${analysis.step} ${analysis.stop}`); break; case 'ac': { const kind = analysis.type ?? 'dec'; const points = analysis.points ?? 20; const fstart = analysis.fstart ?? 1; const fstop = analysis.fstop ?? 1e6; lines.push(`.ac ${kind} ${points} ${fstart} ${fstop}`); break; } } lines.push('.end'); return lines.join('\n'); } // ── Helpers ──────────────────────────────────────────────────────────────── function pinsReferencedByWires(componentId: string, wires: WireForSpice[]): string[] { const pins = new Set(); for (const w of wires) { if (w.start.componentId === componentId) pins.add(w.start.pinName); if (w.end.componentId === componentId) pins.add(w.end.pinName); } return [...pins]; } function assignDeterministicNetNames(uf: UnionFind): Map { const reps = [...uf.nets()].sort(); const out = new Map(); let counter = 0; for (const rep of reps) { if (rep === '0' || rep === 'vcc_rail') { out.set(rep, rep); } else { // Strip characters ngspice doesn't like from auto-names out.set(rep, `n${counter++}`); } } return out; } function hasNet(netNames: Map, name: string): boolean { for (const v of netNames.values()) if (v === name) return true; return false; } /** * Detect nets that lack a DC path to ground. * * Heuristic: look at the emitted cards. A net is "DC-safe" if it is named "0", * or if it appears in the netlist as a terminal of any of: * - a resistor (R...) * - a voltage source (V...) * - a current source (I...) * - an inductor (L... — treated as DC short) * - a switch (S...) * - a behavioral source (B...) * - an MNA-controlled source (E..., G..., F..., H...) * - a subckt instance (X...) * * Components that don't offer a DC path on their own: * - bare capacitor (C...) * - reverse-biased diode (D...) — conservative: treat as floating unless R/L nearby * * We actually take a looser approach: any net that has at least one R/L/V/I/S/B/E/X * terminal is DC-safe. Otherwise, add a 100 MΩ pull. */ function detectFloatingNets(netNames: Map, cards: string[]): Set { const touched = new Set(); const nets = new Set(netNames.values()); for (const line of cards) { const prefix = line[0]; if ('RLVISBEGFHX'.indexOf(prefix) < 0) continue; const tokens = line.split(/\s+/); // Token 0 is the card name; subsequent tokens up to the value are nets for (let i = 1; i < Math.min(tokens.length, 4); i++) { const t = tokens[i]; if (nets.has(t)) touched.add(t); } } const floating = new Set(); for (const net of nets) { if (net === '0' || net === 'vcc_rail') continue; if (!touched.has(net)) floating.add(net); } return floating; } /** * Build a wireId → netName map using the same Union-Find logic as buildNetlist. * Lightweight (no SPICE call) — suitable for the overlay to look up voltages. */ export function buildWireNetMap( input: Pick, ): Map { const { wires, boards, components } = input; const uf = new UnionFind(); const pin = (cId: string, pName: string) => `${cId}:${pName}`; for (const w of wires) { const a = pin(w.start.componentId, w.start.pinName); const b = pin(w.end.componentId, w.end.pinName); uf.add(a); uf.add(b); uf.union(a, b); } for (const board of boards) { for (const pName of board.groundPinNames ?? []) uf.setCanonical(pin(board.id, pName), '0'); for (const pName of board.vccPinNames ?? []) uf.setCanonical(pin(board.id, pName), 'vcc_rail'); } for (const comp of components) { if (comp.metadataId.startsWith('instr-')) continue; for (const pName of pinsReferencedByWires(comp.id, wires)) { if (GROUND_PIN_RE.test(pName)) uf.setCanonical(pin(comp.id, pName), '0'); else if (VCC_PIN_RE.test(pName)) uf.setCanonical(pin(comp.id, pName), 'vcc_rail'); } } const netNames = assignDeterministicNetNames(uf); const result = new Map(); for (const w of wires) { const key = pin(w.start.componentId, w.start.pinName); if (uf.has(key)) { const netName = netNames.get(uf.find(key)); if (netName) result.set(w.id, netName); } } return result; } /** Re-export types for callers. */ export type { BuildNetlistInput, ComponentForSpice, BoardForSpice, WireForSpice } from './types';