@vessel-dsp/runtime 0.4.1 → 0.5.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.
Binary file
@@ -1339,7 +1339,10 @@ var OPAMP_FOLD_STREAK = 6;
1339
1339
  function opAmpHalfSwing(railHigh, railLow) {
1340
1340
  return Math.max((railHigh - railLow) / 2, 1e-9);
1341
1341
  }
1342
- var OPAMP_GAIN_BANDWIDTH_HZ = 1e6;
1342
+ var OPAMP_DEFAULT_GAIN_BANDWIDTH_HZ = 1e6;
1343
+ function opAmpGainBandwidthHz(stamp) {
1344
+ return stamp.gainBandwidthHz ?? OPAMP_DEFAULT_GAIN_BANDWIDTH_HZ;
1345
+ }
1343
1346
  var OPAMP_POLE_BAND = 8;
1344
1347
  function boundOpAmpRaw(raw, halfSwing) {
1345
1348
  const limit = OPAMP_POLE_BAND * halfSwing;
@@ -1850,6 +1853,8 @@ var ReferenceRuntime = class {
1850
1853
  limiterTrace = typeof process !== "undefined" && process.env?.VESSEL_LIMITER_TRACE === "1";
1851
1854
  limitedIterationCount = 0;
1852
1855
  limitedIterationPerSample = [];
1856
+ /** Fold reseeds taken so far (diagnostic, read past `private` by the stall study's tracer). */
1857
+ foldReseedCount = 0;
1853
1858
  /** Series ohms of whatever drives the input jack -- see `prepare()`'s option. */
1854
1859
  inputSourceOhms = 0;
1855
1860
  /**
@@ -4025,7 +4030,9 @@ var ReferenceRuntime = class {
4025
4030
  let bestDelta = Number.POSITIVE_INFINITY;
4026
4031
  let noImprovement = 0;
4027
4032
  let foldStreak = 0;
4028
- let foldReseeded = false;
4033
+ let foldReseeds = 0;
4034
+ let lastWorstNode = -1;
4035
+ let lastWorstSign = 0;
4029
4036
  for (let iteration = 0; iteration < iterations; iteration += 1) {
4030
4037
  used = iteration + 1;
4031
4038
  this.limitedIterate = false;
@@ -4153,6 +4160,12 @@ var ReferenceRuntime = class {
4153
4160
  }
4154
4161
  }
4155
4162
  }
4163
+ const damping = dc ? 1 : this.opAmpStepDamping(block, blockIndex, current, next, dt, dc);
4164
+ if (damping < 1) {
4165
+ for (let i = 0; i < size; i++) {
4166
+ next[i] = current[i] + damping * (next[i] - current[i]);
4167
+ }
4168
+ }
4156
4169
  if (relaxing) {
4157
4170
  for (let i = 0; i < size; i++) {
4158
4171
  next[i] = current[i] + alpha * (next[i] - current[i]);
@@ -4161,6 +4174,10 @@ var ReferenceRuntime = class {
4161
4174
  const delta = maxAbsDifference(next, current);
4162
4175
  worstNode = worstDifferenceIndex(next, current);
4163
4176
  worstDelta = delta;
4177
+ const worstSign = Math.sign((next[worstNode] ?? 0) - (current[worstNode] ?? 0));
4178
+ const reversed = dc || worstNode === lastWorstNode && worstSign !== 0 && lastWorstSign !== 0 && worstSign !== lastWorstSign;
4179
+ lastWorstNode = worstNode;
4180
+ lastWorstSign = worstSign;
4164
4181
  if (delta < bestDelta * 0.999) {
4165
4182
  bestDelta = delta;
4166
4183
  noImprovement = 0;
@@ -4173,7 +4190,7 @@ var ReferenceRuntime = class {
4173
4190
  }
4174
4191
  }
4175
4192
  } else {
4176
- noImprovement += 1;
4193
+ noImprovement += reversed ? 1 : 0;
4177
4194
  if (!relaxing) {
4178
4195
  if (noImprovement >= NEWTON_NON_CONTRACTING_LIMIT && iteration >= NEWTON_RELAXATION_EARLIEST_ITERATION) {
4179
4196
  relaxing = true;
@@ -4204,13 +4221,15 @@ var ReferenceRuntime = class {
4204
4221
  }
4205
4222
  const limitedOpamp = this.limitedOpamp;
4206
4223
  foldStreak = limitedOpamp?.folded === true ? foldStreak + 1 : 0;
4207
- if (!dc && !foldReseeded && limitedOpamp !== null && foldStreak >= OPAMP_FOLD_STREAK) {
4208
- foldReseeded = true;
4224
+ if (!dc && limitedOpamp !== null && foldStreak >= OPAMP_FOLD_STREAK) {
4225
+ foldReseeds += 1;
4226
+ this.foldReseedCount += 1;
4209
4227
  foldStreak = 0;
4210
4228
  const wasHigh = (start[limitedOpamp.output] ?? limitedOpamp.centre) >= limitedOpamp.centre;
4211
- current[limitedOpamp.output] = wasHigh ? limitedOpamp.railLow : limitedOpamp.railHigh;
4229
+ const toHigh = foldReseeds % 2 === 1 ? wasHigh : !wasHigh;
4230
+ current[limitedOpamp.output] = toHigh ? limitedOpamp.railHigh : limitedOpamp.railLow;
4212
4231
  this.opampHistory.set(limitedOpamp.key, {
4213
- differential: wasHigh ? -limitedOpamp.band : limitedOpamp.band,
4232
+ differential: toHigh ? limitedOpamp.band : -limitedOpamp.band,
4214
4233
  step: 0,
4215
4234
  cap: limitedOpamp.maxStep
4216
4235
  });
@@ -4250,6 +4269,53 @@ var ReferenceRuntime = class {
4250
4269
  }
4251
4270
  return { solution: current, converged, used, worstNode, worstDelta };
4252
4271
  }
4272
+ /**
4273
+ * The factor the Newton update is scaled by so that no railed op-amp's differential moves
4274
+ * further than its step cap and no op-amp output node moves further than one full swing:
4275
+ * the smallest `cap / |requested differential step|` and `2 * halfSwing / |requested output
4276
+ * step|` over the block's railed op-amps, or 1. `current` is the iterate the rows were
4277
+ * stamped at and `next` the undamped solve. The history read here is the one the stamps
4278
+ * wrote this iteration, so `differential` is the point each row's tangent was taken at and
4279
+ * `cap` the step the next evaluation will allow; a request inside both bounds scales nothing.
4280
+ * Mirrored in `Engine.cpp` (`opAmpStepDamping`); the two must agree to the operation.
4281
+ */
4282
+ opAmpStepDamping(block, blockIndex, current, next, dt, dc) {
4283
+ let damping = 1;
4284
+ for (const stamp of block.stamps) {
4285
+ if (stamp.kind !== "ideal-opamp" || stamp.railHigh === null || stamp.railLow === null) {
4286
+ continue;
4287
+ }
4288
+ const history = this.opampHistory.get(
4289
+ packHistoryKey3(blockIndex, stamp.plus, stamp.minus, stamp.output)
4290
+ );
4291
+ if (history === void 0) continue;
4292
+ const halfSwing = opAmpHalfSwing(stamp.railHigh, stamp.railLow);
4293
+ const alpha = dc ? 1 : opAmpPoleAlpha(
4294
+ stamp.openLoopGain,
4295
+ opAmpGainBandwidthHz(stamp),
4296
+ dt,
4297
+ this.sampleRate
4298
+ );
4299
+ const effectiveGain = alpha * stamp.openLoopGain;
4300
+ const linearWidth = halfSwing / effectiveGain;
4301
+ const band = OPAMP_DIFFERENTIAL_BAND * linearWidth;
4302
+ const nextDifferential = clamp(
4303
+ (next[stamp.plus] ?? 0) - (next[stamp.minus] ?? 0),
4304
+ -band,
4305
+ band
4306
+ );
4307
+ const magnitude = Math.abs(nextDifferential - history.differential);
4308
+ if (magnitude > history.cap) {
4309
+ damping = Math.min(damping, history.cap / magnitude);
4310
+ }
4311
+ const swing = 2 * halfSwing;
4312
+ const outputStep = Math.abs((next[stamp.output] ?? 0) - (current[stamp.output] ?? 0));
4313
+ if (outputStep > swing) {
4314
+ damping = Math.min(damping, swing / outputStep);
4315
+ }
4316
+ }
4317
+ return damping;
4318
+ }
4253
4319
  /**
4254
4320
  * Build-order step 6, vertical slice: the part of a block's matrix that is the same for
4255
4321
  * every Newton iteration of this sample -- every stamp `newtonPortRowsOf` calls linear,
@@ -5073,7 +5139,7 @@ var ReferenceRuntime = class {
5073
5139
  const key = packHistoryKey3(blockIndex, stamp.plus, stamp.minus, stamp.output);
5074
5140
  const alpha = opAmpPoleAlpha(
5075
5141
  stamp.openLoopGain,
5076
- OPAMP_GAIN_BANDWIDTH_HZ,
5142
+ opAmpGainBandwidthHz(stamp),
5077
5143
  dt,
5078
5144
  this.sampleRate
5079
5145
  );
@@ -5718,26 +5784,29 @@ var ReferenceRuntime = class {
5718
5784
  );
5719
5785
  const alpha = dc ? 1 : opAmpPoleAlpha(
5720
5786
  stamp.openLoopGain,
5721
- OPAMP_GAIN_BANDWIDTH_HZ,
5787
+ opAmpGainBandwidthHz(stamp),
5722
5788
  dt,
5723
5789
  this.sampleRate
5724
5790
  );
5725
5791
  const effectiveGain = alpha * stamp.openLoopGain;
5726
5792
  const linearWidth = halfSwing / effectiveGain;
5727
5793
  const previous = this.opampHistory.get(opampKey);
5728
- const previousDifferential = previous?.differential ?? 0;
5729
5794
  const rawDifferential = (solution[stamp.plus] ?? 0) - (solution[stamp.minus] ?? 0);
5730
5795
  const band = OPAMP_DIFFERENTIAL_BAND * linearWidth;
5731
5796
  const bounded = clamp(rawDifferential, -band, band);
5797
+ const previousDifferential = previous?.differential ?? bounded;
5732
5798
  const step = bounded - previousDifferential;
5733
5799
  const maxStep = OPAMP_MAX_DIFFERENTIAL_STEP * linearWidth;
5734
5800
  const reversing = previous !== void 0 && previous.step !== 0 && step !== 0 && Math.sign(step) !== Math.sign(previous.step);
5735
5801
  const previousCap = previous?.cap ?? maxStep;
5736
5802
  const cap = reversing ? Math.max(previousCap / 2, linearWidth / 64) : Math.min(maxStep, previousCap * 1.2);
5737
5803
  const limited = Math.abs(step) > cap;
5804
+ const folded = cap <= linearWidth / 8 && Math.abs(step) >= linearWidth / 256;
5738
5805
  if (limited) {
5739
5806
  this.limitedIterate = true;
5740
5807
  this.limitedBy = `opamp +=${stamp.plus} -=${stamp.minus} out=${stamp.output}`;
5808
+ }
5809
+ if (limited || folded) {
5741
5810
  this.limitedOpamp = {
5742
5811
  key: opampKey,
5743
5812
  output: stamp.output,
@@ -5746,7 +5815,7 @@ var ReferenceRuntime = class {
5746
5815
  railLow: stamp.railLow,
5747
5816
  band,
5748
5817
  maxStep,
5749
- folded: cap <= linearWidth / 8
5818
+ folded
5750
5819
  };
5751
5820
  }
5752
5821
  const differential = limited ? previousDifferential + Math.sign(step) * cap : bounded;
@@ -5763,10 +5832,11 @@ var ReferenceRuntime = class {
5763
5832
  clamp(scaled, -OPAMP_SATURATION_BAND, OPAMP_SATURATION_BAND)
5764
5833
  );
5765
5834
  const slope = effectiveGain * (1 - derivativeShape * derivativeShape);
5835
+ const linearisedAt = limited ? differential : rawDifferential;
5766
5836
  matrix[row][stamp.output] += 1;
5767
5837
  matrix[row][stamp.plus] -= slope;
5768
5838
  matrix[row][stamp.minus] += slope;
5769
- rhs[row] = output - slope * differential;
5839
+ rhs[row] = output - slope * linearisedAt;
5770
5840
  break;
5771
5841
  }
5772
5842
  case "vccs": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vessel-dsp/runtime",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Real-time MNA simulation runtime and solver console for VesselDSP Program ROMs.",
5
5
  "keywords": [
6
6
  "guitar-pedal",
@@ -58,7 +58,7 @@
58
58
  "generate-kernels": "bun scripts/generate-kernels.ts"
59
59
  },
60
60
  "dependencies": {
61
- "@vessel-dsp/compiler": "0.4.0"
61
+ "@vessel-dsp/compiler": "0.5.0"
62
62
  },
63
63
  "publishConfig": {
64
64
  "access": "public"