@vessel-dsp/compiler 0.18.1 → 0.21.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 (63) hide show
  1. package/dist/compile.d.ts +16 -0
  2. package/dist/compile.d.ts.map +1 -1
  3. package/dist/compile.js +93 -12
  4. package/dist/compile.js.map +1 -1
  5. package/dist/device-laws.d.ts +60 -1
  6. package/dist/device-laws.d.ts.map +1 -1
  7. package/dist/device-laws.js +230 -4
  8. package/dist/device-laws.js.map +1 -1
  9. package/dist/fixtures/circuits.d.ts +40 -0
  10. package/dist/fixtures/circuits.d.ts.map +1 -1
  11. package/dist/fixtures/circuits.js +477 -0
  12. package/dist/fixtures/circuits.js.map +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/link.d.ts.map +1 -1
  18. package/dist/link.js +4 -3
  19. package/dist/link.js.map +1 -1
  20. package/dist/lower.d.ts.map +1 -1
  21. package/dist/lower.js +10 -0
  22. package/dist/lower.js.map +1 -1
  23. package/dist/netlist.d.ts.map +1 -1
  24. package/dist/netlist.js +92 -1
  25. package/dist/netlist.js.map +1 -1
  26. package/dist/part-catalog.d.ts.map +1 -1
  27. package/dist/part-catalog.js +24 -0
  28. package/dist/part-catalog.js.map +1 -1
  29. package/dist/registry-data/bjt.d.ts.map +1 -1
  30. package/dist/registry-data/bjt.js +1422 -21
  31. package/dist/registry-data/bjt.js.map +1 -1
  32. package/dist/registry-data/diode.d.ts.map +1 -1
  33. package/dist/registry-data/diode.js +293 -0
  34. package/dist/registry-data/diode.js.map +1 -1
  35. package/dist/registry-data/index.d.ts +1 -1
  36. package/dist/registry-data/index.d.ts.map +1 -1
  37. package/dist/registry-data/index.js.map +1 -1
  38. package/dist/registry-data/jfet.d.ts.map +1 -1
  39. package/dist/registry-data/jfet.js +79 -0
  40. package/dist/registry-data/jfet.js.map +1 -1
  41. package/dist/registry-data/led.d.ts.map +1 -1
  42. package/dist/registry-data/led.js +16 -0
  43. package/dist/registry-data/led.js.map +1 -1
  44. package/dist/registry-data/loader.d.ts +19 -1
  45. package/dist/registry-data/loader.d.ts.map +1 -1
  46. package/dist/registry-data/loader.js +32 -1
  47. package/dist/registry-data/loader.js.map +1 -1
  48. package/dist/registry-data/mosfet.d.ts.map +1 -1
  49. package/dist/registry-data/mosfet.js +12 -0
  50. package/dist/registry-data/mosfet.js.map +1 -1
  51. package/dist/registry-data/schema.d.ts +14 -1
  52. package/dist/registry-data/schema.d.ts.map +1 -1
  53. package/dist/registry-data/schema.js +6 -2
  54. package/dist/registry-data/schema.js.map +1 -1
  55. package/dist/registry-data/zener.d.ts.map +1 -1
  56. package/dist/registry-data/zener.js +68 -0
  57. package/dist/registry-data/zener.js.map +1 -1
  58. package/dist/registry.d.ts +9 -0
  59. package/dist/registry.d.ts.map +1 -1
  60. package/dist/registry.js.map +1 -1
  61. package/dist/types.d.ts +82 -2
  62. package/dist/types.d.ts.map +1 -1
  63. package/package.json +2 -2
@@ -25,6 +25,135 @@ import { GROUND } from "./types.js";
25
25
  import { LED_COLOUR_RECORD_ID } from "./registry-data/led.js";
26
26
  /** Room-temperature thermal voltage; the diode law needs a reference somewhere. */
27
27
  export const THERMAL_VOLTAGE = 0.025_852;
28
+ /**
29
+ * The absolute temperature every registry fit is referred to, in kelvin.
30
+ *
31
+ * Fitted saturation currents and the engine thermal-voltage constant are stated at
32
+ * this temperature (`THERMAL_VOLTAGE` is kT/q at 300 K, not 273.15 + 27), so the
33
+ * ambient-temperature fold scales relative to it and ngspice parity sets
34
+ * `tnom=26.85` to match. With no ambient option the compile stamps exactly today's
35
+ * numbers: the fold is skipped outright below a 1e-9 K band around this value, so
36
+ * the default path shares no arithmetic with the scaled one.
37
+ */
38
+ export const REFERENCE_TEMPERATURE_KELVIN = 300;
39
+ /** Offset between the compile option's Celsius and the fold's kelvin. */
40
+ const CELSIUS_TO_KELVIN = 273.15;
41
+ /** Boltzmann constant, J/K (exact, SI). */
42
+ const BOLTZMANN_JOULES_PER_KELVIN = 1.380_649e-23;
43
+ /** Elementary charge, C (exact, SI). */
44
+ const ELEMENTARY_CHARGE_COULOMBS = 1.602_176_634e-19;
45
+ /**
46
+ * Bandgap energy per junction material, in eV: the ngspice defaults for the
47
+ * matching device kinds (silicon diode/BJT 1.11, germanium 0.67, Schottky barrier
48
+ * 0.69). An LED junction is a III-V the three-material law does not cover; LEDs run
49
+ * the silicon bandgap and the material-default notice says so.
50
+ */
51
+ const BANDGAP_ELECTRON_VOLTS = {
52
+ silicon: 1.11,
53
+ germanium: 0.67,
54
+ schottky: 0.69,
55
+ };
56
+ /** The saturation-current temperature exponent (SPICE XTI), shared by all three. */
57
+ const SATURATION_TEMPERATURE_EXPONENT = 3;
58
+ /**
59
+ * The compile option's Celsius as an absolute temperature, validated.
60
+ *
61
+ * Throws a `RangeError` naming the option for a non-finite value or anything below
62
+ * absolute zero: an option is a programmer error, not a source refusal, so it
63
+ * propagates rather than becoming a pedal-shaped answer.
64
+ */
65
+ export function kelvinFromAmbientCelsius(celsius) {
66
+ if (!Number.isFinite(celsius)) {
67
+ throw new RangeError(`ambientTemperatureC ${String(celsius)} is not a finite temperature in degrees Celsius`);
68
+ }
69
+ if (celsius < -CELSIUS_TO_KELVIN) {
70
+ throw new RangeError(`ambientTemperatureC ${String(celsius)} is below absolute zero (-273.15 C)`);
71
+ }
72
+ return celsius + CELSIUS_TO_KELVIN;
73
+ }
74
+ /** Whether the fold changes any number: outside a 1e-9 K band around 300 K. */
75
+ export function ambientTemperatureScalesJunction(kelvin) {
76
+ return (Math.abs(kelvin - REFERENCE_TEMPERATURE_KELVIN) > 1e-9 &&
77
+ Number.isFinite(kelvin));
78
+ }
79
+ /**
80
+ * The SPICE temperature ratio a junction saturation current scales by, relative to
81
+ * the 300 K reference the registry fits are referred to:
82
+ * `Is(T) = Is * (T/Tnom)^(XTI/N) * exp((Eg*q/(N*k)) * (1/Tnom - 1/T))` with XTI = 3.
83
+ * `n` is the junction's own emission coefficient (1 for a BJT or OTA bias diode).
84
+ */
85
+ function saturationCurrentRatio(emissionCoefficient, material, kelvin) {
86
+ const bandgapJoules = BANDGAP_ELECTRON_VOLTS[material] * ELEMENTARY_CHARGE_COULOMBS;
87
+ const slope = bandgapJoules /
88
+ (emissionCoefficient * BOLTZMANN_JOULES_PER_KELVIN);
89
+ return ((kelvin / REFERENCE_TEMPERATURE_KELVIN) **
90
+ (SATURATION_TEMPERATURE_EXPONENT / emissionCoefficient) *
91
+ Math.exp(slope * (1 / REFERENCE_TEMPERATURE_KELVIN - 1 / kelvin)));
92
+ }
93
+ /**
94
+ * Fold one finished diode law to the ambient temperature: `thermalVoltage` scales
95
+ * with T (today's constant is kT/q at 300 K) and `saturationCurrent` follows the
96
+ * SPICE law above with the junction's own emission coefficient. Series resistance
97
+ * and breakdown voltage carry no temperature model and are kept as stated.
98
+ */
99
+ function diodeLawAtAmbientTemperature(law, material, kelvin) {
100
+ return {
101
+ ...law,
102
+ thermalVoltage: law.thermalVoltage * (kelvin / REFERENCE_TEMPERATURE_KELVIN),
103
+ saturationCurrent: law.saturationCurrent *
104
+ saturationCurrentRatio(law.emissionCoefficient, material, kelvin),
105
+ };
106
+ }
107
+ /**
108
+ * Fold one finished BJT law to the ambient temperature: as the diode fold, with
109
+ * the Ebers-Moll ideality of 1. The excess collector-base leakage is a junction
110
+ * prefactor beyond the ideal reverse current, so it scales with the same ratio;
111
+ * betas carry no temperature model and are kept as stated.
112
+ */
113
+ function bjtLawAtAmbientTemperature(law, material, kelvin) {
114
+ const ratio = saturationCurrentRatio(1, material, kelvin);
115
+ return {
116
+ ...law,
117
+ thermalVoltage: law.thermalVoltage * (kelvin / REFERENCE_TEMPERATURE_KELVIN),
118
+ saturationCurrent: law.saturationCurrent * ratio,
119
+ leakageAmps: law.leakageAmps * ratio,
120
+ };
121
+ }
122
+ /**
123
+ * Fold one finished OTA law to the ambient temperature: the four-terminal bias
124
+ * diode is a silicon junction with ideality 1. A law that states no bias-diode
125
+ * pair (a sections entry carrying transconductance only) gains the scaled class
126
+ * defaults the lowering would otherwise stamp at 300 K, so the program carries
127
+ * the temperature rather than the lowering re-defaulting it.
128
+ */
129
+ function otaLawAtAmbientTemperature(law, kelvin) {
130
+ const ratio = saturationCurrentRatio(1, "silicon", kelvin);
131
+ return {
132
+ ...law,
133
+ saturationCurrent: (law.saturationCurrent ?? 1e-12) * ratio,
134
+ thermalVoltage: (law.thermalVoltage ?? THERMAL_VOLTAGE) *
135
+ (kelvin / REFERENCE_TEMPERATURE_KELVIN),
136
+ };
137
+ }
138
+ /**
139
+ * A part's junction material for the temperature fold: the registry entry's typed
140
+ * `material` for the exact folded part id -- the same rung the law refinement
141
+ * uses -- or null when no entry states one. Null means the silicon default, which
142
+ * the compile records (`material-default-silicon`); it is never inferred from the
143
+ * part number or any prose.
144
+ */
145
+ export function registryMaterialFor(registry, partNumber) {
146
+ if (partNumber === null) {
147
+ return null;
148
+ }
149
+ const folded = foldPartId(partNumber);
150
+ for (const entry of registry.entries) {
151
+ if (entry.partIds.some((id) => foldPartId(id) === folded)) {
152
+ return entry.material ?? null;
153
+ }
154
+ }
155
+ return null;
156
+ }
28
157
  /**
29
158
  * Closed and open switch resistances. Not arbitrary: the ratio sets the matrix
30
159
  * condition number, so 1e-2 to 1e9 is chosen to be electrically indistinguishable
@@ -107,6 +236,18 @@ export const SILICON_FORWARD_BETA = 100;
107
236
  */
108
237
  export const JFET_DEFAULT_THRESHOLD_VOLTS = -2;
109
238
  export const JFET_DEFAULT_TRANSCONDUCTANCE = 1e-3;
239
+ /**
240
+ * A discrete MOSFET's body-diode class defaults, stamped when the registry
241
+ * record for the exact part types no body parameters (no shipped record does
242
+ * today). The values are ngspice's own MOS bulk-diode defaults (IS 1e-14 A,
243
+ * N 1) by value, deliberately: the deck emitter carries the same pair on its
244
+ * explicit diode, so the two consoles agree by construction rather than by a
245
+ * fit. A silicon PN junction either way; the name is separate from
246
+ * `SILICON_SATURATION_CURRENT` so the body default reads beside the fet law.
247
+ * Named by `fet-law-default-parameters`, never stamped silently.
248
+ */
249
+ export const MOSFET_BODY_SATURATION_CURRENT = 1e-14;
250
+ export const MOSFET_BODY_EMISSION_COEFFICIENT = 1;
110
251
  /**
111
252
  * A winding's DC resistance, in the three states the source can be in.
112
253
  *
@@ -598,7 +739,7 @@ function zenerRailDerivations(netlist, registry, nodeToVoltage, devicesTouchingN
598
739
  }
599
740
  return derived;
600
741
  }
601
- export function attachDeviceLaws(netlist, registry) {
742
+ export function attachDeviceLaws(netlist, registry, options) {
602
743
  const regulatedBase = registrySuppliedVoltages(netlist, registry);
603
744
  const nodeToVoltage = buildNodeToVoltageMap(netlist, regulatedBase);
604
745
  const devicesTouchingNode = deviceCountByNode(netlist);
@@ -653,6 +794,10 @@ export function attachDeviceLaws(netlist, registry) {
653
794
  internalNodes.set(key, node);
654
795
  return node;
655
796
  };
797
+ // The ambient temperature in kelvin this compile folds to. Absent means the
798
+ // 300 K reference: the junction fold and the thermistor resistance both land
799
+ // exactly on today's numbers (the fold is skipped outright near 300 K).
800
+ const ambientKelvin = options?.ambientKelvin ?? REFERENCE_TEMPERATURE_KELVIN;
656
801
  const devices = [];
657
802
  const resolutions = [];
658
803
  for (const device of netlist.devices) {
@@ -664,7 +809,7 @@ export function attachDeviceLaws(netlist, registry) {
664
809
  }
665
810
  if (expanded === null) {
666
811
  devices.push(device);
667
- resolutions.push(resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, netlist));
812
+ resolutions.push(resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, netlist, ambientKelvin));
668
813
  continue;
669
814
  }
670
815
  for (const [section, resolution] of expanded) {
@@ -678,9 +823,44 @@ export function attachDeviceLaws(netlist, registry) {
678
823
  devices,
679
824
  nodes: internalNodes.size === 0 ? netlist.nodes : [...netlist.nodes, ...internalNodes.values()],
680
825
  },
681
- resolutions,
826
+ resolutions: foldAmbientTemperature(devices, resolutions, registry, ambientKelvin),
682
827
  };
683
828
  }
829
+ /**
830
+ * Fold the ambient temperature into every finished junction law, at compile time.
831
+ *
832
+ * With no ambient (or within a 1e-9 K band of 300 K) the resolutions pass through
833
+ * untouched -- the same array contents, so every program is byte-identical to
834
+ * today's. Otherwise each diode, BJT and OTA law is replaced by its scaled copy;
835
+ * every other outcome (macro, open, unsupported, non-junction laws) is returned as
836
+ * is. The runtime, the `Program` shape and both consoles are untouched: the option
837
+ * changes the numbers written into the stamps and nothing else.
838
+ */
839
+ function foldAmbientTemperature(devices, resolutions, registry, ambientKelvin) {
840
+ const kelvin = ambientKelvin;
841
+ if (!ambientTemperatureScalesJunction(kelvin)) {
842
+ return resolutions;
843
+ }
844
+ const byId = new Map(devices.map((device) => [device.id, device]));
845
+ return resolutions.map((resolution) => {
846
+ if (resolution.outcome !== "law") {
847
+ return resolution;
848
+ }
849
+ const law = resolution.law;
850
+ if (law.kind !== "diode" && law.kind !== "bjt" && law.kind !== "ota") {
851
+ return resolution;
852
+ }
853
+ const material = registryMaterialFor(registry, byId.get(resolution.device)?.identity.partNumber ?? null) ??
854
+ "silicon";
855
+ if (law.kind === "diode") {
856
+ return { ...resolution, law: diodeLawAtAmbientTemperature(law, material, kelvin) };
857
+ }
858
+ if (law.kind === "bjt") {
859
+ return { ...resolution, law: bjtLawAtAmbientTemperature(law, material, kelvin) };
860
+ }
861
+ return { ...resolution, law: otaLawAtAmbientTemperature(law, kelvin) };
862
+ });
863
+ }
684
864
  /**
685
865
  * A multi-section part as one device per section, or `null` when the part is not multi-section.
686
866
  *
@@ -2343,7 +2523,7 @@ function packageDeviceParameters(device, registry) {
2343
2523
  }
2344
2524
  return parameters;
2345
2525
  }
2346
- function resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, netlist) {
2526
+ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFrequencyHz, devicesTouchingNode, nodeToVoltage, netlist, ambientKelvin = REFERENCE_TEMPERATURE_KELVIN) {
2347
2527
  // A component declared as one transistor that is really several devices in one shell. Opened
2348
2528
  // rather than stamped, for the reason `isNonExecutableIcSupportShell` opens an IC shell: there
2349
2529
  // is no single device here to describe, and a transistor law would invent one. Reported by
@@ -2469,6 +2649,39 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
2469
2649
  law: { kind: "conductance", siemens: 1 / ohms },
2470
2650
  };
2471
2651
  }
2652
+ case "thermistor": {
2653
+ // An NTC disc at the ambient temperature: `R(T) = R_ref * exp(Beta *
2654
+ // (1/T - 1/T_ref))`, folded here at compile time like the junction fold
2655
+ // above, and stamped as the conductance a resistor of that value takes.
2656
+ // Beta 0 folds to exactly R_ref (exp(0) is 1), so the negative control
2657
+ // compiles to the same program as the same-valued resistor.
2658
+ //
2659
+ // **No self-heating.** The resistance is a function of the ambient
2660
+ // alone: the current through the disc does not warm it. A thermistor
2661
+ // that heats under load needs a thermal state the program does not have.
2662
+ const referenceOhms = device.parameters.referenceOhms ?? 0;
2663
+ const beta = device.parameters.betaKelvin ?? 0;
2664
+ const referenceKelvin = device.parameters.referenceKelvin ?? 298.15;
2665
+ if (device.nodes.length !== 2) {
2666
+ return unsupported(device, `thermistor has ${device.nodes.length} terminals, not the two a polarity-free disc carries`);
2667
+ }
2668
+ if (!(referenceOhms > 0) ||
2669
+ !Number.isFinite(beta) ||
2670
+ beta < 0 ||
2671
+ !(referenceKelvin > 0)) {
2672
+ return unsupported(device, "thermistor carries no usable Resistance, Beta or reference temperature");
2673
+ }
2674
+ const ohms = referenceOhms *
2675
+ Math.exp(beta * (1 / ambientKelvin - 1 / referenceKelvin));
2676
+ if (!(ohms > 0) || !Number.isFinite(ohms)) {
2677
+ return unsupported(device, "thermistor folds to a non-positive resistance at this ambient temperature");
2678
+ }
2679
+ return {
2680
+ outcome: "law",
2681
+ device: device.id,
2682
+ law: { kind: "conductance", siemens: 1 / ohms },
2683
+ };
2684
+ }
2472
2685
  case "potentiometer": {
2473
2686
  const ohms = device.parameters.ohms ?? 0;
2474
2687
  if (!(ohms > 0)) {
@@ -2945,6 +3158,13 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
2945
3158
  // number only. This shares the `fet` law kind with JFET entries; part
2946
3159
  // ids are disjoint across the two classes, so a lookup by exact folded
2947
3160
  // part id cannot cross from one to the other.
3161
+ //
3162
+ // **MOSFET is decided by the declared `kind`, read here.** This `case`
3163
+ // is the law selection the compiler already makes from the device's
3164
+ // typed kind: `kind: mosfet` lowers here, `kind: jfet` to the case
3165
+ // above. No part-number prose is read (a `2N7000` string never decides
3166
+ // anything), and the registry class is not consulted for the decision
3167
+ // either -- it only refines the numbers once the kind has decided.
2948
3168
  const catalogued = registryLawFor(registry, device.identity.partNumber, "fet");
2949
3169
  const entry = catalogued !== null && catalogued.kind === "fet" ? catalogued : null;
2950
3170
  return {
@@ -2963,6 +3183,12 @@ function resolveDevice(device, registry, rails, supply, taperByControl, mainsFre
2963
3183
  gateSaturationCurrent: entry?.gateSaturationCurrent ?? 0,
2964
3184
  gateOnsetVolts: entry?.gateOnsetVolts ?? 0,
2965
3185
  gateScaleVolts: entry?.gateScaleVolts ?? 1,
3186
+ // The body diode. Read from the registry entry only (a MOSFET
3187
+ // gate/body refinement is never a per-device document value, the
3188
+ // same rule the gate terms above follow), else the class
3189
+ // defaults, which `fet-law-default-parameters` names.
3190
+ bodySaturationCurrent: entry?.bodySaturationCurrent ?? MOSFET_BODY_SATURATION_CURRENT,
3191
+ bodyEmissionCoefficient: entry?.bodyEmissionCoefficient ?? MOSFET_BODY_EMISSION_COEFFICIENT,
2966
3192
  },
2967
3193
  };
2968
3194
  }