driftlet 1.3.0 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/impedance.js CHANGED
@@ -22,33 +22,44 @@
22
22
  // → { f, Z: { re, im }, relError, profiles? }, Z in Ω·m² (Ω for a non-planar device) at each
23
23
  // frequency.
24
24
  // Throws a SolverError where the terminal doesn't exist, has a series resistance, or every
25
- // other terminal is driven by a current (no return path: Z infinite), or where a frequency
26
- // isn't positive and finite.
25
+ // other terminal is driven by a current (no return path: Z infinite), where a frequency
26
+ // isn't positive and finite, or where the linearised system is singular at one.
27
27
  // Linearised about the present state: (J + iωS)·δx + Σ_k B_k δV_k = 0 for the grid's rows and,
28
28
  // for each floating terminal k (driven by a current), (C_k + iωC′_k)·δx + (DI_k + iωDI′_k) δV_k
29
29
  // + Σ_k2 (DX + iωDX′)_k,k2 δV_k2 = δI_k, with J the steady Jacobian (dt = ∞) and S, C′, DI′,
30
- // DX′ the storage parts, (X(dt) − X(∞))·dt at dt = 1e-30. A floating terminal that alone feeds
30
+ // DX′ the storage parts, (X(dt) − X(∞))·dt at dt = 1e-30. The measured terminal is always
31
+ // excited by its voltage: driven by a current, it's measured as if held (δV = 1, Z = 1/δI, the
32
+ // same input impedance), its circuit row out of the small system, its current read as a held
33
+ // one's (with its circuit law's change where it has one); its profiles then scaled by Z, per
34
+ // unit current. A floating terminal that alone feeds
31
35
  // a conserved combination (a circuit law: a constraint or time law with its circuit row) has
32
36
  // its current read by that combination's change at every frequency, iω F δS/λ plus what a
33
- // resistance passes: at dt = 1e-30 every circuit law reads by the change (this.lawForm), and
34
- // its steady parts are C = 0, DI = 1/R (0 driven by a current), DX = 0. The measured terminal: held at a
35
- // voltage, δV = 1 and Z = 1/δI, δI its current into the device; driven by a current, δI = 1
36
- // and Z = δV. Every other terminal keeps its drive: held, δV = 0 (AC ground); driven, δI = 0
37
- // (AC open).
37
+ // resistance passes: at dt = 1e-30 every circuit law with a current to count (λ set) reads by
38
+ // the change (this.lawForm), and
39
+ // its steady parts are C = 0, DI = 1/R (0 driven by a current), DX = 0. δI is the measured
40
+ // terminal's current into the device. Every other terminal keeps its drive: held, δV = 0 (AC
41
+ // ground); driven, δI = 0 (AC open).
38
42
  // Each frequency: J + iωS with each row divided by its largest entry, factorised, preconditions
39
43
  // GMRES (m 12, 8 restarts, tol 1e-8; its best iterate kept) on the exact J·v (difference form).
40
- // Solved: y, the response with the floating terminals held (to the measured one's δV, or none),
44
+ // A floating contact otherwise reads its current, in its circuit row, at the reading of least
45
+ // error by the solved responses (its own, or any cut between it and a port's window: the same
46
+ // current); and where every other terminal floats, a reading with no direct V dependence (a
47
+ // cut's, a law's change) reads y as Σ X_b, every terminal's uniform shift passing nothing.
48
+ // Solved: y, the response with the floating terminals held (to the measured one's δV = 1),
41
49
  // and X_k, to each floating terminal's unit δV_k; then the floating terminals' rows, a small
42
50
  // complex system, for their δV; δx = y − Σ X_k δV_k.
43
51
  // The current read (held terminal): DI + iωDI′ + Σ (DX + iωDX′) δV + (C + iωC′)·δx, its error
44
- // estimated from its terms' round-off and the solves' residuals; for a contact, also across
52
+ // estimated from its terms' round-off, the solves' residuals, and each unknown's round-off in
53
+ // its own row, ε Σ_k |A_jk x_k| / |A_jj| (a metal's current unknown, G Δη to a huge G: a
54
+ // bound, cautious where the coupled system pins it better); for a contact, also across
45
55
  // each cut (_currentCuts: a segment's linearised total current, conduction and displacement,
46
56
  // between that contact and the nearest port's window where there are ports), the reading with
47
57
  // the least error taken; and where every other terminal floats, by Kirchhoff, Σ δV_k/R_k over
48
58
  // those behind a resistance, its error (ε + the solves' residual)·|δV_k|/R_k. With no floating
49
59
  // terminal, cuts, and that error over 1% of the current:
50
60
  // the response split into each region's uniform shift (_regionShift) and a remainder solved to
51
- // GMRES's floor, the shift's effect taken only at the rows it can touch (_regionEdgeRows), and
61
+ // GMRES's floor, the shift's effect taken only at the rows it can touch (_regionEdgeRows; a
62
+ // metal's carriers shifted, its current unknown not), and
52
63
  // that reading kept where its error is less.
53
64
  // relError: per frequency, the chosen reading's error estimate over |δI| (held); driven, the
54
65
  // same estimate of its unit current read back from δx. Cautious: it can exceed the true error
@@ -75,8 +86,7 @@
75
86
  // Also written, left as the last assembly at dt = ∞ left them: this.sourceTime (the present
76
87
  // time), cOld, thOld, th0Old, gateOld (the present state's), contactDStart, portQStart, the
77
88
  // residual, the Jacobian and the terminal terms, sysRest and the concentrations; this.lawForm
78
- // (every circuit law by the change, until a step sets it afresh). _regionShift treats every
79
- // node's slot 0 as φ̂, a metal's flux unknown too: harmless while a metal region is one cell.
89
+ // (every circuit law by the change, until a step sets it afresh).
80
90
  // Tests: test/impedance.test.js (Macdonald's blocking electrodes, a junction's depletion
81
91
  // capacitance, a MOS capacitor at both terminals, ports, profiles, Fermi–Dirac against
82
92
  // Boltzmann); the demos' EIS.
@@ -218,8 +228,8 @@ export class SmallSignal {
218
228
  * gives (J + iωM)·δx + Σ B_k δV_k = rhs for the grid's rows, and (C_k + iωC′_k)·δx +
219
229
  * (∂I_k/∂V_k)·δV_k = δI_set,k for each floating terminal's circuit row, with J the steady
220
230
  * Jacobian and M the storage part (from J(dt) = J + M/dt), B_k = ∂res/∂V_k and C_k = ∂I_k/∂x.
221
- * At the measured terminal, a held voltage is perturbed (rhs = −B δV) and its current read
222
- * from C, or a driven current is perturbed and its voltage solved for. The other terminals
231
+ * At the measured terminal, its voltage is perturbed (rhs = −B δV) and its current read, held
232
+ * or driven by a current alike (one input impedance; see the contract). The other terminals
223
233
  * keep their drives: held ones at AC ground, driven ones at AC open circuit.
224
234
  * Z = δV/δI with I into the device (positive real part for a passive device).
225
235
  * @param {ArrayLike<number>} frequencies Hz
@@ -249,8 +259,14 @@ export class SmallSignal {
249
259
  `its impedance is infinite. Measure at one of those instead (impedance(f, { terminal: '${others[0].name}' }), its voltage against '${terminal}'), or hold one of them at a voltage`,
250
260
  );
251
261
  }
252
- const N = nB * M, fl = this.floating, K = fl.length;
253
- const kirchhoff = dT.kind === 'V' && others.every((t) => t.drive.kind === 'I' || t.drive.R > 0);
262
+ // Z is the device's input impedance at kT, the same whether its voltage is perturbed and its
263
+ // current read or the other way round. A terminal driven by a current is measured as if held
264
+ // (δV = 1, Z = 1/δI) to read that current as a held one's is (cuts, Kirchhoff, a circuit law's
265
+ // change): read through its own circuit row instead, at its contact alone, it was a metal's or
266
+ // a barrier's round-off (a reverse-biased diode's contact 4.3% off dV/dI; a Schottky
267
+ // contact's Re Z −32 for 1282), with an error estimate that never saw it.
268
+ const N = nB * M, fl = this.floating.filter((k) => k !== kT), K = fl.length;
269
+ const kirchhoff = others.every((t) => t.drive.kind === 'I' || t.drive.R > 0);
254
270
  this.sourceTime = this.time;
255
271
  this.computeConcentrations();
256
272
  this.cOld.set(this.c);
@@ -271,11 +287,14 @@ export class SmallSignal {
271
287
  // flux at dt = ∞ and by the change at dt = 1e-30, a floating gate's current was counted
272
288
  // twice (its capacitance 2× dQ/dV); read by the flux throughout, through a metal it is the
273
289
  // round-off of G Δη (a gate behind 1 kΩ·m², measured at the back contact: Re Z = −2.7).
274
- this.lawForm = new Map([...this.constraints, ...this.timeLaws].map((cs) => [cs, true]));
290
+ // (A law that counts no current, λ unset, as for a terminal at no current that feeds
291
+ // nothing, can't be read by its change: forced to, a closed Fe³⁺/Fe²⁺ cell's Z was NaN.)
292
+ const byChange = (cs) => cs.circuit >= 0 && !(cs.contacts?.length > 0) && Number.isFinite(cs.lambda) && cs.lambda !== 0;
293
+ this.lawForm = new Map([...this.constraints, ...this.timeLaws].map((cs) => [cs, byChange(cs)]));
275
294
  this.assemble(dts);
276
295
  for (const cs of [...this.constraints, ...this.timeLaws]) {
277
296
  const k = cs.circuit;
278
- if (k < 0 || !fl.includes(k) || cs.contacts?.length > 0) continue;
297
+ if (!byChange(cs) || !(fl.includes(k) || (k === kT && dT.kind === 'I'))) continue;
279
298
  Ct[k].fill(0);
280
299
  DI[k] = this.terms[k].drive.R > 0 ? 1 / this.terms[k].drive.R : 0;
281
300
  DX[k].fill(0);
@@ -288,11 +307,31 @@ export class SmallSignal {
288
307
  // Where the measured contact's current may be read across a segment: anywhere without
289
308
  // ports; with them, only between it and the nearest window (a port's exchange takes current
290
309
  // in or out along it). Never for a port's own terminal.
291
- let cuts = kT < 2 ? this._currentCuts() : null;
292
- if (cuts && this.model.ports.length > 0) {
293
- const windows = this.model.ports.flatMap((p) => Array.from(p.nodes));
294
- const lo = Math.min(...windows), hi = Math.max(...windows);
295
- cuts = cuts.filter((cut) => (kT === 0 ? cut.s + 1 <= lo : cut.s >= hi));
310
+ const allCuts = this._currentCuts(), windows = this.model.ports.flatMap((p) => Array.from(p.nodes));
311
+ const lo = windows.length ? Math.min(...windows) : Infinity, hi = windows.length ? Math.max(...windows) : -Infinity;
312
+ const cutsOf = (k) => allCuts.filter((cut) => (k === 0 ? cut.s + 1 <= lo : cut.s >= hi));
313
+ const cuts = kT < 2 ? cutsOf(kT) : null;
314
+ // A floating contact on a metal: its circuit row's current through the metal is the current
315
+ // unknown G Δη, round-off to a huge G (a Pt electrode behind 3000 Ω·m², read at the bath: Z −
316
+ // R came out 70.5 for 3070.5). Any cut between it and a port's window carries the same current
317
+ // (a metal stores none; the total current is continuous through its face), so each frequency
318
+ // reads it where its error, by the solved responses, is least (see readings, below).
319
+ const lawRead = new Set([...this.constraints, ...this.timeLaws].filter(byChange).map((cs) => cs.circuit));
320
+ const choices = new Map(); // k → [{ c, cs, di, dis, dx, dxs }]: its own reading, then each cut's
321
+ for (const k of fl) {
322
+ if (k > 1 || lawRead.has(k)) continue;
323
+ const own = { c: Ct[k].slice(), cs: Cs[k].slice(), di: DI[k], dis: DIs[k], dx: DX[k].slice(), dxs: DXs[k].slice() }, list = [own];
324
+ const sgn = k === 0 ? 1 : -1, R = this.terms[k].drive.R;
325
+ for (const cut of cutsOf(k)) {
326
+ const c = new Float64Array(own.c.length), cs = new Float64Array(own.c.length);
327
+ cut.idx.forEach((x, a) => {
328
+ if (this.rix[x] >= this.sys.size) return;
329
+ c[this.rix[x]] += sgn * cut.g[a];
330
+ cs[this.rix[x]] += sgn * cut.d[a];
331
+ });
332
+ list.push({ c, cs, di: R > 0 ? 1 / R : 0, dis: 0, dx: new Float64Array(own.dx.length), dxs: new Float64Array(own.dx.length) });
333
+ }
334
+ if (list.length > 1) choices.set(k, list);
296
335
  }
297
336
  // J·v for GMRES, exact: the assembled J holds a flux's dependence on η_L and η_R as two
298
337
  // entries, and where both are huge (an inversion layer) and v nearly uniform, J·v loses the
@@ -348,7 +387,12 @@ export class SmallSignal {
348
387
  }
349
388
  }
350
389
  }
351
- csys.factor();
390
+ try {
391
+ csys.factor();
392
+ } catch (err) {
393
+ // (it threw a bare Error, its block number all it said: a cell 2.8 V off its pzc)
394
+ throw new SolverError(`impedance: the linearised equations are singular at ${f} Hz (${err.message}): some part of the device is held by nothing at that frequency, as a double layer far from where it balances can leave it. See the steady state's warnings (solve()).`);
395
+ }
352
396
  const sx = (X, vr, out) => {
353
397
  for (let blk = 0; blk < nB; blk++) {
354
398
  const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
@@ -404,24 +448,77 @@ export class SmallSignal {
404
448
  };
405
449
  let accuracy = 1e-14; // (relative, of the solves: the readouts' error is weighed with it)
406
450
  // y: the response with the floating terminals held; X_k: to a unit δV_k.
407
- const heldT = dT.kind === 'V';
408
451
  for (let j = 0; j < Nc; j++) {
409
- rr[j] = heldT ? -Bt[kT][j] * scale[j] : 0;
452
+ rr[j] = -Bt[kT][j] * scale[j];
410
453
  ri[j] = 0;
411
454
  }
412
- solve(rr, ri, yr, yi, heldT ? kT : -1, heldT ? 1 : 0);
455
+ solve(rr, ri, yr, yi, kT, 1);
413
456
  fl.forEach((k, a) => {
414
457
  for (let j = 0; j < Nc; j++) rr[j] = Bt[k][j] * scale[j];
415
458
  ri.fill(0);
416
459
  solve(rr, ri, Xr[a], Xi[a], k, -1);
417
460
  });
461
+ // Each unknown's round-off in its own row: |x_j| to ε Σ_k |A_jk x_k| / |A_jj|, A = J + iωS. A
462
+ // metal's current unknown, G Δη to a huge G, is known only to ε G |δη| this way, which the
463
+ // terms' round-off alone never saw (a platinum electrode's current read 20% off at 1 nHz,
464
+ // its estimate 1e-8).
465
+ const rowFloor = (xr0, xi0) => {
466
+ const out = new Float64Array(Nc);
467
+ for (let blk = 0; blk < nB; blk++) {
468
+ const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
469
+ for (let r = 0; r < m; r++) {
470
+ let acc = 0;
471
+ const row = (X, o, len, x0) => {
472
+ for (let cc = 0; cc < len; cc++) acc += (Math.abs(J[X][o + cc]) + w * Math.abs(S[X][o + cc])) * Math.hypot(xr0[x0 + cc], xi0[x0 + cc]);
473
+ };
474
+ if (mp) row('A', offA[blk] + r * mp, mp, offX[blk - 1]);
475
+ row('B', offB[blk] + r * m, m, offX[blk]);
476
+ if (mn) row('C', offC[blk] + r * mn, mn, offX[blk + 1]);
477
+ const dIdx = offB[blk] + r * m + r, diag = Math.hypot(J.B[dIdx], w * S.B[dIdx]);
478
+ out[offX[blk] + r] = diag > 0 ? (1.1e-16 * acc) / diag : 0;
479
+ }
480
+ }
481
+ return out;
482
+ };
483
+ // Each floating metal contact's reading, of those it can take, with the least error by the
484
+ // solved responses (their terms' round-off and each unknown's in its own row).
485
+ const gaugeRead = new Set(lawRead); // (readings with no direct dependence on any V: see y below)
486
+ if (choices.size) {
487
+ const eps = 1.1e-16, parts = [[yr, yi], ...fl.map((_, b) => [Xr[b], Xi[b]])];
488
+ const mag = new Float64Array(Nc), floor = new Float64Array(Nc);
489
+ for (const [pr, pi] of parts) {
490
+ const rf = rowFloor(pr, pi);
491
+ for (let j = 0; j < Nc; j++) {
492
+ mag[j] += Math.hypot(pr[j], pi[j]);
493
+ floor[j] += rf[j];
494
+ }
495
+ }
496
+ for (const [k, list] of choices) {
497
+ let best = null, least = Infinity;
498
+ for (const r of list) {
499
+ let e = eps * (Math.abs(r.di) + w * Math.abs(r.dis));
500
+ for (let j = 0; j < Nc; j++) e += (Math.abs(r.c[j]) + w * Math.abs(r.cs[j])) * (eps * mag[j] + floor[j]);
501
+ if (e < least) [best, least] = [r, e];
502
+ }
503
+ if (best !== list[0]) gaugeRead.add(k);
504
+ Ct[k].set(best.c);
505
+ Cs[k].set(best.cs);
506
+ DI[k] = best.di;
507
+ DIs[k] = best.dis;
508
+ DX[k].set(best.dx);
509
+ DXs[k].set(best.dxs);
510
+ }
511
+ }
418
512
  // The floating terminals' circuit rows, a small complex system for their δV.
419
513
  const Ar = Array.from({ length: K }, () => new Float64Array(K + 1)), Ai = Array.from({ length: K }, () => new Float64Array(K + 1));
420
514
  fl.forEach((k, a) => {
515
+ let sre = 0, sim = 0;
421
516
  for (let b = 0; b < K; b++) {
422
517
  const [re, im] = dot(k, w, Xr[b], Xi[b]);
423
518
  Ar[a][b] = -re;
424
519
  Ai[a][b] = -im;
520
+ sre += re;
521
+ sim += im;
425
522
  }
426
523
  Ar[a][a] += DI[k];
427
524
  Ai[a][a] += w * DIs[k];
@@ -432,9 +529,14 @@ export class SmallSignal {
432
529
  Ar[a][b] += DX[k][k2];
433
530
  Ai[a][b] += w * DXs[k][k2];
434
531
  });
435
- const [yre, yim] = dot(k, w, yr, yi);
436
- Ar[a][K] = (k === kT ? 1 : 0) - yre - (heldT ? DX[k][kT] : 0);
437
- Ai[a][K] = -yim - (heldT ? w * DXs[k][kT] : 0);
532
+ // Every terminal shifted alike moves nothing and passes no current, and y + Σ (−X_b) is
533
+ // that shift where kT and the floating terminals are all there are: so a reading with
534
+ // no direct dependence on a V reads y as Σ X_b, exactly, from responses it resolves, where
535
+ // y itself is nearly that shift and its current a difference of them (a Pt electrode
536
+ // behind 3000 Ω·m², at 1 nHz: its cuts read y 10% apart).
537
+ const [yre, yim] = kirchhoff && gaugeRead.has(k) ? [sre, sim] : dot(k, w, yr, yi);
538
+ Ar[a][K] = -yre - DX[k][kT];
539
+ Ai[a][K] = -yim - w * DXs[k][kT];
438
540
  });
439
541
  const dV = complexSolve(Ar, Ai, K);
440
542
  // δx = y − Σ X_k δV_k
@@ -475,7 +577,8 @@ export class SmallSignal {
475
577
  const [re, im] = dot(kT, w, p.r, p.i);
476
578
  Ir += re;
477
579
  Ii += im;
478
- for (let j = 0; j < Nc; j++) err += (Math.abs(c[j]) + w * Math.abs(cs[j])) * (eps * Math.hypot(p.r[j], p.i[j]) + p.floor);
580
+ p.rf ??= rowFloor(p.r, p.i);
581
+ for (let j = 0; j < Nc; j++) err += (Math.abs(c[j]) + w * Math.abs(cs[j])) * (eps * Math.hypot(p.r[j], p.i[j]) + p.floor + p.rf[j]);
479
582
  }
480
583
  // Kirchhoff, where every other terminal floats: what they pass comes back through kT,
481
584
  // δI = Σ δV_k/R_k over those behind a resistance (one driven by a current passes no small
@@ -503,7 +606,7 @@ export class SmallSignal {
503
606
  // (g + iωd)(xr + i xi)
504
607
  re += gk * p.fr[x] - dk * p.fi[x];
505
608
  im += gk * p.fi[x] + dk * p.fr[x];
506
- e += (Math.abs(gk) + Math.abs(dk)) * (eps * Math.hypot(p.fr[x], p.fi[x]) + p.floor);
609
+ e += (Math.abs(gk) + Math.abs(dk)) * (eps * Math.hypot(p.fr[x], p.fi[x]) + p.floor + (this.rix[x] < Nc ? p.rf[this.rix[x]] : 0));
507
610
  }
508
611
  }
509
612
  if (e < err) [err, Ir, Ii] = [e, sgn * re, sgn * im];
@@ -524,7 +627,7 @@ export class SmallSignal {
524
627
  return [fr, fi];
525
628
  };
526
629
  let Zr, Zi;
527
- if (heldT) {
630
+ {
528
631
  // δI per volt: (C + iωC′)·δx + (∂I/∂V)(1 + iω′) at the terminal…
529
632
  let { Ir, Ii, err } = read([{ r: yr, i: yi, fr: xr, fi: xi, floor: accuracy * largest(yr, yi) }]);
530
633
  // …and where the solves leave that unresolved (the estimate past 1% of the current), the
@@ -565,14 +668,10 @@ export class SmallSignal {
565
668
  const d2 = Ir * Ir + Ii * Ii; // Z = 1/δI
566
669
  Zr = Ir / d2;
567
670
  Zi = -Ii / d2;
568
- } else {
569
- // δV per unit current; its error as the unit current's, read back from δx, is uncertain
570
- // (round-off in the current's terms, and the solves' residuals: see read)
571
- const a = fl.indexOf(kT);
572
- Zr = dV.re[a];
573
- Zi = dV.im[a];
574
- const back = read([{ r: yr, i: yi, fr: xr, fi: xi, floor: accuracy * largest(yr, yi) }], [Zr, Zi]);
575
- out.relError[q] = back.err / Math.hypot(back.Ir, back.Ii);
671
+ }
672
+ // (profiles per unit excitation: for a terminal driven by a current, per A/m², δx·Z)
673
+ if (profiles && dT.kind === 'I') {
674
+ for (let j = 0; j < N; j++) [xr[j], xi[j]] = [xr[j] * Zr - xi[j] * Zi, xr[j] * Zi + xi[j] * Zr];
576
675
  }
577
676
  out.Z.re[q] = Zr;
578
677
  out.Z.im[q] = Zi;
@@ -590,9 +689,12 @@ export class SmallSignal {
590
689
  for (let r = 0; r < model.regions.length; r++) {
591
690
  let sr = 0, si = 0, k = 0;
592
691
  const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
692
+ // (a metal's slot 0 is its segment's current, not φ̂: shifted, a platinum region of many
693
+ // cells read its electrode's current 8% off, at an error estimate of 1e-8)
694
+ const phiAt = (g) => this.nodeConductor[g] < 0 && loc[this.blockOfNode[g] * M] >= 0;
593
695
  for (let g = g0; g <= g1; g++) {
594
696
  const o = this.blockOfNode[g] * M;
595
- if (loc[o] >= 0) {
697
+ if (phiAt(g)) {
596
698
  sr += yr[R[o]];
597
699
  si += yi[R[o]];
598
700
  k++;
@@ -614,7 +716,7 @@ export class SmallSignal {
614
716
  si /= k;
615
717
  for (let g = g0; g <= g1; g++) {
616
718
  const o = this.blockOfNode[g] * M;
617
- if (loc[o] >= 0) {
719
+ if (phiAt(g)) {
618
720
  er[R[o]] = sr;
619
721
  ei[R[o]] = si;
620
722
  }
package/src/index.js CHANGED
@@ -132,6 +132,7 @@ export class Device {
132
132
  solver.sourceTime = old.sourceTime;
133
133
  solver._refreshSources();
134
134
  solver.dtNext = old.dtNext;
135
+ solver.coldStart = old.coldStart; // (a carried state is as cold as it was: see equilibrateFaces)
135
136
  solver.contactDEnd = old.contactDEnd;
136
137
  if (old.portQEnd?.length === solver.portQ.length) solver.portQEnd = old.portQEnd;
137
138
  solver.solvedV = old.solvedV; // where the carried-over state was solved (for continuation)
package/src/kit.js CHANGED
@@ -50,10 +50,14 @@ export function vacuumLevel(def, side, path = 'vacuum') {
50
50
  if (!Number.isFinite(side.offset)) fail(`${path}.offset must be a finite number (V)`);
51
51
  if (side.anchor === 'phi') return -side.offset;
52
52
  const sp = (def.species ?? []).find((s) => s.name === side.anchor);
53
- if (!sp) fail(`${path}.anchor must be 'phi' or a charged species, got ${JSON.stringify(side.anchor)}`);
53
+ if (!sp)
54
+ fail(
55
+ `${path}.anchor must be 'phi' or a charged species of the device, got ${JSON.stringify(side.anchor)}` +
56
+ (typeof side.anchor === 'string' ? ` (to anchor on it, list it in the library: aqueous([..., '${side.anchor}']), say)` : ''),
57
+ );
54
58
  if (!(sp.z !== 0)) fail(`${path}.anchor: '${side.anchor}' is neutral, so it has no level in volts`);
55
59
  const entry = mat.species?.[side.anchor];
56
- if (!isObject(entry)) fail(`${path}.anchor: '${side.anchor}' is absent from '${side.material}'`);
60
+ if (!isObject(entry)) fail(`${path}.anchor: '${side.anchor}' is absent from '${side.material}' (to anchor on it, give the material that species)`);
57
61
  if (!Number.isFinite(entry.mu0)) fail(`${path}.anchor: '${side.anchor}' in '${side.material}' has no mu0`);
58
62
  return entry.mu0 / (sp.z * FARADAY) - side.offset;
59
63
  }
package/src/newton.js CHANGED
@@ -75,7 +75,9 @@
75
75
  // Also written: s.islandsOn (as asked, or on by 8); s.lostMode, s.accepted (7); s.delta, s.deltaV,
76
76
  // s.rowDiv; what an assembly writes, at the returned state where a probe or 6's check ran;
77
77
  // s.conditioning (a steady solve's failure: the worst cancellation a factorisation showed, for
78
- // its message).
78
+ // its message); s.modelError, the message of a SolverError a statistics model threw at a state
79
+ // Newton reached (in the assembly, or computing the concentrations where it stops), which fails
80
+ // the call rather than leaving it (cleared by solveSteady and integrate, for their failures).
79
81
  // Tests: every solve and step goes through here; test/newton.test.js (the islands round, the
80
82
  // residual check's failure) and the MOS gate step in test/ports.test.js (a lost mode, to 1e-6);
81
83
  // test/jacobian.test.js checks the system it solves. The devices that needed each branch are in
@@ -401,6 +403,19 @@ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands
401
403
  };
402
404
  // A steady solve that fails: record how nearly singular the system was, and where; or, failing
403
405
  // with the islands' rows in, go back to the solution without them.
406
+ // The concentrations at the state Newton stops at, where a statistics model can refuse it (a
407
+ // Debye–Hückel solution past its range, at the converged state of a transient: the error was
408
+ // thrown from here, uncaught, out of solve() and advance()). Its message, for the failure.
409
+ const settle = () => {
410
+ try {
411
+ s.computeConcentrations();
412
+ return null;
413
+ } catch (err) {
414
+ if (!(err instanceof SolverError)) throw err;
415
+ s.modelError = err.message;
416
+ return err.message;
417
+ }
418
+ };
404
419
  const fail = (r) => {
405
420
  if (kept) {
406
421
  s.u.set(kept.u);
@@ -430,6 +445,7 @@ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands
430
445
  } catch (err) {
431
446
  // A statistics model can fail far from the solution (e.g. Debye–Hückel beyond its range).
432
447
  if (!(err instanceof SolverError)) throw err;
448
+ s.modelError = err.message;
433
449
  return fail({ converged: false, iterations: it, history, error: err.message });
434
450
  }
435
451
  equilibrate(s);
@@ -500,7 +516,8 @@ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands
500
516
  const done = { converged: true, iterations: it, history, residual: rmax };
501
517
  if (probed()) continue;
502
518
  if (islandsOff(it, done)) continue;
503
- s.computeConcentrations();
519
+ const refused = settle();
520
+ if (refused) return fail({ converged: false, iterations: it, history, error: refused });
504
521
  return done;
505
522
  }
506
523
  // Converged as far as round-off allows: the updates are already tiny (below 1e-6 thermal
@@ -523,12 +540,13 @@ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands
523
540
  const done = { converged: true, iterations: it, history, residual: rmax, roundoff: true };
524
541
  if (probed()) continue;
525
542
  if (islandsOff(it, done)) continue;
526
- s.computeConcentrations();
543
+ const refused = settle();
544
+ if (refused) return fail({ converged: false, iterations: it, history, error: refused });
527
545
  return done;
528
546
  }
529
547
  }
530
- s.computeConcentrations();
531
- return fail({ converged: false, iterations: maxIter, history });
548
+ const refused = settle();
549
+ return fail({ converged: false, iterations: maxIter, history, ...(refused ? { error: refused } : {}) });
532
550
  }
533
551
 
534
552
  // How far the terminal currents (as last assembled) are from adding up to zero, relative to the
package/src/solution.js CHANGED
@@ -272,10 +272,16 @@ export function makeSolution(solver, result = {}) {
272
272
  'advance() in time instead, or give the still species a small D where it is still.',
273
273
  );
274
274
  } else if (slow.length > 0) {
275
+ // Too slow to wait out only where the solve waited: where no time step was taken at all, the
276
+ // start was the trouble (a hole-only layer full of the anode's holes, with traps: "raise its
277
+ // rate constant" changed nothing, ×1e4).
278
+ const waited = (result.history ?? []).some((h) => h.converged && h.dt > 0);
275
279
  for (const { where, affinity } of slow) {
276
280
  sol.warnings.push(
277
281
  `the solve did not converge, likely because ${where} is out of equilibrium by ${(affinity * solver.VT * 1e3).toPrecision(3)} mV of affinity, yet nothing else makes or uses what it changes, so a steady state needs it at equilibrium: ` +
278
- 'it is too slow for the steady solve to wait out. Raise its rate constant (the steady state does not depend on it), or advance() in time.',
282
+ (waited
283
+ ? 'it is too slow for the steady solve to wait out. Raise its rate constant (the steady state does not depend on it), or advance() in time.'
284
+ : 'the solve failed from its first steps, so start it nearer that equilibrium: c0 near it, or solve without the reaction and set() it back.'),
279
285
  );
280
286
  }
281
287
  } else if (result.converged === false && cond && cond.digits > 12) {
@@ -311,6 +317,11 @@ export function makeSolution(solver, result = {}) {
311
317
  `the solve did not converge from a start far from equilibrium: ${solver.violent.join('; ')}, its double layer uncharged. ` +
312
318
  'Hold that terminal at a voltage first, solve(), then let it float (set its I: 0): it keeps the state the held one gave it.',
313
319
  );
320
+ } else if (result.converged === false && !result.stopped && !un && !ch && !lost && slow.length === 0 && !(cond && cond.digits > 12) && solver.modelError) {
321
+ sol.warnings.push(
322
+ `the solve did not converge: the states it reached were refused by a statistics model: ${solver.modelError}. ` +
323
+ 'The model doesn\'t hold there: a smaller concentration or drive, or a model for it (lattice for crowding).',
324
+ );
314
325
  } else if (result.converged === false && !result.stopped && !un && !ch && !lost && slow.length === 0 && !(cond && cond.digits > 12)) {
315
326
  sol.warnings.push(
316
327
  'the solve did not converge, and nothing specific showed why. ' +
package/src/solver.js CHANGED
@@ -291,9 +291,15 @@ export class Solver {
291
291
  this.termDX = this.terms.map(() => new Float64Array(this.terms.length));
292
292
  this.termRes = new Float64Array(this.terms.length);
293
293
  this._refreshSources();
294
- // A floating terminal starts level with the first held one (no current).
295
- const level = this.termV[this.terms.findIndex((_, k) => !this.floating.includes(k))];
296
- for (const k of this.floating) this.termV[k] = this.terms[k].drive.kind === 'V' ? this.termV[k] : level;
294
+ // A floating terminal starts level with the first held one (no current); where none is held,
295
+ // with the source behind the first resistance (it was NaN there, and a metal contact's
296
+ // carrier, its level from that V, asked for a c0 a conductor can't be given), else 0.
297
+ const heldK = this.terms.findIndex((_, k) => !this.floating.includes(k)), behindR = this.terms.find((t) => t.drive.R > 0);
298
+ const level = heldK >= 0 ? this.termV[heldK] : behindR ? sourceAt(behindR.drive.src, this.time) : 0;
299
+ // (One behind a resistance too: at its own source, a silver cell held at 0 V and 16 V behind R
300
+ // began with 16 V across it; left at 0 V, one behind a −30 V source beside a current began
301
+ // with 30 V across it. Level, the circuit rows move them from a device at rest.)
302
+ for (const k of this.floating) this.termV[k] = level;
297
303
  // Accepted steps, most recent first: start time, size, start state (for BDF2 and the
298
304
  // error estimate of adaptive stepping).
299
305
  this.history = [];
@@ -830,6 +836,7 @@ export class Solver {
830
836
  }
831
837
  this.termV.set(src.termV);
832
838
  this.solvedV = src.solvedV;
839
+ this.coldStart = src.coldStart;
833
840
  this.time = src.time;
834
841
  this.computeConcentrations();
835
842
  }
@@ -855,6 +862,7 @@ export class Solver {
855
862
  history: this.history.slice(),
856
863
  termV: Float64Array.from(this.termV),
857
864
  sourceTime: this.sourceTime,
865
+ coldStart: this.coldStart,
858
866
  };
859
867
  }
860
868
 
@@ -878,6 +886,7 @@ export class Solver {
878
886
  this.history = s.history.slice();
879
887
  this.termV.set(s.termV);
880
888
  this.sourceTime = s.sourceTime;
889
+ this.coldStart = s.coldStart;
881
890
  this.computeConcentrations();
882
891
  }
883
892