import { ElectronicCircuitState, ElectronicComponent, SimulationResult, } from '@/types/electronics'; /** * Simplified DC circuit simulator using node-voltage analysis. * * Supports: voltage sources, resistors, ground, switches, LEDs, NMOS/PMOS (ideal). * MOSFETs are modeled as ideal switches: * NMOS: ON (low Rds) when Vgs > threshold (2V), OFF (high Rds) otherwise * PMOS: ON when Vgs < -threshold (-2V), OFF otherwise * * Uses iterative approach for nonlinear elements (transistors): * 1. Assume all transistors OFF * 2. Solve linear circuit * 3. Check transistor states, update if changed * 4. Repeat until stable (max 10 iterations) */ const VTH = 2; // MOSFET threshold voltage const R_ON = 1; // MOSFET ON resistance (Ω) const R_OFF = 1e9; // MOSFET OFF resistance (Ω) const R_LED = 100; // LED effective resistance when forward biased const V_LED = 1.8; // LED forward voltage drop export function simulateElectronics(circuit: ElectronicCircuitState): SimulationResult { const { components, wires } = circuit; if (components.length === 0) { return { success: false, error: 'No hay componentes', nodeVoltages: new Map(), branchCurrents: new Map(), meterReadings: new Map() }; } // Build net list: group connected terminals into nodes const terminalToNode = new Map(); let nodeCounter = 0; function getTerminalId(compId: string, terminal: string): string { return `${compId}:${terminal}`; } function findNode(termId: string): string { let node = terminalToNode.get(termId); if (!node) { node = `n${nodeCounter++}`; terminalToNode.set(termId, node); } return node; } function mergeNodes(a: string, b: string) { const nodeA = findNode(a); const nodeB = findNode(b); if (nodeA === nodeB) return; // Replace all nodeB references with nodeA for (const [key, val] of terminalToNode.entries()) { if (val === nodeB) terminalToNode.set(key, nodeA); } } // Initialize all component terminals for (const comp of components) { const terminals = getComponentTerminals(comp.type); for (const t of terminals) { findNode(getTerminalId(comp.id, t)); } } // Process wires to merge nodes for (const wire of wires) { mergeNodes(wire.from, wire.to); } // Find ground node let groundNode: string | null = null; for (const comp of components) { if (comp.type === 'ground') { groundNode = findNode(getTerminalId(comp.id, 'gnd')); break; } } if (!groundNode) { return { success: false, error: 'Necesitas un nodo de tierra (GND)', nodeVoltages: new Map(), branchCurrents: new Map(), meterReadings: new Map() }; } // Get unique node names (excluding ground) const allNodes = [...new Set(terminalToNode.values())]; const nodeList = allNodes.filter((n) => n !== groundNode); const nodeIndex = new Map(); nodeList.forEach((n, i) => nodeIndex.set(n, i)); const N = nodeList.length; if (N === 0) { return { success: true, nodeVoltages: new Map(), branchCurrents: new Map(), meterReadings: new Map() }; } // Count voltage sources for MNA const voltageSources = components.filter((c) => c.type === 'voltage-source'); const M = voltageSources.length; const size = N + M; // Transistor states (start all OFF) const mosfetState = new Map(); for (const comp of components) { if (comp.type === 'nmos' || comp.type === 'pmos') { mosfetState.set(comp.id, false); } } let voltages: Float64Array = new Float64Array(size); let converged = false; for (let iter = 0; iter < 10; iter++) { // Build MNA matrix: [G B; C D] * [v; i] = [I; E] const A = Array.from({ length: size }, () => new Float64Array(size)); const b = new Float64Array(size); function nodeIdx(termId: string): number { const node = terminalToNode.get(termId); if (!node || node === groundNode) return -1; return nodeIndex.get(node) ?? -1; } // Stamp resistor: G(i,i) += 1/R, G(j,j) += 1/R, G(i,j) -= 1/R, G(j,i) -= 1/R function stampResistor(termA: string, termB: string, R: number) { const i = nodeIdx(termA); const j = nodeIdx(termB); const g = 1 / R; if (i >= 0) A[i][i] += g; if (j >= 0) A[j][j] += g; if (i >= 0 && j >= 0) { A[i][j] -= g; A[j][i] -= g; } } // Stamp voltage source function stampVoltageSource(posTerminal: string, negTerminal: string, voltage: number, vsIdx: number) { const i = nodeIdx(posTerminal); const j = nodeIdx(negTerminal); const k = N + vsIdx; if (i >= 0) { A[i][k] += 1; A[k][i] += 1; } if (j >= 0) { A[j][k] -= 1; A[k][j] -= 1; } b[k] = voltage; } // Process each component let vsCount = 0; for (const comp of components) { switch (comp.type) { case 'resistor': { const R = comp.value ?? 1000; stampResistor(getTerminalId(comp.id, 'a'), getTerminalId(comp.id, 'b'), R); break; } case 'voltage-source': { const V = comp.value ?? 5; stampVoltageSource( getTerminalId(comp.id, 'pos'), getTerminalId(comp.id, 'neg'), V, vsCount++ ); break; } case 'capacitor': { // In DC steady state, capacitor = open circuit (very high R) // But we model it so voltage across it can be measured stampResistor(getTerminalId(comp.id, 'a'), getTerminalId(comp.id, 'b'), R_OFF); break; } case 'led': { // Simplified: resistor + voltage drop (model as resistor for now) stampResistor(getTerminalId(comp.id, 'anode'), getTerminalId(comp.id, 'cathode'), R_LED); break; } case 'switch': { // Switches modeled as low resistance when "on" (value=1), high when "off" (value=0) const isOn = (comp.value ?? 0) > 0; stampResistor(getTerminalId(comp.id, 'a'), getTerminalId(comp.id, 'b'), isOn ? 0.01 : R_OFF); break; } case 'nmos': { const isOn = mosfetState.get(comp.id) ?? false; const R = isOn ? R_ON : R_OFF; stampResistor(getTerminalId(comp.id, 'drain'), getTerminalId(comp.id, 'source'), R); break; } case 'pmos': { const isOn = mosfetState.get(comp.id) ?? false; const R = isOn ? R_ON : R_OFF; stampResistor(getTerminalId(comp.id, 'drain'), getTerminalId(comp.id, 'source'), R); break; } case 'voltmeter': { // Voltmeter = very high resistance (doesn't affect circuit) stampResistor(getTerminalId(comp.id, 'pos'), getTerminalId(comp.id, 'neg'), 1e9); break; } case 'ammeter': { // Ammeter = very low resistance (passes all current) stampResistor(getTerminalId(comp.id, 'a'), getTerminalId(comp.id, 'b'), 0.001); break; } case 'ground': break; } } // Solve A * x = b using Gaussian elimination const solved = solveLinearSystem(A, b, size); if (!solved) { return { success: false, error: 'El circuito no tiene solución (¿cortocircuito?)', nodeVoltages: new Map(), branchCurrents: new Map(), meterReadings: new Map() }; } voltages = solved; // Update MOSFET states based on solved voltages let changed = false; for (const comp of components) { if (comp.type !== 'nmos' && comp.type !== 'pmos') continue; const gateNode = terminalToNode.get(getTerminalId(comp.id, 'gate')); const sourceNode = terminalToNode.get(getTerminalId(comp.id, 'source')); const vGate = getNodeVoltage(gateNode, groundNode, nodeIndex, voltages); const vSource = getNodeVoltage(sourceNode, groundNode, nodeIndex, voltages); const vgs = vGate - vSource; let shouldBeOn = false; if (comp.type === 'nmos') shouldBeOn = vgs > VTH; if (comp.type === 'pmos') shouldBeOn = vgs < -VTH; const wasOn = mosfetState.get(comp.id) ?? false; if (shouldBeOn !== wasOn) { mosfetState.set(comp.id, shouldBeOn); changed = true; } } if (!changed) { converged = true; break; } } if (!converged && mosfetState.size > 0) { // Still usable, just warn } // Build result maps const nodeVoltages = new Map(); for (const [termId, node] of terminalToNode.entries()) { const v = getNodeVoltage(node, groundNode!, nodeIndex, voltages); nodeVoltages.set(termId, Math.round(v * 1000) / 1000); } const branchCurrents = new Map(); // Calculate currents through resistors for (const comp of components) { if (comp.type === 'resistor') { const va = nodeVoltages.get(getTerminalId(comp.id, 'a')) ?? 0; const vb = nodeVoltages.get(getTerminalId(comp.id, 'b')) ?? 0; const R = comp.value ?? 1000; branchCurrents.set(comp.id, Math.round(((va - vb) / R) * 10000) / 10000); } } // Compute meter readings const meterReadings = new Map(); for (const comp of components) { if (comp.type === 'voltmeter') { const vPos = nodeVoltages.get(getTerminalId(comp.id, 'pos')) ?? 0; const vNeg = nodeVoltages.get(getTerminalId(comp.id, 'neg')) ?? 0; const diff = Math.round((vPos - vNeg) * 1000) / 1000; meterReadings.set(comp.id, { value: diff, unit: 'V' }); } if (comp.type === 'ammeter') { const vA = nodeVoltages.get(getTerminalId(comp.id, 'a')) ?? 0; const vB = nodeVoltages.get(getTerminalId(comp.id, 'b')) ?? 0; const current = Math.round(((vA - vB) / 0.001) * 10000) / 10000; meterReadings.set(comp.id, { value: Math.abs(current) * 1000, unit: 'mA' }); } } return { success: true, nodeVoltages, branchCurrents, meterReadings }; } function getNodeVoltage( node: string | undefined, groundNode: string, nodeIndex: Map, voltages: Float64Array ): number { if (!node || node === groundNode) return 0; const idx = nodeIndex.get(node); if (idx === undefined) return 0; return voltages[idx]; } function getComponentTerminals(type: string): string[] { switch (type) { case 'voltage-source': return ['pos', 'neg']; case 'resistor': return ['a', 'b']; case 'capacitor': return ['a', 'b']; case 'led': return ['anode', 'cathode']; case 'switch': return ['a', 'b']; case 'ground': return ['gnd']; case 'nmos': return ['gate', 'drain', 'source']; case 'pmos': return ['gate', 'drain', 'source']; case 'voltmeter': return ['pos', 'neg']; case 'ammeter': return ['a', 'b']; default: return []; } } /** Gaussian elimination with partial pivoting */ function solveLinearSystem(A: Float64Array[], b: Float64Array, n: number): Float64Array | null { // Augmented matrix const aug = A.map((row, i) => { const r = new Float64Array(n + 1); r.set(row); r[n] = b[i]; return r; }); for (let col = 0; col < n; col++) { // Pivot let maxRow = col; let maxVal = Math.abs(aug[col][col]); for (let row = col + 1; row < n; row++) { if (Math.abs(aug[row][col]) > maxVal) { maxVal = Math.abs(aug[row][col]); maxRow = row; } } if (maxVal < 1e-12) continue; // singular, skip [aug[col], aug[maxRow]] = [aug[maxRow], aug[col]]; // Eliminate for (let row = col + 1; row < n; row++) { const factor = aug[row][col] / aug[col][col]; for (let j = col; j <= n; j++) { aug[row][j] -= factor * aug[col][j]; } } } // Back substitution const x = new Float64Array(n); for (let row = n - 1; row >= 0; row--) { if (Math.abs(aug[row][row]) < 1e-12) { x[row] = 0; continue; } let sum = aug[row][n]; for (let j = row + 1; j < n; j++) { sum -= aug[row][j] * x[j]; } x[row] = sum / aug[row][row]; } return x; }