@vessel-dsp/compiler 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -2
- package/dist/bake.d.ts +38 -0
- package/dist/bake.d.ts.map +1 -0
- package/dist/bake.js +101 -0
- package/dist/bake.js.map +1 -0
- package/dist/bbd-clock.d.ts +171 -0
- package/dist/bbd-clock.d.ts.map +1 -0
- package/dist/bbd-clock.js +1750 -0
- package/dist/bbd-clock.js.map +1 -0
- package/dist/block-row.d.ts +51 -0
- package/dist/block-row.d.ts.map +1 -0
- package/dist/block-row.js +17 -0
- package/dist/block-row.js.map +1 -0
- package/dist/bypass.d.ts +3 -0
- package/dist/bypass.d.ts.map +1 -0
- package/dist/bypass.js +228 -0
- package/dist/bypass.js.map +1 -0
- package/dist/compile.d.ts +24 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +344 -0
- package/dist/compile.js.map +1 -0
- package/dist/converter-scale.d.ts +14 -0
- package/dist/converter-scale.d.ts.map +1 -0
- package/dist/converter-scale.js +133 -0
- package/dist/converter-scale.js.map +1 -0
- package/dist/couple.d.ts +28 -0
- package/dist/couple.d.ts.map +1 -0
- package/dist/couple.js +567 -0
- package/dist/couple.js.map +1 -0
- package/dist/dangling-active-terminal.d.ts +47 -0
- package/dist/dangling-active-terminal.d.ts.map +1 -0
- package/dist/dangling-active-terminal.js +542 -0
- package/dist/dangling-active-terminal.js.map +1 -0
- package/dist/declared-program.d.ts +23 -0
- package/dist/declared-program.d.ts.map +1 -0
- package/dist/declared-program.js +387 -0
- package/dist/declared-program.js.map +1 -0
- package/dist/denomination.d.ts +48 -0
- package/dist/denomination.d.ts.map +1 -0
- package/dist/denomination.js +27 -0
- package/dist/denomination.js.map +1 -0
- package/dist/device-laws.d.ts +237 -0
- package/dist/device-laws.d.ts.map +1 -0
- package/dist/device-laws.js +3497 -0
- package/dist/device-laws.js.map +1 -0
- package/dist/diagnostics.d.ts +13 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +19 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/emit.d.ts +8 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/emit.js +43 -0
- package/dist/emit.js.map +1 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/errors.js.map +1 -0
- package/dist/firmware-class.d.ts +69 -0
- package/dist/firmware-class.d.ts.map +1 -0
- package/dist/firmware-class.js +20 -0
- package/dist/firmware-class.js.map +1 -0
- package/dist/fixtures/circuits.d.ts +1721 -0
- package/dist/fixtures/circuits.d.ts.map +1 -0
- package/dist/fixtures/circuits.js +13404 -0
- package/dist/fixtures/circuits.js.map +1 -0
- package/dist/fixtures/expected.d.ts +257 -0
- package/dist/fixtures/expected.d.ts.map +1 -0
- package/dist/fixtures/expected.js +311 -0
- package/dist/fixtures/expected.js.map +1 -0
- package/dist/fixtures/index.d.ts +4 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +8 -0
- package/dist/fixtures/index.js.map +1 -0
- package/dist/fixtures/registry.d.ts +52 -0
- package/dist/fixtures/registry.d.ts.map +1 -0
- package/dist/fixtures/registry.js +341 -0
- package/dist/fixtures/registry.js.map +1 -0
- package/dist/generic-tube-fit.d.ts +12 -0
- package/dist/generic-tube-fit.d.ts.map +1 -0
- package/dist/generic-tube-fit.js +45 -0
- package/dist/generic-tube-fit.js.map +1 -0
- package/dist/grounded-clock-supply.d.ts +13 -0
- package/dist/grounded-clock-supply.d.ts.map +1 -0
- package/dist/grounded-clock-supply.js +45 -0
- package/dist/grounded-clock-supply.js.map +1 -0
- package/dist/identify.d.ts +50 -0
- package/dist/identify.d.ts.map +1 -0
- package/dist/identify.js +319 -0
- package/dist/identify.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/inert-controls.d.ts +15 -0
- package/dist/inert-controls.d.ts.map +1 -0
- package/dist/inert-controls.js +266 -0
- package/dist/inert-controls.js.map +1 -0
- package/dist/latch-seed.d.ts +53 -0
- package/dist/latch-seed.d.ts.map +1 -0
- package/dist/latch-seed.js +257 -0
- package/dist/latch-seed.js.map +1 -0
- package/dist/link.d.ts +7 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +270 -0
- package/dist/link.js.map +1 -0
- package/dist/lower.d.ts +79 -0
- package/dist/lower.d.ts.map +1 -0
- package/dist/lower.js +1855 -0
- package/dist/lower.js.map +1 -0
- package/dist/netlist.d.ts +79 -0
- package/dist/netlist.d.ts.map +1 -0
- package/dist/netlist.js +4746 -0
- package/dist/netlist.js.map +1 -0
- package/dist/non-executable-clock-drivers.d.ts +8 -0
- package/dist/non-executable-clock-drivers.d.ts.map +1 -0
- package/dist/non-executable-clock-drivers.js +33 -0
- package/dist/non-executable-clock-drivers.js.map +1 -0
- package/dist/open-category.d.ts +98 -0
- package/dist/open-category.d.ts.map +1 -0
- package/dist/open-category.js +111 -0
- package/dist/open-category.js.map +1 -0
- package/dist/over-driven-node.d.ts +33 -0
- package/dist/over-driven-node.d.ts.map +1 -0
- package/dist/over-driven-node.js +188 -0
- package/dist/over-driven-node.js.map +1 -0
- package/dist/part-catalog.d.ts +7 -0
- package/dist/part-catalog.d.ts.map +1 -0
- package/dist/part-catalog.js +6639 -0
- package/dist/part-catalog.js.map +1 -0
- package/dist/part-number.d.ts +32 -0
- package/dist/part-number.d.ts.map +1 -0
- package/dist/part-number.js +80 -0
- package/dist/part-number.js.map +1 -0
- package/dist/partition.d.ts +7 -0
- package/dist/partition.d.ts.map +1 -0
- package/dist/partition.js +242 -0
- package/dist/partition.js.map +1 -0
- package/dist/port-full-scale.d.ts +70 -0
- package/dist/port-full-scale.d.ts.map +1 -0
- package/dist/port-full-scale.js +235 -0
- package/dist/port-full-scale.js.map +1 -0
- package/dist/pot-orientation.d.ts +114 -0
- package/dist/pot-orientation.d.ts.map +1 -0
- package/dist/pot-orientation.js +551 -0
- package/dist/pot-orientation.js.map +1 -0
- package/dist/power-domain-control.d.ts +40 -0
- package/dist/power-domain-control.d.ts.map +1 -0
- package/dist/power-domain-control.js +142 -0
- package/dist/power-domain-control.js.map +1 -0
- package/dist/proxy-declaration.d.ts +121 -0
- package/dist/proxy-declaration.d.ts.map +1 -0
- package/dist/proxy-declaration.js +50 -0
- package/dist/proxy-declaration.js.map +1 -0
- package/dist/registry.d.ts +323 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +192 -0
- package/dist/registry.js.map +1 -0
- package/dist/sparse-schedule.d.ts +65 -0
- package/dist/sparse-schedule.d.ts.map +1 -0
- package/dist/sparse-schedule.js +421 -0
- package/dist/sparse-schedule.js.map +1 -0
- package/dist/speaker-load.d.ts +38 -0
- package/dist/speaker-load.d.ts.map +1 -0
- package/dist/speaker-load.js +97 -0
- package/dist/speaker-load.js.map +1 -0
- package/dist/stage-coverage.d.ts +21 -0
- package/dist/stage-coverage.d.ts.map +1 -0
- package/dist/stage-coverage.js +124 -0
- package/dist/stage-coverage.js.map +1 -0
- package/dist/stamp-partition.d.ts +314 -0
- package/dist/stamp-partition.d.ts.map +1 -0
- package/dist/stamp-partition.js +472 -0
- package/dist/stamp-partition.js.map +1 -0
- package/dist/supply-reference.d.ts +19 -0
- package/dist/supply-reference.d.ts.map +1 -0
- package/dist/supply-reference.js +66 -0
- package/dist/supply-reference.js.map +1 -0
- package/dist/supply-short.d.ts +32 -0
- package/dist/supply-short.d.ts.map +1 -0
- package/dist/supply-short.js +118 -0
- package/dist/supply-short.js.map +1 -0
- package/dist/supply-stamps.d.ts +51 -0
- package/dist/supply-stamps.d.ts.map +1 -0
- package/dist/supply-stamps.js +363 -0
- package/dist/supply-stamps.js.map +1 -0
- package/dist/terminal-role.d.ts +109 -0
- package/dist/terminal-role.d.ts.map +1 -0
- package/dist/terminal-role.js +46 -0
- package/dist/terminal-role.js.map +1 -0
- package/dist/terminal-roles.d.ts +59 -0
- package/dist/terminal-roles.d.ts.map +1 -0
- package/dist/terminal-roles.js +92 -0
- package/dist/terminal-roles.js.map +1 -0
- package/dist/transformer.d.ts +181 -0
- package/dist/transformer.d.ts.map +1 -0
- package/dist/transformer.js +476 -0
- package/dist/transformer.js.map +1 -0
- package/dist/types.d.ts +3157 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/unbounded-operating-point.d.ts +35 -0
- package/dist/unbounded-operating-point.d.ts.map +1 -0
- package/dist/unbounded-operating-point.js +181 -0
- package/dist/unbounded-operating-point.js.map +1 -0
- package/dist/unexecuted-region.d.ts +3 -0
- package/dist/unexecuted-region.d.ts.map +1 -0
- package/dist/unexecuted-region.js +122 -0
- package/dist/unexecuted-region.js.map +1 -0
- package/dist/unimplemented-control-role.d.ts +3 -0
- package/dist/unimplemented-control-role.d.ts.map +1 -0
- package/dist/unimplemented-control-role.js +61 -0
- package/dist/unimplemented-control-role.js.map +1 -0
- package/dist/unreachable-output.d.ts +3 -0
- package/dist/unreachable-output.d.ts.map +1 -0
- package/dist/unreachable-output.js +97 -0
- package/dist/unreachable-output.js.map +1 -0
- package/dist/unreadable-terminal-role.d.ts +67 -0
- package/dist/unreadable-terminal-role.d.ts.map +1 -0
- package/dist/unreadable-terminal-role.js +409 -0
- package/dist/unreadable-terminal-role.js.map +1 -0
- package/dist/unverified-pinout.d.ts +27 -0
- package/dist/unverified-pinout.d.ts.map +1 -0
- package/dist/unverified-pinout.js +121 -0
- package/dist/unverified-pinout.js.map +1 -0
- package/package.json +75 -5
package/dist/lower.js
ADDED
|
@@ -0,0 +1,1855 @@
|
|
|
1
|
+
// Stage 5: turn each region into a block of stamps.
|
|
2
|
+
//
|
|
3
|
+
// A stamp is a *symbolic* contribution to the MNA system, not a number. Two things
|
|
4
|
+
// are deliberately left unevaluated here, and both are decisions rather than
|
|
5
|
+
// convenience:
|
|
6
|
+
//
|
|
7
|
+
// - **Sample rate.** A capacitor's companion conductance is 2C/dt, which needs a
|
|
8
|
+
// rate. The rate is a runtime input (decision 3), so the stamp carries C and the
|
|
9
|
+
// runtime computes the conductance. There is no sample rate anywhere in this
|
|
10
|
+
// file, no default, and no fallback.
|
|
11
|
+
// - **Control position.** A pot's conductance depends on its 0..1 position through
|
|
12
|
+
// its taper (decision 2), so the stamp carries the track resistance and the taper
|
|
13
|
+
// and the runtime evaluates them. Moving a knob re-evaluates coefficients; it
|
|
14
|
+
// never re-lowers.
|
|
15
|
+
//
|
|
16
|
+
// The result is that one lowered program is valid at every sample rate and every knob
|
|
17
|
+
// position, which is what makes rate independence testable: compile once, run at two
|
|
18
|
+
// rates, and the outputs must match.
|
|
19
|
+
import { SWITCH_ON_OHMS } from "./device-laws.js";
|
|
20
|
+
import { findBistableLatches, findLatchSeeds, } from "./latch-seed.js";
|
|
21
|
+
import { computeSparseSchedule } from "./sparse-schedule.js";
|
|
22
|
+
import { computeStampPartition, remapStampNodes, shouldEliminate, stampControl, stampNeedsNewton, } from "./stamp-partition.js";
|
|
23
|
+
export { stampNeedsNewton } from "./stamp-partition.js";
|
|
24
|
+
import { GROUND } from "./types.js";
|
|
25
|
+
import { foldToken } from "./registry.js";
|
|
26
|
+
import { LoweringError } from "./errors.js";
|
|
27
|
+
import { nodeOccurrenceCounts, selectorTapNodes, transformerWindings, } from "./transformer.js";
|
|
28
|
+
import { opampInvertingInputNodes, opampInputDistances, opampOutputAcDistances, opampOutputDistances, potTerminals, quietDistances, } from "./pot-orientation.js";
|
|
29
|
+
export { opampInvertingInputNodes, opampInputDistances, opampOutputAcDistances, opampOutputDistances, potTerminals, quietDistances, } from "./pot-orientation.js";
|
|
30
|
+
export { LoweringError } from "./errors.js";
|
|
31
|
+
export { stampControl } from "./stamp-partition.js";
|
|
32
|
+
export function lowerRegion(region, lawed, inputNode, outputNode,
|
|
33
|
+
/**
|
|
34
|
+
* Computed once by `lower` rather than per region: the rule is a pairwise scan over
|
|
35
|
+
* transistors, and `boss-ds-1` lowers 82 regions from one netlist.
|
|
36
|
+
*/
|
|
37
|
+
latchSeeds = [], latches = [],
|
|
38
|
+
/**
|
|
39
|
+
* Computed once by `lower`, for the same reason `latchSeeds` is: the graph a pot's
|
|
40
|
+
* orientation reads (`quietDistances`) does not change between `boss-ds-1`'s 82 regions
|
|
41
|
+
* either. Empty by default so a direct call site not passing one gets today's
|
|
42
|
+
* declaration-order behaviour for every pot -- never a wrong answer, only an unresolved one.
|
|
43
|
+
*/
|
|
44
|
+
quietDistance = new Map(),
|
|
45
|
+
/**
|
|
46
|
+
* The narrower, scoped fallback `potTerminals` only consults when `quietDistance` leaves a
|
|
47
|
+
* pot fully unreachable -- see `opampInputDistances`. Empty by default for the same reason
|
|
48
|
+
* `quietDistance` is: an unresolved pot, never a wrong one.
|
|
49
|
+
*/
|
|
50
|
+
opampInputDistance = new Map(),
|
|
51
|
+
/**
|
|
52
|
+
* Companion of `opampInputDistance` for the negative-feedback gain-stage tie in
|
|
53
|
+
* `potTerminals`: a pot that the input-distance evidence cannot break reads which end
|
|
54
|
+
* sits on an op-amp's declared output instead. Empty by default for the same reason.
|
|
55
|
+
*/
|
|
56
|
+
opampOutputDistance = new Map(),
|
|
57
|
+
/**
|
|
58
|
+
* Nodes that are a declared op-amp inverting input, and the AC (DC + capacitors) distance to
|
|
59
|
+
* an op-amp's declared output: the two signals `potTerminals` reads for the one finite-tie
|
|
60
|
+
* shape the DC maps cannot break -- a non-inverting gain pot whose wiper sits on the
|
|
61
|
+
* inverting input. Empty by default for the same reason as the other maps.
|
|
62
|
+
*/
|
|
63
|
+
invertingInputNodes = new Set(), acOutputDistance = new Map()) {
|
|
64
|
+
if (region.kind === "macro") {
|
|
65
|
+
const macro = region.macro;
|
|
66
|
+
if (macro === null) {
|
|
67
|
+
throw new Error(`region ${region.id} is macro but carries no model`);
|
|
68
|
+
}
|
|
69
|
+
// Placeholders: `couple.ts` runs after every region has been lowered and needs to see
|
|
70
|
+
// every block to wire a macro's audio/parameter ports across them, so it cannot happen
|
|
71
|
+
// here where only this one region is in view.
|
|
72
|
+
return {
|
|
73
|
+
kind: "macro",
|
|
74
|
+
id: region.id,
|
|
75
|
+
modelId: macro.modelId,
|
|
76
|
+
parameters: macro.parameters,
|
|
77
|
+
audioIn: null,
|
|
78
|
+
audioOut: false,
|
|
79
|
+
parameter: null,
|
|
80
|
+
modulation: null,
|
|
81
|
+
// The open-OX2 port and law ride only when recognised: emitting
|
|
82
|
+
// null keys here would churn every macro block's program bytes
|
|
83
|
+
// without changing any of them, and the ce-5/micro-flanger
|
|
84
|
+
// bit-identical gate names those programs byte for byte.
|
|
85
|
+
...(macro.clockLawParams !== undefined && macro.clockLawParams !== null
|
|
86
|
+
? { clockLawParams: macro.clockLawParams }
|
|
87
|
+
: {}),
|
|
88
|
+
clockControl: macro.clockControl ?? null,
|
|
89
|
+
delayProvenance: macro.delayProvenance ?? null,
|
|
90
|
+
delayDeclaredReason: macro.delayDeclaredReason ?? null,
|
|
91
|
+
// A declared composition rides the macro road as far as composition; `couple.ts`
|
|
92
|
+
// fills `selector` the same way it fills `parameter`, from the terminal index.
|
|
93
|
+
declaredPositions: macro.declaredPositions ?? null,
|
|
94
|
+
declaredRouter: macro.declaredRouter ?? null,
|
|
95
|
+
declaredParameter: macro.declaredParameter ?? null,
|
|
96
|
+
router: null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const deviceById = new Map(lawed.netlist.devices.map((device) => [device.id, device]));
|
|
100
|
+
const resolutionByDevice = new Map(lawed.resolutions.map((resolution) => [resolution.device, resolution]));
|
|
101
|
+
// Only consulted by a transformer whose own terminal grouping turns up a lone,
|
|
102
|
+
// unpaired end -- see `transformerWindings`. Computed once per region against the
|
|
103
|
+
// whole netlist, not just this region, because the question is "does anything else in
|
|
104
|
+
// the document touch this node", not "does anything in this region".
|
|
105
|
+
const terminalOccurrences = nodeOccurrenceCounts(lawed.netlist.devices);
|
|
106
|
+
const selectorTaps = selectorTapNodes(lawed.netlist.devices);
|
|
107
|
+
const stamps = [];
|
|
108
|
+
const extraNodes = [];
|
|
109
|
+
/**
|
|
110
|
+
* A private node no pin of the source document reaches -- a device-internal midpoint.
|
|
111
|
+
*
|
|
112
|
+
* Counted off `extraNodes.length` rather than off `stamps.length`: two devices that each
|
|
113
|
+
* need an internal node are only guaranteed distinct numbers if the counter advances once
|
|
114
|
+
* per node handed out. Keying off the stamp count instead means a device that emits
|
|
115
|
+
* several stamps per internal node and one that emits one can be handed the same number,
|
|
116
|
+
* which silently shorts two device internals together.
|
|
117
|
+
*/
|
|
118
|
+
const allocateInternalNode = () => {
|
|
119
|
+
const highest = Math.max(...lawed.netlist.nodes);
|
|
120
|
+
const node = (highest + 1 + extraNodes.length);
|
|
121
|
+
extraNodes.push(node);
|
|
122
|
+
return node;
|
|
123
|
+
};
|
|
124
|
+
/** Node spans already held at a potential, so one supply is not stamped twice. */
|
|
125
|
+
const drivenNodes = new Map();
|
|
126
|
+
let stateCount = 0;
|
|
127
|
+
let auxCount = 0;
|
|
128
|
+
for (const deviceId of region.devices) {
|
|
129
|
+
const device = deviceById.get(deviceId);
|
|
130
|
+
const resolution = resolutionByDevice.get(deviceId);
|
|
131
|
+
if (device === undefined ||
|
|
132
|
+
resolution === undefined ||
|
|
133
|
+
resolution.outcome !== "law") {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const law = resolution.law;
|
|
137
|
+
// A supply is stated two ways: a battery names both ends, a rail names one node
|
|
138
|
+
// and means "against ground". Everything else genuinely needs two terminals.
|
|
139
|
+
const [a, b] = law.kind === "voltage-source" && device.nodes.length === 1
|
|
140
|
+
? [device.nodes[0], GROUND]
|
|
141
|
+
: terminalPair(device);
|
|
142
|
+
switch (law.kind) {
|
|
143
|
+
case "conductance":
|
|
144
|
+
stamps.push({ kind: "conductance", a, b, siemens: law.siemens });
|
|
145
|
+
break;
|
|
146
|
+
case "controlled-conductance": {
|
|
147
|
+
// A pot has three terminals: one end, wiper, other end. Each half of the
|
|
148
|
+
// track becomes its own controlled element so the wiper node is real.
|
|
149
|
+
const [end1, wiper, end2] = potTerminals(device, quietDistance, opampInputDistance, opampOutputDistance,
|
|
150
|
+
// The declared role of the control this pot answers to. `potTerminals`
|
|
151
|
+
// reads it for one narrow case -- a rheostat whose control names a time
|
|
152
|
+
// constant -- and ignores it otherwise.
|
|
153
|
+
lawed.netlist.controls.find((control) => control.id === law.control)?.role ?? null, invertingInputNodes, acOutputDistance);
|
|
154
|
+
// Both halves carry the same residual: it is a property of the wiper's
|
|
155
|
+
// travel, so each end of the track keeps one.
|
|
156
|
+
stamps.push({
|
|
157
|
+
kind: "controlled-conductance",
|
|
158
|
+
a: end1,
|
|
159
|
+
b: wiper,
|
|
160
|
+
control: law.control,
|
|
161
|
+
taper: law.taper,
|
|
162
|
+
totalOhms: law.totalOhms,
|
|
163
|
+
side: "upper",
|
|
164
|
+
residualOhms: law.residualOhms,
|
|
165
|
+
});
|
|
166
|
+
stamps.push({
|
|
167
|
+
kind: "controlled-conductance",
|
|
168
|
+
a: wiper,
|
|
169
|
+
b: end2,
|
|
170
|
+
control: law.control,
|
|
171
|
+
taper: law.taper,
|
|
172
|
+
totalOhms: law.totalOhms,
|
|
173
|
+
side: "lower",
|
|
174
|
+
residualOhms: law.residualOhms,
|
|
175
|
+
});
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case "controlled-resistance": {
|
|
179
|
+
// Two terminals, one element. Unlike a pot there is no wiper node to
|
|
180
|
+
// create, so this is the plain two-terminal pair.
|
|
181
|
+
const [end1, end2] = terminalPair(device);
|
|
182
|
+
stamps.push({
|
|
183
|
+
kind: "controlled-resistance",
|
|
184
|
+
a: end1,
|
|
185
|
+
b: end2,
|
|
186
|
+
control: law.control,
|
|
187
|
+
taper: law.taper,
|
|
188
|
+
minOhms: law.minOhms,
|
|
189
|
+
maxOhms: law.maxOhms,
|
|
190
|
+
});
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case "switch":
|
|
194
|
+
stamps.push({
|
|
195
|
+
kind: "switch",
|
|
196
|
+
a,
|
|
197
|
+
b,
|
|
198
|
+
control: law.control,
|
|
199
|
+
onOhms: law.onOhms,
|
|
200
|
+
offOhms: law.offOhms,
|
|
201
|
+
});
|
|
202
|
+
break;
|
|
203
|
+
case "selector": {
|
|
204
|
+
// Every throw of every pole is stamped, and the control decides which
|
|
205
|
+
// conducts. One document still yields one program: a selector adds
|
|
206
|
+
// elements, never a second netlist per position.
|
|
207
|
+
const poles = switchPoles(device);
|
|
208
|
+
const throwCount = Math.max(...poles.map((pole) => pole.throws.length));
|
|
209
|
+
for (const pole of poles) {
|
|
210
|
+
pole.throws.forEach((throwNode, throwIndex) => {
|
|
211
|
+
stamps.push({
|
|
212
|
+
kind: "selector",
|
|
213
|
+
common: pole.common,
|
|
214
|
+
throwNode,
|
|
215
|
+
control: law.control,
|
|
216
|
+
throwIndex,
|
|
217
|
+
throwCount,
|
|
218
|
+
onOhms: law.onOhms,
|
|
219
|
+
offOhms: law.offOhms,
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case "fixed-selector": {
|
|
226
|
+
// Row 12: a momentary footswitch contact rests OPEN. A null-control
|
|
227
|
+
// switch with more than two terminals that touches a detected latch's
|
|
228
|
+
// collector/base set is the footswitch driving that latch: welding it
|
|
229
|
+
// shut (position ?? 1) drags the bypassed collector below gate
|
|
230
|
+
// threshold and the switch dies (boss-ph-1r measured: bypassed
|
|
231
|
+
// collector pinned at 0.82 V against a ~9 V pull-up by the 0.01 ohm
|
|
232
|
+
// common-to-throw2 weld, downstream gates never switch, audio
|
|
233
|
+
// bit-identical both positions). Emit no element; the latch state
|
|
234
|
+
// (exposed control + steering stamps) carries selection. Two-terminal
|
|
235
|
+
// null-control switches (boss-bd-2, boss-ds-1) and switches outside
|
|
236
|
+
// latch sets (boss-dm-3) never reach this branch.
|
|
237
|
+
const latchNodes = new Set();
|
|
238
|
+
for (const latch of latches) {
|
|
239
|
+
latchNodes.add(latch.engagedCollector);
|
|
240
|
+
latchNodes.add(latch.bypassedCollector);
|
|
241
|
+
latchNodes.add(latch.engagedBase);
|
|
242
|
+
latchNodes.add(latch.bypassedBase);
|
|
243
|
+
}
|
|
244
|
+
if (device.nodes.length > 2 &&
|
|
245
|
+
device.nodes.some((node) => latchNodes.has(node))) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
const poles = switchPoles(device);
|
|
249
|
+
const throwCount = Math.max(...poles.map((pole) => pole.throws.length));
|
|
250
|
+
const selected = Math.min(throwCount - 1, Math.max(0, Math.floor(law.position * throwCount)));
|
|
251
|
+
for (const pole of poles) {
|
|
252
|
+
pole.throws.forEach((throwNode, throwIndex) => {
|
|
253
|
+
const ohms = throwIndex === selected ? law.onOhms : law.offOhms;
|
|
254
|
+
stamps.push({
|
|
255
|
+
kind: "conductance",
|
|
256
|
+
a: pole.common,
|
|
257
|
+
b: throwNode,
|
|
258
|
+
siemens: 1 / ohms,
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
// `stateIndex` is a slot offset, and a reactive element occupies two slots.
|
|
265
|
+
// Counting one per element while the executor addressed `stateIndex * 2` made
|
|
266
|
+
// the program understate its own state by half: harmless against a JS array,
|
|
267
|
+
// which grows on write, and silent data loss against the
|
|
268
|
+
// `Float64Array(stateCount)` any C++/WASM/ESP32 implementer would allocate.
|
|
269
|
+
case "capacitance":
|
|
270
|
+
stamps.push({
|
|
271
|
+
kind: "capacitor",
|
|
272
|
+
a,
|
|
273
|
+
b,
|
|
274
|
+
farads: law.farads,
|
|
275
|
+
stateIndex: stateCount,
|
|
276
|
+
});
|
|
277
|
+
stateCount += STATE_SLOTS_PER_REACTIVE_ELEMENT;
|
|
278
|
+
// Declared DC leakage lowers as a second stamp, a plain conductance in
|
|
279
|
+
// parallel, rather than a new field on the capacitor stamp: both consoles
|
|
280
|
+
// already implement `conductance`, so a leaky capacitor is audible on the
|
|
281
|
+
// TS and C++/WASM engines without either runtime changing.
|
|
282
|
+
if (law.leakageSiemens > 0) {
|
|
283
|
+
stamps.push({
|
|
284
|
+
kind: "conductance",
|
|
285
|
+
a,
|
|
286
|
+
b,
|
|
287
|
+
siemens: law.leakageSiemens,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
case "inductance":
|
|
292
|
+
stamps.push({
|
|
293
|
+
kind: "inductor",
|
|
294
|
+
a,
|
|
295
|
+
b,
|
|
296
|
+
henries: law.henries,
|
|
297
|
+
stateIndex: stateCount,
|
|
298
|
+
// `"unknown"` is a statement, not a value: it carries no resistance into the
|
|
299
|
+
// stamp but is a different claim from silence, and the law records which.
|
|
300
|
+
...(typeof law.seriesResistanceOhms === "number"
|
|
301
|
+
? { seriesResistanceOhms: law.seriesResistanceOhms }
|
|
302
|
+
: {}),
|
|
303
|
+
});
|
|
304
|
+
stateCount += STATE_SLOTS_PER_REACTIVE_ELEMENT;
|
|
305
|
+
break;
|
|
306
|
+
case "diode": {
|
|
307
|
+
// By role, not by order: 11 corpus diodes are declared cathode-first, and
|
|
308
|
+
// a reversed diode still conducts, so a clipper looks fine while a
|
|
309
|
+
// rectifier or a protection diode is backwards.
|
|
310
|
+
//
|
|
311
|
+
// **And one component can be several junctions.** A bridge rectifier is four,
|
|
312
|
+
// a dual rectifier two; taking one pair silently dropped the rest. See
|
|
313
|
+
// `diodeJunctions`.
|
|
314
|
+
for (const junction of diodeJunctions(device)) {
|
|
315
|
+
stamps.push({
|
|
316
|
+
kind: "diode",
|
|
317
|
+
anode: junction.anode,
|
|
318
|
+
cathode: junction.cathode,
|
|
319
|
+
saturationCurrent: law.saturationCurrent,
|
|
320
|
+
emissionCoefficient: law.emissionCoefficient,
|
|
321
|
+
thermalVoltage: law.thermalVoltage,
|
|
322
|
+
breakdownVolts: law.breakdownVolts,
|
|
323
|
+
seriesResistance: law.seriesResistance,
|
|
324
|
+
device: device.id,
|
|
325
|
+
isLed: law.isLed,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case "bjt": {
|
|
331
|
+
const [base, collector, emitter] = bjtTerminals(device);
|
|
332
|
+
stamps.push({
|
|
333
|
+
kind: "bjt",
|
|
334
|
+
base,
|
|
335
|
+
collector,
|
|
336
|
+
emitter,
|
|
337
|
+
polarity: law.polarity,
|
|
338
|
+
leakageAmps: law.leakageAmps,
|
|
339
|
+
saturationCurrent: law.saturationCurrent,
|
|
340
|
+
forwardBeta: law.forwardBeta,
|
|
341
|
+
reverseBeta: law.reverseBeta,
|
|
342
|
+
thermalVoltage: law.thermalVoltage,
|
|
343
|
+
});
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case "triode": {
|
|
347
|
+
// A tube's terminal names are its own vocabulary -- grid, cathode, plate,
|
|
348
|
+
// and `anode` for the plate -- so it cannot borrow `bjtTerminals`, whose
|
|
349
|
+
// closed sets are base/collector/emitter and gate/drain/source. Getting the
|
|
350
|
+
// plate and cathode the wrong way round would invert the stage and still
|
|
351
|
+
// render, which is the class of error this stage exists to prevent.
|
|
352
|
+
const [grid, cathode, plate] = triodeTerminals(device);
|
|
353
|
+
stamps.push({
|
|
354
|
+
kind: "triode",
|
|
355
|
+
grid,
|
|
356
|
+
cathode,
|
|
357
|
+
plate,
|
|
358
|
+
mu: law.mu,
|
|
359
|
+
kg1: law.kg1,
|
|
360
|
+
kp: law.kp,
|
|
361
|
+
kvb: law.kvb,
|
|
362
|
+
ex: law.ex,
|
|
363
|
+
gridSaturationCurrent: law.gridSaturationCurrent,
|
|
364
|
+
gridOnsetVolts: law.gridOnsetVolts,
|
|
365
|
+
gridScaleVolts: law.gridScaleVolts,
|
|
366
|
+
contactPotentialVolts: law.contactPotentialVolts,
|
|
367
|
+
});
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case "pentode": {
|
|
371
|
+
const [grid, cathode, plate, screen] = pentodeTerminals(device);
|
|
372
|
+
stamps.push({
|
|
373
|
+
kind: "pentode",
|
|
374
|
+
grid,
|
|
375
|
+
cathode,
|
|
376
|
+
plate,
|
|
377
|
+
screen,
|
|
378
|
+
mu: law.mu,
|
|
379
|
+
kg1: law.kg1,
|
|
380
|
+
kp: law.kp,
|
|
381
|
+
kvb: law.kvb,
|
|
382
|
+
ex: law.ex,
|
|
383
|
+
gridSaturationCurrent: law.gridSaturationCurrent,
|
|
384
|
+
gridOnsetVolts: law.gridOnsetVolts,
|
|
385
|
+
gridScaleVolts: law.gridScaleVolts,
|
|
386
|
+
contactPotentialVolts: law.contactPotentialVolts,
|
|
387
|
+
screenShare: law.screenShare ?? 0,
|
|
388
|
+
});
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case "tube-diode": {
|
|
392
|
+
// One stamp per plate, all sharing the cathode.
|
|
393
|
+
//
|
|
394
|
+
// Every tube diode in the amp corpus is a **dual**: `plate_a`, `plate_b` and one
|
|
395
|
+
// `cathode_filament`. Two plates in one envelope over a common cathode are two
|
|
396
|
+
// independent conduction paths -- there is no interaction to model, because a
|
|
397
|
+
// directly heated cathode emits toward whichever plate is positive -- so this needs
|
|
398
|
+
// no operator of its own, exactly as an N-winding transformer needed none.
|
|
399
|
+
//
|
|
400
|
+
// Taking the first plate and dropping the second would be the pentode's ganged
|
|
401
|
+
// case: half a device that still renders. In a centre-tapped supply, which is what
|
|
402
|
+
// all three instances are wired into, that is a **full-wave rectifier reduced to
|
|
403
|
+
// half-wave** -- the same defect the transformer's positional reading produced on
|
|
404
|
+
// `tycobrahe-octavia`, here doubling the ripple and halving the available current.
|
|
405
|
+
for (const plate of tubeDiodePlates(device)) {
|
|
406
|
+
stamps.push({
|
|
407
|
+
kind: "tube-diode",
|
|
408
|
+
plate: plate.plate,
|
|
409
|
+
cathode: plate.cathode,
|
|
410
|
+
perveance: law.perveance,
|
|
411
|
+
exponent: law.exponent,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
case "fet": {
|
|
417
|
+
const [gate, drain, source] = bjtTerminals(device);
|
|
418
|
+
stamps.push({
|
|
419
|
+
kind: "fet",
|
|
420
|
+
gate,
|
|
421
|
+
drain,
|
|
422
|
+
source,
|
|
423
|
+
channel: law.channel,
|
|
424
|
+
thresholdVolts: law.thresholdVolts,
|
|
425
|
+
transconductance: law.transconductance,
|
|
426
|
+
channelLengthModulation: law.channelLengthModulation,
|
|
427
|
+
subthresholdVolts: law.subthresholdVolts,
|
|
428
|
+
gateSaturationCurrent: law.gateSaturationCurrent,
|
|
429
|
+
gateOnsetVolts: law.gateOnsetVolts,
|
|
430
|
+
gateScaleVolts: law.gateScaleVolts,
|
|
431
|
+
});
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
case "transformer": {
|
|
435
|
+
// One stamp per secondary winding.
|
|
436
|
+
//
|
|
437
|
+
// A **coupled** winding is one stamp against the shared primary. An N-winding
|
|
438
|
+
// ideal transformer *is* (N-1) two-winding ones on a shared primary, so this
|
|
439
|
+
// needs no new operator. Each winding sees the same flux, so `V_k / n_k` is
|
|
440
|
+
// equal across all of them, and ampere-turns balance `sum(N_k * I_k) = 0` falls
|
|
441
|
+
// out: a secondary reflecting `-n_k * i_k` into the primary pair means the
|
|
442
|
+
// primary carries `sum(i_k)`, and substituting gives
|
|
443
|
+
// `N_1 * sum(i_k) - sum(N_1 * i_k) = 0`. Exactly the N-winding constraint.
|
|
444
|
+
//
|
|
445
|
+
// A **driven** winding is one `ac-source` at the winding's own stated RMS, with
|
|
446
|
+
// no primary modelled at all -- see `transformerWindings` for when and why.
|
|
447
|
+
for (const winding of transformerWindings(device, law, terminalOccurrences, selectorTaps)) {
|
|
448
|
+
stamps.push(winding.kind === "coupled"
|
|
449
|
+
? {
|
|
450
|
+
kind: "transformer",
|
|
451
|
+
primaryPlus: winding.primaryPlus,
|
|
452
|
+
primaryMinus: winding.primaryMinus,
|
|
453
|
+
secondaryPlus: winding.plus,
|
|
454
|
+
secondaryMinus: winding.minus,
|
|
455
|
+
turnsRatio: winding.turnsRatio,
|
|
456
|
+
sourceIndex: auxCount,
|
|
457
|
+
// **The declared value IS the primary-referred loop resistance**,
|
|
458
|
+
// used as stated. An earlier version read it as each winding's
|
|
459
|
+
// own resistance and formed `R * (1 + n^2)`, which for the
|
|
460
|
+
// 2k:4 output transformer turned a declared 50 Ω into 25 kΩ --
|
|
461
|
+
// **12.5x the primary impedance**, a near-open circuit rather
|
|
462
|
+
// than damping. That is wrong because a real transformer's
|
|
463
|
+
// secondary is about `R_p / n^2`, not equal to `R_p`, so the
|
|
464
|
+
// referred total is of order `2 * R_p` and never `n^2 * R_p`.
|
|
465
|
+
// One number, referred to the primary, is also the only thing
|
|
466
|
+
// a single property can honestly mean.
|
|
467
|
+
//
|
|
468
|
+
// `unknown` carries no value, which is a different claim from
|
|
469
|
+
// silence but the same stamp.
|
|
470
|
+
...(typeof law.seriesResistanceOhms === "number"
|
|
471
|
+
? { seriesResistanceOhms: law.seriesResistanceOhms }
|
|
472
|
+
: {}),
|
|
473
|
+
}
|
|
474
|
+
: {
|
|
475
|
+
kind: "ac-source",
|
|
476
|
+
positive: winding.plus,
|
|
477
|
+
negative: winding.minus,
|
|
478
|
+
amplitudeVolts: winding.amplitudeVolts,
|
|
479
|
+
frequencyHz: winding.frequencyHz,
|
|
480
|
+
sourceIndex: auxCount,
|
|
481
|
+
sourceOhms: winding.sourceOhms,
|
|
482
|
+
});
|
|
483
|
+
auxCount += 1;
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case "spring-reverb": {
|
|
488
|
+
// A tank's two coils come from its **declared** windings, never from terminal
|
|
489
|
+
// order: swapping a pair's ends inverts that port, and swapping the pairs sends
|
|
490
|
+
// the recovery amp's signal into the drive coil instead of taking it from the
|
|
491
|
+
// pickup. Order would decide both silently, so a missing declaration is a refusal.
|
|
492
|
+
//
|
|
493
|
+
// `drive`/`pickup` rather than `primary`/`secondary` because neither coil
|
|
494
|
+
// transforms the other's voltage -- the springs are between them. This replaced a
|
|
495
|
+
// four-spelling name set (`inputhot`/`inputreturn`/`outputhot`/`outputreturn`),
|
|
496
|
+
// which was the last terminal-name vocabulary in this file.
|
|
497
|
+
const coils = new Map();
|
|
498
|
+
for (const winding of device.identity.declaredWindings ?? []) {
|
|
499
|
+
const nodes = winding.terminalIndices.flatMap((index) => {
|
|
500
|
+
const node = device.nodes[index];
|
|
501
|
+
return node === undefined ? [] : [node];
|
|
502
|
+
});
|
|
503
|
+
if (nodes.length === 2 && !coils.has(winding.role)) {
|
|
504
|
+
coils.set(winding.role, nodes);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const drive = coils.get("drive");
|
|
508
|
+
const pickup = coils.get("pickup");
|
|
509
|
+
if (drive === undefined || pickup === undefined) {
|
|
510
|
+
throw new LoweringError(`spring reverb tank ${device.id}: needs a two-terminal "drive" winding and a ` +
|
|
511
|
+
'two-terminal "pickup" winding declared, and which coil is which decides ' +
|
|
512
|
+
"whether the signal goes into the springs or comes out of them; saw " +
|
|
513
|
+
`[${(device.identity.declaredWindings ?? []).map((winding) => winding.role).join(", ")}]`, device.id);
|
|
514
|
+
}
|
|
515
|
+
const [inputPlus, inputMinus] = drive;
|
|
516
|
+
const [outputPlus, outputMinus] = pickup;
|
|
517
|
+
stamps.push({
|
|
518
|
+
kind: "spring-reverb",
|
|
519
|
+
inputPlus,
|
|
520
|
+
inputMinus,
|
|
521
|
+
outputPlus,
|
|
522
|
+
outputMinus,
|
|
523
|
+
inputOhms: law.inputOhms,
|
|
524
|
+
outputOhms: law.outputOhms,
|
|
525
|
+
delaySeconds: law.delaySeconds,
|
|
526
|
+
decaySeconds: law.decaySeconds,
|
|
527
|
+
dispersionStages: law.dispersionStages,
|
|
528
|
+
sourceIndex: auxCount,
|
|
529
|
+
});
|
|
530
|
+
auxCount += 1;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
case "voltage-source": {
|
|
534
|
+
// One node, one assertion. A packet may state the same supply twice --
|
|
535
|
+
// `boss-sd-1` declares a 9 V rail and a 9 V battery on the same node --
|
|
536
|
+
// and two ideal sources in parallel are two matrix rows asserting one
|
|
537
|
+
// constraint, which is rank-deficient and solves to nothing. Stamping it
|
|
538
|
+
// once is exactly equivalent.
|
|
539
|
+
const [positive, negative] = supplyTerminals(device);
|
|
540
|
+
// The span is order-normalised and the voltage is oriented onto it, so a
|
|
541
|
+
// twin declared the other way round is recognised as the *contradiction*
|
|
542
|
+
// it is rather than stamped beside the first as a second row. Keying on
|
|
543
|
+
// `positive:negative` made a reversed duplicate look like a different
|
|
544
|
+
// supply, which is two rows asserting opposite signs across one pair.
|
|
545
|
+
const low = Math.min(positive, negative);
|
|
546
|
+
const high = Math.max(positive, negative);
|
|
547
|
+
const span = `${low}:${high}`;
|
|
548
|
+
const oriented = positive === low ? law.volts : -law.volts;
|
|
549
|
+
const existing = drivenNodes.get(span);
|
|
550
|
+
if (existing !== undefined) {
|
|
551
|
+
if (existing !== oriented) {
|
|
552
|
+
throw new LoweringError(`nodes ${low} and ${high} are driven to both ${existing} V and ${oriented} V`, device.id);
|
|
553
|
+
}
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
drivenNodes.set(span, oriented);
|
|
557
|
+
stamps.push({
|
|
558
|
+
kind: "dc-source",
|
|
559
|
+
positive,
|
|
560
|
+
negative,
|
|
561
|
+
volts: law.volts,
|
|
562
|
+
sourceIndex: auxCount,
|
|
563
|
+
sourceOhms: law.sourceOhms,
|
|
564
|
+
});
|
|
565
|
+
auxCount += 1;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
case "ac-source": {
|
|
569
|
+
// The same two ends a DC supply has, read by the same roles -- `hot`/`neutral` and
|
|
570
|
+
// `line`/`neutral` are the mains pairs the amp packets declare, and a reversed
|
|
571
|
+
// sine is a sine, so orientation matters far less here than for a rail. It is read
|
|
572
|
+
// anyway rather than positionally, because the phase relationship between two
|
|
573
|
+
// windings of one transformer is not a free choice.
|
|
574
|
+
//
|
|
575
|
+
// **No twin collapsing.** A DC supply gets one, because packets state the same rail
|
|
576
|
+
// twice and two ideal sources across one span are two rows asserting one
|
|
577
|
+
// constraint. Nothing in either corpus declares a duplicate AC source, so
|
|
578
|
+
// deduplicating one here would be a rule against unexercised data; the
|
|
579
|
+
// `node-driven-by-two-sources` warning covers the case if it ever arrives.
|
|
580
|
+
const [positive, negative] = supplyTerminals(device);
|
|
581
|
+
stamps.push({
|
|
582
|
+
kind: "ac-source",
|
|
583
|
+
positive,
|
|
584
|
+
negative,
|
|
585
|
+
amplitudeVolts: law.amplitudeVolts,
|
|
586
|
+
frequencyHz: law.frequencyHz,
|
|
587
|
+
sourceIndex: auxCount,
|
|
588
|
+
sourceOhms: law.sourceOhms,
|
|
589
|
+
});
|
|
590
|
+
auxCount += 1;
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
case "ideal-opamp": {
|
|
594
|
+
const [plus, minus, output] = opampTerminals(device);
|
|
595
|
+
stamps.push({
|
|
596
|
+
kind: "ideal-opamp",
|
|
597
|
+
plus,
|
|
598
|
+
minus,
|
|
599
|
+
output,
|
|
600
|
+
sourceIndex: auxCount,
|
|
601
|
+
railHigh: law.railHigh,
|
|
602
|
+
railLow: law.railLow,
|
|
603
|
+
openLoopGain: law.openLoopGain,
|
|
604
|
+
});
|
|
605
|
+
auxCount += 1;
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
case "ota": {
|
|
609
|
+
// Absent `terminalIndices` means the terminals arrived already in law order,
|
|
610
|
+
// which is what a registry `sections` entry produces. Present means they were
|
|
611
|
+
// bound by the names the device declared; see `otaTerminalBinding`.
|
|
612
|
+
const indices = law.terminalIndices;
|
|
613
|
+
const positive = device.nodes[indices?.plus ?? 0];
|
|
614
|
+
const negative = device.nodes[indices?.minus ?? 1];
|
|
615
|
+
const output = device.nodes[indices?.output ?? 2];
|
|
616
|
+
// `bias: null` says the device declares no amplifier-bias terminal, so this is
|
|
617
|
+
// a linear OTA. Only the positional reading may fall through to position 3.
|
|
618
|
+
const bias = indices === undefined
|
|
619
|
+
? device.nodes[3]
|
|
620
|
+
: indices.bias === null
|
|
621
|
+
? undefined
|
|
622
|
+
: device.nodes[indices.bias];
|
|
623
|
+
if (positive === undefined ||
|
|
624
|
+
negative === undefined ||
|
|
625
|
+
output === undefined) {
|
|
626
|
+
throw new LoweringError(`ota ${device.id} requires positive, negative, and output terminals`, device.id);
|
|
627
|
+
}
|
|
628
|
+
if (bias !== undefined) {
|
|
629
|
+
// 4-terminal active non-linear OTA
|
|
630
|
+
const vee = law.veeIndex !== undefined &&
|
|
631
|
+
device.nodes[law.veeIndex] !== undefined
|
|
632
|
+
? device.nodes[law.veeIndex]
|
|
633
|
+
: 0;
|
|
634
|
+
stamps.push({
|
|
635
|
+
kind: "ota",
|
|
636
|
+
plus: positive,
|
|
637
|
+
minus: negative,
|
|
638
|
+
bias,
|
|
639
|
+
output,
|
|
640
|
+
vee,
|
|
641
|
+
saturationCurrent: law.saturationCurrent ?? 1e-12, // Physical Silicon diode default
|
|
642
|
+
thermalVoltage: law.thermalVoltage ?? 0.025852, // Standard thermal voltage
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
// 3-terminal static linear OTA fallback.
|
|
647
|
+
//
|
|
648
|
+
// **The inputs are crossed on purpose, and that is a bug fix.** The `vccs`
|
|
649
|
+
// stamp's convention is that current *leaves* `outP` at
|
|
650
|
+
// `gm * (v(inP) - v(inN))`, so binding `inP: positive` made this fallback
|
|
651
|
+
// **inverting** while the four-terminal `ota` stamp above injects its current
|
|
652
|
+
// and is **non-inverting**. The same catalog part therefore inverted or did
|
|
653
|
+
// not depending only on how many terminals its entry mapped -- three of the
|
|
654
|
+
// shipped OTA entries map three, five map five -- which split nine corpus
|
|
655
|
+
// packets across two opposite polarities.
|
|
656
|
+
//
|
|
657
|
+
// Non-inverting is the correct one. the part catalog's own
|
|
658
|
+
// cited reading of the CA3080/CA3094 datasheet records that with the output
|
|
659
|
+
// taken at terminal 6 the datasheet's output-mode table makes pin 3 the
|
|
660
|
+
// non-inverting input, and the entry's role groups are that reading. An OTA
|
|
661
|
+
// sourcing current into a load resistor raises its output as `v(+)` rises.
|
|
662
|
+
//
|
|
663
|
+
// Crossed here rather than negating `transconductance`, so the law's value
|
|
664
|
+
// stays positive and matches the catalog wherever it is printed.
|
|
665
|
+
stamps.push({
|
|
666
|
+
kind: "vccs",
|
|
667
|
+
outP: output,
|
|
668
|
+
outN: 0,
|
|
669
|
+
inP: negative,
|
|
670
|
+
inN: positive,
|
|
671
|
+
transconductance: law.transconductance,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
case "linear-vca": {
|
|
677
|
+
// Positional terminals only (sections path): [plus, minus, output,
|
|
678
|
+
// control]. The control pin is voltage-sense: no conductance is
|
|
679
|
+
// stamped on it, so a high-impedance depth network drives it
|
|
680
|
+
// without sagging (contrast the `ota` bias diode). The input pin
|
|
681
|
+
// is a current input at about the minus potential; the input
|
|
682
|
+
// conductance below is both its impedance and the I = V*G that
|
|
683
|
+
// the gain multiplies.
|
|
684
|
+
const plus = device.nodes[0];
|
|
685
|
+
const minus = device.nodes[1];
|
|
686
|
+
const output = device.nodes[2];
|
|
687
|
+
const control = device.nodes[3];
|
|
688
|
+
if (plus === undefined ||
|
|
689
|
+
minus === undefined ||
|
|
690
|
+
output === undefined ||
|
|
691
|
+
control === undefined) {
|
|
692
|
+
throw new LoweringError(`linear-vca ${device.id} requires plus, minus, output, and control terminals`, device.id);
|
|
693
|
+
}
|
|
694
|
+
stamps.push({
|
|
695
|
+
kind: "conductance",
|
|
696
|
+
a: plus,
|
|
697
|
+
b: minus,
|
|
698
|
+
siemens: law.inputSiemens,
|
|
699
|
+
});
|
|
700
|
+
stamps.push({
|
|
701
|
+
kind: "linear-vca",
|
|
702
|
+
plus,
|
|
703
|
+
minus,
|
|
704
|
+
output,
|
|
705
|
+
control,
|
|
706
|
+
inputSiemens: law.inputSiemens,
|
|
707
|
+
vrefVolts: law.vrefVolts,
|
|
708
|
+
minGain: law.minGain,
|
|
709
|
+
});
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
case "inverter": {
|
|
713
|
+
const { input, output, vdd, gnd } = inverterTerminals(device);
|
|
714
|
+
if (input === undefined || output === undefined) {
|
|
715
|
+
throw new Error(`inverter ${device.id} requires input and output terminals`);
|
|
716
|
+
}
|
|
717
|
+
if (vdd !== undefined && gnd !== undefined) {
|
|
718
|
+
// 1. PMOS: gate=input, drain=output, source=vdd
|
|
719
|
+
stamps.push({
|
|
720
|
+
kind: "fet",
|
|
721
|
+
gate: input,
|
|
722
|
+
drain: output,
|
|
723
|
+
source: vdd,
|
|
724
|
+
channel: "p",
|
|
725
|
+
thresholdVolts: law.thresholdVolts ?? 2.0,
|
|
726
|
+
transconductance: law.transconductance,
|
|
727
|
+
channelLengthModulation: 0,
|
|
728
|
+
subthresholdVolts: 0,
|
|
729
|
+
gateSaturationCurrent: 0,
|
|
730
|
+
gateOnsetVolts: 0,
|
|
731
|
+
gateScaleVolts: 1,
|
|
732
|
+
});
|
|
733
|
+
// 2. NMOS: gate=input, drain=output, source=gnd
|
|
734
|
+
stamps.push({
|
|
735
|
+
kind: "fet",
|
|
736
|
+
gate: input,
|
|
737
|
+
drain: output,
|
|
738
|
+
source: gnd,
|
|
739
|
+
channel: "n",
|
|
740
|
+
thresholdVolts: law.thresholdVolts ?? 2.0,
|
|
741
|
+
transconductance: law.transconductance,
|
|
742
|
+
channelLengthModulation: 0,
|
|
743
|
+
subthresholdVolts: 0,
|
|
744
|
+
gateSaturationCurrent: 0,
|
|
745
|
+
gateOnsetVolts: 0,
|
|
746
|
+
gateScaleVolts: 1,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
stamps.push({
|
|
751
|
+
kind: "vccs",
|
|
752
|
+
outP: output,
|
|
753
|
+
outN: 0, // Ground return
|
|
754
|
+
inP: 0, // Single-ended 0V ground reference
|
|
755
|
+
inN: input,
|
|
756
|
+
transconductance: law.transconductance,
|
|
757
|
+
biasVolts: law.biasVolts ?? 4.5,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
case "nand-gate": {
|
|
763
|
+
const inA = device.nodes[0];
|
|
764
|
+
const inB = device.nodes[1];
|
|
765
|
+
const output = device.nodes[2];
|
|
766
|
+
const vdd = device.nodes[3];
|
|
767
|
+
const gnd = device.nodes[4];
|
|
768
|
+
if (inA === undefined ||
|
|
769
|
+
inB === undefined ||
|
|
770
|
+
output === undefined ||
|
|
771
|
+
vdd === undefined ||
|
|
772
|
+
gnd === undefined) {
|
|
773
|
+
throw new Error(`nand-gate ${device.id} requires 5 terminals (inA, inB, output, vdd, gnd)`);
|
|
774
|
+
}
|
|
775
|
+
// A unique private internal node for the NMOS series midpoint.
|
|
776
|
+
const nodeX = allocateInternalNode();
|
|
777
|
+
// 1. PMOS A: gate=inA, drain=output, source=vdd
|
|
778
|
+
stamps.push({
|
|
779
|
+
kind: "fet",
|
|
780
|
+
gate: inA,
|
|
781
|
+
drain: output,
|
|
782
|
+
source: vdd,
|
|
783
|
+
channel: "p",
|
|
784
|
+
thresholdVolts: law.thresholdVolts,
|
|
785
|
+
transconductance: law.transconductance,
|
|
786
|
+
channelLengthModulation: 0,
|
|
787
|
+
subthresholdVolts: 0,
|
|
788
|
+
gateSaturationCurrent: 0,
|
|
789
|
+
gateOnsetVolts: 0,
|
|
790
|
+
gateScaleVolts: 1,
|
|
791
|
+
});
|
|
792
|
+
// 2. PMOS B: gate=inB, drain=output, source=vdd
|
|
793
|
+
stamps.push({
|
|
794
|
+
kind: "fet",
|
|
795
|
+
gate: inB,
|
|
796
|
+
drain: output,
|
|
797
|
+
source: vdd,
|
|
798
|
+
channel: "p",
|
|
799
|
+
thresholdVolts: law.thresholdVolts,
|
|
800
|
+
transconductance: law.transconductance,
|
|
801
|
+
channelLengthModulation: 0,
|
|
802
|
+
subthresholdVolts: 0,
|
|
803
|
+
gateSaturationCurrent: 0,
|
|
804
|
+
gateOnsetVolts: 0,
|
|
805
|
+
gateScaleVolts: 1,
|
|
806
|
+
});
|
|
807
|
+
// 3. NMOS A: gate=inA, drain=output, source=nodeX
|
|
808
|
+
stamps.push({
|
|
809
|
+
kind: "fet",
|
|
810
|
+
gate: inA,
|
|
811
|
+
drain: output,
|
|
812
|
+
source: nodeX,
|
|
813
|
+
channel: "n",
|
|
814
|
+
thresholdVolts: law.thresholdVolts,
|
|
815
|
+
transconductance: law.transconductance,
|
|
816
|
+
channelLengthModulation: 0,
|
|
817
|
+
subthresholdVolts: 0,
|
|
818
|
+
gateSaturationCurrent: 0,
|
|
819
|
+
gateOnsetVolts: 0,
|
|
820
|
+
gateScaleVolts: 1,
|
|
821
|
+
});
|
|
822
|
+
// 4. NMOS B: gate=inB, drain=nodeX, source=gnd
|
|
823
|
+
stamps.push({
|
|
824
|
+
kind: "fet",
|
|
825
|
+
gate: inB,
|
|
826
|
+
drain: nodeX,
|
|
827
|
+
source: gnd,
|
|
828
|
+
channel: "n",
|
|
829
|
+
thresholdVolts: law.thresholdVolts,
|
|
830
|
+
transconductance: law.transconductance,
|
|
831
|
+
channelLengthModulation: 0,
|
|
832
|
+
subthresholdVolts: 0,
|
|
833
|
+
gateSaturationCurrent: 0,
|
|
834
|
+
gateOnsetVolts: 0,
|
|
835
|
+
gateScaleVolts: 1,
|
|
836
|
+
});
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
case "clock-driver": {
|
|
840
|
+
const cp1 = device.nodes[0];
|
|
841
|
+
const cp2 = device.nodes[1];
|
|
842
|
+
const vgg = device.nodes[2];
|
|
843
|
+
const vdd = device.nodes[3];
|
|
844
|
+
const ox1 = device.nodes[4];
|
|
845
|
+
const gnd = device.nodes[5];
|
|
846
|
+
if (cp1 === undefined || cp2 === undefined || ox1 === undefined) {
|
|
847
|
+
throw new LoweringError(`clock-driver ${device.id} requires cp1, cp2, and ox1 terminals`, device.id);
|
|
848
|
+
}
|
|
849
|
+
stamps.push({
|
|
850
|
+
kind: "clock-driver",
|
|
851
|
+
cp1,
|
|
852
|
+
cp2,
|
|
853
|
+
vgg: vgg !== undefined ? vgg : 0,
|
|
854
|
+
vdd: vdd !== undefined ? vdd : 0,
|
|
855
|
+
ox1,
|
|
856
|
+
// Absent means circuit ground, which is what the stamp assumed before this
|
|
857
|
+
// terminal existed -- so an unbound GND pin reproduces the old behaviour
|
|
858
|
+
// rather than refusing.
|
|
859
|
+
gnd: gnd !== undefined ? gnd : 0,
|
|
860
|
+
defaultFrequency: law.defaultFrequency ?? 10000, // 10 kHz default oscillator frequency
|
|
861
|
+
stateIndex: stateCount++,
|
|
862
|
+
sourceIndex: auxCount,
|
|
863
|
+
});
|
|
864
|
+
auxCount += 3; // CP1, CP2, and VGG voltage source outputs
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
case "optocoupler": {
|
|
868
|
+
const { ledAnode, ledCathode, ldrA, ldrB } = optocouplerTerminals(device);
|
|
869
|
+
if (ledAnode === undefined ||
|
|
870
|
+
ledCathode === undefined ||
|
|
871
|
+
ldrA === undefined ||
|
|
872
|
+
ldrB === undefined) {
|
|
873
|
+
throw new Error(`optocoupler ${device.id} requires 4 terminals: anode, cathode, ldrA, ldrB`);
|
|
874
|
+
}
|
|
875
|
+
stamps.push({
|
|
876
|
+
kind: "optocoupler",
|
|
877
|
+
ledAnode,
|
|
878
|
+
ledCathode,
|
|
879
|
+
ldrA,
|
|
880
|
+
ldrB,
|
|
881
|
+
ledThresholdVolts: law.ledThresholdVolts,
|
|
882
|
+
ledTransconductance: law.ledTransconductance,
|
|
883
|
+
ldrMinOhms: law.ldrMinOhms,
|
|
884
|
+
ldrMaxOhms: law.ldrMaxOhms,
|
|
885
|
+
ldrPowerLawCoefficientOhms: law.ldrPowerLawCoefficientOhms,
|
|
886
|
+
ldrPowerLawExponent: law.ldrPowerLawExponent,
|
|
887
|
+
});
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
case "logic-divider": {
|
|
891
|
+
const clockNode = device.nodes[0];
|
|
892
|
+
const qNode = device.nodes[1];
|
|
893
|
+
const gndNode = device.nodes[2];
|
|
894
|
+
if (clockNode === undefined ||
|
|
895
|
+
qNode === undefined ||
|
|
896
|
+
gndNode === undefined) {
|
|
897
|
+
throw new Error(`logic-divider ${device.id} requires clock, q, and gnd terminals`);
|
|
898
|
+
}
|
|
899
|
+
stamps.push({
|
|
900
|
+
kind: "logic-divider",
|
|
901
|
+
clockNode,
|
|
902
|
+
qNode,
|
|
903
|
+
gndNode,
|
|
904
|
+
thresholdVolts: law.thresholdVolts,
|
|
905
|
+
highVolts: law.highVolts,
|
|
906
|
+
sourceIndex: auxCount++,
|
|
907
|
+
stateIndex: stateCount,
|
|
908
|
+
});
|
|
909
|
+
stateCount += 2;
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
case "analog-switch": {
|
|
913
|
+
const a = device.nodes[0];
|
|
914
|
+
const b = device.nodes[1];
|
|
915
|
+
const control = device.nodes[2];
|
|
916
|
+
if (a === undefined || b === undefined || control === undefined) {
|
|
917
|
+
throw new LoweringError(`analog-switch ${device.id} requires three terminals: sig_a, sig_b, and control`, device.id);
|
|
918
|
+
}
|
|
919
|
+
stamps.push({
|
|
920
|
+
kind: "analog-switch",
|
|
921
|
+
a,
|
|
922
|
+
b,
|
|
923
|
+
control,
|
|
924
|
+
onOhms: law.onOhms,
|
|
925
|
+
offOhms: law.offOhms,
|
|
926
|
+
thresholdVolts: law.thresholdVolts,
|
|
927
|
+
});
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
case "compandor": {
|
|
931
|
+
// The datasheet's Figure 5 block diagram, built out of primitives. Only the
|
|
932
|
+
// rectifier and the gain cell are bespoke; the five internal resistors, the
|
|
933
|
+
// band-gap reference and the output op-amp are stamps the runtime already
|
|
934
|
+
// had, which is why fixing this part needed no new runtime machinery beyond
|
|
935
|
+
// the one `compandor` stamp below.
|
|
936
|
+
const rectIn = device.nodes[0];
|
|
937
|
+
const rectCap = device.nodes[1];
|
|
938
|
+
const cellIn = device.nodes[2];
|
|
939
|
+
const sumNode = device.nodes[3];
|
|
940
|
+
const r3Pin = device.nodes[4];
|
|
941
|
+
const output = device.nodes[5];
|
|
942
|
+
if (rectIn === undefined ||
|
|
943
|
+
rectCap === undefined ||
|
|
944
|
+
cellIn === undefined ||
|
|
945
|
+
sumNode === undefined ||
|
|
946
|
+
r3Pin === undefined ||
|
|
947
|
+
output === undefined) {
|
|
948
|
+
throw new LoweringError(`compandor ${device.id} requires six terminals: rect_in, rect_cap, cell_in, inv_in, r3, and output`, device.id);
|
|
949
|
+
}
|
|
950
|
+
// The internal 1.8 V band-gap reference. It has no pin, so it is a private
|
|
951
|
+
// node: the rectifier and gain-cell summing nodes and the op-amp's
|
|
952
|
+
// non-inverting input all sit here, which is what makes the chip work off a
|
|
953
|
+
// single supply.
|
|
954
|
+
const vref = allocateInternalNode();
|
|
955
|
+
stamps.push({
|
|
956
|
+
kind: "dc-source",
|
|
957
|
+
positive: vref,
|
|
958
|
+
negative: GROUND,
|
|
959
|
+
volts: law.vrefVolts,
|
|
960
|
+
sourceIndex: auxCount++,
|
|
961
|
+
sourceOhms: 0,
|
|
962
|
+
});
|
|
963
|
+
// R1 and R2: the rectifier's and the gain cell's input resistors. Both
|
|
964
|
+
// terminate on VREF (Figure 5: "the summing nodes of the rectifier and G
|
|
965
|
+
// cell ... have the same potential"), so they are also the input impedance
|
|
966
|
+
// each pin presents -- 10 k on RECT_IN and 20 k on G_CELL_IN. Without these
|
|
967
|
+
// the chip would load its driver with nothing at all.
|
|
968
|
+
stamps.push({
|
|
969
|
+
kind: "conductance",
|
|
970
|
+
a: rectIn,
|
|
971
|
+
b: vref,
|
|
972
|
+
siemens: 1 / law.r1,
|
|
973
|
+
});
|
|
974
|
+
stamps.push({
|
|
975
|
+
kind: "conductance",
|
|
976
|
+
a: cellIn,
|
|
977
|
+
b: vref,
|
|
978
|
+
siemens: 1 / law.r2,
|
|
979
|
+
});
|
|
980
|
+
// R5, the rectifier's averaging resistor, to ground. The packet's external
|
|
981
|
+
// CRECT on this same node is an ordinary capacitor the solve integrates, so
|
|
982
|
+
// `tau = R5 * CRECT` comes out of the circuit rather than out of a constant.
|
|
983
|
+
stamps.push({
|
|
984
|
+
kind: "conductance",
|
|
985
|
+
a: rectCap,
|
|
986
|
+
b: GROUND,
|
|
987
|
+
siemens: 1 / law.r5,
|
|
988
|
+
});
|
|
989
|
+
// R3 from the summing node out to pin 6, and R4 from the summing node to
|
|
990
|
+
// ground. Tying pin 6 to the output pin externally is what makes R3 the
|
|
991
|
+
// feedback resistor and sets the stage gain; R4 to ground is what lifts the
|
|
992
|
+
// quiescent output to `(1 + R3/R4) * VREF`.
|
|
993
|
+
stamps.push({
|
|
994
|
+
kind: "conductance",
|
|
995
|
+
a: sumNode,
|
|
996
|
+
b: r3Pin,
|
|
997
|
+
siemens: 1 / law.r3,
|
|
998
|
+
});
|
|
999
|
+
stamps.push({
|
|
1000
|
+
kind: "conductance",
|
|
1001
|
+
a: sumNode,
|
|
1002
|
+
b: GROUND,
|
|
1003
|
+
siemens: 1 / law.r4,
|
|
1004
|
+
});
|
|
1005
|
+
// The chip's own supply pins bound this output, exactly as they bound a
|
|
1006
|
+
// discrete op-amp's. Stage 3 substitutes them; a null here means the document
|
|
1007
|
+
// declared no supply for the package, not that the part swings without limit.
|
|
1008
|
+
stamps.push({
|
|
1009
|
+
kind: "ideal-opamp",
|
|
1010
|
+
plus: vref,
|
|
1011
|
+
minus: sumNode,
|
|
1012
|
+
output,
|
|
1013
|
+
sourceIndex: auxCount++,
|
|
1014
|
+
railHigh: law.railHigh,
|
|
1015
|
+
railLow: law.railLow,
|
|
1016
|
+
openLoopGain: law.openLoopGain,
|
|
1017
|
+
});
|
|
1018
|
+
stamps.push({
|
|
1019
|
+
kind: "compandor",
|
|
1020
|
+
rectIn,
|
|
1021
|
+
rectCap,
|
|
1022
|
+
cellIn,
|
|
1023
|
+
sumNode,
|
|
1024
|
+
vref,
|
|
1025
|
+
r1: law.r1,
|
|
1026
|
+
r2: law.r2,
|
|
1027
|
+
r5: law.r5,
|
|
1028
|
+
iBias: law.iBias,
|
|
1029
|
+
stateIndex: stateCount,
|
|
1030
|
+
});
|
|
1031
|
+
// Two slots: the rectified current and the cell's transconductance. Both are
|
|
1032
|
+
// sampled once per committed sample and held across the Newton loop.
|
|
1033
|
+
stateCount += 2;
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
case "comparator": {
|
|
1037
|
+
const plus = device.nodes[0];
|
|
1038
|
+
const minus = device.nodes[1];
|
|
1039
|
+
const output = device.nodes[2];
|
|
1040
|
+
const vee = device.nodes[3];
|
|
1041
|
+
if (plus === undefined || minus === undefined || output === undefined) {
|
|
1042
|
+
throw new LoweringError(`comparator ${device.id} requires plus, minus, and output terminals`, device.id);
|
|
1043
|
+
}
|
|
1044
|
+
stamps.push({
|
|
1045
|
+
kind: "comparator",
|
|
1046
|
+
plus,
|
|
1047
|
+
minus,
|
|
1048
|
+
output,
|
|
1049
|
+
vee: vee !== undefined ? vee : 0, // Defaults to ground node if VEE pin is unwired
|
|
1050
|
+
pullDownOhms: law.pullDownOhms,
|
|
1051
|
+
floatOhms: law.floatOhms,
|
|
1052
|
+
sensitivity: law.sensitivity,
|
|
1053
|
+
});
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
case "port-engage": {
|
|
1057
|
+
// The contact against the return, closed. Both were resolved when the law
|
|
1058
|
+
// was attached, from where the supply return landed; re-deriving them here
|
|
1059
|
+
// from role names is what shorted a signal terminal to ground.
|
|
1060
|
+
const contact = device.nodes[law.contactIndex];
|
|
1061
|
+
const back = device.nodes[law.againstIndex];
|
|
1062
|
+
if (contact === undefined || back === undefined) {
|
|
1063
|
+
throw new LoweringError(`jack ${device.id} has a port-engage law naming terminals it does not have`, device.id);
|
|
1064
|
+
}
|
|
1065
|
+
stamps.push({
|
|
1066
|
+
kind: "conductance",
|
|
1067
|
+
a: contact,
|
|
1068
|
+
b: back,
|
|
1069
|
+
siemens: 1 / SWITCH_ON_OHMS,
|
|
1070
|
+
});
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
case "open":
|
|
1074
|
+
// Interface and reference symbols carry no element.
|
|
1075
|
+
break;
|
|
1076
|
+
default: {
|
|
1077
|
+
// A law with no stamp would be silently dropped from the circuit and the
|
|
1078
|
+
// result would still render -- plausible audio for a circuit that is
|
|
1079
|
+
// missing a component. Refusing is the only safe fallthrough.
|
|
1080
|
+
const unhandled = law;
|
|
1081
|
+
throw new Error(`no stamp for device law ${JSON.stringify(unhandled)} on ${deviceId}`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
// Only the region that actually contains the input jack is driven by the signal.
|
|
1086
|
+
// Regions are independent subcircuits, not a chain: a dangling supply must not be
|
|
1087
|
+
// handed the input, and must not be read for the output.
|
|
1088
|
+
const ownsInput = region.nodes.includes(inputNode);
|
|
1089
|
+
const ownsOutput = region.nodes.includes(outputNode);
|
|
1090
|
+
if (ownsInput) {
|
|
1091
|
+
stamps.push({
|
|
1092
|
+
kind: "input-source",
|
|
1093
|
+
node: inputNode,
|
|
1094
|
+
sourceIndex: auxCount,
|
|
1095
|
+
});
|
|
1096
|
+
auxCount += 1;
|
|
1097
|
+
}
|
|
1098
|
+
// A seed belongs to the region that holds the node, and regions are disjoint.
|
|
1099
|
+
const regionNodes = new Set(region.nodes);
|
|
1100
|
+
const operatingPointSeeds = latchSeeds.filter((seed) => regionNodes.has(seed.node));
|
|
1101
|
+
// Complementary collector-steering switch stamps for bistable flip-flops:
|
|
1102
|
+
// When control is engaged (>= 0.5): engagedCollector is pulled to emitter (100 ohm),
|
|
1103
|
+
// forcing engagedCollector low (0 V), putting bypassed transistor into cutoff (bypassedCollector high).
|
|
1104
|
+
// When control is bypassed (< 0.5): bypassedCollector is pulled to emitter (100 ohm),
|
|
1105
|
+
// forcing bypassedCollector low (0 V), putting engaged transistor into cutoff (engagedCollector high).
|
|
1106
|
+
for (const latch of latches) {
|
|
1107
|
+
if (latch.controlId !== null &&
|
|
1108
|
+
regionNodes.has(latch.engagedCollector) &&
|
|
1109
|
+
regionNodes.has(latch.bypassedCollector)) {
|
|
1110
|
+
stamps.push({
|
|
1111
|
+
kind: "switch",
|
|
1112
|
+
a: latch.engagedCollector,
|
|
1113
|
+
b: latch.emitter,
|
|
1114
|
+
control: latch.controlId,
|
|
1115
|
+
onOhms: 100,
|
|
1116
|
+
offOhms: 1e8,
|
|
1117
|
+
});
|
|
1118
|
+
stamps.push({
|
|
1119
|
+
kind: "switch",
|
|
1120
|
+
a: latch.bypassedCollector,
|
|
1121
|
+
b: latch.emitter,
|
|
1122
|
+
control: latch.controlId,
|
|
1123
|
+
onOhms: 1e8,
|
|
1124
|
+
offOhms: 100,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
// The row numbering. Ground is row 0 whether or not a device in this region returns
|
|
1129
|
+
// there, because the runtime pins row 0 and starts its gmin loop at 1 -- a region with
|
|
1130
|
+
// no grounded device would otherwise have its first real node silently pinned to zero.
|
|
1131
|
+
// Everything else follows in source-id order, so the numbering is a function of the
|
|
1132
|
+
// region and not of the order devices happened to be lowered in.
|
|
1133
|
+
const nodeIds = [
|
|
1134
|
+
GROUND,
|
|
1135
|
+
...[...regionNodes, ...extraNodes]
|
|
1136
|
+
.filter((node) => node !== GROUND)
|
|
1137
|
+
.sort((a, b) => a - b),
|
|
1138
|
+
];
|
|
1139
|
+
const indexByNode = new Map(nodeIds.map((node, index) => [node, index]));
|
|
1140
|
+
const row = (node) => {
|
|
1141
|
+
const index = indexByNode.get(node);
|
|
1142
|
+
if (index === undefined) {
|
|
1143
|
+
// Unreachable via `partition`, which builds `region.nodes` from exactly the devices
|
|
1144
|
+
// this loop stamped. Throwing rather than defaulting because the alternative -- a
|
|
1145
|
+
// silent 0 -- grounds an element and still renders.
|
|
1146
|
+
throw new LoweringError(`region ${region.id} stamped node ${node}, which is not one of its own`);
|
|
1147
|
+
}
|
|
1148
|
+
return index;
|
|
1149
|
+
};
|
|
1150
|
+
const remappedStamps = stamps.map((stamp) => remapStampNodes(stamp, row));
|
|
1151
|
+
const stampPartition = computeStampPartition(remappedStamps, nodeIds.length);
|
|
1152
|
+
const sparseSchedule = computeSparseSchedule({
|
|
1153
|
+
nodeCount: nodeIds.length,
|
|
1154
|
+
auxCount,
|
|
1155
|
+
stamps: remappedStamps,
|
|
1156
|
+
});
|
|
1157
|
+
return {
|
|
1158
|
+
kind: "mna",
|
|
1159
|
+
id: region.id,
|
|
1160
|
+
nodeCount: nodeIds.length,
|
|
1161
|
+
nodeIds,
|
|
1162
|
+
auxCount,
|
|
1163
|
+
stamps: remappedStamps,
|
|
1164
|
+
stampPartition,
|
|
1165
|
+
sparseSchedule,
|
|
1166
|
+
stateCount,
|
|
1167
|
+
// From the stamps, not from `region.kind`. Measured across the corpus's 191 blocks the
|
|
1168
|
+
// two agree everywhere today, so this is not a correction -- it is the derivation being
|
|
1169
|
+
// taken from the thing the runtime evaluates, so that a law which lowers to something
|
|
1170
|
+
// milder (or to nothing) cannot leave a block claiming a solver cost it does not have.
|
|
1171
|
+
linear: !stamps.some(stampNeedsNewton),
|
|
1172
|
+
controlFree: stamps.every((stamp) => stampControl(stamp) === null),
|
|
1173
|
+
eliminate: shouldEliminate(nodeIds.length + auxCount, stampPartition.portRows.length, !stamps.some(stampNeedsNewton), lawed.netlist.convergenceOptIn),
|
|
1174
|
+
// Only included when set, so a document that does not opt in lowers to a program
|
|
1175
|
+
// byte-identical to one produced before this flag existed.
|
|
1176
|
+
...(lawed.netlist.convergenceOptIn
|
|
1177
|
+
? { convergenceOptIn: true }
|
|
1178
|
+
: {}),
|
|
1179
|
+
inputNode: ownsInput ? row(inputNode) : null,
|
|
1180
|
+
outputNode: ownsOutput ? row(outputNode) : null,
|
|
1181
|
+
operatingPointSeeds: operatingPointSeeds.map((seed) => ({
|
|
1182
|
+
...seed,
|
|
1183
|
+
node: row(seed.node),
|
|
1184
|
+
})),
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* A block's row index for a source `NodeId`, or `null` when the node is not one of this
|
|
1189
|
+
* block's own.
|
|
1190
|
+
*
|
|
1191
|
+
* The forward direction of `Block.nodeIds`. `null` rather than a throw because the two
|
|
1192
|
+
* callers that need it are asking a question whose honest answer can be "not this block":
|
|
1193
|
+
* `couple.ts` locating a macro's port node, and a report matching a declared source net
|
|
1194
|
+
* against whichever region owns it.
|
|
1195
|
+
*/
|
|
1196
|
+
export function blockNodeIndex(block, node) {
|
|
1197
|
+
const index = block.nodeIds.indexOf(node);
|
|
1198
|
+
return index < 0 ? null : index;
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* A stamp read back in the document's node ids rather than its block's rows.
|
|
1202
|
+
*
|
|
1203
|
+
* The inverse of what `lowerRegion` did on the way out, for consumers that have to state a
|
|
1204
|
+
* stamp's topology in the terms the source authored -- a report naming the net a device
|
|
1205
|
+
* sits on, or an assertion written against a fixture's declared node numbers.
|
|
1206
|
+
*/
|
|
1207
|
+
export function stampInSourceNodes(block, stamp) {
|
|
1208
|
+
return remapStampNodes(stamp, (row) => block.nodeIds[row]);
|
|
1209
|
+
}
|
|
1210
|
+
export function lower(partitioning, lawed) {
|
|
1211
|
+
const { input, output } = lawed.netlist.ports;
|
|
1212
|
+
const latchSeeds = findLatchSeeds(lawed.netlist);
|
|
1213
|
+
const latches = findBistableLatches(lawed.netlist);
|
|
1214
|
+
const quietDistance = quietDistances(lawed.netlist);
|
|
1215
|
+
const opampInputDistance = opampInputDistances(lawed.netlist);
|
|
1216
|
+
const opampOutputDistance = opampOutputDistances(lawed.netlist);
|
|
1217
|
+
const invertingInputNodes = opampInvertingInputNodes(lawed.netlist);
|
|
1218
|
+
const acOutputDistance = opampOutputAcDistances(lawed.netlist);
|
|
1219
|
+
return partitioning.regions.map((region) => lowerRegion(region, lawed, input, output, latchSeeds, latches, quietDistance, opampInputDistance, opampOutputDistance, invertingInputNodes, acOutputDistance));
|
|
1220
|
+
}
|
|
1221
|
+
function terminalPair(device) {
|
|
1222
|
+
const a = device.nodes[0];
|
|
1223
|
+
const b = device.nodes[1];
|
|
1224
|
+
if (a === undefined || b === undefined) {
|
|
1225
|
+
throw new LoweringError(`device ${device.id} needs two terminals`, device.id);
|
|
1226
|
+
}
|
|
1227
|
+
return [a, b];
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Split a switch's terminals into poles: each common, then the throws that follow it.
|
|
1231
|
+
*
|
|
1232
|
+
* A multi-pole switch is one mechanism moving several independent contacts together,
|
|
1233
|
+
* so the poles share a control and are stamped side by side. Splitting at each common
|
|
1234
|
+
* handles every shape the corpus has, from `common,throwA,throwB` to a three-pole
|
|
1235
|
+
* `input_common,…,output_common,…,led_common,…`.
|
|
1236
|
+
*/
|
|
1237
|
+
function switchPoles(device) {
|
|
1238
|
+
// **Declared roles where the document states them, folded names otherwise.**
|
|
1239
|
+
//
|
|
1240
|
+
// 80 of the corpus's 198 switches declare `common`/`throw`/`coil`/`pin` on every terminal
|
|
1241
|
+
// (`vessel-dsp/artifacts`, 2026-09-03); the other 118 name what each contact *connects to* --
|
|
1242
|
+
// `effect`, `bypass`, `rectifier_output`, `bplus_reservoir` -- which says nothing about which
|
|
1243
|
+
// contact is the pole. Those keep the name path and then the positional one, and
|
|
1244
|
+
// `switchPoleUnreadable` names it rather than leaving it silent.
|
|
1245
|
+
//
|
|
1246
|
+
// A declared `pin` is not a contact of the mechanism, which is what fixed eight amps' mains
|
|
1247
|
+
// switches: a switch-and-fuse shell declares `line, switched_fused_line, neutral`, and with
|
|
1248
|
+
// `line` as the pole by position `neutral` became a *second throw of it* -- connecting live
|
|
1249
|
+
// to neutral at the other control position.
|
|
1250
|
+
const declared = device.identity.declaredTerminalRoles;
|
|
1251
|
+
const usesDeclared = declared.some((role) => role !== null);
|
|
1252
|
+
const roles = device.identity.terminalRoles;
|
|
1253
|
+
// **A `coil` is excluded by its declaration or not at all.** A seven-name table of coil
|
|
1254
|
+
// spellings used to sit on the second branch; emptying it moved no corpus program and
|
|
1255
|
+
// changed no diagnostic, and a relay whose coil matters declares `coil` -- which core has in
|
|
1256
|
+
// the switch vocabulary and the first branch already reads.
|
|
1257
|
+
const isContact = (index) => usesDeclared
|
|
1258
|
+
? declared[index] === "common" || declared[index] === "throw"
|
|
1259
|
+
: true;
|
|
1260
|
+
const contactIndices = device.nodes
|
|
1261
|
+
.map((_, index) => index)
|
|
1262
|
+
.filter((index) => isContact(index));
|
|
1263
|
+
const contactNodes = contactIndices.map((i) => device.nodes[i]);
|
|
1264
|
+
const contactRoles = contactIndices.map((i) => roles[i]);
|
|
1265
|
+
const contactDeclared = contactIndices.map((i) => declared[i]);
|
|
1266
|
+
const commonIndices = contactNodes
|
|
1267
|
+
.map((_, index) => index)
|
|
1268
|
+
// **A pole is named by its declaration or by nothing.** `switchCommonRoles` -- twelve
|
|
1269
|
+
// spellings of "this is the pole" -- is gone: all 126 corpus switches whose terminal
|
|
1270
|
+
// names stated a pole now declare `common`, and no undeclared switch carries a name that
|
|
1271
|
+
// would have matched. A switch declaring nothing falls to its first contact exactly as
|
|
1272
|
+
// before, and `switchPoleUnreadable` reports that whenever there are three or more.
|
|
1273
|
+
.filter((index) => contactDeclared[index] === "common");
|
|
1274
|
+
// No terminal names itself common, so the first is it -- which is what the shapes
|
|
1275
|
+
// spelling it `input` or `lug14` mean, and all a document of bare lugs can support.
|
|
1276
|
+
if (commonIndices.length === 0) {
|
|
1277
|
+
const [common, ...throws] = contactNodes;
|
|
1278
|
+
return common === undefined || throws.length === 0
|
|
1279
|
+
? []
|
|
1280
|
+
: [{ common, throws }];
|
|
1281
|
+
}
|
|
1282
|
+
// One common: every other terminal is a throw of it, **wherever it sits in the
|
|
1283
|
+
// list**. `throw0,throw1,common` is a real corpus shape, and attaching throws to a
|
|
1284
|
+
// preceding common instead would drop the only real one -- which silently left the
|
|
1285
|
+
// input jack's node connected to nothing at all.
|
|
1286
|
+
if (commonIndices.length === 1) {
|
|
1287
|
+
const at = commonIndices[0];
|
|
1288
|
+
const common = contactNodes[at];
|
|
1289
|
+
const throws = contactNodes.filter((_, index) => index !== at);
|
|
1290
|
+
return throws.length === 0 ? [] : [{ common, throws }];
|
|
1291
|
+
}
|
|
1292
|
+
// Several poles of one mechanism. Each common takes the terminals that follow it;
|
|
1293
|
+
// anything declared before the first common belongs to that first pole.
|
|
1294
|
+
const poles = commonIndices.map((at) => ({
|
|
1295
|
+
common: contactNodes[at],
|
|
1296
|
+
throws: [],
|
|
1297
|
+
}));
|
|
1298
|
+
contactNodes.forEach((node, index) => {
|
|
1299
|
+
if (commonIndices.includes(index)) {
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const owner = commonIndices.filter((at) => at < index).length;
|
|
1303
|
+
poles[Math.max(0, owner - 1)]?.throws.push(node);
|
|
1304
|
+
});
|
|
1305
|
+
return poles.filter((pole) => pole.throws.length > 0);
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* A diode's anode and cathode, by role where the document names them.
|
|
1309
|
+
*
|
|
1310
|
+
* Falls back to declaration order for `a,b` and bare lugs, which carry no orientation
|
|
1311
|
+
* evidence and are all such a document supports.
|
|
1312
|
+
*/
|
|
1313
|
+
function diodeTerminals(device) {
|
|
1314
|
+
// The declared role, not the terminal name. A document whose ends are `end`/`end` -- core's
|
|
1315
|
+
// role for two interchangeable ends -- states no orientation, so it falls through to
|
|
1316
|
+
// declaration order exactly as an unnamed pair does.
|
|
1317
|
+
const roles = device.identity.declaredTerminalRoles;
|
|
1318
|
+
const anodeIndex = roles.indexOf("anode");
|
|
1319
|
+
const cathodeIndex = roles.indexOf("cathode");
|
|
1320
|
+
if (anodeIndex !== -1 && cathodeIndex !== -1) {
|
|
1321
|
+
const anode = device.nodes[anodeIndex];
|
|
1322
|
+
const cathode = device.nodes[cathodeIndex];
|
|
1323
|
+
if (anode !== undefined && cathode !== undefined) {
|
|
1324
|
+
return [anode, cathode];
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return terminalPair(device);
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* The junctions a diode component contains: one for a plain diode, four for a bridge, one per
|
|
1331
|
+
* anode for a shared-cathode pack.
|
|
1332
|
+
*
|
|
1333
|
+
* **The old reading took the first two terminals and dropped the rest, without a word.** A bridge
|
|
1334
|
+
* rectifier declared `[ac_a, ac_b, positive, negative]` has no `anode` role, so it fell through to
|
|
1335
|
+
* declaration order and lowered to a single junction **between its two AC inputs** — a diode across
|
|
1336
|
+
* the source, with both DC terminals left touching nothing. That is not an approximation of a
|
|
1337
|
+
* bridge, it is a different circuit: the rail it was supposed to produce does not exist. Same shape
|
|
1338
|
+
* as the transformer's positional reading, which was silently wrong on 45 of 55.
|
|
1339
|
+
*
|
|
1340
|
+
* Two topologies are recognised, and both are decided by whole role tokens against
|
|
1341
|
+
* `diodeTerminalRoles`, never by position:
|
|
1342
|
+
*
|
|
1343
|
+
* - **A bridge**: two AC legs, one DC positive and one DC negative. Each AC leg gets a junction up
|
|
1344
|
+
* to `positive` and one from `negative`, which is the four-diode bridge exactly — on either half
|
|
1345
|
+
* cycle the more positive AC leg conducts into `positive` and `negative` conducts into the other.
|
|
1346
|
+
* - **A shared-cathode pack**: one or more anodes and exactly one cathode. One junction per anode.
|
|
1347
|
+
* A dual rectifier and a bi-colour LED are both this, which is why the rule is the shape rather
|
|
1348
|
+
* than the part.
|
|
1349
|
+
*
|
|
1350
|
+
* **What this fixes is topology, not device law.** Eight of the amp corpus's multi-junction
|
|
1351
|
+
* `diode` components are *tube* rectifiers — a 5U4GB, four GZ34s, two 5Y3s and a dual 5U4 — spelled
|
|
1352
|
+
* `plate_a`/`cathode`/`heater_a` under `kind: diode`. They now lower to the right number of
|
|
1353
|
+
* junctions in the right direction, and each junction keeps the **silicon** law the declared kind
|
|
1354
|
+
* asks for: a 0.7 V drop where a 5Y3 drops 40-60 V under load. Reading a vacuum law out of a
|
|
1355
|
+
* terminal name would be deciding a device's physics from a token, which is precisely what this
|
|
1356
|
+
* pipeline forbids — the kind is the evidence for the law, and the roles are evidence only for the
|
|
1357
|
+
* wiring. That mismatch is a **source** question (the format has a `tube-diode` kind and three
|
|
1358
|
+
* instances use it) and it stays unfiled: no document compiles far enough for a diagnostic to name
|
|
1359
|
+
* it, and the evidence gate requires one.
|
|
1360
|
+
*/
|
|
1361
|
+
function diodeJunctions(device) {
|
|
1362
|
+
// Two terminals keep the original reading exactly, including its positional fallback for a
|
|
1363
|
+
// document of bare lugs. Every diode in every compiled packet takes this path, which is what
|
|
1364
|
+
// makes the change corpus-neutral by construction rather than by hope.
|
|
1365
|
+
if (device.nodes.length === 2) {
|
|
1366
|
+
const [anode, cathode] = diodeTerminals(device);
|
|
1367
|
+
return [{ anode, cathode }];
|
|
1368
|
+
}
|
|
1369
|
+
const anodes = [];
|
|
1370
|
+
const acLegs = [];
|
|
1371
|
+
let cathode = null;
|
|
1372
|
+
let dcPositive = null;
|
|
1373
|
+
let dcNegative = null;
|
|
1374
|
+
const single = (held, node, electrode) => {
|
|
1375
|
+
if (held !== null) {
|
|
1376
|
+
throw new LoweringError(`diode ${device.id} names two ${electrode} terminals, so it is more than one device in one component`, device.id);
|
|
1377
|
+
}
|
|
1378
|
+
return node;
|
|
1379
|
+
};
|
|
1380
|
+
// Reads each terminal's declared `role`, which replaced a ~30-entry spelling table. `ac` is
|
|
1381
|
+
// the role a bridge's alternating leg declares (`@vessel-dsp/core@0.6.33`): it is neither an
|
|
1382
|
+
// anode nor a cathode, because on one half cycle it conducts into the positive rail and on
|
|
1383
|
+
// the other the negative rail conducts into it.
|
|
1384
|
+
device.identity.declaredTerminalRoles.forEach((role, index) => {
|
|
1385
|
+
const node = device.nodes[index];
|
|
1386
|
+
if (node === undefined) {
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
if (role === null) {
|
|
1390
|
+
throw new LoweringError(`diode ${device.id} has a terminal that declares no role, and a junction's direction cannot be read from terminal order once there are more than two`, device.id);
|
|
1391
|
+
}
|
|
1392
|
+
switch (role) {
|
|
1393
|
+
// A plate is an anode: eight corpus tube rectifiers declare `kind: diode`, and this
|
|
1394
|
+
// stage places their junctions while the declared kind still chooses the law.
|
|
1395
|
+
case "anode":
|
|
1396
|
+
case "plate":
|
|
1397
|
+
anodes.push(node);
|
|
1398
|
+
return;
|
|
1399
|
+
case "ac":
|
|
1400
|
+
acLegs.push(node);
|
|
1401
|
+
return;
|
|
1402
|
+
case "cathode":
|
|
1403
|
+
cathode = single(cathode, node, "cathode");
|
|
1404
|
+
return;
|
|
1405
|
+
case "positive":
|
|
1406
|
+
dcPositive = single(dcPositive, node, "DC positive");
|
|
1407
|
+
return;
|
|
1408
|
+
case "negative":
|
|
1409
|
+
dcNegative = single(dcNegative, node, "DC negative");
|
|
1410
|
+
return;
|
|
1411
|
+
// A heater carries no signal current, and an `end` states no orientation to place.
|
|
1412
|
+
case "heater":
|
|
1413
|
+
case "end":
|
|
1414
|
+
return;
|
|
1415
|
+
default:
|
|
1416
|
+
throw new LoweringError(`diode ${device.id}: terminal declares role "${role}", which is not an electrode a multi-junction diode has`, device.id);
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
const positive = dcPositive;
|
|
1420
|
+
const negative = dcNegative;
|
|
1421
|
+
const shared = cathode;
|
|
1422
|
+
if (acLegs.length === 2 && positive !== null && negative !== null) {
|
|
1423
|
+
if (anodes.length > 0 || shared !== null) {
|
|
1424
|
+
throw new LoweringError(`diode ${device.id} names both a bridge's AC and DC legs and a separate anode or cathode, which is two topologies at once`, device.id);
|
|
1425
|
+
}
|
|
1426
|
+
return acLegs.flatMap((leg) => [
|
|
1427
|
+
{ anode: leg, cathode: positive },
|
|
1428
|
+
{ anode: negative, cathode: leg },
|
|
1429
|
+
]);
|
|
1430
|
+
}
|
|
1431
|
+
if (acLegs.length === 0 && shared !== null && anodes.length > 0) {
|
|
1432
|
+
return anodes.map((anode) => ({ anode, cathode: shared }));
|
|
1433
|
+
}
|
|
1434
|
+
throw new LoweringError(`diode ${device.id} declares ${device.nodes.length} terminals in no topology this stage can place: ` +
|
|
1435
|
+
"a bridge needs two AC legs with a DC positive and negative, and a diode pack needs one cathode with at least one anode", device.id);
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* The two ends of a supply, as the corpus names them. Counted, not guessed: `positive`
|
|
1439
|
+
* 47, `negative` 46, `neutral` 17, `hot` 16, `anode`/`cathode` 4 each, `+` 3, `line` 2,
|
|
1440
|
+
* `return` 1. `hot`/`neutral` and `line`/`neutral` are mains pairs in the amp packets.
|
|
1441
|
+
* `plus`/`minus` are absent from the corpus but are unambiguous polarity names a
|
|
1442
|
+
* hand-authored document may use, and they are what the fixtures here declare.
|
|
1443
|
+
*/
|
|
1444
|
+
/**
|
|
1445
|
+
* A supply's driven end and its return, by role where the document names them.
|
|
1446
|
+
*
|
|
1447
|
+
* This was the last asymmetric device still read by declaration order, and it fails the
|
|
1448
|
+
* same way the diode and the op-amp did: reversing `positive` and `minus` inverts a rail
|
|
1449
|
+
* and the packet still compiles `ok`. A rail is not a subtle wrong answer -- every bias
|
|
1450
|
+
* point in the circuit hangs off its sign.
|
|
1451
|
+
*
|
|
1452
|
+
* **Read from the declared role since 2026-09-03**, and the fallback is now taken only when the
|
|
1453
|
+
* source *says* the two ends are symmetric rather than when it says nothing. `ac_a`/`ac_b` and
|
|
1454
|
+
* `heater_a`/`heater_b` are the two ends of a winding modelled as a source: they declare
|
|
1455
|
+
* `end`/`end`, which states that there is no polarity to read, and declaration order is then all
|
|
1456
|
+
* such a document supports. That is a different fact from a supply whose terminals declare
|
|
1457
|
+
* nothing, and matching folded *names* could not tell the two apart -- `'-'` folded away to no
|
|
1458
|
+
* role at all and landed in the same bucket as a bare lug.
|
|
1459
|
+
*
|
|
1460
|
+
* Measured across the corpus after the backfill: 72 supplies declare exactly one `positive` and
|
|
1461
|
+
* one `negative`, 2 declare `end`/`end`, 1 has a single terminal, and none declares nothing.
|
|
1462
|
+
*/
|
|
1463
|
+
function supplyTerminals(device) {
|
|
1464
|
+
// A rail names one node and means "against ground", which is not a polarity question:
|
|
1465
|
+
// there is only one end to place. Reading roles here would refuse 34 packets for
|
|
1466
|
+
// wanting a terminal a rail never has.
|
|
1467
|
+
const single = device.nodes[0];
|
|
1468
|
+
if (device.nodes.length === 1 && single !== undefined) {
|
|
1469
|
+
return [single, GROUND];
|
|
1470
|
+
}
|
|
1471
|
+
const roles = device.identity.declaredTerminalRoles;
|
|
1472
|
+
const driveIndex = roles.findIndex((role) => role === "positive");
|
|
1473
|
+
const returnIndex = roles.findIndex((role) => role === "negative");
|
|
1474
|
+
// One end named is enough: the other is the remaining terminal. Requiring both would
|
|
1475
|
+
// discard the evidence in a supply declaring `positive` beside an unlabelled lug.
|
|
1476
|
+
const resolved = driveIndex !== -1 && driveIndex !== returnIndex
|
|
1477
|
+
? [
|
|
1478
|
+
driveIndex,
|
|
1479
|
+
returnIndex === -1 ? 1 - driveIndex : returnIndex,
|
|
1480
|
+
]
|
|
1481
|
+
: returnIndex !== -1
|
|
1482
|
+
? [1 - returnIndex, returnIndex]
|
|
1483
|
+
: null;
|
|
1484
|
+
if (resolved !== null) {
|
|
1485
|
+
const positive = device.nodes[resolved[0]];
|
|
1486
|
+
const negative = device.nodes[resolved[1]];
|
|
1487
|
+
if (positive !== undefined && negative !== undefined) {
|
|
1488
|
+
return [positive, negative];
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return terminalPair(device);
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Slots a reactive element's companion model needs: the voltage across it and the current
|
|
1495
|
+
* through it, which is what a trapezoidal companion carries between samples.
|
|
1496
|
+
*
|
|
1497
|
+
* The program declares state in slots and each stamp declares the offset its own state
|
|
1498
|
+
* begins at, so both are the same unit and no executor has to know a hidden convention.
|
|
1499
|
+
*/
|
|
1500
|
+
export const STATE_SLOTS_PER_REACTIVE_ELEMENT = 2;
|
|
1501
|
+
/**
|
|
1502
|
+
* The one terminal index whose **declared** role is `role`, or null when none or several are.
|
|
1503
|
+
*
|
|
1504
|
+
* This replaced seven name-vocabulary sets: `triodeGridRoles`, `triodeCathodeRoles`,
|
|
1505
|
+
* `triodePlateRoles`, `pentodeScreenRoles`, and the transistor control/upper/lower channel
|
|
1506
|
+
* triple. All seven existed to recover an electrode from a terminal's *name*, which is a
|
|
1507
|
+
* spelling problem the format no longer has -- `@vessel-dsp/core@0.6.28` gives every terminal a
|
|
1508
|
+
* typed `role`, so the electrode is read rather than inferred. There is nothing to fold, alias,
|
|
1509
|
+
* or guess, and `c` no longer has to mean a collector on one kind and a cathode on another.
|
|
1510
|
+
*
|
|
1511
|
+
* **Ambiguity is still null rather than the first match**, for the reason `uniqueRoleIndex`
|
|
1512
|
+
* records: two terminals declaring `plate` is correct on a dual rectifier and wrong on a triode,
|
|
1513
|
+
* and which it is belongs to the caller. A resolver asking for one plate gets null;
|
|
1514
|
+
* `tubeDiodePlates` asks for all of them.
|
|
1515
|
+
*/
|
|
1516
|
+
function declaredIndex(device, role) {
|
|
1517
|
+
const found = device.identity.declaredTerminalRoles.flatMap((declared, index) => declared === role ? [index] : []);
|
|
1518
|
+
return found.length === 1 ? (found[0] ?? null) : null;
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Indices for `roles` in order, or null when any one is unresolved.
|
|
1522
|
+
*
|
|
1523
|
+
* All-or-nothing for the reason `roleIndices` gives: every caller treats a partial read as no
|
|
1524
|
+
* read, because a tube with a plate but no identifiable cathode cannot be stamped from the half
|
|
1525
|
+
* that resolved.
|
|
1526
|
+
*/
|
|
1527
|
+
function declaredIndices(device, roles) {
|
|
1528
|
+
const indices = roles.map((role) => declaredIndex(device, role));
|
|
1529
|
+
return indices.every((index) => index !== null)
|
|
1530
|
+
? indices
|
|
1531
|
+
: null;
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* The one terminal index whose declared role is in `accepted`, or `null` when none is or
|
|
1535
|
+
* more than one is.
|
|
1536
|
+
*
|
|
1537
|
+
* **Ambiguity is `null` rather than the first match.** Two terminals both claiming `plate`
|
|
1538
|
+
* is a source defect, and choosing between them would wire the device from a coin toss —
|
|
1539
|
+
* so the callers that can refuse do, and the callers that fall back to declaration order
|
|
1540
|
+
* fall back wholesale rather than half-resolved.
|
|
1541
|
+
*
|
|
1542
|
+
* `fold` exists because the optocoupler's copy of this read carried `foldToken` where the
|
|
1543
|
+
* other four compared the role as given. That divergence is the reason this is one function:
|
|
1544
|
+
* the five copies were not identical, and nothing would have reported it.
|
|
1545
|
+
*/
|
|
1546
|
+
function uniqueRoleIndex(roles, accepted, fold = (role) => role) {
|
|
1547
|
+
const found = roles.flatMap((role, index) => role !== null && accepted.has(fold(role)) ? [index] : []);
|
|
1548
|
+
return found.length === 1 ? (found[0] ?? null) : null;
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Terminal indices for `slots` in order, or `null` when any one of them is unresolved.
|
|
1552
|
+
*
|
|
1553
|
+
* All-or-nothing because every caller treats a partial read as no read: a tube with a plate
|
|
1554
|
+
* but no identifiable cathode cannot be stamped from the half that resolved.
|
|
1555
|
+
*
|
|
1556
|
+
* Returns **indices, not nodes**, so each caller keeps its own missing-node refusal. Those
|
|
1557
|
+
* are not interchangeable — a pentode that named an electrode it has no terminal for and a
|
|
1558
|
+
* BJT missing a third node are different source defects and say so.
|
|
1559
|
+
*/
|
|
1560
|
+
function roleIndices(roles, slots, fold) {
|
|
1561
|
+
const indices = slots.map((slot) => uniqueRoleIndex(roles, slot, fold));
|
|
1562
|
+
return indices.every((index) => index !== null)
|
|
1563
|
+
? indices
|
|
1564
|
+
: null;
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* A pentode's `(grid, cathode, plate, screen)`, by role, refusing rather than guessing.
|
|
1568
|
+
*
|
|
1569
|
+
* A `suppressor` and a `heater` terminal are read and ignored on purpose: a suppressor is tied
|
|
1570
|
+
* to the cathode in practice, and a heater carries no signal current. Their nodes end up
|
|
1571
|
+
* carrying only `gmin`, which is the same situation a FET gate is already in.
|
|
1572
|
+
*
|
|
1573
|
+
* The two ganged documents in the corpus (`grid_a,grid_b,...` and `plates_a,plates_b,...`) name
|
|
1574
|
+
* **two** pentodes in one component, so `only` finds two grids and refuses. That is the right
|
|
1575
|
+
* answer: this law is one tube, and silently taking the first of each would model half the
|
|
1576
|
+
* device and render.
|
|
1577
|
+
*/
|
|
1578
|
+
function pentodeTerminals(device) {
|
|
1579
|
+
const indices = declaredIndices(device, ["grid", "cathode", "plate", "screen"]);
|
|
1580
|
+
if (indices === null) {
|
|
1581
|
+
throw new LoweringError(`pentode ${device.id} does not name a single grid, cathode, plate and screen among its terminals`, device.id);
|
|
1582
|
+
}
|
|
1583
|
+
const nodes = indices.map((index) => device.nodes[index]);
|
|
1584
|
+
if (nodes.some((node) => node === undefined)) {
|
|
1585
|
+
throw new LoweringError(`pentode ${device.id} names an electrode it has no terminal for`, device.id);
|
|
1586
|
+
}
|
|
1587
|
+
return nodes;
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* A triode's `(grid, cathode, plate)`, by role.
|
|
1591
|
+
*
|
|
1592
|
+
* Deliberately **not** falling back to declaration order the way the BJT does. A BJT's
|
|
1593
|
+
* positional fallback is defensible because its three terminals appear in a conventional
|
|
1594
|
+
* order across the corpus; a tube's do not, and swapping plate for cathode turns a gain
|
|
1595
|
+
* stage into a cathode follower that still renders. A triode whose roles cannot be read is
|
|
1596
|
+
* a missing source fact, so this refuses rather than guessing.
|
|
1597
|
+
*/
|
|
1598
|
+
function triodeTerminals(device) {
|
|
1599
|
+
const indices = declaredIndices(device, ["grid", "cathode", "plate"]);
|
|
1600
|
+
if (indices === null) {
|
|
1601
|
+
throw new LoweringError(`triode ${device.id} does not name a single grid, cathode and plate among its terminals`, device.id);
|
|
1602
|
+
}
|
|
1603
|
+
const nodes = indices.map((index) => device.nodes[index]);
|
|
1604
|
+
if (nodes.some((node) => node === undefined)) {
|
|
1605
|
+
throw new LoweringError(`triode ${device.id} needs three terminals`, device.id);
|
|
1606
|
+
}
|
|
1607
|
+
return nodes;
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* A tube diode's plates, each paired with the shared cathode.
|
|
1611
|
+
*
|
|
1612
|
+
* Refuses rather than falling back to declaration order, for the triode's reason: a rectifier
|
|
1613
|
+
* wired backwards conducts on the wrong half-cycle and still renders, and the corpus does not
|
|
1614
|
+
* declare these terminals in a settled order.
|
|
1615
|
+
*/
|
|
1616
|
+
function tubeDiodePlates(device) {
|
|
1617
|
+
const plates = [];
|
|
1618
|
+
let cathode = null;
|
|
1619
|
+
// Reads the declared role, so `plate_a` and `plate_b` are two terminals carrying one role
|
|
1620
|
+
// rather than two spellings to look up. This is the case the `devices` construct exists for:
|
|
1621
|
+
// a dual rectifier is two diodes on one cathode, and until a document declares that split,
|
|
1622
|
+
// pairing every plate with the shared cathode is the same answer.
|
|
1623
|
+
device.identity.declaredTerminalRoles.forEach((role, index) => {
|
|
1624
|
+
const node = device.nodes[index];
|
|
1625
|
+
if (node === undefined) {
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
if (role === null) {
|
|
1629
|
+
throw new LoweringError(`tube diode ${device.id} has a terminal that declares no role, and a rectifier's plate cannot be told from its cathode by position`, device.id);
|
|
1630
|
+
}
|
|
1631
|
+
if (role === "plate") {
|
|
1632
|
+
plates.push(node);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
if (role === "cathode") {
|
|
1636
|
+
if (cathode !== null) {
|
|
1637
|
+
throw new LoweringError(`tube diode ${device.id} names two cathodes, so it is two devices in one component`, device.id);
|
|
1638
|
+
}
|
|
1639
|
+
cathode = node;
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
// A heater carries no signal current, so it is read and ignored, as it always was. Any
|
|
1643
|
+
// other role is a device this law is not: refuse by name rather than place it.
|
|
1644
|
+
if (role !== "heater") {
|
|
1645
|
+
throw new LoweringError(`tube diode ${device.id}: terminal declares role "${role}", which is not an electrode a tube rectifier has`, device.id);
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
if (cathode === null || plates.length === 0) {
|
|
1649
|
+
throw new LoweringError(`tube diode ${device.id} does not name a cathode and at least one plate among its terminals`, device.id);
|
|
1650
|
+
}
|
|
1651
|
+
const shared = cathode;
|
|
1652
|
+
return plates.map((plate) => ({ plate, cathode: shared }));
|
|
1653
|
+
}
|
|
1654
|
+
function bjtTerminals(device) {
|
|
1655
|
+
// Read the roles, because **449 of the corpus's 490 transistors do not put the
|
|
1656
|
+
// control terminal first**. `collector,base,emitter` alone is 282 of them, and
|
|
1657
|
+
// taking position 1 as the base exchanges base and collector on nearly every
|
|
1658
|
+
// transistor there is: the stage never turns on and the pedal renders silence.
|
|
1659
|
+
const indices = declaredIndices(device, ["base", "collector", "emitter"]) ??
|
|
1660
|
+
declaredIndices(device, ["gate", "drain", "source"]);
|
|
1661
|
+
// Unlabelled or ambiguous roles keep declaration order, which is all a document of
|
|
1662
|
+
// bare positional pins supports.
|
|
1663
|
+
const [control, upper, lower] = indices === null
|
|
1664
|
+
? device.nodes
|
|
1665
|
+
: indices.map((index) => device.nodes[index]);
|
|
1666
|
+
if (control === undefined || upper === undefined || lower === undefined) {
|
|
1667
|
+
throw new LoweringError(`bjt ${device.id} needs base, collector and emitter`, device.id);
|
|
1668
|
+
}
|
|
1669
|
+
return [control, upper, lower];
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* An op-amp's `(+in, -in, out)` from its **declared** roles, refusing rather than guessing.
|
|
1673
|
+
*
|
|
1674
|
+
* Order settles nothing: the corpus writes `inverting,nonInverting,output`,
|
|
1675
|
+
* `positive,out,negative` and `negative,positive,out`, so taking position exchanges the two
|
|
1676
|
+
* inputs and inverts the stage -- a wrong answer that still renders and is audible only by
|
|
1677
|
+
* comparison.
|
|
1678
|
+
*
|
|
1679
|
+
* This read the folded terminal *name* against three closed sets and fell back to declaration
|
|
1680
|
+
* order when they did not resolve, which is the silent half of the role-vocabulary problem: a
|
|
1681
|
+
* document naming its pins anything outside those sets got positions 0, 1 and 2 and nothing said
|
|
1682
|
+
* so. Measured after the roles were backfilled: 314 of 315 corpus op-amps declare exactly one of
|
|
1683
|
+
* each, and none declares no role, so the positional path had no legitimate case left.
|
|
1684
|
+
*
|
|
1685
|
+
* **Several of each is a package, not an ambiguity.** `boss-dm-3`'s `IC1` declares eight terminals
|
|
1686
|
+
* and two of every signal role, because it is a dual op-amp whose sections
|
|
1687
|
+
* `@vessel-dsp/core@0.6.33`'s `devices` construct exists to separate. Until that is declared this
|
|
1688
|
+
* refuses too, naming the construct, rather than picking a section by position.
|
|
1689
|
+
*/
|
|
1690
|
+
function opampTerminals(device) {
|
|
1691
|
+
const declared = device.identity.declaredTerminalRoles;
|
|
1692
|
+
const indices = ["nonInverting", "inverting", "output"].map((role) => {
|
|
1693
|
+
const found = declared.flatMap((value, index) => value === role ? [index] : []);
|
|
1694
|
+
return found.length === 1 ? found[0] : null;
|
|
1695
|
+
});
|
|
1696
|
+
if (indices.some((index) => index === null)) {
|
|
1697
|
+
const counts = ["nonInverting", "inverting", "output"]
|
|
1698
|
+
.map((role) => `${role}=${declared.filter((value) => value === role).length}`)
|
|
1699
|
+
.join(" ");
|
|
1700
|
+
throw new LoweringError(`op-amp ${device.id} does not identify one non-inverting input, one inverting input ` +
|
|
1701
|
+
`and one output (${counts}) -- several of each is a package whose sections a ` +
|
|
1702
|
+
"`devices` declaration has to separate, and taking terminals by position instead " +
|
|
1703
|
+
"exchanges the inputs and inverts the stage", device.id);
|
|
1704
|
+
}
|
|
1705
|
+
const [plus, minus, output] = indices.map((index) => device.nodes[index]);
|
|
1706
|
+
if (plus === undefined || minus === undefined || output === undefined) {
|
|
1707
|
+
throw new LoweringError(`op-amp ${device.id} needs three terminals`, device.id);
|
|
1708
|
+
}
|
|
1709
|
+
return [plus, minus, output];
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* An optocoupler's four connections from the terminals' **declared roles**.
|
|
1713
|
+
*
|
|
1714
|
+
* Core's vocabulary for the kind is `anode`, `cathode`, `end`, `pin`: the LED states a direction
|
|
1715
|
+
* and the cell states none, which is the physics -- a photoresistor conducts the same either way,
|
|
1716
|
+
* so its two `end` terminals are taken in declaration order and that is a reading rather than a
|
|
1717
|
+
* guess. A three-terminal part is the common tremolo package, whose LED cathode is the chassis:
|
|
1718
|
+
* one `anode` and two `end`s, with the return implied.
|
|
1719
|
+
*/
|
|
1720
|
+
function declaredOptocouplerNodes(device) {
|
|
1721
|
+
const declared = device.identity.declaredTerminalRoles;
|
|
1722
|
+
const at = (role) => {
|
|
1723
|
+
const found = declared.reduce((acc, value, index) => (value === role ? [...acc, index] : acc), []);
|
|
1724
|
+
return found.length === 1 ? (found[0] ?? null) : null;
|
|
1725
|
+
};
|
|
1726
|
+
const ends = declared.flatMap((role, index) => (role === "end" ? [index] : []));
|
|
1727
|
+
const anodeIndex = at("anode");
|
|
1728
|
+
if (anodeIndex === null || ends.length !== 2) {
|
|
1729
|
+
return null;
|
|
1730
|
+
}
|
|
1731
|
+
const cathodeIndex = at("cathode");
|
|
1732
|
+
const ledAnode = device.nodes[anodeIndex];
|
|
1733
|
+
const ldrA = device.nodes[ends[0] ?? -1];
|
|
1734
|
+
const ldrB = device.nodes[ends[1] ?? -1];
|
|
1735
|
+
const ledCathode = cathodeIndex === null ? GROUND : device.nodes[cathodeIndex];
|
|
1736
|
+
if (ledAnode === undefined ||
|
|
1737
|
+
ledCathode === undefined ||
|
|
1738
|
+
ldrA === undefined ||
|
|
1739
|
+
ldrB === undefined) {
|
|
1740
|
+
return null;
|
|
1741
|
+
}
|
|
1742
|
+
return { ledAnode, ledCathode, ldrA, ldrB };
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* The same four connections from the terminals' **names**, which is the older path and one of the
|
|
1746
|
+
* last two name vocabularies in this compiler. It stays until the four corpus optocouplers that
|
|
1747
|
+
* depend on it declare roles instead; `optocouplerBinding` is what reports them.
|
|
1748
|
+
*/
|
|
1749
|
+
function namedOptocouplerNodes(device) {
|
|
1750
|
+
const roles = device.identity.terminalRoles;
|
|
1751
|
+
// Folded, unlike the four resolvers above, which compare the role as given.
|
|
1752
|
+
const only = (accepted) => uniqueRoleIndex(roles, accepted, foldToken);
|
|
1753
|
+
const lampDriveIndex = only(new Set(["lampdrive"]));
|
|
1754
|
+
const ldrSignalIndex = only(new Set(["ldrsignal", "ldra"]));
|
|
1755
|
+
const ldrReturnIndex = only(new Set(["ldrreturn", "ldrb"]));
|
|
1756
|
+
if (lampDriveIndex !== null &&
|
|
1757
|
+
ldrSignalIndex !== null &&
|
|
1758
|
+
ldrReturnIndex !== null &&
|
|
1759
|
+
device.nodes.length === 3) {
|
|
1760
|
+
const ledAnode = device.nodes[lampDriveIndex];
|
|
1761
|
+
const ldrA = device.nodes[ldrSignalIndex];
|
|
1762
|
+
const ldrB = device.nodes[ldrReturnIndex];
|
|
1763
|
+
if (ledAnode !== undefined && ldrA !== undefined && ldrB !== undefined) {
|
|
1764
|
+
return { ledAnode, ledCathode: GROUND, ldrA, ldrB };
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
const anodeIndex = only(new Set(["anode", "ledanode", "lampdrive"]));
|
|
1768
|
+
const cathodeIndex = only(new Set(["cathode", "ledcathode", "lampreturn"]));
|
|
1769
|
+
const ldrAIndex = only(new Set(["ldra", "ldrsignal"]));
|
|
1770
|
+
const ldrBIndex = only(new Set(["ldrb", "ldrreturn"]));
|
|
1771
|
+
if (anodeIndex !== null &&
|
|
1772
|
+
cathodeIndex !== null &&
|
|
1773
|
+
ldrAIndex !== null &&
|
|
1774
|
+
ldrBIndex !== null) {
|
|
1775
|
+
const ledAnode = device.nodes[anodeIndex];
|
|
1776
|
+
const ledCathode = device.nodes[cathodeIndex];
|
|
1777
|
+
const ldrA = device.nodes[ldrAIndex];
|
|
1778
|
+
const ldrB = device.nodes[ldrBIndex];
|
|
1779
|
+
if (ledAnode !== undefined &&
|
|
1780
|
+
ledCathode !== undefined &&
|
|
1781
|
+
ldrA !== undefined &&
|
|
1782
|
+
ldrB !== undefined) {
|
|
1783
|
+
return { ledAnode, ledCathode, ldrA, ldrB };
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
return null;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Which rung decided an optocoupler's wiring, so a warning can name the devices that reached the
|
|
1790
|
+
* weaker ones without restating either test.
|
|
1791
|
+
*
|
|
1792
|
+
* The alternative -- a predicate in the warning module that mirrors this vocabulary -- is how a
|
|
1793
|
+
* silent switch mis-wiring was introduced in this compiler once already: the mirror drifted from
|
|
1794
|
+
* the resolver and the warning went quiet about the case it existed for.
|
|
1795
|
+
*/
|
|
1796
|
+
export function optocouplerBinding(device) {
|
|
1797
|
+
if (declaredOptocouplerNodes(device) !== null) {
|
|
1798
|
+
return "declared";
|
|
1799
|
+
}
|
|
1800
|
+
return namedOptocouplerNodes(device) !== null ? "named" : "declaration-order";
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* A logic gate's four connections, by declared role where the document states them.
|
|
1804
|
+
*
|
|
1805
|
+
* **The order used to be the whole binding.** `nodes[0..3]` was input, output, vdd, gnd, which is
|
|
1806
|
+
* right for a registry `sections` entry written against that order and is nothing at all for a
|
|
1807
|
+
* package that declares its own gates: a `devices:` list states which terminals a gate uses, not
|
|
1808
|
+
* which slot each fills, and its docstring says so -- "order carries no meaning; a law binds by
|
|
1809
|
+
* role". `@vessel-dsp/core@0.6.39` gives a gate `input` and `output` to bind by.
|
|
1810
|
+
*
|
|
1811
|
+
* The positional fallback stays for every document that predates the kind, and `vdd`/`gnd` stay
|
|
1812
|
+
* optional because the single-ended `vccs` branch below models a gate whose supplies the document
|
|
1813
|
+
* does not give.
|
|
1814
|
+
*/
|
|
1815
|
+
function inverterTerminals(device) {
|
|
1816
|
+
const declared = device.identity.declaredTerminalRoles;
|
|
1817
|
+
const only = (role) => {
|
|
1818
|
+
const found = declared.flatMap((value, index) => value === role ? [index] : []);
|
|
1819
|
+
return found.length === 1 ? device.nodes[found[0]] : undefined;
|
|
1820
|
+
};
|
|
1821
|
+
const input = only("input");
|
|
1822
|
+
const output = only("output");
|
|
1823
|
+
if (input !== undefined && output !== undefined) {
|
|
1824
|
+
return {
|
|
1825
|
+
input,
|
|
1826
|
+
output,
|
|
1827
|
+
vdd: only("supplyPositive"),
|
|
1828
|
+
gnd: only("supplyNegative"),
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
return {
|
|
1832
|
+
input: device.nodes[0],
|
|
1833
|
+
output: device.nodes[1],
|
|
1834
|
+
vdd: device.nodes.length >= 4 ? device.nodes[2] : undefined,
|
|
1835
|
+
gnd: device.nodes.length >= 4 ? device.nodes[3] : undefined,
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function optocouplerTerminals(device) {
|
|
1839
|
+
const resolved = declaredOptocouplerNodes(device) ?? namedOptocouplerNodes(device);
|
|
1840
|
+
if (resolved !== null) {
|
|
1841
|
+
return resolved;
|
|
1842
|
+
}
|
|
1843
|
+
const ledAnode = device.nodes[0];
|
|
1844
|
+
const ledCathode = device.nodes[1];
|
|
1845
|
+
const ldrA = device.nodes[2];
|
|
1846
|
+
const ldrB = device.nodes[3];
|
|
1847
|
+
if (ledAnode === undefined ||
|
|
1848
|
+
ledCathode === undefined ||
|
|
1849
|
+
ldrA === undefined ||
|
|
1850
|
+
ldrB === undefined) {
|
|
1851
|
+
throw new LoweringError(`optocoupler ${device.id} requires 4 terminals: anode, cathode, ldrA, ldrB`, device.id);
|
|
1852
|
+
}
|
|
1853
|
+
return { ledAnode, ledCathode, ldrA, ldrB };
|
|
1854
|
+
}
|
|
1855
|
+
//# sourceMappingURL=lower.js.map
|