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