@vessel-dsp/compiler 0.0.0-stage → 0.3.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 +118 -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 +348 -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 +3513 -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 +1742 -0
- package/dist/fixtures/circuits.d.ts.map +1 -0
- package/dist/fixtures/circuits.js +13896 -0
- package/dist/fixtures/circuits.js.map +1 -0
- package/dist/fixtures/expected.d.ts +261 -0
- package/dist/fixtures/expected.d.ts.map +1 -0
- package/dist/fixtures/expected.js +315 -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 +15 -0
- package/dist/part-catalog.d.ts.map +1 -0
- package/dist/part-catalog.js +6712 -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
|
@@ -0,0 +1,3513 @@
|
|
|
1
|
+
// Stage 3: attach each device's constitutive relation.
|
|
2
|
+
//
|
|
3
|
+
// Three outcomes, and the third is a product decision rather than a technical one:
|
|
4
|
+
//
|
|
5
|
+
// Law lumped, stamped into the matrix and solved
|
|
6
|
+
// MacroModel not solvable as lumped elements; a DSP algorithm with ports
|
|
7
|
+
// Unsupported no model available -> the whole pedal/amp does not compile
|
|
8
|
+
//
|
|
9
|
+
// Decision (Joseph, 2026-08-10): an unknown chip makes the pedal unsupported. Not an
|
|
10
|
+
// open circuit, not a pass-through. So there is no partially-modelled circuit and no
|
|
11
|
+
// fabricated port assignment anywhere in the design -- and consequently a reason
|
|
12
|
+
// string is a required output here, not a nicety.
|
|
13
|
+
//
|
|
14
|
+
// A pot's law is R_total split by f(x), where x is the 0..1 control position. This file
|
|
15
|
+
// decides *which* taper f is; it deliberately cannot evaluate one. Per decision 2, f is
|
|
16
|
+
// carried into the program and applied by whoever runs it, so the curve itself lives in
|
|
17
|
+
// `src/runtime/taper.ts` and no stage here can fold it into a coefficient by accident.
|
|
18
|
+
import { checkDeclaredProgram } from "./declared-program.js";
|
|
19
|
+
import { bjtPartRegisteredAsSections, identify, mayCarryRegistrySections, opampPartRegisteredAsOtherLaw, requiresIdentification, } from "./identify.js";
|
|
20
|
+
import { isMultiDeviceTransistorShell } from "./unreadable-terminal-role.js";
|
|
21
|
+
import { deriveBbdDelayFromNetlist, resolveClockModulationSource, deriveOpenOx2ClockLaw, derivePt2399DelayFromNetlist, deriveM50195DelayFromNetlist, OPEN_OX2_VDD_VOLTS, OPEN_OX2_VTH_VOLTS, OPEN_OX2_VF_VOLTS, OPEN_OX2_FLOOR_VOLTS, } from "./bbd-clock.js";
|
|
22
|
+
import { foldPartId, foldToken, pinoutMatches, registryEntryFor, registryLawFor, registryModelFor, registryOpampSwingDrop, terminalWithRole, withRecordedReads, } from "./registry.js";
|
|
23
|
+
/** Room-temperature thermal voltage; the diode law needs a reference somewhere. */
|
|
24
|
+
export const THERMAL_VOLTAGE = 0.025_852;
|
|
25
|
+
/**
|
|
26
|
+
* Closed and open switch resistances. Not arbitrary: the ratio sets the matrix
|
|
27
|
+
* condition number, so 1e-2 to 1e9 is chosen to be electrically indistinguishable
|
|
28
|
+
* from ideal at audio levels while staying far from the solver's precision limit.
|
|
29
|
+
*/
|
|
30
|
+
export const SWITCH_ON_OHMS = 1e-2;
|
|
31
|
+
/**
|
|
32
|
+
* The terminal a jack's switch contact makes against, and the terminal a supply calls
|
|
33
|
+
* its return. Closed sets, compared whole.
|
|
34
|
+
*
|
|
35
|
+
* There is deliberately **no set of contact role names**. The corpus names switch
|
|
36
|
+
* contacts eleven ways (`batterySwitch`, `switch`, `switched`, `contact_top`,
|
|
37
|
+
* `normalSwitch`, `switch_black`, ...) and they do not share a sense: a battery contact
|
|
38
|
+
* makes against the sleeve, a `switched_tip` makes against the tip, and a `normalSwitch`
|
|
39
|
+
* on a stereo output *breaks* when a plug goes in. A name table gets the sense wrong on
|
|
40
|
+
* two of those three, and closing a signal contact to the sleeve shorts it to ground.
|
|
41
|
+
* See `portEngage` for what is derived instead.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* Series impedance of a pedal supply, in ohms: the **default position of what belongs to a
|
|
45
|
+
* user-selectable power profile**, not a physical claim about one cell.
|
|
46
|
+
*
|
|
47
|
+
* Decided by Joseph, 2026-08-13: source impedance is an *input profile on the UI*, alongside the
|
|
48
|
+
* other things a player chooses about how a pedal is fed. A fresh alkaline, a sagging one and a
|
|
49
|
+
* regulated adapter are three profiles a user picks between, so this stage does not have to guess
|
|
50
|
+
* which the packet meant — a question that has no answer, since a schematic draws a 9 V battery
|
|
51
|
+
* symbol and internal resistance is a property of the cell rather than of the drawing.
|
|
52
|
+
*
|
|
53
|
+
* Until that control exists, this is the default, and it is the low end of the plausible range so
|
|
54
|
+
* the default errs toward the ideal behaviour it replaces: a fresh 9 V alkaline measures 1-2 ohms,
|
|
55
|
+
* a depleted one tens, an adapter well under one. Measured, one ohm moves a working pedal's rail by
|
|
56
|
+
* about 10 mV — `boss-bd-2-blues-driver` by `9.9 mV`, `boss-ge-7` by `19 mV` — and the corpus's
|
|
57
|
+
* ngspice agreement is unchanged at 31.
|
|
58
|
+
*
|
|
59
|
+
* **What zero cost.** Zero is the one value known to be wrong, since no real supply has it, and it
|
|
60
|
+
* let a supply deliver unbounded current: `ibanez-ts808` drew **193 A** through the short
|
|
61
|
+
* `diode-forward-across-supply` warns about and still rendered a plausible gain of 0.506. One ohm
|
|
62
|
+
* collapses that rail instead, which is also what let the diode's `limitJunction` land after two
|
|
63
|
+
* reverts.
|
|
64
|
+
*
|
|
65
|
+
* **Not a battery model.** It does not deplete and it does not tell an adapter from a cell. Both
|
|
66
|
+
* are the profile's job once it exists; this is the mechanism and one defensible default.
|
|
67
|
+
*/
|
|
68
|
+
export const SUPPLY_SOURCE_OHMS = 1;
|
|
69
|
+
/**
|
|
70
|
+
* What a bipolar transistor is when the packet does not say, and why the default is silicon.
|
|
71
|
+
*
|
|
72
|
+
* Only 10% of the corpus's 385 transistors declare `IS`, so most still land here, and a
|
|
73
|
+
* small-signal silicon junction is the right assumption for an undated schematic. It is
|
|
74
|
+
* emphatically **not** right for the ones that do declare it: germanium sits five orders of
|
|
75
|
+
* magnitude away at around `1e-9`, which is 0.29 V of base-emitter bias, and those packets
|
|
76
|
+
* now get what they declare.
|
|
77
|
+
*
|
|
78
|
+
* Nothing infers germanium from a part number. `PartNumber` is present on 97% of them and
|
|
79
|
+
* reading `OC41` as germanium would be prose matching; `Type` and `IS` are the evidence.
|
|
80
|
+
*/
|
|
81
|
+
/**
|
|
82
|
+
* Open-loop gain for an op-amp that does not state one, dimensionless.
|
|
83
|
+
*
|
|
84
|
+
* A generic small-signal figure. 21 of the corpus's op-amps declare exactly this and 31 declare
|
|
85
|
+
* otherwise, so it is a fallback rather than a description of anything.
|
|
86
|
+
*/
|
|
87
|
+
const DEFAULT_OPEN_LOOP_GAIN = 1e5;
|
|
88
|
+
/**
|
|
89
|
+
* The factor an `ac-source` law multiplies a declared magnitude by, converting the project's
|
|
90
|
+
* RMS convention to the peak amplitude the sine evaluation needs. See the `ac-source` branch
|
|
91
|
+
* of `voltage-source`'s law below for the full reasoning and the escape hatch.
|
|
92
|
+
*/
|
|
93
|
+
const AC_SOURCE_RMS_TO_PEAK = Math.SQRT2;
|
|
94
|
+
// Exported so `report-part-store-drift.ts` can check this one shared fallback against a
|
|
95
|
+
// representative registry part instead of quoting a second copy of the numbers that could drift
|
|
96
|
+
// from this one silently -- the exact failure mode that script exists to catch.
|
|
97
|
+
export const SILICON_SATURATION_CURRENT = 1e-14;
|
|
98
|
+
export const SILICON_FORWARD_BETA = 100;
|
|
99
|
+
/**
|
|
100
|
+
* The JFET law's own parameters when the source types neither `Vt0` nor `Beta`. Not a part: no
|
|
101
|
+
* datasheet in the corpus prints these, and a 2N5952 or 2SK30A whose document leaves them untyped
|
|
102
|
+
* runs on them regardless of its grade. Exported so `compile.ts` can name every device that does
|
|
103
|
+
* (`fet-law-default-parameters`) against the values actually stamped, not a second copy.
|
|
104
|
+
*/
|
|
105
|
+
export const JFET_DEFAULT_THRESHOLD_VOLTS = -2;
|
|
106
|
+
export const JFET_DEFAULT_TRANSCONDUCTANCE = 1e-3;
|
|
107
|
+
/**
|
|
108
|
+
* A winding's DC resistance, in the three states the source can be in.
|
|
109
|
+
*
|
|
110
|
+
* **Absent is not zero.** Every real winding has copper resistance, so zero is physically
|
|
111
|
+
* impossible rather than merely unusual -- and a lossless inductor in a feedback loop has unbounded
|
|
112
|
+
* Q, which is how a marginally stable real amplifier becomes an unstable simulated one. Defaulting
|
|
113
|
+
* absence to zero would preserve every current number and leave all 67 corpus windings quietly
|
|
114
|
+
* wrong, which is exactly how this arrived: until 2026-09-09 the law had no parameter at all, so
|
|
115
|
+
* `.vdsp` had nowhere to put a value and 29 packets were lossless by construction rather than by
|
|
116
|
+
* omission.
|
|
117
|
+
*
|
|
118
|
+
* So there is **no default**. A packet states a number, or states `unknown` when the drawing does
|
|
119
|
+
* not print it -- Fender sheets give turns ratios and impedances, never copper resistance -- and
|
|
120
|
+
* `unknown` is a first-class machine-readable claim rather than an omission indistinguishable from
|
|
121
|
+
* an oversight. Silence produces neither, and a caller can tell the three apart.
|
|
122
|
+
*/
|
|
123
|
+
function windingResistance(device) {
|
|
124
|
+
const declared = device.parameters.windingResistanceOhms;
|
|
125
|
+
if (typeof declared === "number" && Number.isFinite(declared) && declared > 0) {
|
|
126
|
+
return { seriesResistanceOhms: declared };
|
|
127
|
+
}
|
|
128
|
+
if (device.parameters.windingResistanceUnknown === 1) {
|
|
129
|
+
return { seriesResistanceOhms: "unknown" };
|
|
130
|
+
}
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
const FERRITE_BEAD_DEFAULT_HENRIES = 10e-6;
|
|
134
|
+
/**
|
|
135
|
+
* The roles a jack uses for the terminal current returns through.
|
|
136
|
+
*
|
|
137
|
+
* **This replaced a name vocabulary that carried `cathode`**, because the `.schx` conversion
|
|
138
|
+
* named every two-terminal component after a diode: 265 corpus jacks called their sleeve
|
|
139
|
+
* `cathode` and their tip `anode`. 249 now declare `sleeve`/`tip`, decided by which terminal
|
|
140
|
+
* sits on ground rather than by what it is called; the 16 that were ambiguous -- both live,
|
|
141
|
+
* both grounded, or the pair reversed -- declare nothing and reach no port-engage law.
|
|
142
|
+
*
|
|
143
|
+
* The name path is gone rather than kept as a fallback, and that is measured: emptying it
|
|
144
|
+
* leaves all 21 port-engage laws standing and every one of the 142 corpus programs
|
|
145
|
+
* byte-identical.
|
|
146
|
+
*/
|
|
147
|
+
const portReturnRoles = new Set([
|
|
148
|
+
"sleeve",
|
|
149
|
+
"ground",
|
|
150
|
+
"negative",
|
|
151
|
+
]);
|
|
152
|
+
/** The index of a jack's declared return terminal, or -1 when it declares none. */
|
|
153
|
+
function portReturnIndex(device) {
|
|
154
|
+
return device.identity.declaredTerminalRoles.findIndex((role) => role !== null && portReturnRoles.has(role));
|
|
155
|
+
}
|
|
156
|
+
/** How a supply names the terminal current comes back to. */
|
|
157
|
+
export const SWITCH_OFF_OHMS = 1e9;
|
|
158
|
+
function buildNodeToVoltageMap(netlist, regulated) {
|
|
159
|
+
const map = new Map();
|
|
160
|
+
map.set(0, 0); // Ground is always 0V
|
|
161
|
+
// A registered regulator's output node states a voltage the document never writes down.
|
|
162
|
+
// Set first so a document that *does* declare the node keeps the last word. See
|
|
163
|
+
// `registrySuppliedVoltages`.
|
|
164
|
+
for (const supply of regulated) {
|
|
165
|
+
map.set(supply.node, supply.volts);
|
|
166
|
+
}
|
|
167
|
+
for (const device of netlist.devices) {
|
|
168
|
+
const volts = device.parameters.volts;
|
|
169
|
+
if (volts === undefined || !Number.isFinite(volts)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (device.kind === "rail") {
|
|
173
|
+
if (device.parameters.frequency !== undefined) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const node = device.nodes[0];
|
|
177
|
+
if (node !== undefined) {
|
|
178
|
+
map.set(node, volts);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (device.kind === "voltage-source") {
|
|
182
|
+
if (device.parameters.frequency !== undefined) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// The driven end by declared role, the same field and the same value `lower.ts`'s
|
|
186
|
+
// `supplyTerminals` reads -- stage 3 and stage 5 must agree about which end of a
|
|
187
|
+
// supply is positive or they assert the rail at different nodes.
|
|
188
|
+
device.identity.declaredTerminalRoles.forEach((role, index) => {
|
|
189
|
+
if (role === "positive") {
|
|
190
|
+
const node = device.nodes[index];
|
|
191
|
+
if (node !== undefined) {
|
|
192
|
+
map.set(node, volts);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
else if (device.kind === "ic") {
|
|
198
|
+
const outNode = findNodeWithRole(device, "out");
|
|
199
|
+
if (outNode !== undefined) {
|
|
200
|
+
map.set(outNode, volts);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return map;
|
|
205
|
+
}
|
|
206
|
+
function findNodeWithRole(device, role) {
|
|
207
|
+
const index = device.identity.terminalRoles.indexOf(role);
|
|
208
|
+
return index !== -1 ? device.nodes[index] : undefined;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Role tokens that can only ever name a supply pin, folded as `terminalRoleToken` leaves them.
|
|
212
|
+
*
|
|
213
|
+
* No op-amp calls a signal input `vcc` or `negativesupply`, so these are read unconditionally.
|
|
214
|
+
* `vccpin8`/`veepin4` are the numbered spellings: pins 8 and 4 of a single op-amp in an 8-pin
|
|
215
|
+
* package are its rails, and `terminalRoleToken` strips a leading `pinN` but not a trailing one.
|
|
216
|
+
*/
|
|
217
|
+
const unambiguousSupplyRoles = new Map([
|
|
218
|
+
["vcc", "high"],
|
|
219
|
+
["vccpin8", "high"],
|
|
220
|
+
["positivesupply", "high"],
|
|
221
|
+
["vdd", "high"],
|
|
222
|
+
["vee", "low"],
|
|
223
|
+
["veepin4", "low"],
|
|
224
|
+
["negativesupply", "low"],
|
|
225
|
+
["vss", "low"],
|
|
226
|
+
]);
|
|
227
|
+
/**
|
|
228
|
+
* Role tokens that name a supply pin **on a part that separately names its inputs**, and could
|
|
229
|
+
* otherwise be an input.
|
|
230
|
+
*
|
|
231
|
+
* This distinction is the whole reason this is two maps rather than one. A census of all 294
|
|
232
|
+
* op-amp-shaped components in the corpus found `vplus`/`vminus` used as **rails** in every one
|
|
233
|
+
* of the 60 devices that declare them -- always beside a named input pair (`inverting` /
|
|
234
|
+
* `noninverting`, or `positive` / `negative`), never instead of one. So they are supplies here.
|
|
235
|
+
*
|
|
236
|
+
* But `vplus`/`vminus` is also the natural spelling for a bare differential pair, and
|
|
237
|
+
* `lower.ts`'s `opampTerminals` falls back to *declaration order* when it cannot resolve a named
|
|
238
|
+
* input pair -- so on a three-terminal `vplus, vminus, output` op-amp it wires those two as the
|
|
239
|
+
* inputs. Reading them as rails as well would clamp that op-amp to its own input voltages. The
|
|
240
|
+
* guard below is therefore not a corpus statistic, it is the invariant: **a terminal cannot be
|
|
241
|
+
* both a signal input and a rail**, so these spellings are read as rails only when the part
|
|
242
|
+
* states its inputs some other way.
|
|
243
|
+
*/
|
|
244
|
+
const supplyRolesNeedingNamedInputs = new Map([
|
|
245
|
+
["vplus", "high"],
|
|
246
|
+
["vpluspin8", "high"],
|
|
247
|
+
["v+", "high"],
|
|
248
|
+
["vminus", "low"],
|
|
249
|
+
["vminuspin4", "low"],
|
|
250
|
+
// Folded from `v-` by `terminalRoleToken`'s separator strip. `boss-ce-2` names its
|
|
251
|
+
// supply pair `V+`/`V-`, which `netlist.ts`'s own supply census also records.
|
|
252
|
+
["v", "low"],
|
|
253
|
+
]);
|
|
254
|
+
/** Input-naming roles that are never a supply, so finding one proves the inputs are stated. */
|
|
255
|
+
const namedInputRoles = new Set([
|
|
256
|
+
"positive",
|
|
257
|
+
"negative",
|
|
258
|
+
"noninverting",
|
|
259
|
+
"inverting",
|
|
260
|
+
"noninvertinginput",
|
|
261
|
+
"invertinginput",
|
|
262
|
+
"inputplus",
|
|
263
|
+
"inputminus",
|
|
264
|
+
"vin+",
|
|
265
|
+
"vin",
|
|
266
|
+
"in+",
|
|
267
|
+
"in",
|
|
268
|
+
"noninvertinga",
|
|
269
|
+
"invertinga",
|
|
270
|
+
"noninvertingb",
|
|
271
|
+
"invertingb",
|
|
272
|
+
]);
|
|
273
|
+
/**
|
|
274
|
+
* The rails this op-amp is powered by: its own supply terminals when it declares any, else the
|
|
275
|
+
* circuit's supply set.
|
|
276
|
+
*
|
|
277
|
+
* Before this, only the exact spellings `vcc` and `vee` were read; every other spelling fell
|
|
278
|
+
* through to the circuit-wide supply extremes -- the *widest* pair in the document -- so an
|
|
279
|
+
* op-amp on a regulated rail inside a pedal with a higher raw inlet was given a clipping ceiling
|
|
280
|
+
* it does not have. A fallback is the right behaviour for an op-amp that states no supply at
|
|
281
|
+
* all; it is a wrong answer that still renders for one that states it under another name.
|
|
282
|
+
*
|
|
283
|
+
* **One measured basis, to keep the numbers here comparable.** Censusing every lowered
|
|
284
|
+
* `ideal-opamp` stamp before and after this change: of **251** stamps, **4** get different
|
|
285
|
+
* rails -- both op-amps in `boss-bd-2-blues-driver` and its Keeley variant, `railHigh` 9 -> 8,
|
|
286
|
+
* which is correct because that pedal declares three supply ports (9 V, 8 V, 4 V) and both
|
|
287
|
+
* op-amps' `vPlus` is on the 8 V one. Many more op-amps *declare* a non-`vcc` spelling, but
|
|
288
|
+
* on a node whose voltage already equals a circuit extreme, so the old lookup returned the
|
|
289
|
+
* right answer for the wrong reason. Counting declarations rather than changed rails
|
|
290
|
+
* overstates this fix by an order of magnitude; the 4 is the number that means anything.
|
|
291
|
+
*/
|
|
292
|
+
function opampRails(device, rails, nodeToVoltage) {
|
|
293
|
+
const roles = device.identity.terminalRoles;
|
|
294
|
+
const statesItsInputs = roles.some((role) => role !== null && namedInputRoles.has(role));
|
|
295
|
+
let high = rails.high;
|
|
296
|
+
let low = rails.low;
|
|
297
|
+
roles.forEach((role, index) => {
|
|
298
|
+
if (role === null) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const side = unambiguousSupplyRoles.get(role) ??
|
|
302
|
+
(statesItsInputs ? supplyRolesNeedingNamedInputs.get(role) : undefined);
|
|
303
|
+
if (side === undefined) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const node = device.nodes[index];
|
|
307
|
+
const volts = node === undefined ? undefined : nodeToVoltage.get(node);
|
|
308
|
+
if (volts === undefined) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (side === "high") {
|
|
312
|
+
high = volts;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
low = volts;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
// **A rail pair must be ordered, or it is not a pair.** `railHigh <= railLow` is not a
|
|
319
|
+
// narrow clamp, it is a contradiction: the op-amp's linear region has negative width and
|
|
320
|
+
// its output is pinned wherever the limiter happens to land. Reading a device's own supply
|
|
321
|
+
// pins can produce that from a document the old `vcc`/`vee`-only lookup never asked, so the
|
|
322
|
+
// guard belongs here rather than in the caller.
|
|
323
|
+
//
|
|
324
|
+
// Measured on `mxr-m117r-flanger`, which is why this is not defensive programming: its `U4`
|
|
325
|
+
// declares `pin8_vplus` and `pin4_vminus`, and **both resolve to the same node** -- ground,
|
|
326
|
+
// carrying a declared +15 V, because the packet puts its `V15` rail component on the ground
|
|
327
|
+
// node. That yielded `high = low = 15` on eight op-amp stamps. The document's own supply
|
|
328
|
+
// pins are not usable evidence there, so the circuit-wide set is the better answer, and
|
|
329
|
+
// falling back to it leaves those eight exactly where they were before this change.
|
|
330
|
+
if (high !== null && low !== null && high <= low) {
|
|
331
|
+
return rails;
|
|
332
|
+
}
|
|
333
|
+
return { high, low };
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The rails an op-amp's output can reach: each supply rail pulled in by the part's stated drop.
|
|
337
|
+
* An unstated drop, an unknown rail, or a drop that would close the pair leaves the rails as they
|
|
338
|
+
* are, so a part with no entry renders bit-for-bit as before.
|
|
339
|
+
*/
|
|
340
|
+
function railsAfterSwingDrop(rails, dropVolts) {
|
|
341
|
+
if (dropVolts === null || dropVolts <= 0 || rails.high === null || rails.low === null) {
|
|
342
|
+
return rails;
|
|
343
|
+
}
|
|
344
|
+
const high = rails.high - dropVolts;
|
|
345
|
+
const low = rails.low + dropVolts;
|
|
346
|
+
return high > low ? { high, low } : rails;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The one high-side and one low-side supply pin a device declares, read by the same role tokens
|
|
350
|
+
* `opampRails` uses, or `null` unless each side is declared exactly once.
|
|
351
|
+
*
|
|
352
|
+
* `gnd` counts as a low side here and not in `opampRails`: a package ground pin is never a signal
|
|
353
|
+
* input, and the MN3101 names its pair `vdd`/`gnd`. Which side is which does not matter to a
|
|
354
|
+
* conductance, which is all this is used for.
|
|
355
|
+
*/
|
|
356
|
+
function supplyPinNodes(device) {
|
|
357
|
+
const roles = device.identity.terminalRoles;
|
|
358
|
+
const statesItsInputs = roles.some((role) => role !== null && namedInputRoles.has(role));
|
|
359
|
+
const high = [];
|
|
360
|
+
const low = [];
|
|
361
|
+
roles.forEach((role, index) => {
|
|
362
|
+
if (role === null) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const side = role === "gnd"
|
|
366
|
+
? "low"
|
|
367
|
+
: (unambiguousSupplyRoles.get(role) ??
|
|
368
|
+
(statesItsInputs ? supplyRolesNeedingNamedInputs.get(role) : undefined));
|
|
369
|
+
const node = device.nodes[index];
|
|
370
|
+
if (side === undefined || node === undefined) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
(side === "high" ? high : low).push(node);
|
|
374
|
+
});
|
|
375
|
+
if (high.length !== 1 || low.length !== 1 || high[0] === low[0]) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
return { high: high[0], low: low[0] };
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* **An IC's own supply draw, as a conductance across its supply pins**, or `null` when its part
|
|
382
|
+
* entry records no `quiescentSupplyAmps` or the pins or their voltages cannot be read.
|
|
383
|
+
*
|
|
384
|
+
* Sized at this pedal's rail span, so the device draws the datasheet current at the rail it is
|
|
385
|
+
* actually fitted to. A bipolar op-amp's supply current is close to flat with voltage (the
|
|
386
|
+
* uPC4558's curve moves 2.6 -> 2.7 mA from +/-4.5 to +/-20 V), so fixing it at the operating span
|
|
387
|
+
* is the right first-order model, and it needs no new stamp in either console.
|
|
388
|
+
*
|
|
389
|
+
* `expanded` is whether the registry split this device into its sections here, in which case it is
|
|
390
|
+
* the whole package. A device the document declared as one section of a multi-section part --
|
|
391
|
+
* `boss-ce-2`'s `IC1A` and `IC1B` are the two halves of one uPC4558C -- draws that section's share,
|
|
392
|
+
* so the package is counted once however the document split it.
|
|
393
|
+
*/
|
|
394
|
+
function quiescentSupplyDraw(device, registry, expanded, rails, nodeToVoltage) {
|
|
395
|
+
const partNumber = device.identity.partNumber;
|
|
396
|
+
if (partNumber === null) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
const entry = registryEntryFor(registry, { partId: partNumber, evidence: "exact-part" });
|
|
400
|
+
const packageAmps = entry?.quiescentSupplyAmps;
|
|
401
|
+
if (entry === null || packageAmps === undefined) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
const pins = supplyPinNodes(device);
|
|
405
|
+
if (pins === null) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
const voltageAt = (node, fallback) => node === 0 ? 0 : (nodeToVoltage.get(node) ?? fallback);
|
|
409
|
+
const high = voltageAt(pins.high, rails.high);
|
|
410
|
+
const low = voltageAt(pins.low, rails.low);
|
|
411
|
+
if (high === null || low === null) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
const span = Math.abs(high - low);
|
|
415
|
+
if (!(span > 0)) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
const sections = entry.model.kind === "sections" ? entry.model.sections.length : 1;
|
|
419
|
+
const amps = expanded ? packageAmps : packageAmps / sections;
|
|
420
|
+
const id = `${device.id}#supply`;
|
|
421
|
+
return [
|
|
422
|
+
{
|
|
423
|
+
id,
|
|
424
|
+
kind: "resistor",
|
|
425
|
+
nodes: [pins.high, pins.low],
|
|
426
|
+
parameters: { ohms: span / amps },
|
|
427
|
+
control: null,
|
|
428
|
+
identity: {
|
|
429
|
+
partNumber: null,
|
|
430
|
+
declaredType: null,
|
|
431
|
+
terminalRoles: ["end", "end"],
|
|
432
|
+
declaredTerminalRoles: [null, null],
|
|
433
|
+
declaredWindings: null,
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
{ outcome: "law", device: id, law: { kind: "conductance", siemens: amps / span } },
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Nodes that reach a declared voltage through one two-terminal passive
|
|
441
|
+
* (diode, capacitor, resistor, inductor). Covers the LT1054 → D17 → VA-rail
|
|
442
|
+
* path where the charge pump's output is a diode away from the declared rail.
|
|
443
|
+
*/
|
|
444
|
+
function buildOneHopVoltageMap(netlist, nodeToVoltage) {
|
|
445
|
+
const map = new Map();
|
|
446
|
+
for (const device of netlist.devices) {
|
|
447
|
+
if (device.nodes.length !== 2 ||
|
|
448
|
+
(device.kind !== "diode" &&
|
|
449
|
+
device.kind !== "capacitor" &&
|
|
450
|
+
device.kind !== "resistor" &&
|
|
451
|
+
device.kind !== "inductor")) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const [a, b] = device.nodes;
|
|
455
|
+
const va = nodeToVoltage.get(a);
|
|
456
|
+
const vb = nodeToVoltage.get(b);
|
|
457
|
+
if (va !== undefined && b !== 0 && vb === undefined) {
|
|
458
|
+
map.set(b, va);
|
|
459
|
+
}
|
|
460
|
+
else if (vb !== undefined && a !== 0 && va === undefined) {
|
|
461
|
+
map.set(a, vb);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return map;
|
|
465
|
+
}
|
|
466
|
+
export function attachDeviceLaws(netlist, registry) {
|
|
467
|
+
const regulated = registrySuppliedVoltages(netlist, registry);
|
|
468
|
+
const rails = supplyRails(netlist, regulated);
|
|
469
|
+
const supply = supplyNodes(netlist);
|
|
470
|
+
const nodeToVoltage = buildNodeToVoltageMap(netlist, regulated);
|
|
471
|
+
const oneHopVoltage = buildOneHopVoltageMap(netlist, nodeToVoltage);
|
|
472
|
+
const mainsFrequencyHz = mainsFrequency(netlist);
|
|
473
|
+
// Stage 1 (`netlist.ts`) already resolved each control's real taper -- from the panel
|
|
474
|
+
// control's own declaration, or failing that, from the first device bound to it -- onto
|
|
475
|
+
// `Netlist.controls`. This stage's job is to carry that already-decided value onto the
|
|
476
|
+
// law by the same control id, never to decide it again or default it.
|
|
477
|
+
const taperByControl = new Map(netlist.controls.map((control) => [control.id, control.taper]));
|
|
478
|
+
// A multi-section part becomes one device per section, here and nowhere else.
|
|
479
|
+
//
|
|
480
|
+
// This is the only stage that knows the registry, so it is the only stage that can know a part
|
|
481
|
+
// is multi-section. Expanding into ordinary devices means partitioning, lowering and the runtime
|
|
482
|
+
// see nothing new -- the alternative, a fourth `DeviceResolution` outcome, would have to be
|
|
483
|
+
// understood by every consumer of one.
|
|
484
|
+
const devicesTouchingNode = deviceCountByNode(netlist);
|
|
485
|
+
// Nodes inside a registered package, numbered past every node the document uses.
|
|
486
|
+
const internalNodes = new Map();
|
|
487
|
+
const firstInternal = Math.max(0, ...netlist.nodes) + 1;
|
|
488
|
+
const internalNode = (deviceId, index) => {
|
|
489
|
+
const key = `${deviceId}\n${index}`;
|
|
490
|
+
const existing = internalNodes.get(key);
|
|
491
|
+
if (existing !== undefined)
|
|
492
|
+
return existing;
|
|
493
|
+
const node = (firstInternal + internalNodes.size);
|
|
494
|
+
internalNodes.set(key, node);
|
|
495
|
+
return node;
|
|
496
|
+
};
|
|
497
|
+
const devices = [];
|
|
498
|
+
const resolutions = [];
|
|
499
|
+
for (const device of netlist.devices) {
|
|
500
|
+
const expanded = sectionDevices(device, registry, rails, supply, nodeToVoltage, internalNode, netlist);
|
|
501
|
+
const draw = quiescentSupplyDraw(device, registry, expanded !== null, rails, nodeToVoltage);
|
|
502
|
+
if (draw !== null) {
|
|
503
|
+
devices.push(draw[0]);
|
|
504
|
+
resolutions.push(draw[1]);
|
|
505
|
+
}
|
|
506
|
+
if (expanded === null) {
|
|
507
|
+
devices.push(device);
|
|
508
|
+
resolutions.push(resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, oneHopVoltage, netlist));
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
for (const [section, resolution] of expanded) {
|
|
512
|
+
devices.push(section);
|
|
513
|
+
resolutions.push(resolution);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
netlist: {
|
|
518
|
+
...netlist,
|
|
519
|
+
devices,
|
|
520
|
+
nodes: internalNodes.size === 0 ? netlist.nodes : [...netlist.nodes, ...internalNodes.values()],
|
|
521
|
+
},
|
|
522
|
+
resolutions,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* A multi-section part as one device per section, or `null` when the part is not multi-section.
|
|
527
|
+
*
|
|
528
|
+
* Each section device carries only the terminals that section uses, in the order the registry gave
|
|
529
|
+
* them, and its `identity.terminalRoles` are the canonical role names for its law's arity. That is
|
|
530
|
+
* what lets lowering treat it as an ordinary device: a section of a dual op-amp is indistinguishable
|
|
531
|
+
* from a packet that declared that half as its own `opamp` component, which is how the packets that
|
|
532
|
+
* already compile transcribe the same chip.
|
|
533
|
+
*
|
|
534
|
+
* A section naming a terminal the device does not have is a registry error and refuses the pedal,
|
|
535
|
+
* rather than silently dropping a section and rendering half a circuit.
|
|
536
|
+
*/
|
|
537
|
+
/**
|
|
538
|
+
* A single-section `voltage-source` model on a regulator or converter that declares which terminal
|
|
539
|
+
* is its output and which is its reference, resolved from those declarations.
|
|
540
|
+
*
|
|
541
|
+
* `null` whenever the document does not settle it: a different law, more than one section, a kind
|
|
542
|
+
* whose format vocabulary has no supply electrodes, or an output/reference role declared zero or
|
|
543
|
+
* several times. Every one of those falls through to the entry's pin naming exactly as before.
|
|
544
|
+
*/
|
|
545
|
+
export function declaredRegulatorTerminals(device, model) {
|
|
546
|
+
// **Gated on the declaration, not on `device.kind`.** A document's `kind: regulator` folds to
|
|
547
|
+
// device kind `ic` by the time laws are attached -- `boss-rv-3` and `electro-harmonix-q-tron`
|
|
548
|
+
// both declare `regulator` and both arrive here as `ic` -- so a kind test would never fire.
|
|
549
|
+
// The declaration is its own gate: core admits `positive`/`negative`/`ground` only on the
|
|
550
|
+
// kinds whose vocabulary carries them (`regulator`, `power-converter`, `voltage-source`,
|
|
551
|
+
// `rail`, `battery`), and an `ic` may declare nothing but `pin`. A device that reaches here
|
|
552
|
+
// carrying a supply electrode was therefore declared as a kind entitled to state one.
|
|
553
|
+
if (model.sections.length !== 1) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
const section = model.sections[0];
|
|
557
|
+
if (section.law.kind !== "voltage-source" || section.terminals.length !== 2) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
const declared = device.identity.declaredTerminalRoles;
|
|
561
|
+
const only = (role) => {
|
|
562
|
+
const found = declared.flatMap((value, index) => value === role ? [index] : []);
|
|
563
|
+
return found.length === 1 ? found[0] : null;
|
|
564
|
+
};
|
|
565
|
+
const ground = only("ground");
|
|
566
|
+
const output = only("positive") ?? only("negative");
|
|
567
|
+
if (ground === null || output === null) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
const outputNode = device.nodes[output];
|
|
571
|
+
const groundNode = device.nodes[ground];
|
|
572
|
+
if (outputNode === undefined || groundNode === undefined) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
// A converter declaring its output `negative` states a rail below its reference, so the
|
|
576
|
+
// source's sense is reversed rather than its magnitude negated -- the law carries a
|
|
577
|
+
// magnitude.
|
|
578
|
+
const inverting = only("positive") === null;
|
|
579
|
+
const order = inverting
|
|
580
|
+
? [ground, output]
|
|
581
|
+
: [output, ground];
|
|
582
|
+
// **Built exactly like any other section**, because it is one: a `#0` id and canonical roles
|
|
583
|
+
// for the law, not the package's own role array. The parent's array does not describe a
|
|
584
|
+
// section and is a different length, and leaving it in place misaligned every index-based
|
|
585
|
+
// reader -- `supplyTerminals` resolves an unnamed end as `1 - driveIndex`, which is only the
|
|
586
|
+
// other terminal when there are two.
|
|
587
|
+
const sectionId = `${device.id}#0`;
|
|
588
|
+
const nodes = order.map((index) => device.nodes[index]);
|
|
589
|
+
return [
|
|
590
|
+
[
|
|
591
|
+
{
|
|
592
|
+
...device,
|
|
593
|
+
id: sectionId,
|
|
594
|
+
nodes,
|
|
595
|
+
identity: {
|
|
596
|
+
...device.identity,
|
|
597
|
+
terminalRoles: canonicalRoles(section.law.kind, nodes.length),
|
|
598
|
+
declaredTerminalRoles: canonicalRoles(section.law.kind, nodes.length),
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
{ outcome: "law", device: sectionId, law: section.law },
|
|
602
|
+
],
|
|
603
|
+
];
|
|
604
|
+
}
|
|
605
|
+
function sectionDevices(device, registry, rails, supply, nodeToVoltage, internalNode = () => {
|
|
606
|
+
throw new Error("no internal-node allocator");
|
|
607
|
+
}, netlist) {
|
|
608
|
+
// **`mayCarryRegistrySections`, not `requiresIdentification`.** An OTA reaches here so a
|
|
609
|
+
// registered multi-section part (the BA662A's OTA plus its Darlington buffer) can be expanded;
|
|
610
|
+
// when the registry has no matching entry this returns null and the caller falls through to
|
|
611
|
+
// the device-class law, which is what every unregistered OTA keeps. An op-amp whose registered
|
|
612
|
+
// *part* is not an op-amp (`opampPartRegisteredAsOtherLaw` -- the LM339 drawn as an op-amp)
|
|
613
|
+
// reaches here the same way, and the class override is decided entirely in `identify`.
|
|
614
|
+
if (!mayCarryRegistrySections(device) &&
|
|
615
|
+
!opampPartRegisteredAsOtherLaw(device, registry) &&
|
|
616
|
+
!bjtPartRegisteredAsSections(device, registry)) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
if (isNonExecutableIcSupportShell(device)) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
const identity = identify(device, registry);
|
|
623
|
+
if (identity === null) {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
const entry = registryEntryFor(registry, identity, device.nodes.length);
|
|
627
|
+
if (entry === null) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
const model = entry.model;
|
|
631
|
+
if (model.kind !== "sections" && model.kind !== "sections-core") {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
// A `sections` entry's `terminals` index into the entry's canonical pin order, not the
|
|
635
|
+
// declaration order the document happened to use. When the entry names its pins by role and
|
|
636
|
+
// that naming is a clean bijection onto the device's declared terminals, resolve each
|
|
637
|
+
// canonical position to the device's node by role, so a part declared in a different order
|
|
638
|
+
// still lands on the right nodes: q-tron's 78L18 declares `in, out, ground`, the entry's
|
|
639
|
+
// groups are `in, gnd, out`, and an all-null pinout used to skip this check entirely,
|
|
640
|
+
// silently swapping the regulated rail onto ground.
|
|
641
|
+
//
|
|
642
|
+
// The bijection is the guard. A part whose declared roles do not map one-to-one onto the
|
|
643
|
+
// entry's groups -- the MN3101 carries extra clock terminals the entry does not model, and a
|
|
644
|
+
// 7809 may spell its pins `inputpin1` -- falls back to the declaration order its `pinout`
|
|
645
|
+
// was written against, exactly as before: that order is enforced by `pinoutMatches`, and a
|
|
646
|
+
// role-named entry with an all-null pinout cannot be wired by role OR by position, so it is
|
|
647
|
+
// refused rather than mis-wired. See `PartPinout`.
|
|
648
|
+
// **A regulator's own declared electrodes outrank the entry's pin numbering.**
|
|
649
|
+
//
|
|
650
|
+
// The 78Lxx entry's role groups list `pin1`/`pin2`/`pin3` among their aliases, which asserts
|
|
651
|
+
// a package numbering the corpus does not agree on: three packets name a 78Lxx
|
|
652
|
+
// `in, gnd, out` and `electro-harmonix-q-tron` names it `in, out, ground`. For
|
|
653
|
+
// `moogerfooger-mf-102`'s `U13` -- three terminals literally named `pin1`, `pin2`, `pin3` --
|
|
654
|
+
// the entry's `pin2 -> gnd`, `pin3 -> out` is backwards against the packet's own source
|
|
655
|
+
// trace, which records pin2 on `plus5` and pin3 on `gnd`. The result was the regulator's
|
|
656
|
+
// reference stamped onto the pedal's rail: node 3 carried 24 devices at **-4.9989 V** and 18
|
|
657
|
+
// of 26 active devices sat dark, reverse-biased by a whole supply, with no diagnostic.
|
|
658
|
+
//
|
|
659
|
+
// Only the two terminals the law actually spans need to be found, and for this kind both
|
|
660
|
+
// carry an unambiguous role: the regulated output is `positive` (or `negative` on an
|
|
661
|
+
// inverting converter) and the reference is `ground`. A regulator's *input* has no distinct
|
|
662
|
+
// role in the format, which is why a full bijection cannot be built from declarations here
|
|
663
|
+
// and this binds the law's own two slots rather than every pin.
|
|
664
|
+
const declaredSupplyBinding = declaredRegulatorTerminals(device, model);
|
|
665
|
+
if (declaredSupplyBinding !== null) {
|
|
666
|
+
return declaredSupplyBinding;
|
|
667
|
+
}
|
|
668
|
+
const roles = device.identity.terminalRoles;
|
|
669
|
+
const groups = entry.terminalRoleGroups;
|
|
670
|
+
const positions = groups.map((group) => terminalWithRole(roles, group));
|
|
671
|
+
const cleanRoleResolution = groups.length > 0 &&
|
|
672
|
+
groups.length === roles.length &&
|
|
673
|
+
positions.every((position) => position !== null) &&
|
|
674
|
+
new Set(positions).size === positions.length;
|
|
675
|
+
// **For an OTA the registry only wins when it can bind by role.** Everything else here may
|
|
676
|
+
// fall back to declaration order, which is right for a part whose entry was written against
|
|
677
|
+
// that order -- but an OTA already has a correct role-bound device-class law, so a positional
|
|
678
|
+
// guess from a registry entry written for different role spellings is strictly worse than
|
|
679
|
+
// keeping it. Measured: `keeley-compressor-plus` names its terminals `iabc` and `out` while
|
|
680
|
+
// the LM13600 entry names its groups `i_biasa` and `vouta`, so the bijection fails, and the
|
|
681
|
+
// positional fallback swapped that device's `bias` and `output`.
|
|
682
|
+
if (device.kind === "ota" && !cleanRoleResolution) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
let canonicalNodeAt;
|
|
686
|
+
if (cleanRoleResolution) {
|
|
687
|
+
canonicalNodeAt = (index) => {
|
|
688
|
+
const position = positions[index];
|
|
689
|
+
return position === null ? null : device.nodes[position];
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
else if (!pinoutMatches(model.pinout, roles)) {
|
|
693
|
+
// **Opened and named, not refused.** The part *is* registered -- this is an arity
|
|
694
|
+
// disagreement between a real entry and a real document, not an unknown chip -- so the
|
|
695
|
+
// two facts a reader needs are which entry and which counts, and both are in the warning.
|
|
696
|
+
// Refusing here failed the whole document for one component that could not be placed,
|
|
697
|
+
// which is the trade this repository settled the other way earlier today: losing a pedal
|
|
698
|
+
// entirely is a worse answer than losing the one stage nothing can bind.
|
|
699
|
+
//
|
|
700
|
+
// `boss-dd-3b`'s `IC2` is the case that forced it. Its `SA571D` is declared as a
|
|
701
|
+
// six-terminal source-visible shell (`rect_cap1`, `rect_cap2`, `audio_in`, `audio_out`,
|
|
702
|
+
// `vcc`, `gnd`) against the SA571's real sixteen, and four of the six pins a `compandor`
|
|
703
|
+
// section binds -- `rect_in`, `cell_in`, `inv_in` and the R3 return -- are not declared at
|
|
704
|
+
// all, with both rect-cap nodes touched by nothing but the shell itself. The packet's own
|
|
705
|
+
// record calls it a shell and claims no terminal trace for it, so there is nothing here to
|
|
706
|
+
// correct upstream and nothing this stage may invent: the summing node and the R3 return
|
|
707
|
+
// are where the external network sets the gain, and choosing them would be choosing the
|
|
708
|
+
// law. The same chip is modelled in full wherever a document declares its sixteen pins.
|
|
709
|
+
return [
|
|
710
|
+
[
|
|
711
|
+
device,
|
|
712
|
+
{
|
|
713
|
+
outcome: "law",
|
|
714
|
+
device: device.id,
|
|
715
|
+
law: { kind: "open" },
|
|
716
|
+
openReason: "registry-arity-mismatch",
|
|
717
|
+
insteadOfRefusal: `part ${identity.partId} is registered against a ${model.pinout.length}-terminal pinout this component's ${roles.length} declared terminals do not match, and its role naming does not resolve cleanly to its sections`,
|
|
718
|
+
},
|
|
719
|
+
],
|
|
720
|
+
];
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
canonicalNodeAt = (index) => device.nodes[index];
|
|
724
|
+
}
|
|
725
|
+
// **A section terminal past the part's own pins is a node inside the package** -- a digital
|
|
726
|
+
// transistor's base behind its built-in series resistor. Allocated once per device and index,
|
|
727
|
+
// so every section naming it shares it, and no pin of the document can reach it.
|
|
728
|
+
const pinCount = Math.max(model.pinout.length, entry.terminalRoleGroups.length);
|
|
729
|
+
const sectioned = model.sections.map((section, index) => {
|
|
730
|
+
const nodes = section.terminals.map((terminal) => terminal >= pinCount ? internalNode(device.id, terminal) : canonicalNodeAt(terminal));
|
|
731
|
+
if (nodes.some((node) => node === undefined || node === null)) {
|
|
732
|
+
return [
|
|
733
|
+
device,
|
|
734
|
+
{
|
|
735
|
+
outcome: "unsupported",
|
|
736
|
+
device: device.id,
|
|
737
|
+
reason: `part ${identity.partId} section ${index} names a terminal this component does not declare`,
|
|
738
|
+
},
|
|
739
|
+
];
|
|
740
|
+
}
|
|
741
|
+
// Read from the section's *parent* device, which is where the package's supply pins
|
|
742
|
+
// are declared -- a section's own terminal list is only its three signal pins.
|
|
743
|
+
const sectionDrop = section.law.kind === "ideal-opamp" ? (section.law.outputSwingDropVolts ?? null) : null;
|
|
744
|
+
const sectionRails = railsAfterSwingDrop(opampRails(device, rails, nodeToVoltage), sectionDrop);
|
|
745
|
+
const railHigh = sectionRails.high;
|
|
746
|
+
const railLow = sectionRails.low;
|
|
747
|
+
// An op-amp's rails come from the op-amp's own supply terminals or the circuit's supply set,
|
|
748
|
+
// not from the registry -- the same substitution a declared `opamp` gets. A registry cannot know them:
|
|
749
|
+
// it describes a part, and the rails are a property of the pedal the part is fitted to.
|
|
750
|
+
// A compandor carries an op-amp too, and it needs the same substitution: its rails were
|
|
751
|
+
// computed just above and then dropped on the floor, because this only matched the
|
|
752
|
+
// `ideal-opamp` kind. An unrailed internal op-amp is not a small error in the Figure 7
|
|
753
|
+
// compressor, where the gain cell is the only AC feedback and the gain is unbounded
|
|
754
|
+
// until the detector charges -- `mxr-carbon-copy` answered a 2 ms burst with 195 V.
|
|
755
|
+
const law = section.law.kind === "ideal-opamp"
|
|
756
|
+
? {
|
|
757
|
+
kind: "ideal-opamp",
|
|
758
|
+
railHigh,
|
|
759
|
+
railLow,
|
|
760
|
+
// The registry states the part's gain; only the rails are the pedal's.
|
|
761
|
+
openLoopGain: section.law.openLoopGain,
|
|
762
|
+
// Kept on the lowered law so `findOpampLawDefaults` can tell a part that
|
|
763
|
+
// states how close it gets to the rails from one that does not.
|
|
764
|
+
...(sectionDrop === null ? {} : { outputSwingDropVolts: sectionDrop }),
|
|
765
|
+
}
|
|
766
|
+
: section.law.kind === "compandor"
|
|
767
|
+
? { ...section.law, railHigh, railLow }
|
|
768
|
+
: section.law;
|
|
769
|
+
const sectionDevice = {
|
|
770
|
+
...device,
|
|
771
|
+
id: `${device.id}#${index}`,
|
|
772
|
+
nodes: nodes,
|
|
773
|
+
identity: {
|
|
774
|
+
...device.identity,
|
|
775
|
+
terminalRoles: canonicalRoles(law.kind, nodes.length),
|
|
776
|
+
// A section's terminal list is its own, so the package's declared roles do not
|
|
777
|
+
// describe it -- and `canonicalRoles` above already states each section pin's
|
|
778
|
+
// role exactly. Carrying the parent's array would misalign it by length.
|
|
779
|
+
declaredTerminalRoles: canonicalRoles(law.kind, nodes.length),
|
|
780
|
+
},
|
|
781
|
+
};
|
|
782
|
+
return [
|
|
783
|
+
sectionDevice,
|
|
784
|
+
{
|
|
785
|
+
outcome: "law",
|
|
786
|
+
device: sectionDevice.id,
|
|
787
|
+
law,
|
|
788
|
+
},
|
|
789
|
+
];
|
|
790
|
+
});
|
|
791
|
+
// A `sections-core` entry additionally keeps its sampled core: the sections above
|
|
792
|
+
// become ordinary devices, and the whole device resolves as the existing macro
|
|
793
|
+
// against the core model, binding its ports to the same boundary nodes the
|
|
794
|
+
// sections share. The core keeps the package id; the sections took `#index`.
|
|
795
|
+
if (model.kind === "sections-core") {
|
|
796
|
+
return [
|
|
797
|
+
...sectioned,
|
|
798
|
+
[
|
|
799
|
+
device,
|
|
800
|
+
resolveMacro(device, identity.partId, model.core, registry, netlist),
|
|
801
|
+
],
|
|
802
|
+
];
|
|
803
|
+
}
|
|
804
|
+
return sectioned;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* A registry macro against one device: terminal roles resolved to indices, pedal-specific
|
|
808
|
+
* parameters merged in.
|
|
809
|
+
*
|
|
810
|
+
* Two things the registry cannot state and this stage supplies, both for the same reason the
|
|
811
|
+
* `ideal-opamp` branch above takes its rails from the circuit — a registry describes a part, and
|
|
812
|
+
* these belong to the pedal it was fitted to:
|
|
813
|
+
*
|
|
814
|
+
* - **where the ports landed**, from `MacroPortRoles`. Order-independent, so one entry serves
|
|
815
|
+
* every declaration order that names its terminals.
|
|
816
|
+
* - **how long the delay is**, from the source's own `DelayMs`. A bucket brigade's delay is
|
|
817
|
+
* `stages / (2 * f_clock)` and only the stage count is part-intrinsic, so the registry cannot
|
|
818
|
+
* know it. See `parametersFor`'s `ic` branch.
|
|
819
|
+
*
|
|
820
|
+
* **A macro with no locatable audio port is refused, not defaulted.** Measured: leaving
|
|
821
|
+
* `audioPortImpedanceOhms` null keeps both ports absent, and a macro with no tap and no write-back
|
|
822
|
+
* is not in the signal path at all -- `electro-harmonix-electric-mistress` renders exact zero that
|
|
823
|
+
* way, because its whole signal path is the macro. A pedal that compiles to silence is worse than
|
|
824
|
+
* one that refuses by name. See `thoughts/shared/experiments/macro-audio-port-impedance/`.
|
|
825
|
+
*/
|
|
826
|
+
/**
|
|
827
|
+
* The physical position a bare-pin document's terminal sits at, when this entry names one. Returns
|
|
828
|
+
* `null` unless the entry supplies positions for `port`, the device is fully bare (every terminal a
|
|
829
|
+
* `pinN` with no role token), and the position is within the device's declared terminal count. A
|
|
830
|
+
* document that names even one terminal resolves by role and never reaches here.
|
|
831
|
+
*/
|
|
832
|
+
function barePinPosition(macro, roles, port) {
|
|
833
|
+
const position = macro.barePinPositions?.[port];
|
|
834
|
+
if (position === undefined) {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
if (!roles.every((role) => role === null)) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
if (position >= roles.length) {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
return position;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Thin wrapper that brackets the real resolution so every registry entry consulted while computing
|
|
847
|
+
* this macro's parameters is recorded against the macro's device id. **No behaviour change** -- the
|
|
848
|
+
* wrapper only opens and closes the recording.
|
|
849
|
+
*/
|
|
850
|
+
function resolveMacro(device, partId, macro, registry, netlist) {
|
|
851
|
+
return withRecordedReads(`macro:${device.id}`, () => resolveMacroInner(device, partId, macro, registry, netlist));
|
|
852
|
+
}
|
|
853
|
+
function resolveMacroInner(device, partId, macro,
|
|
854
|
+
// Required, and ahead of the optional `netlist` so it cannot be omitted. It was
|
|
855
|
+
// `registry?` while `bbd-clock.ts` defaulted its own parameter to the part catalog, so an
|
|
856
|
+
// omission here resolved to the full catalog instead of failing — the leak that kept the
|
|
857
|
+
// compiler from being part-free. Its one caller always had a registry to give.
|
|
858
|
+
registry, netlist) {
|
|
859
|
+
const roles = device.identity.terminalRoles;
|
|
860
|
+
const audioIn = terminalWithRole(roles, macro.ports.audioIn) ??
|
|
861
|
+
barePinPosition(macro, roles, "audioIn");
|
|
862
|
+
const audioOut = terminalWithRole(roles, macro.ports.audioOut) ??
|
|
863
|
+
barePinPosition(macro, roles, "audioOut");
|
|
864
|
+
// A second output, only for a part that declares one and a device that names it. Absent is
|
|
865
|
+
// the ordinary case and stays a one-output macro: unlike the ports above, failing to find
|
|
866
|
+
// this is not a refusal, because most parts genuinely have one output.
|
|
867
|
+
const audioOut2 = (macro.ports.audioOut2 ?? []).length === 0
|
|
868
|
+
? null
|
|
869
|
+
: terminalWithRole(roles, macro.ports.audioOut2 ?? []);
|
|
870
|
+
if (audioIn === null || audioOut === null) {
|
|
871
|
+
const missing = [
|
|
872
|
+
audioIn === null ? "audio in" : null,
|
|
873
|
+
audioOut === null ? "audio out" : null,
|
|
874
|
+
].filter((label) => label !== null);
|
|
875
|
+
return {
|
|
876
|
+
outcome: "unsupported",
|
|
877
|
+
device: device.id,
|
|
878
|
+
reason: `part ${partId} declares no terminal this registry can read as ${missing.join(" or ")}: its ${roles.length} terminals are named ${describeRoles(roles)}`,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const parameterPort = macro.ports.parameter ?? [];
|
|
882
|
+
const parameterTerminal = parameterPort.length === 0
|
|
883
|
+
? null
|
|
884
|
+
: terminalWithRole(roles, parameterPort) ??
|
|
885
|
+
barePinPosition(macro, roles, "parameter");
|
|
886
|
+
// Delay time resolution precedence:
|
|
887
|
+
// 1. If a clock driver exists and its timing network resolves, derive delaySeconds.
|
|
888
|
+
// 2. Otherwise fall back to declared device.parameters.delaySeconds (DelayMs).
|
|
889
|
+
// 3. If neither derived delay nor declared DelayMs exists, refuse naming missing DelayMs.
|
|
890
|
+
let delaySeconds = device.parameters.delaySeconds;
|
|
891
|
+
let clockControl = null;
|
|
892
|
+
// Provenance, so a consumer can tell a delay the circuit produced from one the source
|
|
893
|
+
// asserted. Starts as `declared` and is only promoted by an actual derivation below.
|
|
894
|
+
// `clock-law` is the third state: the static base stays declared (it sizes the line)
|
|
895
|
+
// while the sweep is derived per sample by the open-OX2 law.
|
|
896
|
+
let delayProvenance = "declared";
|
|
897
|
+
let delayDeclaredReason = "this part's model carries no clock derivation";
|
|
898
|
+
let clockLawFields = {};
|
|
899
|
+
if (macro.modelId === "bucket-brigade-delay-line") {
|
|
900
|
+
const stages = macro.parameters.stages;
|
|
901
|
+
if (stages === undefined || !(stages > 0) || !Number.isInteger(stages)) {
|
|
902
|
+
return {
|
|
903
|
+
outcome: "unsupported",
|
|
904
|
+
device: device.id,
|
|
905
|
+
reason: `part ${partId} is a bucket-brigade delay line with no stage count declared in its registry entry`,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
if (netlist !== undefined) {
|
|
909
|
+
// The open-OX2 relaxation form first: its sweep is a per-sample law, not a
|
|
910
|
+
// static number, so the star derivation below must not claim it. Absent
|
|
911
|
+
// here falls through to the existing path untouched.
|
|
912
|
+
const openOx2 = deriveOpenOx2ClockLaw(netlist, registry);
|
|
913
|
+
if (openOx2.outcome === "recognized") {
|
|
914
|
+
delayProvenance = "clock-law";
|
|
915
|
+
delayDeclaredReason =
|
|
916
|
+
`static base is the declared DelayMs fallback; the sweep is derived per sample ` +
|
|
917
|
+
`by the open-OX2 clock law from node ${openOx2.slowNode} ` +
|
|
918
|
+
`(R ${openOx2.rOhms} C ${openOx2.cFarads}, steered by ${openOx2.steeredBy})`;
|
|
919
|
+
clockLawFields = {
|
|
920
|
+
clockLawNode: openOx2.slowNode,
|
|
921
|
+
clockLawSteeredBy: openOx2.steeredBy,
|
|
922
|
+
clockLawParams: {
|
|
923
|
+
rOhms: openOx2.rOhms,
|
|
924
|
+
cFarads: openOx2.cFarads,
|
|
925
|
+
vddVolts: OPEN_OX2_VDD_VOLTS,
|
|
926
|
+
vthVolts: OPEN_OX2_VTH_VOLTS,
|
|
927
|
+
vfVolts: OPEN_OX2_VF_VOLTS,
|
|
928
|
+
floorVolts: OPEN_OX2_FLOOR_VOLTS,
|
|
929
|
+
},
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
const clockDerivation = deriveBbdDelayFromNetlist(device, netlist, stages, registry);
|
|
934
|
+
delayDeclaredReason =
|
|
935
|
+
clockDerivation.outcome === "no-clock-driver"
|
|
936
|
+
? "no clock driver device is wired to this delay line"
|
|
937
|
+
: clockDerivation.outcome === "refused"
|
|
938
|
+
? clockDerivation.reason
|
|
939
|
+
: null;
|
|
940
|
+
if (clockDerivation.outcome === "derived") {
|
|
941
|
+
delayProvenance = "derived";
|
|
942
|
+
delaySeconds = clockDerivation.delaySeconds;
|
|
943
|
+
if (clockDerivation.controlId !== null) {
|
|
944
|
+
clockControl = {
|
|
945
|
+
controlId: clockDerivation.controlId,
|
|
946
|
+
taper: clockDerivation.taper,
|
|
947
|
+
ohmsAtControlMin: clockDerivation.ohmsAtControlMin,
|
|
948
|
+
ohmsAtControlMax: clockDerivation.ohmsAtControlMax,
|
|
949
|
+
farads: clockDerivation.cFarads,
|
|
950
|
+
stages: clockDerivation.stages,
|
|
951
|
+
formulaConstant: clockDerivation.formulaConstant,
|
|
952
|
+
offsetSeconds: clockDerivation.offsetSeconds,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
else if (macro.modelId === "digital-delay-line") {
|
|
960
|
+
const part = foldPartId(device.identity.partNumber ?? "");
|
|
961
|
+
if (part === "pt2399" || part === "pt2399s") {
|
|
962
|
+
if (netlist !== undefined) {
|
|
963
|
+
const ptDerivation = derivePt2399DelayFromNetlist(device, netlist);
|
|
964
|
+
delayDeclaredReason =
|
|
965
|
+
ptDerivation.outcome === "refused" ? ptDerivation.reason : null;
|
|
966
|
+
if (ptDerivation.outcome === "derived") {
|
|
967
|
+
delayProvenance = "derived";
|
|
968
|
+
delaySeconds = ptDerivation.delaySeconds;
|
|
969
|
+
if (ptDerivation.controlId !== null) {
|
|
970
|
+
clockControl = {
|
|
971
|
+
controlId: ptDerivation.controlId,
|
|
972
|
+
taper: ptDerivation.taper,
|
|
973
|
+
ohmsAtControlMin: ptDerivation.ohmsAtControlMin,
|
|
974
|
+
ohmsAtControlMax: ptDerivation.ohmsAtControlMax,
|
|
975
|
+
farads: ptDerivation.cFarads,
|
|
976
|
+
stages: ptDerivation.stages,
|
|
977
|
+
formulaConstant: ptDerivation.formulaConstant,
|
|
978
|
+
offsetSeconds: ptDerivation.offsetSeconds,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
else if (part === "m50195" || part === "m50195p") {
|
|
985
|
+
if (netlist !== undefined) {
|
|
986
|
+
const mDerivation = deriveM50195DelayFromNetlist(device, netlist);
|
|
987
|
+
delayDeclaredReason =
|
|
988
|
+
mDerivation.outcome === "refused" ? mDerivation.reason : null;
|
|
989
|
+
if (mDerivation.outcome === "derived") {
|
|
990
|
+
delayProvenance = "derived";
|
|
991
|
+
delaySeconds = mDerivation.delaySeconds;
|
|
992
|
+
if (mDerivation.controlId !== null) {
|
|
993
|
+
clockControl = {
|
|
994
|
+
controlId: mDerivation.controlId,
|
|
995
|
+
taper: mDerivation.taper,
|
|
996
|
+
ohmsAtControlMin: mDerivation.ohmsAtControlMin,
|
|
997
|
+
ohmsAtControlMax: mDerivation.ohmsAtControlMax,
|
|
998
|
+
farads: mDerivation.cFarads,
|
|
999
|
+
stages: mDerivation.stages,
|
|
1000
|
+
formulaConstant: mDerivation.formulaConstant,
|
|
1001
|
+
offsetSeconds: mDerivation.offsetSeconds,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
// **Only a delay model needs a delay time.** A reverb's decay is part-intrinsic -- the
|
|
1009
|
+
// BTDR-2 datasheet's T60 -- so it arrives in the registry entry's `parameters` and the pedal's
|
|
1010
|
+
// wiring says nothing about it. Applying the delay refusal to every macro made a reverb
|
|
1011
|
+
// module refuse with "declares no positive DelayMs", which misattributes the gap to the packet
|
|
1012
|
+
// and would invite a bogus `DelayMs` into a correct `.vdsp`.
|
|
1013
|
+
// **The modulation port, resolved only for a bucket brigade and only from the netlist.**
|
|
1014
|
+
// A delay whose clock is steered by an LFO cannot state its length as one number, which is
|
|
1015
|
+
// what `deriveBbdDelayFromNetlist` now refuses to pretend. This is the other half of that
|
|
1016
|
+
// refusal: the length still moves, and this names the node it moves with.
|
|
1017
|
+
let modulationFields = {};
|
|
1018
|
+
if (macro.modelId === "bucket-brigade-delay-line" && netlist !== undefined) {
|
|
1019
|
+
const source = resolveClockModulationSource(device, netlist, registry);
|
|
1020
|
+
if (source.outcome === "resolved") {
|
|
1021
|
+
modulationFields = {
|
|
1022
|
+
modulationNode: source.node,
|
|
1023
|
+
modulationSteeredBy: source.steeredBy,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const needsDelayTime = macro.modelId === "bucket-brigade-delay-line" ||
|
|
1028
|
+
macro.modelId === "digital-delay-line";
|
|
1029
|
+
if (needsDelayTime && (delaySeconds === undefined || !(delaySeconds > 0))) {
|
|
1030
|
+
return {
|
|
1031
|
+
outcome: "unsupported",
|
|
1032
|
+
device: device.id,
|
|
1033
|
+
reason: `part ${partId} is a delay macro and this component declares no positive DelayMs, which is the one parameter its model cannot be given by the registry`,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
outcome: "macro",
|
|
1038
|
+
device: device.id,
|
|
1039
|
+
macro: {
|
|
1040
|
+
modelId: macro.modelId,
|
|
1041
|
+
parameters: delaySeconds === undefined
|
|
1042
|
+
? { ...macro.parameters }
|
|
1043
|
+
: { ...macro.parameters, delaySeconds },
|
|
1044
|
+
portTerminals: audioOut2 === null
|
|
1045
|
+
? [audioIn, audioOut]
|
|
1046
|
+
: [audioIn, audioOut, audioOut2],
|
|
1047
|
+
audioPortImpedanceOhms: macro.audioPortImpedanceOhms,
|
|
1048
|
+
// Both or neither, which `MacroModel` requires: a terminal with no reference voltage
|
|
1049
|
+
// cannot be scaled against anything.
|
|
1050
|
+
...(parameterTerminal !== null && macro.parameterReferenceVolts !== null
|
|
1051
|
+
? {
|
|
1052
|
+
parameterTerminal,
|
|
1053
|
+
parameterReferenceVolts: macro.parameterReferenceVolts,
|
|
1054
|
+
}
|
|
1055
|
+
: { parameterTerminal: null, parameterReferenceVolts: null }),
|
|
1056
|
+
...modulationFields,
|
|
1057
|
+
...clockLawFields,
|
|
1058
|
+
clockControl,
|
|
1059
|
+
delayProvenance,
|
|
1060
|
+
delayDeclaredReason: delayProvenance === "derived" ? null : delayDeclaredReason,
|
|
1061
|
+
},
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
/** A device's terminal roles as a diagnostic reads them, with unnamed pins called what they are. */
|
|
1065
|
+
function describeRoles(roles) {
|
|
1066
|
+
const named = roles.filter((role) => role !== null);
|
|
1067
|
+
return named.length === 0
|
|
1068
|
+
? "by pin number alone, carrying no role tokens"
|
|
1069
|
+
: named.join(", ");
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Role tokens a section device presents, so lowering's per-kind terminal lookup finds them.
|
|
1073
|
+
*
|
|
1074
|
+
* Only the kinds a registry can currently supply as a section. Anything else gets no roles, which
|
|
1075
|
+
* leaves lowering on its positional fallback -- correct for a two-terminal law and the reason an
|
|
1076
|
+
* unlisted three-terminal kind must be added here rather than assumed to work.
|
|
1077
|
+
*/
|
|
1078
|
+
function canonicalRoles(kind, count) {
|
|
1079
|
+
// **Core role values, not folded name tokens.** `declaredTerminalRoles` holds "the role each
|
|
1080
|
+
// terminal declares, as core parsed it", and this wrote `noninverting` -- the folded *name*
|
|
1081
|
+
// token the old `opampPlusRoles` set matched. Harmless while lowering compared against that
|
|
1082
|
+
// set, and a wrong answer the moment it compared against the core vocabulary: nine packets'
|
|
1083
|
+
// op-amp sections reported `nonInverting=0` and refused.
|
|
1084
|
+
if (kind === "ideal-opamp" && count === 3) {
|
|
1085
|
+
return ["nonInverting", "inverting", "output"];
|
|
1086
|
+
}
|
|
1087
|
+
if (kind === "transformer" && count === 4) {
|
|
1088
|
+
return ["primarya", "primaryb", "secondarya", "secondaryb"];
|
|
1089
|
+
}
|
|
1090
|
+
if (kind === "clock-driver" && count === 6) {
|
|
1091
|
+
// `gnd` is the part's own GND pin and is last because it was added after the other five.
|
|
1092
|
+
// It is not circuit ground: the MN3101/MN3102 run on a single negative supply, so a +9 V
|
|
1093
|
+
// pedal ties this pin to the positive rail and ties `vdd` to circuit ground.
|
|
1094
|
+
return ["cp1", "cp2", "vgg", "vdd", "ox1", "gnd"];
|
|
1095
|
+
}
|
|
1096
|
+
// A registry section's nodes are already in the law's canonical order -- `section.terminals`
|
|
1097
|
+
// indexed them -- so naming them here states what is true rather than inferring it. Added
|
|
1098
|
+
// 2026-09-03 because `lower.ts` now reads declared roles: without these a three-terminal
|
|
1099
|
+
// `bjt`/`fet` section arrived with no roles, took the positional path (correctly, since the
|
|
1100
|
+
// order is canonical) and was reported as an unreadable-role fallback. A false positive on a
|
|
1101
|
+
// binding the registry had already got right.
|
|
1102
|
+
if (kind === "bjt" && count === 3) {
|
|
1103
|
+
return ["base", "collector", "emitter"];
|
|
1104
|
+
}
|
|
1105
|
+
if (kind === "fet" && count === 3) {
|
|
1106
|
+
return ["gate", "drain", "source"];
|
|
1107
|
+
}
|
|
1108
|
+
// A regulator or converter lowered to a supply: `section.terminals` already put the output
|
|
1109
|
+
// first and the reference second, so naming them states that order rather than guessing it.
|
|
1110
|
+
// Without this the section arrived unlabelled, `supplyTerminals` fell through to
|
|
1111
|
+
// `terminalPair`, and which end got the voltage was whichever the entry happened to list
|
|
1112
|
+
// first -- how `moogerfooger-mf-102` came to hold its +5 rail at -4.9989 V.
|
|
1113
|
+
if (kind === "voltage-source" && count === 2) {
|
|
1114
|
+
return ["positive", "negative"];
|
|
1115
|
+
}
|
|
1116
|
+
// Same shape as the supply above: `section.terminals` has already put the LED first and its
|
|
1117
|
+
// return second, followed by the cell's two ends, which is the order `optocouplerTerminals`
|
|
1118
|
+
// falls through to. Naming them states that order instead of leaving the section to depend on
|
|
1119
|
+
// it -- and an unlabelled section is indistinguishable from a document that declared nothing,
|
|
1120
|
+
// so without this the two get the same warning and only one of them deserves it.
|
|
1121
|
+
//
|
|
1122
|
+
// `end` twice is deliberate and is the physics: a photoresistor conducts the same either way,
|
|
1123
|
+
// so the cell states no polarity and either declaration order is the same circuit.
|
|
1124
|
+
if (kind === "optocoupler" && count === 4) {
|
|
1125
|
+
return ["anode", "cathode", "end", "end"];
|
|
1126
|
+
}
|
|
1127
|
+
return Array.from({ length: count }, () => null);
|
|
1128
|
+
}
|
|
1129
|
+
function supplyNodes(netlist) {
|
|
1130
|
+
const returns = new Set();
|
|
1131
|
+
const drives = new Set();
|
|
1132
|
+
const degree = new Map();
|
|
1133
|
+
for (const device of netlist.devices) {
|
|
1134
|
+
for (const node of device.nodes) {
|
|
1135
|
+
degree.set(node, (degree.get(node) ?? 0) + 1);
|
|
1136
|
+
}
|
|
1137
|
+
if (device.kind !== "voltage-source") {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
// Declared roles, as everywhere else that asks which end of a supply is which.
|
|
1141
|
+
device.identity.declaredTerminalRoles.forEach((role, index) => {
|
|
1142
|
+
const node = device.nodes[index];
|
|
1143
|
+
if (role === null || node === undefined) {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (role === "negative") {
|
|
1147
|
+
returns.add(node);
|
|
1148
|
+
}
|
|
1149
|
+
if (role === "positive") {
|
|
1150
|
+
drives.add(node);
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
return { returns, drives, degree };
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* The generic triode, following the generic BJT's precedent exactly: an unregistered
|
|
1158
|
+
* transistor gets a device-class law rather than a refusal, so an unregistered tube does
|
|
1159
|
+
* too. A registry entry refines it; nothing here reads a part number.
|
|
1160
|
+
*
|
|
1161
|
+
* These are the 12AX7 Koren coefficients `whole-amp-5f1` already validated, taken rather
|
|
1162
|
+
* than re-derived. That experiment solves a 2x12AX7 + 6V6 path to a physically sane bias
|
|
1163
|
+
* (V1A `Vk 1.251`, `Vp 166.6`, `Ip 0.83 mA` on a 250 V rail through 100k with a 1.5k
|
|
1164
|
+
* cathode), which makes it a real oracle this repository already owns.
|
|
1165
|
+
*
|
|
1166
|
+
* **There are two prior tube experiments and they validate different things** — worth knowing
|
|
1167
|
+
* before treating either as the reference:
|
|
1168
|
+
*
|
|
1169
|
+
* - `whole-amp-5f1` uses **Koren** with registry-fit coefficients and validates a **static
|
|
1170
|
+
* operating point** against textbook 12AX7 values. That is what this law follows.
|
|
1171
|
+
* - `compact-mna-dynamic-triode-cell` uses a **different, simpler law** (`triodeK`, `mu`,
|
|
1172
|
+
* `triodeSoftness`) and validates **dynamics** — grid/coupling recovery — plus DK-reduction
|
|
1173
|
+
* equivalence to `1e-9` correlation with an analytic reduced Jacobian. Its own boundary note
|
|
1174
|
+
* says it "is not calibrated to a real 12AX7 datasheet or ngspice tube model yet", so its
|
|
1175
|
+
* coefficients are not a datasheet anchor and are deliberately not used here.
|
|
1176
|
+
*
|
|
1177
|
+
* When the re-orientation reaches state-space reduction, that second experiment is the proven
|
|
1178
|
+
* reference for a DK-reduced triode cell; it is not a reference for this law's coefficients.
|
|
1179
|
+
*
|
|
1180
|
+
* A 12AX7 as the *generic* triode is a choice worth stating: it is the overwhelmingly
|
|
1181
|
+
* common preamp triode, and a power triode is a different fit. An amp whose character
|
|
1182
|
+
* comes from a specific tube needs a registry entry, and this default should not be read
|
|
1183
|
+
* as a claim about that tube.
|
|
1184
|
+
*/
|
|
1185
|
+
const GENERIC_TRIODE = {
|
|
1186
|
+
mu: 100.29,
|
|
1187
|
+
kg1: 1375.3,
|
|
1188
|
+
kp: 2052.0,
|
|
1189
|
+
kvb: 9706.8,
|
|
1190
|
+
ex: 1.0,
|
|
1191
|
+
// Grid conduction, from `compact-mna-dynamic-triode-cell`'s validated cell rather than
|
|
1192
|
+
// invented: `gridIs 1e-5`, `gridVt 0.06`, `gridThreshold 0.05`. Those are the values it
|
|
1193
|
+
// reports 0 Newton failures with across a 40x velocity ladder, at 2.1 to 2.8 iterations
|
|
1194
|
+
// per sample.
|
|
1195
|
+
gridSaturationCurrent: 1.0e-5,
|
|
1196
|
+
gridOnsetVolts: 0.05,
|
|
1197
|
+
gridScaleVolts: 0.06,
|
|
1198
|
+
// Omitted by the fits these coefficients came from, so 0 keeps them self-consistent.
|
|
1199
|
+
contactPotentialVolts: 0,
|
|
1200
|
+
};
|
|
1201
|
+
/**
|
|
1202
|
+
* The generic pentode: `whole-amp-5f1`'s 6V6GT fit, again taken rather than re-derived. That
|
|
1203
|
+
* experiment solves the 6V6 to `Vk 23.3`, `Vp 329.1`, `Ip 49.6 mA` — and records honestly that
|
|
1204
|
+
* this is "~10 mA hot vs a real 5F1", calling it "first-order pentode-model calibration TODO,
|
|
1205
|
+
* not a viability blocker". So this law is a working power pentode and **not** a calibrated one;
|
|
1206
|
+
* the plate current is known to run high.
|
|
1207
|
+
*
|
|
1208
|
+
* **`kg1` was rescaled from 1060.0 when the plate gate changed from `tanh` against a 40 V knee to
|
|
1209
|
+
* Koren's `atan(Vpk/kvb)`, so this fit is behaviour-neutral across that change.** Unlike the
|
|
1210
|
+
* catalogued tubes -- whose published coefficients were generated against `atan` and were therefore
|
|
1211
|
+
* 26-34% low under `tanh` -- these numbers were tuned locally *with* `tanh`, so its saturation at
|
|
1212
|
+
* 1.0 is already inside `kg1`. The factor is `atan(305.8/38) / tanh(305.8/40)` = 1.44717, taken
|
|
1213
|
+
* at the operating point this fit's own experiment records (`Vp 329.1`, `Vk 23.3`), which makes the
|
|
1214
|
+
* plate current identical there and within a percent nearby. **The known ~10 mA of excess above is
|
|
1215
|
+
* untouched and is a separate defect**; this rescale deliberately preserves it rather than silently
|
|
1216
|
+
* folding a second correction into a first.
|
|
1217
|
+
*
|
|
1218
|
+
* **The grid parameters are the weakest numbers here and are reused, not measured.** They come
|
|
1219
|
+
* from `compact-mna-dynamic-triode-cell`'s *preamp triode*; a power tube's grid conducts at a
|
|
1220
|
+
* different onset and much harder, and blocking distortion on an overdriven power stage is
|
|
1221
|
+
* exactly that behaviour. Omitting grid current would be worse — it is the mechanism that makes
|
|
1222
|
+
* an overdriven output stage sound like one — but a registry entry should refine these before
|
|
1223
|
+
* any claim about a specific power tube's overdrive.
|
|
1224
|
+
*/
|
|
1225
|
+
const GENERIC_PENTODE = {
|
|
1226
|
+
mu: 8.0,
|
|
1227
|
+
kg1: 1534.0,
|
|
1228
|
+
kp: 72.0,
|
|
1229
|
+
kvb: 38.0,
|
|
1230
|
+
ex: 1.34,
|
|
1231
|
+
gridSaturationCurrent: 1.0e-5,
|
|
1232
|
+
gridOnsetVolts: 0.05,
|
|
1233
|
+
gridScaleVolts: 0.06,
|
|
1234
|
+
// Omitted by the fits these coefficients came from, so 0 keeps them self-consistent.
|
|
1235
|
+
contactPotentialVolts: 0,
|
|
1236
|
+
};
|
|
1237
|
+
/**
|
|
1238
|
+
* The generic tube rectifier, following the triode and pentode's discipline: coefficients taken
|
|
1239
|
+
* from a datasheet-anchored source this repository already holds, never re-derived and never
|
|
1240
|
+
* invented.
|
|
1241
|
+
*
|
|
1242
|
+
* The source is `thoughts/shared/experiments/power-domain-tube-rectifier/`, whose two cases fit
|
|
1243
|
+
* `K` from admitted datasheet operating points and reproduce them to better than 0.15%:
|
|
1244
|
+
*
|
|
1245
|
+
* ```
|
|
1246
|
+
* K (A/V^1.5) anchor reproduces
|
|
1247
|
+
* 5Y3GT 3.54e-4 50 V, 125 mA 0.125158 A
|
|
1248
|
+
* GZ34 3.21e-3 17 V, 225 mA 0.224998 A
|
|
1249
|
+
* ```
|
|
1250
|
+
*
|
|
1251
|
+
* **5Y3GT is the generic, and that choice is weaker than the triode's 12AX7.** The 12AX7 is the
|
|
1252
|
+
* overwhelmingly common preamp triode; there is no equivalently dominant rectifier, and the amp
|
|
1253
|
+
* corpus's three instances are one 5Y3GT, one 5U4 and one GZ34 — one each. The two anchored fits
|
|
1254
|
+
* are a **factor of nine apart in `K`**, which at 50 mA is 47 V of drop against 10 V, so this
|
|
1255
|
+
* default is a directly-heated 5Y3-class rectifier and a specific amp's sag needs a registry
|
|
1256
|
+
* entry. It is the conservative end of the range (the largest drop of the two) and it belongs to
|
|
1257
|
+
* `fender-5f1-champ`, the one amp whose preamp stage this repository already validates against an
|
|
1258
|
+
* ngspice oracle.
|
|
1259
|
+
*
|
|
1260
|
+
* Nothing here reads `PartNumber`. All three instances declare one — `5Y3GT`, `5U4`, `GZ34` — and
|
|
1261
|
+
* acting on it is stage 2's job through the registry, exactly as for an unregistered transistor.
|
|
1262
|
+
*/
|
|
1263
|
+
const GENERIC_TUBE_DIODE = {
|
|
1264
|
+
perveance: 3.54e-4,
|
|
1265
|
+
exponent: 1.5,
|
|
1266
|
+
};
|
|
1267
|
+
/**
|
|
1268
|
+
* The supply extremes a circuit establishes. An op-amp cannot drive its output past
|
|
1269
|
+
* them, and that needs no part number: both facts are in the netlist. A circuit that
|
|
1270
|
+
* declares no supply has no rails, and its op-amps stay unbounded.
|
|
1271
|
+
*/
|
|
1272
|
+
/**
|
|
1273
|
+
* Terminal roles that name a regulator's regulated output, as `terminalRoleToken` folds them.
|
|
1274
|
+
*
|
|
1275
|
+
* Closed vocabulary, compared as whole tokens. `output` is here because
|
|
1276
|
+
* `electro-harmonix-deluxe-memory-man`'s `U12_LM7915` spells it that way, and the only
|
|
1277
|
+
* pre-existing reader of this shape looked for `out` exactly and so could never have found it.
|
|
1278
|
+
*/
|
|
1279
|
+
const REGULATOR_OUTPUT_ROLES = ["out", "output", "vout", "voutput"];
|
|
1280
|
+
/**
|
|
1281
|
+
* Supply voltages a **registered regulator** holds its output at, and the node each one drives.
|
|
1282
|
+
*
|
|
1283
|
+
* Why this exists: a three-terminal regulator declares no voltage of its own. `U12_LM7915` in
|
|
1284
|
+
* `electro-harmonix-deluxe-memory-man` carries `parameters: {}` — the −15 V is a property of the
|
|
1285
|
+
* *part*, stated once in the registry as a `voltage-source` law, not of the document. Both
|
|
1286
|
+
* readers below were built from the netlist alone, so the pedal's entire negative rail was
|
|
1287
|
+
* invisible to them while being perfectly visible to lowering, which stamps it as a `dc-source`.
|
|
1288
|
+
*
|
|
1289
|
+
* The cost of that gap was not subtle. All ten of that pedal's op-amps declare `vminus` on the
|
|
1290
|
+
* regulator's output node, `supplyRails` returned `low: 0` because it had only seen the +24 V
|
|
1291
|
+
* rail, and every op-amp was told it could not swing below ground. Outputs clipped at 0 V
|
|
1292
|
+
* instead of −15 V, and the DC error propagated until the NE570's summing node settled at
|
|
1293
|
+
* **−20.5 V**, five volts below a rail the compiler did not know existed.
|
|
1294
|
+
*
|
|
1295
|
+
* `supply-reference.ts` already had to learn this and says so in its header: it reads the lowered
|
|
1296
|
+
* `dc-source` stamps "rather than the declared `rail`/`battery` devices". This stage runs before
|
|
1297
|
+
* lowering, so it cannot read stamps — but it holds the registry, which is where the number is.
|
|
1298
|
+
*/
|
|
1299
|
+
function registrySuppliedVoltages(netlist, registry) {
|
|
1300
|
+
const supplies = [];
|
|
1301
|
+
for (const device of netlist.devices) {
|
|
1302
|
+
// A device that states its own voltage is already read by both callers; this is only
|
|
1303
|
+
// for the ones whose voltage exists solely as a registered part fact.
|
|
1304
|
+
if (device.parameters.volts !== undefined) {
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
const identity = identify(device, registry);
|
|
1308
|
+
if (identity === null) {
|
|
1309
|
+
continue;
|
|
1310
|
+
}
|
|
1311
|
+
const entry = registryEntryFor(registry, identity, device.nodes.length);
|
|
1312
|
+
if (entry === null || entry.model.kind !== "sections") {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
for (const section of entry.model.sections) {
|
|
1316
|
+
if (section.law.kind !== "voltage-source") {
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
const volts = section.law.volts;
|
|
1320
|
+
if (!Number.isFinite(volts)) {
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
const index = terminalWithRole(device.identity.terminalRoles, REGULATOR_OUTPUT_ROLES);
|
|
1324
|
+
if (index === null) {
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
const node = device.nodes[index];
|
|
1328
|
+
if (node !== undefined && node !== 0) {
|
|
1329
|
+
supplies.push({ node, volts });
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return supplies;
|
|
1334
|
+
}
|
|
1335
|
+
function supplyRails(netlist, regulated) {
|
|
1336
|
+
// Rails count as supplies too, or an op-amp on a rail-only pedal has no ceiling to
|
|
1337
|
+
// clip against and renders a voltage the circuit could not make.
|
|
1338
|
+
//
|
|
1339
|
+
// **An AC source is not a rail**, and is excluded by the same evidence the law uses: a
|
|
1340
|
+
// declared frequency. A mains inlet is not something an op-amp output can clip against --
|
|
1341
|
+
// it swings both ways at 60 Hz, and the DC rails a solid-state amp's op-amps actually see
|
|
1342
|
+
// are downstream of its rectifier and reservoir. Counting a 120 V mains peak as `railHigh`
|
|
1343
|
+
// would give every op-amp in a mains-powered amp a ceiling 20x too high, which is a wrong
|
|
1344
|
+
// answer that renders. No packet exercises this today: the pedal corpus has no AC supply,
|
|
1345
|
+
// and no amp document reaches a program.
|
|
1346
|
+
const volts = netlist.devices
|
|
1347
|
+
.filter((device) => device.kind === "voltage-source" || device.kind === "rail")
|
|
1348
|
+
.filter((device) => device.parameters.frequency === undefined)
|
|
1349
|
+
.map((device) => device.parameters.volts)
|
|
1350
|
+
.filter((value) => value !== undefined && Number.isFinite(value))
|
|
1351
|
+
// A regulator's rail counts the same as a declared one: it is a fixed supply the
|
|
1352
|
+
// circuit's op-amps clip against. See `registrySuppliedVoltages`.
|
|
1353
|
+
.concat(regulated.map((supply) => supply.volts));
|
|
1354
|
+
if (volts.length === 0) {
|
|
1355
|
+
return { high: null, low: null };
|
|
1356
|
+
}
|
|
1357
|
+
// Ground is a rail whenever any supply exists, so a single positive supply gives a
|
|
1358
|
+
// 0..V swing rather than a symmetric one.
|
|
1359
|
+
return { high: Math.max(0, ...volts), low: Math.min(0, ...volts) };
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* The one AC frequency this document states, in Hz, or `null`.
|
|
1363
|
+
*
|
|
1364
|
+
* A transformer does not change frequency: every winding on one core runs at whatever the
|
|
1365
|
+
* primary is fed at. So a transformer lowered as independent secondary sources still needs the
|
|
1366
|
+
* mains frequency, and this is where it comes from -- the same place it already came from
|
|
1367
|
+
* before, since a coupled winding inherited it from the AC source stamped on the primary. The
|
|
1368
|
+
* change is that it is now read rather than propagated, not that a new fact is being assumed.
|
|
1369
|
+
*
|
|
1370
|
+
* **Zero or several distinct frequencies is `null`, and `null` is a refusal downstream.** One
|
|
1371
|
+
* value is unambiguous; a document with two different AC frequencies in it has no single
|
|
1372
|
+
* "mains" and this stage will not pick one. Measured across the 24 canonical amps: 20 declare
|
|
1373
|
+
* exactly one frequency, 4 declare none (`marshall-jcm800`, `orange-gro100`, `peavey-5150`,
|
|
1374
|
+
* and both `vox-ac15` packets, which declare no supply component at all), and none declares
|
|
1375
|
+
* two. So the several-frequencies branch is unexercised today and is written as a refusal for
|
|
1376
|
+
* the same reason every other unexercised branch here is.
|
|
1377
|
+
*
|
|
1378
|
+
* A frequency of 0 or less is not a waveform and is excluded, matching the `voltage-source`
|
|
1379
|
+
* law's own reading of a declared 0 Hz as DC.
|
|
1380
|
+
*/
|
|
1381
|
+
function mainsFrequency(netlist) {
|
|
1382
|
+
const frequencies = new Set(netlist.devices
|
|
1383
|
+
.map((device) => device.parameters.frequency)
|
|
1384
|
+
.filter((value) => value !== undefined && Number.isFinite(value) && value > 0));
|
|
1385
|
+
return frequencies.size === 1 ? [...frequencies][0] : null;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Is every one of this device's nodes private to itself — touched by no other device and
|
|
1389
|
+
* never ground?
|
|
1390
|
+
*
|
|
1391
|
+
* A component that shares no net with anything cannot affect the solve, however it would
|
|
1392
|
+
* have been modelled: physically, a disconnected chip does nothing. Refusing the whole
|
|
1393
|
+
* pedal for it is over-strict; several packets deliberately carry such blocks as declared
|
|
1394
|
+
* view-only power clusters (klon-centaur's `MAX1044_CLUSTER_VIEW_ONLY` draws the charge
|
|
1395
|
+
* pump on four nodes nothing else touches, and separately declares the rails it produces
|
|
1396
|
+
* as voltage-carrying ports — its own record says "not a solved terminal charge-pump
|
|
1397
|
+
* model"). The resolution is `open` plus a LOUD compile warning, so a cluster isolated by
|
|
1398
|
+
* a wiring mistake rather than by intent is still named, never silently swallowed.
|
|
1399
|
+
*
|
|
1400
|
+
* Deliberately narrow: ONE shared node — even ground — defeats it. pigtronix's LT1054
|
|
1401
|
+
* drives a diode-doubler from its switching pin, shares nets, and keeps refusing.
|
|
1402
|
+
*/
|
|
1403
|
+
export function isElectricallyIsolated(device, devicesTouchingNode) {
|
|
1404
|
+
if (device.nodes.length === 0) {
|
|
1405
|
+
return false; // zero-terminal components are the unconnected-behavior scanner's beat
|
|
1406
|
+
}
|
|
1407
|
+
return device.nodes.every((node) => node !== 0 && (devicesTouchingNode.get(node) ?? 0) <= 1);
|
|
1408
|
+
}
|
|
1409
|
+
/** How many devices touch each node, counting a device once however many terminals it lands. */
|
|
1410
|
+
export function deviceCountByNode(netlist) {
|
|
1411
|
+
const counts = new Map();
|
|
1412
|
+
for (const device of netlist.devices) {
|
|
1413
|
+
for (const node of new Set(device.nodes)) {
|
|
1414
|
+
counts.set(node, (counts.get(node) ?? 0) + 1);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return counts;
|
|
1418
|
+
}
|
|
1419
|
+
const CHARGE_PUMP_PARTS = new Set([
|
|
1420
|
+
"tc1044",
|
|
1421
|
+
"tc1044scpa",
|
|
1422
|
+
"max1044",
|
|
1423
|
+
"7662",
|
|
1424
|
+
"lt1054cp",
|
|
1425
|
+
"lt1054",
|
|
1426
|
+
]);
|
|
1427
|
+
/**
|
|
1428
|
+
* A charge-pump IC whose output node is already backed by a declared rail is a
|
|
1429
|
+
* view-only structural anchor: the rail supplies the voltage, the IC adds nothing.
|
|
1430
|
+
* The Klon's MAX1044 is handled by `isElectricallyIsolated` (all nodes private);
|
|
1431
|
+
* this covers the wider class where the pump shares nets with the declared rail.
|
|
1432
|
+
*/
|
|
1433
|
+
function isChargePumpWithDeclaredRail(device, nodeToVoltage, oneHopVoltage) {
|
|
1434
|
+
const partNumber = device.identity.partNumber;
|
|
1435
|
+
if (partNumber === null) {
|
|
1436
|
+
return false;
|
|
1437
|
+
}
|
|
1438
|
+
if (!CHARGE_PUMP_PARTS.has(partNumber.toLowerCase())) {
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
return device.nodes.some((node) => node !== 0 && (nodeToVoltage.has(node) || oneHopVoltage.has(node)));
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* An OTA's signal terminals, bound by the names the device declares for them.
|
|
1445
|
+
*
|
|
1446
|
+
* The five OTA parts in this corpus spell the same five pins five different ways, so
|
|
1447
|
+
* the closed vocabularies below are unions of what the sources actually declare, each
|
|
1448
|
+
* compared as a whole folded token by `terminalWithRole` -- never as a substring, and
|
|
1449
|
+
* never against a description. `terminalWithRole` also refuses an ambiguous match, so a
|
|
1450
|
+
* part declaring two terminals that both fold to `output` binds neither.
|
|
1451
|
+
*
|
|
1452
|
+
* Why this exists: this law is reached by `kind === "ota"` straight off the netlist,
|
|
1453
|
+
* which bypasses the registry's `sections` mapping entirely, so nothing had ever
|
|
1454
|
+
* reordered these terminals. Reading positions 0/1/2/3 of the *declared* order then
|
|
1455
|
+
* bound the CA3080's balance pin as its input, its non-inverting input as its output,
|
|
1456
|
+
* and its negative supply as its bias current -- and `bias === vee` makes
|
|
1457
|
+
* `iAbc = is * (exp(0) - 1) = 0`, so the stage had zero transconductance by
|
|
1458
|
+
* construction and `mxr-dyna-comp` was silent. Every OTA in the corpus mis-bound at
|
|
1459
|
+
* least one pin; the ones that worked did so by coincidence.
|
|
1460
|
+
*/
|
|
1461
|
+
/**
|
|
1462
|
+
* Signal-pin positions of an 8-pin OTA package, 0-indexed.
|
|
1463
|
+
*
|
|
1464
|
+
* **Verified against the CA3094 datasheet** (Intersil/RCA `CA3094, CA3094A, CA3094B`, "30MHz,
|
|
1465
|
+
* High Output Current Operational Transconductance Amplifier", Pinouts, PDIP/SOIC top view), not
|
|
1466
|
+
* just inherited from the CA3080 this comment used to cite: pin 2 and pin 3 are the differential
|
|
1467
|
+
* inputs, pin 4 is GND/V-, pin 5 is I_ABC, pin 6 is the drive output (emitter), pin 7 is V+, pin 8
|
|
1468
|
+
* is the sink output (collector), pin 1 is external frequency compensation or the inhibit input.
|
|
1469
|
+
* Those map to the indices below exactly.
|
|
1470
|
+
*
|
|
1471
|
+
* **The input polarity is conditional, which the pinout diagram does not show.** The datasheet's
|
|
1472
|
+
* output-mode table gives it: taking the output at terminal 6 ("source" mode) makes terminal 2
|
|
1473
|
+
* inverting and terminal 3 non-inverting; taking it at terminal 8 ("sink" mode) swaps them,
|
|
1474
|
+
* because the collector inverts what the emitter follows. The indices below are the terminal-6
|
|
1475
|
+
* reading, which is the output this law binds. A circuit that used pin 8 instead would need the
|
|
1476
|
+
* inputs exchanged, so an OTA whose output is not pin 6 must not be resolved by this constant.
|
|
1477
|
+
*/
|
|
1478
|
+
const OTA_DIP8_POSITIONS = {
|
|
1479
|
+
plus: 2,
|
|
1480
|
+
minus: 1,
|
|
1481
|
+
output: 5,
|
|
1482
|
+
bias: 4,
|
|
1483
|
+
vee: 3,
|
|
1484
|
+
};
|
|
1485
|
+
/**
|
|
1486
|
+
* The OTA's device-class law: what an unregistered OTA gets, and what a registered one falls back
|
|
1487
|
+
* to when its entry supplies no model. Terminals bind by role -- see `otaTerminalBinding`.
|
|
1488
|
+
*/
|
|
1489
|
+
function otaDeviceClassLaw(device) {
|
|
1490
|
+
return {
|
|
1491
|
+
outcome: "law",
|
|
1492
|
+
device: device.id,
|
|
1493
|
+
law: {
|
|
1494
|
+
kind: "ota",
|
|
1495
|
+
transconductance: device.parameters.transconductance ?? 1e-3,
|
|
1496
|
+
saturationCurrent: device.parameters.saturationCurrent,
|
|
1497
|
+
thermalVoltage: device.parameters.thermalVoltage,
|
|
1498
|
+
...otaTerminalBinding(device),
|
|
1499
|
+
},
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* An OTA's four signal terminals from its **declared** electrodes, or null when it does not
|
|
1504
|
+
* declare the three that decide the binding.
|
|
1505
|
+
*
|
|
1506
|
+
* Shared by `otaTerminalBinding` and `otaPackageOrderAssumed` so the resolver and the warning
|
|
1507
|
+
* cannot disagree about whether a document stated its pins.
|
|
1508
|
+
*/
|
|
1509
|
+
function otaDeclaredBinding(device) {
|
|
1510
|
+
const declared = device.identity.declaredTerminalRoles;
|
|
1511
|
+
const declaredIndex = (role) => {
|
|
1512
|
+
const found = declared.findIndex((value) => value === role);
|
|
1513
|
+
return found === -1 ? null : found;
|
|
1514
|
+
};
|
|
1515
|
+
const plus = declaredIndex("nonInverting");
|
|
1516
|
+
const minus = declaredIndex("inverting");
|
|
1517
|
+
const output = declaredIndex("output");
|
|
1518
|
+
if (plus === null || minus === null || output === null) {
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
return {
|
|
1522
|
+
terminalIndices: { plus, minus, output, bias: declaredIndex("bias") },
|
|
1523
|
+
veeIndex: declaredIndex("supplyNegative") ?? device.parameters.veeIndex,
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* An OTA whose four signal connections come from the **DIP-8 package order** rather than from
|
|
1528
|
+
* anything the document states about its electrodes.
|
|
1529
|
+
*
|
|
1530
|
+
* Exported so `findUnreadableTerminalRoles` can report exactly the devices this fallback catches,
|
|
1531
|
+
* instead of mirroring its condition. A warning kept on a different test from the resolver it
|
|
1532
|
+
* guards is how a silent mis-wiring was introduced once already in this compiler, on a switch,
|
|
1533
|
+
* and the same shape would be available here: the fallback needs eight all-null *names*, and a
|
|
1534
|
+
* predicate that asked about declared roles instead would report a different set of devices.
|
|
1535
|
+
*
|
|
1536
|
+
* The assumption itself is sound and deliberate -- reading a role-less eight-terminal shell as its
|
|
1537
|
+
* package beats reading positions 0..3, which puts the output on pin 3. It is still an assumption
|
|
1538
|
+
* the source does not make, which is the whole reason it should say so out loud.
|
|
1539
|
+
*/
|
|
1540
|
+
export function otaPackageOrderAssumed(device) {
|
|
1541
|
+
// The declared branch of `otaTerminalBinding` decides before the package fallback is
|
|
1542
|
+
// reached, so a device that declares its electrodes assumes nothing and must not warn.
|
|
1543
|
+
// Shared with the resolver rather than restated: the first version of this predicate read
|
|
1544
|
+
// only the names, which meant a packet could declare every electrode correctly and still be
|
|
1545
|
+
// told its pins had been guessed.
|
|
1546
|
+
if (otaDeclaredBinding(device) !== null) {
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
const names = device.identity.terminalRoles;
|
|
1550
|
+
return names.length === 8 && names.every((role) => role === null);
|
|
1551
|
+
}
|
|
1552
|
+
function otaTerminalBinding(device) {
|
|
1553
|
+
// A declared electrode decides; a name only speaks where the document declares nothing.
|
|
1554
|
+
const fromDeclaration = otaDeclaredBinding(device);
|
|
1555
|
+
if (fromDeclaration !== null) {
|
|
1556
|
+
return fromDeclaration;
|
|
1557
|
+
}
|
|
1558
|
+
// **The name vocabularies that used to sit here are gone.** Five sets -- plus, minus,
|
|
1559
|
+
// output, bias and vee -- spelled 24 ways between them, and emptying all five left every
|
|
1560
|
+
// one of the 142 corpus programs byte-identical with all 15 terminal bindings standing.
|
|
1561
|
+
// The 10 OTAs that need a binding declare their electrodes; the other 6 OTA laws are
|
|
1562
|
+
// registry *sections*, whose terminals the registry already selected in law order.
|
|
1563
|
+
//
|
|
1564
|
+
// A device that names none of its terminals still states its package by declaring
|
|
1565
|
+
// eight of them: `terminalRoleToken` maps a bare `pin1` to null on purpose, so
|
|
1566
|
+
// `electro-harmonix-small-stone`'s CA3094 shells arrive role-less with their pins in
|
|
1567
|
+
// package order. Reading that order as the package pinout is an assumption, but a far
|
|
1568
|
+
// better founded one than reading positions 0..3, which put the output on pin 3.
|
|
1569
|
+
// The names are read only for their *absence* here: `pin1`..`pin8` folds to null, which is
|
|
1570
|
+
// what says "this shell states a package rather than a set of electrodes".
|
|
1571
|
+
const names = device.identity.terminalRoles;
|
|
1572
|
+
if (otaPackageOrderAssumed(device)) {
|
|
1573
|
+
return {
|
|
1574
|
+
terminalIndices: {
|
|
1575
|
+
plus: OTA_DIP8_POSITIONS.plus,
|
|
1576
|
+
minus: OTA_DIP8_POSITIONS.minus,
|
|
1577
|
+
output: OTA_DIP8_POSITIONS.output,
|
|
1578
|
+
bias: OTA_DIP8_POSITIONS.bias,
|
|
1579
|
+
},
|
|
1580
|
+
veeIndex: OTA_DIP8_POSITIONS.vee,
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
// Neither named nor a recognised package: leave the positional reading in place so a
|
|
1584
|
+
// registry `sections` entry, which has already mapped its own terminals, is unaffected.
|
|
1585
|
+
return { veeIndex: device.parameters.veeIndex };
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Law parameters a **registered part** contributes to a device the **source** split out of its
|
|
1589
|
+
* package.
|
|
1590
|
+
*
|
|
1591
|
+
* The two splits know different things and neither is sufficient alone. A registry `sections`
|
|
1592
|
+
* entry knows the part's numbers -- an M5218's 110 dB open-loop gain -- but decides the terminal
|
|
1593
|
+
* mapping by a positional *name* signature. A `devices:` declaration knows the terminals, because
|
|
1594
|
+
* the document states them and core scopes each role to one device, but says nothing about the
|
|
1595
|
+
* part. So the source decides the terminals and the registry decides the numbers.
|
|
1596
|
+
*
|
|
1597
|
+
* Measured before this existed: `boss-ch-1`'s M5218 split by declaration produced a byte-identical
|
|
1598
|
+
* topology -- same 151 stamps, same plus/minus/output nodes, same rails -- with `openLoopGain`
|
|
1599
|
+
* fallen from 316227.766 to the class default. That is the whole gap this closes.
|
|
1600
|
+
*
|
|
1601
|
+
* **Gated on `packageDeviceId`, so it cannot touch a device that reached here any other way.**
|
|
1602
|
+
* Widening the registry's reach over op-amps generally was tried on 2026-09-02 and measured wrong;
|
|
1603
|
+
* this is the opposite move, narrowed to devices whose split the source itself declared.
|
|
1604
|
+
*
|
|
1605
|
+
* Rails are excluded because they are the pedal's rather than the part's, which is the same
|
|
1606
|
+
* reason `sectionDevices` substitutes them. The device's own parameters win over these, so a
|
|
1607
|
+
* document that states a value keeps it.
|
|
1608
|
+
*/
|
|
1609
|
+
const LAW_KIND_BY_DEVICE_KIND = {
|
|
1610
|
+
opamp: "ideal-opamp",
|
|
1611
|
+
ota: "ota",
|
|
1612
|
+
optocoupler: "optocoupler",
|
|
1613
|
+
};
|
|
1614
|
+
function packageDeviceParameters(device, registry) {
|
|
1615
|
+
if (device.packageDeviceId === undefined) {
|
|
1616
|
+
return {};
|
|
1617
|
+
}
|
|
1618
|
+
const expected = LAW_KIND_BY_DEVICE_KIND[device.kind];
|
|
1619
|
+
if (expected === undefined) {
|
|
1620
|
+
return {};
|
|
1621
|
+
}
|
|
1622
|
+
// **Matched by part id alone, deliberately not through `identify`.** That function guards the
|
|
1623
|
+
// registry's *terminal-mapping* use: it refuses a device whose terminal count does not equal
|
|
1624
|
+
// the entry's pinout length, and it refuses an op-amp that does not need registry sections at
|
|
1625
|
+
// all. Both are right for what they protect and both are wrong here -- a device the source
|
|
1626
|
+
// split out of a package has a subset of the package's pins by construction (five of the
|
|
1627
|
+
// M5218's eight), and it is asking for the part's numbers rather than for its own wiring.
|
|
1628
|
+
const partNumber = device.identity.partNumber;
|
|
1629
|
+
if (partNumber === null) {
|
|
1630
|
+
return {};
|
|
1631
|
+
}
|
|
1632
|
+
const folded = foldPartId(partNumber);
|
|
1633
|
+
const entry = registry.entries.find((each) => each.partIds.some((id) => foldPartId(id) === folded));
|
|
1634
|
+
const model = entry?.model;
|
|
1635
|
+
if (model === undefined || model.kind !== "sections") {
|
|
1636
|
+
return {};
|
|
1637
|
+
}
|
|
1638
|
+
const section = model.sections.find((each) => each.law.kind === expected);
|
|
1639
|
+
if (section === undefined) {
|
|
1640
|
+
return {};
|
|
1641
|
+
}
|
|
1642
|
+
const parameters = {};
|
|
1643
|
+
for (const [key, value] of Object.entries(section.law)) {
|
|
1644
|
+
if (typeof value === "number" && key !== "railHigh" && key !== "railLow") {
|
|
1645
|
+
parameters[key] = value;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return parameters;
|
|
1649
|
+
}
|
|
1650
|
+
function resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, oneHopVoltage, netlist) {
|
|
1651
|
+
// A component declared as one transistor that is really several devices in one shell. Opened
|
|
1652
|
+
// rather than stamped, for the reason `isNonExecutableIcSupportShell` opens an IC shell: there
|
|
1653
|
+
// is no single device here to describe, and a transistor law would invent one. Reported by
|
|
1654
|
+
// `findMultiDeviceShells`, which runs over the same predicate.
|
|
1655
|
+
if (isMultiDeviceTransistorShell(device)) {
|
|
1656
|
+
return { outcome: "law", device: device.id, law: { kind: "open" } };
|
|
1657
|
+
}
|
|
1658
|
+
// A source that states a device class we have no law for. Opened, not stamped with the
|
|
1659
|
+
// nearest kind the format could carry; reported by `findUnimplementedDeviceLaws`.
|
|
1660
|
+
if (declaresUnimplementedDeviceLaw(device)) {
|
|
1661
|
+
return { outcome: "law", device: device.id, law: { kind: "open" } };
|
|
1662
|
+
}
|
|
1663
|
+
if (requiresIdentification(device)) {
|
|
1664
|
+
// **The second resolution key, consulted only where the first has nothing.** A declared
|
|
1665
|
+
// program never overrides a registered model: a fixed-function part is still identified
|
|
1666
|
+
// and still takes the law its part number resolves to. It is reached when the registry
|
|
1667
|
+
// has no executable model to give, which for a reprogrammable chip is always -- one
|
|
1668
|
+
// catalog entry keyed on the part number cannot hold the three different effects
|
|
1669
|
+
// `TC25SC080AU-104` is across this corpus.
|
|
1670
|
+
//
|
|
1671
|
+
// Deliberately *after* the three shell exits below. An isolated chip, a view-only source
|
|
1672
|
+
// boundary and a charge pump with a declared rail are all statements that this component
|
|
1673
|
+
// is not in the audio path at all, and a program declaration does not put it back.
|
|
1674
|
+
const declared = () => resolveDeclaredProgram(device, netlist);
|
|
1675
|
+
if (isNonExecutableIcSupportShell(device)) {
|
|
1676
|
+
return openIc(device, "source-boundary-shell");
|
|
1677
|
+
}
|
|
1678
|
+
if (isElectricallyIsolated(device, devicesTouchingNode)) {
|
|
1679
|
+
// See `isElectricallyIsolated` -- and the warning is emitted by
|
|
1680
|
+
// `findElectricallyIsolatedIcs`, which compile() runs over the same predicate.
|
|
1681
|
+
return openIc(device, "electrically-isolated");
|
|
1682
|
+
}
|
|
1683
|
+
if (isChargePumpWithDeclaredRail(device, nodeToVoltage, oneHopVoltage)) {
|
|
1684
|
+
// The rail the pump generates is separately declared as a voltage-carrying
|
|
1685
|
+
// port; the IC itself is a view-only structural anchor, not a solved model.
|
|
1686
|
+
return openIc(device, "charge-pump-declared-rail");
|
|
1687
|
+
}
|
|
1688
|
+
const identity = identify(device, registry);
|
|
1689
|
+
if (identity === null) {
|
|
1690
|
+
// **The message has to say which rung failed, because two different failures land
|
|
1691
|
+
// here and they call for opposite fixes.** A device with no part number at all needs a
|
|
1692
|
+
// registry entry or a pinout. A device that NAMES a part number which matched nothing
|
|
1693
|
+
// has given its strongest evidence and had it rejected -- and its declared class is
|
|
1694
|
+
// then not allowed to supply a macro, because a macro asserts a specific chip identity
|
|
1695
|
+
// that the unmatched part number cannot support. Saying "declared type ... matched"
|
|
1696
|
+
// failed would be wrong in that second case: the class matched and was refused.
|
|
1697
|
+
const named = device.identity.partNumber?.trim();
|
|
1698
|
+
const fromProgram = declared();
|
|
1699
|
+
if (fromProgram !== null)
|
|
1700
|
+
return fromProgram;
|
|
1701
|
+
return openWhereClassPermits(device, named !== undefined && named !== ""
|
|
1702
|
+
? `integrated circuit has no model: its part number "${named}" matched no registry entry, and a declared class may not supply a macro for a part whose identity is unresolved`
|
|
1703
|
+
: "integrated circuit has no model: no registered part id, declared type or pinout signature matched");
|
|
1704
|
+
}
|
|
1705
|
+
const model = registryModelFor(registry, identity, device.nodes.length);
|
|
1706
|
+
if (model === null) {
|
|
1707
|
+
const fromProgram = declared();
|
|
1708
|
+
if (fromProgram !== null)
|
|
1709
|
+
return fromProgram;
|
|
1710
|
+
return openWhereClassPermits(device, `part ${identity.partId} was identified but the registry supplies no model`);
|
|
1711
|
+
}
|
|
1712
|
+
if (model.kind === "law") {
|
|
1713
|
+
// A registered `open` is the registry *stating* that this part has no executable
|
|
1714
|
+
// model, which is a different claim from every other law it can supply and the only
|
|
1715
|
+
// one whose consequence -- a component in the source and not in the program -- a
|
|
1716
|
+
// reader has to be told about.
|
|
1717
|
+
if (model.law.kind === "open") {
|
|
1718
|
+
const fromProgram = declared();
|
|
1719
|
+
if (fromProgram !== null)
|
|
1720
|
+
return fromProgram;
|
|
1721
|
+
return openIc(device, "registry-open");
|
|
1722
|
+
}
|
|
1723
|
+
return { outcome: "law", device: device.id, law: model.law };
|
|
1724
|
+
}
|
|
1725
|
+
if (model.kind === "macro") {
|
|
1726
|
+
return resolveMacro(device, identity.partId, model.macro, registry, netlist);
|
|
1727
|
+
}
|
|
1728
|
+
// Expanded by `sectionDevices` before this runs, so reaching here is a compiler defect
|
|
1729
|
+
// rather than a source or registry one.
|
|
1730
|
+
return {
|
|
1731
|
+
outcome: "unsupported",
|
|
1732
|
+
device: device.id,
|
|
1733
|
+
reason: `part ${identity.partId} is multi-section and was not expanded`,
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
// A registered part's numbers reach a source-declared device here; see
|
|
1737
|
+
// `packageDeviceParameters`. A no-op for every device that did not come from a `devices:`
|
|
1738
|
+
// declaration, which today is all of them.
|
|
1739
|
+
const fromRegistry = packageDeviceParameters(device, registry);
|
|
1740
|
+
if (Object.keys(fromRegistry).length > 0) {
|
|
1741
|
+
device = {
|
|
1742
|
+
...device,
|
|
1743
|
+
parameters: { ...fromRegistry, ...device.parameters },
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
switch (device.kind) {
|
|
1747
|
+
case "resistor": {
|
|
1748
|
+
// `parametersFor` already refuses a resistor that declares no resistance at all
|
|
1749
|
+
// (`netlist.ts`'s `component ... resistor has no Resistance`), so reaching here with
|
|
1750
|
+
// `ohms === 0` means the source stated exactly zero, not that it stated nothing.
|
|
1751
|
+
//
|
|
1752
|
+
// `mxr-noise-gate-line-driver`'s `R_U1B_FB` is `Resistance: { raw: "0 Ohm", value: 0,
|
|
1753
|
+
// unit: "Ω" }`, described in the same document as "Direct feedback wire from U1B
|
|
1754
|
+
// output to negative input represented as a zero-ohm link" -- a schematic-capture
|
|
1755
|
+
// convention for an ideal wire, not a missing or invalid value. `1 / 0` is not
|
|
1756
|
+
// representable as a conductance, so this reuses `SWITCH_ON_OHMS`, the same
|
|
1757
|
+
// electrically-indistinguishable-from-ideal short this file already stamps for a
|
|
1758
|
+
// closed switch, rather than refusing a resistance the source did in fact state.
|
|
1759
|
+
const ohms = device.parameters.ohms ?? 0;
|
|
1760
|
+
if (ohms === 0) {
|
|
1761
|
+
return {
|
|
1762
|
+
outcome: "law",
|
|
1763
|
+
device: device.id,
|
|
1764
|
+
law: { kind: "conductance", siemens: 1 / SWITCH_ON_OHMS },
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
if (!(ohms > 0)) {
|
|
1768
|
+
return unsupported(device, "resistor has a non-positive resistance");
|
|
1769
|
+
}
|
|
1770
|
+
return {
|
|
1771
|
+
outcome: "law",
|
|
1772
|
+
device: device.id,
|
|
1773
|
+
law: { kind: "conductance", siemens: 1 / ohms },
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
case "potentiometer": {
|
|
1777
|
+
const ohms = device.parameters.ohms ?? 0;
|
|
1778
|
+
if (!(ohms > 0)) {
|
|
1779
|
+
return unsupported(device, "potentiometer has a non-positive resistance");
|
|
1780
|
+
}
|
|
1781
|
+
if (device.control === null) {
|
|
1782
|
+
return unsupported(device, "potentiometer is bound to no control");
|
|
1783
|
+
}
|
|
1784
|
+
// Declared-only end resistance, and it has to fit inside the track twice: both
|
|
1785
|
+
// legs carry one, and a residual at or above half the track would leave the
|
|
1786
|
+
// wiper no travel at all (or invert it). Refused rather than clamped, because a
|
|
1787
|
+
// residual that large is a source error, not a value to quietly reinterpret.
|
|
1788
|
+
const declaredResidual = device.parameters.minOhms ?? 0;
|
|
1789
|
+
const residualOhms = declaredResidual > 0 && declaredResidual < ohms / 2
|
|
1790
|
+
? declaredResidual
|
|
1791
|
+
: 0;
|
|
1792
|
+
// The wiper element this device represents is decided at stamp time from
|
|
1793
|
+
// terminal order; the law carries the whole track plus its taper.
|
|
1794
|
+
return {
|
|
1795
|
+
outcome: "law",
|
|
1796
|
+
device: device.id,
|
|
1797
|
+
law: {
|
|
1798
|
+
kind: "controlled-conductance",
|
|
1799
|
+
control: device.control,
|
|
1800
|
+
// Falls back to "linear" only for a control id `netlist.controls` never
|
|
1801
|
+
// carried -- unreachable for a real compiled netlist, since every device
|
|
1802
|
+
// bound to a control puts that id there (`netlist.ts`'s `controlIds`).
|
|
1803
|
+
taper: taperByControl.get(device.control) ?? "linear",
|
|
1804
|
+
totalOhms: ohms,
|
|
1805
|
+
side: "upper",
|
|
1806
|
+
residualOhms,
|
|
1807
|
+
},
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
case "rheostat": {
|
|
1811
|
+
const maxOhms = device.parameters.maxOhms ?? 0;
|
|
1812
|
+
if (!(maxOhms > 0)) {
|
|
1813
|
+
return unsupported(device, "rheostat has a non-positive resistance");
|
|
1814
|
+
}
|
|
1815
|
+
if (device.control === null) {
|
|
1816
|
+
return unsupported(device, "rheostat is bound to no control");
|
|
1817
|
+
}
|
|
1818
|
+
// A residual minimum is normal -- a pot used as a rheostat rarely reaches
|
|
1819
|
+
// zero -- but it must be below the track, or the sweep runs backwards.
|
|
1820
|
+
const declaredMin = device.parameters.minOhms ?? 0;
|
|
1821
|
+
const minOhms = declaredMin > 0 && declaredMin < maxOhms ? declaredMin : 0;
|
|
1822
|
+
return {
|
|
1823
|
+
outcome: "law",
|
|
1824
|
+
device: device.id,
|
|
1825
|
+
law: {
|
|
1826
|
+
kind: "controlled-resistance",
|
|
1827
|
+
control: device.control,
|
|
1828
|
+
// Same fallback note as the pot law above: unreachable in practice, since
|
|
1829
|
+
// `netlist.controls` always carries an entry for a bound control's id.
|
|
1830
|
+
taper: taperByControl.get(device.control) ?? "linear",
|
|
1831
|
+
minOhms,
|
|
1832
|
+
maxOhms,
|
|
1833
|
+
},
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
case "capacitor": {
|
|
1837
|
+
const farads = device.parameters.farads ?? 0;
|
|
1838
|
+
if (!(farads > 0)) {
|
|
1839
|
+
return unsupported(device, "capacitor has a non-positive capacitance");
|
|
1840
|
+
}
|
|
1841
|
+
// Declared-only DC leakage; 0 means ideal, and ideal is the diagnostic default.
|
|
1842
|
+
// `InsulationResistance` is already a resistance. The electrolytic form is the
|
|
1843
|
+
// datasheet's leakage current at rated voltage read as V/I -- a linear stand-in
|
|
1844
|
+
// for a current that in the physical part grows sub-linearly below the rating,
|
|
1845
|
+
// so at a working voltage under the rating this slightly overstates the leak.
|
|
1846
|
+
const insulationOhms = device.parameters.insulationOhms ?? 0;
|
|
1847
|
+
const leakageAmps = device.parameters.leakageAmps ?? 0;
|
|
1848
|
+
const ratedVolts = device.parameters.ratedVolts ?? 0;
|
|
1849
|
+
const leakageSiemens = insulationOhms > 0
|
|
1850
|
+
? 1 / insulationOhms
|
|
1851
|
+
: leakageAmps > 0 && ratedVolts > 0
|
|
1852
|
+
? leakageAmps / ratedVolts
|
|
1853
|
+
: 0;
|
|
1854
|
+
return {
|
|
1855
|
+
outcome: "law",
|
|
1856
|
+
device: device.id,
|
|
1857
|
+
law: { kind: "capacitance", farads, leakageSiemens },
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
case "inductor": {
|
|
1861
|
+
// A ferrite bead spec'd by impedance-vs-frequency (e.g. "100 Ω @ 100 MHz")
|
|
1862
|
+
// carries no inductance in the source. At audio frequencies it is a small
|
|
1863
|
+
// inductor well below self-resonance; 10 µH matches the corpus' only
|
|
1864
|
+
// decoded bead (Boss DD-3a DSS306 "103" = 10 µH).
|
|
1865
|
+
const henries = device.parameters.henries ?? FERRITE_BEAD_DEFAULT_HENRIES;
|
|
1866
|
+
return {
|
|
1867
|
+
outcome: "law",
|
|
1868
|
+
device: device.id,
|
|
1869
|
+
law: {
|
|
1870
|
+
kind: "inductance",
|
|
1871
|
+
henries,
|
|
1872
|
+
...windingResistance(device),
|
|
1873
|
+
},
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
case "diode": {
|
|
1877
|
+
// **A catalogued tube rectifier is not a semiconductor.** Eight of the corpus's eleven
|
|
1878
|
+
// tube rectifiers are declared `kind: diode` while naming a tube part, and a vacuum
|
|
1879
|
+
// rectifier drops tens of volts where silicon drops less than one -- which sets the
|
|
1880
|
+
// amp's whole B+. Resolved by **exact whole part id** against the catalog, which is
|
|
1881
|
+
// admissible evidence; this is not the string inference the comment below rules out.
|
|
1882
|
+
// That one is about reading a *parameter* out of a part number's shape (`1N4733` versus
|
|
1883
|
+
// `1N4148`); this is a closed list of ids compared whole.
|
|
1884
|
+
const tubeRectifier = registryLawFor(registry, device.identity.partNumber, "tube-diode");
|
|
1885
|
+
if (tubeRectifier !== null) {
|
|
1886
|
+
return { outcome: "law", device: device.id, law: tubeRectifier };
|
|
1887
|
+
}
|
|
1888
|
+
const catalogued = registryLawFor(registry, device.identity.partNumber, "diode");
|
|
1889
|
+
if (catalogued !== null) {
|
|
1890
|
+
// Declared parameters win over the catalog, every one of them: the entries
|
|
1891
|
+
// exist to fill absence, not to overrule a packet that states the value.
|
|
1892
|
+
// A zener entry's forward parameters are avowed class defaults, so a source
|
|
1893
|
+
// that types its own saturation current or series resistance keeps it, and
|
|
1894
|
+
// a device the source declares as an LED stays one (`boss-ph-1r` declares
|
|
1895
|
+
// 11 V where the registry's RD11EB3 row says 10.99, and keeps that too).
|
|
1896
|
+
if (catalogued.kind === "diode") {
|
|
1897
|
+
const declared = device.parameters;
|
|
1898
|
+
if (declared.breakdownVolts !== undefined ||
|
|
1899
|
+
declared.saturationCurrent !== undefined ||
|
|
1900
|
+
declared.seriesResistance !== undefined ||
|
|
1901
|
+
device.isLed === true) {
|
|
1902
|
+
return {
|
|
1903
|
+
outcome: "law",
|
|
1904
|
+
device: device.id,
|
|
1905
|
+
law: {
|
|
1906
|
+
...catalogued,
|
|
1907
|
+
breakdownVolts: declared.breakdownVolts ?? catalogued.breakdownVolts,
|
|
1908
|
+
saturationCurrent: declared.saturationCurrent ?? catalogued.saturationCurrent,
|
|
1909
|
+
seriesResistance: declared.seriesResistance ?? catalogued.seriesResistance,
|
|
1910
|
+
isLed: device.isLed === true ? true : catalogued.isLed,
|
|
1911
|
+
},
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return { outcome: "law", device: device.id, law: catalogued };
|
|
1916
|
+
}
|
|
1917
|
+
const isLed = device.isLed === true;
|
|
1918
|
+
return {
|
|
1919
|
+
outcome: "law",
|
|
1920
|
+
device: device.id,
|
|
1921
|
+
law: {
|
|
1922
|
+
kind: "diode",
|
|
1923
|
+
// If it is an LED, use physical LED defaults (higher forward knee ~1.8V to 2.2V),
|
|
1924
|
+
// otherwise use class defaults (1N4148-ish silicon signal diode).
|
|
1925
|
+
saturationCurrent: device.parameters.saturationCurrent ?? (isLed ? 1e-12 : 2.52e-9),
|
|
1926
|
+
emissionCoefficient: 1.752,
|
|
1927
|
+
thermalVoltage: device.parameters.thermalVoltage ?? (isLed ? 0.05 : THERMAL_VOLTAGE),
|
|
1928
|
+
// 0 for an ordinary diode. Read from the source, never inferred from a part
|
|
1929
|
+
// number: `1N4733` is a 5.1 V zener and `1N4148` is not, and telling them apart
|
|
1930
|
+
// from the string is the prose matching the engineering principles forbid.
|
|
1931
|
+
breakdownVolts: device.parameters.breakdownVolts ?? 0,
|
|
1932
|
+
seriesResistance: device.parameters.seriesResistance ?? (isLed ? 2.0 : 1.0),
|
|
1933
|
+
isLed,
|
|
1934
|
+
},
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
case "triode":
|
|
1938
|
+
if (device.nodes.length !== 3) {
|
|
1939
|
+
return unsupported(device, `triode has ${device.nodes.length} terminals, not three`);
|
|
1940
|
+
}
|
|
1941
|
+
// The registry refines the class default, exactly as `GENERIC_TRIODE`'s own note
|
|
1942
|
+
// says it should. Exact part number only -- see `registryLawFor`.
|
|
1943
|
+
return {
|
|
1944
|
+
outcome: "law",
|
|
1945
|
+
device: device.id,
|
|
1946
|
+
law: registryLawFor(registry, device.identity.partNumber, "triode") ?? {
|
|
1947
|
+
kind: "triode",
|
|
1948
|
+
...GENERIC_TRIODE,
|
|
1949
|
+
},
|
|
1950
|
+
};
|
|
1951
|
+
case "pentode":
|
|
1952
|
+
// Four signal electrodes minimum. 8 of the corpus's 56 add a suppressor and 4 add
|
|
1953
|
+
// a heater as well; both are read and ignored by the lowering, because a
|
|
1954
|
+
// suppressor is tied to the cathode in practice and a heater carries no signal.
|
|
1955
|
+
if (device.nodes.length < 4) {
|
|
1956
|
+
return unsupported(device, `pentode has ${device.nodes.length} terminals, fewer than the four electrodes its law needs`);
|
|
1957
|
+
}
|
|
1958
|
+
return {
|
|
1959
|
+
outcome: "law",
|
|
1960
|
+
device: device.id,
|
|
1961
|
+
law: registryLawFor(registry, device.identity.partNumber, "pentode") ?? {
|
|
1962
|
+
kind: "pentode",
|
|
1963
|
+
...GENERIC_PENTODE,
|
|
1964
|
+
},
|
|
1965
|
+
};
|
|
1966
|
+
case "tube-diode":
|
|
1967
|
+
// Two electrodes minimum. The corpus's three are all five-terminal duals -- two
|
|
1968
|
+
// plates, a shared cathode and two heaters -- and how many elements that becomes is
|
|
1969
|
+
// the lowering's question, not this one.
|
|
1970
|
+
if (device.nodes.length < 2) {
|
|
1971
|
+
return unsupported(device, `tube diode has ${device.nodes.length} terminals, fewer than a plate and a cathode`);
|
|
1972
|
+
}
|
|
1973
|
+
return {
|
|
1974
|
+
outcome: "law",
|
|
1975
|
+
device: device.id,
|
|
1976
|
+
law: registryLawFor(registry, device.identity.partNumber, "tube-diode") ?? {
|
|
1977
|
+
kind: "tube-diode",
|
|
1978
|
+
...GENERIC_TUBE_DIODE,
|
|
1979
|
+
},
|
|
1980
|
+
};
|
|
1981
|
+
case "opamp": {
|
|
1982
|
+
const declaredDrop = registryOpampSwingDrop(registry, device.identity.partNumber);
|
|
1983
|
+
const declaredRails = railsAfterSwingDrop(opampRails(device, rails, nodeToVoltage), declaredDrop);
|
|
1984
|
+
const railHigh = declaredRails.high;
|
|
1985
|
+
const railLow = declaredRails.low;
|
|
1986
|
+
return {
|
|
1987
|
+
outcome: "law",
|
|
1988
|
+
device: device.id,
|
|
1989
|
+
law: {
|
|
1990
|
+
kind: "ideal-opamp",
|
|
1991
|
+
railHigh,
|
|
1992
|
+
railLow,
|
|
1993
|
+
openLoopGain: device.parameters.openLoopGain ?? DEFAULT_OPEN_LOOP_GAIN,
|
|
1994
|
+
...(declaredDrop === null ? {} : { outputSwingDropVolts: declaredDrop }),
|
|
1995
|
+
},
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
case "jack":
|
|
1999
|
+
return portEngage(device, supply);
|
|
2000
|
+
case "ground":
|
|
2001
|
+
// Interface and reference symbols carry no element of their own.
|
|
2002
|
+
return { outcome: "law", device: device.id, law: { kind: "open" } };
|
|
2003
|
+
case "rail": {
|
|
2004
|
+
// A rail is not an interface symbol. It **asserts a potential** at its node,
|
|
2005
|
+
// and 84 of the corpus's 90 declare that voltage. Treating it as `open`
|
|
2006
|
+
// deleted the supply: `ibanez-ts808` states its whole 9 V supply as one rail,
|
|
2007
|
+
// so every node sat at 0 V, no bias network biased anything, and the pedal
|
|
2008
|
+
// rendered leakage. A rail that declares no voltage is a named support shell
|
|
2009
|
+
// with nothing to assert, and stays open.
|
|
2010
|
+
const volts = device.parameters.volts;
|
|
2011
|
+
if (volts === undefined || !Number.isFinite(volts)) {
|
|
2012
|
+
return { outcome: "law", device: device.id, law: { kind: "open" } };
|
|
2013
|
+
}
|
|
2014
|
+
return {
|
|
2015
|
+
outcome: "law",
|
|
2016
|
+
device: device.id,
|
|
2017
|
+
law: { kind: "voltage-source", volts, sourceOhms: SUPPLY_SOURCE_OHMS },
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
case "switch": {
|
|
2021
|
+
// A few amp packets encode the tremolo optocoupler shell as a three-terminal
|
|
2022
|
+
// `kind: switch` (`lampdrive`, `ldrsignal`, `ldrreturn`). Treat that exact
|
|
2023
|
+
// typed role set as the operator it is, not as a routed selector.
|
|
2024
|
+
if (hasExactRoleSet(device, ["lampdrive", "ldrsignal", "ldrreturn"])) {
|
|
2025
|
+
return {
|
|
2026
|
+
outcome: "law",
|
|
2027
|
+
device: device.id,
|
|
2028
|
+
law: {
|
|
2029
|
+
kind: "optocoupler",
|
|
2030
|
+
ledThresholdVolts: device.parameters.ledThresholdVolts ?? 1.6,
|
|
2031
|
+
ledTransconductance: device.parameters.ledTransconductance ?? 1e-3,
|
|
2032
|
+
ldrMinOhms: device.parameters.ldrMinOhms ?? 100,
|
|
2033
|
+
ldrMaxOhms: device.parameters.ldrMaxOhms ?? 1e7,
|
|
2034
|
+
ldrPowerLawCoefficientOhms: device.parameters.ldrPowerLawCoefficientOhms,
|
|
2035
|
+
ldrPowerLawExponent: device.parameters.ldrPowerLawExponent,
|
|
2036
|
+
},
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
if (device.control === null) {
|
|
2040
|
+
const position = device.parameters.position ?? 1;
|
|
2041
|
+
if (device.nodes.length > 2) {
|
|
2042
|
+
return {
|
|
2043
|
+
outcome: "law",
|
|
2044
|
+
device: device.id,
|
|
2045
|
+
law: {
|
|
2046
|
+
kind: "fixed-selector",
|
|
2047
|
+
position,
|
|
2048
|
+
onOhms: SWITCH_ON_OHMS,
|
|
2049
|
+
offOhms: SWITCH_OFF_OHMS,
|
|
2050
|
+
},
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
const isClosed = position >= 0.5;
|
|
2054
|
+
return {
|
|
2055
|
+
outcome: "law",
|
|
2056
|
+
device: device.id,
|
|
2057
|
+
law: {
|
|
2058
|
+
kind: "conductance",
|
|
2059
|
+
siemens: isClosed ? 1 / SWITCH_ON_OHMS : 1 / SWITCH_OFF_OHMS,
|
|
2060
|
+
},
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
// Two terminals make or break; three or more route. 84 of the corpus's 118
|
|
2064
|
+
// wired switches have three or more, and stamping only the first two threw
|
|
2065
|
+
// every other throw away -- a selector silently reduced to a two-terminal
|
|
2066
|
+
// short, which is how a bypass switch ends up rendering the wrong path.
|
|
2067
|
+
return {
|
|
2068
|
+
outcome: "law",
|
|
2069
|
+
device: device.id,
|
|
2070
|
+
law: {
|
|
2071
|
+
kind: device.nodes.length > 2 ? "selector" : "switch",
|
|
2072
|
+
control: device.control,
|
|
2073
|
+
onOhms: SWITCH_ON_OHMS,
|
|
2074
|
+
offOhms: SWITCH_OFF_OHMS,
|
|
2075
|
+
},
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
case "voltage-source": {
|
|
2079
|
+
const volts = device.parameters.volts ?? 0;
|
|
2080
|
+
// **A frequency is what makes this an AC source**, and the discriminator is the
|
|
2081
|
+
// presence of the parameter rather than anything in the packet's prose. Stage 1
|
|
2082
|
+
// writes `frequency` only when the component declares a typed `Frequency`, and a
|
|
2083
|
+
// battery cannot declare one, so a mains inlet and a 9 V cell are distinguishable by
|
|
2084
|
+
// schema shape alone. Before this, 21 of the two corpora's 122 supplies were AC by
|
|
2085
|
+
// their own text and every one of them compiled into a DC battery -- silently, which
|
|
2086
|
+
// is the failure class this pipeline refuses hardest.
|
|
2087
|
+
//
|
|
2088
|
+
// A declared **0 Hz is DC**, which is a physical reading rather than a guard: a
|
|
2089
|
+
// zero-frequency sine is a constant, and `amplitude * sin(0)` would hold the rail at
|
|
2090
|
+
// 0 V forever -- a wrong answer that renders. A negative or non-finite frequency is
|
|
2091
|
+
// not a waveform and falls through the same way.
|
|
2092
|
+
const frequency = device.parameters.frequency;
|
|
2093
|
+
if (frequency !== undefined &&
|
|
2094
|
+
Number.isFinite(frequency) &&
|
|
2095
|
+
frequency > 0) {
|
|
2096
|
+
// **Decided 2026-08-14: a typed AC-source magnitude means RMS.** This is a
|
|
2097
|
+
// convention this project declares, not a prose read -- the law never inspects
|
|
2098
|
+
// `raw`. A supply states `{raw: "120 VAC RMS assumed nominal", value: 120, unit:
|
|
2099
|
+
// "V"}`: the typed part is a bare magnitude, the word "RMS" lives only in `raw`,
|
|
2100
|
+
// and no stage may read it. Mains is quoted RMS by universal convention, and
|
|
2101
|
+
// every amp packet's prose agrees, so treating the typed magnitude as RMS is the
|
|
2102
|
+
// reading consistent with what these packets actually mean -- unlike the
|
|
2103
|
+
// previous reading (typed magnitude taken directly as peak), which put every
|
|
2104
|
+
// B+ figure from a mains inlet about 41% (`sqrt(2)`) high.
|
|
2105
|
+
//
|
|
2106
|
+
// So this law is where the declared RMS magnitude is converted to the peak
|
|
2107
|
+
// amplitude `AcSourceLaw.amplitudeVolts` states and the sine evaluation (here and
|
|
2108
|
+
// in the runtime) needs: `amplitudeVolts = volts * sqrt(2)`. Both consumers of
|
|
2109
|
+
// this field -- the runtime's `sin()` evaluation and the ngspice deck's `SIN`
|
|
2110
|
+
// amplitude parameter, which is itself peak -- read `amplitudeVolts` as peak, so
|
|
2111
|
+
// converting once here keeps them in agreement by construction rather than by
|
|
2112
|
+
// each independently reproducing the same `sqrt(2)`.
|
|
2113
|
+
//
|
|
2114
|
+
// **Escape hatch, unused today:** this conversion is unconditional because the
|
|
2115
|
+
// schema has no typed field distinguishing an RMS-declared supply from a
|
|
2116
|
+
// peak-declared one -- every AC `voltage-source` in both corpora is read as RMS.
|
|
2117
|
+
// If a packet is ever authored to mean peak, the source must say so through a new
|
|
2118
|
+
// typed field (there is none yet, so nothing exercises this branch); this law is
|
|
2119
|
+
// where that field would be read and the conversion skipped.
|
|
2120
|
+
return {
|
|
2121
|
+
outcome: "law",
|
|
2122
|
+
device: device.id,
|
|
2123
|
+
law: {
|
|
2124
|
+
kind: "ac-source",
|
|
2125
|
+
amplitudeVolts: volts * AC_SOURCE_RMS_TO_PEAK,
|
|
2126
|
+
frequencyHz: frequency,
|
|
2127
|
+
sourceOhms: SUPPLY_SOURCE_OHMS,
|
|
2128
|
+
},
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
return {
|
|
2132
|
+
outcome: "law",
|
|
2133
|
+
device: device.id,
|
|
2134
|
+
law: { kind: "voltage-source", volts, sourceOhms: SUPPLY_SOURCE_OHMS },
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
case "bjt":
|
|
2138
|
+
return {
|
|
2139
|
+
outcome: "law",
|
|
2140
|
+
device: device.id,
|
|
2141
|
+
law: {
|
|
2142
|
+
kind: "bjt",
|
|
2143
|
+
polarity: device.parameters.pnp === 1 ? "pnp" : "npn",
|
|
2144
|
+
saturationCurrent: device.parameters.saturationCurrent ?? SILICON_SATURATION_CURRENT,
|
|
2145
|
+
forwardBeta: device.parameters.beta ?? SILICON_FORWARD_BETA,
|
|
2146
|
+
reverseBeta: device.parameters.reverseBeta ?? 1,
|
|
2147
|
+
thermalVoltage: THERMAL_VOLTAGE,
|
|
2148
|
+
// Absent for every silicon part in the corpus, and for the one germanium
|
|
2149
|
+
// packet whose `LeakageCurrent` is a sentence rather than a value.
|
|
2150
|
+
leakageAmps: device.parameters.leakageAmps ?? 0,
|
|
2151
|
+
},
|
|
2152
|
+
};
|
|
2153
|
+
case "jfet": {
|
|
2154
|
+
// A registered part refines the class gate, exactly as `GENERIC_TUBE_GRID`'s
|
|
2155
|
+
// note says a registry entry should. Exact part number only -- see
|
|
2156
|
+
// `registryLawFor`. The refinement is the gate term alone: the channel
|
|
2157
|
+
// stays the declared-or-class law byte-for-byte (rank-dependent, and the
|
|
2158
|
+
// database beta disagrees with at least one packet's record), so a covered
|
|
2159
|
+
// part's channel is indistinguishable from an unregistered one's. Declared
|
|
2160
|
+
// Vt0/Beta win over everything, every time: the entries exist to fill
|
|
2161
|
+
// absence, not to overrule a packet that states the value (the same overlay
|
|
2162
|
+
// the diode case applies). Unregistered parts keep the class default
|
|
2163
|
+
// bit-identically, including the microamp gate clamp that `boss-cs-2` Q2
|
|
2164
|
+
// and `boss-ds-2` Q15 lean on -- a global change there would trade their
|
|
2165
|
+
// readable operating points for the switch fix.
|
|
2166
|
+
const catalogued = registryLawFor(registry, device.identity.partNumber, "fet");
|
|
2167
|
+
const entry = catalogued !== null && catalogued.kind === "fet" ? catalogued : null;
|
|
2168
|
+
return {
|
|
2169
|
+
outcome: "law",
|
|
2170
|
+
device: device.id,
|
|
2171
|
+
law: {
|
|
2172
|
+
kind: "fet",
|
|
2173
|
+
channel: device.parameters.pChannel === 1 ? "p" : "n",
|
|
2174
|
+
// A JFET is depletion mode: it conducts at zero gate bias and pinches
|
|
2175
|
+
// off as the gate goes negative, so the threshold is negative.
|
|
2176
|
+
thresholdVolts: device.parameters.thresholdVolts ?? JFET_DEFAULT_THRESHOLD_VOLTS,
|
|
2177
|
+
transconductance: device.parameters.transconductance ?? JFET_DEFAULT_TRANSCONDUCTANCE,
|
|
2178
|
+
channelLengthModulation: 0,
|
|
2179
|
+
// Subthreshold conduction (see the `fet` law docs in types.ts): the
|
|
2180
|
+
// gradual region a knee-biased JFET actually sweeps in. MOSFETs
|
|
2181
|
+
// keep 0 below.
|
|
2182
|
+
subthresholdVolts: 0.07,
|
|
2183
|
+
// A JFET gate is a silicon PN junction, so it conducts once forward-biased.
|
|
2184
|
+
// Saturation current and scale are **reused from the triode grid**, which
|
|
2185
|
+
// `compact-mna-dynamic-triode-cell` reports 0 Newton failures with; the onset
|
|
2186
|
+
// is moved from a tube grid's 0.05 V to a silicon junction's 0.5 V. The
|
|
2187
|
+
// class default is not measured against any specific JFET: a registered
|
|
2188
|
+
// part refines the current through its catalog entry, and the
|
|
2189
|
+
// unregistered default stays exactly what it was.
|
|
2190
|
+
gateSaturationCurrent: entry?.gateSaturationCurrent ?? 1.0e-5,
|
|
2191
|
+
gateOnsetVolts: 0.5,
|
|
2192
|
+
gateScaleVolts: 0.06,
|
|
2193
|
+
},
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
case "mosfet":
|
|
2197
|
+
return {
|
|
2198
|
+
outcome: "law",
|
|
2199
|
+
device: device.id,
|
|
2200
|
+
law: {
|
|
2201
|
+
kind: "fet",
|
|
2202
|
+
channel: device.parameters.pChannel === 1 ? "p" : "n",
|
|
2203
|
+
thresholdVolts: device.parameters.thresholdVolts ?? 2,
|
|
2204
|
+
transconductance: device.parameters.transconductance ?? 1e-3,
|
|
2205
|
+
channelLengthModulation: 0,
|
|
2206
|
+
// Insulated gate: no subthreshold region is modelled, behavior unchanged.
|
|
2207
|
+
subthresholdVolts: 0,
|
|
2208
|
+
// An insulated gate draws no current at any bias. Zero, deliberately: this
|
|
2209
|
+
// is what the law did for every FET before the JFET junction was added.
|
|
2210
|
+
gateSaturationCurrent: 0,
|
|
2211
|
+
gateOnsetVolts: 0,
|
|
2212
|
+
gateScaleVolts: 1,
|
|
2213
|
+
},
|
|
2214
|
+
};
|
|
2215
|
+
case "ota":
|
|
2216
|
+
return otaDeviceClassLaw(device);
|
|
2217
|
+
case "inverter":
|
|
2218
|
+
return {
|
|
2219
|
+
outcome: "law",
|
|
2220
|
+
device: device.id,
|
|
2221
|
+
law: {
|
|
2222
|
+
kind: "inverter",
|
|
2223
|
+
transconductance: device.parameters.transconductance ?? 5e-3,
|
|
2224
|
+
biasVolts: device.parameters.biasVolts ?? 4.5,
|
|
2225
|
+
thresholdVolts: device.parameters.thresholdVolts ?? 2.0,
|
|
2226
|
+
},
|
|
2227
|
+
};
|
|
2228
|
+
case "nand-gate":
|
|
2229
|
+
return {
|
|
2230
|
+
outcome: "law",
|
|
2231
|
+
device: device.id,
|
|
2232
|
+
law: {
|
|
2233
|
+
kind: "nand-gate",
|
|
2234
|
+
thresholdVolts: device.parameters.thresholdVolts ?? 2.0,
|
|
2235
|
+
transconductance: device.parameters.transconductance ?? 1e-3,
|
|
2236
|
+
},
|
|
2237
|
+
};
|
|
2238
|
+
case "clock-driver":
|
|
2239
|
+
return {
|
|
2240
|
+
outcome: "law",
|
|
2241
|
+
device: device.id,
|
|
2242
|
+
law: {
|
|
2243
|
+
kind: "clock-driver",
|
|
2244
|
+
defaultFrequency: device.parameters.defaultFrequency,
|
|
2245
|
+
},
|
|
2246
|
+
};
|
|
2247
|
+
case "optocoupler":
|
|
2248
|
+
return {
|
|
2249
|
+
outcome: "law",
|
|
2250
|
+
device: device.id,
|
|
2251
|
+
law: {
|
|
2252
|
+
kind: "optocoupler",
|
|
2253
|
+
ledThresholdVolts: device.parameters.ledThresholdVolts ?? 1.6,
|
|
2254
|
+
ledTransconductance: device.parameters.ledTransconductance ?? 1e-3,
|
|
2255
|
+
ldrMinOhms: device.parameters.ldrMinOhms ?? 100,
|
|
2256
|
+
ldrMaxOhms: device.parameters.ldrMaxOhms ?? 1e7,
|
|
2257
|
+
ldrPowerLawCoefficientOhms: device.parameters.ldrPowerLawCoefficientOhms,
|
|
2258
|
+
ldrPowerLawExponent: device.parameters.ldrPowerLawExponent,
|
|
2259
|
+
},
|
|
2260
|
+
};
|
|
2261
|
+
case "transformer": {
|
|
2262
|
+
// A spring reverb tank arrives here because that is what its shell declares -- a
|
|
2263
|
+
// two-terminal-pair `Circuit.Transformer` with `InputImpedance`/`OutputImpedance`.
|
|
2264
|
+
// The interface is right and the behaviour is not: an ideal transformer is
|
|
2265
|
+
// memoryless, so a tank admitted as one is a wire with a turns ratio. An exact part
|
|
2266
|
+
// number is the evidence that separates the two, and it is the only evidence used
|
|
2267
|
+
// here -- never the shell's authored `SourceBoundaryRole` prose.
|
|
2268
|
+
const tankPartNumber = device.identity.partNumber;
|
|
2269
|
+
const tank = tankPartNumber === null
|
|
2270
|
+
? undefined
|
|
2271
|
+
: springReverbTanks[foldPartId(tankPartNumber)];
|
|
2272
|
+
if (tank !== undefined) {
|
|
2273
|
+
return {
|
|
2274
|
+
outcome: "law",
|
|
2275
|
+
device: device.id,
|
|
2276
|
+
law: {
|
|
2277
|
+
kind: "spring-reverb",
|
|
2278
|
+
// Source first: the packet's own declared pair wins over the part
|
|
2279
|
+
// code's published figures, which are the fallback for a tank whose
|
|
2280
|
+
// shell states no impedances.
|
|
2281
|
+
inputOhms: device.parameters.primaryImpedanceOhms ?? tank.inputOhms,
|
|
2282
|
+
outputOhms: device.parameters.secondaryImpedanceOhms ?? tank.outputOhms,
|
|
2283
|
+
delaySeconds: tank.delaySeconds,
|
|
2284
|
+
decaySeconds: tank.decaySeconds,
|
|
2285
|
+
// Cost/character tradeoff, not a measured device fact: enough allpass
|
|
2286
|
+
// sections to disperse audibly, few enough to stay inside the sample
|
|
2287
|
+
// budget. Stated plainly so it is not read as a datasheet number.
|
|
2288
|
+
dispersionStages: 16,
|
|
2289
|
+
},
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
// The source states a winding voltage in RMS, by the same project convention the
|
|
2293
|
+
// `ac-source` branch above records, and the conversion to peak happens here for the
|
|
2294
|
+
// same reason it happens there: one place, so the runtime's sine and the ngspice
|
|
2295
|
+
// deck's `SIN` amplitude agree by construction.
|
|
2296
|
+
// One entry per declared coil, in declaration order, so a transformer with two coils
|
|
2297
|
+
// of the same role keeps two voltages. A class-keyed record kept one.
|
|
2298
|
+
const windingAmplitudeVolts = (device.identity.declaredWindings ?? []).map((winding) => winding.voltageRmsVolts === null
|
|
2299
|
+
? null
|
|
2300
|
+
: winding.voltageRmsVolts * AC_SOURCE_RMS_TO_PEAK);
|
|
2301
|
+
return {
|
|
2302
|
+
outcome: "law",
|
|
2303
|
+
device: device.id,
|
|
2304
|
+
law: {
|
|
2305
|
+
kind: "transformer",
|
|
2306
|
+
turnsRatio: device.parameters.ratio ?? 1,
|
|
2307
|
+
windingAmplitudeVolts,
|
|
2308
|
+
mainsFrequencyHz,
|
|
2309
|
+
windingSourceOhms: SUPPLY_SOURCE_OHMS,
|
|
2310
|
+
...windingResistance(device),
|
|
2311
|
+
},
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
case "logic-divider":
|
|
2315
|
+
return {
|
|
2316
|
+
outcome: "law",
|
|
2317
|
+
device: device.id,
|
|
2318
|
+
law: {
|
|
2319
|
+
kind: "logic-divider",
|
|
2320
|
+
thresholdVolts: device.parameters.thresholdVolts ?? 2.0,
|
|
2321
|
+
highVolts: device.parameters.highVolts ?? 5.0,
|
|
2322
|
+
},
|
|
2323
|
+
};
|
|
2324
|
+
case "analog-switch":
|
|
2325
|
+
return {
|
|
2326
|
+
outcome: "law",
|
|
2327
|
+
device: device.id,
|
|
2328
|
+
law: {
|
|
2329
|
+
kind: "analog-switch",
|
|
2330
|
+
onOhms: device.parameters.onOhms ?? 100.0,
|
|
2331
|
+
offOhms: device.parameters.offOhms ?? 1e9,
|
|
2332
|
+
thresholdVolts: device.parameters.thresholdVolts ?? 2.5,
|
|
2333
|
+
},
|
|
2334
|
+
};
|
|
2335
|
+
default:
|
|
2336
|
+
return unsupported(device, `device kind "${device.kind}" has no law`);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Source-boundary/support wrappers expressed as `ic` for topology continuity, not execution.
|
|
2341
|
+
*
|
|
2342
|
+
* These rows carry no executable device law and are deliberately represented as open circuits so
|
|
2343
|
+
* explicit package/boundary shells do not block execution for separately-declared active sections.
|
|
2344
|
+
*/
|
|
2345
|
+
/**
|
|
2346
|
+
* Spring reverb tanks, by exact part number.
|
|
2347
|
+
*
|
|
2348
|
+
* **Closed, and deliberately not extrapolated.** Accutronics/Belton codes are decodable --
|
|
2349
|
+
* `4AB3C1B` is input `A` (8 Ohm), output `B` (2250 Ohm), decay `3` (long, 2.75-4.0 s) -- but
|
|
2350
|
+
* decoding an arbitrary code would admit tanks nobody has checked, and a tank this table does
|
|
2351
|
+
* not know stays a `transformer` exactly as it is today rather than getting guessed parameters.
|
|
2352
|
+
* Add a row when a packet declares a tank, the same standing every `part-catalog.ts` entry has.
|
|
2353
|
+
*
|
|
2354
|
+
* The impedances here are the published figures for the code and are used only when the packet
|
|
2355
|
+
* declares none of its own; the four corpus packets all declare `8 Ohm`/`2250 Ohm` themselves,
|
|
2356
|
+
* which agrees with the code and is what actually gets used.
|
|
2357
|
+
*/
|
|
2358
|
+
const springReverbTanks = {
|
|
2359
|
+
// Accutronics type 4, the 16.75" long pan Fender used across the blackface reverb amps.
|
|
2360
|
+
// `delaySeconds` is the shortest of its three springs' published transit times (29/34/41 ms);
|
|
2361
|
+
// the runtime spreads the other two from it. `decaySeconds` is the midpoint of decay code
|
|
2362
|
+
// `3`'s stated 2.75-4.0 s band -- a band, so the midpoint is a choice, and it is named here
|
|
2363
|
+
// rather than hidden as a magic number.
|
|
2364
|
+
[foldPartId("4AB3C1B")]: {
|
|
2365
|
+
inputOhms: 8,
|
|
2366
|
+
outputOhms: 2250,
|
|
2367
|
+
delaySeconds: 0.029,
|
|
2368
|
+
decaySeconds: 3.375,
|
|
2369
|
+
},
|
|
2370
|
+
};
|
|
2371
|
+
/**
|
|
2372
|
+
* Device classes a source can *state* but this pipeline has no law for.
|
|
2373
|
+
*
|
|
2374
|
+
* **The problem this solves.** `.vdsp` carries a typed `kind` from a closed vocabulary, and that
|
|
2375
|
+
* vocabulary has no entry for every real device. A transcriber facing one picks the nearest kind
|
|
2376
|
+
* available and records the truth in the equally typed `sourceTypeName` --
|
|
2377
|
+
* `electro-harmonix-slapback-echo`'s `Q1_UJT` is `kind: bjt` with
|
|
2378
|
+
* `sourceTypeName: Circuit.UnijunctionTransistor` and `PartNumber: 2N4871`. Stamping it as a BJT
|
|
2379
|
+
* then models a device the source never claimed: a UJT is a negative-resistance relaxation
|
|
2380
|
+
* element, its `base1`/`base2` are the ends of one resistive bar rather than a collector and an
|
|
2381
|
+
* emitter, and no bias point makes a BJT behave like one. That packet's own `Role` for it is
|
|
2382
|
+
* `delay clock oscillator`, which is precisely the behaviour a BJT model cannot produce.
|
|
2383
|
+
*
|
|
2384
|
+
* **Typed evidence, compared whole.** `declaredType` is the source's own closed type vocabulary,
|
|
2385
|
+
* so this is a whole-value comparison against it -- not a reading of `Description`, `Role` or the
|
|
2386
|
+
* component name.
|
|
2387
|
+
*
|
|
2388
|
+
* **One entry, deliberately.** It is the only kind/type mismatch in the corpus that names a law
|
|
2389
|
+
* this pipeline lacks. Measured 2026-09-02 over all 142 packets: the other primitive-kind
|
|
2390
|
+
* mismatches are `circuit.led` on a `diode` (an LED *is* a diode and the law carries `isLed`) and
|
|
2391
|
+
* `circuit.tube` on a `diode` (the tube-rectifier-as-silicon case `diodeJunctions` already
|
|
2392
|
+
* documents and deliberately leaves unfiled). Every remaining mismatch sits on `kind: ic`, where
|
|
2393
|
+
* naming any class is exactly what that kind is for. Adding a class here on speculation would be
|
|
2394
|
+
* vocabulary against unexercised data.
|
|
2395
|
+
*/
|
|
2396
|
+
const unimplementedDeviceLaws = new Set([
|
|
2397
|
+
"circuit.unijunctiontransistor",
|
|
2398
|
+
]);
|
|
2399
|
+
/** An `ic`/`power-amp` given no element, tagged with the branch that decided it. */
|
|
2400
|
+
/**
|
|
2401
|
+
* A component's declared program as a resolution, or null where it declares none.
|
|
2402
|
+
*
|
|
2403
|
+
* **Why an unreadable declaration refuses rather than opens.** An `ic` with no model is opened and
|
|
2404
|
+
* the packet renders silence, which CLAUDE.md records as the gap that cost a session: the engine
|
|
2405
|
+
* cannot tell an unimplemented DSP pedal from an analog pedal with a severed net. A component that
|
|
2406
|
+
* *declares* its program is not that case. The source has stated what the chip does, so failing to
|
|
2407
|
+
* execute it is this pipeline's gap rather than the document's, and the console/ROM invariant
|
|
2408
|
+
* applies in full -- named, never silent.
|
|
2409
|
+
*
|
|
2410
|
+
* Blast radius today is zero: no corpus packet declares a program, so no packet that compiles can
|
|
2411
|
+
* change. That is the bound the design set for this step, and it is checked by the corpus ratchet
|
|
2412
|
+
* rather than asserted here.
|
|
2413
|
+
*/
|
|
2414
|
+
function resolveDeclaredProgram(device, netlist) {
|
|
2415
|
+
const program = device.program;
|
|
2416
|
+
if (program === undefined)
|
|
2417
|
+
return null;
|
|
2418
|
+
const check = checkDeclaredProgram(program);
|
|
2419
|
+
if (!check.ok) {
|
|
2420
|
+
return {
|
|
2421
|
+
outcome: "unsupported",
|
|
2422
|
+
device: device.id,
|
|
2423
|
+
reason: `component declares a program this pipeline cannot execute: ${check.reason}`,
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
// A swept gain reads its knob directly, like a scanned router, so the knob it names has to be
|
|
2427
|
+
// on the panel. Otherwise the gain would read a control that does not exist and sit at 0.5.
|
|
2428
|
+
for (const position of check.positions) {
|
|
2429
|
+
for (const op of position.ops) {
|
|
2430
|
+
if (op.op !== "mix")
|
|
2431
|
+
continue;
|
|
2432
|
+
for (const term of op.terms) {
|
|
2433
|
+
if (term.sweep !== undefined && !controlExists(term.sweep.controlId, netlist)) {
|
|
2434
|
+
return {
|
|
2435
|
+
outcome: "unsupported",
|
|
2436
|
+
device: device.id,
|
|
2437
|
+
reason: `component's program "${position.id}" sweeps a gain with control "${term.sweep.controlId}", which this document does not declare`,
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
// The audio boundary, read from the typed roles core 0.8.1 made legal on an opaque chip.
|
|
2444
|
+
// Reading the terminals' authored *names* instead is what the Typed Source Evidence rule
|
|
2445
|
+
// forbids, and it is why this fact had to ship upstream before this branch could exist.
|
|
2446
|
+
const roles = device.identity.declaredTerminalRoles;
|
|
2447
|
+
const audioIn = roles.indexOf("input");
|
|
2448
|
+
const audioOut = roles.indexOf("output");
|
|
2449
|
+
if (audioIn < 0 || audioOut < 0) {
|
|
2450
|
+
const missing = [
|
|
2451
|
+
audioIn < 0 ? "input" : null,
|
|
2452
|
+
audioOut < 0 ? "output" : null,
|
|
2453
|
+
].filter((label) => label !== null);
|
|
2454
|
+
return {
|
|
2455
|
+
outcome: "unsupported",
|
|
2456
|
+
device: device.id,
|
|
2457
|
+
reason: `component declares a program but no terminal declares role ${missing.join(" or ")}, so its audio boundary is unknown; a program says what the chip computes, never which pins carry the signal`,
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
// A cited control on a line's `delaySeconds` is a claim that a knob moves that length, and a
|
|
2461
|
+
// claim nothing wires is a dead knob. Two routes, by how the source says the chip reads it:
|
|
2462
|
+
//
|
|
2463
|
+
// - **scanned / tapped**: per line, below, since a DELAY position and a TEMPO position read
|
|
2464
|
+
// different controls. The knob has to be on the panel; core checked the reader is on the
|
|
2465
|
+
// board.
|
|
2466
|
+
// - **node**: a solved wiper voltage through the block's one `parameter` port, and so at most
|
|
2467
|
+
// one control across the whole block, as before.
|
|
2468
|
+
for (const position of program.positions) {
|
|
2469
|
+
for (const parameters of Object.values(position.lines)) {
|
|
2470
|
+
for (const parameter of Object.values(parameters)) {
|
|
2471
|
+
if (parameter.read === "node" || parameter.source === undefined)
|
|
2472
|
+
continue;
|
|
2473
|
+
if (parameter.control === undefined || !controlExists(parameter.control, netlist)) {
|
|
2474
|
+
return {
|
|
2475
|
+
outcome: "unsupported",
|
|
2476
|
+
device: device.id,
|
|
2477
|
+
reason: `component's program "${position.id}" cites ${parameter.read} control "${parameter.control ?? "(none)"}", which this document does not declare`,
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
let declaredParameter = null;
|
|
2484
|
+
const timeControl = citedNodeControl(program);
|
|
2485
|
+
if (timeControl !== null) {
|
|
2486
|
+
const node = selectorNodeFor(timeControl, netlist);
|
|
2487
|
+
const supply = highestPositiveRailVolts(netlist);
|
|
2488
|
+
if (typeof node === "number" && supply !== null) {
|
|
2489
|
+
declaredParameter = { controlId: timeControl, node, referenceVolts: supply };
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
// The router. Only needed where there is a choice to make: one program runs whatever any
|
|
2493
|
+
// control is doing.
|
|
2494
|
+
let declaredRouter = null;
|
|
2495
|
+
const router = program.router;
|
|
2496
|
+
if (router !== null) {
|
|
2497
|
+
if (router.positions < check.positions.length) {
|
|
2498
|
+
return {
|
|
2499
|
+
outcome: "unsupported",
|
|
2500
|
+
device: device.id,
|
|
2501
|
+
reason: `component declares ${check.positions.length} programs but its router says control "${router.control}" has only ${router.positions} positions`,
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
// **A scanned control has no node, by declaration.** The chip that reads it sits behind
|
|
2505
|
+
// a path the source cannot state, so there is nothing to resolve and nothing to refuse;
|
|
2506
|
+
// the runtime takes the control's position directly. Core has already checked that the
|
|
2507
|
+
// reader named exists in the document, which is what keeps this from being a way to
|
|
2508
|
+
// make any dangling control appear to work.
|
|
2509
|
+
let node = null;
|
|
2510
|
+
let supply = null;
|
|
2511
|
+
if (router.read === "node") {
|
|
2512
|
+
const resolved = selectorNodeFor(router.control, netlist);
|
|
2513
|
+
if (typeof resolved === "string") {
|
|
2514
|
+
return { outcome: "unsupported", device: device.id, reason: resolved };
|
|
2515
|
+
}
|
|
2516
|
+
node = resolved;
|
|
2517
|
+
supply = highestPositiveRailVolts(netlist);
|
|
2518
|
+
if (supply === null) {
|
|
2519
|
+
return {
|
|
2520
|
+
outcome: "unsupported",
|
|
2521
|
+
device: device.id,
|
|
2522
|
+
reason: `component declares a router on control "${router.control}" but the document declares no positive supply rail, so the control's node has no full scale to be read against`,
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
else if (!controlExists(router.control, netlist)) {
|
|
2527
|
+
// A scanned router still has to name a control this document declares. Otherwise the
|
|
2528
|
+
// knob it claims to read is not on the panel at all.
|
|
2529
|
+
return {
|
|
2530
|
+
outcome: "unsupported",
|
|
2531
|
+
device: device.id,
|
|
2532
|
+
reason: `component declares a scanned router on control "${router.control}", which this document does not declare`,
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
// Dense, one entry per detent: a lookup per sample must be an index, not a search.
|
|
2536
|
+
// -1 is "the source declares no program here", which is a statement rather than a gap.
|
|
2537
|
+
const byId = new Map(check.positions.map((position, index) => [position.id, index]));
|
|
2538
|
+
const routes = new Array(router.positions).fill(-1);
|
|
2539
|
+
for (const route of router.routes) {
|
|
2540
|
+
const index = byId.get(route.program);
|
|
2541
|
+
if (index === undefined) {
|
|
2542
|
+
return {
|
|
2543
|
+
outcome: "unsupported",
|
|
2544
|
+
device: device.id,
|
|
2545
|
+
reason: `router sends position ${route.position} to program "${route.program}", which this component does not declare`,
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
routes[route.position] = index;
|
|
2549
|
+
}
|
|
2550
|
+
declaredRouter = {
|
|
2551
|
+
controlId: router.control,
|
|
2552
|
+
node,
|
|
2553
|
+
referenceVolts: supply,
|
|
2554
|
+
positions: router.positions,
|
|
2555
|
+
routes,
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
else if (check.positions.length > 1) {
|
|
2559
|
+
return {
|
|
2560
|
+
outcome: "unsupported",
|
|
2561
|
+
device: device.id,
|
|
2562
|
+
reason: `component declares ${check.positions.length} programs and no router saying which control chooses between them`,
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
return {
|
|
2566
|
+
outcome: "macro",
|
|
2567
|
+
device: device.id,
|
|
2568
|
+
macro: {
|
|
2569
|
+
// The part's own id, so a composition carries what chip it came from rather
|
|
2570
|
+
// than a synthetic name. A declared program is not a registry model and must
|
|
2571
|
+
// not borrow one's identity.
|
|
2572
|
+
modelId: device.identity.partNumber?.trim() || `${device.id}-program`,
|
|
2573
|
+
parameters: {},
|
|
2574
|
+
portTerminals: [audioIn, audioOut],
|
|
2575
|
+
audioPortImpedanceOhms: DECLARED_PROGRAM_PORT_OHMS,
|
|
2576
|
+
parameterTerminal: null,
|
|
2577
|
+
parameterReferenceVolts: null,
|
|
2578
|
+
declaredParameter,
|
|
2579
|
+
declaredPositions: check.positions.map((position) => ({
|
|
2580
|
+
id: position.id,
|
|
2581
|
+
ops: position.ops,
|
|
2582
|
+
out: publishedTemp(position.ops),
|
|
2583
|
+
lines: Object.fromEntries(position.lines.map((lineId) => [
|
|
2584
|
+
lineId,
|
|
2585
|
+
(() => {
|
|
2586
|
+
const sweep = declaredLineSweep(program, position.id, lineId);
|
|
2587
|
+
return {
|
|
2588
|
+
delaySeconds: declaredLineSeconds(program, position.id, lineId),
|
|
2589
|
+
minSeconds: declaredLineFloorSeconds(program, position.id, lineId),
|
|
2590
|
+
...(sweep === undefined ? {} : { sweep }),
|
|
2591
|
+
};
|
|
2592
|
+
})(),
|
|
2593
|
+
])),
|
|
2594
|
+
})),
|
|
2595
|
+
declaredRouter,
|
|
2596
|
+
},
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Audio port impedances for a declared composition.
|
|
2601
|
+
*
|
|
2602
|
+
* A registry macro states its part's real input and output impedances. A declaration
|
|
2603
|
+
* does not, and inventing per-part numbers from nothing would be exactly the guess this
|
|
2604
|
+
* programme exists to remove -- so these are the neutral pair: a high input that loads
|
|
2605
|
+
* the stage before it as little as a chip's input pin does, and a low output that drives
|
|
2606
|
+
* the stage after it. Stated here once rather than spread as literals.
|
|
2607
|
+
*/
|
|
2608
|
+
const DECLARED_PROGRAM_PORT_OHMS = { input: 1e6, output: 1e3 };
|
|
2609
|
+
/**
|
|
2610
|
+
* Which temporary a declared position publishes.
|
|
2611
|
+
*
|
|
2612
|
+
* **The last value the graph produces**, which is the last op carrying an `out`. Not simply
|
|
2613
|
+
* the last op: `delay-push` writes into a line and produces nothing, and a delay graph
|
|
2614
|
+
* almost always ends with one, so taking the final index would publish a temporary that
|
|
2615
|
+
* was never written and render silence on a program that is correct.
|
|
2616
|
+
*
|
|
2617
|
+
* The format does not state this and does not need to. A composition is evaluated top to
|
|
2618
|
+
* bottom and a source may only read an earlier op, so "the last value produced" is
|
|
2619
|
+
* unambiguous from the op list alone.
|
|
2620
|
+
*/
|
|
2621
|
+
function publishedTemp(ops) {
|
|
2622
|
+
for (let index = ops.length - 1; index >= 0; index -= 1) {
|
|
2623
|
+
const op = ops[index];
|
|
2624
|
+
if (op !== undefined && "out" in op)
|
|
2625
|
+
return op.out;
|
|
2626
|
+
}
|
|
2627
|
+
return 0;
|
|
2628
|
+
}
|
|
2629
|
+
/**
|
|
2630
|
+
* The cited delay for one line of one position, or zero where the declaration cites none.
|
|
2631
|
+
*
|
|
2632
|
+
* **An uncited parameter is not set**, which is the declaration format's own rule: a
|
|
2633
|
+
* number with no `source` is not evidenced and the block makes no claim on that axis.
|
|
2634
|
+
* Zero here means the line has no length, which a delay op then reads as no delay -- the
|
|
2635
|
+
* honest reading of "the document does not say".
|
|
2636
|
+
*/
|
|
2637
|
+
function declaredLineSeconds(program, positionId, lineId) {
|
|
2638
|
+
const position = program.positions.find((entry) => entry.id === positionId);
|
|
2639
|
+
const cited = position?.lines[lineId]?.delaySeconds;
|
|
2640
|
+
if (cited === undefined || cited.source === undefined)
|
|
2641
|
+
return 0;
|
|
2642
|
+
// The maximum of the cited range: the line is sized for the longest delay the mode can
|
|
2643
|
+
// be asked for, exactly as a clocked macro sizes for the longest its knob can reach.
|
|
2644
|
+
return Number.isFinite(cited.max) && cited.max > 0 ? cited.max : 0;
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* The minimum of a line's cited range, or 0 where the source cites none.
|
|
2648
|
+
*
|
|
2649
|
+
* The floor a control sweeps up from; the line itself is sized from the maximum. A range the
|
|
2650
|
+
* source does not cite stays unset, as for the maximum, and so does a minimum that is not below
|
|
2651
|
+
* the maximum -- a floor that reaches the top would silently pin the knob.
|
|
2652
|
+
*/
|
|
2653
|
+
function declaredLineFloorSeconds(program, positionId, lineId) {
|
|
2654
|
+
const position = program.positions.find((entry) => entry.id === positionId);
|
|
2655
|
+
const cited = position?.lines[lineId]?.delaySeconds;
|
|
2656
|
+
if (cited === undefined || cited.source === undefined)
|
|
2657
|
+
return 0;
|
|
2658
|
+
return Number.isFinite(cited.min) && cited.min > 0 && cited.min < cited.max ? cited.min : 0;
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* The one control a declaration's **node-read** parameters name, or null.
|
|
2662
|
+
*
|
|
2663
|
+
* **One, deliberately.** A composed block has a single `parameter` port, so node-read citations
|
|
2664
|
+
* of two different controls have no faithful lowering, and taking the first would silently drop
|
|
2665
|
+
* the second knob. Null in that case, which leaves those lengths at their stated maximum. Scanned
|
|
2666
|
+
* and tapped citations do not come through here: each line carries its own (`ComposedLineSweep`).
|
|
2667
|
+
*/
|
|
2668
|
+
function citedNodeControl(program) {
|
|
2669
|
+
const named = new Set();
|
|
2670
|
+
for (const position of program.positions) {
|
|
2671
|
+
for (const parameters of Object.values(position.lines)) {
|
|
2672
|
+
for (const parameter of Object.values(parameters)) {
|
|
2673
|
+
if (parameter.read === "node" &&
|
|
2674
|
+
parameter.control !== undefined &&
|
|
2675
|
+
parameter.source !== undefined) {
|
|
2676
|
+
named.add(parameter.control);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
return named.size === 1 ? ([...named][0] ?? null) : null;
|
|
2682
|
+
}
|
|
2683
|
+
/**
|
|
2684
|
+
* The per-line sweep a line's cited `delaySeconds` declares, or undefined for a fixed or
|
|
2685
|
+
* node-read length.
|
|
2686
|
+
*/
|
|
2687
|
+
function declaredLineSweep(program, positionId, lineId) {
|
|
2688
|
+
const position = program.positions.find((entry) => entry.id === positionId);
|
|
2689
|
+
const cited = position?.lines[lineId]?.delaySeconds;
|
|
2690
|
+
if (cited === undefined ||
|
|
2691
|
+
cited.source === undefined ||
|
|
2692
|
+
cited.control === undefined ||
|
|
2693
|
+
cited.read === "node") {
|
|
2694
|
+
return undefined;
|
|
2695
|
+
}
|
|
2696
|
+
return { controlId: cited.control, read: cited.read, ratio: cited.ratio ?? 1 };
|
|
2697
|
+
}
|
|
2698
|
+
/**
|
|
2699
|
+
* The node whose voltage carries a control's position, or why none does.
|
|
2700
|
+
*
|
|
2701
|
+
* **This is the pot's wiper, and it is deliberately not a pin of the declaring chip.** The
|
|
2702
|
+
* knob does not reach the DSP; on a DD-5 it reaches the CPU's analog input, and the CPU tells
|
|
2703
|
+
* the DSP which program to run. What the engine can solve is the voltage the wiper produces,
|
|
2704
|
+
* and that is all a router needs. Requiring the node to sit on the declaring component would
|
|
2705
|
+
* refuse every firmware-mediated control, which is most of them.
|
|
2706
|
+
*
|
|
2707
|
+
* The wiper is found by its declared `role`, a closed vocabulary compared whole. A control
|
|
2708
|
+
* bound to a device with no wiper -- a plain switch -- has no continuous reading to quantize,
|
|
2709
|
+
* and is refused by name rather than guessed at.
|
|
2710
|
+
*/
|
|
2711
|
+
function selectorNodeFor(control, netlist) {
|
|
2712
|
+
if (netlist === undefined) {
|
|
2713
|
+
return `component declares a router on control "${control}" but this stage was given no netlist to resolve it against`;
|
|
2714
|
+
}
|
|
2715
|
+
const bound = netlist.devices.filter((device) => device.control === control);
|
|
2716
|
+
if (bound.length === 0) {
|
|
2717
|
+
return `component declares a router on control "${control}", which no device in this document is bound to`;
|
|
2718
|
+
}
|
|
2719
|
+
const wipers = [];
|
|
2720
|
+
for (const device of bound) {
|
|
2721
|
+
for (const [index, role] of device.identity.declaredTerminalRoles.entries()) {
|
|
2722
|
+
if (role !== "wiper")
|
|
2723
|
+
continue;
|
|
2724
|
+
const node = device.nodes[index];
|
|
2725
|
+
if (node !== undefined)
|
|
2726
|
+
wipers.push(node);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
const distinct = [...new Set(wipers)];
|
|
2730
|
+
if (distinct.length === 0) {
|
|
2731
|
+
return `component declares a router on control "${control}", but no device bound to it declares a wiper terminal, so the control produces no voltage this engine can read`;
|
|
2732
|
+
}
|
|
2733
|
+
if (distinct.length > 1) {
|
|
2734
|
+
return `component declares a router on control "${control}", and ${distinct.length} distinct wiper nodes are bound to it, so which one carries the selection is ambiguous`;
|
|
2735
|
+
}
|
|
2736
|
+
return distinct[0] ?? -1;
|
|
2737
|
+
}
|
|
2738
|
+
/** Whether the document declares this panel control at all. */
|
|
2739
|
+
function controlExists(control, netlist) {
|
|
2740
|
+
return netlist?.controls.some((candidate) => candidate.id === control) ?? false;
|
|
2741
|
+
}
|
|
2742
|
+
/** The highest positive rail the document declares, or null where it declares none. */
|
|
2743
|
+
function highestPositiveRailVolts(netlist) {
|
|
2744
|
+
if (netlist === undefined)
|
|
2745
|
+
return null;
|
|
2746
|
+
let highest = null;
|
|
2747
|
+
for (const device of netlist.devices) {
|
|
2748
|
+
if (device.kind !== "voltage-source" && device.kind !== "rail")
|
|
2749
|
+
continue;
|
|
2750
|
+
const volts = device.parameters.volts;
|
|
2751
|
+
if (typeof volts !== "number" || !Number.isFinite(volts) || volts <= 0)
|
|
2752
|
+
continue;
|
|
2753
|
+
if (highest === null || volts > highest)
|
|
2754
|
+
highest = volts;
|
|
2755
|
+
}
|
|
2756
|
+
return highest;
|
|
2757
|
+
}
|
|
2758
|
+
function openIc(device, reason) {
|
|
2759
|
+
return {
|
|
2760
|
+
outcome: "law",
|
|
2761
|
+
device: device.id,
|
|
2762
|
+
law: { kind: "open" },
|
|
2763
|
+
openReason: reason,
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Declared component classes for which an unmodellable chip is **opened rather than refused**.
|
|
2768
|
+
*
|
|
2769
|
+
* **This is a degradation rule, not an identification rung, and the distinction is the whole
|
|
2770
|
+
* point.** It never says what a component *is*, produces no `PartIdentity`, and is consulted only
|
|
2771
|
+
* after the registry has already failed to bind a model -- so an exact part id and a fitting
|
|
2772
|
+
* pinout always win. `identify.ts`'s ladder is untouched.
|
|
2773
|
+
*
|
|
2774
|
+
* **What it replaced.** Until 2026-09-04 these class names sat in the `declaredTypes` of the
|
|
2775
|
+
* catalog's DRAM entry (`M12L64164A`), and `Circuit.IC` additionally in the uPC1252H2 VCA's, so a
|
|
2776
|
+
* chip the registry could not place was *identified as one of those parts* and took its law.
|
|
2777
|
+
* Measured: `boss-dd-3b`'s `SA571D` compandor and `electro-harmonix-holy-grail`'s `CS4811` DSP both
|
|
2778
|
+
* reported `partId: M12L64164A` with evidence `exact-part`, and four components reached the VCA
|
|
2779
|
+
* entry only by declaring the generic `Circuit.IC`. A wide `declaredTypes` net is a registry entry
|
|
2780
|
+
* claiming a class it does not model, which is the wrong answer `identify.ts` refuses to give:
|
|
2781
|
+
* "a wrong identity is worse than none". The degradation those entries were really providing is
|
|
2782
|
+
* this, stated as itself.
|
|
2783
|
+
*
|
|
2784
|
+
* **Sized from measurement, not from the old list.** The DRAM entry carried seventeen classes;
|
|
2785
|
+
* these are the seven the corpus exercises. The other ten (`Circuit.AudioCodec`, `Circuit.Codec`,
|
|
2786
|
+
* `Circuit.VoltageDetector`, `Circuit.SwitchingRegulator`, `Circuit.EEPROM`,
|
|
2787
|
+
* `Circuit.SupportChip`, `Circuit.DCDCConverter`, `Circuit.Compander`, `Circuit.PowerConverter`,
|
|
2788
|
+
* `Circuit.LogicIC`) are reached today only by exact part id, so listing them would be
|
|
2789
|
+
* speculative vocabulary against unexercised data -- and dropping them is what makes a future
|
|
2790
|
+
* compandor or logic IC whose pinout does not fit **refuse and name itself** rather than quietly
|
|
2791
|
+
* become a hole. Several of these classes have modelled members: a `Circuit.LogicIC` is often a
|
|
2792
|
+
* CD4049 inverter, a `Circuit.Compander` an NE570 that binds. Membership here is not a claim about
|
|
2793
|
+
* the class.
|
|
2794
|
+
*
|
|
2795
|
+
* **`circuit.ic` is deliberately absent**, and the tests are what settled it. It is the format's
|
|
2796
|
+
* generic "this is a chip", so admitting it would mean *no* unidentified chip ever refuses again --
|
|
2797
|
+
* it defeats the stated decision that an unknown chip makes the pedal unsupported, and it stops
|
|
2798
|
+
* `emptyRegistry` refusing at all, which is the measurement `--empty-registry` exists to take.
|
|
2799
|
+
* Four corpus components declaring it still need to compile; `namesNoPartToLookUp` is the narrower
|
|
2800
|
+
* reason they get instead.
|
|
2801
|
+
*/
|
|
2802
|
+
const classesOpenedWithoutModel = new Set([
|
|
2803
|
+
"circuit.digitalsignalprocessor",
|
|
2804
|
+
"circuit.microcontroller",
|
|
2805
|
+
"circuit.memoryic",
|
|
2806
|
+
"circuit.resetsupervisor",
|
|
2807
|
+
"circuit.crystal",
|
|
2808
|
+
"circuit.sourcevisiblesubsystem",
|
|
2809
|
+
// A converter is the same case as the DSP and the microcontroller above it: what it does to a
|
|
2810
|
+
// sample is digital and no dump makes it modellable, so an open with a named warning is the
|
|
2811
|
+
// honest outcome and a refusal that takes the whole packet down is not.
|
|
2812
|
+
//
|
|
2813
|
+
// Reached on `boss-dd-5` only once its supply rails were declared. Before that its AK5345-VS
|
|
2814
|
+
// was excused for being electrically isolated, which is the weaker reason: a packet should
|
|
2815
|
+
// not depend on a chip being unwired in order to compile.
|
|
2816
|
+
//
|
|
2817
|
+
"circuit.adc",
|
|
2818
|
+
// Admitted on a packet, as the rule above demanded before it was. `boss-dd-5` v1.36 wires its
|
|
2819
|
+
// output mixer as drawn, so its uPD6379's RO pin now drives the panning-out wet chain and the
|
|
2820
|
+
// chip stopped being isolated. Same case as the ADC: the conversion is the declared program's
|
|
2821
|
+
// output, what the chip does to a sample is digital, and refusing it would take a working
|
|
2822
|
+
// packet down for wiring what the sheet shows. The other two corpus DACs (`boss-rv-3`,
|
|
2823
|
+
// `boss-dsd-3`) compile byte-identically with and without this entry.
|
|
2824
|
+
"circuit.dac",
|
|
2825
|
+
]);
|
|
2826
|
+
/** Whether the source states one of {@link classesOpenedWithoutModel}. */
|
|
2827
|
+
function declaresClassOpenedWithoutModel(device) {
|
|
2828
|
+
const declaredType = device.identity.declaredType;
|
|
2829
|
+
return (declaredType !== null &&
|
|
2830
|
+
classesOpenedWithoutModel.has(foldToken(declaredType)));
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* A component declaring the generic `Circuit.IC` and **no part number at all**.
|
|
2834
|
+
*
|
|
2835
|
+
* **This is the discriminator the failing tests produced, and it is sharper than the class.** The
|
|
2836
|
+
* `unknownChip` fixture declares `Circuit.IC` with `PartNumber: UNKNOWN-PART-XYZ`, and it must
|
|
2837
|
+
* refuse: someone fitted a real chip, the catalog does not model it, and the refusal is
|
|
2838
|
+
* actionable -- add the part. The four corpus components here declare `Circuit.IC` and name no
|
|
2839
|
+
* part, so every rung of `identify`'s ladder is empty *by construction*: there is no part id to
|
|
2840
|
+
* match, the class is the format's generic "this is a chip", and rung 3 sees only shell pin names
|
|
2841
|
+
* (`jackinput`, `codeclinrin`, `outputshellin`). A refusal nobody can act on -- there is no part
|
|
2842
|
+
* number to add to the catalog -- is a worse answer than an open the reader is told about.
|
|
2843
|
+
*
|
|
2844
|
+
* `jhs-clover`'s `ACTIVE_EQ_BOUNDARY` and `boss-st-2`'s three analog-support shells are the four,
|
|
2845
|
+
* and each is a boundary the packet drew rather than a chip it fitted. `jhs-clover`'s says so in
|
|
2846
|
+
* its own `SourceBoundaryStatus`, which nothing reads -- and reading it was not chosen here
|
|
2847
|
+
* because exactly one component in 142 documents carries that property, so it would be a rule
|
|
2848
|
+
* sized to one row.
|
|
2849
|
+
*
|
|
2850
|
+
* **Checked only after identification has already failed**, like the class rule beside it, so a
|
|
2851
|
+
* part-less component the registry *can* place by declared type -- a `Circuit.DelayMemoryChip`
|
|
2852
|
+
* reaching the bucket-brigade macro -- never comes near this.
|
|
2853
|
+
*/
|
|
2854
|
+
function namesNoPartToLookUp(device) {
|
|
2855
|
+
return (device.identity.partNumber === null &&
|
|
2856
|
+
device.identity.declaredType !== null &&
|
|
2857
|
+
foldToken(device.identity.declaredType) === "circuit.ic");
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* An unmodellable IC opened because its source states one of the classes above, or the refusal it
|
|
2861
|
+
* would otherwise have been.
|
|
2862
|
+
*
|
|
2863
|
+
* **The refusal is still the default**, and `identify.ts` records why: an `ic` has no behaviour
|
|
2864
|
+
* except a part's, so the registry's silence is a refusal rather than a default. What this adds is
|
|
2865
|
+
* that a digital or support subsystem the source declares as such does not take the whole pedal
|
|
2866
|
+
* down with it -- the analog circuit around a DSP is still a circuit. The suppressed refusal is
|
|
2867
|
+
* carried onto the resolution so the warning can say which lookup failed and how, which is the
|
|
2868
|
+
* half a reader can act on.
|
|
2869
|
+
*/
|
|
2870
|
+
function openWhereClassPermits(device, refusal) {
|
|
2871
|
+
if (!declaresClassOpenedWithoutModel(device) && !namesNoPartToLookUp(device)) {
|
|
2872
|
+
return { outcome: "unsupported", device: device.id, reason: refusal };
|
|
2873
|
+
}
|
|
2874
|
+
return {
|
|
2875
|
+
outcome: "law",
|
|
2876
|
+
device: device.id,
|
|
2877
|
+
law: { kind: "open" },
|
|
2878
|
+
openReason: "declared-class-without-model",
|
|
2879
|
+
insteadOfRefusal: refusal,
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* Does this device's source state a class this pipeline cannot model? Primitive kinds only: an
|
|
2884
|
+
* `ic` declares its class in `declaredType` by design and is resolved through the registry.
|
|
2885
|
+
*/
|
|
2886
|
+
function declaresUnimplementedDeviceLaw(device) {
|
|
2887
|
+
if (requiresIdentification(device)) {
|
|
2888
|
+
return false;
|
|
2889
|
+
}
|
|
2890
|
+
const declaredType = device.identity.declaredType;
|
|
2891
|
+
return (declaredType !== null &&
|
|
2892
|
+
unimplementedDeviceLaws.has(foldToken(declaredType)));
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* The devices opened by `declaresUnimplementedDeviceLaw`, so a packet is told which stated device
|
|
2896
|
+
* is absent from its program rather than silently receiving the nearest law.
|
|
2897
|
+
*/
|
|
2898
|
+
export function findUnimplementedDeviceLaws(lawed) {
|
|
2899
|
+
const opened = new Set(lawed.resolutions.flatMap((resolution) => resolution.outcome === "law" && resolution.law.kind === "open"
|
|
2900
|
+
? [resolution.device]
|
|
2901
|
+
: []));
|
|
2902
|
+
return lawed.netlist.devices.flatMap((device) => opened.has(device.id) && declaresUnimplementedDeviceLaw(device)
|
|
2903
|
+
? [
|
|
2904
|
+
{
|
|
2905
|
+
code: "device-law-not-implemented",
|
|
2906
|
+
device: device.id,
|
|
2907
|
+
detail: `${device.id} is declared \`kind: ${device.kind}\` but its source states ` +
|
|
2908
|
+
`${device.identity.declaredType}, a device class this pipeline has no law for. ` +
|
|
2909
|
+
"It is not executed: modelling it as its declared kind would simulate a different " +
|
|
2910
|
+
"device, so whatever it does in the real circuit is absent from this program.",
|
|
2911
|
+
},
|
|
2912
|
+
]
|
|
2913
|
+
: []);
|
|
2914
|
+
}
|
|
2915
|
+
function isNonExecutableIcSupportShell(device) {
|
|
2916
|
+
const declaredType = device.identity.declaredType === null
|
|
2917
|
+
? null
|
|
2918
|
+
: foldToken(device.identity.declaredType);
|
|
2919
|
+
if (declaredType === "circuit.opamp" &&
|
|
2920
|
+
hasExactRoleSet(device, ["sectiona", "sectionb", "vplus", "vminus"])) {
|
|
2921
|
+
return true;
|
|
2922
|
+
}
|
|
2923
|
+
if (declaredType === "circuit.audiocodec" &&
|
|
2924
|
+
(hasExactRoleSet(device, ["analoginputshell", "adcboundary"]) ||
|
|
2925
|
+
hasExactRoleSet(device, ["dacboundary", "analogoutputshell"]))) {
|
|
2926
|
+
return true;
|
|
2927
|
+
}
|
|
2928
|
+
if (declaredType === "circuit.logicic" &&
|
|
2929
|
+
hasExactRoleSet(device, ["vcc", "gnd"])) {
|
|
2930
|
+
return true;
|
|
2931
|
+
}
|
|
2932
|
+
if (declaredType === "circuit.poweramp" ||
|
|
2933
|
+
declaredType === "circuit.reverb" ||
|
|
2934
|
+
declaredType === "circuit.module") {
|
|
2935
|
+
return true;
|
|
2936
|
+
}
|
|
2937
|
+
if ((declaredType === "circuit.ic" || declaredType === "circuit.ota") &&
|
|
2938
|
+
(foldToken(device.identity.partNumber ?? "") === "ir3109" ||
|
|
2939
|
+
foldToken(device.identity.partNumber ?? "") === "jhscloveractiveeqboundary")) {
|
|
2940
|
+
return true;
|
|
2941
|
+
}
|
|
2942
|
+
const partNumberFolded = foldToken(device.identity.partNumber ?? "");
|
|
2943
|
+
if (partNumberFolded.startsWith("preampsupportnetwork") ||
|
|
2944
|
+
partNumberFolded === "slp24b" ||
|
|
2945
|
+
partNumberFolded === "slp24bleds" ||
|
|
2946
|
+
partNumberFolded === "ce1powertransformer" ||
|
|
2947
|
+
partNumberFolded === "ta7504s") {
|
|
2948
|
+
return true;
|
|
2949
|
+
}
|
|
2950
|
+
return false;
|
|
2951
|
+
}
|
|
2952
|
+
function hasExactRoleSet(device, roles) {
|
|
2953
|
+
if (device.identity.terminalRoles.length !== roles.length) {
|
|
2954
|
+
return false;
|
|
2955
|
+
}
|
|
2956
|
+
const observed = new Set();
|
|
2957
|
+
for (const role of device.identity.terminalRoles) {
|
|
2958
|
+
if (role === null) {
|
|
2959
|
+
return false;
|
|
2960
|
+
}
|
|
2961
|
+
observed.add(foldToken(role));
|
|
2962
|
+
}
|
|
2963
|
+
for (const role of roles.map((entry) => foldToken(entry))) {
|
|
2964
|
+
if (!observed.has(role)) {
|
|
2965
|
+
return false;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
return true;
|
|
2969
|
+
}
|
|
2970
|
+
function unsupported(device, reason) {
|
|
2971
|
+
return { outcome: "unsupported", device: device.id, reason };
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* A jack is a pure interface symbol until a **supply runs through it**. Then it is a
|
|
2975
|
+
* switch in the supply path, and `open` severs the supply.
|
|
2976
|
+
*
|
|
2977
|
+
* Which terminal the contact makes against is derived from *which end of the supply is
|
|
2978
|
+
* stranded*, never from the contact's name. The corpus names these contacts eleven ways
|
|
2979
|
+
* and they do not share a sense, so a name table gets two of three wrong -- it shorted 13
|
|
2980
|
+
* jacks, five of them supply rails. Two cases are derivable, and they are mirror images:
|
|
2981
|
+
*
|
|
2982
|
+
* A. A supply **return** sits on a jack terminal. It has to reach the jack's own
|
|
2983
|
+
* return or the supply has no path back. `boss-hm-2` and `boss-sp-1-spectrum` put
|
|
2984
|
+
* the battery negative on the input jack's switch contact; without this the whole
|
|
2985
|
+
* circuit floats and renders leakage. 15 jacks.
|
|
2986
|
+
* B. A supply **drive** sits on a jack terminal whose node carries nothing but that
|
|
2987
|
+
* supply and this jack, so the rail is stranded behind the contact and must reach
|
|
2988
|
+
* the jack's remaining terminal. `boss-od-3` routes its 9 V to the pedal only
|
|
2989
|
+
* through the DC jack's contact -- `BATTERY.positive` and `DC_JACK.tip` alone on
|
|
2990
|
+
* node 45, the circuit rail on node 1 with 13 devices -- so without this the entire
|
|
2991
|
+
* pedal compiles into a block with no voltage source and Newton burns its whole
|
|
2992
|
+
* iteration cap every sample hunting an operating point that cannot exist. 3 jacks.
|
|
2993
|
+
*
|
|
2994
|
+
* The direction is not fixed and does not need to be: `boss-od-3` has the supply on the
|
|
2995
|
+
* tip and the circuit on the contact, `boss-ce-5` has it the other way round, and joining
|
|
2996
|
+
* the stranded end to the remaining terminal is right for both.
|
|
2997
|
+
*
|
|
2998
|
+
* Everything else stays open. A `switched_tip` or a `normalSwitch` carrying signal has a
|
|
2999
|
+
* sense that depends on a plug state the packet does not state, and no structural fact
|
|
3000
|
+
* distinguishes them -- so guessing would short a signal rather than leave it unmodelled.
|
|
3001
|
+
*/
|
|
3002
|
+
function portEngage(device, supply) {
|
|
3003
|
+
const open = {
|
|
3004
|
+
outcome: "law",
|
|
3005
|
+
device: device.id,
|
|
3006
|
+
law: { kind: "open" },
|
|
3007
|
+
};
|
|
3008
|
+
const returnIndex = portReturnIndex(device);
|
|
3009
|
+
if (returnIndex === -1) {
|
|
3010
|
+
return open;
|
|
3011
|
+
}
|
|
3012
|
+
const engage = (contactIndex, toIndex) =>
|
|
3013
|
+
// Already the same node: the path exists and an element would be a dead short
|
|
3014
|
+
// across nothing.
|
|
3015
|
+
device.nodes[contactIndex] === device.nodes[toIndex]
|
|
3016
|
+
? open
|
|
3017
|
+
: {
|
|
3018
|
+
outcome: "law",
|
|
3019
|
+
device: device.id,
|
|
3020
|
+
law: { kind: "port-engage", contactIndex, againstIndex: toIndex },
|
|
3021
|
+
};
|
|
3022
|
+
const strandedReturn = device.nodes.findIndex((node, index) => index !== returnIndex && supply.returns.has(node));
|
|
3023
|
+
if (strandedReturn !== -1) {
|
|
3024
|
+
return engage(strandedReturn, returnIndex);
|
|
3025
|
+
}
|
|
3026
|
+
const strandedDrive = device.nodes.findIndex((node, index) => index !== returnIndex &&
|
|
3027
|
+
supply.drives.has(node) &&
|
|
3028
|
+
// Stranded means the node carries only the supply end and this jack terminal.
|
|
3029
|
+
// A drive that already reaches the circuit needs no help, and closing a contact
|
|
3030
|
+
// onto it would bridge two live nodes.
|
|
3031
|
+
(supply.degree.get(node) ?? 0) <= 2);
|
|
3032
|
+
if (strandedDrive === -1) {
|
|
3033
|
+
return open;
|
|
3034
|
+
}
|
|
3035
|
+
const onward = device.nodes
|
|
3036
|
+
.map((node, index) => ({ node, index }))
|
|
3037
|
+
.filter(({ node, index }) => index !== returnIndex &&
|
|
3038
|
+
index !== strandedDrive &&
|
|
3039
|
+
node !== device.nodes[returnIndex]);
|
|
3040
|
+
// More than one candidate is a genuine ambiguity, not a tie to break: `boss-ch-1`
|
|
3041
|
+
// declares `adapter_positive`, `battery_positive` and `switched_positive` on one jack,
|
|
3042
|
+
// and picking between them would be guessing which is wired to the pedal.
|
|
3043
|
+
const only = onward.length === 1 ? onward[0] : undefined;
|
|
3044
|
+
return only === undefined ? open : engage(strandedDrive, only.index);
|
|
3045
|
+
}
|
|
3046
|
+
// The taper *curve* deliberately does not live here. A pot's law carries which taper it
|
|
3047
|
+
// is; evaluating it is `src/runtime/taper.ts`, so no compiler stage can fold a control
|
|
3048
|
+
// curve into a coefficient even by accident. See decision 2 in the pipeline plan.
|
|
3049
|
+
/**
|
|
3050
|
+
* Every integrated circuit the source declares and the program does not execute.
|
|
3051
|
+
*
|
|
3052
|
+
* **A residue reporter, deliberately.** It does not ask the five opening predicates their
|
|
3053
|
+
* questions again -- it reads the reason `resolveDevice` recorded when it chose the `open`. So a
|
|
3054
|
+
* sixth opening path added later is reported the day it is added, as `unrecorded`, instead of
|
|
3055
|
+
* joining the silent class this exists to close; and no copy of a resolver's question can drift
|
|
3056
|
+
* from the resolver, which is the defect that bit this file twice on 2026-09-03.
|
|
3057
|
+
*
|
|
3058
|
+
* `electrically-isolated` is excluded because `findElectricallyIsolatedIcs` already names those
|
|
3059
|
+
* devices and says more about them than this could. The exclusion is on the recorded decision, not
|
|
3060
|
+
* on a re-run of `isElectricallyIsolated`, for the reason above.
|
|
3061
|
+
*
|
|
3062
|
+
* Runs after section expansion, which is what makes it safe: a package the registry could expand
|
|
3063
|
+
* is no longer in `lawed.netlist.devices` at all, so a working dual op-amp cannot appear here.
|
|
3064
|
+
* Non-IC opens are out of scope and stay so -- a ground symbol and an unwired jack contact carry
|
|
3065
|
+
* no element because the symbol has none, which is not a hole in the model.
|
|
3066
|
+
*/
|
|
3067
|
+
/**
|
|
3068
|
+
* `digital-delay-line` as a composition of primitives (board-p3 row 4):
|
|
3069
|
+
* an integer delay line plus one multiply-accumulate for the feedback mix.
|
|
3070
|
+
*
|
|
3071
|
+
* The op order mirrors the kernel's own evaluation order exactly -- read the
|
|
3072
|
+
* delayed sample, publish it, write back tap plus feedback times delayed --
|
|
3073
|
+
* so the interpreter reproduces its bits rather than approximating them.
|
|
3074
|
+
* Length-mode priority mirrors the kernel's branch priority (clock control,
|
|
3075
|
+
* then parameter port, else capacity). Returns `null` for anything but this
|
|
3076
|
+
* model, and for a block carrying `modulation`, which the kernel has no
|
|
3077
|
+
* branch for: there is no faithful composition of a port the kernel ignores.
|
|
3078
|
+
*/
|
|
3079
|
+
export function decomposeDigitalDelayLine(block) {
|
|
3080
|
+
if (block.modelId !== "digital-delay-line") {
|
|
3081
|
+
return null;
|
|
3082
|
+
}
|
|
3083
|
+
if (block.modulation !== undefined && block.modulation !== null) {
|
|
3084
|
+
return null;
|
|
3085
|
+
}
|
|
3086
|
+
const feedback = block.parameters.feedback ?? 0;
|
|
3087
|
+
return {
|
|
3088
|
+
kind: "composed",
|
|
3089
|
+
modelSource: "registry",
|
|
3090
|
+
id: block.id,
|
|
3091
|
+
modelId: block.modelId,
|
|
3092
|
+
parameters: block.parameters,
|
|
3093
|
+
audioIn: block.audioIn,
|
|
3094
|
+
audioOut: block.audioOut,
|
|
3095
|
+
parameter: block.parameter,
|
|
3096
|
+
clockControl: block.clockControl,
|
|
3097
|
+
delayProvenance: block.delayProvenance,
|
|
3098
|
+
delayDeclaredReason: block.delayDeclaredReason,
|
|
3099
|
+
modulationRefusal: block.modulationRefusal,
|
|
3100
|
+
router: null,
|
|
3101
|
+
positions: [
|
|
3102
|
+
{
|
|
3103
|
+
id: "default",
|
|
3104
|
+
ops: [
|
|
3105
|
+
{
|
|
3106
|
+
op: "delay-tap",
|
|
3107
|
+
line: "dl",
|
|
3108
|
+
length: block.clockControl !== undefined && block.clockControl !== null
|
|
3109
|
+
? { mode: "clock" }
|
|
3110
|
+
: block.parameter !== null && block.parameter.referenceVolts > 0
|
|
3111
|
+
? { mode: "parameter" }
|
|
3112
|
+
: { mode: "capacity" },
|
|
3113
|
+
out: 0,
|
|
3114
|
+
},
|
|
3115
|
+
{
|
|
3116
|
+
op: "mix",
|
|
3117
|
+
terms: [
|
|
3118
|
+
{ source: { kind: "input" }, gain: 1 },
|
|
3119
|
+
{ source: { kind: "temp", index: 0 }, gain: feedback },
|
|
3120
|
+
],
|
|
3121
|
+
out: 1,
|
|
3122
|
+
},
|
|
3123
|
+
{
|
|
3124
|
+
op: "delay-push",
|
|
3125
|
+
line: "dl",
|
|
3126
|
+
input: { kind: "temp", index: 1 },
|
|
3127
|
+
},
|
|
3128
|
+
],
|
|
3129
|
+
out: 0,
|
|
3130
|
+
lines: {
|
|
3131
|
+
dl: { delaySeconds: block.parameters.delaySeconds ?? 0, minSeconds: 0 },
|
|
3132
|
+
},
|
|
3133
|
+
},
|
|
3134
|
+
],
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
/**
|
|
3138
|
+
* `bucket-brigade-delay-line` as a composition of primitives (board-p3
|
|
3139
|
+
* row 4): input coupling estimator, fractionally interpolated delay line,
|
|
3140
|
+
* write-back.
|
|
3141
|
+
*
|
|
3142
|
+
* Op order mirrors the kernel exactly -- couple the tap to AC, read the
|
|
3143
|
+
* delayed sample with linear interpolation, publish it, write the AC back --
|
|
3144
|
+
* and length priority mirrors its branches (clock, clock-law, modulation,
|
|
3145
|
+
* parameter).
|
|
3146
|
+
*
|
|
3147
|
+
* Deliberately no `converter` op: the shipped kernel performs no conversion
|
|
3148
|
+
* stage (no companding, no quantization -- `grep compand` finds a device
|
|
3149
|
+
* law and comments), so a bit-identical decomposition has nowhere to put
|
|
3150
|
+
* one. The enum gains `converter` when a decomposition needs it, not before.
|
|
3151
|
+
*/
|
|
3152
|
+
export function decomposeBucketBrigadeDelayLine(block) {
|
|
3153
|
+
if (block.modelId !== "bucket-brigade-delay-line") {
|
|
3154
|
+
return null;
|
|
3155
|
+
}
|
|
3156
|
+
const clockLawLength = block.clockLaw !== undefined &&
|
|
3157
|
+
block.clockLaw !== null &&
|
|
3158
|
+
block.clockLawParams !== undefined &&
|
|
3159
|
+
block.clockLawParams !== null
|
|
3160
|
+
? {
|
|
3161
|
+
mode: "clock-law",
|
|
3162
|
+
rOhms: block.clockLawParams.rOhms,
|
|
3163
|
+
cFarads: block.clockLawParams.cFarads,
|
|
3164
|
+
vddVolts: block.clockLawParams.vddVolts,
|
|
3165
|
+
vthVolts: block.clockLawParams.vthVolts,
|
|
3166
|
+
vfVolts: block.clockLawParams.vfVolts,
|
|
3167
|
+
floorVolts: block.clockLawParams.floorVolts,
|
|
3168
|
+
stages: block.parameters.stages ?? 0,
|
|
3169
|
+
}
|
|
3170
|
+
: null;
|
|
3171
|
+
return {
|
|
3172
|
+
kind: "composed",
|
|
3173
|
+
modelSource: "registry",
|
|
3174
|
+
id: block.id,
|
|
3175
|
+
modelId: block.modelId,
|
|
3176
|
+
parameters: block.parameters,
|
|
3177
|
+
audioIn: block.audioIn,
|
|
3178
|
+
audioOut: block.audioOut,
|
|
3179
|
+
parameter: block.parameter,
|
|
3180
|
+
clockControl: block.clockControl,
|
|
3181
|
+
modulation: block.modulation,
|
|
3182
|
+
// Present only when recognised: same byte-churn rule as `lower.ts` --
|
|
3183
|
+
// every other program must stay digest-identical.
|
|
3184
|
+
...(block.clockLaw !== undefined &&
|
|
3185
|
+
block.clockLaw !== null &&
|
|
3186
|
+
block.clockLawParams !== undefined &&
|
|
3187
|
+
block.clockLawParams !== null
|
|
3188
|
+
? { clockLaw: block.clockLaw, clockLawParams: block.clockLawParams }
|
|
3189
|
+
: {}),
|
|
3190
|
+
delayProvenance: block.delayProvenance,
|
|
3191
|
+
delayDeclaredReason: block.delayDeclaredReason,
|
|
3192
|
+
modulationRefusal: block.modulationRefusal,
|
|
3193
|
+
router: null,
|
|
3194
|
+
positions: [
|
|
3195
|
+
{
|
|
3196
|
+
id: "default",
|
|
3197
|
+
ops: [
|
|
3198
|
+
{ op: "filter-dcblock", input: { kind: "input" }, out: 0 },
|
|
3199
|
+
{
|
|
3200
|
+
op: "delay-tap-fractional",
|
|
3201
|
+
line: "dl",
|
|
3202
|
+
length: block.clockControl !== undefined && block.clockControl !== null
|
|
3203
|
+
? { mode: "clock" }
|
|
3204
|
+
: clockLawLength ?? (block.modulation !== undefined && block.modulation !== null
|
|
3205
|
+
? { mode: "modulation" }
|
|
3206
|
+
: block.parameter !== null && block.parameter.referenceVolts > 0
|
|
3207
|
+
? { mode: "parameter" }
|
|
3208
|
+
: { mode: "capacity" }),
|
|
3209
|
+
out: 1,
|
|
3210
|
+
},
|
|
3211
|
+
{
|
|
3212
|
+
op: "delay-push",
|
|
3213
|
+
line: "dl",
|
|
3214
|
+
input: { kind: "temp", index: 0 },
|
|
3215
|
+
},
|
|
3216
|
+
],
|
|
3217
|
+
out: 1,
|
|
3218
|
+
lines: {
|
|
3219
|
+
dl: { delaySeconds: block.parameters.delaySeconds ?? 0, minSeconds: 0 },
|
|
3220
|
+
},
|
|
3221
|
+
},
|
|
3222
|
+
],
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* `digital-reverb-module` as a composition of primitives (board-p3 row 4):
|
|
3227
|
+
* four parallel feedback combs averaged, two series allpass diffusers,
|
|
3228
|
+
* output gain. A Schroeder reverb, which is what the kernel is.
|
|
3229
|
+
*
|
|
3230
|
+
* Arithmetic mirrors the kernel term for term: combs accumulate
|
|
3231
|
+
* left-to-right then scale by exactly one quarter (a power of two, so the
|
|
3232
|
+
* multiply rounds like the kernel's divide), allpasses keep the kernel's
|
|
3233
|
+
* multiply order. The kernel has no clock, parameter, or modulation
|
|
3234
|
+
* branches, so those ports are dropped rather than carried -- carrying a
|
|
3235
|
+
* port the kernel ignores would imply behaviour the composition does not
|
|
3236
|
+
* have. `decaySeconds` travels per comb op; the echo-density tables and
|
|
3237
|
+
* the allpass coefficient stay interpreter-owned, exactly as today.
|
|
3238
|
+
*/
|
|
3239
|
+
export function decomposeDigitalReverbModule(block) {
|
|
3240
|
+
if (block.modelId !== "digital-reverb-module") {
|
|
3241
|
+
return null;
|
|
3242
|
+
}
|
|
3243
|
+
const decaySeconds = block.parameters.decaySeconds ?? 0;
|
|
3244
|
+
const outputGain = block.parameters.outputGain ?? 1;
|
|
3245
|
+
return {
|
|
3246
|
+
kind: "composed",
|
|
3247
|
+
modelSource: "registry",
|
|
3248
|
+
id: block.id,
|
|
3249
|
+
modelId: block.modelId,
|
|
3250
|
+
parameters: block.parameters,
|
|
3251
|
+
audioIn: block.audioIn,
|
|
3252
|
+
audioOut: block.audioOut,
|
|
3253
|
+
parameter: null,
|
|
3254
|
+
delayProvenance: block.delayProvenance,
|
|
3255
|
+
delayDeclaredReason: block.delayDeclaredReason,
|
|
3256
|
+
modulationRefusal: block.modulationRefusal,
|
|
3257
|
+
router: null,
|
|
3258
|
+
positions: [
|
|
3259
|
+
{
|
|
3260
|
+
id: "default",
|
|
3261
|
+
ops: [
|
|
3262
|
+
{ op: "comb", index: 0, decaySeconds, input: { kind: "input" }, out: 0 },
|
|
3263
|
+
{ op: "comb", index: 1, decaySeconds, input: { kind: "input" }, out: 1 },
|
|
3264
|
+
{ op: "comb", index: 2, decaySeconds, input: { kind: "input" }, out: 2 },
|
|
3265
|
+
{ op: "comb", index: 3, decaySeconds, input: { kind: "input" }, out: 3 },
|
|
3266
|
+
{
|
|
3267
|
+
op: "mix",
|
|
3268
|
+
terms: [
|
|
3269
|
+
{ source: { kind: "temp", index: 0 }, gain: 1 },
|
|
3270
|
+
{ source: { kind: "temp", index: 1 }, gain: 1 },
|
|
3271
|
+
{ source: { kind: "temp", index: 2 }, gain: 1 },
|
|
3272
|
+
{ source: { kind: "temp", index: 3 }, gain: 1 },
|
|
3273
|
+
],
|
|
3274
|
+
out: 4,
|
|
3275
|
+
},
|
|
3276
|
+
{
|
|
3277
|
+
op: "mix",
|
|
3278
|
+
// The kernel averages over its four combs; 0.25 is exact here
|
|
3279
|
+
// because the interpreter's comb table is fixed at four. If
|
|
3280
|
+
// that table ever grows, this literal must follow it.
|
|
3281
|
+
terms: [{ source: { kind: "temp", index: 4 }, gain: 0.25 }],
|
|
3282
|
+
out: 5,
|
|
3283
|
+
},
|
|
3284
|
+
{ op: "allpass", index: 0, input: { kind: "temp", index: 5 }, out: 6 },
|
|
3285
|
+
{ op: "allpass", index: 1, input: { kind: "temp", index: 6 }, out: 7 },
|
|
3286
|
+
{
|
|
3287
|
+
op: "mix",
|
|
3288
|
+
terms: [{ source: { kind: "temp", index: 7 }, gain: outputGain }],
|
|
3289
|
+
out: 8,
|
|
3290
|
+
},
|
|
3291
|
+
],
|
|
3292
|
+
out: 8,
|
|
3293
|
+
lines: {},
|
|
3294
|
+
},
|
|
3295
|
+
],
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
/**
|
|
3299
|
+
* Emit compositions for the three decomposed models, passthrough for
|
|
3300
|
+
* everything else (board-p3 row 4, the flip). Called once per program after
|
|
3301
|
+
* `couple` wires ports: decompositions read fully-resolved blocks, so an
|
|
3302
|
+
* earlier flip would compose unwired placeholders. A `null` decomposition
|
|
3303
|
+
* (a port the kernel ignores, a model outside the three) keeps the macro
|
|
3304
|
+
* block rather than failing the program.
|
|
3305
|
+
*/
|
|
3306
|
+
export function composeMacroBlocks(blocks) {
|
|
3307
|
+
return blocks.map((block) => {
|
|
3308
|
+
if (block.kind !== "macro") {
|
|
3309
|
+
return block;
|
|
3310
|
+
}
|
|
3311
|
+
// **A declared composition composes to itself.** The three decompositions below
|
|
3312
|
+
// translate a registry model into ops; a declaration arrives as ops already, so
|
|
3313
|
+
// there is nothing to translate and no model table to consult. That is the whole
|
|
3314
|
+
// point of the second resolution key: the source states the program.
|
|
3315
|
+
const declared = block.declaredPositions;
|
|
3316
|
+
if (declared !== null && declared !== undefined && declared.length > 0) {
|
|
3317
|
+
return {
|
|
3318
|
+
kind: "composed",
|
|
3319
|
+
modelSource: "declared",
|
|
3320
|
+
id: block.id,
|
|
3321
|
+
modelId: block.modelId,
|
|
3322
|
+
parameters: block.parameters,
|
|
3323
|
+
audioIn: block.audioIn,
|
|
3324
|
+
audioOut: block.audioOut,
|
|
3325
|
+
parameter: block.parameter,
|
|
3326
|
+
clockControl: block.clockControl,
|
|
3327
|
+
delayProvenance: block.delayProvenance,
|
|
3328
|
+
delayDeclaredReason: block.delayDeclaredReason,
|
|
3329
|
+
modulationRefusal: block.modulationRefusal,
|
|
3330
|
+
positions: declared,
|
|
3331
|
+
router: block.router ?? null,
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
if (block.modelId === "digital-delay-line") {
|
|
3335
|
+
return decomposeDigitalDelayLine(block) ?? block;
|
|
3336
|
+
}
|
|
3337
|
+
if (block.modelId === "bucket-brigade-delay-line") {
|
|
3338
|
+
return decomposeBucketBrigadeDelayLine(block) ?? block;
|
|
3339
|
+
}
|
|
3340
|
+
if (block.modelId === "digital-reverb-module") {
|
|
3341
|
+
return decomposeDigitalReverbModule(block) ?? block;
|
|
3342
|
+
}
|
|
3343
|
+
return block;
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
/**
|
|
3347
|
+
* Which phase-3 bucket a gap-candidate open IC belongs in (board-p3 row 1).
|
|
3348
|
+
*
|
|
3349
|
+
* Pure over typed and structural evidence only: the entry's `firmwareClass`
|
|
3350
|
+
* (compared whole-value, never prose) and whether the chip shares a net with
|
|
3351
|
+
* a device the program executes. `fixed-function` means no program is
|
|
3352
|
+
* missing, so the only question left is what covers the chip's behaviour: an
|
|
3353
|
+
* executing macro (memory inside a delay kernel, e.g. a 4164 behind an
|
|
3354
|
+
* M50195P) or an executing law (a redundant view of modelled behaviour, e.g.
|
|
3355
|
+
* a single-terminal divider placeholder on an executing divider's clock
|
|
3356
|
+
* net). Everything else stays `programmable-no-program` -- including
|
|
3357
|
+
* `undetermined` firmware, which is an open question rather than evidence of
|
|
3358
|
+
* a fixed function, so the phase-3 set keeps the row until proven otherwise.
|
|
3359
|
+
*/
|
|
3360
|
+
export function classifyOpenIcGap(input) {
|
|
3361
|
+
if (input.firmwareClass === "fixed-function" &&
|
|
3362
|
+
input.sharesNodeWithExecutedMacro) {
|
|
3363
|
+
return "support-chip-subsumed";
|
|
3364
|
+
}
|
|
3365
|
+
if (input.firmwareClass === "fixed-function" &&
|
|
3366
|
+
input.sharesNodeWithExecutedLaw) {
|
|
3367
|
+
return "part-number-miss";
|
|
3368
|
+
}
|
|
3369
|
+
return "programmable-no-program";
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* The entry's firmware class for an exact part-number match, whole-value.
|
|
3373
|
+
* No entry or no evidence reads `undetermined`, which classifies as
|
|
3374
|
+
* `programmable-no-program`: an open question keeps the phase-3 row.
|
|
3375
|
+
*/
|
|
3376
|
+
function firmwareClassOf(registry, partNumber) {
|
|
3377
|
+
if (partNumber === null) {
|
|
3378
|
+
return "undetermined";
|
|
3379
|
+
}
|
|
3380
|
+
const folded = foldPartId(partNumber);
|
|
3381
|
+
for (const entry of registry.entries) {
|
|
3382
|
+
if (entry.partIds.some((id) => foldPartId(id) === folded)) {
|
|
3383
|
+
return entry.firmware?.firmwareClass ?? "undetermined";
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return "undetermined";
|
|
3387
|
+
}
|
|
3388
|
+
export function findIcsNotExecuted(lawed, original, registry) {
|
|
3389
|
+
// **One device, one warning, and isolation is the half that speaks.** A chip sharing no net
|
|
3390
|
+
// with anything is not a modelling question -- no entry at any terminal count would connect
|
|
3391
|
+
// it -- so where `findElectricallyIsolatedIcs` already names a device, this stays quiet.
|
|
3392
|
+
// `boss-oc-3`'s `SRC_AK4552VT` carried both: every one of its twenty pins is a private node,
|
|
3393
|
+
// and it reached the pinout check only because section expansion runs before the isolation
|
|
3394
|
+
// branch in `resolveDevice`. Counted over `original` -- the same pre-expansion netlist the
|
|
3395
|
+
// isolation reporter reads -- so the two cannot disagree about the same device.
|
|
3396
|
+
const devicesTouchingNode = deviceCountByNode(original);
|
|
3397
|
+
const openById = new Map(lawed.resolutions.flatMap((resolution) => resolution.outcome === "law" && resolution.law.kind === "open"
|
|
3398
|
+
? [[resolution.device, resolution]]
|
|
3399
|
+
: []));
|
|
3400
|
+
const warnings = [];
|
|
3401
|
+
const deviceById = new Map(lawed.netlist.devices.map((device) => [device.id, device]));
|
|
3402
|
+
const executedMacroNodes = new Set();
|
|
3403
|
+
const executedLawNodes = new Set();
|
|
3404
|
+
for (const resolution of lawed.resolutions) {
|
|
3405
|
+
const resolved = deviceById.get(resolution.device);
|
|
3406
|
+
if (resolved === undefined) {
|
|
3407
|
+
continue;
|
|
3408
|
+
}
|
|
3409
|
+
if (resolution.outcome === "macro") {
|
|
3410
|
+
for (const node of resolved.nodes) {
|
|
3411
|
+
executedMacroNodes.add(node);
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
else if (resolution.outcome === "law" &&
|
|
3415
|
+
resolution.law.kind !== "open") {
|
|
3416
|
+
for (const node of resolved.nodes) {
|
|
3417
|
+
executedLawNodes.add(node);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
for (const device of lawed.netlist.devices) {
|
|
3422
|
+
if (!requiresIdentification(device)) {
|
|
3423
|
+
continue;
|
|
3424
|
+
}
|
|
3425
|
+
const resolution = openById.get(device.id);
|
|
3426
|
+
if (resolution === undefined || resolution.outcome !== "law") {
|
|
3427
|
+
continue;
|
|
3428
|
+
}
|
|
3429
|
+
const reason = resolution.openReason;
|
|
3430
|
+
if (reason === "electrically-isolated" ||
|
|
3431
|
+
isElectricallyIsolated(device, devicesTouchingNode)) {
|
|
3432
|
+
continue;
|
|
3433
|
+
}
|
|
3434
|
+
// Names the chip by the hardest evidence the document carries, because that is what a
|
|
3435
|
+
// reader needs to look it up: a part number where there is one, the declared class where
|
|
3436
|
+
// there is not.
|
|
3437
|
+
const named = device.identity.partNumber ?? device.identity.declaredType ?? "unnamed";
|
|
3438
|
+
const head = `${device.kind} ${device.id} (${named}) is in the source and not in the program`;
|
|
3439
|
+
// **How much of it is wired at all**, appended to the arity reason because it decides which
|
|
3440
|
+
// half of that reason's advice applies. A pin on a node no other device touches cannot be
|
|
3441
|
+
// connected by any registry entry, so where most of a component's terminals are private the
|
|
3442
|
+
// gap is a source trace and adding a catalog entry at this count would produce stamps that
|
|
3443
|
+
// cannot affect the circuit. Measured on the AK4552VT codecs: every one of them, at every
|
|
3444
|
+
// arity the corpus declares, has all four of its analog pins on private nodes -- including
|
|
3445
|
+
// the two the catalog already models, whose buffers stamp into nothing.
|
|
3446
|
+
const privatePins = device.nodes.filter(
|
|
3447
|
+
// Literal 0 for ground, as `isElectricallyIsolated` writes it just above.
|
|
3448
|
+
(node) => node !== 0 && (devicesTouchingNode.get(node) ?? 0) <= 1).length;
|
|
3449
|
+
const wiring = privatePins === 0
|
|
3450
|
+
? ""
|
|
3451
|
+
: ` ${privatePins} of its ${device.nodes.length} terminals sit on nodes no other component touches, which no registry entry can connect.`;
|
|
3452
|
+
const gapClass = reason === "registry-open" || reason === "declared-class-without-model"
|
|
3453
|
+
? classifyOpenIcGap({
|
|
3454
|
+
firmwareClass: firmwareClassOf(registry, device.identity.partNumber),
|
|
3455
|
+
sharesNodeWithExecutedMacro: device.nodes.some((node) => executedMacroNodes.has(node)),
|
|
3456
|
+
sharesNodeWithExecutedLaw: device.nodes.some((node) => executedLawNodes.has(node)),
|
|
3457
|
+
})
|
|
3458
|
+
: undefined;
|
|
3459
|
+
const gapSuffix = gapClass === "support-chip-subsumed"
|
|
3460
|
+
? " Classified support-chip-subsumed (phase-1 row, not phase 3): its catalog entry states fixed-function behaviour and it shares a net with an executing macro, so no program is missing."
|
|
3461
|
+
: gapClass === "part-number-miss"
|
|
3462
|
+
? " Classified part-number-miss (phase-1 row, not phase 3): its catalog entry states fixed-function behaviour and it shares a net with an executing law, so it is a redundant view of modelled behaviour rather than an unmodelled core."
|
|
3463
|
+
: "";
|
|
3464
|
+
warnings.push({
|
|
3465
|
+
code: "ic-not-executed",
|
|
3466
|
+
device: device.id,
|
|
3467
|
+
reason: reason ?? "unrecorded",
|
|
3468
|
+
detail: (reason === "source-boundary-shell"
|
|
3469
|
+
? `${head}: it is registered as a source-boundary shell, drawn for topological continuity rather than execution, so it is compiled as open.`
|
|
3470
|
+
: reason === "charge-pump-declared-rail"
|
|
3471
|
+
? `${head}: it is a charge pump whose generated rail the document declares separately, so the rail is modelled and the pump itself is compiled as open.`
|
|
3472
|
+
: reason === "registry-arity-mismatch"
|
|
3473
|
+
? `${head}: ${resolution.insteadOfRefusal ?? "its registry entry could not bind the declared terminals"}. The part is registered, so this is an arity disagreement rather than an unknown chip: either the document declares a shell rather than the part's own pins, or the catalog needs an entry at this terminal count.${wiring} It is compiled as open rather than refusing the pedal.`
|
|
3474
|
+
: reason === "registry-open"
|
|
3475
|
+
? `${head}: the registry states no executable model for it and it is compiled as open. Whatever it does in the real circuit is absent.`
|
|
3476
|
+
: reason === "declared-class-without-model"
|
|
3477
|
+
? `${head}: no registered part model binds it -- ${resolution.insteadOfRefusal ?? "the registry supplied none"} -- and ${device.identity.partNumber === null
|
|
3478
|
+
? `its source names no part number and declares only the generic ${device.identity.declaredType}, so there is nothing to look up`
|
|
3479
|
+
: `its source declares ${device.identity.declaredType}, a class this pipeline opens rather than refusing the pedal for`}. Fixing it means a registry entry, a pinout that fits, or a packet correction.`
|
|
3480
|
+
: `${head}: it was compiled as open by a path that records no reason, so this cannot say which. A new opening branch in \`resolveDevice\` needs to tag itself.`) + gapSuffix,
|
|
3481
|
+
...(gapClass === undefined ? {} : { gapClass }),
|
|
3482
|
+
});
|
|
3483
|
+
}
|
|
3484
|
+
return warnings;
|
|
3485
|
+
}
|
|
3486
|
+
/**
|
|
3487
|
+
* The loud half of `isElectricallyIsolated`: every identification-requiring device whose
|
|
3488
|
+
* nodes are all private got an `open` law instead of a refusal, and each one is named here
|
|
3489
|
+
* so a cluster isolated by a wiring mistake rather than by intent is never silently
|
|
3490
|
+
* swallowed. Runs over the ORIGINAL netlist (pre-section-expansion), because isolation is a
|
|
3491
|
+
* property of the drawn component, not of a registry section.
|
|
3492
|
+
*/
|
|
3493
|
+
export function findElectricallyIsolatedIcs(netlist) {
|
|
3494
|
+
const devicesTouchingNode = deviceCountByNode(netlist);
|
|
3495
|
+
const warnings = [];
|
|
3496
|
+
for (const device of netlist.devices) {
|
|
3497
|
+
if (!requiresIdentification(device)) {
|
|
3498
|
+
continue;
|
|
3499
|
+
}
|
|
3500
|
+
if (!isElectricallyIsolated(device, devicesTouchingNode)) {
|
|
3501
|
+
continue;
|
|
3502
|
+
}
|
|
3503
|
+
warnings.push({
|
|
3504
|
+
code: "electrically-isolated-ic",
|
|
3505
|
+
device: device.id,
|
|
3506
|
+
detail: `component ${device.id} shares no net with any other component, so it is treated ` +
|
|
3507
|
+
`as a view-only block and compiled as open. If that is a wiring mistake rather ` +
|
|
3508
|
+
`than a declared boundary, the source needs the missing nets, not a device model.`,
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3511
|
+
return warnings;
|
|
3512
|
+
}
|
|
3513
|
+
//# sourceMappingURL=device-laws.js.map
|