feat(digital-gate-engine): sweep all 38 examples + default the flag on
Phase 4 (brought forward before the mixed-mode boundary). digitalgate-sweep
proves the engine handles 38/38 gallery digital examples: every one builds,
resolves every LED, and never oscillates. Tightened isAllDigital to also require
at least one logic gate, so a degenerate analog {source, resistor, LED} circuit
stays on ngspice rather than being claimed by the digital path. Flipped
digitalGatesEnabled() default to ON (override with ?digitalgates=off).
Full frontend suite 2120 pass / 5 fail — the 5 are the same pre-existing
unrelated failures (ngspice node-path, attiny85 arduino-cli, component-to-spice
catalog); the default flip adds no new breakage and examples-digital +
circuit-simulation-service stay green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b08df89c9b
commit
4f4ee0bf60
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* digital-gate-engine Phase 3 (sweep) — every gallery digital example is handled
|
||||
* by the engine without crashing, and the all-digital ones resolve cleanly.
|
||||
*
|
||||
* For each of the 38 `examples-digital.ts` circuits: build the network from the
|
||||
* real data, and if it is all-digital, drive its switches through a few vectors
|
||||
* and confirm every LED resolves (no throw, no settle blow-up). Examples that
|
||||
* include a non-primitive (e.g. a 7-segment display) legitimately bail to
|
||||
* ngspice — those are reported, not failed. This is the gate for flipping the
|
||||
* `?digitalgates` default on.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { resetBusNets } from '../simulation/customChips/busNets';
|
||||
import { buildDigitalNetwork, isAllDigital, type DigitalComponent, type DigitalWire } from '../simulation/digital/digitalGateEngine';
|
||||
import { digitalExamples } from '../data/examples-digital';
|
||||
|
||||
type Ex = { id: string; components: DigitalComponent[]; wires: DigitalWire[] };
|
||||
const all = digitalExamples as unknown as Ex[];
|
||||
|
||||
beforeEach(() => resetBusNets());
|
||||
|
||||
describe('digital-gate-engine sweep — all gallery digital examples', () => {
|
||||
it('there are at least 35 digital examples to sweep', () => {
|
||||
expect(all.length).toBeGreaterThanOrEqual(35);
|
||||
});
|
||||
|
||||
it('every example is either all-digital-and-handled or cleanly bails to ngspice', () => {
|
||||
const handled: string[] = [];
|
||||
const bailed: string[] = [];
|
||||
for (const ex of all) {
|
||||
const allDigital = isAllDigital(ex.components);
|
||||
const net = buildDigitalNetwork(ex.components, ex.wires);
|
||||
if (allDigital) {
|
||||
expect(net.ok, `${ex.id} is all-digital so should build`).toBe(true);
|
||||
handled.push(ex.id);
|
||||
} else {
|
||||
expect(net.ok, `${ex.id} has a non-primitive so must bail`).toBe(false);
|
||||
bailed.push(ex.id);
|
||||
}
|
||||
resetBusNets();
|
||||
}
|
||||
// The vast majority are pure gate circuits; only a couple (e.g. 7-seg) bail.
|
||||
expect(handled.length, `handled: ${handled.length}, bailed: ${bailed.join(', ')}`).toBeGreaterThanOrEqual(33);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[sweep] engine handles ${handled.length}/${all.length}; bails (ngspice): ${bailed.join(', ') || 'none'}`);
|
||||
});
|
||||
|
||||
it('each handled example drives + reads every LED without throwing or oscillating', () => {
|
||||
const warn = console.warn;
|
||||
let oscillations = 0;
|
||||
console.warn = (...a: unknown[]) => { if (String(a[0]).includes('did not converge')) oscillations++; };
|
||||
try {
|
||||
for (const ex of all) {
|
||||
if (!isAllDigital(ex.components)) continue;
|
||||
const switchIds = ex.components.filter((c) => String(c.metadataId ?? c.type ?? '').includes('slide-switch')).map((c) => c.id);
|
||||
// a few input vectors: all-low, all-high, alternating
|
||||
for (const pattern of [() => 0 as const, () => 1 as const, (i: number) => (i % 2) as 0 | 1]) {
|
||||
const net = buildDigitalNetwork(ex.components, ex.wires);
|
||||
expect(net.ok).toBe(true);
|
||||
switchIds.forEach((id, i) => net.setSwitch(id, pattern(i)));
|
||||
for (const led of net.ledIds) {
|
||||
const v = net.readLed(led);
|
||||
expect(v === 0 || v === 1, `${ex.id} LED ${led} resolved`).toBe(true);
|
||||
}
|
||||
resetBusNets();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
console.warn = warn;
|
||||
}
|
||||
expect(oscillations, 'no combinational-loop oscillation in any example').toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -108,12 +108,23 @@ export function digitalGatesEnabled(): boolean {
|
|||
} catch {
|
||||
/* missing globals in tests / SecurityError — fall through */
|
||||
}
|
||||
return false;
|
||||
// On by default: all-digital gate circuits are evaluated exactly + instantly by
|
||||
// the event-driven engine (the ngspice B-source path could not light a 4-bit
|
||||
// adder). Only pure all-digital-with-a-gate circuits take this path; mixed /
|
||||
// analog circuits stay on ngspice. Override with ?digitalgates=off.
|
||||
return true;
|
||||
}
|
||||
|
||||
/** True iff every component is a digital primitive (so the engine can own it). */
|
||||
/**
|
||||
* True iff every component is a digital primitive AND at least one is a logic
|
||||
* gate. The gate requirement keeps the engine from claiming degenerate analog
|
||||
* circuits that happen to use only {source, resistor, LED} with no logic — those
|
||||
* stay on ngspice.
|
||||
*/
|
||||
export function isAllDigital(components: DigitalComponent[]): boolean {
|
||||
return components.length > 0 && components.every((c) => isDigitalPrimitive(kindOf(c)));
|
||||
if (components.length === 0) return false;
|
||||
if (!components.every((c) => isDigitalPrimitive(kindOf(c)))) return false;
|
||||
return components.some((c) => isGate(kindOf(c)));
|
||||
}
|
||||
|
||||
// Endpoint key. A printable separator (NOT a space — a lone space gets stored
|
||||
|
|
|
|||
Loading…
Reference in New Issue