@vessel-dsp/compiler 0.5.0 → 0.7.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.
Files changed (88) hide show
  1. package/README.md +4 -2
  2. package/dist/bbd-clock.d.ts +42 -0
  3. package/dist/bbd-clock.d.ts.map +1 -1
  4. package/dist/bbd-clock.js +383 -1
  5. package/dist/bbd-clock.js.map +1 -1
  6. package/dist/compile.d.ts.map +1 -1
  7. package/dist/compile.js +316 -34
  8. package/dist/compile.js.map +1 -1
  9. package/dist/device-laws.d.ts.map +1 -1
  10. package/dist/device-laws.js +581 -98
  11. package/dist/device-laws.js.map +1 -1
  12. package/dist/fixtures/circuits.d.ts +23 -2
  13. package/dist/fixtures/circuits.d.ts.map +1 -1
  14. package/dist/fixtures/circuits.js +205 -2
  15. package/dist/fixtures/circuits.js.map +1 -1
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/lower.d.ts.map +1 -1
  21. package/dist/lower.js +13 -0
  22. package/dist/lower.js.map +1 -1
  23. package/dist/netlist.d.ts +30 -0
  24. package/dist/netlist.d.ts.map +1 -1
  25. package/dist/netlist.js +502 -8
  26. package/dist/netlist.js.map +1 -1
  27. package/dist/part-catalog.d.ts +42 -18
  28. package/dist/part-catalog.d.ts.map +1 -1
  29. package/dist/part-catalog.js +2518 -576
  30. package/dist/part-catalog.js.map +1 -1
  31. package/dist/partition.d.ts.map +1 -1
  32. package/dist/partition.js +6 -0
  33. package/dist/partition.js.map +1 -1
  34. package/dist/registry-data/bjt.d.ts +3 -0
  35. package/dist/registry-data/bjt.d.ts.map +1 -0
  36. package/dist/registry-data/bjt.js +1350 -0
  37. package/dist/registry-data/bjt.js.map +1 -0
  38. package/dist/registry-data/charge-pump.d.ts +3 -0
  39. package/dist/registry-data/charge-pump.d.ts.map +1 -0
  40. package/dist/registry-data/charge-pump.js +204 -0
  41. package/dist/registry-data/charge-pump.js.map +1 -0
  42. package/dist/registry-data/diode.d.ts +3 -0
  43. package/dist/registry-data/diode.d.ts.map +1 -0
  44. package/dist/registry-data/diode.js +786 -0
  45. package/dist/registry-data/diode.js.map +1 -0
  46. package/dist/registry-data/index.d.ts +21 -0
  47. package/dist/registry-data/index.d.ts.map +1 -0
  48. package/dist/registry-data/index.js +103 -0
  49. package/dist/registry-data/index.js.map +1 -0
  50. package/dist/registry-data/jfet.d.ts +3 -0
  51. package/dist/registry-data/jfet.d.ts.map +1 -0
  52. package/dist/registry-data/jfet.js +605 -0
  53. package/dist/registry-data/jfet.js.map +1 -0
  54. package/dist/registry-data/loader.d.ts +137 -0
  55. package/dist/registry-data/loader.d.ts.map +1 -0
  56. package/dist/registry-data/loader.js +350 -0
  57. package/dist/registry-data/loader.js.map +1 -0
  58. package/dist/registry-data/mosfet.d.ts +3 -0
  59. package/dist/registry-data/mosfet.d.ts.map +1 -0
  60. package/dist/registry-data/mosfet.js +63 -0
  61. package/dist/registry-data/mosfet.js.map +1 -0
  62. package/dist/registry-data/opamp.d.ts +37 -0
  63. package/dist/registry-data/opamp.d.ts.map +1 -0
  64. package/dist/registry-data/opamp.js +669 -0
  65. package/dist/registry-data/opamp.js.map +1 -0
  66. package/dist/registry-data/regulator.d.ts +3 -0
  67. package/dist/registry-data/regulator.d.ts.map +1 -0
  68. package/dist/registry-data/regulator.js +189 -0
  69. package/dist/registry-data/regulator.js.map +1 -0
  70. package/dist/registry-data/schema.d.ts +60 -0
  71. package/dist/registry-data/schema.d.ts.map +1 -0
  72. package/dist/registry-data/schema.js +151 -0
  73. package/dist/registry-data/schema.js.map +1 -0
  74. package/dist/registry-data/ujt.d.ts +3 -0
  75. package/dist/registry-data/ujt.d.ts.map +1 -0
  76. package/dist/registry-data/ujt.js +48 -0
  77. package/dist/registry-data/ujt.js.map +1 -0
  78. package/dist/registry-data/zener.d.ts +3 -0
  79. package/dist/registry-data/zener.d.ts.map +1 -0
  80. package/dist/registry-data/zener.js +547 -0
  81. package/dist/registry-data/zener.js.map +1 -0
  82. package/dist/registry.d.ts +93 -6
  83. package/dist/registry.d.ts.map +1 -1
  84. package/dist/registry.js +35 -7
  85. package/dist/registry.js.map +1 -1
  86. package/dist/types.d.ts +242 -19
  87. package/dist/types.d.ts.map +1 -1
  88. package/package.json +1 -1
@@ -19,7 +19,8 @@ import { checkDeclaredProgram } from "./declared-program.js";
19
19
  import { bjtPartRegisteredAsSections, identify, mayCarryRegistrySections, opampPartRegisteredAsOtherLaw, requiresIdentification, } from "./identify.js";
20
20
  import { isMultiDeviceTransistorShell } from "./unreadable-terminal-role.js";
21
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, registryOpampGainBandwidth, registryOpampSwingDrop, terminalWithRole, withRecordedReads, } from "./registry.js";
22
+ import { foldPartId, foldToken, pinoutMatches, registryEntryFor, registryLawFor, registryModelFor, registryOpampGainBandwidth, registryOpampSwing, opampOutputSwingOf, terminalWithRole, withRecordedReads, } from "./registry.js";
23
+ import { GROUND } from "./types.js";
23
24
  /** Room-temperature thermal voltage; the diode law needs a reference somewhere. */
24
25
  export const THERMAL_VOLTAGE = 0.025_852;
25
26
  /**
@@ -333,17 +334,33 @@ function opampRails(device, rails, nodeToVoltage) {
333
334
  return { high, low };
334
335
  }
335
336
  /**
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.
337
+ * The rails an op-amp's output can reach: each supply rail pulled in by the part's printed drop for
338
+ * that side, carried to this op-amp's own supply by the row's stated `translation`.
339
+ *
340
+ * `"constant"` applies the printed drops as printed. `"proportional"` multiplies both by the
341
+ * op-amp's supply span (`high - low`, so 9 for a 9 V single supply and 18 for a +-9 V pair) over the
342
+ * span the row was printed at. An unstated swing, an unknown rail, a span that is not positive, or a
343
+ * pair that would close the output interval (`high - dropHigh <= low + dropLow`) leaves the rails as
344
+ * they are, so a part with no entry renders bit-for-bit as before. `carried` is set only when the rails
345
+ * moved; `opamp-law-default-parameters` reads it to tell a pair used at its printed supply from one
346
+ * carried across a supply.
339
347
  */
340
- function railsAfterSwingDrop(rails, dropVolts) {
341
- if (dropVolts === null || dropVolts <= 0 || rails.high === null || rails.low === null) {
342
- return rails;
348
+ function railsAfterSwing(rails, swing) {
349
+ if (swing === null || rails.high === null || rails.low === null) {
350
+ return { rails, carried: null };
343
351
  }
344
- const high = rails.high - dropVolts;
345
- const low = rails.low + dropVolts;
346
- return high > low ? { high, low } : rails;
352
+ const span = rails.high - rails.low;
353
+ if (!(span > 0)) {
354
+ return { rails, carried: null };
355
+ }
356
+ const scale = swing.printed.translation === "proportional" ? span / swing.printed.supplyVolts : 1;
357
+ const highVolts = swing.highVolts * scale;
358
+ const lowVolts = swing.lowVolts * scale;
359
+ const high = rails.high - highVolts;
360
+ const low = rails.low + lowVolts;
361
+ return high > low
362
+ ? { rails: { high, low }, carried: { highVolts, lowVolts, circuitSupplyVolts: span } }
363
+ : { rails, carried: null };
347
364
  }
348
365
  /**
349
366
  * The one high-side and one low-side supply pin a device declares, read by the same role tokens
@@ -436,39 +453,32 @@ function quiescentSupplyDraw(device, registry, expanded, rails, nodeToVoltage) {
436
453
  { outcome: "law", device: id, law: { kind: "conductance", siemens: amps / span } },
437
454
  ];
438
455
  }
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();
456
+ export function attachDeviceLaws(netlist, registry) {
457
+ const regulatedBase = registrySuppliedVoltages(netlist, registry);
458
+ const nodeToVoltage = buildNodeToVoltageMap(netlist, regulatedBase);
459
+ const devicesTouchingNode = deviceCountByNode(netlist);
460
+ // A pump's rail is derived from the declared input, so every derivation runs against
461
+ // the document's own voltages -- before any pump output joins the map -- and the
462
+ // derived supplies merge into the rail set after. A pump can never back itself.
463
+ const pumpDerivations = new Map();
446
464
  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;
465
+ const derivation = pumpDerivation(device, registry, nodeToVoltage, devicesTouchingNode);
466
+ if (derivation !== null) {
467
+ pumpDerivations.set(device.id, derivation);
453
468
  }
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);
469
+ }
470
+ const regulated = [...regulatedBase];
471
+ for (const derivation of pumpDerivations.values()) {
472
+ if (derivation.kind !== "model") {
473
+ continue;
459
474
  }
460
- else if (vb !== undefined && a !== 0 && va === undefined) {
461
- map.set(a, vb);
475
+ regulated.push({ node: derivation.outputNode, volts: derivation.volts });
476
+ if (!nodeToVoltage.has(derivation.outputNode)) {
477
+ nodeToVoltage.set(derivation.outputNode, derivation.volts);
462
478
  }
463
479
  }
464
- return map;
465
- }
466
- export function attachDeviceLaws(netlist, registry) {
467
- const regulated = registrySuppliedVoltages(netlist, registry);
468
480
  const rails = supplyRails(netlist, regulated);
469
481
  const supply = supplyNodes(netlist);
470
- const nodeToVoltage = buildNodeToVoltageMap(netlist, regulated);
471
- const oneHopVoltage = buildOneHopVoltageMap(netlist, nodeToVoltage);
472
482
  const mainsFrequencyHz = mainsFrequency(netlist);
473
483
  // Stage 1 (`netlist.ts`) already resolved each control's real taper -- from the panel
474
484
  // control's own declaration, or failing that, from the first device bound to it -- onto
@@ -481,7 +491,6 @@ export function attachDeviceLaws(netlist, registry) {
481
491
  // is multi-section. Expanding into ordinary devices means partitioning, lowering and the runtime
482
492
  // see nothing new -- the alternative, a fourth `DeviceResolution` outcome, would have to be
483
493
  // understood by every consumer of one.
484
- const devicesTouchingNode = deviceCountByNode(netlist);
485
494
  // Nodes inside a registered package, numbered past every node the document uses.
486
495
  const internalNodes = new Map();
487
496
  const firstInternal = Math.max(0, ...netlist.nodes) + 1;
@@ -497,7 +506,7 @@ export function attachDeviceLaws(netlist, registry) {
497
506
  const devices = [];
498
507
  const resolutions = [];
499
508
  for (const device of netlist.devices) {
500
- const expanded = sectionDevices(device, registry, rails, supply, nodeToVoltage, internalNode, netlist);
509
+ const expanded = sectionDevices(device, registry, rails, supply, nodeToVoltage, internalNode, netlist, pumpDerivations.get(device.id) ?? null);
501
510
  const draw = quiescentSupplyDraw(device, registry, expanded !== null, rails, nodeToVoltage);
502
511
  if (draw !== null) {
503
512
  devices.push(draw[0]);
@@ -505,7 +514,7 @@ export function attachDeviceLaws(netlist, registry) {
505
514
  }
506
515
  if (expanded === null) {
507
516
  devices.push(device);
508
- resolutions.push(resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, oneHopVoltage, netlist));
517
+ resolutions.push(resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, netlist));
509
518
  continue;
510
519
  }
511
520
  for (const [section, resolution] of expanded) {
@@ -550,6 +559,12 @@ export function declaredRegulatorTerminals(device, model) {
550
559
  // kinds whose vocabulary carries them (`regulator`, `power-converter`, `voltage-source`,
551
560
  // `rail`, `battery`), and an `ic` may declare nothing but `pin`. A device that reaches here
552
561
  // carrying a supply electrode was therefore declared as a kind entitled to state one.
562
+ // Charge-pump parts never take this path: their volts are derived per circuit from the
563
+ // input node, and the placeholder volts in their entries must not be stamped. They are
564
+ // bound by `pumpDerivation` instead, which runs before this is consulted.
565
+ if (isChargePumpPart(device)) {
566
+ return null;
567
+ }
553
568
  if (model.sections.length !== 1) {
554
569
  return null;
555
570
  }
@@ -604,7 +619,16 @@ export function declaredRegulatorTerminals(device, model) {
604
619
  }
605
620
  function sectionDevices(device, registry, rails, supply, nodeToVoltage, internalNode = () => {
606
621
  throw new Error("no internal-node allocator");
607
- }, netlist) {
622
+ }, netlist, pump = null) {
623
+ // A decided pump never reaches generic expansion: the placeholder volts in the pump
624
+ // entries must not be stamped. An isolated pump defers to `resolveDevice`, which
625
+ // names the isolation before any pump question.
626
+ if (pump !== null) {
627
+ if (pump.kind === "isolated") {
628
+ return null;
629
+ }
630
+ return pumpSectionDevices(device, pump);
631
+ }
608
632
  // **`mayCarryRegistrySections`, not `requiresIdentification`.** An OTA reaches here so a
609
633
  // registered multi-section part (the BA662A's OTA plus its Darlington buffer) can be expanded;
610
634
  // when the registry has no matching entry this returns null and the caller falls through to
@@ -631,6 +655,25 @@ function sectionDevices(device, registry, rails, supply, nodeToVoltage, internal
631
655
  if (model.kind !== "sections" && model.kind !== "sections-core") {
632
656
  return null;
633
657
  }
658
+ // An explicit override cannot target one section of a package the registry expands: the
659
+ // section laws are the entry's numbers (a dual op-amp's two amplifiers share one part's
660
+ // figures), and `device.parameters` -- legacy or override -- never reach them. Accepting
661
+ // the block would be a silent drop, so it refuses by name instead. The way out is a
662
+ // source-declared `devices:` split, whose devices take the device-class law that the
663
+ // override does feed.
664
+ if (device.deviceOverride !== undefined) {
665
+ return [
666
+ [
667
+ device,
668
+ {
669
+ outcome: "unsupported",
670
+ device: device.id,
671
+ reason: `part ${identity.partId} is expanded from a registry sections entry, whose numbers the override cannot replace: ` +
672
+ `declare the package's devices explicitly (a "devices:" split) to take the device-class law an override feeds, or remove the DeviceOverride block`,
673
+ },
674
+ ],
675
+ ];
676
+ }
634
677
  // A `sections` entry's `terminals` index into the entry's canonical pin order, not the
635
678
  // declaration order the document happened to use. When the entry names its pins by role and
636
679
  // that naming is a clean bijection onto the device's declared terminals, resolve each
@@ -740,10 +783,12 @@ function sectionDevices(device, registry, rails, supply, nodeToVoltage, internal
740
783
  }
741
784
  // Read from the section's *parent* device, which is where the package's supply pins
742
785
  // 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;
786
+ const sectionSwing = section.law.kind === "ideal-opamp"
787
+ ? opampOutputSwingOf(section.law, identity.partId)
788
+ : null;
789
+ const sectionSwung = railsAfterSwing(opampRails(device, rails, nodeToVoltage), sectionSwing);
790
+ const railHigh = sectionSwung.rails.high;
791
+ const railLow = sectionSwung.rails.low;
747
792
  // An op-amp's rails come from the op-amp's own supply terminals or the circuit's supply set,
748
793
  // not from the registry -- the same substitution a declared `opamp` gets. A registry cannot know them:
749
794
  // it describes a part, and the rails are a property of the pedal the part is fitted to.
@@ -760,8 +805,16 @@ function sectionDevices(device, registry, rails, supply, nodeToVoltage, internal
760
805
  // The registry states the part's gain; only the rails are the pedal's.
761
806
  openLoopGain: section.law.openLoopGain,
762
807
  // 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 }),
808
+ // states how close it gets to the rails from one that does not, and a pair
809
+ // used at its printed supply from one the lowering carried across a supply.
810
+ ...(sectionSwing === null
811
+ ? {}
812
+ : {
813
+ outputSwingDropHighVolts: sectionSwing.highVolts,
814
+ outputSwingDropLowVolts: sectionSwing.lowVolts,
815
+ outputSwingPrinted: sectionSwing.printed,
816
+ }),
817
+ ...(sectionSwung.carried === null ? {} : { outputSwingCarried: sectionSwung.carried }),
765
818
  // The part's datasheet product, when the entry states one. Absent stays
766
819
  // absent: it lowers to a `null` stamp, which the compile names.
767
820
  ...(section.law.gainBandwidthHz === undefined
@@ -847,6 +900,34 @@ function barePinPosition(macro, roles, port) {
847
900
  }
848
901
  return position;
849
902
  }
903
+ /**
904
+ * The terminal the audio leaves a chained two-section part at, or `null` when the part is not
905
+ * chained in this document.
906
+ *
907
+ * Chained means one thing: the first section's output and the second section's input are the same
908
+ * non-ground node, which is connectivity, not a name. All three terminals must resolve to exactly
909
+ * one role each, so a document that names the sections differently, or ties the second section's
910
+ * input to ground (the Deluxe Memory Man uses only section A), falls through to the single-section
911
+ * port unchanged.
912
+ */
913
+ function cascadedAudioOut(device, macro, sectionAudioOut) {
914
+ const cascade = macro.ports.cascade;
915
+ if (cascade === undefined || sectionAudioOut === null) {
916
+ return null;
917
+ }
918
+ const roles = device.identity.terminalRoles;
919
+ const sectionOut = terminalWithRole(roles, cascade.sectionOut);
920
+ const nextIn = terminalWithRole(roles, cascade.nextIn);
921
+ const finalOut = terminalWithRole(roles, cascade.finalOut);
922
+ if (sectionOut === null || nextIn === null || finalOut === null || sectionOut !== sectionAudioOut) {
923
+ return null;
924
+ }
925
+ const joint = device.nodes[sectionOut];
926
+ if (joint === undefined || joint === GROUND || device.nodes[nextIn] !== joint) {
927
+ return null;
928
+ }
929
+ return finalOut;
930
+ }
850
931
  /**
851
932
  * Thin wrapper that brackets the real resolution so every registry entry consulted while computing
852
933
  * this macro's parameters is recorded against the macro's device id. **No behaviour change** -- the
@@ -864,8 +945,9 @@ registry, netlist) {
864
945
  const roles = device.identity.terminalRoles;
865
946
  const audioIn = terminalWithRole(roles, macro.ports.audioIn) ??
866
947
  barePinPosition(macro, roles, "audioIn");
867
- const audioOut = terminalWithRole(roles, macro.ports.audioOut) ??
948
+ const sectionAudioOut = terminalWithRole(roles, macro.ports.audioOut) ??
868
949
  barePinPosition(macro, roles, "audioOut");
950
+ const audioOut = cascadedAudioOut(device, macro, sectionAudioOut) ?? sectionAudioOut;
869
951
  // A second output, only for a part that declares one and a device that names it. Absent is
870
952
  // the ordinary case and stays a one-output macro: unlike the ports above, failing to find
871
953
  // this is not a refusal, because most parts genuinely have one output.
@@ -1309,6 +1391,12 @@ function registrySuppliedVoltages(netlist, registry) {
1309
1391
  if (device.parameters.volts !== undefined) {
1310
1392
  continue;
1311
1393
  }
1394
+ // Pump parts never supply a static rail: their output is derived per circuit from
1395
+ // the input node by `pumpDerivation`, and the placeholder volts in their entries
1396
+ // must not leak into the rail set through a role that happens to read as an output.
1397
+ if (isChargePumpPart(device)) {
1398
+ continue;
1399
+ }
1312
1400
  const identity = identify(device, registry);
1313
1401
  if (identity === null) {
1314
1402
  continue;
@@ -1421,29 +1509,375 @@ export function deviceCountByNode(netlist) {
1421
1509
  }
1422
1510
  return counts;
1423
1511
  }
1512
+ /**
1513
+ * Charge-pump inverter parts, by folded part number: every spelling the DIY BOM census
1514
+ * attributes to the family (TC1044S/SCPA, LT1054/CP, 7660 // SCPAZ, TC7662BC, ICL7660SCPA(Z),
1515
+ * LTC1144, MAX1044/S) plus the order codes and package markings the datasheets print.
1516
+ * Compared whole with `foldPartId`, never by substring: membership routes a part to the
1517
+ * pump handling, it does not model it -- modelling needs a registry entry, and a member
1518
+ * with no entry falls through to the honest unknown-chip path.
1519
+ */
1424
1520
  const CHARGE_PUMP_PARTS = new Set([
1425
- "tc1044",
1521
+ "tc1044s",
1426
1522
  "tc1044scpa",
1427
- "max1044",
1523
+ "tc1044scoa",
1524
+ "tc1044sepa",
1525
+ "tc1044seoa",
1526
+ "tc1044sija",
1527
+ "tc1044smja",
1528
+ "icl7660",
1529
+ "icl7660cpa",
1530
+ "icl7660cpaz",
1531
+ "icl7660cba",
1532
+ "icl7660cbaz",
1533
+ "7660cpa",
1534
+ "7660cpaz",
1535
+ "7660cba",
1536
+ "7660cbaz",
1537
+ "icl7660a",
1538
+ "icl7660acpa",
1539
+ "icl7660acpaz",
1540
+ "icl7660acba",
1541
+ "icl7660acbaz",
1542
+ "icl7660aiba",
1543
+ "icl7660aibaz",
1544
+ "7660acpa",
1545
+ "7660acpaz",
1546
+ "7660acba",
1547
+ "7660acbaz",
1548
+ "7660aiba",
1549
+ "7660aibaz",
1550
+ "icl7660s",
1551
+ "icl7660scpa",
1552
+ "icl7660scpaz",
1553
+ "7660scpa",
1554
+ "7660scpaz",
1555
+ "tc7662b",
1556
+ "tc7662bcpa",
1557
+ "tc7662bepa",
1558
+ "tc7662bcoa",
1559
+ "tc7662beoa",
1428
1560
  "7662",
1429
- "lt1054cp",
1430
1561
  "lt1054",
1562
+ "lt1054c",
1563
+ "lt1054cp",
1564
+ "lt1054cpe4",
1565
+ "lt1054i",
1566
+ "ltc1144",
1567
+ "ltc1144cn8",
1568
+ "ltc1144in8",
1569
+ "ltc1144cs8",
1570
+ "ltc1144is8",
1571
+ "max1044",
1572
+ "max1044cpa",
1573
+ "max1044csa",
1574
+ "max1044epa",
1575
+ "max1044esa",
1576
+ // Census spellings with no registry entry: routed here so a pump whose rail the
1577
+ // document declares still opens honestly, and one whose rail is undeclared refuses
1578
+ // as an unknown chip instead of claiming a modelled rail.
1579
+ "tc7662bc",
1580
+ "max1044s",
1581
+ "7660",
1431
1582
  ]);
1583
+ /** Whether this device names a charge-pump inverter part, compared whole. */
1584
+ function isChargePumpPart(device) {
1585
+ const partNumber = device.identity.partNumber;
1586
+ if (partNumber === null) {
1587
+ return false;
1588
+ }
1589
+ return CHARGE_PUMP_PARTS.has(foldPartId(partNumber));
1590
+ }
1591
+ /**
1592
+ * The closed role vocabularies a pump's three supply terminals are bound by, compared as
1593
+ * whole folded tokens by `terminalWithRole` -- never as substrings, never against a
1594
+ * description. These query the ENTRY's role groups (which carry the datasheet pinout in
1595
+ * package order), so a document naming its terminals in any order binds, and a document
1596
+ * numbering its pins binds positionally instead.
1597
+ */
1598
+ const PUMP_INPUT_ROLES = ["vplus", "vin", "vdd", "vcc", "input"];
1599
+ const PUMP_OUTPUT_ROLES = [
1600
+ "negativeoutput",
1601
+ "voutnegative",
1602
+ "vout",
1603
+ "voutput",
1604
+ "out",
1605
+ "output",
1606
+ ];
1607
+ const PUMP_GROUND_ROLES = ["gnd", "ground"];
1432
1608
  /**
1433
- * A charge-pump IC whose output node is already backed by a declared rail is a
1609
+ * A charge-pump IC whose OUTPUT node is already backed by a declared rail is a
1434
1610
  * view-only structural anchor: the rail supplies the voltage, the IC adds nothing.
1435
1611
  * The Klon's MAX1044 is handled by `isElectricallyIsolated` (all nodes private);
1436
1612
  * this covers the wider class where the pump shares nets with the declared rail.
1613
+ *
1614
+ * Tests the pump's OUTPUT node, by role, and nothing else. The previous version asked
1615
+ * whether ANY non-ground node of the pump touched a declared OR one-hop voltage, and a
1616
+ * reservoir capacitor to ground was enough to satisfy it: with both Plumes rails deleted,
1617
+ * nodes 35 and 36 each one-hop to 0 V through C102/C101, and the pump claimed a modelled
1618
+ * rail on ground continuity alone. Ground is not a rail, and an input rail is not the
1619
+ * output rail -- pigtronix's LT1054 sat on the same false positive through its input
1620
+ * capacitor, and moogerfooger's 7662 through its output capacitor.
1437
1621
  */
1438
- function isChargePumpWithDeclaredRail(device, nodeToVoltage, oneHopVoltage) {
1439
- const partNumber = device.identity.partNumber;
1440
- if (partNumber === null) {
1622
+ function isChargePumpWithDeclaredRail(device, nodeToVoltage) {
1623
+ if (!isChargePumpPart(device)) {
1441
1624
  return false;
1442
1625
  }
1443
- if (!CHARGE_PUMP_PARTS.has(partNumber.toLowerCase())) {
1626
+ const output = chargePumpOutputIndex(device);
1627
+ if (output === null) {
1444
1628
  return false;
1445
1629
  }
1446
- return device.nodes.some((node) => node !== 0 && (nodeToVoltage.has(node) || oneHopVoltage.has(node)));
1630
+ const node = device.nodes[output];
1631
+ if (node === undefined || node === 0) {
1632
+ return false;
1633
+ }
1634
+ return nodeToVoltage.has(node);
1635
+ }
1636
+ /**
1637
+ * The index of the pump's inverted-output terminal, or `null` when the document does
1638
+ * not settle it.
1639
+ *
1640
+ * A declared supply electrode decides first: a shell that states `ground` plus
1641
+ * `negative` (or `positive`, the regulator convention deluxe-memory-man uses for its
1642
+ * -15 V output) names its output outright. Otherwise the folded terminal names are read
1643
+ * against the output vocabulary -- whole tokens, unambiguous or nothing.
1644
+ */
1645
+ function chargePumpOutputIndex(device) {
1646
+ const declared = device.identity.declaredTerminalRoles;
1647
+ const onlyDeclared = (role) => {
1648
+ const found = declared.flatMap((value, index) => value === role ? [index] : []);
1649
+ return found.length === 1 ? found[0] : null;
1650
+ };
1651
+ const fromDeclaration = onlyDeclared("negative") ?? onlyDeclared("positive");
1652
+ if (fromDeclaration !== null) {
1653
+ return fromDeclaration;
1654
+ }
1655
+ return terminalWithRole(device.identity.terminalRoles, PUMP_OUTPUT_ROLES);
1656
+ }
1657
+ function pumpDerivation(device, registry, nodeToVoltage, devicesTouchingNode) {
1658
+ if (!isChargePumpPart(device)) {
1659
+ return null;
1660
+ }
1661
+ // Isolation stays the isolation reporter's beat: a pump on private nodes is view-only
1662
+ // however it is part-numbered, and `resolveDevice` names it before any pump question.
1663
+ if (isElectricallyIsolated(device, devicesTouchingNode)) {
1664
+ return { kind: "isolated" };
1665
+ }
1666
+ const identity = identify(device, registry);
1667
+ if (identity === null) {
1668
+ return null;
1669
+ }
1670
+ const entry = registryEntryFor(registry, identity, device.nodes.length);
1671
+ if (entry === null || entry.model.kind !== "sections") {
1672
+ return null;
1673
+ }
1674
+ const section = entry.model.sections[0];
1675
+ if (entry.model.sections.length !== 1 ||
1676
+ section === undefined ||
1677
+ section.law.kind !== "voltage-source" ||
1678
+ section.terminals.length !== 2) {
1679
+ return {
1680
+ kind: "unbound",
1681
+ detail: `part ${identity.partId} is routed to the charge-pump derivation but its registry ` +
1682
+ `entry is not a single two-terminal voltage-source section, so no rail can be ` +
1683
+ `derived from it`,
1684
+ };
1685
+ }
1686
+ const sourceOhms = section.law.sourceOhms;
1687
+ // Which package positions the supply input, inverted output, and reference sit at,
1688
+ // read off the ENTRY's role groups -- the datasheet pinout in package order -- so one
1689
+ // entry serves every declaration order that names its terminals at all.
1690
+ const groups = entry.terminalRoleGroups;
1691
+ const groupIndex = (vocab) => {
1692
+ const wanted = new Set(vocab.map((alias) => foldToken(alias)));
1693
+ const hits = [];
1694
+ groups.forEach((group, index) => {
1695
+ if (group.some((alias) => wanted.has(foldToken(alias)))) {
1696
+ hits.push(index);
1697
+ }
1698
+ });
1699
+ return hits.length === 1 ? hits[0] : null;
1700
+ };
1701
+ const inputPos = groupIndex(PUMP_INPUT_ROLES);
1702
+ const outputPos = groupIndex(PUMP_OUTPUT_ROLES);
1703
+ const groundPos = groupIndex(PUMP_GROUND_ROLES);
1704
+ if (inputPos === null || outputPos === null || groundPos === null) {
1705
+ return {
1706
+ kind: "unbound",
1707
+ detail: `part ${identity.partId} is registered as a charge pump but its entry does not ` +
1708
+ `name a distinct supply input, inverted output, and ground reference`,
1709
+ };
1710
+ }
1711
+ const roles = device.identity.terminalRoles;
1712
+ const declared = device.identity.declaredTerminalRoles;
1713
+ const onlyDeclared = (role) => {
1714
+ const found = declared.flatMap((value, index) => value === role ? [index] : []);
1715
+ return found.length === 1 ? found[0] : null;
1716
+ };
1717
+ // A declared supply electrode outranks the entry's pin numbering, the same rule the
1718
+ // regulator path documents: the reference is `ground`, the inverted output is
1719
+ // `negative` (or `positive`, the convention deluxe-memory-man's -15 V output uses),
1720
+ // and the input is the one remaining terminal sitting on a declared nonzero voltage.
1721
+ const declaredBinding = (() => {
1722
+ const ground = onlyDeclared("ground");
1723
+ const output = onlyDeclared("negative") ?? onlyDeclared("positive");
1724
+ if (ground === null || output === null || ground === output) {
1725
+ return null;
1726
+ }
1727
+ const candidates = roles
1728
+ .map((_, index) => index)
1729
+ .filter((index) => index !== ground && index !== output)
1730
+ .filter((index) => {
1731
+ const node = device.nodes[index];
1732
+ if (node === undefined || node === 0) {
1733
+ return false;
1734
+ }
1735
+ const volts = nodeToVoltage.get(node);
1736
+ return volts !== undefined && volts !== 0;
1737
+ });
1738
+ if (candidates.length !== 1) {
1739
+ return null;
1740
+ }
1741
+ return { input: candidates[0], output, ground };
1742
+ })();
1743
+ // Otherwise the folded terminal names, bound through the entry's own aliases so any
1744
+ // declaration order lands on the right pins.
1745
+ const roleBinding = (() => {
1746
+ const input = terminalWithRole(roles, [...groups[inputPos]]);
1747
+ const output = terminalWithRole(roles, [...groups[outputPos]]);
1748
+ const ground = terminalWithRole(roles, [...groups[groundPos]]);
1749
+ if (input === null || output === null || ground === null) {
1750
+ return null;
1751
+ }
1752
+ if (new Set([input, output, ground]).size !== 3) {
1753
+ return null;
1754
+ }
1755
+ return { input, output, ground };
1756
+ })();
1757
+ // Otherwise a bare-pin shell in package order: terminals numbered pin1..pinN fold to
1758
+ // no role at all, so declaration order against the entry's package-order pinout is
1759
+ // the only reading, exactly as the regulator entries already assume.
1760
+ const positionalBinding = (() => {
1761
+ if (!roles.every((role) => role === null)) {
1762
+ return null;
1763
+ }
1764
+ if (entry.model.kind !== "sections" ||
1765
+ entry.model.pinout.length !== roles.length) {
1766
+ return null;
1767
+ }
1768
+ return { input: inputPos, output: outputPos, ground: groundPos };
1769
+ })();
1770
+ const binding = declaredBinding ?? roleBinding ?? positionalBinding;
1771
+ if (binding === null) {
1772
+ return {
1773
+ kind: "unbound",
1774
+ detail: `part ${identity.partId} is registered as a charge pump, but neither its terminal ` +
1775
+ `names, its declared supply electrodes, nor its pin order bind a supply input, ` +
1776
+ `inverted output, and ground reference: ${device.nodes.length} terminals against ` +
1777
+ `a ${entry.model.pinout.length}-terminal pinout`,
1778
+ };
1779
+ }
1780
+ const inputNode = device.nodes[binding.input];
1781
+ const outputNode = device.nodes[binding.output];
1782
+ const groundNode = device.nodes[binding.ground];
1783
+ if (inputNode === undefined ||
1784
+ outputNode === undefined ||
1785
+ groundNode === undefined) {
1786
+ return {
1787
+ kind: "unbound",
1788
+ detail: `part ${identity.partId} charge-pump binding names a terminal this component ` +
1789
+ `does not declare`,
1790
+ };
1791
+ }
1792
+ // The reference is circuit ground. A pump tied to anything else is a shell the
1793
+ // derivation cannot place, not a rail against an unknown reference.
1794
+ if (groundNode !== 0 || outputNode === 0) {
1795
+ return {
1796
+ kind: "unbound",
1797
+ detail: `part ${identity.partId} charge-pump binding puts the reference off circuit ` +
1798
+ `ground, so no rail can be derived from it`,
1799
+ };
1800
+ }
1801
+ // The document keeps the last word: an output the document already drives --
1802
+ // declared rail or regulator-supplied -- stays the document's, and the pump stays open.
1803
+ if (nodeToVoltage.has(outputNode)) {
1804
+ return { kind: "backed" };
1805
+ }
1806
+ const inputVolts = nodeToVoltage.get(inputNode);
1807
+ if (inputVolts === undefined || inputVolts === 0) {
1808
+ return { kind: "no-input" };
1809
+ }
1810
+ // An output no other device touches cannot move the solve: stamping it would only
1811
+ // widen the circuit-wide rail extremes other devices fall back to.
1812
+ if ((devicesTouchingNode.get(outputNode) ?? 0) < 2) {
1813
+ return { kind: "unloaded" };
1814
+ }
1815
+ const lossVolts = entry.chargePumpLossVolts ?? 0;
1816
+ return {
1817
+ kind: "model",
1818
+ inputNode,
1819
+ outputNode,
1820
+ groundNode,
1821
+ inputVolts,
1822
+ volts: -inputVolts - Math.sign(-inputVolts) * lossVolts,
1823
+ sourceOhms: section.law.sourceOhms,
1824
+ };
1825
+ }
1826
+ /**
1827
+ * A derived pump rail as one ordinary section device plus its law, or the original device
1828
+ * opened with the reason the derivation recorded. Never `null` for a device the
1829
+ * derivation decided: the placeholder volts in the pump entries must not reach generic
1830
+ * expansion, so every decided device is consumed here.
1831
+ */
1832
+ function pumpSectionDevices(device, derivation) {
1833
+ switch (derivation.kind) {
1834
+ case "backed":
1835
+ return [[device, openIc(device, "charge-pump-declared-rail")]];
1836
+ case "no-input":
1837
+ return [[device, openIc(device, "charge-pump-no-input")]];
1838
+ case "unloaded":
1839
+ return [[device, openIc(device, "charge-pump-output-unloaded")]];
1840
+ case "unbound":
1841
+ return [
1842
+ [
1843
+ device,
1844
+ {
1845
+ outcome: "law",
1846
+ device: device.id,
1847
+ law: { kind: "open" },
1848
+ openReason: "registry-arity-mismatch",
1849
+ insteadOfRefusal: derivation.detail,
1850
+ },
1851
+ ],
1852
+ ];
1853
+ case "model": {
1854
+ const sectionId = `${device.id}#0`;
1855
+ const sectionDevice = {
1856
+ ...device,
1857
+ id: sectionId,
1858
+ nodes: [derivation.outputNode, derivation.groundNode],
1859
+ identity: {
1860
+ ...device.identity,
1861
+ terminalRoles: canonicalRoles("voltage-source", 2),
1862
+ declaredTerminalRoles: canonicalRoles("voltage-source", 2),
1863
+ },
1864
+ };
1865
+ return [
1866
+ [
1867
+ sectionDevice,
1868
+ {
1869
+ outcome: "law",
1870
+ device: sectionId,
1871
+ law: {
1872
+ kind: "voltage-source",
1873
+ volts: derivation.volts,
1874
+ sourceOhms: derivation.sourceOhms,
1875
+ },
1876
+ },
1877
+ ],
1878
+ ];
1879
+ }
1880
+ }
1447
1881
  }
1448
1882
  /**
1449
1883
  * An OTA's signal terminals, bound by the names the device declares for them.
@@ -1652,7 +2086,7 @@ function packageDeviceParameters(device, registry) {
1652
2086
  }
1653
2087
  return parameters;
1654
2088
  }
1655
- function resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, oneHopVoltage, netlist) {
2089
+ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, netlist) {
1656
2090
  // A component declared as one transistor that is really several devices in one shell. Opened
1657
2091
  // rather than stamped, for the reason `isNonExecutableIcSupportShell` opens an IC shell: there
1658
2092
  // is no single device here to describe, and a transistor law would invent one. Reported by
@@ -1685,7 +2119,7 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
1685
2119
  // `findElectricallyIsolatedIcs`, which compile() runs over the same predicate.
1686
2120
  return openIc(device, "electrically-isolated");
1687
2121
  }
1688
- if (isChargePumpWithDeclaredRail(device, nodeToVoltage, oneHopVoltage)) {
2122
+ if (isChargePumpWithDeclaredRail(device, nodeToVoltage)) {
1689
2123
  // The rail the pump generates is separately declared as a voltage-carrying
1690
2124
  // port; the IC itself is a view-only structural anchor, not a solved model.
1691
2125
  return openIc(device, "charge-pump-declared-rail");
@@ -1903,6 +2337,7 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
1903
2337
  if (declared.breakdownVolts !== undefined ||
1904
2338
  declared.saturationCurrent !== undefined ||
1905
2339
  declared.seriesResistance !== undefined ||
2340
+ declared.emissionCoefficient !== undefined ||
1906
2341
  device.isLed === true) {
1907
2342
  return {
1908
2343
  outcome: "law",
@@ -1912,6 +2347,8 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
1912
2347
  breakdownVolts: declared.breakdownVolts ?? catalogued.breakdownVolts,
1913
2348
  saturationCurrent: declared.saturationCurrent ?? catalogued.saturationCurrent,
1914
2349
  seriesResistance: declared.seriesResistance ?? catalogued.seriesResistance,
2350
+ emissionCoefficient: declared.emissionCoefficient ?? catalogued.emissionCoefficient,
2351
+ thermalVoltage: declared.thermalVoltage ?? catalogued.thermalVoltage,
1915
2352
  isLed: device.isLed === true ? true : catalogued.isLed,
1916
2353
  },
1917
2354
  };
@@ -1928,7 +2365,8 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
1928
2365
  // If it is an LED, use physical LED defaults (higher forward knee ~1.8V to 2.2V),
1929
2366
  // otherwise use class defaults (1N4148-ish silicon signal diode).
1930
2367
  saturationCurrent: device.parameters.saturationCurrent ?? (isLed ? 1e-12 : 2.52e-9),
1931
- emissionCoefficient: 1.752,
2368
+ // The source's own `EmissionCoefficient` when it states one, else the class default.
2369
+ emissionCoefficient: device.parameters.emissionCoefficient ?? 1.752,
1932
2370
  thermalVoltage: device.parameters.thermalVoltage ?? (isLed ? 0.05 : THERMAL_VOLTAGE),
1933
2371
  // 0 for an ordinary diode. Read from the source, never inferred from a part
1934
2372
  // number: `1N4733` is a 5.1 V zener and `1N4148` is not, and telling them apart
@@ -1984,10 +2422,10 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
1984
2422
  },
1985
2423
  };
1986
2424
  case "opamp": {
1987
- const declaredDrop = registryOpampSwingDrop(registry, device.identity.partNumber);
1988
- const declaredRails = railsAfterSwingDrop(opampRails(device, rails, nodeToVoltage), declaredDrop);
1989
- const railHigh = declaredRails.high;
1990
- const railLow = declaredRails.low;
2425
+ const declaredSwing = registryOpampSwing(registry, device.identity.partNumber);
2426
+ const declaredSwung = railsAfterSwing(opampRails(device, rails, nodeToVoltage), declaredSwing);
2427
+ const railHigh = declaredSwung.rails.high;
2428
+ const railLow = declaredSwung.rails.low;
1991
2429
  // The gain-bandwidth product: the packet's own typed `GBP` first, else the datasheet figure
1992
2430
  // of the exact part id, else none. Never a class number -- `null` lowers to a stamp the
1993
2431
  // consoles run on their default, and `opamp-gain-bandwidth-default` names it.
@@ -2001,7 +2439,14 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
2001
2439
  railHigh,
2002
2440
  railLow,
2003
2441
  openLoopGain: device.parameters.openLoopGain ?? DEFAULT_OPEN_LOOP_GAIN,
2004
- ...(declaredDrop === null ? {} : { outputSwingDropVolts: declaredDrop }),
2442
+ ...(declaredSwing === null
2443
+ ? {}
2444
+ : {
2445
+ outputSwingDropHighVolts: declaredSwing.highVolts,
2446
+ outputSwingDropLowVolts: declaredSwing.lowVolts,
2447
+ outputSwingPrinted: declaredSwing.printed,
2448
+ }),
2449
+ ...(declaredSwung.carried === null ? {} : { outputSwingCarried: declaredSwung.carried }),
2005
2450
  ...(gainBandwidthHz === null ? {} : { gainBandwidthHz }),
2006
2451
  },
2007
2452
  };
@@ -2145,29 +2590,41 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
2145
2590
  law: { kind: "voltage-source", volts, sourceOhms: SUPPLY_SOURCE_OHMS },
2146
2591
  };
2147
2592
  }
2148
- case "bjt":
2593
+ case "bjt": {
2594
+ // A registered part refines the class defaults, exactly as the diode and
2595
+ // JFET-gate cases do. Exact part number only -- see `registryLawFor`.
2596
+ // Declared parameters win over everything, every time: the entries exist
2597
+ // to fill absence, not to overrule a packet that states the value.
2598
+ // Unregistered parts keep the class default bit-identically.
2599
+ const catalogued = registryLawFor(registry, device.identity.partNumber, "bjt");
2600
+ const entry = catalogued !== null && catalogued.kind === "bjt" ? catalogued : null;
2149
2601
  return {
2150
2602
  outcome: "law",
2151
2603
  device: device.id,
2152
2604
  law: {
2153
2605
  kind: "bjt",
2154
2606
  polarity: device.parameters.pnp === 1 ? "pnp" : "npn",
2155
- saturationCurrent: device.parameters.saturationCurrent ?? SILICON_SATURATION_CURRENT,
2156
- forwardBeta: device.parameters.beta ?? SILICON_FORWARD_BETA,
2157
- reverseBeta: device.parameters.reverseBeta ?? 1,
2158
- thermalVoltage: THERMAL_VOLTAGE,
2607
+ saturationCurrent: device.parameters.saturationCurrent ??
2608
+ entry?.saturationCurrent ??
2609
+ SILICON_SATURATION_CURRENT,
2610
+ forwardBeta: device.parameters.beta ?? entry?.forwardBeta ?? SILICON_FORWARD_BETA,
2611
+ reverseBeta: device.parameters.reverseBeta ?? entry?.reverseBeta ?? 1,
2612
+ thermalVoltage: device.parameters.thermalVoltage ??
2613
+ entry?.thermalVoltage ??
2614
+ THERMAL_VOLTAGE,
2159
2615
  // Absent for every silicon part in the corpus, and for the one germanium
2160
2616
  // packet whose `LeakageCurrent` is a sentence rather than a value.
2161
- leakageAmps: device.parameters.leakageAmps ?? 0,
2617
+ leakageAmps: device.parameters.leakageAmps ?? entry?.leakageAmps ?? 0,
2162
2618
  },
2163
2619
  };
2620
+ }
2164
2621
  case "jfet": {
2165
- // A registered part refines the class gate, exactly as `GENERIC_TUBE_GRID`'s
2622
+ // A registered part refines the class channel, exactly as `GENERIC_TUBE_GRID`'s
2166
2623
  // note says a registry entry should. Exact part number only -- see
2167
- // `registryLawFor`. The refinement is the gate term alone: the channel
2168
- // stays the declared-or-class law byte-for-byte (rank-dependent, and the
2169
- // database beta disagrees with at least one packet's record), so a covered
2170
- // part's channel is indistinguishable from an unregistered one's. Declared
2624
+ // `registryLawFor`. The 2SK30A entry's channel repeats the class default by
2625
+ // construction (rank-dependent, and the database beta disagrees with at
2626
+ // least one packet's record), so a covered part's channel is
2627
+ // indistinguishable from an unregistered one's. Declared
2171
2628
  // Vt0/Beta win over everything, every time: the entries exist to fill
2172
2629
  // absence, not to overrule a packet that states the value (the same overlay
2173
2630
  // the diode case applies). Unregistered parts keep the class default
@@ -2184,13 +2641,17 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
2184
2641
  channel: device.parameters.pChannel === 1 ? "p" : "n",
2185
2642
  // A JFET is depletion mode: it conducts at zero gate bias and pinches
2186
2643
  // off as the gate goes negative, so the threshold is negative.
2187
- thresholdVolts: device.parameters.thresholdVolts ?? JFET_DEFAULT_THRESHOLD_VOLTS,
2188
- transconductance: device.parameters.transconductance ?? JFET_DEFAULT_TRANSCONDUCTANCE,
2189
- channelLengthModulation: 0,
2644
+ thresholdVolts: device.parameters.thresholdVolts ??
2645
+ entry?.thresholdVolts ??
2646
+ JFET_DEFAULT_THRESHOLD_VOLTS,
2647
+ transconductance: device.parameters.transconductance ??
2648
+ entry?.transconductance ??
2649
+ JFET_DEFAULT_TRANSCONDUCTANCE,
2650
+ channelLengthModulation: entry?.channelLengthModulation ?? 0,
2190
2651
  // Subthreshold conduction (see the `fet` law docs in types.ts): the
2191
2652
  // gradual region a knee-biased JFET actually sweeps in. MOSFETs
2192
2653
  // keep 0 below.
2193
- subthresholdVolts: 0.07,
2654
+ subthresholdVolts: entry?.subthresholdVolts ?? 0.07,
2194
2655
  // A JFET gate is a silicon PN junction, so it conducts once forward-biased.
2195
2656
  // Saturation current and scale are **reused from the triode grid**, which
2196
2657
  // `compact-mna-dynamic-triode-cell` reports 0 Newton failures with; the onset
@@ -2199,30 +2660,39 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
2199
2660
  // part refines the current through its catalog entry, and the
2200
2661
  // unregistered default stays exactly what it was.
2201
2662
  gateSaturationCurrent: entry?.gateSaturationCurrent ?? 1.0e-5,
2202
- gateOnsetVolts: 0.5,
2203
- gateScaleVolts: 0.06,
2663
+ gateOnsetVolts: entry?.gateOnsetVolts ?? 0.5,
2664
+ gateScaleVolts: entry?.gateScaleVolts ?? 0.06,
2204
2665
  },
2205
2666
  };
2206
2667
  }
2207
- case "mosfet":
2668
+ case "mosfet": {
2669
+ // The same registry refinement as the JFET channel above: a registered
2670
+ // part refines the class defaults, declared parameters win, and
2671
+ // unregistered parts keep the class default bit-identically. Exact part
2672
+ // number only. This shares the `fet` law kind with JFET entries; part
2673
+ // ids are disjoint across the two classes, so a lookup by exact folded
2674
+ // part id cannot cross from one to the other.
2675
+ const catalogued = registryLawFor(registry, device.identity.partNumber, "fet");
2676
+ const entry = catalogued !== null && catalogued.kind === "fet" ? catalogued : null;
2208
2677
  return {
2209
2678
  outcome: "law",
2210
2679
  device: device.id,
2211
2680
  law: {
2212
2681
  kind: "fet",
2213
2682
  channel: device.parameters.pChannel === 1 ? "p" : "n",
2214
- thresholdVolts: device.parameters.thresholdVolts ?? 2,
2215
- transconductance: device.parameters.transconductance ?? 1e-3,
2216
- channelLengthModulation: 0,
2683
+ thresholdVolts: device.parameters.thresholdVolts ?? entry?.thresholdVolts ?? 2,
2684
+ transconductance: device.parameters.transconductance ?? entry?.transconductance ?? 1e-3,
2685
+ channelLengthModulation: entry?.channelLengthModulation ?? 0,
2217
2686
  // Insulated gate: no subthreshold region is modelled, behavior unchanged.
2218
- subthresholdVolts: 0,
2687
+ subthresholdVolts: entry?.subthresholdVolts ?? 0,
2219
2688
  // An insulated gate draws no current at any bias. Zero, deliberately: this
2220
2689
  // is what the law did for every FET before the JFET junction was added.
2221
- gateSaturationCurrent: 0,
2222
- gateOnsetVolts: 0,
2223
- gateScaleVolts: 1,
2690
+ gateSaturationCurrent: entry?.gateSaturationCurrent ?? 0,
2691
+ gateOnsetVolts: entry?.gateOnsetVolts ?? 0,
2692
+ gateScaleVolts: entry?.gateScaleVolts ?? 1,
2224
2693
  },
2225
2694
  };
2695
+ }
2226
2696
  case "ota":
2227
2697
  return otaDeviceClassLaw(device);
2228
2698
  case "inverter":
@@ -2403,6 +2873,15 @@ const springReverbTanks = {
2403
2873
  * documents and deliberately leaves unfiled). Every remaining mismatch sits on `kind: ic`, where
2404
2874
  * naming any class is exactly what that kind is for. Adding a class here on speculation would be
2405
2875
  * vocabulary against unexercised data.
2876
+ *
2877
+ * A UJT relaxation-oscillator law was attempted on branch `indiejoseph/ujt-law` (2026-10-20):
2878
+ * bar split at eta, emitter diode into the tap, conductivity modulation as a VCCS, plus a
2879
+ * clamp diode for saturation. It oscillates at the right period while it runs, but the
2880
+ * firing-point crossing is a Newton lottery (about one stalled peak in 70 cycles, then the
2881
+ * hold poisons the run permanently) in the reference runtime, and ngspice timestep-collapses
2882
+ * at the same instant -- see the branch report. The minimal addition is a hysteretic switch
2883
+ * stamp (once-per-sample evaluation, no Newton involvement), which no existing stamp is.
2884
+ * Until that exists this entry stays: opening is honest, stamping a BJT is not.
2406
2885
  */
2407
2886
  const unimplementedDeviceLaws = new Set([
2408
2887
  "circuit.unijunctiontransistor",
@@ -3480,15 +3959,19 @@ export function findIcsNotExecuted(lawed, original, registry) {
3480
3959
  ? `${head}: it is registered as a source-boundary shell, drawn for topological continuity rather than execution, so it is compiled as open.`
3481
3960
  : reason === "charge-pump-declared-rail"
3482
3961
  ? `${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.`
3483
- : reason === "registry-arity-mismatch"
3484
- ? `${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.`
3485
- : reason === "registry-open"
3486
- ? `${head}: the registry states no executable model for it and it is compiled as open. Whatever it does in the real circuit is absent.`
3487
- : reason === "declared-class-without-model"
3488
- ? `${head}: no registered part model binds it -- ${resolution.insteadOfRefusal ?? "the registry supplied none"} -- and ${device.identity.partNumber === null
3489
- ? `its source names no part number and declares only the generic ${device.identity.declaredType}, so there is nothing to look up`
3490
- : `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.`
3491
- : `${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,
3962
+ : reason === "charge-pump-no-input"
3963
+ ? `${head}: it is a charge pump whose generated rail the document does not declare and whose supply input carries no declared voltage, so no rail can be derived from it and it is compiled as open. Whatever the pump does in the real circuit is absent.`
3964
+ : reason === "charge-pump-output-unloaded"
3965
+ ? `${head}: it is a charge pump whose supply input is declared but whose output terminal drives nothing else in the circuit, so stamping its derived rail would change nothing and it is compiled as open.`
3966
+ : reason === "registry-arity-mismatch"
3967
+ ? `${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.`
3968
+ : reason === "registry-open"
3969
+ ? `${head}: the registry states no executable model for it and it is compiled as open. Whatever it does in the real circuit is absent.`
3970
+ : reason === "declared-class-without-model"
3971
+ ? `${head}: no registered part model binds it -- ${resolution.insteadOfRefusal ?? "the registry supplied none"} -- and ${device.identity.partNumber === null
3972
+ ? `its source names no part number and declares only the generic ${device.identity.declaredType}, so there is nothing to look up`
3973
+ : `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.`
3974
+ : `${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,
3492
3975
  ...(gapClass === undefined ? {} : { gapClass }),
3493
3976
  });
3494
3977
  }