1047 lines
41 KiB
TypeScript
1047 lines
41 KiB
TypeScript
/**
|
||
* Digital examples — SPICE netlist + truth-table validation.
|
||
*
|
||
* For each board-less digital example:
|
||
* 1. It's exported as boardFilter:'digital' (so the gallery groups it).
|
||
* 2. Its wires reference only components that actually exist.
|
||
* 3. buildNetlist produces a non-empty netlist with a ground node.
|
||
* 4. Every component metadataId used is actually mapped to SPICE.
|
||
* 5. Every gate / flip-flop INPUT pin is wired — no floating inputs.
|
||
* This is the test that catches "loose wires" before they ship.
|
||
* 6. A representative subset is actually solved by ngspice and the
|
||
* output LED voltages are checked against the expected truth table.
|
||
*/
|
||
import { describe, it, expect } from 'vitest';
|
||
import { digitalExamples } from '../data/examples-digital';
|
||
import { buildNetlist } from '../simulation/spice/NetlistBuilder';
|
||
import { mappedMetadataIds } from '../simulation/spice/componentToSpice';
|
||
import { runNetlist } from './helpers/testSolver';
|
||
import { exampleProjects } from '../data/examples';
|
||
import type { ExampleProject } from '../data/examples';
|
||
|
||
function toSpiceComponents(example: (typeof digitalExamples)[number]) {
|
||
return example.components.map((c) => ({
|
||
id: c.id,
|
||
metadataId: c.type.replace(/^(wokwi|velxio)-/, ''),
|
||
properties: c.properties ?? {},
|
||
}));
|
||
}
|
||
|
||
function toSpiceWires(example: (typeof digitalExamples)[number]) {
|
||
return example.wires.map((w) => ({
|
||
id: w.id,
|
||
start: { componentId: w.start.componentId, pinName: w.start.pinName },
|
||
end: { componentId: w.end.componentId, pinName: w.end.pinName },
|
||
}));
|
||
}
|
||
|
||
describe('digitalExamples — shape', () => {
|
||
it('exports at least 15 board-less digital circuits', () => {
|
||
expect(digitalExamples.length).toBeGreaterThanOrEqual(15);
|
||
});
|
||
|
||
it('every example uses boardFilter: "digital" and category: "circuits"', () => {
|
||
for (const ex of digitalExamples) {
|
||
expect((ex as any).boardFilter, `${ex.id} boardFilter`).toBe('digital');
|
||
expect(ex.category, `${ex.id} category`).toBe('circuits');
|
||
}
|
||
});
|
||
|
||
it('every digital example id is unique and ends up in exampleProjects', () => {
|
||
const allIds = new Set(exampleProjects.map((e) => e.id));
|
||
const missing = digitalExamples.map((e) => e.id).filter((id) => !allIds.has(id));
|
||
expect(missing).toEqual([]);
|
||
});
|
||
|
||
it('no example lists a board in its components[]', () => {
|
||
const BOARD_PREFIXES = [
|
||
'wokwi-arduino-',
|
||
'wokwi-esp32',
|
||
'wokwi-raspberry-',
|
||
'wokwi-nano-rp',
|
||
'velxio-esp32',
|
||
'velxio-raspberry-',
|
||
'velxio-pi-pico-w',
|
||
'wokwi-attiny',
|
||
];
|
||
for (const ex of digitalExamples) {
|
||
const boards = ex.components.filter((c) => BOARD_PREFIXES.some((p) => c.type.startsWith(p)));
|
||
expect(
|
||
boards.map((b) => b.id),
|
||
`${ex.id}`,
|
||
).toEqual([]);
|
||
}
|
||
});
|
||
|
||
it('no example sets a boardType (digital circuits are board-less)', () => {
|
||
for (const ex of digitalExamples) {
|
||
expect(ex.boardType, `${ex.id}`).toBeUndefined();
|
||
}
|
||
});
|
||
|
||
it('every component type has a SPICE mapping (flip-flops are digital-engine-only)', () => {
|
||
const mapped = new Set(mappedMetadataIds());
|
||
const unmapped = new Set<string>();
|
||
for (const ex of digitalExamples) {
|
||
for (const c of ex.components) {
|
||
const id = c.type.replace(/^(wokwi|velxio)-/, '');
|
||
// Flip-flops have no SPICE mapper by design (no edge detection at DC);
|
||
// they are evaluated by the digital gate engine, not ngspice.
|
||
if (id.startsWith('flip-flop')) continue;
|
||
if (!mapped.has(id)) unmapped.add(`${ex.id}:${c.id}(${id})`);
|
||
}
|
||
}
|
||
expect(Array.from(unmapped)).toEqual([]);
|
||
});
|
||
|
||
it('every wire endpoint references a component that exists', () => {
|
||
for (const ex of digitalExamples) {
|
||
const ids = new Set(ex.components.map((c) => c.id));
|
||
for (const w of ex.wires) {
|
||
expect(ids.has(w.start.componentId), `${ex.id}:${w.id}.start(${w.start.componentId})`).toBe(
|
||
true,
|
||
);
|
||
expect(ids.has(w.end.componentId), `${ex.id}:${w.id}.end(${w.end.componentId})`).toBe(true);
|
||
}
|
||
}
|
||
});
|
||
|
||
it('every example has at least one signal-generator (5V rail + SPICE ground)', () => {
|
||
for (const ex of digitalExamples) {
|
||
const hasSig = ex.components.some((c) => c.type === 'wokwi-signal-generator');
|
||
expect(hasSig, `${ex.id} has no signal-generator (no 5V / ground reference)`).toBe(true);
|
||
}
|
||
});
|
||
|
||
it('every example has at least one logic gate or flip-flop', () => {
|
||
for (const ex of digitalExamples) {
|
||
const logic = ex.components.filter(
|
||
(c) => c.type.startsWith('velxio-logic-gate-') || c.type.startsWith('velxio-flip-flop-'),
|
||
);
|
||
expect(logic.length, `${ex.id} has no logic gate or flip-flop`).toBeGreaterThanOrEqual(1);
|
||
}
|
||
});
|
||
});
|
||
|
||
/** A sequential example contains a flip-flop — it is evaluated by the digital
|
||
* gate engine, not ngspice, so it is exempt from the SPICE netlist checks. */
|
||
const isSequential = (ex: (typeof digitalExamples)[number]) =>
|
||
ex.components.some((c) => c.type.startsWith('velxio-flip-flop-'));
|
||
|
||
describe('digitalExamples — netlist generation', () => {
|
||
it('each example produces a non-empty netlist with a ground net', () => {
|
||
for (const ex of digitalExamples) {
|
||
if (isSequential(ex)) continue; // flip-flop circuits have no SPICE netlist
|
||
const { netlist } = buildNetlist({
|
||
components: toSpiceComponents(ex),
|
||
wires: toSpiceWires(ex),
|
||
boards: [],
|
||
analysis: { kind: 'op' },
|
||
});
|
||
expect(netlist.length, `${ex.id} netlist empty`).toBeGreaterThan(20);
|
||
expect(netlist, `${ex.id} missing .end`).toContain('.end');
|
||
// Each signal-generator must drop a V-source whose second node is 0.
|
||
const sigs = ex.components.filter((c) => c.type === 'wokwi-signal-generator');
|
||
for (const sig of sigs) {
|
||
const re = new RegExp(`^V_${sig.id}\\s+\\S+\\s+0\\b`, 'm');
|
||
expect(netlist, `${ex.id}: ${sig.id} GND not canonicalised to 0`).toMatch(re);
|
||
}
|
||
}
|
||
});
|
||
|
||
it('every gate emits a B-source card with a 1 MΩ load', () => {
|
||
for (const ex of digitalExamples) {
|
||
const { netlist } = buildNetlist({
|
||
components: toSpiceComponents(ex),
|
||
wires: toSpiceWires(ex),
|
||
boards: [],
|
||
analysis: { kind: 'op' },
|
||
});
|
||
const gates = ex.components.filter((c) => c.type.startsWith('velxio-logic-gate-'));
|
||
for (const g of gates) {
|
||
const bre = new RegExp(`^B_${g.id}\\b`, 'm');
|
||
const rre = new RegExp(`^R_${g.id}_load\\b`, 'm');
|
||
expect(netlist, `${ex.id}: gate ${g.id} missing B-source`).toMatch(bre);
|
||
expect(netlist, `${ex.id}: gate ${g.id} missing load resistor`).toMatch(rre);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// ─── Structural: every gate input pin must be wired ───────────────────────
|
||
// "Loose wires" usually mean a gate's A/B/C/D input is left dangling — the
|
||
// SPICE B-source then references an undriven net and the solve goes
|
||
// non-physical. This test enumerates every gate in every example and
|
||
// asserts the pins are connected.
|
||
const GATE_INPUT_PINS: Record<string, string[]> = {
|
||
'logic-gate-not': ['A'],
|
||
'logic-gate-and': ['A', 'B'],
|
||
'logic-gate-or': ['A', 'B'],
|
||
'logic-gate-nand': ['A', 'B'],
|
||
'logic-gate-nor': ['A', 'B'],
|
||
'logic-gate-xor': ['A', 'B'],
|
||
'logic-gate-xnor': ['A', 'B'],
|
||
'logic-gate-and-3': ['A', 'B', 'C'],
|
||
'logic-gate-or-3': ['A', 'B', 'C'],
|
||
'logic-gate-nand-3': ['A', 'B', 'C'],
|
||
'logic-gate-nor-3': ['A', 'B', 'C'],
|
||
'logic-gate-and-4': ['A', 'B', 'C', 'D'],
|
||
'logic-gate-or-4': ['A', 'B', 'C', 'D'],
|
||
'logic-gate-nand-4': ['A', 'B', 'C', 'D'],
|
||
'logic-gate-nor-4': ['A', 'B', 'C', 'D'],
|
||
};
|
||
|
||
describe('digitalExamples — no loose wires', () => {
|
||
it('every gate input pin is connected to at least one wire', () => {
|
||
const failures: string[] = [];
|
||
for (const ex of digitalExamples) {
|
||
for (const c of ex.components) {
|
||
const metaId = c.type.replace(/^(wokwi|velxio)-/, '');
|
||
const inputPins = GATE_INPUT_PINS[metaId];
|
||
if (!inputPins) continue;
|
||
for (const pin of inputPins) {
|
||
const wired = ex.wires.some(
|
||
(w) =>
|
||
(w.start.componentId === c.id && w.start.pinName === pin) ||
|
||
(w.end.componentId === c.id && w.end.pinName === pin),
|
||
);
|
||
if (!wired) failures.push(`${ex.id}: ${c.id}.${pin} (${metaId}) is floating`);
|
||
}
|
||
}
|
||
}
|
||
expect(failures).toEqual([]);
|
||
});
|
||
|
||
it('every gate output pin (Y) feeds at least one wire', () => {
|
||
// A gate whose Y is not consumed isn't catastrophic (SPICE handles it
|
||
// because of the 1 MΩ load), but it usually indicates a wiring mistake
|
||
// — the gate was placed and forgotten. Surface it as a separate failure
|
||
// category so the maintainer can tell which kind of mistake it is.
|
||
const failures: string[] = [];
|
||
for (const ex of digitalExamples) {
|
||
for (const c of ex.components) {
|
||
const metaId = c.type.replace(/^(wokwi|velxio)-/, '');
|
||
if (!GATE_INPUT_PINS[metaId]) continue;
|
||
const wired = ex.wires.some(
|
||
(w) =>
|
||
(w.start.componentId === c.id && w.start.pinName === 'Y') ||
|
||
(w.end.componentId === c.id && w.end.pinName === 'Y'),
|
||
);
|
||
if (!wired) failures.push(`${ex.id}: ${c.id}.Y (${metaId}) output is unused`);
|
||
}
|
||
}
|
||
expect(failures).toEqual([]);
|
||
});
|
||
|
||
it('every LED has both A and C wired', () => {
|
||
const failures: string[] = [];
|
||
for (const ex of digitalExamples) {
|
||
const leds = ex.components.filter((c) => c.type === 'wokwi-led');
|
||
for (const l of leds) {
|
||
for (const pin of ['A', 'C']) {
|
||
const wired = ex.wires.some(
|
||
(w) =>
|
||
(w.start.componentId === l.id && w.start.pinName === pin) ||
|
||
(w.end.componentId === l.id && w.end.pinName === pin),
|
||
);
|
||
if (!wired) failures.push(`${ex.id}: led ${l.id}.${pin} is floating`);
|
||
}
|
||
}
|
||
}
|
||
expect(failures).toEqual([]);
|
||
});
|
||
|
||
it('every slide switch routes pin 2 (output) to something', () => {
|
||
// A slide-switch is an SPDT whose common wiper is pin 2 (it selects pin 1
|
||
// at value=0 or pin 3 at value=1). If pin 2 is floating, toggling the
|
||
// switch produces no visible effect.
|
||
const failures: string[] = [];
|
||
for (const ex of digitalExamples) {
|
||
const sws = ex.components.filter((c) => c.type === 'wokwi-slide-switch');
|
||
for (const s of sws) {
|
||
const wired = ex.wires.some(
|
||
(w) =>
|
||
(w.start.componentId === s.id && w.start.pinName === '2') ||
|
||
(w.end.componentId === s.id && w.end.pinName === '2'),
|
||
);
|
||
if (!wired) failures.push(`${ex.id}: switch ${s.id}.2 (output) is floating`);
|
||
}
|
||
}
|
||
expect(failures).toEqual([]);
|
||
});
|
||
|
||
it('every wire endpoint references a real pin on its component', () => {
|
||
// Hard-coded valid pin names per component metadataId. If we ever rename
|
||
// a pin upstream, this test will flag every example that refers to the
|
||
// old name — useful as both a unit test and a refactor net.
|
||
const VALID_PINS: Record<string, string[]> = {
|
||
'signal-generator': ['SIG', 'GND'],
|
||
led: ['A', 'C'],
|
||
resistor: ['1', '2'],
|
||
'slide-switch': ['1', '2', '3'],
|
||
pushbutton: ['1.l', '2.l', '1.r', '2.r'],
|
||
'logic-gate-not': ['A', 'Y'],
|
||
'logic-gate-and': ['A', 'B', 'Y'],
|
||
'logic-gate-or': ['A', 'B', 'Y'],
|
||
'logic-gate-nand': ['A', 'B', 'Y'],
|
||
'logic-gate-nor': ['A', 'B', 'Y'],
|
||
'logic-gate-xor': ['A', 'B', 'Y'],
|
||
'logic-gate-xnor': ['A', 'B', 'Y'],
|
||
'logic-gate-and-3': ['A', 'B', 'C', 'Y'],
|
||
'logic-gate-or-3': ['A', 'B', 'C', 'Y'],
|
||
'logic-gate-nand-3': ['A', 'B', 'C', 'Y'],
|
||
'logic-gate-nor-3': ['A', 'B', 'C', 'Y'],
|
||
'logic-gate-and-4': ['A', 'B', 'C', 'D', 'Y'],
|
||
'logic-gate-or-4': ['A', 'B', 'C', 'D', 'Y'],
|
||
'logic-gate-nand-4': ['A', 'B', 'C', 'D', 'Y'],
|
||
'logic-gate-nor-4': ['A', 'B', 'C', 'D', 'Y'],
|
||
};
|
||
const failures: string[] = [];
|
||
for (const ex of digitalExamples) {
|
||
const byId = new Map(ex.components.map((c) => [c.id, c]));
|
||
for (const wire of ex.wires) {
|
||
for (const ep of [wire.start, wire.end]) {
|
||
const c = byId.get(ep.componentId);
|
||
if (!c) continue; // already caught by a previous test
|
||
const metaId = c.type.replace(/^(wokwi|velxio)-/, '');
|
||
const valid = VALID_PINS[metaId];
|
||
if (!valid) continue; // skip components we haven't catalogued
|
||
if (!valid.includes(ep.pinName)) {
|
||
failures.push(
|
||
`${ex.id}: wire ${wire.id} references ${ep.componentId}.${ep.pinName} (not a valid ${metaId} pin)`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
expect(failures).toEqual([]);
|
||
});
|
||
|
||
it('no wire shorts two switch outputs together', () => {
|
||
// A wire connecting two slide-switch pin-2 outputs would create a
|
||
// contention point — both switches drive the same node and SPICE picks
|
||
// a compromise voltage. Catch this anti-pattern early.
|
||
const failures: string[] = [];
|
||
for (const ex of digitalExamples) {
|
||
const switchIds = new Set(
|
||
ex.components.filter((c) => c.type === 'wokwi-slide-switch').map((c) => c.id),
|
||
);
|
||
for (const w of ex.wires) {
|
||
const a = w.start;
|
||
const b = w.end;
|
||
if (
|
||
a.pinName === '2' &&
|
||
b.pinName === '2' &&
|
||
switchIds.has(a.componentId) &&
|
||
switchIds.has(b.componentId) &&
|
||
a.componentId !== b.componentId
|
||
) {
|
||
failures.push(`${ex.id}: wire ${w.id} shorts ${a.componentId}.2 to ${b.componentId}.2`);
|
||
}
|
||
}
|
||
}
|
||
expect(failures).toEqual([]);
|
||
});
|
||
|
||
it('no wire drives a slide-switch terminal from a gate output', () => {
|
||
// A gate Y connected to switch pin 1 / 2 / 3 would let the gate override
|
||
// the user's switch state — meaningless and confusing in a teaching
|
||
// example. (Pull-down resistors are wired to switch pin 2 from the
|
||
// GROUND side, never from a gate output.)
|
||
const failures: string[] = [];
|
||
for (const ex of digitalExamples) {
|
||
const byId = new Map(ex.components.map((c) => [c.id, c]));
|
||
for (const w of ex.wires) {
|
||
const startC = byId.get(w.start.componentId);
|
||
const endC = byId.get(w.end.componentId);
|
||
const startIsGateY =
|
||
startC?.type.startsWith('velxio-logic-gate-') && w.start.pinName === 'Y';
|
||
const endIsGateY =
|
||
endC?.type.startsWith('velxio-logic-gate-') && w.end.pinName === 'Y';
|
||
const startIsSwitch = startC?.type === 'wokwi-slide-switch';
|
||
const endIsSwitch = endC?.type === 'wokwi-slide-switch';
|
||
if ((startIsGateY && endIsSwitch) || (endIsGateY && startIsSwitch)) {
|
||
failures.push(
|
||
`${ex.id}: wire ${w.id} drives a switch terminal from a gate Y output`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
expect(failures).toEqual([]);
|
||
});
|
||
|
||
it('every signal-generator has its SIG and GND pins wired', () => {
|
||
// The power source has only two pins. If either is unwired the SPICE
|
||
// ground reference (or the 5 V rail) is missing.
|
||
const failures: string[] = [];
|
||
for (const ex of digitalExamples) {
|
||
const sgs = ex.components.filter((c) => c.type === 'wokwi-signal-generator');
|
||
for (const s of sgs) {
|
||
for (const pin of ['SIG', 'GND']) {
|
||
const wired = ex.wires.some(
|
||
(w) =>
|
||
(w.start.componentId === s.id && w.start.pinName === pin) ||
|
||
(w.end.componentId === s.id && w.end.pinName === pin),
|
||
);
|
||
if (!wired) failures.push(`${ex.id}: signal-generator ${s.id}.${pin} floating`);
|
||
}
|
||
}
|
||
}
|
||
expect(failures).toEqual([]);
|
||
});
|
||
|
||
it('every resistor has both terminals wired', () => {
|
||
const failures: string[] = [];
|
||
for (const ex of digitalExamples) {
|
||
const rs = ex.components.filter((c) => c.type === 'wokwi-resistor');
|
||
for (const r of rs) {
|
||
for (const pin of ['1', '2']) {
|
||
const wired = ex.wires.some(
|
||
(w) =>
|
||
(w.start.componentId === r.id && w.start.pinName === pin) ||
|
||
(w.end.componentId === r.id && w.end.pinName === pin),
|
||
);
|
||
if (!wired) failures.push(`${ex.id}: resistor ${r.id}.${pin} floating`);
|
||
}
|
||
}
|
||
}
|
||
expect(failures).toEqual([]);
|
||
});
|
||
});
|
||
|
||
// ─── Live SPICE solves — verify the netlist actually converges ──────────────
|
||
// We don't run a full truth table per circuit (that'd be hundreds of solves
|
||
// per CI run). Instead, every example is solved with all switches OPEN
|
||
// (low) and all switches CLOSED (high) — if either fails, the circuit is
|
||
// structurally broken.
|
||
function withSwitchValues(ex: ExampleProject, value: 0 | 1): ExampleProject {
|
||
return {
|
||
...ex,
|
||
components: ex.components.map((c) =>
|
||
c.type === 'wokwi-slide-switch'
|
||
? { ...c, properties: { ...c.properties, value } }
|
||
: c,
|
||
),
|
||
};
|
||
}
|
||
|
||
async function solveExample(ex: ExampleProject) {
|
||
const { netlist } = buildNetlist({
|
||
components: toSpiceComponents(ex),
|
||
wires: toSpiceWires(ex),
|
||
boards: [],
|
||
analysis: { kind: 'op' },
|
||
});
|
||
return runNetlist(netlist);
|
||
}
|
||
|
||
describe('digitalExamples — live ngspice convergence', () => {
|
||
for (const ex of digitalExamples) {
|
||
it(
|
||
`${ex.id} solves with switches all-LOW and all-HIGH`,
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const low = await solveExample(withSwitchValues(ex, 0));
|
||
expect(
|
||
low.variableNames.length,
|
||
`${ex.id} (all LOW): no variables returned`,
|
||
).toBeGreaterThan(0);
|
||
const high = await solveExample(withSwitchValues(ex, 1));
|
||
expect(
|
||
high.variableNames.length,
|
||
`${ex.id} (all HIGH): no variables returned`,
|
||
).toBeGreaterThan(0);
|
||
},
|
||
);
|
||
}
|
||
});
|
||
|
||
// ─── Truth-table archetypes ────────────────────────────────────────────────
|
||
// For each gate-archetype, sweep its switches through every input
|
||
// combination and check that the gate output drives the expected LED.
|
||
// `gateOutputV` extracts the SPICE node attached to a gate's Y pin by
|
||
// parsing the gate's `B_<id>` card — the first token after the name is the
|
||
// positive node, which is the Y net.
|
||
async function gateOutputV(
|
||
ex: ExampleProject,
|
||
gateId: string,
|
||
switchSettings: Record<string, 0 | 1>,
|
||
): Promise<number> {
|
||
const patched: ExampleProject = {
|
||
...ex,
|
||
components: ex.components.map((c) => {
|
||
if (c.type !== 'wokwi-slide-switch') return c;
|
||
const v = switchSettings[c.id];
|
||
if (v === undefined) return c;
|
||
return { ...c, properties: { ...c.properties, value: v } };
|
||
}),
|
||
};
|
||
const { netlist } = buildNetlist({
|
||
components: toSpiceComponents(patched),
|
||
wires: toSpiceWires(patched),
|
||
boards: [],
|
||
analysis: { kind: 'op' },
|
||
});
|
||
// Gates emit `B_<id> <yNode> 0 V = ...` — pick out yNode.
|
||
const bRe = new RegExp(`^B_${gateId}\\s+(\\S+)\\s+`, 'm');
|
||
const m = netlist.match(bRe);
|
||
if (!m) throw new Error(`No B-source for gate ${gateId} in netlist`);
|
||
const yNet = m[1];
|
||
const result = await runNetlist(netlist);
|
||
return result.dcValue(`v(${yNet})`);
|
||
}
|
||
|
||
function isHIGH(v: number) {
|
||
return v > 4.0;
|
||
}
|
||
function isLOW(v: number) {
|
||
return v < 1.0;
|
||
}
|
||
|
||
describe('digitalExamples — truth table spot checks', () => {
|
||
it(
|
||
'AND gate: only HIGH when both inputs HIGH',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-and-two-switches')!;
|
||
expect(ex, 'AND example present').toBeDefined();
|
||
const cases: Array<[0 | 1, 0 | 1, boolean]> = [
|
||
[0, 0, false],
|
||
[1, 0, false],
|
||
[0, 1, false],
|
||
[1, 1, true],
|
||
];
|
||
for (const [a, b, expected] of cases) {
|
||
const v = await gateOutputV(ex, 'u1', { s1: a, s2: b });
|
||
if (expected) expect(isHIGH(v), `AND(${a},${b}) → ${v}V (want HIGH)`).toBe(true);
|
||
else expect(isLOW(v), `AND(${a},${b}) → ${v}V (want LOW)`).toBe(true);
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'XOR gate: HIGH when inputs differ',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-xor-difference')!;
|
||
expect(ex, 'XOR example present').toBeDefined();
|
||
const cases: Array<[0 | 1, 0 | 1, boolean]> = [
|
||
[0, 0, false],
|
||
[1, 0, true],
|
||
[0, 1, true],
|
||
[1, 1, false],
|
||
];
|
||
for (const [a, b, expected] of cases) {
|
||
const v = await gateOutputV(ex, 'u1', { s1: a, s2: b });
|
||
if (expected) expect(isHIGH(v), `XOR(${a},${b}) → ${v}V (want HIGH)`).toBe(true);
|
||
else expect(isLOW(v), `XOR(${a},${b}) → ${v}V (want LOW)`).toBe(true);
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'NAND-built XOR: matches XOR truth table',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-xor-from-nands')!;
|
||
expect(ex, 'NAND-only XOR present').toBeDefined();
|
||
const cases: Array<[0 | 1, 0 | 1, boolean]> = [
|
||
[0, 0, false],
|
||
[1, 0, true],
|
||
[0, 1, true],
|
||
[1, 1, false],
|
||
];
|
||
for (const [a, b, expected] of cases) {
|
||
const v = await gateOutputV(ex, 'n4', { sA: a, sB: b });
|
||
if (expected) expect(isHIGH(v), `NAND-XOR(${a},${b}) → ${v}V (want HIGH)`).toBe(true);
|
||
else expect(isLOW(v), `NAND-XOR(${a},${b}) → ${v}V (want LOW)`).toBe(true);
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'Half adder: SUM = A XOR B, CARRY = A AND B',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-half-adder')!;
|
||
expect(ex, 'half adder present').toBeDefined();
|
||
const cases: Array<[0 | 1, 0 | 1, boolean, boolean]> = [
|
||
[0, 0, false, false],
|
||
[1, 0, true, false],
|
||
[0, 1, true, false],
|
||
[1, 1, false, true],
|
||
];
|
||
for (const [a, b, sum, car] of cases) {
|
||
const vS = await gateOutputV(ex, 'gSum', { sA: a, sB: b });
|
||
const vC = await gateOutputV(ex, 'gC', { sA: a, sB: b });
|
||
if (sum) expect(isHIGH(vS), `HA(${a},${b}).SUM → ${vS}V (want HIGH)`).toBe(true);
|
||
else expect(isLOW(vS), `HA(${a},${b}).SUM → ${vS}V (want LOW)`).toBe(true);
|
||
if (car) expect(isHIGH(vC), `HA(${a},${b}).CARRY → ${vC}V (want HIGH)`).toBe(true);
|
||
else expect(isLOW(vC), `HA(${a},${b}).CARRY → ${vC}V (want LOW)`).toBe(true);
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'Full adder: SUM and Cout match A+B+Cin',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-full-adder')!;
|
||
expect(ex, 'full adder present').toBeDefined();
|
||
for (let a = 0 as 0 | 1; a <= 1; a = (a + 1) as 0 | 1) {
|
||
for (let b = 0 as 0 | 1; b <= 1; b = (b + 1) as 0 | 1) {
|
||
for (let ci = 0 as 0 | 1; ci <= 1; ci = (ci + 1) as 0 | 1) {
|
||
const total = a + b + ci;
|
||
const expSum = total & 1;
|
||
const expCo = total >> 1;
|
||
const vS = await gateOutputV(ex, 'x2', { sA: a, sB: b, sCi: ci });
|
||
const vC = await gateOutputV(ex, 'orC', { sA: a, sB: b, sCi: ci });
|
||
if (expSum)
|
||
expect(isHIGH(vS), `FA(${a},${b},${ci}).SUM → ${vS}V (want HIGH)`).toBe(true);
|
||
else expect(isLOW(vS), `FA(${a},${b},${ci}).SUM → ${vS}V (want LOW)`).toBe(true);
|
||
if (expCo)
|
||
expect(isHIGH(vC), `FA(${a},${b},${ci}).Cout → ${vC}V (want HIGH)`).toBe(true);
|
||
else expect(isLOW(vC), `FA(${a},${b},${ci}).Cout → ${vC}V (want LOW)`).toBe(true);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'Majority voter: HIGH iff ≥ 2 of 3 inputs HIGH',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-majority-voter')!;
|
||
expect(ex, 'majority voter present').toBeDefined();
|
||
for (let a = 0 as 0 | 1; a <= 1; a = (a + 1) as 0 | 1) {
|
||
for (let b = 0 as 0 | 1; b <= 1; b = (b + 1) as 0 | 1) {
|
||
for (let c = 0 as 0 | 1; c <= 1; c = (c + 1) as 0 | 1) {
|
||
const expected = a + b + c >= 2;
|
||
const v = await gateOutputV(ex, 'or3', { sA: a, sB: b, sC: c });
|
||
if (expected) expect(isHIGH(v), `MAJ(${a},${b},${c}) → ${v}V (want HIGH)`).toBe(true);
|
||
else expect(isLOW(v), `MAJ(${a},${b},${c}) → ${v}V (want LOW)`).toBe(true);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'2-to-1 MUX: SEL routes D0 or D1 to Y',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-mux-2to1')!;
|
||
expect(ex, '2-to-1 MUX present').toBeDefined();
|
||
// SEL = 0 → Y = D0; SEL = 1 → Y = D1
|
||
const cases: Array<[0 | 1, 0 | 1, 0 | 1, boolean]> = [
|
||
[0, 0, 0, false],
|
||
[0, 1, 0, true],
|
||
[0, 0, 1, false],
|
||
[0, 1, 1, true],
|
||
[1, 0, 0, false],
|
||
[1, 1, 0, false],
|
||
[1, 0, 1, true],
|
||
[1, 1, 1, true],
|
||
];
|
||
for (const [sel, d0, d1, expected] of cases) {
|
||
const v = await gateOutputV(ex, 'orY', { sSel: sel, sD0: d0, sD1: d1 });
|
||
if (expected)
|
||
expect(isHIGH(v), `MUX(sel=${sel},d0=${d0},d1=${d1}) → ${v}V HIGH`).toBe(true);
|
||
else expect(isLOW(v), `MUX(sel=${sel},d0=${d0},d1=${d1}) → ${v}V LOW`).toBe(true);
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'2-to-4 decoder: exactly one output HIGH per input combination',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-decoder-2to4')!;
|
||
expect(ex, '2-to-4 decoder present').toBeDefined();
|
||
const outs = ['a0', 'a1', 'a2', 'a3']; // Y0..Y3 gates
|
||
for (let a = 0 as 0 | 1; a <= 1; a = (a + 1) as 0 | 1) {
|
||
for (let b = 0 as 0 | 1; b <= 1; b = (b + 1) as 0 | 1) {
|
||
const expectedIdx = (b << 1) | a;
|
||
for (let i = 0; i < 4; i++) {
|
||
const v = await gateOutputV(ex, outs[i], { sA: a, sB: b });
|
||
if (i === expectedIdx)
|
||
expect(isHIGH(v), `dec(A=${a},B=${b}) Y${i} → ${v}V (want HIGH)`).toBe(true);
|
||
else expect(isLOW(v), `dec(A=${a},B=${b}) Y${i} → ${v}V (want LOW)`).toBe(true);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'Hamming(7,4): p1 = D0 XOR D1 XOR D3 (spot check)',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-hamming-encoder-74')!;
|
||
expect(ex, 'Hamming encoder present').toBeDefined();
|
||
// Spot-check four points of the 16-row truth table for p1.
|
||
const cases: Array<[0 | 1, 0 | 1, 0 | 1, 0 | 1, boolean]> = [
|
||
[0, 0, 0, 0, false],
|
||
[1, 0, 0, 0, true],
|
||
[0, 1, 0, 0, true],
|
||
[1, 1, 0, 1, true],
|
||
];
|
||
for (const [d0, d1, d2, d3, expected] of cases) {
|
||
const v = await gateOutputV(ex, 'hmP1b', { hmD0: d0, hmD1: d1, hmD2: d2, hmD3: d3 });
|
||
if (expected)
|
||
expect(isHIGH(v), `p1(D=${d3}${d2}${d1}${d0}) → ${v}V (want HIGH)`).toBe(true);
|
||
else expect(isLOW(v), `p1(D=${d3}${d2}${d1}${d0}) → ${v}V (want LOW)`).toBe(true);
|
||
}
|
||
},
|
||
);
|
||
});
|
||
|
||
// ─── Truth-table verification for the BIG advanced examples ────────────────
|
||
// These were only smoke-tested for SPICE convergence before. We now run a
|
||
// handful of input combinations through each large network and confirm the
|
||
// gate outputs match the textbook truth table. Each test is bounded to a
|
||
// few cases so the SPICE solve count per CI run stays reasonable.
|
||
describe('digitalExamples — advanced truth tables', () => {
|
||
it(
|
||
'4-bit ripple-carry adder: sum bits match A+B+Cin',
|
||
{ timeout: 60_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-ripple-adder-4bit')!;
|
||
expect(ex, 'ripple adder present').toBeDefined();
|
||
type Bit = 0 | 1;
|
||
const cases: Array<{ A: number; B: number; Ci: Bit }> = [
|
||
{ A: 0b0000, B: 0b0000, Ci: 0 },
|
||
{ A: 0b0001, B: 0b0001, Ci: 0 }, // 1+1=2
|
||
{ A: 0b0011, B: 0b0101, Ci: 0 }, // 3+5=8
|
||
{ A: 0b0111, B: 0b1000, Ci: 0 }, // 7+8=15
|
||
{ A: 0b1111, B: 0b0001, Ci: 0 }, // 15+1=16 → Cout
|
||
];
|
||
for (const tc of cases) {
|
||
const total = tc.A + tc.B + tc.Ci;
|
||
const switches: Record<string, Bit> = { sCin: tc.Ci };
|
||
for (let i = 0; i < 4; i++) {
|
||
switches[`sA${i}`] = ((tc.A >> i) & 1) as Bit;
|
||
switches[`sB${i}`] = ((tc.B >> i) & 1) as Bit;
|
||
}
|
||
for (let i = 0; i < 4; i++) {
|
||
const v = await gateOutputV(ex, `x2_${i}`, switches);
|
||
const want = (total >> i) & 1;
|
||
if (want)
|
||
expect(isHIGH(v), `RCA(${tc.A}+${tc.B}+${tc.Ci}).S${i} → ${v}V (want HIGH)`).toBe(true);
|
||
else
|
||
expect(isLOW(v), `RCA(${tc.A}+${tc.B}+${tc.Ci}).S${i} → ${v}V (want LOW)`).toBe(true);
|
||
}
|
||
const cout = (total >> 4) & 1;
|
||
const vC = await gateOutputV(ex, 'orC_3', switches);
|
||
if (cout)
|
||
expect(isHIGH(vC), `RCA(${tc.A}+${tc.B}).Cout → ${vC}V (want HIGH)`).toBe(true);
|
||
else expect(isLOW(vC), `RCA(${tc.A}+${tc.B}).Cout → ${vC}V (want LOW)`).toBe(true);
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'2-bit × 2-bit multiplier: product bits match A × B',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-multiplier-2x2')!;
|
||
expect(ex, 'multiplier present').toBeDefined();
|
||
type Bit = 0 | 1;
|
||
const cases: Array<{ A: number; B: number }> = [
|
||
{ A: 0, B: 0 },
|
||
{ A: 2, B: 3 }, // 6
|
||
{ A: 3, B: 3 }, // 9
|
||
{ A: 3, B: 2 }, // 6
|
||
];
|
||
for (const { A, B } of cases) {
|
||
const product = A * B;
|
||
const switches: Record<string, Bit> = {
|
||
sA0: (A & 1) as Bit,
|
||
sA1: ((A >> 1) & 1) as Bit,
|
||
sB0: (B & 1) as Bit,
|
||
sB1: ((B >> 1) & 1) as Bit,
|
||
};
|
||
const probes: Array<[string, number]> = [
|
||
['pA0B0', 0],
|
||
['p1Sum', 1],
|
||
['p2Sum', 2],
|
||
['p3Car', 3],
|
||
];
|
||
for (const [gate, bit] of probes) {
|
||
const v = await gateOutputV(ex, gate, switches);
|
||
const want = (product >> bit) & 1;
|
||
if (want)
|
||
expect(isHIGH(v), `${A}×${B}=${product}, P${bit} → ${v}V (want HIGH)`).toBe(true);
|
||
else
|
||
expect(isLOW(v), `${A}×${B}=${product}, P${bit} → ${v}V (want LOW)`).toBe(true);
|
||
}
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'4-bit popcount: bit count of X3..X0 matches output',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-popcount-4bit')!;
|
||
expect(ex, 'popcount present').toBeDefined();
|
||
type Bit = 0 | 1;
|
||
const cases: Array<{ in: number; count: number }> = [
|
||
{ in: 0b0000, count: 0 },
|
||
{ in: 0b0001, count: 1 },
|
||
{ in: 0b0101, count: 2 },
|
||
{ in: 0b0111, count: 3 },
|
||
{ in: 0b1111, count: 4 },
|
||
];
|
||
const probes: Array<[string, number]> = [
|
||
['pcSC', 0],
|
||
['pcFA_x2', 1],
|
||
['pcFA_or', 2],
|
||
];
|
||
for (const { in: inVal, count } of cases) {
|
||
const switches: Record<string, Bit> = {};
|
||
for (let i = 0; i < 4; i++) switches[`pcX${i}`] = ((inVal >> i) & 1) as Bit;
|
||
for (const [gate, bit] of probes) {
|
||
const v = await gateOutputV(ex, gate, switches);
|
||
const want = (count >> bit) & 1;
|
||
if (want)
|
||
expect(isHIGH(v), `popcount(${inVal.toString(2)})=${count}, bit${bit} → ${v}V HIGH`).toBe(true);
|
||
else
|
||
expect(isLOW(v), `popcount(${inVal.toString(2)})=${count}, bit${bit} → ${v}V LOW`).toBe(true);
|
||
}
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'3-to-8 decoder: exactly Y_i is HIGH for input i',
|
||
{ timeout: 60_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-decoder-3to8')!;
|
||
expect(ex, '3-to-8 decoder present').toBeDefined();
|
||
type Bit = 0 | 1;
|
||
for (let n = 0; n < 8; n++) {
|
||
const switches: Record<string, Bit> = {
|
||
dec3A0: (n & 1) as Bit,
|
||
dec3A1: ((n >> 1) & 1) as Bit,
|
||
dec3A2: ((n >> 2) & 1) as Bit,
|
||
};
|
||
for (let i = 0; i < 8; i++) {
|
||
const v = await gateOutputV(ex, `dec3Y${i}`, switches);
|
||
if (i === n)
|
||
expect(isHIGH(v), `dec(in=${n}) Y${i} → ${v}V (want HIGH)`).toBe(true);
|
||
else expect(isLOW(v), `dec(in=${n}) Y${i} → ${v}V (want LOW)`).toBe(true);
|
||
}
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'4-bit magnitude comparator: A>B / A=B / A<B',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-comparator-4bit')!;
|
||
expect(ex, '4-bit comparator present').toBeDefined();
|
||
type Bit = 0 | 1;
|
||
const cases: Array<{ A: number; B: number; gt: boolean; eq: boolean; lt: boolean }> = [
|
||
{ A: 0, B: 0, gt: false, eq: true, lt: false },
|
||
{ A: 5, B: 3, gt: true, eq: false, lt: false },
|
||
{ A: 3, B: 5, gt: false, eq: false, lt: true },
|
||
{ A: 15, B: 0, gt: true, eq: false, lt: false },
|
||
{ A: 0, B: 15, gt: false, eq: false, lt: true },
|
||
{ A: 10, B: 10, gt: false, eq: true, lt: false },
|
||
];
|
||
for (const { A, B, gt, eq, lt } of cases) {
|
||
const switches: Record<string, Bit> = {};
|
||
for (let i = 0; i < 4; i++) {
|
||
switches[`cmpA${i}`] = ((A >> i) & 1) as Bit;
|
||
switches[`cmpB${i}`] = ((B >> i) & 1) as Bit;
|
||
}
|
||
const vGt = await gateOutputV(ex, 'cmpGtAll', switches);
|
||
const vEq = await gateOutputV(ex, 'cmpEqAll', switches);
|
||
const vLt = await gateOutputV(ex, 'cmpLt', switches);
|
||
if (gt) expect(isHIGH(vGt), `cmp(${A}>${B}) → ${vGt}V HIGH`).toBe(true);
|
||
else expect(isLOW(vGt), `cmp(${A}>${B}) → ${vGt}V LOW`).toBe(true);
|
||
if (eq) expect(isHIGH(vEq), `cmp(${A}=${B}) → ${vEq}V HIGH`).toBe(true);
|
||
else expect(isLOW(vEq), `cmp(${A}=${B}) → ${vEq}V LOW`).toBe(true);
|
||
if (lt) expect(isHIGH(vLt), `cmp(${A}<${B}) → ${vLt}V HIGH`).toBe(true);
|
||
else expect(isLOW(vLt), `cmp(${A}<${B}) → ${vLt}V LOW`).toBe(true);
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'4-bit adder/subtractor: M=0 adds, M=1 subtracts (two\'s complement)',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-adder-subtractor-4bit')!;
|
||
expect(ex, 'adder/subtractor present').toBeDefined();
|
||
type Bit = 0 | 1;
|
||
// Addition: 5+3=8 ; 7+8=15
|
||
// Subtraction: 5-3=2 ; 8-5=3 (two's complement gives 4-bit + Cout=1)
|
||
const cases: Array<{ A: number; B: number; M: Bit; result: number }> = [
|
||
{ A: 5, B: 3, M: 0, result: 8 },
|
||
{ A: 7, B: 8, M: 0, result: 15 },
|
||
{ A: 5, B: 3, M: 1, result: 2 },
|
||
{ A: 8, B: 5, M: 1, result: 3 },
|
||
];
|
||
for (const { A, B, M, result } of cases) {
|
||
const switches: Record<string, Bit> = { asM: M };
|
||
for (let i = 0; i < 4; i++) {
|
||
switches[`asA${i}`] = ((A >> i) & 1) as Bit;
|
||
switches[`asB${i}`] = ((B >> i) & 1) as Bit;
|
||
}
|
||
for (let i = 0; i < 4; i++) {
|
||
const v = await gateOutputV(ex, `asX2_${i}`, switches);
|
||
const want = (result >> i) & 1;
|
||
const op = M ? `${A}-${B}=${result}` : `${A}+${B}=${result}`;
|
||
if (want)
|
||
expect(isHIGH(v), `${op}, bit ${i} → ${v}V HIGH`).toBe(true);
|
||
else expect(isLOW(v), `${op}, bit ${i} → ${v}V LOW`).toBe(true);
|
||
}
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'1-bit ALU slice: mode select drives AND / OR / XOR / ADD',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-alu-slice-1bit')!;
|
||
expect(ex, 'ALU slice present').toBeDefined();
|
||
type Bit = 0 | 1;
|
||
const switches = (a: Bit, b: Bit, ci: Bit, m0: Bit, m1: Bit): Record<string, Bit> => ({
|
||
aluA: a,
|
||
aluB: b,
|
||
aluCi: ci,
|
||
aluM0: m0,
|
||
aluM1: m1,
|
||
});
|
||
// M1 M0 = 00 → AND
|
||
let v = await gateOutputV(ex, 'aluY', switches(1, 1, 0, 0, 0));
|
||
expect(isHIGH(v), `ALU AND(1,1) → ${v}V HIGH`).toBe(true);
|
||
v = await gateOutputV(ex, 'aluY', switches(1, 0, 0, 0, 0));
|
||
expect(isLOW(v), `ALU AND(1,0) → ${v}V LOW`).toBe(true);
|
||
// M1 M0 = 01 → OR
|
||
v = await gateOutputV(ex, 'aluY', switches(0, 1, 0, 1, 0));
|
||
expect(isHIGH(v), `ALU OR(0,1) → ${v}V HIGH`).toBe(true);
|
||
v = await gateOutputV(ex, 'aluY', switches(0, 0, 0, 1, 0));
|
||
expect(isLOW(v), `ALU OR(0,0) → ${v}V LOW`).toBe(true);
|
||
// M1 M0 = 10 → XOR
|
||
v = await gateOutputV(ex, 'aluY', switches(1, 0, 0, 0, 1));
|
||
expect(isHIGH(v), `ALU XOR(1,0) → ${v}V HIGH`).toBe(true);
|
||
v = await gateOutputV(ex, 'aluY', switches(1, 1, 0, 0, 1));
|
||
expect(isLOW(v), `ALU XOR(1,1) → ${v}V LOW`).toBe(true);
|
||
// M1 M0 = 11 → ADD; 1+1+0 = 10 → sum bit LOW, Cout HIGH
|
||
v = await gateOutputV(ex, 'aluY', switches(1, 1, 0, 1, 1));
|
||
expect(isLOW(v), `ALU ADD(1+1).sum → ${v}V LOW`).toBe(true);
|
||
const vCo = await gateOutputV(ex, 'aluCout', switches(1, 1, 0, 1, 1));
|
||
expect(isHIGH(vCo), `ALU ADD(1+1).Cout → ${vCo}V HIGH`).toBe(true);
|
||
},
|
||
);
|
||
|
||
it(
|
||
'4-bit carry-lookahead adder: matches the ripple-carry truth table',
|
||
{ timeout: 60_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-carry-lookahead-adder-4bit')!;
|
||
expect(ex, 'CLA present').toBeDefined();
|
||
type Bit = 0 | 1;
|
||
const cases: Array<{ A: number; B: number; Ci: Bit }> = [
|
||
{ A: 0b0000, B: 0b0000, Ci: 0 },
|
||
{ A: 0b0110, B: 0b0011, Ci: 0 }, // 6+3=9
|
||
{ A: 0b1010, B: 0b0101, Ci: 1 }, // 10+5+1=16 → Cout
|
||
{ A: 0b1111, B: 0b1111, Ci: 0 }, // 15+15=30 = 0b11110
|
||
];
|
||
for (const { A, B, Ci } of cases) {
|
||
const total = A + B + Ci;
|
||
const switches: Record<string, Bit> = { claC0: Ci };
|
||
for (let i = 0; i < 4; i++) {
|
||
switches[`claA${i}`] = ((A >> i) & 1) as Bit;
|
||
switches[`claB${i}`] = ((B >> i) & 1) as Bit;
|
||
}
|
||
for (let i = 0; i < 4; i++) {
|
||
const v = await gateOutputV(ex, `claS${i}`, switches);
|
||
const want = (total >> i) & 1;
|
||
if (want)
|
||
expect(isHIGH(v), `CLA(${A}+${B}+${Ci}).S${i} → ${v}V HIGH`).toBe(true);
|
||
else
|
||
expect(isLOW(v), `CLA(${A}+${B}+${Ci}).S${i} → ${v}V LOW`).toBe(true);
|
||
}
|
||
const cout = (total >> 4) & 1;
|
||
const vC = await gateOutputV(ex, 'claC4', switches);
|
||
if (cout) expect(isHIGH(vC), `CLA Cout → ${vC}V HIGH`).toBe(true);
|
||
else expect(isLOW(vC), `CLA Cout → ${vC}V LOW`).toBe(true);
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'8-to-3 priority encoder: Y outputs encode the active input',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-priority-encoder-8to3')!;
|
||
expect(ex, 'priority encoder present').toBeDefined();
|
||
type Bit = 0 | 1;
|
||
// Single-input-active cases — the encoded value equals the input index.
|
||
const cases = [1, 3, 5, 7];
|
||
for (const idx of cases) {
|
||
const switches: Record<string, Bit> = {};
|
||
for (let i = 0; i < 8; i++) switches[`pe${i}`] = (i === idx ? 1 : 0) as Bit;
|
||
const vY0 = await gateOutputV(ex, 'peY0', switches);
|
||
const vY1 = await gateOutputV(ex, 'peY1', switches);
|
||
const vY2 = await gateOutputV(ex, 'peY2', switches);
|
||
const wantY0 = idx & 1;
|
||
const wantY1 = (idx >> 1) & 1;
|
||
const wantY2 = (idx >> 2) & 1;
|
||
if (wantY0) expect(isHIGH(vY0), `PE(in=${idx}).Y0 → ${vY0}V HIGH`).toBe(true);
|
||
else expect(isLOW(vY0), `PE(in=${idx}).Y0 → ${vY0}V LOW`).toBe(true);
|
||
if (wantY1) expect(isHIGH(vY1), `PE(in=${idx}).Y1 → ${vY1}V HIGH`).toBe(true);
|
||
else expect(isLOW(vY1), `PE(in=${idx}).Y1 → ${vY1}V LOW`).toBe(true);
|
||
if (wantY2) expect(isHIGH(vY2), `PE(in=${idx}).Y2 → ${vY2}V HIGH`).toBe(true);
|
||
else expect(isLOW(vY2), `PE(in=${idx}).Y2 → ${vY2}V LOW`).toBe(true);
|
||
}
|
||
},
|
||
);
|
||
|
||
it(
|
||
'Hamming(7,4) encoder: complete parity check on all 16 patterns (sampled)',
|
||
{ timeout: 30_000 },
|
||
async () => {
|
||
const ex = digitalExamples.find((e) => e.id === 'digital-hamming-encoder-74')!;
|
||
expect(ex, 'Hamming encoder present').toBeDefined();
|
||
type Bit = 0 | 1;
|
||
// Sample 6 of the 16 input vectors. p1, p2, p4 must each equal the
|
||
// XOR of their respective data-bit subsets.
|
||
const samples = [0b0000, 0b0001, 0b0010, 0b0111, 0b1001, 0b1111];
|
||
for (const data of samples) {
|
||
const d: [Bit, Bit, Bit, Bit] = [
|
||
(data & 1) as Bit,
|
||
((data >> 1) & 1) as Bit,
|
||
((data >> 2) & 1) as Bit,
|
||
((data >> 3) & 1) as Bit,
|
||
];
|
||
const switches: Record<string, Bit> = {
|
||
hmD0: d[0],
|
||
hmD1: d[1],
|
||
hmD2: d[2],
|
||
hmD3: d[3],
|
||
};
|
||
const expP1 = (d[0] ^ d[1] ^ d[3]) as Bit;
|
||
const expP2 = (d[0] ^ d[2] ^ d[3]) as Bit;
|
||
const expP4 = (d[1] ^ d[2] ^ d[3]) as Bit;
|
||
const vP1 = await gateOutputV(ex, 'hmP1b', switches);
|
||
const vP2 = await gateOutputV(ex, 'hmP2b', switches);
|
||
const vP4 = await gateOutputV(ex, 'hmP4b', switches);
|
||
if (expP1) expect(isHIGH(vP1), `p1(D=${data.toString(2)}) → ${vP1}V HIGH`).toBe(true);
|
||
else expect(isLOW(vP1), `p1(D=${data.toString(2)}) → ${vP1}V LOW`).toBe(true);
|
||
if (expP2) expect(isHIGH(vP2), `p2(D=${data.toString(2)}) → ${vP2}V HIGH`).toBe(true);
|
||
else expect(isLOW(vP2), `p2(D=${data.toString(2)}) → ${vP2}V LOW`).toBe(true);
|
||
if (expP4) expect(isHIGH(vP4), `p4(D=${data.toString(2)}) → ${vP4}V HIGH`).toBe(true);
|
||
else expect(isLOW(vP4), `p4(D=${data.toString(2)}) → ${vP4}V LOW`).toBe(true);
|
||
}
|
||
},
|
||
);
|
||
});
|