feat(digital-gate-engine): Phase 3 core - digital/analog boundary handoff
buildMixedNetwork evaluates the gate (digital) side of a MIXED circuit on the settle kernel and exposes the boundary with the analog (ngspice) domain. Unlike buildDigitalNetwork it does not bail on non-primitive components - those are the analog side; their pins mark the nets they touch as boundary. Exposes boundaryNets, readBoundary(net) (digital->analog: the gate-driven level to seed an ngspice voltage source) and setBoundaryInput(net, level) (analog->digital: ngspice's solved+thresholded level, which re-evaluates downstream gates). Test digitalgate-mixed-boundary (4): the boundary nets are exactly the digital/analog bridges; both directions track; a digital->analog->digital coupler loop converges. No ngspice needed - the analog side is supplied by the test. Wiring the handoff to the live ngspice netlist (0/Vcc sources + threshold + settle<->solve iteration) is the remaining step; it needs the running solver (the node loader is broken by a pre-existing path bug) and a mixed example. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4f4ee0bf60
commit
77c85eefac
|
|
@ -0,0 +1,84 @@
|
||||||
|
/**
|
||||||
|
* digital-gate-engine Phase 3 (core) — the digital/analog boundary handoff.
|
||||||
|
*
|
||||||
|
* A mixed circuit: a switch drives a NOT gate whose output feeds an ANALOG
|
||||||
|
* device (a BJT, here a stand-in), and another analog node feeds a second NOT
|
||||||
|
* gate. buildMixedNetwork evaluates the gate (digital) side on the settle kernel
|
||||||
|
* and exposes the boundary nets where the two motors hand off:
|
||||||
|
* - digital -> analog: readBoundary() gives the gate-driven level to seed an
|
||||||
|
* ngspice voltage source.
|
||||||
|
* - analog -> digital: setBoundaryInput() pushes ngspice's solved+thresholded
|
||||||
|
* node level onto the net so downstream gates re-evaluate.
|
||||||
|
*
|
||||||
|
* Verifiable with NO ngspice (the analog side is supplied here). Wiring it to
|
||||||
|
* the live solver is the follow-up — the node ngspice loader is broken by a
|
||||||
|
* pre-existing path bug, so that step is browser-verified.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { resetBusNets } from '../simulation/customChips/busNets';
|
||||||
|
import { buildMixedNetwork, type DigitalComponent, type DigitalWire } from '../simulation/digital/digitalGateEngine';
|
||||||
|
|
||||||
|
beforeEach(() => resetBusNets());
|
||||||
|
|
||||||
|
// switch -> NOT(g1) -> [BJT a1] -> NOT(g2) -> out
|
||||||
|
const components: DigitalComponent[] = [
|
||||||
|
{ id: 'src', metadataId: 'signal-generator' },
|
||||||
|
{ id: 'sw1', metadataId: 'slide-switch', properties: { value: 0 } },
|
||||||
|
{ id: 'g1', metadataId: 'logic-gate-not' },
|
||||||
|
{ id: 'a1', metadataId: 'bjt-npn' }, // analog (non-primitive)
|
||||||
|
{ id: 'g2', metadataId: 'logic-gate-not' },
|
||||||
|
];
|
||||||
|
const W = (a: string, ap: string, b: string, bp: string): DigitalWire => ({ start: { componentId: a, pinName: ap }, end: { componentId: b, pinName: bp } });
|
||||||
|
const wires: DigitalWire[] = [
|
||||||
|
W('src', 'SIG', 'sw1', '1'),
|
||||||
|
W('sw1', '2', 'g1', 'A'),
|
||||||
|
W('g1', 'Y', 'a1', 'B'), // boundary OUT (digital drives, analog reads)
|
||||||
|
W('a1', 'C', 'g2', 'A'), // boundary IN (analog drives, digital reads)
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('digital-gate-engine Phase 3 — mixed boundary', () => {
|
||||||
|
it('identifies exactly the two nets that bridge digital and analog', () => {
|
||||||
|
const net = buildMixedNetwork(components, wires);
|
||||||
|
expect(net.ok).toBe(true);
|
||||||
|
const outBoundary = net.netOf('g1', 'Y');
|
||||||
|
const inBoundary = net.netOf('a1', 'C');
|
||||||
|
expect(outBoundary).toBeDefined();
|
||||||
|
expect(inBoundary).toBeDefined();
|
||||||
|
expect(new Set(net.boundaryNets)).toEqual(new Set([outBoundary, inBoundary]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('digital -> analog: the gate-driven boundary level tracks the switch', () => {
|
||||||
|
const net = buildMixedNetwork(components, wires);
|
||||||
|
const out = net.netOf('g1', 'Y')!;
|
||||||
|
net.setSwitch('sw1', 1); // NOT(1) = 0
|
||||||
|
expect(net.readBoundary(out), 'sw=1 -> NOT -> 0').toBe(0);
|
||||||
|
net.setSwitch('sw1', 0); // NOT(0) = 1
|
||||||
|
expect(net.readBoundary(out), 'sw=0 -> NOT -> 1').toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('analog -> digital: pushing a boundary level re-evaluates the gate', () => {
|
||||||
|
const net = buildMixedNetwork(components, wires);
|
||||||
|
const inNet = net.netOf('a1', 'C')!;
|
||||||
|
const out = net.netOf('g2', 'Y')!;
|
||||||
|
net.setBoundaryInput(inNet, 1); // NOT(1) = 0
|
||||||
|
expect(net.readNet(out), 'analog 1 -> NOT -> 0').toBe(0);
|
||||||
|
net.setBoundaryInput(inNet, 0); // NOT(0) = 1
|
||||||
|
expect(net.readNet(out), 'analog 0 -> NOT -> 1').toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a digital->analog->digital chain converges like a coupler iteration', () => {
|
||||||
|
// Coupler loop: read the digital-driven boundary, "solve" the analog (here an
|
||||||
|
// ideal wire: collector follows base), push it back, read the final output.
|
||||||
|
const net = buildMixedNetwork(components, wires);
|
||||||
|
const outB = net.netOf('g1', 'Y')!;
|
||||||
|
const inB = net.netOf('a1', 'C')!;
|
||||||
|
const finalOut = net.netOf('g2', 'Y')!;
|
||||||
|
for (const sw of [0, 1, 0, 1] as const) {
|
||||||
|
net.setSwitch('sw1', sw);
|
||||||
|
const analogIn = net.readBoundary(outB); // g1 = NOT(sw)
|
||||||
|
net.setBoundaryInput(inB, analogIn); // ideal analog: C = B
|
||||||
|
// g2 = NOT(analogIn) = NOT(NOT(sw)) = sw
|
||||||
|
expect(net.readNet(finalOut), `chain sw=${sw}`).toBe(sw);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -298,3 +298,123 @@ export function buildDigitalNetwork(
|
||||||
ledIds,
|
ledIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Phase 3: mixed digital/analog boundary ──────────────────────────────────
|
||||||
|
|
||||||
|
export interface MixedNetwork {
|
||||||
|
ok: boolean;
|
||||||
|
pinManager: PinManager;
|
||||||
|
netOf(componentId: string, pin: string): number | undefined;
|
||||||
|
readNet(net: number): 0 | 1;
|
||||||
|
setSwitch(switchId: string, value: 0 | 1): void;
|
||||||
|
/** Nets that bridge a digital pin (gate/switch) and an analog pin. These are
|
||||||
|
* where the two motors hand off. */
|
||||||
|
boundaryNets: number[];
|
||||||
|
/** Digital-side level of a boundary net — what to drive into ngspice as a
|
||||||
|
* 0 / Vcc voltage source on that node (digital -> analog). */
|
||||||
|
readBoundary(net: number): 0 | 1;
|
||||||
|
/** Push an analog-side level (ngspice's threshold-converted node voltage) onto
|
||||||
|
* a boundary net so the gates downstream re-evaluate (analog -> digital). */
|
||||||
|
setBoundaryInput(net: number, level: 0 | 1): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the digital half of a MIXED circuit and expose its boundary with the
|
||||||
|
* analog (ngspice) domain. Unlike buildDigitalNetwork it does NOT bail on
|
||||||
|
* non-primitive components — those are the analog side; their pins simply mark
|
||||||
|
* the nets they touch as boundary. The caller (the ngspice coupler) reads the
|
||||||
|
* digital-driven boundary nets to seed voltage sources, and pushes ngspice's
|
||||||
|
* solved+thresholded boundary voltages back via setBoundaryInput, iterating to a
|
||||||
|
* fixed point. Settle on the digital side is the same exact kernel as the
|
||||||
|
* all-digital path.
|
||||||
|
*
|
||||||
|
* Phase 3 core: the boundary handoff + digital settle, verifiable headlessly
|
||||||
|
* (the analog side is supplied by the test). Wiring it to the live ngspice
|
||||||
|
* netlist is the follow-up (needs the browser solver; the node loader is broken
|
||||||
|
* by a pre-existing path bug).
|
||||||
|
*/
|
||||||
|
export function buildMixedNetwork(
|
||||||
|
components: DigitalComponent[],
|
||||||
|
wires: DigitalWire[],
|
||||||
|
pinManager?: PinManager,
|
||||||
|
): MixedNetwork {
|
||||||
|
const pm = pinManager ?? new PinManager();
|
||||||
|
const byId = new Map(components.map((c) => [c.id, c]));
|
||||||
|
const uf = new UnionFind();
|
||||||
|
for (const w of wires) uf.union(epKey(w.start.componentId, w.start.pinName), epKey(w.end.componentId, w.end.pinName));
|
||||||
|
const pinNet = (id: string, pin: string) => uf.find(epKey(id, pin));
|
||||||
|
|
||||||
|
const gndRoots = new Set<string>();
|
||||||
|
const railRoots = new Set<string>();
|
||||||
|
for (const c of components) {
|
||||||
|
if (isPower(kindOf(c))) { gndRoots.add(pinNet(c.id, 'GND')); railRoots.add(pinNet(c.id, 'SIG')); }
|
||||||
|
}
|
||||||
|
const isGnd = (r: string) => gndRoots.has(r);
|
||||||
|
const isRail = (r: string) => railRoots.has(r);
|
||||||
|
|
||||||
|
const keyOf = new Map<string, number>();
|
||||||
|
let nextKey = 1;
|
||||||
|
const netKey = (id: string, pin: string): number => {
|
||||||
|
const root = pinNet(id, pin);
|
||||||
|
let k = keyOf.get(root);
|
||||||
|
if (k === undefined) { k = nextKey++; keyOf.set(root, k); }
|
||||||
|
return k;
|
||||||
|
};
|
||||||
|
const netOf = (id: string, pin: string): number | undefined => (byId.has(id) ? netKey(id, pin) : undefined);
|
||||||
|
|
||||||
|
// Classify each net root by who touches it, walking wire endpoints.
|
||||||
|
const hasDigital = new Set<string>();
|
||||||
|
const hasAnalog = new Set<string>();
|
||||||
|
const mark = (compId: string, pin: string) => {
|
||||||
|
const c = byId.get(compId);
|
||||||
|
if (!c) return;
|
||||||
|
const k = kindOf(c);
|
||||||
|
const root = pinNet(compId, pin);
|
||||||
|
if (isGate(k) || isSwitch(k) || isLed(k)) hasDigital.add(root);
|
||||||
|
else if (!isPower(k) && !isResistor(k)) hasAnalog.add(root); // non-primitive = analog
|
||||||
|
};
|
||||||
|
for (const w of wires) { mark(w.start.componentId, w.start.pinName); mark(w.end.componentId, w.end.pinName); }
|
||||||
|
|
||||||
|
// Static rail/gnd + switches + gates (same model as the all-digital path).
|
||||||
|
for (const c of components) {
|
||||||
|
if (isPower(kindOf(c))) {
|
||||||
|
setBusDrive(pm, netKey(c.id, 'SIG'), `${c.id}::SIG`, STRONG(1));
|
||||||
|
setBusDrive(pm, netKey(c.id, 'GND'), `${c.id}::GND`, STRONG(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const switchState = new Map<string, 0 | 1>();
|
||||||
|
const driveSwitch = (c: DigitalComponent) => {
|
||||||
|
const closed = switchState.get(c.id) ?? (Number(c.properties?.value) === 1 ? 1 : 0);
|
||||||
|
const n1 = netKey(c.id, '1'), n2 = netKey(c.id, '2');
|
||||||
|
const [src, dst] = isRail(pinNet(c.id, '1')) ? [n1, n2] : [n2, n1];
|
||||||
|
if (closed) setBusDrive(pm, dst, `${c.id}::pass`, STRONG(pm.getPinState(src) ? 1 : 0));
|
||||||
|
else setBusDrive(pm, dst, `${c.id}::pass`, { value: 0, strength: Strength.HIGHZ });
|
||||||
|
};
|
||||||
|
for (const c of components) if (isSwitch(kindOf(c))) driveSwitch(c);
|
||||||
|
for (const c of components) {
|
||||||
|
if (!isGate(kindOf(c))) continue;
|
||||||
|
const spec = parseGate(kindOf(c));
|
||||||
|
if (!spec) continue;
|
||||||
|
const inNets = spec.inputs.map((p) => netKey(c.id, p));
|
||||||
|
const outNet = netKey(c.id, 'Y');
|
||||||
|
const st = inNets.map((n) => pm.getPinState(n));
|
||||||
|
const update = () => setBusDrive(pm, outNet, `${c.id}::Y`, STRONG(spec.fn(st) ? 1 : 0));
|
||||||
|
inNets.forEach((n, i) => pm.onPinChange(n, (_p, s) => { st[i] = s; update(); }));
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
for (const c of components) if (isSwitch(kindOf(c))) driveSwitch(c);
|
||||||
|
|
||||||
|
const boundaryRoots = [...hasDigital].filter((r) => hasAnalog.has(r));
|
||||||
|
const boundaryNets = boundaryRoots.map((root) => { const k = keyOf.get(root); return k ?? (keyOf.set(root, nextKey).get(root), nextKey++); });
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
pinManager: pm,
|
||||||
|
netOf,
|
||||||
|
readNet: (net) => (pm.getPinState(net) ? 1 : 0),
|
||||||
|
setSwitch: (switchId, value) => { switchState.set(switchId, value); const c = byId.get(switchId); if (c) driveSwitch(c); },
|
||||||
|
boundaryNets,
|
||||||
|
readBoundary: (net) => (pm.getPinState(net) ? 1 : 0),
|
||||||
|
setBoundaryInput: (net, level) => setBusDrive(pm, net, `analog::${net}`, STRONG(level)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue