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.
@@ -0,0 +1,755 @@
1
+ // Small signal: the impedance at one terminal about a steady state, by linearising the steady
2
+ // equations and adding the storage term's iω, and the complex profiles of the response.
3
+ //
4
+ // CONTRACT
5
+ //
6
+ // Methods of the Solver (src/solver.js; its layout is in docs/internals.md), installed on its
7
+ // prototype like the assembly's. Device.impedance (src/index.js) calls impedance() right after a
8
+ // steady solve that converged (src/steady.js), at the state it left. What they use of the rest:
9
+ // this.assemble(dt) (src/assemble.js) at dt = ∞ the steady Jacobian in this.sys (blocks A, B,
10
+ // C), and for every terminal while allTerminals is set, termB[k] = ∂res/∂V_k,
11
+ // termC[k] = ∂I_k/∂x, termDI[k] = ∂I_k/∂V_k, termDX[k][k2] = ∂I_k/∂V_k2,
12
+ // each compact (sys order). At a finite dt the same plus the storage
13
+ // term's derivatives over dt.
14
+ // this._assembleDifference(dt) (src/difference.js) the same Jacobian with the dilute kernels'
15
+ // terms kept apart in difference form (`lin`, applied exactly) and the rest
16
+ // in this.sysRest: J·v without the cancellation of an assembled matrix.
17
+ // this._fluxConcentrated (src/regions.js), this.jL / this.jR, sgFlux (src/bernoulli.js) a
18
+ // segment's flux and its derivatives, for the current cuts.
19
+ // ComplexBlockTridiagonal (src/blockTridiagonal.js) a complex block-tridiagonal factorisation.
20
+ //
21
+ // impedance(frequencies, { terminal = 'right', profiles = false })
22
+ // → { f, Z: { re, im }, relError, profiles? }, Z in Ω·m² (Ω for a non-planar device) at each
23
+ // frequency.
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.
27
+ // Linearised about the present state: (J + iωS)·δx + Σ_k B_k δV_k = 0 for the grid's rows and,
28
+ // for each floating terminal k (driven by a current), (C_k + iωC′_k)·δx + (DI_k + iωDI′_k) δV_k
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
31
+ // a conserved combination (a circuit law: a constraint or time law with its circuit row) has
32
+ // 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).
38
+ // Each frequency: J + iωS with each row divided by its largest entry, factorised, preconditions
39
+ // 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),
41
+ // and X_k, to each floating terminal's unit δV_k; then the floating terminals' rows, a small
42
+ // complex system, for their δV; δx = y − Σ X_k δV_k.
43
+ // 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
45
+ // each cut (_currentCuts: a segment's linearised total current, conduction and displacement,
46
+ // between that contact and the nearest port's window where there are ports), the reading with
47
+ // the least error taken; and where every other terminal floats, by Kirchhoff, Σ δV_k/R_k over
48
+ // those behind a resistance, its error (ε + the solves' residual)·|δV_k|/R_k. With no floating
49
+ // terminal, cuts, and that error over 1% of the current:
50
+ // 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
52
+ // that reading kept where its error is less.
53
+ // relError: per frequency, the chosen reading's error estimate over |δI| (held); driven, the
54
+ // same estimate of its unit current read back from δx. Cautious: it can exceed the true error
55
+ // many times (a factorised solve already under GMRES's tol is taken as it stands, and its
56
+ // residual, carried through a current that cancels, can put it 1e5 times over: blocking
57
+ // electrodes at 100 Hz), and Device.impedance warns where it's over 1%, or relError·|Z| is over
58
+ // 10·|Re Z|.
59
+ // profiles: per frequency, _smallSignalProfiles(δx): δφ (V), δμ̄_i (J/mol) and δc_i (mol/m³)
60
+ // at each node, complex, per volt (held) or per unit current (driven); NaN where φ is undefined,
61
+ // where a species is absent, and for a metal's concentration.
62
+ //
63
+ // Invariants (each says what a reviewer should be able to check here):
64
+ // 1. u, uLo, termV and the time are as before the call; allTerminals as it was.
65
+ // 2. Z is the linear response of the equations the steady solve solved: as f → 0 it tends to
66
+ // the steady state's differential dV/dI at the measured terminal (the other terminals'
67
+ // drives held), and the readout doesn't depend on which cut or the contact it was read at
68
+ // beyond its error estimate.
69
+ // 2b. The same terminal measured held (at the voltage it has) or driven by a current (at the
70
+ // current it passes) has the same Z, within relError: one input impedance.
71
+ // 3. A term the steady system holds by a law in place of a balance row (src/lawRows.js) is in
72
+ // the linearisation only as the assembly's rows are: steady rows replaced by laws are not
73
+ // in J (the assembly here runs with this.steady false).
74
+ // 4. Deterministic: the same state, frequencies and options give the same Z, bit for bit.
75
+ // Also written, left as the last assembly at dt = ∞ left them: this.sourceTime (the present
76
+ // time), cOld, thOld, th0Old, gateOld (the present state's), contactDStart, portQStart, the
77
+ // 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.
80
+ // Tests: test/impedance.test.js (Macdonald's blocking electrodes, a junction's depletion
81
+ // capacitance, a MOS capacitor at both terminals, ports, profiles, Fermi–Dirac against
82
+ // Boltzmann); the demos' EIS.
83
+
84
+ import { ComplexBlockTridiagonal } from './blockTridiagonal.js';
85
+ import { sgFlux } from './bernoulli.js';
86
+ import { EPS0, FARADAY } from './constants.js';
87
+ import { SolverError } from './solver.js';
88
+
89
+ // Solve the K×K complex system (Ar + i Ai) x = (last column), partial pivoting by modulus.
90
+ function complexSolve(Ar, Ai, K) {
91
+ for (let col = 0; col < K; col++) {
92
+ let p = col;
93
+ for (let r = col + 1; r < K; r++) if (Math.hypot(Ar[r][col], Ai[r][col]) > Math.hypot(Ar[p][col], Ai[p][col])) p = r;
94
+ [Ar[col], Ar[p]] = [Ar[p], Ar[col]];
95
+ [Ai[col], Ai[p]] = [Ai[p], Ai[col]];
96
+ const dr = Ar[col][col], di = Ai[col][col], d2 = dr * dr + di * di;
97
+ for (let r = col + 1; r < K; r++) {
98
+ const fr = (Ar[r][col] * dr + Ai[r][col] * di) / d2, fi = (Ai[r][col] * dr - Ar[r][col] * di) / d2;
99
+ for (let k = col; k <= K; k++) {
100
+ const ur = Ar[col][k], ui = Ai[col][k];
101
+ Ar[r][k] -= fr * ur - fi * ui;
102
+ Ai[r][k] -= fr * ui + fi * ur;
103
+ }
104
+ }
105
+ }
106
+ const re = new Float64Array(K), im = new Float64Array(K);
107
+ for (let r = K - 1; r >= 0; r--) {
108
+ let vr = Ar[r][K], vi = Ai[r][K];
109
+ for (let k = r + 1; k < K; k++) {
110
+ vr -= Ar[r][k] * re[k] - Ai[r][k] * im[k];
111
+ vi -= Ar[r][k] * im[k] + Ai[r][k] * re[k];
112
+ }
113
+ const dr = Ar[r][r], di = Ai[r][r], d2 = dr * dr + di * di;
114
+ re[r] = (vr * dr + vi * di) / d2;
115
+ im[r] = (vi * dr - vr * di) / d2;
116
+ }
117
+ return { re, im };
118
+ }
119
+
120
+ // GMRES(m), restarted, for a complex system op(x) = b, with vectors as [re, im] pairs and op
121
+ // already preconditioned; x holds the starting guess and receives the answer. A cycle's own
122
+ // residual estimate can drift far from the true one (a preconditioner nearly singular at low
123
+ // frequency, or an op whose round-off the preconditioner amplifies), so each cycle is judged by
124
+ // the true residual, at the next one's start: it returns once that's below tol, and gives up when
125
+ // two cycles in a row fail to lower it, leaving x at the best iterate. Returns the true residuals (relative
126
+ // to b) at the start and at that iterate.
127
+ function gmres(op, b, x, { m = 12, restarts = 8, tol = 1e-8 } = {}) {
128
+ const N = b[0].length;
129
+ const cdot = (a, c) => {
130
+ let re = 0, im = 0;
131
+ for (let j = 0; j < N; j++) {
132
+ re += a[0][j] * c[0][j] + a[1][j] * c[1][j];
133
+ im += a[0][j] * c[1][j] - a[1][j] * c[0][j];
134
+ }
135
+ return [re, im]; // conj(a)·c
136
+ };
137
+ const norm = (a) => Math.sqrt(cdot(a, a)[0]);
138
+ const bn = norm(b) || 1;
139
+ const best = [Float64Array.from(x[0]), Float64Array.from(x[1])];
140
+ let initial = Infinity, least = Infinity, idle = 0;
141
+ for (let cycle = 0; ; cycle++) {
142
+ const ax = op(x), r = [Float64Array.from(b[0], (v, j) => v - ax[0][j]), Float64Array.from(b[1], (v, j) => v - ax[1][j])];
143
+ const beta = norm(r) / bn;
144
+ if (cycle === 0) initial = beta;
145
+ idle = beta < least ? 0 : idle + 1;
146
+ if (idle === 0) {
147
+ least = beta;
148
+ best[0].set(x[0]);
149
+ best[1].set(x[1]);
150
+ }
151
+ if (!(beta > tol) || cycle === restarts || idle === 2) break;
152
+ const V = [[r[0].map((v) => v / (beta * bn)), r[1].map((v) => v / (beta * bn))]];
153
+ const H = Array.from({ length: m + 1 }, () => Array.from({ length: m }, () => [0, 0]));
154
+ const cs = [], sn = [], g = Array.from({ length: m + 1 }, () => [0, 0]);
155
+ g[0] = [beta * bn, 0];
156
+ let k = 0;
157
+ for (; k < m; k++) {
158
+ const w = op(V[k]);
159
+ for (let i = 0; i <= k; i++) {
160
+ const [hr, hi] = cdot(V[i], w);
161
+ H[i][k] = [hr, hi];
162
+ for (let j = 0; j < N; j++) {
163
+ w[0][j] -= hr * V[i][0][j] - hi * V[i][1][j];
164
+ w[1][j] -= hr * V[i][1][j] + hi * V[i][0][j];
165
+ }
166
+ }
167
+ const hn = norm(w);
168
+ H[k + 1][k] = [hn, 0];
169
+ V.push([w[0].map((v) => v / (hn || 1)), w[1].map((v) => v / (hn || 1))]);
170
+ // The earlier rotations, then a new one zeroing H[k+1][k]: x' = c x + s y, y' = −s̄ x + c y.
171
+ for (let i = 0; i < k; i++) {
172
+ const [xr, xi] = H[i][k], [yr, yi] = H[i + 1][k], c = cs[i], [sr, si] = sn[i];
173
+ H[i][k] = [c * xr + sr * yr - si * yi, c * xi + sr * yi + si * yr];
174
+ H[i + 1][k] = [-(sr * xr + si * xi) + c * yr, -(sr * xi - si * xr) + c * yi];
175
+ }
176
+ const [ar, ai] = H[k][k], am = Math.hypot(ar, ai), t = Math.hypot(am, hn);
177
+ const c = t > 0 ? am / t : 1, s = am > 0 ? [(ar / am) * (hn / t), (ai / am) * (hn / t)] : [1, 0];
178
+ cs.push(c);
179
+ sn.push(s);
180
+ H[k][k] = am > 0 ? [(ar / am) * t, (ai / am) * t] : [hn, 0];
181
+ H[k + 1][k] = [0, 0];
182
+ const [gr, gi] = g[k];
183
+ g[k] = [c * gr, c * gi];
184
+ g[k + 1] = [-(s[0] * gr + s[1] * gi), -(s[0] * gi - s[1] * gr)];
185
+ if (!(Math.hypot(g[k + 1][0], g[k + 1][1]) > tol * bn)) {
186
+ k++;
187
+ break;
188
+ }
189
+ }
190
+ // Back-substitute H y = g over the first k columns, then x += V y.
191
+ const y = Array.from({ length: k }, () => [0, 0]);
192
+ for (let i = k - 1; i >= 0; i--) {
193
+ let [vr, vi] = g[i];
194
+ for (let j = i + 1; j < k; j++) {
195
+ const [hr, hi] = H[i][j], [yr, yi] = y[j];
196
+ vr -= hr * yr - hi * yi;
197
+ vi -= hr * yi + hi * yr;
198
+ }
199
+ const [dr, di] = H[i][i], d2 = dr * dr + di * di;
200
+ y[i] = [(vr * dr + vi * di) / d2, (vi * dr - vr * di) / d2];
201
+ }
202
+ for (let i = 0; i < k; i++) {
203
+ const [yr, yi] = y[i];
204
+ for (let j = 0; j < N; j++) {
205
+ x[0][j] += yr * V[i][0][j] - yi * V[i][1][j];
206
+ x[1][j] += yr * V[i][1][j] + yi * V[i][0][j];
207
+ }
208
+ }
209
+ }
210
+ x[0].set(best[0]);
211
+ x[1].set(best[1]);
212
+ return { initial, residual: least };
213
+ }
214
+
215
+ export class SmallSignal {
216
+ /**
217
+ * Small-signal impedance about the current (steady) state, seen at one terminal. Linearising
218
+ * gives (J + iωM)·δx + Σ B_k δV_k = rhs for the grid's rows, and (C_k + iωC′_k)·δx +
219
+ * (∂I_k/∂V_k)·δV_k = δI_set,k for each floating terminal's circuit row, with J the steady
220
+ * 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
223
+ * keep their drives: held ones at AC ground, driven ones at AC open circuit.
224
+ * Z = δV/δI with I into the device (positive real part for a passive device).
225
+ * @param {ArrayLike<number>} frequencies Hz
226
+ */
227
+ impedance(frequencies, opts) {
228
+ const was = this.allTerminals;
229
+ this.allTerminals = true; // (it reads its terminal's ∂res/∂V and ∂I/∂x)
230
+ try {
231
+ return this._impedance(frequencies, opts);
232
+ } finally {
233
+ this.allTerminals = was;
234
+ }
235
+ }
236
+
237
+ _impedance(frequencies, { profiles = false, terminal = 'right' } = {}) {
238
+ const { M, nB, sys, VT } = this;
239
+ const kT = this.terms.findIndex((t) => t.name === terminal);
240
+ if (kT < 0) throw new SolverError(`impedance: no terminal named '${terminal}' (${this.terms.map((t) => t.name).join(', ')})`);
241
+ const dT = this.terms[kT].drive;
242
+ if (dT.R > 0) throw new SolverError(`impedance: terminal '${terminal}' has a series resistance; that belongs to the external circuit`);
243
+ // A small signal returns through the terminals held at a voltage: with every other one driven
244
+ // by a current (held open), none comes back, and the impedance is infinite (else round-off).
245
+ const others = this.terms.filter((t, k) => k !== kT);
246
+ if (others.length && others.every((t) => t.drive.kind === 'I')) {
247
+ throw new SolverError(
248
+ `impedance: every other terminal (${others.map((t) => t.name).join(', ')}) is driven by a current, so it is open to a small signal and no current returns through '${terminal}': ` +
249
+ `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
+ );
251
+ }
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);
254
+ this.sourceTime = this.time;
255
+ this.computeConcentrations();
256
+ this.cOld.set(this.c);
257
+ this.thOld.set(this.th);
258
+ this.th0Old.set(this.th0);
259
+ this.gateList.forEach((gt, j) => (this.gateOld[j] = this.u[gt.o] + this.uLo[gt.o]));
260
+ this.assemble(Infinity);
261
+ this.contactDStart = { ...this.contactD };
262
+ this.portQStart = Float64Array.from(this.portQ);
263
+ this.assemble(Infinity);
264
+ // The steady parts, then the storage parts from an assembly at a tiny dt.
265
+ const J = { A: sys.A.slice(), B: sys.B.slice(), C: sys.C.slice() };
266
+ const Bt = this.termB.map((v) => v.slice()), Ct = this.termC.map((v) => v.slice()), DI = Float64Array.from(this.termDI), DX = this.termDX.map((v) => v.slice());
267
+ const dts = 1e-30;
268
+ // A floating terminal that alone feeds a conserved combination has its current read, at a
269
+ // finite dt, by that combination's change (circuitLaws in src/lawRows.js), and here always
270
+ // so: iω F δS/λ, plus what a resistance passes, with no steady part of its own. Read by the
271
+ // flux at dt = ∞ and by the change at dt = 1e-30, a floating gate's current was counted
272
+ // twice (its capacitance 2× dQ/dV); read by the flux throughout, through a metal it is the
273
+ // 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]));
275
+ this.assemble(dts);
276
+ for (const cs of [...this.constraints, ...this.timeLaws]) {
277
+ const k = cs.circuit;
278
+ if (k < 0 || !fl.includes(k) || cs.contacts?.length > 0) continue;
279
+ Ct[k].fill(0);
280
+ DI[k] = this.terms[k].drive.R > 0 ? 1 / this.terms[k].drive.R : 0;
281
+ DX[k].fill(0);
282
+ }
283
+ const S = {};
284
+ for (const X of ['A', 'B', 'C']) S[X] = sys[X].map((v, k) => (v - J[X][k]) * dts);
285
+ const Cs = this.termC.map((v, k) => v.map((x, j) => (x - Ct[k][j]) * dts));
286
+ const DIs = this.termDI.map((x, k) => (x - DI[k]) * dts);
287
+ const DXs = this.termDX.map((v, k) => v.map((x, j) => (x - DX[k][j]) * dts));
288
+ // Where the measured contact's current may be read across a segment: anywhere without
289
+ // ports; with them, only between it and the nearest window (a port's exchange takes current
290
+ // 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));
296
+ }
297
+ // J·v for GMRES, exact: the assembled J holds a flux's dependence on η_L and η_R as two
298
+ // entries, and where both are huge (an inversion layer) and v nearly uniform, J·v loses the
299
+ // flux to round-off. So the dilute kernels' terms are kept in difference form (see
300
+ // DifferenceTerms), with the rest from a matrix of their own, steady (Jr) and storage (Sr).
301
+ this._assembleDifference(Infinity);
302
+ const Jr = { A: this.sysRest.A.slice(), B: this.sysRest.B.slice(), C: this.sysRest.C.slice() };
303
+ const { lin } = this._assembleDifference(dts);
304
+ const Sr = {};
305
+ for (const X of ['A', 'B', 'C']) Sr[X] = this.sysRest[X].map((v, k) => (v - Jr[X][k]) * dts);
306
+ this.assemble(Infinity);
307
+
308
+ const { sizes, offA, offB, offC, offX } = sys;
309
+ const csys = new ComplexBlockTridiagonal(nB, sizes);
310
+ const Nc = csys.size;
311
+ const rr = new Float64Array(Nc), ri = new Float64Array(Nc);
312
+ const yr = new Float64Array(Nc), yi = new Float64Array(Nc);
313
+ const Xr = fl.map(() => new Float64Array(Nc)), Xi = fl.map(() => new Float64Array(Nc));
314
+ const xr = new Float64Array(N), xi = new Float64Array(N);
315
+ const rRes = new Float64Array(Nc), iRes = new Float64Array(Nc);
316
+ const jr = new Float64Array(Nc), ji = new Float64Array(Nc), sr = new Float64Array(Nc), si = new Float64Array(Nc);
317
+ const out = { f: Float64Array.from(frequencies), Z: { re: new Float64Array(frequencies.length), im: new Float64Array(frequencies.length) }, relError: new Float64Array(frequencies.length) };
318
+ if (profiles) out.profiles = [];
319
+ // (C + iωC′)·(ar + i ai) for terminal k
320
+ const dot = (k, w, ar, ai) => {
321
+ let re = 0, im = 0;
322
+ const c = Ct[k], cs = Cs[k];
323
+ for (let j = 0; j < Nc; j++) {
324
+ re += c[j] * ar[j] - w * cs[j] * ai[j];
325
+ im += c[j] * ai[j] + w * cs[j] * ar[j];
326
+ }
327
+ return [re, im];
328
+ };
329
+ Array.from(frequencies).forEach((f, q) => {
330
+ const w = 2 * Math.PI * f;
331
+ if (!(w > 0 && Number.isFinite(w))) throw new SolverError('impedance: frequencies must be positive and finite');
332
+ // Rows scaled by their largest entry (and the terminal columns with them).
333
+ const scale = new Float64Array(Nc);
334
+ for (let blk = 0; blk < nB; blk++) {
335
+ const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
336
+ for (let r = 0; r < m; r++) {
337
+ const rows = [['A', offA[blk] + r * mp, mp], ['B', offB[blk] + r * m, m], ['C', offC[blk] + r * mn, mn]];
338
+ let mx = 0;
339
+ for (const [X, o, len] of rows) for (let c = 0; c < len; c++) mx = Math.max(mx, Math.abs(J[X][o + c]), w * Math.abs(S[X][o + c]));
340
+ const sc = mx > 0 ? 1 / mx : 1;
341
+ scale[offX[blk] + r] = sc;
342
+ for (const [X, o, len] of rows) {
343
+ const JX = J[X], SX = S[X], Xre = csys[X + 'r'], Xim = csys[X + 'i'];
344
+ for (let c = 0; c < len; c++) {
345
+ Xre[o + c] = JX[o + c] * sc;
346
+ Xim[o + c] = w * SX[o + c] * sc;
347
+ }
348
+ }
349
+ }
350
+ }
351
+ csys.factor();
352
+ const sx = (X, vr, out) => {
353
+ for (let blk = 0; blk < nB; blk++) {
354
+ const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
355
+ for (let r = 0; r < m; r++) {
356
+ let acc = 0;
357
+ for (let c = 0; c < mp; c++) acc += X.A[offA[blk] + r * mp + c] * vr[offX[blk - 1] + c];
358
+ for (let c = 0; c < m; c++) acc += X.B[offB[blk] + r * m + c] * vr[offX[blk] + c];
359
+ for (let c = 0; c < mn; c++) acc += X.C[offC[blk] + r * mn + c] * vr[offX[blk + 1] + c];
360
+ out[offX[blk] + r] = acc;
361
+ }
362
+ }
363
+ };
364
+ // A x = −σ ∂res/∂V_k, x = outr + i outi (σ = 1 for the response to the measured terminal,
365
+ // −1 for a floating one's unit δV): GMRES on the system preconditioned by the factorised one
366
+ // (whose error is concentrated in a few slow modes, which it finds in a few iterations).
367
+ const zero = new Float64Array(Nc);
368
+ const precondition = (ar, ai) => {
369
+ const outR = new Float64Array(Nc), outI = new Float64Array(Nc);
370
+ for (let j = 0; j < Nc; j++) {
371
+ rRes[j] = ar[j] * scale[j];
372
+ iRes[j] = ai[j] * scale[j];
373
+ }
374
+ csys.solve(rRes, iRes, outR, outI);
375
+ return [outR, outI];
376
+ };
377
+ // (J + iωS)·v, exactly (see DifferenceTerms), into (ar, ai).
378
+ const jv = (vr, vi, ar, ai) => {
379
+ sx(Jr, vr, jr);
380
+ sx(Jr, vi, ji);
381
+ sx(Sr, vi, si);
382
+ sx(Sr, vr, sr);
383
+ for (let j = 0; j < Nc; j++) {
384
+ ar[j] = jr[j] - w * si[j];
385
+ ai[j] = ji[j] + w * sr[j];
386
+ }
387
+ lin.apply(vr, ar, 1, 0);
388
+ lin.apply(vi, ar, 0, -w);
389
+ lin.apply(vi, ai, 1, 0);
390
+ lin.apply(vr, ai, 0, w);
391
+ };
392
+ const op = ([vr, vi]) => {
393
+ const ar = new Float64Array(Nc), ai = new Float64Array(Nc);
394
+ jv(vr, vi, ar, ai);
395
+ return precondition(ar, ai);
396
+ };
397
+ const solve = (br, bi, outr, outi, k, sigma) => {
398
+ const rhs = k >= 0 ? precondition(Bt[k].map((v) => -sigma * v), zero) : [new Float64Array(Nc), new Float64Array(Nc)];
399
+ outr.set(rhs[0]);
400
+ outi.set(rhs[1]);
401
+ // (GMRES keeps its best iterate, which is never worse than the factorised solve.)
402
+ const { residual } = gmres(op, rhs, [outr, outi]);
403
+ accuracy = Math.max(accuracy, residual);
404
+ };
405
+ let accuracy = 1e-14; // (relative, of the solves: the readouts' error is weighed with it)
406
+ // y: the response with the floating terminals held; X_k: to a unit δV_k.
407
+ const heldT = dT.kind === 'V';
408
+ for (let j = 0; j < Nc; j++) {
409
+ rr[j] = heldT ? -Bt[kT][j] * scale[j] : 0;
410
+ ri[j] = 0;
411
+ }
412
+ solve(rr, ri, yr, yi, heldT ? kT : -1, heldT ? 1 : 0);
413
+ fl.forEach((k, a) => {
414
+ for (let j = 0; j < Nc; j++) rr[j] = Bt[k][j] * scale[j];
415
+ ri.fill(0);
416
+ solve(rr, ri, Xr[a], Xi[a], k, -1);
417
+ });
418
+ // The floating terminals' circuit rows, a small complex system for their δV.
419
+ const Ar = Array.from({ length: K }, () => new Float64Array(K + 1)), Ai = Array.from({ length: K }, () => new Float64Array(K + 1));
420
+ fl.forEach((k, a) => {
421
+ for (let b = 0; b < K; b++) {
422
+ const [re, im] = dot(k, w, Xr[b], Xi[b]);
423
+ Ar[a][b] = -re;
424
+ Ai[a][b] = -im;
425
+ }
426
+ Ar[a][a] += DI[k];
427
+ Ai[a][a] += w * DIs[k];
428
+ // (another terminal's voltage, where k's current reads rows it reaches: a port's window
429
+ // in a contact's end box)
430
+ fl.forEach((k2, b) => {
431
+ if (b === a) return;
432
+ Ar[a][b] += DX[k][k2];
433
+ Ai[a][b] += w * DXs[k][k2];
434
+ });
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);
438
+ });
439
+ const dV = complexSolve(Ar, Ai, K);
440
+ // δx = y − Σ X_k δV_k
441
+ for (let j = 0; j < Nc; j++) {
442
+ let re = yr[j], im = yi[j];
443
+ for (let a = 0; a < K; a++) {
444
+ re -= Xr[a][j] * dV.re[a] - Xi[a][j] * dV.im[a];
445
+ im -= Xr[a][j] * dV.im[a] + Xi[a][j] * dV.re[a];
446
+ }
447
+ yr[j] = re;
448
+ yi[j] = im;
449
+ xr[this.fullOf[j]] = re;
450
+ xi[this.fullOf[j]] = im;
451
+ }
452
+ // The current into terminal kT per volt, (C + iωC′)·δx + (∂I/∂V)(1 + iω′), with its error.
453
+ // At the contact it's a sum whose terms can be far larger than itself (in a junction's
454
+ // neutral ends, a conductance times a δη that cancels its neighbours' to 1e-17), and its
455
+ // round-off reads as a constant conductance. Between the two contacts of a device without
456
+ // ports, the total current (conduction and displacement) is the same through every cut, so
457
+ // it's also read across each segment, and the reading with the least error taken. Each
458
+ // reading's error is its terms' round-off and the solves' error in δx (to `accuracy` of its
459
+ // largest entry) through its coefficients: a contact's or a cut's flux is a difference of
460
+ // levels that the solved rows don't pin.
461
+ // `parts`: the response as a sum, each part compact (r, i) and full-index (fr, fi), with the
462
+ // absolute error of its entries.
463
+ // `own`: the measured terminal's δV (1 held; driven, its solved one).
464
+ const read = (parts, own = [1, 0]) => {
465
+ const eps = 1.1e-16, c = Ct[kT], cs = Cs[kT], dr = DI[kT], di = w * DIs[kT];
466
+ let Ir = dr * own[0] - di * own[1], Ii = dr * own[1] + di * own[0], err = eps * (Math.abs(dr) + Math.abs(di)) * Math.hypot(...own);
467
+ // (the floating terminals' voltages, where kT's current reads rows they reach)
468
+ fl.forEach((k2, b) => {
469
+ if (k2 === kT) return;
470
+ const xr = DX[kT][k2], xi = w * DXs[kT][k2];
471
+ Ir += xr * dV.re[b] - xi * dV.im[b];
472
+ Ii += xr * dV.im[b] + xi * dV.re[b];
473
+ });
474
+ for (const p of parts) {
475
+ const [re, im] = dot(kT, w, p.r, p.i);
476
+ Ir += re;
477
+ 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);
479
+ }
480
+ // Kirchhoff, where every other terminal floats: what they pass comes back through kT,
481
+ // δI = Σ δV_k/R_k over those behind a resistance (one driven by a current passes no small
482
+ // signal), from their voltages alone. (Read at the contact, a back contact's current with
483
+ // the gate behind 1 kΩ·m² is the difference of two responses equal to 1e-6 of each: its
484
+ // Im Z came out 2.7× off at 1 MHz.)
485
+ if (kirchhoff) {
486
+ let re = 0, im = 0, e = 0;
487
+ fl.forEach((k2, b) => {
488
+ const R = this.terms[k2].drive.R;
489
+ if (k2 === kT || !(R > 0)) return;
490
+ re += dV.re[b] / R;
491
+ im += dV.im[b] / R;
492
+ e += ((eps + accuracy) * Math.hypot(dV.re[b], dV.im[b])) / R;
493
+ });
494
+ if (e < err) [err, Ir, Ii] = [e, re, im];
495
+ }
496
+ if (!cuts) return { Ir, Ii, err };
497
+ const sgn = kT === 0 ? 1 : -1;
498
+ for (const cut of cuts) {
499
+ let re = 0, im = 0, e = 0;
500
+ for (const p of parts) {
501
+ for (let a = 0; a < cut.idx.length; a++) {
502
+ const x = cut.idx[a], gk = cut.g[a], dk = w * cut.d[a];
503
+ // (g + iωd)(xr + i xi)
504
+ re += gk * p.fr[x] - dk * p.fi[x];
505
+ 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);
507
+ }
508
+ }
509
+ if (e < err) [err, Ir, Ii] = [e, sgn * re, sgn * im];
510
+ }
511
+ return { Ir, Ii, err };
512
+ };
513
+ const largest = (r, i) => {
514
+ let m = 0;
515
+ for (let j = 0; j < r.length; j++) m = Math.max(m, Math.hypot(r[j], i[j]));
516
+ return m;
517
+ };
518
+ const full = (r, i) => {
519
+ const fr = new Float64Array(N), fi = new Float64Array(N);
520
+ for (let j = 0; j < Nc; j++) {
521
+ fr[this.fullOf[j]] = r[j];
522
+ fi[this.fullOf[j]] = i[j];
523
+ }
524
+ return [fr, fi];
525
+ };
526
+ let Zr, Zi;
527
+ if (heldT) {
528
+ // δI per volt: (C + iωC′)·δx + (∂I/∂V)(1 + iω′) at the terminal…
529
+ let { Ir, Ii, err } = read([{ r: yr, i: yi, fr: xr, fi: xi, floor: accuracy * largest(yr, yi) }]);
530
+ // …and where the solves leave that unresolved (the estimate past 1% of the current), the
531
+ // response is split: a uniform shift of each region (z_i s on every η_i, s on φ̂), which
532
+ // changes no flux and no concentration, so that J·e vanishes inside each region and is
533
+ // taken at its edges; and what's left, small, solved for to GMRES's floor, its error now
534
+ // relative to itself. An electrolyte at its open circuit needs it: the response to its
535
+ // terminal is nearly such a shift, and its current the slope of levels uniform to 1e-14,
536
+ // more than a double can hold. (A port's exchange sees the shift: its window's rows are
537
+ // kept with the edges'.) So does a MOS capacitor's back contact, its channel held by a port.
538
+ if (K === 0 && cuts && err > 1e-2 * Math.hypot(Ir, Ii)) {
539
+ const [er, ei] = this._regionShift(yr, yi);
540
+ const tr = new Float64Array(Nc), ti = new Float64Array(Nc);
541
+ jv(er, ei, tr, ti);
542
+ const edge = this._regionEdgeRows();
543
+ const ar = new Float64Array(Nc), ai = new Float64Array(Nc);
544
+ for (let j = 0; j < Nc; j++) {
545
+ ar[j] = -Bt[kT][j] - (edge[j] ? tr[j] : 0);
546
+ ai[j] = edge[j] ? -ti[j] : 0;
547
+ }
548
+ const rhs = precondition(ar, ai);
549
+ const dr = yr.map((v, j) => v - er[j]), di = yi.map((v, j) => v - ei[j]);
550
+ const { residual } = gmres(op, rhs, [dr, di], { tol: 1e-11 });
551
+ const [efr, efi] = full(er, ei), [dfr, dfi] = full(dr, di);
552
+ const r2 = read([
553
+ { r: er, i: ei, fr: efr, fi: efi, floor: 0 },
554
+ { r: dr, i: di, fr: dfr, fi: dfi, floor: Math.max(1e-14, residual) * largest(dr, di) },
555
+ ]);
556
+ if (r2.err < err) ({ Ir, Ii, err } = r2);
557
+ if (profiles) {
558
+ for (let j = 0; j < N; j++) {
559
+ xr[j] = efr[j] + dfr[j];
560
+ xi[j] = efi[j] + dfi[j];
561
+ }
562
+ }
563
+ }
564
+ out.relError[q] = err / Math.hypot(Ir, Ii);
565
+ const d2 = Ir * Ir + Ii * Ii; // Z = 1/δI
566
+ Zr = Ir / d2;
567
+ 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);
576
+ }
577
+ out.Z.re[q] = Zr;
578
+ out.Z.im[q] = Zi;
579
+ if (profiles) out.profiles.push(this._smallSignalProfiles(xr, xi));
580
+ });
581
+ return out;
582
+ }
583
+
584
+ // A uniform shift of each region, fitted to a small-signal response (compact, complex): φ̂ by
585
+ // s_r and each species' η by z_i s_r, with s_r the region's mean δφ̂ (or, where φ̂ isn't an
586
+ // unknown, its carriers' mean δη/z). Coverages and flux unknowns aren't shifted.
587
+ _regionShift(yr, yi) {
588
+ const { n, M, z, loc, model } = this, R = this.rix, { grid } = model;
589
+ const er = new Float64Array(yr.length), ei = new Float64Array(yr.length);
590
+ for (let r = 0; r < model.regions.length; r++) {
591
+ let sr = 0, si = 0, k = 0;
592
+ const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
593
+ for (let g = g0; g <= g1; g++) {
594
+ const o = this.blockOfNode[g] * M;
595
+ if (loc[o] >= 0) {
596
+ sr += yr[R[o]];
597
+ si += yi[R[o]];
598
+ k++;
599
+ }
600
+ }
601
+ if (k === 0) {
602
+ for (let g = g0; g <= g1; g++) {
603
+ const o = this.blockOfNode[g] * M;
604
+ for (let i = 0; i < n; i++) {
605
+ if (z[i] === 0 || loc[o + 1 + i] < 0) continue;
606
+ sr += yr[R[o + 1 + i]] / z[i];
607
+ si += yi[R[o + 1 + i]] / z[i];
608
+ k++;
609
+ }
610
+ }
611
+ }
612
+ if (k === 0) continue;
613
+ sr /= k;
614
+ si /= k;
615
+ for (let g = g0; g <= g1; g++) {
616
+ const o = this.blockOfNode[g] * M;
617
+ if (loc[o] >= 0) {
618
+ er[R[o]] = sr;
619
+ ei[R[o]] = si;
620
+ }
621
+ for (let i = 0; i < n; i++) {
622
+ if (loc[o + 1 + i] < 0) continue;
623
+ er[R[o + 1 + i]] = z[i] * sr;
624
+ ei[R[o + 1 + i]] = z[i] * si;
625
+ }
626
+ }
627
+ }
628
+ return [er, ei];
629
+ }
630
+
631
+ // Compact rows a region's uniform shift can touch: faces, the nodes beside them, the two ends
632
+ // and the ports' windows (whatever holds a level from outside). Elsewhere inside a region,
633
+ // every row is unchanged by it.
634
+ _regionEdgeRows() {
635
+ const { M, loc, nB } = this, R = this.rix;
636
+ const edge = new Uint8Array(this.sys.size);
637
+ const mark = (b) => {
638
+ if (b < 0 || b >= nB) return;
639
+ for (let r = 0; r < M; r++) if (loc[b * M + r] >= 0) edge[R[b * M + r]] = 1;
640
+ };
641
+ mark(0);
642
+ mark(nB - 1);
643
+ for (let f = 0; f < this.nFaces; f++) {
644
+ const b = this.blockOfFace[f];
645
+ mark(b - 1);
646
+ mark(b);
647
+ mark(b + 1);
648
+ }
649
+ for (const port of this.model.ports) for (const g of port.nodes) mark(this.blockOfNode[g]);
650
+ return edge;
651
+ }
652
+
653
+ // The cuts where the impedance may read a small-signal current. Each is the linearised total
654
+ // current toward +x through one segment s of a region, Σ (g + iω d)·δu over the unknowns idx:
655
+ // the Scharfetter–Gummel fluxes' derivatives (as in _segmentsDilute, or _segmentConcentrated
656
+ // for non-ideal statistics) and the displacement's. Without ports it's the same through every cut; with them, only between a
657
+ // contact and the nearest port's window (see _impedance).
658
+ _currentCuts() {
659
+ const { model, n, M, u, uLo, c, z } = this, F = FARADAY;
660
+ const { grid, materials, regions } = model;
661
+ const cuts = [];
662
+ for (let r = 0; r < regions.length; r++) {
663
+ const reg = regions[r], mat = materials[reg.material];
664
+ if (mat.conductor || reg.mixing > 0) continue;
665
+ const vel = reg.velocity;
666
+ for (let s = grid.regionStart[r]; s < grid.regionEnd[r]; s++) {
667
+ const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s];
668
+ const terms = new Map();
669
+ const add = (x, g, d) => {
670
+ const t = terms.get(x) || [0, 0];
671
+ t[0] += g;
672
+ t[1] += d;
673
+ terms.set(x, t);
674
+ };
675
+ const phiL = u[bL * M], phiR = u[bR * M], hasPhi = !this.phiUndefined[s];
676
+ for (let i = 0; i < n; i++) {
677
+ if (z[i] === 0 || !mat.present[i] || mat.D[i] === 0) continue;
678
+ if (!mat.ideal) {
679
+ this._fluxConcentrated(s, bL, bR, mat, h, vel, i);
680
+ for (let k = 0; k < M; k++) {
681
+ if (k === 0 && !hasPhi) continue;
682
+ if (this.jL[k] !== 0) add(bL * M + k, z[i] * F * this.jL[k], 0);
683
+ if (this.jR[k] !== 0) add(bR * M + k, z[i] * F * this.jR[k], 0);
684
+ }
685
+ continue;
686
+ }
687
+ const zi = z[i], r1 = 1 + i, cL = c[s * n + i], g = mat.D[i] / h;
688
+ const pe = (vel * h) / mat.D[i];
689
+ const d = zi * (phiR - phiL) - pe;
690
+ const sg = sgFlux(g, d, u[bR * M + r1] - u[bL * M + r1] + (uLo[bR * M + r1] - uLo[bL * M + r1]) - pe, cL, c[(s + 1) * n + i], this.sg);
691
+ const dNdd = sg[1];
692
+ add(bL * M + r1, zi * F * sg[2], 0);
693
+ add(bR * M + r1, -zi * F * sg[3], 0);
694
+ if (hasPhi) {
695
+ add(bL * M, zi * F * (-zi * dNdd - zi * sg[0]), 0);
696
+ add(bR * M, zi * F * zi * dNdd, 0);
697
+ }
698
+ }
699
+ // The displacement toward +x, ε(φ_L − φ_R)/h (in φ̂ = φ/V_T).
700
+ const k = hasPhi ? (mat.epsr * EPS0 * this.VT) / h : 0;
701
+ if (k > 0) {
702
+ add(bL * M, 0, k);
703
+ add(bR * M, 0, -k);
704
+ }
705
+ if (terms.size === 0) continue;
706
+ const idx = [...terms.keys()];
707
+ cuts.push({ s, idx, g: idx.map((x) => terms.get(x)[0]), d: idx.map((x) => terms.get(x)[1]) });
708
+ }
709
+ }
710
+ return cuts;
711
+ }
712
+
713
+ // Complex profiles of δφ (V), δμ̄ (J/mol) and δc (mol/m³) from a small-signal solution.
714
+ _smallSignalProfiles(xr, xi) {
715
+ const { n, M, z, VT, model } = this;
716
+ const nN = this.nNodes, RT = model.RT;
717
+ const pair = () => ({ re: new Float64Array(nN), im: new Float64Array(nN) });
718
+ const phi = pair(), mu = {}, c = {};
719
+ for (const sp of model.species) {
720
+ mu[sp.name] = pair();
721
+ c[sp.name] = pair();
722
+ }
723
+ for (let g = 0; g < nN; g++) {
724
+ const b = this.blockOfNode[g];
725
+ const undef = this.phiUndefined[g];
726
+ phi.re[g] = undef ? NaN : VT * xr[b * M];
727
+ phi.im[g] = undef ? NaN : VT * xi[b * M];
728
+ for (let i = 0; i < n; i++) {
729
+ const name = model.species[i].name;
730
+ if (!this.present[g * n + i]) {
731
+ mu[name].re[g] = mu[name].im[g] = c[name].re[g] = c[name].im[g] = NaN;
732
+ continue;
733
+ }
734
+ mu[name].re[g] = RT * xr[b * M + 1 + i];
735
+ mu[name].im[g] = RT * xi[b * M + 1 + i];
736
+ if (this.nodeConductor[g] >= 0) {
737
+ // a metal has a Fermi level but no concentration (as a solution reports it)
738
+ c[name].re[g] = c[name].im[g] = NaN;
739
+ continue;
740
+ }
741
+ // δc_i = Σ_j K_ij (δη_j − z_j δφ̂)
742
+ let sr = 0, si = 0;
743
+ for (let j = 0; j < n; j++) {
744
+ const Kij = this.nodeIdeal[g] ? (i === j ? this.c[g * n + i] : 0) : this.K[g * n * n + i * n + j];
745
+ if (Kij === 0) continue;
746
+ sr += Kij * (xr[b * M + 1 + j] - z[j] * xr[b * M]);
747
+ si += Kij * (xi[b * M + 1 + j] - z[j] * xi[b * M]);
748
+ }
749
+ c[name].re[g] = sr;
750
+ c[name].im[g] = si;
751
+ }
752
+ }
753
+ return { phi, mu, c };
754
+ }
755
+ }