diff --git a/agent_docs/simulation-engine.md b/agent_docs/simulation-engine.md index 0f373c6..d7b0ad6 100644 --- a/agent_docs/simulation-engine.md +++ b/agent_docs/simulation-engine.md @@ -13,7 +13,7 @@ On every canvas state change, `simulate(nodes, edges, timeSeconds)`: 4. **Mixed Island Detection**: Flags unsupported direct analog-to-gate connections with a warning on the analog node. ## Digital Evaluation & Relaxation (`digital.ts`) -- Iterative relaxation loop (up to 50 iterations) to settle combinational feedback, latches, and subcircuits. +- Iterative relaxation loop (up to 50 iterations) to settle combinational feedback, latches, and subcircuits. Pre-indexes incoming edges by target node and sorts by target handle once per run to avoid repeated linear scans. - Supports hierarchical subcircuit evaluation by looking up definitions in `localStorage` (`customComponents`). - Dispatches legacy primitive gates via `evaluateGate.ts` for backwards compatibility. diff --git a/frontend/src/logic/simulation/digital.test.ts b/frontend/src/logic/simulation/digital.test.ts index 18a785c..2712e9c 100644 --- a/frontend/src/logic/simulation/digital.test.ts +++ b/frontend/src/logic/simulation/digital.test.ts @@ -1,3 +1,9 @@ +/** + * @file digital.test.ts + * @description Unit tests for digital circuit evaluation and relaxation simulation, + * including hierarchical components, input handle ordering, and adder settling. + */ + import { runSimulation } from './digital'; import type { DigiEdge, DigiNode } from '../../types'; @@ -85,4 +91,87 @@ describe('runSimulation', () => { const updated = runSimulation(nodes, edges); expect(updated.length).toBe(2); }); + + test('orders gate inputs by target handle regardless of edge list order', () => { + // mux: a = sel, b = data0, c = data1. Edges are deliberately listed c, b, a. + const nodes: DigiNode[] = [ + { id: 'sel', type: 'input', data: { label: 'sel', value: 1 }, position: { x: 0, y: 0 } }, + { id: 'd0', type: 'input', data: { label: 'd0', value: 0 }, position: { x: 0, y: 0 } }, + { id: 'd1', type: 'input', data: { label: 'd1', value: 1 }, position: { x: 0, y: 0 } }, + { id: 'mux', type: 'mux', data: { label: 'mux' }, position: { x: 0, y: 0 } }, + { id: 'out', type: 'output', data: { label: 'out' }, position: { x: 0, y: 0 } }, + ]; + const edges: DigiEdge[] = [ + { id: 'e3', source: 'd1', target: 'mux', targetHandle: 'c' }, + { id: 'e2', source: 'd0', target: 'mux', targetHandle: 'b' }, + { id: 'e1', source: 'sel', target: 'mux', targetHandle: 'a' }, + { id: 'e4', source: 'mux', target: 'out' }, + ]; + + expect(runSimulation(nodes, edges).find((n) => n.id === 'out')?.data.value).toBe(1); + nodes[0] = { ...nodes[0], data: { ...nodes[0].data, value: 0 } }; + expect(runSimulation(nodes, edges).find((n) => n.id === 'out')?.data.value).toBe(0); + }); + + test('settles a ripple-carry adder with nodes listed in reverse dependency order', () => { + const bits = 8; + const a = 0b10110101; + const b = 0b01101110; + const nodes: DigiNode[] = []; + const edges: DigiEdge[] = []; + /** + * Append a directed edge targeting a handle. + * @param source - Source node ID + * @param target - Target node ID + * @param handle - Target handle name + */ + const wire = (source: string, target: string, handle: string): void => { + edges.push({ id: `e${edges.length}`, source, target, targetHandle: handle } as DigiEdge); + }; + + /** + * Append a circuit node. + * @param id - Unique node ID + * @param type - Component type + * @param value - Optional initial value + */ + const node = (id: string, type: string, value?: number): void => { + nodes.push({ id, type, data: { label: id, ...(value === undefined ? {} : { value }) }, position: { x: 0, y: 0 } } as DigiNode); + }; + node('cin', 'input', 0); + let carry = 'cin'; + for (let i = 0; i < bits; i++) { + node(`a${i}`, 'input', (a >> i) & 1); + node(`b${i}`, 'input', (b >> i) & 1); + node(`x${i}`, 'xorGate'); + node(`s${i}`, 'xorGate'); + node(`n1${i}`, 'andGate'); + node(`n2${i}`, 'andGate'); + node(`c${i}`, 'orGate'); + node(`o${i}`, 'output'); + wire(`a${i}`, `x${i}`, 'a'); + wire(`b${i}`, `x${i}`, 'b'); + wire(`x${i}`, `s${i}`, 'a'); + wire(carry, `s${i}`, 'b'); + wire(`a${i}`, `n1${i}`, 'a'); + wire(`b${i}`, `n1${i}`, 'b'); + wire(`x${i}`, `n2${i}`, 'a'); + wire(carry, `n2${i}`, 'b'); + wire(`n1${i}`, `c${i}`, 'a'); + wire(`n2${i}`, `c${i}`, 'b'); + wire(`s${i}`, `o${i}`, 'a'); + carry = `c${i}`; + } + node('cout', 'output'); + wire(carry, 'cout', 'a'); + + // Sum bits and carry-out must match integer addition in either node order. + for (const ordered of [nodes, [...nodes].reverse()]) { + const byId = new Map(runSimulation(ordered, edges).map((n) => [n.id, n.data.value])); + let sum = 0; + for (let i = 0; i < bits; i++) sum |= (byId.get(`o${i}`) as number) << i; + sum |= (byId.get('cout') as number) << bits; + expect(sum).toBe(a + b); + } + }); }); diff --git a/frontend/src/logic/simulation/digital.ts b/frontend/src/logic/simulation/digital.ts index 2d1d2e8..3cc951e 100644 --- a/frontend/src/logic/simulation/digital.ts +++ b/frontend/src/logic/simulation/digital.ts @@ -1,6 +1,6 @@ /** * @file digital.ts - * @description Digital circuit evaluation — Relaxation algorithm propagates 0/1/Z/X + * @description Digital circuit evaluation - Relaxation algorithm propagates 0/1/Z/X * logic values gate-by-gate until stable, resolving feedback loops and latches. * Supports hierarchical subcircuit evaluation via customComponents and * switch-level CMOS transistor networks (NMOS/PMOS + supplies) so hand-wired @@ -17,10 +17,27 @@ export type SimulateCircuit = ( timeSeconds?: number ) => DigiNode[]; +const NO_EDGES: readonly DigiEdge[] = []; + +/** + * Simulate circuit logic values across nodes and edges using relaxation evaluation. + * @param currentNodes - Array of circuit nodes to evaluate + * @param currentEdges - Array of circuit edges defining connections + * @param timeSeconds - Simulation elapsed time in seconds + * @returns Array of evaluated nodes with updated states + */ export const runSimulation: SimulateCircuit = (currentNodes, currentEdges, timeSeconds = 0) => { return runSimulationWithCycleGuard(currentNodes, currentEdges, timeSeconds, new Set()); }; +/** + * Relaxation evaluation loop with cycle detection for hierarchical subcircuits. + * @param currentNodes - Array of circuit nodes to evaluate + * @param currentEdges - Array of circuit edges defining connections + * @param timeSeconds - Simulation elapsed time in seconds + * @param visitedSubcircuits - Set of visited subcircuit IDs to guard against recursion cycles + * @returns Array of evaluated nodes with updated states + */ const runSimulationWithCycleGuard = ( currentNodes: DigiNode[], currentEdges: DigiEdge[], @@ -33,6 +50,19 @@ const runSimulationWithCycleGuard = ( })); const nodeMap = new Map(newNodes.map((node) => [node.id, node])); + // Index incoming edges once per run (O(E)) instead of rescanning every edge + // for every node on every relaxation pass. Each list keeps edge order and is + // sorted by handle name ('a', 'b', 'c', ...) or index, exactly as before. + const incomingByTarget = new Map(); + for (const edge of currentEdges) { + const list = incomingByTarget.get(edge.target); + if (list) list.push(edge); + else incomingByTarget.set(edge.target, [edge]); + } + incomingByTarget.forEach((list) => { + list.sort((e1, e2) => (e1.targetHandle || '').localeCompare(e2.targetHandle || '')); + }); + let changed = true; let iterations = 0; const MAX_ITERATIONS = 50; // Enough to settle combinational loops and latches @@ -64,14 +94,7 @@ const runSimulationWithCycleGuard = ( continue; } - const incomingEdges = currentEdges.filter((edge) => edge.target === node.id); - - // Sort edges by handle name ('a', 'b', 'c', ...) or index - incomingEdges.sort((e1, e2) => { - const h1 = e1.targetHandle || ''; - const h2 = e2.targetHandle || ''; - return h1.localeCompare(h2); - }); + const incomingEdges = incomingByTarget.get(node.id) ?? NO_EDGES; const inputs = incomingEdges.map((edge) => { const src = nodeMap.get(edge.source);