@vessel-dsp/compiler 0.4.0 → 0.6.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 (80) hide show
  1. package/README.md +4 -2
  2. package/dist/bbd-clock.d.ts.map +1 -1
  3. package/dist/bbd-clock.js +5 -0
  4. package/dist/bbd-clock.js.map +1 -1
  5. package/dist/compile.d.ts.map +1 -1
  6. package/dist/compile.js +305 -12
  7. package/dist/compile.js.map +1 -1
  8. package/dist/device-laws.d.ts.map +1 -1
  9. package/dist/device-laws.js +552 -97
  10. package/dist/device-laws.js.map +1 -1
  11. package/dist/fixtures/circuits.d.ts +23 -2
  12. package/dist/fixtures/circuits.d.ts.map +1 -1
  13. package/dist/fixtures/circuits.js +205 -2
  14. package/dist/fixtures/circuits.js.map +1 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/lower.d.ts.map +1 -1
  20. package/dist/lower.js +4 -0
  21. package/dist/lower.js.map +1 -1
  22. package/dist/netlist.d.ts +52 -0
  23. package/dist/netlist.d.ts.map +1 -1
  24. package/dist/netlist.js +585 -13
  25. package/dist/netlist.js.map +1 -1
  26. package/dist/part-catalog.d.ts +35 -6
  27. package/dist/part-catalog.d.ts.map +1 -1
  28. package/dist/part-catalog.js +2544 -443
  29. package/dist/part-catalog.js.map +1 -1
  30. package/dist/registry-data/bjt.d.ts +3 -0
  31. package/dist/registry-data/bjt.d.ts.map +1 -0
  32. package/dist/registry-data/bjt.js +1350 -0
  33. package/dist/registry-data/bjt.js.map +1 -0
  34. package/dist/registry-data/charge-pump.d.ts +3 -0
  35. package/dist/registry-data/charge-pump.d.ts.map +1 -0
  36. package/dist/registry-data/charge-pump.js +204 -0
  37. package/dist/registry-data/charge-pump.js.map +1 -0
  38. package/dist/registry-data/diode.d.ts +3 -0
  39. package/dist/registry-data/diode.d.ts.map +1 -0
  40. package/dist/registry-data/diode.js +683 -0
  41. package/dist/registry-data/diode.js.map +1 -0
  42. package/dist/registry-data/index.d.ts +20 -0
  43. package/dist/registry-data/index.d.ts.map +1 -0
  44. package/dist/registry-data/index.js +100 -0
  45. package/dist/registry-data/index.js.map +1 -0
  46. package/dist/registry-data/jfet.d.ts +3 -0
  47. package/dist/registry-data/jfet.d.ts.map +1 -0
  48. package/dist/registry-data/jfet.js +605 -0
  49. package/dist/registry-data/jfet.js.map +1 -0
  50. package/dist/registry-data/loader.d.ts +137 -0
  51. package/dist/registry-data/loader.d.ts.map +1 -0
  52. package/dist/registry-data/loader.js +350 -0
  53. package/dist/registry-data/loader.js.map +1 -0
  54. package/dist/registry-data/mosfet.d.ts +3 -0
  55. package/dist/registry-data/mosfet.d.ts.map +1 -0
  56. package/dist/registry-data/mosfet.js +63 -0
  57. package/dist/registry-data/mosfet.js.map +1 -0
  58. package/dist/registry-data/opamp.d.ts +37 -0
  59. package/dist/registry-data/opamp.d.ts.map +1 -0
  60. package/dist/registry-data/opamp.js +669 -0
  61. package/dist/registry-data/opamp.js.map +1 -0
  62. package/dist/registry-data/regulator.d.ts +3 -0
  63. package/dist/registry-data/regulator.d.ts.map +1 -0
  64. package/dist/registry-data/regulator.js +189 -0
  65. package/dist/registry-data/regulator.js.map +1 -0
  66. package/dist/registry-data/schema.d.ts +56 -0
  67. package/dist/registry-data/schema.d.ts.map +1 -0
  68. package/dist/registry-data/schema.js +142 -0
  69. package/dist/registry-data/schema.js.map +1 -0
  70. package/dist/registry-data/zener.d.ts +3 -0
  71. package/dist/registry-data/zener.d.ts.map +1 -0
  72. package/dist/registry-data/zener.js +547 -0
  73. package/dist/registry-data/zener.js.map +1 -0
  74. package/dist/registry.d.ts +72 -5
  75. package/dist/registry.d.ts.map +1 -1
  76. package/dist/registry.js +65 -6
  77. package/dist/registry.js.map +1 -1
  78. package/dist/types.d.ts +255 -19
  79. package/dist/types.d.ts.map +1 -1
  80. package/package.json +1 -1
@@ -19,7 +19,7 @@ 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, registryOpampSwingDrop, terminalWithRole, withRecordedReads, } from "./registry.js";
22
+ import { foldPartId, foldToken, pinoutMatches, registryEntryFor, registryLawFor, registryModelFor, registryOpampGainBandwidth, registryOpampSwing, opampOutputSwingOf, terminalWithRole, withRecordedReads, } from "./registry.js";
23
23
  /** Room-temperature thermal voltage; the diode law needs a reference somewhere. */
24
24
  export const THERMAL_VOLTAGE = 0.025_852;
25
25
  /**
@@ -333,17 +333,33 @@ function opampRails(device, rails, nodeToVoltage) {
333
333
  return { high, low };
334
334
  }
335
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.
336
+ * The rails an op-amp's output can reach: each supply rail pulled in by the part's printed drop for
337
+ * that side, carried to this op-amp's own supply by the row's stated `translation`.
338
+ *
339
+ * `"constant"` applies the printed drops as printed. `"proportional"` multiplies both by the
340
+ * op-amp's supply span (`high - low`, so 9 for a 9 V single supply and 18 for a +-9 V pair) over the
341
+ * span the row was printed at. An unstated swing, an unknown rail, a span that is not positive, or a
342
+ * pair that would close the output interval (`high - dropHigh <= low + dropLow`) leaves the rails as
343
+ * they are, so a part with no entry renders bit-for-bit as before. `carried` is set only when the rails
344
+ * moved; `opamp-law-default-parameters` reads it to tell a pair used at its printed supply from one
345
+ * carried across a supply.
339
346
  */
340
- function railsAfterSwingDrop(rails, dropVolts) {
341
- if (dropVolts === null || dropVolts <= 0 || rails.high === null || rails.low === null) {
342
- return rails;
347
+ function railsAfterSwing(rails, swing) {
348
+ if (swing === null || rails.high === null || rails.low === null) {
349
+ return { rails, carried: null };
343
350
  }
344
- const high = rails.high - dropVolts;
345
- const low = rails.low + dropVolts;
346
- return high > low ? { high, low } : rails;
351
+ const span = rails.high - rails.low;
352
+ if (!(span > 0)) {
353
+ return { rails, carried: null };
354
+ }
355
+ const scale = swing.printed.translation === "proportional" ? span / swing.printed.supplyVolts : 1;
356
+ const highVolts = swing.highVolts * scale;
357
+ const lowVolts = swing.lowVolts * scale;
358
+ const high = rails.high - highVolts;
359
+ const low = rails.low + lowVolts;
360
+ return high > low
361
+ ? { rails: { high, low }, carried: { highVolts, lowVolts, circuitSupplyVolts: span } }
362
+ : { rails, carried: null };
347
363
  }
348
364
  /**
349
365
  * The one high-side and one low-side supply pin a device declares, read by the same role tokens
@@ -436,39 +452,32 @@ function quiescentSupplyDraw(device, registry, expanded, rails, nodeToVoltage) {
436
452
  { outcome: "law", device: id, law: { kind: "conductance", siemens: amps / span } },
437
453
  ];
438
454
  }
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();
455
+ export function attachDeviceLaws(netlist, registry) {
456
+ const regulatedBase = registrySuppliedVoltages(netlist, registry);
457
+ const nodeToVoltage = buildNodeToVoltageMap(netlist, regulatedBase);
458
+ const devicesTouchingNode = deviceCountByNode(netlist);
459
+ // A pump's rail is derived from the declared input, so every derivation runs against
460
+ // the document's own voltages -- before any pump output joins the map -- and the
461
+ // derived supplies merge into the rail set after. A pump can never back itself.
462
+ const pumpDerivations = new Map();
446
463
  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;
464
+ const derivation = pumpDerivation(device, registry, nodeToVoltage, devicesTouchingNode);
465
+ if (derivation !== null) {
466
+ pumpDerivations.set(device.id, derivation);
453
467
  }
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);
468
+ }
469
+ const regulated = [...regulatedBase];
470
+ for (const derivation of pumpDerivations.values()) {
471
+ if (derivation.kind !== "model") {
472
+ continue;
459
473
  }
460
- else if (vb !== undefined && a !== 0 && va === undefined) {
461
- map.set(a, vb);
474
+ regulated.push({ node: derivation.outputNode, volts: derivation.volts });
475
+ if (!nodeToVoltage.has(derivation.outputNode)) {
476
+ nodeToVoltage.set(derivation.outputNode, derivation.volts);
462
477
  }
463
478
  }
464
- return map;
465
- }
466
- export function attachDeviceLaws(netlist, registry) {
467
- const regulated = registrySuppliedVoltages(netlist, registry);
468
479
  const rails = supplyRails(netlist, regulated);
469
480
  const supply = supplyNodes(netlist);
470
- const nodeToVoltage = buildNodeToVoltageMap(netlist, regulated);
471
- const oneHopVoltage = buildOneHopVoltageMap(netlist, nodeToVoltage);
472
481
  const mainsFrequencyHz = mainsFrequency(netlist);
473
482
  // Stage 1 (`netlist.ts`) already resolved each control's real taper -- from the panel
474
483
  // control's own declaration, or failing that, from the first device bound to it -- onto
@@ -481,7 +490,6 @@ export function attachDeviceLaws(netlist, registry) {
481
490
  // is multi-section. Expanding into ordinary devices means partitioning, lowering and the runtime
482
491
  // see nothing new -- the alternative, a fourth `DeviceResolution` outcome, would have to be
483
492
  // understood by every consumer of one.
484
- const devicesTouchingNode = deviceCountByNode(netlist);
485
493
  // Nodes inside a registered package, numbered past every node the document uses.
486
494
  const internalNodes = new Map();
487
495
  const firstInternal = Math.max(0, ...netlist.nodes) + 1;
@@ -497,7 +505,7 @@ export function attachDeviceLaws(netlist, registry) {
497
505
  const devices = [];
498
506
  const resolutions = [];
499
507
  for (const device of netlist.devices) {
500
- const expanded = sectionDevices(device, registry, rails, supply, nodeToVoltage, internalNode, netlist);
508
+ const expanded = sectionDevices(device, registry, rails, supply, nodeToVoltage, internalNode, netlist, pumpDerivations.get(device.id) ?? null);
501
509
  const draw = quiescentSupplyDraw(device, registry, expanded !== null, rails, nodeToVoltage);
502
510
  if (draw !== null) {
503
511
  devices.push(draw[0]);
@@ -505,7 +513,7 @@ export function attachDeviceLaws(netlist, registry) {
505
513
  }
506
514
  if (expanded === null) {
507
515
  devices.push(device);
508
- resolutions.push(resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, oneHopVoltage, netlist));
516
+ resolutions.push(resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, netlist));
509
517
  continue;
510
518
  }
511
519
  for (const [section, resolution] of expanded) {
@@ -550,6 +558,12 @@ export function declaredRegulatorTerminals(device, model) {
550
558
  // kinds whose vocabulary carries them (`regulator`, `power-converter`, `voltage-source`,
551
559
  // `rail`, `battery`), and an `ic` may declare nothing but `pin`. A device that reaches here
552
560
  // carrying a supply electrode was therefore declared as a kind entitled to state one.
561
+ // Charge-pump parts never take this path: their volts are derived per circuit from the
562
+ // input node, and the placeholder volts in their entries must not be stamped. They are
563
+ // bound by `pumpDerivation` instead, which runs before this is consulted.
564
+ if (isChargePumpPart(device)) {
565
+ return null;
566
+ }
553
567
  if (model.sections.length !== 1) {
554
568
  return null;
555
569
  }
@@ -604,7 +618,16 @@ export function declaredRegulatorTerminals(device, model) {
604
618
  }
605
619
  function sectionDevices(device, registry, rails, supply, nodeToVoltage, internalNode = () => {
606
620
  throw new Error("no internal-node allocator");
607
- }, netlist) {
621
+ }, netlist, pump = null) {
622
+ // A decided pump never reaches generic expansion: the placeholder volts in the pump
623
+ // entries must not be stamped. An isolated pump defers to `resolveDevice`, which
624
+ // names the isolation before any pump question.
625
+ if (pump !== null) {
626
+ if (pump.kind === "isolated") {
627
+ return null;
628
+ }
629
+ return pumpSectionDevices(device, pump);
630
+ }
608
631
  // **`mayCarryRegistrySections`, not `requiresIdentification`.** An OTA reaches here so a
609
632
  // registered multi-section part (the BA662A's OTA plus its Darlington buffer) can be expanded;
610
633
  // when the registry has no matching entry this returns null and the caller falls through to
@@ -631,6 +654,25 @@ function sectionDevices(device, registry, rails, supply, nodeToVoltage, internal
631
654
  if (model.kind !== "sections" && model.kind !== "sections-core") {
632
655
  return null;
633
656
  }
657
+ // An explicit override cannot target one section of a package the registry expands: the
658
+ // section laws are the entry's numbers (a dual op-amp's two amplifiers share one part's
659
+ // figures), and `device.parameters` -- legacy or override -- never reach them. Accepting
660
+ // the block would be a silent drop, so it refuses by name instead. The way out is a
661
+ // source-declared `devices:` split, whose devices take the device-class law that the
662
+ // override does feed.
663
+ if (device.deviceOverride !== undefined) {
664
+ return [
665
+ [
666
+ device,
667
+ {
668
+ outcome: "unsupported",
669
+ device: device.id,
670
+ reason: `part ${identity.partId} is expanded from a registry sections entry, whose numbers the override cannot replace: ` +
671
+ `declare the package's devices explicitly (a "devices:" split) to take the device-class law an override feeds, or remove the DeviceOverride block`,
672
+ },
673
+ ],
674
+ ];
675
+ }
634
676
  // A `sections` entry's `terminals` index into the entry's canonical pin order, not the
635
677
  // declaration order the document happened to use. When the entry names its pins by role and
636
678
  // that naming is a clean bijection onto the device's declared terminals, resolve each
@@ -740,10 +782,12 @@ function sectionDevices(device, registry, rails, supply, nodeToVoltage, internal
740
782
  }
741
783
  // Read from the section's *parent* device, which is where the package's supply pins
742
784
  // 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;
785
+ const sectionSwing = section.law.kind === "ideal-opamp"
786
+ ? opampOutputSwingOf(section.law, identity.partId)
787
+ : null;
788
+ const sectionSwung = railsAfterSwing(opampRails(device, rails, nodeToVoltage), sectionSwing);
789
+ const railHigh = sectionSwung.rails.high;
790
+ const railLow = sectionSwung.rails.low;
747
791
  // An op-amp's rails come from the op-amp's own supply terminals or the circuit's supply set,
748
792
  // not from the registry -- the same substitution a declared `opamp` gets. A registry cannot know them:
749
793
  // it describes a part, and the rails are a property of the pedal the part is fitted to.
@@ -760,8 +804,21 @@ function sectionDevices(device, registry, rails, supply, nodeToVoltage, internal
760
804
  // The registry states the part's gain; only the rails are the pedal's.
761
805
  openLoopGain: section.law.openLoopGain,
762
806
  // 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 }),
807
+ // states how close it gets to the rails from one that does not, and a pair
808
+ // used at its printed supply from one the lowering carried across a supply.
809
+ ...(sectionSwing === null
810
+ ? {}
811
+ : {
812
+ outputSwingDropHighVolts: sectionSwing.highVolts,
813
+ outputSwingDropLowVolts: sectionSwing.lowVolts,
814
+ outputSwingPrinted: sectionSwing.printed,
815
+ }),
816
+ ...(sectionSwung.carried === null ? {} : { outputSwingCarried: sectionSwung.carried }),
817
+ // The part's datasheet product, when the entry states one. Absent stays
818
+ // absent: it lowers to a `null` stamp, which the compile names.
819
+ ...(section.law.gainBandwidthHz === undefined
820
+ ? {}
821
+ : { gainBandwidthHz: section.law.gainBandwidthHz }),
765
822
  }
766
823
  : section.law.kind === "compandor"
767
824
  ? { ...section.law, railHigh, railLow }
@@ -1304,6 +1361,12 @@ function registrySuppliedVoltages(netlist, registry) {
1304
1361
  if (device.parameters.volts !== undefined) {
1305
1362
  continue;
1306
1363
  }
1364
+ // Pump parts never supply a static rail: their output is derived per circuit from
1365
+ // the input node by `pumpDerivation`, and the placeholder volts in their entries
1366
+ // must not leak into the rail set through a role that happens to read as an output.
1367
+ if (isChargePumpPart(device)) {
1368
+ continue;
1369
+ }
1307
1370
  const identity = identify(device, registry);
1308
1371
  if (identity === null) {
1309
1372
  continue;
@@ -1416,29 +1479,375 @@ export function deviceCountByNode(netlist) {
1416
1479
  }
1417
1480
  return counts;
1418
1481
  }
1482
+ /**
1483
+ * Charge-pump inverter parts, by folded part number: every spelling the DIY BOM census
1484
+ * attributes to the family (TC1044S/SCPA, LT1054/CP, 7660 // SCPAZ, TC7662BC, ICL7660SCPA(Z),
1485
+ * LTC1144, MAX1044/S) plus the order codes and package markings the datasheets print.
1486
+ * Compared whole with `foldPartId`, never by substring: membership routes a part to the
1487
+ * pump handling, it does not model it -- modelling needs a registry entry, and a member
1488
+ * with no entry falls through to the honest unknown-chip path.
1489
+ */
1419
1490
  const CHARGE_PUMP_PARTS = new Set([
1420
- "tc1044",
1491
+ "tc1044s",
1421
1492
  "tc1044scpa",
1422
- "max1044",
1493
+ "tc1044scoa",
1494
+ "tc1044sepa",
1495
+ "tc1044seoa",
1496
+ "tc1044sija",
1497
+ "tc1044smja",
1498
+ "icl7660",
1499
+ "icl7660cpa",
1500
+ "icl7660cpaz",
1501
+ "icl7660cba",
1502
+ "icl7660cbaz",
1503
+ "7660cpa",
1504
+ "7660cpaz",
1505
+ "7660cba",
1506
+ "7660cbaz",
1507
+ "icl7660a",
1508
+ "icl7660acpa",
1509
+ "icl7660acpaz",
1510
+ "icl7660acba",
1511
+ "icl7660acbaz",
1512
+ "icl7660aiba",
1513
+ "icl7660aibaz",
1514
+ "7660acpa",
1515
+ "7660acpaz",
1516
+ "7660acba",
1517
+ "7660acbaz",
1518
+ "7660aiba",
1519
+ "7660aibaz",
1520
+ "icl7660s",
1521
+ "icl7660scpa",
1522
+ "icl7660scpaz",
1523
+ "7660scpa",
1524
+ "7660scpaz",
1525
+ "tc7662b",
1526
+ "tc7662bcpa",
1527
+ "tc7662bepa",
1528
+ "tc7662bcoa",
1529
+ "tc7662beoa",
1423
1530
  "7662",
1424
- "lt1054cp",
1425
1531
  "lt1054",
1532
+ "lt1054c",
1533
+ "lt1054cp",
1534
+ "lt1054cpe4",
1535
+ "lt1054i",
1536
+ "ltc1144",
1537
+ "ltc1144cn8",
1538
+ "ltc1144in8",
1539
+ "ltc1144cs8",
1540
+ "ltc1144is8",
1541
+ "max1044",
1542
+ "max1044cpa",
1543
+ "max1044csa",
1544
+ "max1044epa",
1545
+ "max1044esa",
1546
+ // Census spellings with no registry entry: routed here so a pump whose rail the
1547
+ // document declares still opens honestly, and one whose rail is undeclared refuses
1548
+ // as an unknown chip instead of claiming a modelled rail.
1549
+ "tc7662bc",
1550
+ "max1044s",
1551
+ "7660",
1426
1552
  ]);
1553
+ /** Whether this device names a charge-pump inverter part, compared whole. */
1554
+ function isChargePumpPart(device) {
1555
+ const partNumber = device.identity.partNumber;
1556
+ if (partNumber === null) {
1557
+ return false;
1558
+ }
1559
+ return CHARGE_PUMP_PARTS.has(foldPartId(partNumber));
1560
+ }
1561
+ /**
1562
+ * The closed role vocabularies a pump's three supply terminals are bound by, compared as
1563
+ * whole folded tokens by `terminalWithRole` -- never as substrings, never against a
1564
+ * description. These query the ENTRY's role groups (which carry the datasheet pinout in
1565
+ * package order), so a document naming its terminals in any order binds, and a document
1566
+ * numbering its pins binds positionally instead.
1567
+ */
1568
+ const PUMP_INPUT_ROLES = ["vplus", "vin", "vdd", "vcc", "input"];
1569
+ const PUMP_OUTPUT_ROLES = [
1570
+ "negativeoutput",
1571
+ "voutnegative",
1572
+ "vout",
1573
+ "voutput",
1574
+ "out",
1575
+ "output",
1576
+ ];
1577
+ const PUMP_GROUND_ROLES = ["gnd", "ground"];
1427
1578
  /**
1428
- * A charge-pump IC whose output node is already backed by a declared rail is a
1579
+ * A charge-pump IC whose OUTPUT node is already backed by a declared rail is a
1429
1580
  * view-only structural anchor: the rail supplies the voltage, the IC adds nothing.
1430
1581
  * The Klon's MAX1044 is handled by `isElectricallyIsolated` (all nodes private);
1431
1582
  * this covers the wider class where the pump shares nets with the declared rail.
1583
+ *
1584
+ * Tests the pump's OUTPUT node, by role, and nothing else. The previous version asked
1585
+ * whether ANY non-ground node of the pump touched a declared OR one-hop voltage, and a
1586
+ * reservoir capacitor to ground was enough to satisfy it: with both Plumes rails deleted,
1587
+ * nodes 35 and 36 each one-hop to 0 V through C102/C101, and the pump claimed a modelled
1588
+ * rail on ground continuity alone. Ground is not a rail, and an input rail is not the
1589
+ * output rail -- pigtronix's LT1054 sat on the same false positive through its input
1590
+ * capacitor, and moogerfooger's 7662 through its output capacitor.
1432
1591
  */
1433
- function isChargePumpWithDeclaredRail(device, nodeToVoltage, oneHopVoltage) {
1434
- const partNumber = device.identity.partNumber;
1435
- if (partNumber === null) {
1592
+ function isChargePumpWithDeclaredRail(device, nodeToVoltage) {
1593
+ if (!isChargePumpPart(device)) {
1594
+ return false;
1595
+ }
1596
+ const output = chargePumpOutputIndex(device);
1597
+ if (output === null) {
1436
1598
  return false;
1437
1599
  }
1438
- if (!CHARGE_PUMP_PARTS.has(partNumber.toLowerCase())) {
1600
+ const node = device.nodes[output];
1601
+ if (node === undefined || node === 0) {
1439
1602
  return false;
1440
1603
  }
1441
- return device.nodes.some((node) => node !== 0 && (nodeToVoltage.has(node) || oneHopVoltage.has(node)));
1604
+ return nodeToVoltage.has(node);
1605
+ }
1606
+ /**
1607
+ * The index of the pump's inverted-output terminal, or `null` when the document does
1608
+ * not settle it.
1609
+ *
1610
+ * A declared supply electrode decides first: a shell that states `ground` plus
1611
+ * `negative` (or `positive`, the regulator convention deluxe-memory-man uses for its
1612
+ * -15 V output) names its output outright. Otherwise the folded terminal names are read
1613
+ * against the output vocabulary -- whole tokens, unambiguous or nothing.
1614
+ */
1615
+ function chargePumpOutputIndex(device) {
1616
+ const declared = device.identity.declaredTerminalRoles;
1617
+ const onlyDeclared = (role) => {
1618
+ const found = declared.flatMap((value, index) => value === role ? [index] : []);
1619
+ return found.length === 1 ? found[0] : null;
1620
+ };
1621
+ const fromDeclaration = onlyDeclared("negative") ?? onlyDeclared("positive");
1622
+ if (fromDeclaration !== null) {
1623
+ return fromDeclaration;
1624
+ }
1625
+ return terminalWithRole(device.identity.terminalRoles, PUMP_OUTPUT_ROLES);
1626
+ }
1627
+ function pumpDerivation(device, registry, nodeToVoltage, devicesTouchingNode) {
1628
+ if (!isChargePumpPart(device)) {
1629
+ return null;
1630
+ }
1631
+ // Isolation stays the isolation reporter's beat: a pump on private nodes is view-only
1632
+ // however it is part-numbered, and `resolveDevice` names it before any pump question.
1633
+ if (isElectricallyIsolated(device, devicesTouchingNode)) {
1634
+ return { kind: "isolated" };
1635
+ }
1636
+ const identity = identify(device, registry);
1637
+ if (identity === null) {
1638
+ return null;
1639
+ }
1640
+ const entry = registryEntryFor(registry, identity, device.nodes.length);
1641
+ if (entry === null || entry.model.kind !== "sections") {
1642
+ return null;
1643
+ }
1644
+ const section = entry.model.sections[0];
1645
+ if (entry.model.sections.length !== 1 ||
1646
+ section === undefined ||
1647
+ section.law.kind !== "voltage-source" ||
1648
+ section.terminals.length !== 2) {
1649
+ return {
1650
+ kind: "unbound",
1651
+ detail: `part ${identity.partId} is routed to the charge-pump derivation but its registry ` +
1652
+ `entry is not a single two-terminal voltage-source section, so no rail can be ` +
1653
+ `derived from it`,
1654
+ };
1655
+ }
1656
+ const sourceOhms = section.law.sourceOhms;
1657
+ // Which package positions the supply input, inverted output, and reference sit at,
1658
+ // read off the ENTRY's role groups -- the datasheet pinout in package order -- so one
1659
+ // entry serves every declaration order that names its terminals at all.
1660
+ const groups = entry.terminalRoleGroups;
1661
+ const groupIndex = (vocab) => {
1662
+ const wanted = new Set(vocab.map((alias) => foldToken(alias)));
1663
+ const hits = [];
1664
+ groups.forEach((group, index) => {
1665
+ if (group.some((alias) => wanted.has(foldToken(alias)))) {
1666
+ hits.push(index);
1667
+ }
1668
+ });
1669
+ return hits.length === 1 ? hits[0] : null;
1670
+ };
1671
+ const inputPos = groupIndex(PUMP_INPUT_ROLES);
1672
+ const outputPos = groupIndex(PUMP_OUTPUT_ROLES);
1673
+ const groundPos = groupIndex(PUMP_GROUND_ROLES);
1674
+ if (inputPos === null || outputPos === null || groundPos === null) {
1675
+ return {
1676
+ kind: "unbound",
1677
+ detail: `part ${identity.partId} is registered as a charge pump but its entry does not ` +
1678
+ `name a distinct supply input, inverted output, and ground reference`,
1679
+ };
1680
+ }
1681
+ const roles = device.identity.terminalRoles;
1682
+ const declared = device.identity.declaredTerminalRoles;
1683
+ const onlyDeclared = (role) => {
1684
+ const found = declared.flatMap((value, index) => value === role ? [index] : []);
1685
+ return found.length === 1 ? found[0] : null;
1686
+ };
1687
+ // A declared supply electrode outranks the entry's pin numbering, the same rule the
1688
+ // regulator path documents: the reference is `ground`, the inverted output is
1689
+ // `negative` (or `positive`, the convention deluxe-memory-man's -15 V output uses),
1690
+ // and the input is the one remaining terminal sitting on a declared nonzero voltage.
1691
+ const declaredBinding = (() => {
1692
+ const ground = onlyDeclared("ground");
1693
+ const output = onlyDeclared("negative") ?? onlyDeclared("positive");
1694
+ if (ground === null || output === null || ground === output) {
1695
+ return null;
1696
+ }
1697
+ const candidates = roles
1698
+ .map((_, index) => index)
1699
+ .filter((index) => index !== ground && index !== output)
1700
+ .filter((index) => {
1701
+ const node = device.nodes[index];
1702
+ if (node === undefined || node === 0) {
1703
+ return false;
1704
+ }
1705
+ const volts = nodeToVoltage.get(node);
1706
+ return volts !== undefined && volts !== 0;
1707
+ });
1708
+ if (candidates.length !== 1) {
1709
+ return null;
1710
+ }
1711
+ return { input: candidates[0], output, ground };
1712
+ })();
1713
+ // Otherwise the folded terminal names, bound through the entry's own aliases so any
1714
+ // declaration order lands on the right pins.
1715
+ const roleBinding = (() => {
1716
+ const input = terminalWithRole(roles, [...groups[inputPos]]);
1717
+ const output = terminalWithRole(roles, [...groups[outputPos]]);
1718
+ const ground = terminalWithRole(roles, [...groups[groundPos]]);
1719
+ if (input === null || output === null || ground === null) {
1720
+ return null;
1721
+ }
1722
+ if (new Set([input, output, ground]).size !== 3) {
1723
+ return null;
1724
+ }
1725
+ return { input, output, ground };
1726
+ })();
1727
+ // Otherwise a bare-pin shell in package order: terminals numbered pin1..pinN fold to
1728
+ // no role at all, so declaration order against the entry's package-order pinout is
1729
+ // the only reading, exactly as the regulator entries already assume.
1730
+ const positionalBinding = (() => {
1731
+ if (!roles.every((role) => role === null)) {
1732
+ return null;
1733
+ }
1734
+ if (entry.model.kind !== "sections" ||
1735
+ entry.model.pinout.length !== roles.length) {
1736
+ return null;
1737
+ }
1738
+ return { input: inputPos, output: outputPos, ground: groundPos };
1739
+ })();
1740
+ const binding = declaredBinding ?? roleBinding ?? positionalBinding;
1741
+ if (binding === null) {
1742
+ return {
1743
+ kind: "unbound",
1744
+ detail: `part ${identity.partId} is registered as a charge pump, but neither its terminal ` +
1745
+ `names, its declared supply electrodes, nor its pin order bind a supply input, ` +
1746
+ `inverted output, and ground reference: ${device.nodes.length} terminals against ` +
1747
+ `a ${entry.model.pinout.length}-terminal pinout`,
1748
+ };
1749
+ }
1750
+ const inputNode = device.nodes[binding.input];
1751
+ const outputNode = device.nodes[binding.output];
1752
+ const groundNode = device.nodes[binding.ground];
1753
+ if (inputNode === undefined ||
1754
+ outputNode === undefined ||
1755
+ groundNode === undefined) {
1756
+ return {
1757
+ kind: "unbound",
1758
+ detail: `part ${identity.partId} charge-pump binding names a terminal this component ` +
1759
+ `does not declare`,
1760
+ };
1761
+ }
1762
+ // The reference is circuit ground. A pump tied to anything else is a shell the
1763
+ // derivation cannot place, not a rail against an unknown reference.
1764
+ if (groundNode !== 0 || outputNode === 0) {
1765
+ return {
1766
+ kind: "unbound",
1767
+ detail: `part ${identity.partId} charge-pump binding puts the reference off circuit ` +
1768
+ `ground, so no rail can be derived from it`,
1769
+ };
1770
+ }
1771
+ // The document keeps the last word: an output the document already drives --
1772
+ // declared rail or regulator-supplied -- stays the document's, and the pump stays open.
1773
+ if (nodeToVoltage.has(outputNode)) {
1774
+ return { kind: "backed" };
1775
+ }
1776
+ const inputVolts = nodeToVoltage.get(inputNode);
1777
+ if (inputVolts === undefined || inputVolts === 0) {
1778
+ return { kind: "no-input" };
1779
+ }
1780
+ // An output no other device touches cannot move the solve: stamping it would only
1781
+ // widen the circuit-wide rail extremes other devices fall back to.
1782
+ if ((devicesTouchingNode.get(outputNode) ?? 0) < 2) {
1783
+ return { kind: "unloaded" };
1784
+ }
1785
+ const lossVolts = entry.chargePumpLossVolts ?? 0;
1786
+ return {
1787
+ kind: "model",
1788
+ inputNode,
1789
+ outputNode,
1790
+ groundNode,
1791
+ inputVolts,
1792
+ volts: -inputVolts - Math.sign(-inputVolts) * lossVolts,
1793
+ sourceOhms: section.law.sourceOhms,
1794
+ };
1795
+ }
1796
+ /**
1797
+ * A derived pump rail as one ordinary section device plus its law, or the original device
1798
+ * opened with the reason the derivation recorded. Never `null` for a device the
1799
+ * derivation decided: the placeholder volts in the pump entries must not reach generic
1800
+ * expansion, so every decided device is consumed here.
1801
+ */
1802
+ function pumpSectionDevices(device, derivation) {
1803
+ switch (derivation.kind) {
1804
+ case "backed":
1805
+ return [[device, openIc(device, "charge-pump-declared-rail")]];
1806
+ case "no-input":
1807
+ return [[device, openIc(device, "charge-pump-no-input")]];
1808
+ case "unloaded":
1809
+ return [[device, openIc(device, "charge-pump-output-unloaded")]];
1810
+ case "unbound":
1811
+ return [
1812
+ [
1813
+ device,
1814
+ {
1815
+ outcome: "law",
1816
+ device: device.id,
1817
+ law: { kind: "open" },
1818
+ openReason: "registry-arity-mismatch",
1819
+ insteadOfRefusal: derivation.detail,
1820
+ },
1821
+ ],
1822
+ ];
1823
+ case "model": {
1824
+ const sectionId = `${device.id}#0`;
1825
+ const sectionDevice = {
1826
+ ...device,
1827
+ id: sectionId,
1828
+ nodes: [derivation.outputNode, derivation.groundNode],
1829
+ identity: {
1830
+ ...device.identity,
1831
+ terminalRoles: canonicalRoles("voltage-source", 2),
1832
+ declaredTerminalRoles: canonicalRoles("voltage-source", 2),
1833
+ },
1834
+ };
1835
+ return [
1836
+ [
1837
+ sectionDevice,
1838
+ {
1839
+ outcome: "law",
1840
+ device: sectionId,
1841
+ law: {
1842
+ kind: "voltage-source",
1843
+ volts: derivation.volts,
1844
+ sourceOhms: derivation.sourceOhms,
1845
+ },
1846
+ },
1847
+ ],
1848
+ ];
1849
+ }
1850
+ }
1442
1851
  }
1443
1852
  /**
1444
1853
  * An OTA's signal terminals, bound by the names the device declares for them.
@@ -1647,7 +2056,7 @@ function packageDeviceParameters(device, registry) {
1647
2056
  }
1648
2057
  return parameters;
1649
2058
  }
1650
- function resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, oneHopVoltage, netlist) {
2059
+ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, netlist) {
1651
2060
  // A component declared as one transistor that is really several devices in one shell. Opened
1652
2061
  // rather than stamped, for the reason `isNonExecutableIcSupportShell` opens an IC shell: there
1653
2062
  // is no single device here to describe, and a transistor law would invent one. Reported by
@@ -1680,7 +2089,7 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
1680
2089
  // `findElectricallyIsolatedIcs`, which compile() runs over the same predicate.
1681
2090
  return openIc(device, "electrically-isolated");
1682
2091
  }
1683
- if (isChargePumpWithDeclaredRail(device, nodeToVoltage, oneHopVoltage)) {
2092
+ if (isChargePumpWithDeclaredRail(device, nodeToVoltage)) {
1684
2093
  // The rail the pump generates is separately declared as a voltage-carrying
1685
2094
  // port; the IC itself is a view-only structural anchor, not a solved model.
1686
2095
  return openIc(device, "charge-pump-declared-rail");
@@ -1898,6 +2307,7 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
1898
2307
  if (declared.breakdownVolts !== undefined ||
1899
2308
  declared.saturationCurrent !== undefined ||
1900
2309
  declared.seriesResistance !== undefined ||
2310
+ declared.emissionCoefficient !== undefined ||
1901
2311
  device.isLed === true) {
1902
2312
  return {
1903
2313
  outcome: "law",
@@ -1907,6 +2317,8 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
1907
2317
  breakdownVolts: declared.breakdownVolts ?? catalogued.breakdownVolts,
1908
2318
  saturationCurrent: declared.saturationCurrent ?? catalogued.saturationCurrent,
1909
2319
  seriesResistance: declared.seriesResistance ?? catalogued.seriesResistance,
2320
+ emissionCoefficient: declared.emissionCoefficient ?? catalogued.emissionCoefficient,
2321
+ thermalVoltage: declared.thermalVoltage ?? catalogued.thermalVoltage,
1910
2322
  isLed: device.isLed === true ? true : catalogued.isLed,
1911
2323
  },
1912
2324
  };
@@ -1923,7 +2335,8 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
1923
2335
  // If it is an LED, use physical LED defaults (higher forward knee ~1.8V to 2.2V),
1924
2336
  // otherwise use class defaults (1N4148-ish silicon signal diode).
1925
2337
  saturationCurrent: device.parameters.saturationCurrent ?? (isLed ? 1e-12 : 2.52e-9),
1926
- emissionCoefficient: 1.752,
2338
+ // The source's own `EmissionCoefficient` when it states one, else the class default.
2339
+ emissionCoefficient: device.parameters.emissionCoefficient ?? 1.752,
1927
2340
  thermalVoltage: device.parameters.thermalVoltage ?? (isLed ? 0.05 : THERMAL_VOLTAGE),
1928
2341
  // 0 for an ordinary diode. Read from the source, never inferred from a part
1929
2342
  // number: `1N4733` is a 5.1 V zener and `1N4148` is not, and telling them apart
@@ -1979,10 +2392,15 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
1979
2392
  },
1980
2393
  };
1981
2394
  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;
2395
+ const declaredSwing = registryOpampSwing(registry, device.identity.partNumber);
2396
+ const declaredSwung = railsAfterSwing(opampRails(device, rails, nodeToVoltage), declaredSwing);
2397
+ const railHigh = declaredSwung.rails.high;
2398
+ const railLow = declaredSwung.rails.low;
2399
+ // The gain-bandwidth product: the packet's own typed `GBP` first, else the datasheet figure
2400
+ // of the exact part id, else none. Never a class number -- `null` lowers to a stamp the
2401
+ // consoles run on their default, and `opamp-gain-bandwidth-default` names it.
2402
+ const gainBandwidthHz = device.parameters.gainBandwidthHz ??
2403
+ registryOpampGainBandwidth(registry, device.identity.partNumber);
1986
2404
  return {
1987
2405
  outcome: "law",
1988
2406
  device: device.id,
@@ -1991,7 +2409,15 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
1991
2409
  railHigh,
1992
2410
  railLow,
1993
2411
  openLoopGain: device.parameters.openLoopGain ?? DEFAULT_OPEN_LOOP_GAIN,
1994
- ...(declaredDrop === null ? {} : { outputSwingDropVolts: declaredDrop }),
2412
+ ...(declaredSwing === null
2413
+ ? {}
2414
+ : {
2415
+ outputSwingDropHighVolts: declaredSwing.highVolts,
2416
+ outputSwingDropLowVolts: declaredSwing.lowVolts,
2417
+ outputSwingPrinted: declaredSwing.printed,
2418
+ }),
2419
+ ...(declaredSwung.carried === null ? {} : { outputSwingCarried: declaredSwung.carried }),
2420
+ ...(gainBandwidthHz === null ? {} : { gainBandwidthHz }),
1995
2421
  },
1996
2422
  };
1997
2423
  }
@@ -2134,29 +2560,41 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
2134
2560
  law: { kind: "voltage-source", volts, sourceOhms: SUPPLY_SOURCE_OHMS },
2135
2561
  };
2136
2562
  }
2137
- case "bjt":
2563
+ case "bjt": {
2564
+ // A registered part refines the class defaults, exactly as the diode and
2565
+ // JFET-gate cases do. Exact part number only -- see `registryLawFor`.
2566
+ // Declared parameters win over everything, every time: the entries exist
2567
+ // to fill absence, not to overrule a packet that states the value.
2568
+ // Unregistered parts keep the class default bit-identically.
2569
+ const catalogued = registryLawFor(registry, device.identity.partNumber, "bjt");
2570
+ const entry = catalogued !== null && catalogued.kind === "bjt" ? catalogued : null;
2138
2571
  return {
2139
2572
  outcome: "law",
2140
2573
  device: device.id,
2141
2574
  law: {
2142
2575
  kind: "bjt",
2143
2576
  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,
2577
+ saturationCurrent: device.parameters.saturationCurrent ??
2578
+ entry?.saturationCurrent ??
2579
+ SILICON_SATURATION_CURRENT,
2580
+ forwardBeta: device.parameters.beta ?? entry?.forwardBeta ?? SILICON_FORWARD_BETA,
2581
+ reverseBeta: device.parameters.reverseBeta ?? entry?.reverseBeta ?? 1,
2582
+ thermalVoltage: device.parameters.thermalVoltage ??
2583
+ entry?.thermalVoltage ??
2584
+ THERMAL_VOLTAGE,
2148
2585
  // Absent for every silicon part in the corpus, and for the one germanium
2149
2586
  // packet whose `LeakageCurrent` is a sentence rather than a value.
2150
- leakageAmps: device.parameters.leakageAmps ?? 0,
2587
+ leakageAmps: device.parameters.leakageAmps ?? entry?.leakageAmps ?? 0,
2151
2588
  },
2152
2589
  };
2590
+ }
2153
2591
  case "jfet": {
2154
- // A registered part refines the class gate, exactly as `GENERIC_TUBE_GRID`'s
2592
+ // A registered part refines the class channel, exactly as `GENERIC_TUBE_GRID`'s
2155
2593
  // 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
2594
+ // `registryLawFor`. The 2SK30A entry's channel repeats the class default by
2595
+ // construction (rank-dependent, and the database beta disagrees with at
2596
+ // least one packet's record), so a covered part's channel is
2597
+ // indistinguishable from an unregistered one's. Declared
2160
2598
  // Vt0/Beta win over everything, every time: the entries exist to fill
2161
2599
  // absence, not to overrule a packet that states the value (the same overlay
2162
2600
  // the diode case applies). Unregistered parts keep the class default
@@ -2173,13 +2611,17 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
2173
2611
  channel: device.parameters.pChannel === 1 ? "p" : "n",
2174
2612
  // A JFET is depletion mode: it conducts at zero gate bias and pinches
2175
2613
  // 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,
2614
+ thresholdVolts: device.parameters.thresholdVolts ??
2615
+ entry?.thresholdVolts ??
2616
+ JFET_DEFAULT_THRESHOLD_VOLTS,
2617
+ transconductance: device.parameters.transconductance ??
2618
+ entry?.transconductance ??
2619
+ JFET_DEFAULT_TRANSCONDUCTANCE,
2620
+ channelLengthModulation: entry?.channelLengthModulation ?? 0,
2179
2621
  // Subthreshold conduction (see the `fet` law docs in types.ts): the
2180
2622
  // gradual region a knee-biased JFET actually sweeps in. MOSFETs
2181
2623
  // keep 0 below.
2182
- subthresholdVolts: 0.07,
2624
+ subthresholdVolts: entry?.subthresholdVolts ?? 0.07,
2183
2625
  // A JFET gate is a silicon PN junction, so it conducts once forward-biased.
2184
2626
  // Saturation current and scale are **reused from the triode grid**, which
2185
2627
  // `compact-mna-dynamic-triode-cell` reports 0 Newton failures with; the onset
@@ -2188,30 +2630,39 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
2188
2630
  // part refines the current through its catalog entry, and the
2189
2631
  // unregistered default stays exactly what it was.
2190
2632
  gateSaturationCurrent: entry?.gateSaturationCurrent ?? 1.0e-5,
2191
- gateOnsetVolts: 0.5,
2192
- gateScaleVolts: 0.06,
2633
+ gateOnsetVolts: entry?.gateOnsetVolts ?? 0.5,
2634
+ gateScaleVolts: entry?.gateScaleVolts ?? 0.06,
2193
2635
  },
2194
2636
  };
2195
2637
  }
2196
- case "mosfet":
2638
+ case "mosfet": {
2639
+ // The same registry refinement as the JFET channel above: a registered
2640
+ // part refines the class defaults, declared parameters win, and
2641
+ // unregistered parts keep the class default bit-identically. Exact part
2642
+ // number only. This shares the `fet` law kind with JFET entries; part
2643
+ // ids are disjoint across the two classes, so a lookup by exact folded
2644
+ // part id cannot cross from one to the other.
2645
+ const catalogued = registryLawFor(registry, device.identity.partNumber, "fet");
2646
+ const entry = catalogued !== null && catalogued.kind === "fet" ? catalogued : null;
2197
2647
  return {
2198
2648
  outcome: "law",
2199
2649
  device: device.id,
2200
2650
  law: {
2201
2651
  kind: "fet",
2202
2652
  channel: device.parameters.pChannel === 1 ? "p" : "n",
2203
- thresholdVolts: device.parameters.thresholdVolts ?? 2,
2204
- transconductance: device.parameters.transconductance ?? 1e-3,
2205
- channelLengthModulation: 0,
2653
+ thresholdVolts: device.parameters.thresholdVolts ?? entry?.thresholdVolts ?? 2,
2654
+ transconductance: device.parameters.transconductance ?? entry?.transconductance ?? 1e-3,
2655
+ channelLengthModulation: entry?.channelLengthModulation ?? 0,
2206
2656
  // Insulated gate: no subthreshold region is modelled, behavior unchanged.
2207
- subthresholdVolts: 0,
2657
+ subthresholdVolts: entry?.subthresholdVolts ?? 0,
2208
2658
  // An insulated gate draws no current at any bias. Zero, deliberately: this
2209
2659
  // is what the law did for every FET before the JFET junction was added.
2210
- gateSaturationCurrent: 0,
2211
- gateOnsetVolts: 0,
2212
- gateScaleVolts: 1,
2660
+ gateSaturationCurrent: entry?.gateSaturationCurrent ?? 0,
2661
+ gateOnsetVolts: entry?.gateOnsetVolts ?? 0,
2662
+ gateScaleVolts: entry?.gateScaleVolts ?? 1,
2213
2663
  },
2214
2664
  };
2665
+ }
2215
2666
  case "ota":
2216
2667
  return otaDeviceClassLaw(device);
2217
2668
  case "inverter":
@@ -3469,15 +3920,19 @@ export function findIcsNotExecuted(lawed, original, registry) {
3469
3920
  ? `${head}: it is registered as a source-boundary shell, drawn for topological continuity rather than execution, so it is compiled as open.`
3470
3921
  : reason === "charge-pump-declared-rail"
3471
3922
  ? `${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,
3923
+ : reason === "charge-pump-no-input"
3924
+ ? `${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.`
3925
+ : reason === "charge-pump-output-unloaded"
3926
+ ? `${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.`
3927
+ : reason === "registry-arity-mismatch"
3928
+ ? `${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.`
3929
+ : reason === "registry-open"
3930
+ ? `${head}: the registry states no executable model for it and it is compiled as open. Whatever it does in the real circuit is absent.`
3931
+ : reason === "declared-class-without-model"
3932
+ ? `${head}: no registered part model binds it -- ${resolution.insteadOfRefusal ?? "the registry supplied none"} -- and ${device.identity.partNumber === null
3933
+ ? `its source names no part number and declares only the generic ${device.identity.declaredType}, so there is nothing to look up`
3934
+ : `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.`
3935
+ : `${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
3936
  ...(gapClass === undefined ? {} : { gapClass }),
3482
3937
  });
3483
3938
  }