Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agent_docs/simulation-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
89 changes: 89 additions & 0 deletions frontend/src/logic/simulation/digital.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);
}
});
});
41 changes: 32 additions & 9 deletions frontend/src/logic/simulation/digital.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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[],
Expand All @@ -33,6 +50,19 @@ const runSimulationWithCycleGuard = (
}));
const nodeMap = new Map<string, DigiNode>(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<string, DigiEdge[]>();
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
Expand Down Expand Up @@ -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);
Expand Down
Loading