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