driftlet 1.2.4 → 1.3.2

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/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)
@@ -241,7 +242,30 @@ export class Device {
241
242
  if (!steady.converged) throw new SolverError('impedance: the steady state did not converge');
242
243
  // The steady state's warnings (an unresolved double layer, say) hold for the impedance too.
243
244
  const { warnings } = makeSolution(this.solver, steady);
244
- return { ...this.solver.impedance(frequencies, opts), warnings };
245
+ const out = this.solver.impedance(frequencies, opts);
246
+ // Where the current a frequency carries is lost to round-off (far below the device's slowest
247
+ // relaxation, or read as a small difference of large terms), Z is said to be unresolved: an
248
+ // electrolyte between blocking electrodes at 1e-4 Hz read Re Z 1e4 times too large, and a
249
+ // closed host's held terminal at 1e-7 Hz a negative one, each returned without a word; Re Z
250
+ // as well, where its error estimate is over 10 times it (the same electrodes at 1e-3 Hz: 153×
251
+ // too large, its estimate 170 times it; at 100 Hz, right, 2 times).
252
+ const g = (x) => String(Number(x.toPrecision(3))), lost = [], rough = [];
253
+ out.f.forEach((f, q) => {
254
+ const e = out.relError[q], Z = Math.hypot(out.Z.re[q], out.Z.im[q]);
255
+ if (!(e <= 1e-2)) lost.push(f);
256
+ else if (!(10 * Math.abs(out.Z.re[q]) > e * Z)) rough.push(f); // (the estimate is cautious: right ones sat within 3× of it)
257
+ });
258
+ if (lost.length > 0) {
259
+ warnings.push(
260
+ `impedance: Z isn't resolved at ${g(lost[0])}${lost.length > 1 ? `…${g(lost.at(-1))}` : ''} Hz (${lost.length} of ${out.f.length} frequencies): its estimated error (relError) is over 1%, the current there lost to round-off ` +
261
+ '(far below a capacitive device\'s relaxation, where almost no current flows, or where the current is a small difference of large terms). ' +
262
+ 'Measuring at another terminal, or with this one driven by a current (at its steady current, e.g. I: 0), can avoid it: a closed host at open circuit resolved so, blocking electrodes did not.',
263
+ );
264
+ }
265
+ if (rough.length > 0) {
266
+ warnings.push(`impedance: Re Z isn't resolved at ${g(rough[0])}${rough.length > 1 ? `…${g(rough.at(-1))}` : ''} Hz (${rough.length} of ${out.f.length} frequencies): it's under a tenth of its estimated error, relError·|Z| (a cautious estimate), so there read it only as small next to Im Z.`);
267
+ }
268
+ return { ...out, warnings };
245
269
  }
246
270
 
247
271
  /**
package/src/newton.js ADDED
@@ -0,0 +1,605 @@
1
+ // Newton's iteration: the equations at one time (a step's end, or a steady state at dt = Infinity)
2
+ // solved from the state as it stands, its linear solves refined by GMRES where the factorised
3
+ // Jacobian loses a mode.
4
+ //
5
+ // CONTRACT
6
+ //
7
+ // newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands = false }) iterates on a Solver
8
+ // `s` (src/solver.js; its layout is in docs/internals.md): its unknowns u (+ uLo, compensated) and
9
+ // the voltages termV of its floating terminals. It's called by step() in src/timeSteps.js, which
10
+ // sets what the equations are (s.cOld, the sources' time, s.steady, s.combining) and restores the
11
+ // state where it fails. Each iteration calls s.assemble(dt) (the residual s.res, the Jacobian
12
+ // s.sys, the floating terminals' terms termRes, termB, termC, termDI, termDX, and in a steady
13
+ // solve the pins s.pins), then s._factor(), solveBordered (src/lawRows.js) and, where the
14
+ // assembly transformed the unknowns (s.transformed), s._untransform.
15
+ //
16
+ // Each iteration, in order:
17
+ // 1. Assembled; every row of the Jacobian (its three blocks), with its residual and its termB
18
+ // entries, divided by its largest entry (s.rowDiv; a row of zeros is left). A SolverError
19
+ // from the assembly or the solve, or any error from the factorisation, fails the call with
20
+ // its message.
21
+ // 2. Solved for the update; refined (7) where refining is on.
22
+ // 3. Its size: `step`, the largest |Δ| over φ̂ and each present η at the grid's nodes (a metal's
23
+ // carrier, not its flux slot), each coverage and gate fraction, and each floating terminal's
24
+ // |ΔV|/V_T; `own`, the same without the terminals.
25
+ // 4. Fails where step isn't finite ('non-finite update'), or where own > 1e4 after the first
26
+ // iteration, or own > 10× the first's after the sixth ('diverging').
27
+ // 5. Added, damped by α = min(1, maxStep/own) (the terminal voltages by the same α), u and uLo
28
+ // in compensated arithmetic.
29
+ // 6. Converged, undamped, where step < tol; or in a time step (it ≥ 2; not on a lost-mode
30
+ // device) where the contraction θ = step/(the last step) < 0.1, step < 1e-6 and
31
+ // θ/(1 − θ)·step < tol; or at round-off (it ≥ 4; refined at least once where refining is on)
32
+ // where step and the last are both under 1e-6 and neither shrank 4× from the one before it:
33
+ // `roundoff: true`. On a lost-mode device's time step (7), only on a refined update, unless
34
+ // its refinement failed three times in the call. Where a pivot was perturbed this iteration
35
+ // (s.sys.perturbedPivots) or dt = Infinity, a convergence stands only if the block system's
36
+ // residual (s.res; not the floating terminals' circuit rows nor the pins), reassembled and
37
+ // scaled at the new state, is under 1e-6 in every row; else the call fails, saying which.
38
+ // 7. Refining (refineSolve): on from an iteration with a perturbed pivot, or once Newton stalls
39
+ // (it ≥ 3, undamped, step > ¼ of the last and either under 1e-4 or grown twice running), or
40
+ // throughout a lost-mode device's time steps. GMRES on the exact J·v (the dilute terms in
41
+ // difference form, s._assembleDifference), preconditioned by the factorised solve, from that
42
+ // solve's update; the refined update is taken where GMRES converged and moved it by under one
43
+ // thermal unit, else refining stops: for the rest of the call, or on a lost-mode device for
44
+ // two iterations, three times at most. A lost-mode device (s.lostMode, set for good): in a
45
+ // time step, a refined correction that the next update kept (under a tenth of it), where the
46
+ // correction was of a plain update under 1e-4, by more than its size and 1e-6, or a probe's.
47
+ // A probe: on a device not yet lost-mode, a time step's 1st, 2nd, 4th, 8th… and every 32nd
48
+ // convergence (counted in s.accepted) is followed by one refined update at the accepted
49
+ // state, reassembled; over 1e-8 thermal units, it isn't accepted, and Newton goes on
50
+ // refining.
51
+ // 8. Islands (s.steady and dt = Infinity, with islands): converged, the terminal currents off
52
+ // Kirchhoff by more than 1e-9 of the largest and 1e-18 A/m², the islands' summed rows go in
53
+ // (s.islandsOn) and Newton carries on, refining from the second iteration after; at most
54
+ // two rounds, one where the call began with the rows in (`islands`). If it then fails, or
55
+ // goes 12 iterations (or to maxIter) without converging, the solution it converged to before
56
+ // that round is restored, with s.islandsOn as it was, and returned as converged.
57
+ //
58
+ // Returns { converged, iterations, history (each iteration's step), residual (the largest scaled
59
+ // residual before the last update; converged only), roundoff (true where 6 accepted at
60
+ // round-off), error (a message, on some failures) }.
61
+ //
62
+ // Invariants (each says what a reviewer should be able to check here):
63
+ // 1. Converged: the last update met 6, and where 6 reassembles, every scaled residual row of the
64
+ // block system at the returned state is under 1e-6.
65
+ // 2. Only u, uLo and termV are moved: by step 5, by 8's restore, and in a steady solve by each
66
+ // assembly's snapFlat (src/lawRows.js); on return s.c and the coverages are those of the
67
+ // returned state (computed there, or by the assembly at it), but after a SolverError from
68
+ // computeConcentrations, which leaves them part done.
69
+ // 3. Failed: the state is where the last iteration left it (8's restore aside); the caller
70
+ // restores it.
71
+ // 4. refineSolve leaves the residual, the terminal terms, the pins' residuals, s.transformed and
72
+ // the factorised Jacobian as it found them, and the update as the plain solve gave it unless
73
+ // it returns true.
74
+ // 5. Deterministic: the same state and options give the same iterates, bit for bit.
75
+ // Also written: s.islandsOn (as asked, or on by 8); s.lostMode, s.accepted (7); s.delta, s.deltaV,
76
+ // s.rowDiv; what an assembly writes, at the returned state where a probe or 6's check ran;
77
+ // s.conditioning (a steady solve's failure: the worst cancellation a factorisation showed, for
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).
81
+ // Tests: every solve and step goes through here; test/newton.test.js (the islands round, the
82
+ // residual check's failure) and the MOS gate step in test/ports.test.js (a lost mode, to 1e-6);
83
+ // test/jacobian.test.js checks the system it solves. The devices that needed each branch are in
84
+ // its comments.
85
+
86
+ import { solveBordered } from './lawRows.js';
87
+ import { SolverError } from './solver.js';
88
+
89
+ // GMRES(m), restarted, for a real system op(x) = b, op already preconditioned; x holds the
90
+ // starting guess and receives the answer. Returns whether it converged.
91
+ function gmresReal(op, b, x, { m = 10, restarts = 3, tol = 1e-12 } = {}) {
92
+ const N = b.length;
93
+ const dot = (a, c) => {
94
+ let t = 0;
95
+ for (let j = 0; j < N; j++) t += a[j] * c[j];
96
+ return t;
97
+ };
98
+ const bn = Math.sqrt(dot(b, b)) || 1;
99
+ for (let cycle = 0; cycle < restarts; cycle++) {
100
+ const ax = op(x), r = Float64Array.from(b, (v, j) => v - ax[j]);
101
+ const beta = Math.sqrt(dot(r, r));
102
+ if (!(beta > tol * bn)) return true;
103
+ const V = [r.map((v) => v / beta)], H = Array.from({ length: m + 1 }, () => new Float64Array(m));
104
+ const cs = [], sn = [], g = new Float64Array(m + 1);
105
+ g[0] = beta;
106
+ let k = 0, converged = false;
107
+ for (; k < m; k++) {
108
+ const w = op(V[k]);
109
+ for (let i = 0; i <= k; i++) {
110
+ const h = dot(V[i], w);
111
+ H[i][k] = h;
112
+ for (let j = 0; j < N; j++) w[j] -= h * V[i][j];
113
+ }
114
+ const hn = Math.sqrt(dot(w, w));
115
+ V.push(w.map((v) => v / (hn || 1)));
116
+ for (let i = 0; i < k; i++) {
117
+ const a = H[i][k], c = H[i + 1][k];
118
+ H[i][k] = cs[i] * a + sn[i] * c;
119
+ H[i + 1][k] = -sn[i] * a + cs[i] * c;
120
+ }
121
+ const t = Math.hypot(H[k][k], hn), c = t > 0 ? H[k][k] / t : 1, sv = t > 0 ? hn / t : 0;
122
+ cs.push(c);
123
+ sn.push(sv);
124
+ H[k][k] = t;
125
+ g[k + 1] = -sv * g[k];
126
+ g[k] *= c;
127
+ if (!(Math.abs(g[k + 1]) > tol * bn) || !(hn > 0)) {
128
+ k++;
129
+ converged = true;
130
+ break;
131
+ }
132
+ }
133
+ const y = new Float64Array(k);
134
+ for (let i = k - 1; i >= 0; i--) {
135
+ let v = g[i];
136
+ for (let j = i + 1; j < k; j++) v -= H[i][j] * y[j];
137
+ y[i] = v / H[i][i];
138
+ }
139
+ for (let i = 0; i < k; i++) for (let j = 0; j < N; j++) x[j] += y[i] * V[i][j];
140
+ if (converged) return true;
141
+ }
142
+ return false;
143
+ }
144
+
145
+ // Scale every row by its largest Jacobian entry (in place, residual too), dividing by it: its
146
+ // reciprocal overflows where it's subnormal (a co-ion crowded out of a double layer, ~1e-310).
147
+ function equilibrate(s) {
148
+ const { nB, sys, res, termB } = s, live = s._liveTerms();
149
+ const { A, B, C, sizes, offA, offB, offC, offX } = sys;
150
+ for (let b = 0; b < nB; b++) {
151
+ const m = sizes[b], mp = b > 0 ? sizes[b - 1] : 0, mn = b < nB - 1 ? sizes[b + 1] : 0;
152
+ for (let r = 0; r < m; r++) {
153
+ const oa = offA[b] + r * mp, ob = offB[b] + r * m, oc = offC[b] + r * mn;
154
+ let mx = 0;
155
+ for (let k = 0; k < mp; k++) mx = Math.max(mx, Math.abs(A[oa + k]));
156
+ for (let k = 0; k < m; k++) mx = Math.max(mx, Math.abs(B[ob + k]));
157
+ for (let k = 0; k < mn; k++) mx = Math.max(mx, Math.abs(C[oc + k]));
158
+ if (mx === 0) {
159
+ s.rowDiv[offX[b] + r] = 1;
160
+ continue;
161
+ }
162
+ const d = mx;
163
+ s.rowDiv[offX[b] + r] = d;
164
+ for (let k = 0; k < mp; k++) A[oa + k] /= d;
165
+ for (let k = 0; k < m; k++) B[ob + k] /= d;
166
+ for (let k = 0; k < mn; k++) C[oc + k] /= d;
167
+ res[offX[b] + r] /= d;
168
+ for (const k of live) termB[k][offX[b] + r] /= d; // (each terminal's ∂/∂V)
169
+ }
170
+ }
171
+ }
172
+
173
+ // A Newton solve refined by GMRES: the factorised Jacobian as the preconditioner, and J·x
174
+ // exact, with the dilute kernels' terms in difference form (see DifferenceTerms), as the
175
+ // impedance does. The assembled J holds a flux's dependence on η at its two ends as two
176
+ // entries; where they're huge (an inversion layer's 0.1 nm cells, a conductance ~1e11 against
177
+ // the layer's own storage and the trickle from the bulk), eliminating them loses the layer's
178
+ // overall level to round-off, and Newton rattles there. delta and deltaV come in as the plain
179
+ // solve's and leave refined (in the transformed unknowns, as solveBordered gives them); false
180
+ // if refining didn't help, leaving them as they came.
181
+ function refineSolve(s, dt, delta, deltaV) {
182
+ const { res, termRes, termB, termC, termDI, rowDiv, floating: fl } = s;
183
+ const N = s.sys.size, K = fl.length, pins = s.pins, R = s.rix;
184
+ const save = { res: Float64Array.from(res), termRes: Float64Array.from(termRes), termB: termB.map((b) => Float64Array.from(b)), termC: termC.map((c) => Float64Array.from(c)), termDI: Float64Array.from(termDI), termDX: s.termDX.map((x) => Float64Array.from(x)), transformed: s.transformed, pinRes: s.pins.map((p) => p.res) };
185
+ const restoreArrays = () => {
186
+ res.set(save.res);
187
+ termRes.set(save.termRes);
188
+ save.termB.forEach((b, k) => termB[k].set(b));
189
+ save.termC.forEach((c, k) => termC[k].set(c));
190
+ termDI.set(save.termDI);
191
+ save.termDX.forEach((x, k) => s.termDX[k].set(x));
192
+ s.transformed = save.transformed;
193
+ s.pins.forEach((p, a) => (p.res = save.pinRes[a]));
194
+ };
195
+ // The same system, its terms kept apart: the rest, and each floating terminal's (unscaled).
196
+ const { rest, lin } = s._assembleDifference(dt);
197
+ const Bk = fl.map((k) => Float64Array.from(termB[k])), Ck = fl.map((k) => Float64Array.from(termC[k])), Dk = fl.map((k) => termDI[k]);
198
+ restoreArrays();
199
+ const v = new Float64Array(N + 1), jv = new Float64Array(N);
200
+ const op = (x) => {
201
+ for (let j = 0; j < N; j++) v[j] = x[j];
202
+ v[N] = 0;
203
+ rest.multiply(v, jv);
204
+ if (save.transformed) s._untransform(v); // (the dilute terms in the plain unknowns)
205
+ lin.apply(v, jv, 1, 1 / dt);
206
+ // A flat stretch's rows hold each level to its first node's (see flatRows in src/lawRows.js).
207
+ for (const p of pins) {
208
+ if (!p.col) continue;
209
+ for (const o of p.col) if (o !== p.row && s.loc[o] >= 0) jv[R[o]] -= x[R[p.row]];
210
+ }
211
+ const out = new Float64Array(N + K);
212
+ for (let j = 0; j < N; j++) {
213
+ let t = jv[j];
214
+ for (let a = 0; a < K; a++) t += Bk[a][j] * x[N + a];
215
+ out[j] = t / rowDiv[j];
216
+ }
217
+ for (let a = 0; a < K; a++) {
218
+ let t = Dk[a] * x[N + a];
219
+ for (let b = 0; b < K; b++) if (b !== a) t += s.termDX[fl[a]][fl[b]] * x[N + b];
220
+ for (let j = 0; j < N; j++) t += Ck[a][j] * x[j];
221
+ out[N + a] = t;
222
+ }
223
+ // A pinned row is its constraint's (a conserved amount, an island's summed balance).
224
+ for (const p of pins) {
225
+ let t = 0;
226
+ for (let j = 0; j < p.len; j++) t += p.w[j] * x[p.idx[j]];
227
+ out[R[p.row]] = t;
228
+ }
229
+ return precondition(out);
230
+ };
231
+ const rhs = new Float64Array(N + 1), out = new Float64Array(N + 1), outV = new Float64Array(K);
232
+ const precondition = (r) => {
233
+ for (let j = 0; j < N; j++) rhs[j] = r[j];
234
+ fl.forEach((k, a) => (termRes[k] = r[N + a]));
235
+ for (const p of pins) {
236
+ p.res = rhs[R[p.row]];
237
+ rhs[R[p.row]] = 0;
238
+ }
239
+ solveBordered(s, rhs, out, outV, pins);
240
+ const z = new Float64Array(N + K);
241
+ for (let j = 0; j < N; j++) z[j] = out[j];
242
+ for (let a = 0; a < K; a++) z[N + a] = outV[a];
243
+ return z;
244
+ };
245
+ try {
246
+ const b = new Float64Array(N + K);
247
+ for (let j = 0; j < N; j++) b[j] = save.res[j];
248
+ fl.forEach((k, a) => (b[N + a] = save.termRes[k]));
249
+ pins.forEach((p, a) => (b[R[p.row]] = save.pinRes[a]));
250
+ const pb = precondition(b), x = Float64Array.from(pb);
251
+ // A lost mode or two, GMRES finds in a couple of iterations. The refined update is kept
252
+ // where GMRES converged and its correction is under a thermal unit; otherwise newton()
253
+ // stops refining.
254
+ const x0 = Float64Array.from(x);
255
+ const ok = gmresReal(op, pb, x, { m: 8, restarts: 1, tol: 1e-8 });
256
+ // (Measured on the potentials, as Newton's own step is: a rate the plain solve left
257
+ // unsolved, at a perturbed pivot, can need a correction of 41.7 mol/(m²·s).)
258
+ const dx = new Float64Array(N + 1), dV = new Float64Array(K);
259
+ for (let j = 0; j < N; j++) dx[j] = x[j] - x0[j];
260
+ for (let a = 0; a < K; a++) dV[a] = x[N + a] - x0[N + a];
261
+ if (s.transformed) s._untransform(dx);
262
+ const c = maxPotentialStep(s, dx, dV);
263
+ if (!ok || !(c < 1)) return false;
264
+ // A time step's plain update near the solution corrected by more than its own size: either the
265
+ // factorised solves lose a mode here (an inversion layer's level behind its 0.1 nm cells),
266
+ // which neither their updates nor the residual show, or J·v's own round-off in a row of huge
267
+ // entries made the correction (one ulp's worth, 2⁻¹³ or 2⁻¹⁹ exactly, on healthy devices).
268
+ // newton() tells them apart by the next update: a real correction stays.
269
+ s.lostCandidate = 0;
270
+ if (dt !== Infinity && !s.lostMode && c > 1e-6) {
271
+ const px = new Float64Array(N + 1), pV = new Float64Array(K);
272
+ for (let j = 0; j < N; j++) px[j] = x0[j];
273
+ for (let a = 0; a < K; a++) pV[a] = x0[N + a];
274
+ if (s.transformed) s._untransform(px);
275
+ const p = maxPotentialStep(s, px, pV);
276
+ if (p < 1e-4 && c > p) s.lostCandidate = c;
277
+ }
278
+ for (let j = 0; j < N; j++) delta[j] = x[j];
279
+ for (let a = 0; a < K; a++) deltaV[a] = x[N + a];
280
+ return true;
281
+ } finally {
282
+ restoreArrays();
283
+ }
284
+ }
285
+
286
+ // Largest update among the potential-like unknowns (φ̂ and η at grid nodes).
287
+ export function maxPotentialStep(s, delta, deltaV = s.deltaV) { // (deltaV null: the device's potentials only)
288
+ const { n, M } = s;
289
+ const R = s.rix; // (the sink entry of delta is 0)
290
+ let mx = 0;
291
+ if (deltaV) for (let a = 0; a < s.floating.length; a++) mx = Math.max(mx, Math.abs(deltaV[a]) / s.VT);
292
+ for (let g = 0; g < s.nNodes; g++) {
293
+ const b = s.blockOfNode[g];
294
+ if (s.nodeConductor[g] < 0) mx = Math.max(mx, Math.abs(delta[R[b * M]])); // (a metal's slot 0 is a flux)
295
+ for (let i = 0; i < n; i++) {
296
+ if (s.present[g * n + i]) mx = Math.max(mx, Math.abs(delta[R[b * M + 1 + i]]));
297
+ }
298
+ const k = s.surfPort[g]; // an electrode surface's coverages, as η
299
+ if (k >= 0) for (let q = 0; q < s.model.ports[k].surface.length; q++) mx = Math.max(mx, Math.abs(delta[R[b * M + 1 + n + q]]));
300
+ }
301
+ for (const gt of s.gateList) mx = Math.max(mx, Math.abs(delta[R[gt.o]])); // a gate's fraction open
302
+ return mx;
303
+ }
304
+
305
+ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands = false } = {}) {
306
+ const { delta, res } = s;
307
+ const deltaV = s.deltaV ?? (s.deltaV = new Float64Array(s.floating.length));
308
+ const history = [], ownHistory = [];
309
+ // Refined solves (see _refine), once the plain ones stall.
310
+ let refine = false, refined = 0, noRefine = false;
311
+ // A device whose plain solves lose a mode (see refineSolve): its time steps refined throughout,
312
+ // a convergence taken only on a refined update. A refinement that fails is tried again two
313
+ // iterations on (an early iterate's GMRES(8) can fail where a later one converges), three times.
314
+ const lost = () => dt !== Infinity && s.lostMode === true;
315
+ let lastRefined = false, failedAt = 0, failures = 0, candidate = 0, pending = 0;
316
+ if (lost()) refine = true;
317
+ // Islands (see applyIslands in src/lawRows.js): their summed rows go in only once the plain
318
+ // solves converge.
319
+ // Far from the solution, Newton exact along a weakly held level takes it by 1e5 thermal
320
+ // units (a floating base, cold at bias), where the plain solves, which barely see that level,
321
+ // converge. Converged, an island at the wrong level shows: it takes in a different current
322
+ // than it passes on, so the terminal currents don't add up to zero. Then the summed rows go
323
+ // in and Newton carries on from there. Converged again, a current that tiny against the
324
+ // conduction around it rides on differences (~1e-28 thermal units for 5 fA/m² through a bulk
325
+ // of 1e4 mol/m³ at 1e-4 m²/s) that the last update leaves known only to its own round-off:
326
+ // where the currents still don't add up, one more update polishes them. Where the islands'
327
+ // own system is held by static pivots elsewhere (a GaAs stack's minority carriers, at 1e-17
328
+ // A/m² of noise), their rows can throw Newton off instead: if it doesn't converge again
329
+ // within a dozen iterations, the plain solution stands.
330
+ s.islandsOn = islands;
331
+ let rounds = islands ? 1 : 0, kept = null, perturbed = false; // (rows in from the start: only the polishing round)
332
+ // Converged by a small update, but solved at a perturbed pivot, or in a steady solve (where a
333
+ // factorisation that lost a row's digits left a reaction's rate 3.6e7 off it, the update
334
+ // small, an I = 0 electrode at a voltage 0.4 V from its steady one): the residual itself,
335
+ // assembled at the state, must be small too (each row scaled by its largest Jacobian entry).
336
+ // Returns why it isn't solved, or null.
337
+ const unsolved = () => {
338
+ if (!perturbed && dt !== Infinity) return null;
339
+ try {
340
+ s.assemble(dt);
341
+ } catch (err) {
342
+ if (!(err instanceof SolverError)) throw err;
343
+ return err.message;
344
+ }
345
+ equilibrate(s);
346
+ let r = 0;
347
+ for (let k = 0; k < s.SINK; k++) r = Math.max(r, Math.abs(s.res[k]));
348
+ if (r < 1e-6) return null;
349
+ if (dt === Infinity) {
350
+ // (the conditioning at the state it stopped at, the assembly having cleared the last one)
351
+ try {
352
+ s._factor();
353
+ } catch {}
354
+ noteConditioning(s);
355
+ }
356
+ return perturbed ? 'a row left unsolved at a pivot that cancelled exactly' : `its updates converged, but a row's residual is ${r.toExponential(1)} of its largest term`;
357
+ };
358
+ // One refined update at the state as it stands (see refineSolve), its size; NaN where it can't.
359
+ const probe = () => {
360
+ try {
361
+ s.assemble(dt);
362
+ equilibrate(s);
363
+ try {
364
+ s._factor();
365
+ } catch {
366
+ return NaN;
367
+ }
368
+ deltaV.fill(0);
369
+ solveBordered(s, res, delta, deltaV, s.pins);
370
+ if (!refineSolve(s, dt, delta, deltaV)) return NaN;
371
+ if (s.transformed) s._untransform(delta);
372
+ return maxPotentialStep(s, delta, deltaV);
373
+ } catch (err) {
374
+ if (!(err instanceof SolverError)) throw err;
375
+ return NaN;
376
+ }
377
+ };
378
+ // A lost mode that never stalls Newton: every plain update converges quadratically while the
379
+ // mode keeps the predictor's error (a floating gate at rest in inversion, its electrons
380
+ // reaching the back contact only by minority diffusion: 2.3e-8 thermal units at the 16th
381
+ // step, 8e-5 V of drift ten steps on, as the extrapolation grew it). So a time step's 1st,
382
+ // 2nd, 4th, 8th… and every 32nd acceptance on a device not yet known to lose one is probed
383
+ // with a refined update at the accepted state: over the suite, under 1e-8 but for about a
384
+ // dozen (round-off acceptances on strictly neutral and 1e-4 s steps). One over 1e-8 sends
385
+ // Newton on, refining, and the next update decides (see candidate).
386
+ const probed = () => {
387
+ if (dt === Infinity || lost()) return false;
388
+ const k = (s.accepted = (s.accepted ?? 0) + 1);
389
+ if ((k & (k - 1)) !== 0 && k % 32 !== 0) return false;
390
+ const d = probe();
391
+ if (!(d > 1e-8)) return false;
392
+ refine = true;
393
+ noRefine = false;
394
+ pending = d;
395
+ return true;
396
+ };
397
+ const islandsOff = (it, result) => {
398
+ if (dt !== Infinity || !s.steady || s.islands.length === 0 || rounds >= 2 || !(kirchhoff(s) > 1e-9)) return false;
399
+ kept = { it, result, islandsOn: s.islandsOn, u: Float64Array.from(s.u), uLo: Float64Array.from(s.uLo), termV: Float64Array.from(s.termV) };
400
+ s.islandsOn = true;
401
+ rounds++;
402
+ return true;
403
+ };
404
+ // A steady solve that fails: record how nearly singular the system was, and where; or, failing
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
+ };
419
+ const fail = (r) => {
420
+ if (kept) {
421
+ s.u.set(kept.u);
422
+ s.uLo.set(kept.uLo);
423
+ s.termV.set(kept.termV);
424
+ s.islandsOn = kept.islandsOn;
425
+ s.computeConcentrations();
426
+ return { ...kept.result, iterations: r.iterations, history };
427
+ }
428
+ if (dt === Infinity) noteConditioning(s);
429
+ return r;
430
+ };
431
+ for (let it = 1; it <= maxIter; it++) {
432
+ if (kept && it - kept.it > 12) return fail({ converged: false, iterations: it - 1, history });
433
+ if (noRefine && failures < 3 && it - failedAt >= 2 && lost()) {
434
+ noRefine = false;
435
+ refine = true;
436
+ }
437
+ if (kept && it === kept.it + 2) {
438
+ // Refined from the islands' second update (their first moves the level, perhaps by
439
+ // more than a refinement may), since the plain solves have shown a mode they lose.
440
+ refine = true;
441
+ noRefine = false;
442
+ }
443
+ try {
444
+ s.assemble(dt);
445
+ } catch (err) {
446
+ // A statistics model can fail far from the solution (e.g. Debye–Hückel beyond its range).
447
+ if (!(err instanceof SolverError)) throw err;
448
+ s.modelError = err.message;
449
+ return fail({ converged: false, iterations: it, history, error: err.message });
450
+ }
451
+ equilibrate(s);
452
+ let rmax = 0;
453
+ for (let k = 0; k < s.SINK; k++) rmax = Math.max(rmax, Math.abs(res[k]));
454
+ try {
455
+ s._factor();
456
+ } catch (err) {
457
+ return fail({ converged: false, iterations: it, history, error: err.message });
458
+ }
459
+ // A pivot perturbed (one that cancelled to exactly nothing) can leave a row unsolved, its
460
+ // update small while its residual isn't: the same couple written twice on an electrode at
461
+ // open circuit converged with one spelling's rate row 41.7 mol/(m²·s) off, the couple
462
+ // 1.5 RT from equilibrium. Refine the solves (exact J·v) wherever one was, and before
463
+ // taking a small update for convergence there, look at the residual itself (see unsolved).
464
+ perturbed = s.sys.perturbedPivots > 0;
465
+ if (perturbed && !noRefine) refine = true;
466
+ try {
467
+ const pins = s.pins;
468
+ deltaV.fill(0);
469
+ solveBordered(s, res, delta, deltaV, pins);
470
+ lastRefined = refine && refineSolve(s, dt, delta, deltaV);
471
+ if (refine && !lastRefined) {
472
+ refine = false; // (GMRES didn't converge, or strayed: the plain solves it is)
473
+ noRefine = true;
474
+ failedAt = it;
475
+ failures++;
476
+ }
477
+ if (s.transformed) s._untransform(delta);
478
+ } catch (err) {
479
+ if (!(err instanceof SolverError)) throw err;
480
+ return fail({ converged: false, iterations: it, history, error: err.message });
481
+ }
482
+ // Convergence counts every unknown, floating terminal voltages too. Damping and the
483
+ // divergence check count only the device's own potentials: a terminal voltage enters
484
+ // linearly (conductance links, held levels, a capacitive face), so a large swing in one is
485
+ // safe, and damping it would throttle the whole update (a current-driven port switching
486
+ // off, its voltage collapsing by hundreds of volts, crawled 10 thermal units at a time).
487
+ const step = maxPotentialStep(s, delta, deltaV);
488
+ const own = maxPotentialStep(s, delta, null);
489
+ // A refined correction the next update keeps (under a tenth of it) was a lost mode: this
490
+ // device's time steps are refined from now on, a convergence taken only on a refined update.
491
+ if (candidate > 0 && step < 0.1 * candidate) s.lostMode = true;
492
+ if (lost() && !noRefine) refine = true;
493
+ candidate = lastRefined ? pending || s.lostCandidate : 0;
494
+ pending = 0;
495
+ history.push(step);
496
+ ownHistory.push(own);
497
+ if (!Number.isFinite(step)) return fail({ converged: false, iterations: it, history, error: 'non-finite update' });
498
+ // Give up early on clear divergence; the caller will take a smaller step instead.
499
+ if ((it > 1 && own > 1e4) || (it > 6 && own > 10 * ownHistory[0])) {
500
+ s.computeConcentrations();
501
+ return fail({ converged: false, iterations: it, history, error: 'diverging' });
502
+ }
503
+ const alpha = own > maxStep ? maxStep / own : 1;
504
+ addToState(s, delta, -alpha);
505
+ s.floating.forEach((k, a) => (s.termV[k] -= alpha * deltaV[a]));
506
+ // A time step's Newton contracts fast from its predictor (1e-2, 1e-9, 1e-15 thermal
507
+ // units). The next update would be about θ = step/(the last one) times this one, so where
508
+ // θ/(1 − θ) of it (Hairer and Wanner's estimate) is under tol, this update converged it:
509
+ // the iteration that would only show that is skipped, a third of a typical step's work.
510
+ const last = history[history.length - 2], theta = step / last;
511
+ const trusted = !lost() || lastRefined || failures >= 3;
512
+ const contracted = dt !== Infinity && !lost() && it >= 2 && step < 1e-6 && theta < 0.1 && (theta / (1 - theta)) * step < tol;
513
+ if (alpha === 1 && trusted && (step < tol || contracted)) {
514
+ const why = unsolved();
515
+ if (why) return fail({ converged: false, iterations: it, history, error: why });
516
+ const done = { converged: true, iterations: it, history, residual: rmax };
517
+ if (probed()) continue;
518
+ if (islandsOff(it, done)) continue;
519
+ const refused = settle();
520
+ if (refused) return fail({ converged: false, iterations: it, history, error: refused });
521
+ return done;
522
+ }
523
+ // Converged as far as round-off allows: the updates are already tiny (below 1e-6 thermal
524
+ // units, ~26 nV) and have stopped shrinking. A badly conditioned system's floor can sit
525
+ // above tol: a strictly neutral material on a short step, where φ is fixed only through
526
+ // fluxes that the storage term dwarfs, rattles at ~1e-9 after converging quadratically.
527
+ // (Raising this floor lets a weakly held population drift: a MOS capacitor without a
528
+ // channel port then showed a DC leak through its oxide.)
529
+ const [p1, p2] = [history[history.length - 2], history[history.length - 3]], floor = 1e-6;
530
+ // Stalled near the solution (not shrinking quadratically), or growing undamped twice
531
+ // running: the factorised Jacobian may have lost a slow mode to cancellation, and
532
+ // round-off's floor would accept it wrong, or its noise keep Newton from the solution (a
533
+ // bipolar stack's floating base, its holes held ~1e14 more weakly than they move within
534
+ // it). Refine the solves from here.
535
+ if (refine) refined++;
536
+ else if (!noRefine && alpha === 1 && it >= 3 && step > 0.25 * p1 && (step < 1e-4 || (step > p1 && p1 > p2))) refine = true;
537
+ if (alpha === 1 && trusted && it >= 4 && (!refine || refined >= 1 || noRefine) && step < floor && p1 < floor && step > 0.25 * p1 && p1 > 0.25 * p2) {
538
+ const why = unsolved();
539
+ if (why) return fail({ converged: false, iterations: it, history, error: why });
540
+ const done = { converged: true, iterations: it, history, residual: rmax, roundoff: true };
541
+ if (probed()) continue;
542
+ if (islandsOff(it, done)) continue;
543
+ const refused = settle();
544
+ if (refused) return fail({ converged: false, iterations: it, history, error: refused });
545
+ return done;
546
+ }
547
+ }
548
+ const refused = settle();
549
+ return fail({ converged: false, iterations: maxIter, history, ...(refused ? { error: refused } : {}) });
550
+ }
551
+
552
+ // How far the terminal currents (as last assembled) are from adding up to zero, relative to the
553
+ // largest of them: Kirchhoff's law, which a steady state keeps.
554
+ function kirchhoff(s) {
555
+ let sum = 0, mx = 0;
556
+ for (const I of s.termI) {
557
+ sum += I;
558
+ mx = Math.max(mx, Math.abs(I));
559
+ }
560
+ // (currents adding to under 1e-18 A/m², a few charges per m² a second, are round-off: at
561
+ // equilibrium every terminal's is, 1e-20 to 1e-31, and their sum as large as they are)
562
+ return mx > 0 && Math.abs(sum) > 1e-18 ? Math.abs(sum) / mx : 0;
563
+ }
564
+
565
+ // How many digits the last factorisation lost to cancellation, and where (the worst seen since
566
+ // the steady solve began). Only for diagnosing failures: a saturated species, for instance,
567
+ // can lose digits harmlessly.
568
+ function noteConditioning(s) {
569
+ let c;
570
+ try {
571
+ c = s.sys.cancellation();
572
+ } catch {
573
+ return;
574
+ }
575
+ if (!(c.digits > (s.conditioning?.digits ?? -1))) return;
576
+ const { grid } = s.model;
577
+ let x = NaN, where = '', nodes = [];
578
+ for (let g = 0; g < s.nNodes; g++) {
579
+ if (s.blockOfNode[g] !== c.block) continue;
580
+ x = grid.x[g];
581
+ where = s.model.regions[grid.nodeRegion[g]].name;
582
+ nodes = [g];
583
+ }
584
+ for (let f = 0; f < s.nFaces; f++) {
585
+ if (s.blockOfFace[f] !== c.block) continue;
586
+ x = grid.x[grid.regionEnd[f]];
587
+ where = `interfaces[${f}]`;
588
+ nodes = [grid.regionEnd[f], grid.regionEnd[f] + 1];
589
+ }
590
+ s.conditioning = { digits: c.digits, x, where, nodes };
591
+ }
592
+
593
+ // u += scale·d in compensated arithmetic (Knuth two-sum, then renormalise hi/lo).
594
+ function addToState(s, d, scale) {
595
+ const { u, uLo, fullOf } = s;
596
+ for (let j = 0; j < fullOf.length; j++) {
597
+ const k = fullOf[j], a = u[k], b = scale * d[j];
598
+ const sum = a + b, bb = sum - a;
599
+ const err = a - (sum - bb) + (b - bb);
600
+ const lo = uLo[k] + err;
601
+ const hi = sum + lo;
602
+ u[k] = hi;
603
+ uLo[k] = lo - (hi - sum);
604
+ }
605
+ }