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/solver.js
CHANGED
|
@@ -1,232 +1,28 @@
|
|
|
1
|
-
// Butler–Volmer factor g(a) = e^{αa} − e^{−(1−α)a} = e^{−(1−α)a}·expm1(a) (precise near a = 0),
|
|
2
|
-
// and its derivative g′(a) = α e^{αa} + (1−α) e^{−(1−α)a}.
|
|
3
|
-
function bvFactor(a, alpha) {
|
|
4
|
-
const e = Math.exp(-(1 - alpha) * a);
|
|
5
|
-
// (far from equilibrium, the plain difference: the product would give 0·∞ there)
|
|
6
|
-
const g = Math.abs(a) > 50 ? Math.exp(alpha * a) - e : e * Math.expm1(a);
|
|
7
|
-
return { g, gp: alpha * Math.exp(alpha * a) + (1 - alpha) * e };
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// GMRES(m), restarted, for a real system op(x) = b, op already preconditioned; x holds the
|
|
11
|
-
// starting guess and receives the answer. Returns whether it converged.
|
|
12
|
-
function gmresReal(op, b, x, { m = 10, restarts = 3, tol = 1e-12 } = {}) {
|
|
13
|
-
const N = b.length;
|
|
14
|
-
const dot = (a, c) => {
|
|
15
|
-
let t = 0;
|
|
16
|
-
for (let j = 0; j < N; j++) t += a[j] * c[j];
|
|
17
|
-
return t;
|
|
18
|
-
};
|
|
19
|
-
const bn = Math.sqrt(dot(b, b)) || 1;
|
|
20
|
-
for (let cycle = 0; cycle < restarts; cycle++) {
|
|
21
|
-
const ax = op(x), r = Float64Array.from(b, (v, j) => v - ax[j]);
|
|
22
|
-
const beta = Math.sqrt(dot(r, r));
|
|
23
|
-
if (!(beta > tol * bn)) return true;
|
|
24
|
-
const V = [r.map((v) => v / beta)], H = Array.from({ length: m + 1 }, () => new Float64Array(m));
|
|
25
|
-
const cs = [], sn = [], g = new Float64Array(m + 1);
|
|
26
|
-
g[0] = beta;
|
|
27
|
-
let k = 0, converged = false;
|
|
28
|
-
for (; k < m; k++) {
|
|
29
|
-
const w = op(V[k]);
|
|
30
|
-
for (let i = 0; i <= k; i++) {
|
|
31
|
-
const h = dot(V[i], w);
|
|
32
|
-
H[i][k] = h;
|
|
33
|
-
for (let j = 0; j < N; j++) w[j] -= h * V[i][j];
|
|
34
|
-
}
|
|
35
|
-
const hn = Math.sqrt(dot(w, w));
|
|
36
|
-
V.push(w.map((v) => v / (hn || 1)));
|
|
37
|
-
for (let i = 0; i < k; i++) {
|
|
38
|
-
const a = H[i][k], c = H[i + 1][k];
|
|
39
|
-
H[i][k] = cs[i] * a + sn[i] * c;
|
|
40
|
-
H[i + 1][k] = -sn[i] * a + cs[i] * c;
|
|
41
|
-
}
|
|
42
|
-
const t = Math.hypot(H[k][k], hn), c = t > 0 ? H[k][k] / t : 1, sv = t > 0 ? hn / t : 0;
|
|
43
|
-
cs.push(c);
|
|
44
|
-
sn.push(sv);
|
|
45
|
-
H[k][k] = t;
|
|
46
|
-
g[k + 1] = -sv * g[k];
|
|
47
|
-
g[k] *= c;
|
|
48
|
-
if (!(Math.abs(g[k + 1]) > tol * bn) || !(hn > 0)) {
|
|
49
|
-
k++;
|
|
50
|
-
converged = true;
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
const y = new Float64Array(k);
|
|
55
|
-
for (let i = k - 1; i >= 0; i--) {
|
|
56
|
-
let v = g[i];
|
|
57
|
-
for (let j = i + 1; j < k; j++) v -= H[i][j] * y[j];
|
|
58
|
-
y[i] = v / H[i][i];
|
|
59
|
-
}
|
|
60
|
-
for (let i = 0; i < k; i++) for (let j = 0; j < N; j++) x[j] += y[i] * V[i][j];
|
|
61
|
-
if (converged) return true;
|
|
62
|
-
}
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Solve the K×K complex system (Ar + i Ai) x = (last column), partial pivoting by modulus.
|
|
67
|
-
function complexSolve(Ar, Ai, K) {
|
|
68
|
-
for (let col = 0; col < K; col++) {
|
|
69
|
-
let p = col;
|
|
70
|
-
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;
|
|
71
|
-
[Ar[col], Ar[p]] = [Ar[p], Ar[col]];
|
|
72
|
-
[Ai[col], Ai[p]] = [Ai[p], Ai[col]];
|
|
73
|
-
const dr = Ar[col][col], di = Ai[col][col], d2 = dr * dr + di * di;
|
|
74
|
-
for (let r = col + 1; r < K; r++) {
|
|
75
|
-
const fr = (Ar[r][col] * dr + Ai[r][col] * di) / d2, fi = (Ai[r][col] * dr - Ar[r][col] * di) / d2;
|
|
76
|
-
for (let k = col; k <= K; k++) {
|
|
77
|
-
const ur = Ar[col][k], ui = Ai[col][k];
|
|
78
|
-
Ar[r][k] -= fr * ur - fi * ui;
|
|
79
|
-
Ai[r][k] -= fr * ui + fi * ur;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
const re = new Float64Array(K), im = new Float64Array(K);
|
|
84
|
-
for (let r = K - 1; r >= 0; r--) {
|
|
85
|
-
let vr = Ar[r][K], vi = Ai[r][K];
|
|
86
|
-
for (let k = r + 1; k < K; k++) {
|
|
87
|
-
vr -= Ar[r][k] * re[k] - Ai[r][k] * im[k];
|
|
88
|
-
vi -= Ar[r][k] * im[k] + Ai[r][k] * re[k];
|
|
89
|
-
}
|
|
90
|
-
const dr = Ar[r][r], di = Ai[r][r], d2 = dr * dr + di * di;
|
|
91
|
-
re[r] = (vr * dr + vi * di) / d2;
|
|
92
|
-
im[r] = (vi * dr - vr * di) / d2;
|
|
93
|
-
}
|
|
94
|
-
return { re, im };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// GMRES(m), restarted, for a complex system op(x) = b, with vectors as [re, im] pairs and op
|
|
98
|
-
// already preconditioned; x holds the starting guess and receives the answer. A cycle's own
|
|
99
|
-
// residual estimate can drift far from the true one (a preconditioner nearly singular at low
|
|
100
|
-
// frequency, or an op whose round-off the preconditioner amplifies), so each cycle is judged by
|
|
101
|
-
// the true residual, at the next one's start: it returns once that's below tol, and gives up when
|
|
102
|
-
// two cycles in a row fail to lower it, leaving x at the best iterate. Returns the true residuals (relative
|
|
103
|
-
// to b) at the start and at that iterate.
|
|
104
|
-
function gmres(op, b, x, { m = 12, restarts = 8, tol = 1e-8 } = {}) {
|
|
105
|
-
const N = b[0].length;
|
|
106
|
-
const cdot = (a, c) => {
|
|
107
|
-
let re = 0, im = 0;
|
|
108
|
-
for (let j = 0; j < N; j++) {
|
|
109
|
-
re += a[0][j] * c[0][j] + a[1][j] * c[1][j];
|
|
110
|
-
im += a[0][j] * c[1][j] - a[1][j] * c[0][j];
|
|
111
|
-
}
|
|
112
|
-
return [re, im]; // conj(a)·c
|
|
113
|
-
};
|
|
114
|
-
const norm = (a) => Math.sqrt(cdot(a, a)[0]);
|
|
115
|
-
const bn = norm(b) || 1;
|
|
116
|
-
const best = [Float64Array.from(x[0]), Float64Array.from(x[1])];
|
|
117
|
-
let initial = Infinity, least = Infinity, idle = 0;
|
|
118
|
-
for (let cycle = 0; ; cycle++) {
|
|
119
|
-
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])];
|
|
120
|
-
const beta = norm(r) / bn;
|
|
121
|
-
if (cycle === 0) initial = beta;
|
|
122
|
-
idle = beta < least ? 0 : idle + 1;
|
|
123
|
-
if (idle === 0) {
|
|
124
|
-
least = beta;
|
|
125
|
-
best[0].set(x[0]);
|
|
126
|
-
best[1].set(x[1]);
|
|
127
|
-
}
|
|
128
|
-
if (!(beta > tol) || cycle === restarts || idle === 2) break;
|
|
129
|
-
const V = [[r[0].map((v) => v / (beta * bn)), r[1].map((v) => v / (beta * bn))]];
|
|
130
|
-
const H = Array.from({ length: m + 1 }, () => Array.from({ length: m }, () => [0, 0]));
|
|
131
|
-
const cs = [], sn = [], g = Array.from({ length: m + 1 }, () => [0, 0]);
|
|
132
|
-
g[0] = [beta * bn, 0];
|
|
133
|
-
let k = 0;
|
|
134
|
-
for (; k < m; k++) {
|
|
135
|
-
const w = op(V[k]);
|
|
136
|
-
for (let i = 0; i <= k; i++) {
|
|
137
|
-
const [hr, hi] = cdot(V[i], w);
|
|
138
|
-
H[i][k] = [hr, hi];
|
|
139
|
-
for (let j = 0; j < N; j++) {
|
|
140
|
-
w[0][j] -= hr * V[i][0][j] - hi * V[i][1][j];
|
|
141
|
-
w[1][j] -= hr * V[i][1][j] + hi * V[i][0][j];
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
const hn = norm(w);
|
|
145
|
-
H[k + 1][k] = [hn, 0];
|
|
146
|
-
V.push([w[0].map((v) => v / (hn || 1)), w[1].map((v) => v / (hn || 1))]);
|
|
147
|
-
// The earlier rotations, then a new one zeroing H[k+1][k]: x' = c x + s y, y' = −s̄ x + c y.
|
|
148
|
-
for (let i = 0; i < k; i++) {
|
|
149
|
-
const [xr, xi] = H[i][k], [yr, yi] = H[i + 1][k], c = cs[i], [sr, si] = sn[i];
|
|
150
|
-
H[i][k] = [c * xr + sr * yr - si * yi, c * xi + sr * yi + si * yr];
|
|
151
|
-
H[i + 1][k] = [-(sr * xr + si * xi) + c * yr, -(sr * xi - si * xr) + c * yi];
|
|
152
|
-
}
|
|
153
|
-
const [ar, ai] = H[k][k], am = Math.hypot(ar, ai), t = Math.hypot(am, hn);
|
|
154
|
-
const c = t > 0 ? am / t : 1, s = am > 0 ? [(ar / am) * (hn / t), (ai / am) * (hn / t)] : [1, 0];
|
|
155
|
-
cs.push(c);
|
|
156
|
-
sn.push(s);
|
|
157
|
-
H[k][k] = am > 0 ? [(ar / am) * t, (ai / am) * t] : [hn, 0];
|
|
158
|
-
H[k + 1][k] = [0, 0];
|
|
159
|
-
const [gr, gi] = g[k];
|
|
160
|
-
g[k] = [c * gr, c * gi];
|
|
161
|
-
g[k + 1] = [-(s[0] * gr + s[1] * gi), -(s[0] * gi - s[1] * gr)];
|
|
162
|
-
if (!(Math.hypot(g[k + 1][0], g[k + 1][1]) > tol * bn)) {
|
|
163
|
-
k++;
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
// Back-substitute H y = g over the first k columns, then x += V y.
|
|
168
|
-
const y = Array.from({ length: k }, () => [0, 0]);
|
|
169
|
-
for (let i = k - 1; i >= 0; i--) {
|
|
170
|
-
let [vr, vi] = g[i];
|
|
171
|
-
for (let j = i + 1; j < k; j++) {
|
|
172
|
-
const [hr, hi] = H[i][j], [yr, yi] = y[j];
|
|
173
|
-
vr -= hr * yr - hi * yi;
|
|
174
|
-
vi -= hr * yi + hi * yr;
|
|
175
|
-
}
|
|
176
|
-
const [dr, di] = H[i][i], d2 = dr * dr + di * di;
|
|
177
|
-
y[i] = [(vr * dr + vi * di) / d2, (vi * dr - vr * di) / d2];
|
|
178
|
-
}
|
|
179
|
-
for (let i = 0; i < k; i++) {
|
|
180
|
-
const [yr, yi] = y[i];
|
|
181
|
-
for (let j = 0; j < N; j++) {
|
|
182
|
-
x[0][j] += yr * V[i][0][j] - yi * V[i][1][j];
|
|
183
|
-
x[1][j] += yr * V[i][1][j] + yi * V[i][0][j];
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
x[0].set(best[0]);
|
|
188
|
-
x[1].set(best[1]);
|
|
189
|
-
return { initial, residual: least };
|
|
190
|
-
}
|
|
191
1
|
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
// it has active slots, and the linear system's block sizes vary from node to node. `loc` maps a
|
|
205
|
-
// slot to its row within its block (or −1), and `rix` to its index in the compact vectors (or
|
|
206
|
-
// the sink, one spare entry at the end that absorbs writes for slots that aren't unknowns).
|
|
207
|
-
//
|
|
208
|
-
// Balance rows (node g, box volume v per unit area):
|
|
209
|
-
// φ: D_out − D_in − v·(F Σ z_i c_i + ρ_fixed) = 0
|
|
210
|
-
// i: v·(c_i − c_i,old)/dt + N_out − N_in = 0
|
|
211
|
-
// Fluxes along segments are Scharfetter–Gummel. Each flux is computed once per segment and
|
|
212
|
-
// added with opposite signs to both neighbours, so sums over boxes telescope exactly.
|
|
213
|
-
//
|
|
214
|
-
// Concentrations come from each material's statistics, c(ζ) with ζ_i = η_i − μ°_i/RT − z_i φ̂,
|
|
215
|
-
// and K = ∂c/∂ζ. Nodes of ideal materials (c = c_ref e^ζ) take a fast path throughout.
|
|
216
|
-
|
|
217
|
-
import { BlockTridiagonal, ComplexBlockTridiagonal } from './blockTridiagonal.js';
|
|
218
|
-
import { bernoulli, bernoulliDerivative } from './bernoulli.js';
|
|
219
|
-
import { EPS0, FARADAY } from './constants.js';
|
|
2
|
+
// The numerical core: the Solver's state (laid out in docs/internals.md), its setup, and the
|
|
3
|
+
// drivers around the modules that state their contracts: the equations (src/assemble.js, with
|
|
4
|
+
// src/regions.js and src/boundaries.js, whose methods are the Solver's own), Newton
|
|
5
|
+
// (src/newton.js), time steps (src/timeSteps.js), steady solves (src/steady.js) and the small
|
|
6
|
+
// signal (src/impedance.js), the Solver's own too, the cold start (src/start.js) and the laws
|
|
7
|
+
// (src/laws.js, src/lawRows.js).
|
|
8
|
+
|
|
9
|
+
import { Assembly } from './assemble.js';
|
|
10
|
+
import { BlockTridiagonal } from './blockTridiagonal.js';
|
|
11
|
+
import { bernoulli, SG_FAR, sgFlux } from './bernoulli.js';
|
|
12
|
+
import { BoundaryTerms } from './boundaries.js';
|
|
13
|
+
import { FARADAY } from './constants.js';
|
|
220
14
|
import { sourceAt } from './device.js';
|
|
221
|
-
import { DifferenceTerms } from './difference.js';
|
|
222
15
|
import { DeviceError } from './errors.js';
|
|
223
|
-
import {
|
|
224
|
-
import {
|
|
225
|
-
import {
|
|
226
|
-
import {
|
|
16
|
+
import { conservedMoieties, findLaws, noCurrent } from './laws.js';
|
|
17
|
+
import { newton as newtonFn } from './newton.js';
|
|
18
|
+
import { RegionTerms } from './regions.js';
|
|
19
|
+
import { SteadySolves } from './steady.js';
|
|
20
|
+
import { SmallSignal } from './impedance.js';
|
|
21
|
+
import { advance as advanceFn, integrate as integrateFn, step as stepFn } from './timeSteps.js';
|
|
22
|
+
import { initFromComposition as initFromCompositionFn } from './start.js';
|
|
227
23
|
|
|
228
24
|
// A profile's value at x (c0, kf): piecewise linear, constant beyond its ends.
|
|
229
|
-
function profileAt({ x, values: c }, at) {
|
|
25
|
+
export function profileAt({ x, values: c }, at) {
|
|
230
26
|
if (at <= x[0]) return c[0];
|
|
231
27
|
const last = x.length - 1;
|
|
232
28
|
if (at >= x[last]) return c[last];
|
|
@@ -264,26 +60,6 @@ export class SolverError extends Error {
|
|
|
264
60
|
}
|
|
265
61
|
}
|
|
266
62
|
|
|
267
|
-
// A gate's rate law (see normalizeGates in device.js) at voltage V: [rate, ∂rate/∂V], 1/s and 1/(s·V).
|
|
268
|
-
function gateRate(law, V) {
|
|
269
|
-
const { rate, midpoint, scale } = law;
|
|
270
|
-
if (law.type === 'exp') {
|
|
271
|
-
// (held finite where Newton's iterates stray to absurd voltages: β_m at −13 V would overflow)
|
|
272
|
-
const y = (V - midpoint) / scale, v = rate * Math.exp(Math.min(y, 700));
|
|
273
|
-
return [v, y > 700 ? 0 : v / scale];
|
|
274
|
-
}
|
|
275
|
-
if (law.type === 'sigmoid') {
|
|
276
|
-
const e = Math.exp((midpoint - V) / scale), d = 1 + e;
|
|
277
|
-
return Number.isFinite(e) ? [rate / d, (rate * e) / (scale * d * d)] : [0, 0];
|
|
278
|
-
}
|
|
279
|
-
// expLinear: rate·y/(1 − e^−y), smooth through y = 0 (rate there) and → rate·y for large y
|
|
280
|
-
const y = (V - midpoint) / scale;
|
|
281
|
-
if (Math.abs(y) < 1e-6) return [rate * (1 + y / 2), rate / (2 * scale)];
|
|
282
|
-
if (y < -700) return [0, 0];
|
|
283
|
-
const em = -Math.expm1(-y), ey = Math.exp(-y); // 1 − e^−y, e^−y
|
|
284
|
-
return [(rate * y) / em, (rate * (em - y * ey)) / (em * em * scale)];
|
|
285
|
-
}
|
|
286
|
-
|
|
287
63
|
export class Solver {
|
|
288
64
|
constructor(model) {
|
|
289
65
|
this.model = model;
|
|
@@ -449,6 +225,13 @@ export class Solver {
|
|
|
449
225
|
});
|
|
450
226
|
this._activeSlots();
|
|
451
227
|
this.K = this.anyNonIdeal ? new Float64Array(nNodes * n * n) : null;
|
|
228
|
+
// Crowded nodes under eddy mixing: K's slope too (_statSlopes), for its mobility's Jacobian.
|
|
229
|
+
this.nodeDK = new Uint8Array(nNodes);
|
|
230
|
+
regions.forEach((reg, r) => {
|
|
231
|
+
if (reg.mixing > 0) for (let g = grid.regionStart[r]; g <= grid.regionEnd[r]; g++) if (!this.nodeIdeal[g]) this.nodeDK[g] = 1;
|
|
232
|
+
});
|
|
233
|
+
this.dK = this.nodeDK.some((v) => v) ? new Float64Array(nNodes * n * n * n) : null;
|
|
234
|
+
if (!this.dK) this.nodeDK = null;
|
|
452
235
|
this.ex = new Float64Array(nNodes * n);
|
|
453
236
|
this.zeta = new Float64Array(n);
|
|
454
237
|
this.scratch = new Map(); // per statistics model: ζ, c, K work arrays
|
|
@@ -456,6 +239,7 @@ export class Solver {
|
|
|
456
239
|
this.dB = new Float64Array(M);
|
|
457
240
|
this.jL = new Float64Array(M);
|
|
458
241
|
this.jR = new Float64Array(M);
|
|
242
|
+
this.sg = new Float64Array(5); // a Scharfetter–Gummel flux and its derivatives (sgFlux)
|
|
459
243
|
this.time = 0;
|
|
460
244
|
this.lastDt = Infinity;
|
|
461
245
|
this.atSteady = false; // whether the state is a converged steady solve
|
|
@@ -533,10 +317,12 @@ export class Solver {
|
|
|
533
317
|
nu: Float64Array.from([...rx.reactants.map((p) => p.nu), ...rx.products.map((p) => -p.nu)]),
|
|
534
318
|
}));
|
|
535
319
|
|
|
536
|
-
// Each reacting port's electrode area per volume at its window's nodes
|
|
537
|
-
// each node's box, as for a rate constant
|
|
320
|
+
// Each reacting port's electrode area per volume at its window's nodes: a profile's mean over
|
|
321
|
+
// each node's box, as for a rate constant, with no electrode outside the window (the profile's
|
|
322
|
+
// mean over the part inside, times that part's share of the box, port.fill). And its
|
|
323
|
+
// reactions' rates there, mol/(m²·s).
|
|
538
324
|
this.portArea = model.ports.map((port) =>
|
|
539
|
-
port.area === null ? null : Float64Array.from(port.nodes, (g) => port.area.value ?? profileMean(port.area,
|
|
325
|
+
port.area === null ? null : Float64Array.from(port.nodes, (g, w) => (port.area.value ?? profileMean(port.area, port.lo[w], port.hi[w])) * port.fill[w]),
|
|
540
326
|
);
|
|
541
327
|
this.portRates = model.ports.map((port) => port.reactions.map(() => new Float64Array(port.nodes.length)));
|
|
542
328
|
|
|
@@ -566,7 +352,7 @@ export class Solver {
|
|
|
566
352
|
this.closedEnd = { left: closed('left'), right: closed('right') };
|
|
567
353
|
held[0] = this.closedEnd.left ? 0 : 1;
|
|
568
354
|
held[nNodes - 1] = this.closedEnd.right ? 0 : 1;
|
|
569
|
-
for (const port of model.ports) if (port.species.some((l) => l.type === 'equilibrium'))
|
|
355
|
+
for (const port of model.ports) if (port.species.some((l) => l.type === 'equilibrium')) port.nodes.forEach((g, w) => port.inside[w] && (held[g] = 1));
|
|
570
356
|
this.chargeNode = new Uint8Array(nNodes);
|
|
571
357
|
const neutralFace = (f) => (f === -1 ? this.closedEnd.left : f === model.interfaces.length ? this.closedEnd.right : model.interfaces[f].phi.type === 'neutral');
|
|
572
358
|
regions.forEach((reg, r) => {
|
|
@@ -596,7 +382,8 @@ export class Solver {
|
|
|
596
382
|
this.portHeldEnd = new Uint8Array(2 * n);
|
|
597
383
|
model.ports.forEach((port) => {
|
|
598
384
|
[0, nNodes - 1].forEach((g, k) => {
|
|
599
|
-
|
|
385
|
+
const w = port.nodes.indexOf(g);
|
|
386
|
+
if (w < 0 || !port.inside[w]) return;
|
|
600
387
|
const ct = contacts[k === 0 ? 'left' : 'right'];
|
|
601
388
|
for (let i = 0; i < n; i++) if (port.species[i].type === 'equilibrium' && ct.species[i].type === 'blocked') this.portHeldEnd[k * n + i] = 1;
|
|
602
389
|
});
|
|
@@ -844,13 +631,6 @@ export class Solver {
|
|
|
844
631
|
return { model, n, M, z, nNodes, stretches, stretchOf, flatStretches, rough, surfCols, surfColOf, terms, floating, blockOfNode, blockOfFace, rxsIn, nodeMaterial, phiUndefined };
|
|
845
632
|
}
|
|
846
633
|
|
|
847
|
-
// Every stretch either reaches a contact or is kept by a law, and no law is left unkept: the
|
|
848
|
-
// steady equations can be solved directly (dt = ∞), with the laws in place of their rows. Else
|
|
849
|
-
// the steady solve goes by time steps, which keep every amount themselves.
|
|
850
|
-
_directSteady() {
|
|
851
|
-
return this.unkept.length === 0 && this.stretches.every((st) => st.connected || st.conserved);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
634
|
// An electrode surface at its starting coverages at node g: η = μ°/RT + ln(θ/θ₀).
|
|
855
635
|
_surfaceStart(g) {
|
|
856
636
|
const { n, M, u, model } = this, surf = model.ports[this.surfPort[g]].surface, b = this.blockOfNode[g];
|
|
@@ -890,50 +670,6 @@ export class Solver {
|
|
|
890
670
|
this.history = [];
|
|
891
671
|
}
|
|
892
672
|
|
|
893
|
-
// After a steady solve: each reaction that alone changes some combination nothing else feeds
|
|
894
|
-
// (see src/laws.js), whose total rate a steady state therefore sets to zero, but whose affinity has one
|
|
895
|
-
// sign wherever it acts, so that its total can't be zero. Too slow to show, its rate lost to
|
|
896
|
-
// round-off, it was left where the solve began: a slow complexation 43 mV off, an electrode
|
|
897
|
-
// port's slow couple at its starting composition, each converged without a word. (A face
|
|
898
|
-
// reaction stands on its affinity, so it's only caught here if that couldn't be arranged.)
|
|
899
|
-
stuckReactions() {
|
|
900
|
-
const { n, M, u, uLo, model } = this, grid = model.grid, S = this.stretches.length, X = S + this.surfCols.length, out = [];
|
|
901
|
-
const passes = (k) => !noCurrent(this.terms[k]);
|
|
902
|
-
const fed = (w) => w.some((x, k) => x !== 0 && ((k >= X && passes(this.carriers[k - X])) || (k < S && this.terms.some((t, j) => t.drive.kind === 'I' && passes(j) && feeds(j, this.stretches[k])))));
|
|
903
|
-
const needed = (rx) => {
|
|
904
|
-
const basis = conservedMoieties(this._structure(), { drives: true, skip: rx });
|
|
905
|
-
return basis.some((w) => !fed(w) && basis.skipped.some((row) => Math.abs(row.reduce((t, x, k) => t + x * w[k], 0)) > 1e-9));
|
|
906
|
-
};
|
|
907
|
-
const eta = (g, slot) => u[this.blockOfNode[g] * M + slot] + uLo[this.blockOfNode[g] * M + slot];
|
|
908
|
-
const report = (where, as) => {
|
|
909
|
-
if (as.length === 0 || !(as.every((a) => a > 1e-6) || as.every((a) => a < -1e-6))) return;
|
|
910
|
-
out.push({ where, affinity: as.reduce((m, a) => (Math.abs(a) < Math.abs(m) ? a : m)) });
|
|
911
|
-
};
|
|
912
|
-
model.interfaces.forEach((itf, f) => {
|
|
913
|
-
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1];
|
|
914
|
-
itf.reactions.forEach((rx, q) => {
|
|
915
|
-
if (this.affinities.some((af) => af.rx === rx) || !needed(rx)) return;
|
|
916
|
-
report(`interfaces[${f}].reactions[${q}]`, [rx.part.reduce((a, p) => a - p.nu * eta(p.side ? gR : gL, 1 + p.i), rx.fixedA)]);
|
|
917
|
-
});
|
|
918
|
-
});
|
|
919
|
-
model.reactions.forEach((mrx, x) => {
|
|
920
|
-
const rx = this.rxs[x];
|
|
921
|
-
if (rx.generation || !needed(mrx)) return;
|
|
922
|
-
const as = [];
|
|
923
|
-
for (let g = 0; g < this.nNodes; g++) if (rx.kfNode[g] > 0 || rx.srh?.[this.nodeMaterial[g]]) as.push(rx.sp.reduce((a, i, p) => a + rx.nu[p] * eta(g, 1 + i), rx.fixedA));
|
|
924
|
-
report(`bulkReactions[${x}]`, as);
|
|
925
|
-
});
|
|
926
|
-
model.ports.forEach((port, k) => {
|
|
927
|
-
port.reactions.forEach((rx, q) => {
|
|
928
|
-
if (!needed(rx)) return;
|
|
929
|
-
let aV = rx.fixedA;
|
|
930
|
-
for (const p of rx.part) if (p.side === 1) aV -= p.nu * this.portEta(port, p.i);
|
|
931
|
-
report(`ports[${k}].reactions[${q}]`, port.nodes.map((g) => rx.part.reduce((a, p) => (p.side === 1 ? a : a - p.nu * eta(g, p.side === 2 ? 1 + n + p.s : 1 + p.i)), aV)));
|
|
932
|
-
});
|
|
933
|
-
});
|
|
934
|
-
return out;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
673
|
// How each terminal is driven, as far as the laws care (see src/laws.js): by a voltage or a current,
|
|
938
674
|
// behind a resistance or not, and a current that's held at zero throughout (which feeds a
|
|
939
675
|
// combination nothing, so a reaction's affinity may stand in for its summed balance).
|
|
@@ -965,2336 +701,137 @@ export class Solver {
|
|
|
965
701
|
return z === 0 ? link.mu / this.model.RT : (z * (this.termV[side === 'left' ? 0 : 1] + link.offset)) / this.VT;
|
|
966
702
|
}
|
|
967
703
|
|
|
968
|
-
|
|
969
|
-
* Cold start. Each species takes its region's c0 where one is given (for a spectator, it fixes
|
|
970
|
-
* the conserved amount), else its contact's level. φ in each region is then chosen for local neutrality (node by node where a
|
|
971
|
-
* profile varies), or continued across the interface dipole if nothing there responds.
|
|
972
|
-
*/
|
|
704
|
+
// The cold start (src/start.js).
|
|
973
705
|
initFromComposition() {
|
|
974
|
-
|
|
975
|
-
const { regions, species, interfaces, materials } = model;
|
|
976
|
-
const grid = model.grid;
|
|
977
|
-
u.fill(0);
|
|
978
|
-
this.uLo.fill(0);
|
|
979
|
-
const eta = new Float64Array(n), cFix = new Float64Array(n), mode = new Int8Array(n); // 1 level, 2 amount
|
|
980
|
-
// φ̂ runs from the left: from a pinned (or gated) left contact's φ at its zero charge, else 0.
|
|
981
|
-
const leftPhi = model.contacts.left.phi;
|
|
982
|
-
let phiHat = leftPhi.type === 'pinned' || leftPhi.type === 'capacitive' ? (this.termV[0] - leftPhi.zeroCharge) / this.VT : 0;
|
|
983
|
-
for (let r = 0; r < regions.length; r++) {
|
|
984
|
-
if (r > 0) phiHat += interfaces[r - 1].dipole / this.VT;
|
|
985
|
-
const reg = regions[r], mat = materials[reg.material];
|
|
986
|
-
mode.fill(0);
|
|
987
|
-
const profiled = reg.c0Profile.some((p) => p !== null);
|
|
988
|
-
for (let i = 0; i < n; i++) {
|
|
989
|
-
if (!mat.present[i]) continue;
|
|
990
|
-
const st = this.stretches[this.stretchOf[r * n + i]];
|
|
991
|
-
if (reg.c0Profile[i] || reg.c0[i] > 0) {
|
|
992
|
-
// A given c0 is the starting state, even where a contact feeds the species.
|
|
993
|
-
mode[i] = 2;
|
|
994
|
-
cFix[i] = reg.c0Profile[i] ? profileAt(reg.c0Profile[i], grid.x[grid.regionStart[r]]) : reg.c0[i];
|
|
995
|
-
} else if (!st.contactFed && mat.conductor) {
|
|
996
|
-
// A conductor away from the contacts: start uncharged, with its carrier's level in
|
|
997
|
-
// equilibrium with the first reaction on its left face that takes it, else at the
|
|
998
|
-
// running φ.
|
|
999
|
-
eta[i] = z[i] * phiHat;
|
|
1000
|
-
const rx = r > 0 ? interfaces[r - 1].reactions.find((x) => x.part.some((p) => p.side === 1 && p.i === i)) : undefined;
|
|
1001
|
-
if (rx) {
|
|
1002
|
-
const bn = this.blockOfNode[grid.regionEnd[r - 1]];
|
|
1003
|
-
let a = rx.fixedA, nu = 0; // a = fixedA − Σ ν η = 0, solved for the carrier's η
|
|
1004
|
-
for (const p of rx.part) {
|
|
1005
|
-
if (p.side === 1) nu += p.nu;
|
|
1006
|
-
else a -= p.nu * u[bn * M + 1 + p.i];
|
|
1007
|
-
}
|
|
1008
|
-
eta[i] = a / nu;
|
|
1009
|
-
}
|
|
1010
|
-
mode[i] = 1;
|
|
1011
|
-
} else if (!st.contactFed) {
|
|
1012
|
-
if (!(reg.c0[i] > 0)) {
|
|
1013
|
-
throw new SolverError(
|
|
1014
|
-
`regions[${r}].c0.${species[i].name}: a species that doesn't reach a contact needs its initial concentration` +
|
|
1015
|
-
(st.connected ? ' (reactions make and consume it, so this is only where the solve starts, not an amount it keeps: a tiny value will do)' : ', which fixes the amount it conserves'),
|
|
1016
|
-
);
|
|
1017
|
-
}
|
|
1018
|
-
mode[i] = 2;
|
|
1019
|
-
cFix[i] = reg.c0[i];
|
|
1020
|
-
} else {
|
|
1021
|
-
const left = st.regions[0] === 0 ? this.contactEta('left', i) : NaN;
|
|
1022
|
-
eta[i] = Number.isFinite(left) ? left : this.contactEta('right', i);
|
|
1023
|
-
if (!Number.isFinite(eta[i]) && st.linked.length > 0) eta[i] = this.portEta(model.ports[st.linked[0]], i);
|
|
1024
|
-
mode[i] = 1;
|
|
1025
|
-
if (!Number.isFinite(eta[i])) {
|
|
1026
|
-
// Fed only through reactions (no level held at a contact): start from c0.
|
|
1027
|
-
if (!(reg.c0[i] > 0)) {
|
|
1028
|
-
throw new SolverError(
|
|
1029
|
-
`regions[${r}].c0.${species[i].name}: a species fed only through reactions needs its initial concentration`,
|
|
1030
|
-
);
|
|
1031
|
-
}
|
|
1032
|
-
mode[i] = 2;
|
|
1033
|
-
cFix[i] = reg.c0[i];
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
// Net charge (mol/m³) at trial φ̂; decreasing in φ̂ wherever a level-fixed ion responds.
|
|
1038
|
-
const zeta = new Float64Array(n), cc = new Float64Array(n);
|
|
1039
|
-
const charge = (ph) => {
|
|
1040
|
-
if (!mat.ideal) {
|
|
1041
|
-
this._materialAt(mat, reg.background, eta, mode, cFix, ph, zeta, cc);
|
|
1042
|
-
let q = reg.fixedCharge / FARADAY;
|
|
1043
|
-
for (let i = 0; i < n; i++) if (mat.present[i]) q += z[i] * cc[i];
|
|
1044
|
-
return q;
|
|
1045
|
-
}
|
|
1046
|
-
let q = reg.fixedCharge / FARADAY;
|
|
1047
|
-
for (let i = 0; i < n; i++) {
|
|
1048
|
-
if (mode[i] === 2) q += z[i] * cFix[i];
|
|
1049
|
-
else if (mode[i] === 1 && z[i] !== 0) {
|
|
1050
|
-
const ex = Math.min(700, Math.max(-700, eta[i] - mat.mu0[i] / model.RT - z[i] * ph));
|
|
1051
|
-
q += z[i] * mat.cRef[i] * Math.exp(ex);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
return q;
|
|
1055
|
-
};
|
|
1056
|
-
const responds = mode.some((m, i) => m === 1 && z[i] !== 0);
|
|
1057
|
-
const mustBalance = !responds && !mat.conductor && !mat.phiFree && mat.epsr === 0;
|
|
1058
|
-
const checkNeutral = (where) => {
|
|
1059
|
-
// Strictly neutral, with every charged species' amount given: its c0 must be neutral.
|
|
1060
|
-
let q = reg.fixedCharge / FARADAY, scale = Math.abs(q);
|
|
1061
|
-
for (let i = 0; i < n; i++) {
|
|
1062
|
-
if (mode[i] !== 2 || z[i] === 0) continue;
|
|
1063
|
-
q += z[i] * cFix[i];
|
|
1064
|
-
scale += Math.abs(z[i] * cFix[i]);
|
|
1065
|
-
}
|
|
1066
|
-
if (Math.abs(q) > 1e-9 * scale) {
|
|
1067
|
-
throw new SolverError(
|
|
1068
|
-
`regions[${r}] (${reg.name}): its initial composition carries a net charge of ${q.toPrecision(3)} mol/m³${where} (Σ z·c0, with any fixed charge), ` +
|
|
1069
|
-
'but ε = 0 makes it strictly neutral; adjust c0 so that it balances',
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1072
|
-
};
|
|
1073
|
-
if (mustBalance && !profiled) checkNeutral('');
|
|
1074
|
-
if (!mat.phiFree && !responds && mat.ideal && r > 0 && materials[regions[r - 1].material].conductor) {
|
|
1075
|
-
// Nothing here fixes φ, but an electrode on the left does: start with its first reaction
|
|
1076
|
-
// that takes a species from this side at equilibrium (the electrode at its open-circuit
|
|
1077
|
-
// level), rather than φ carried over from the metal, which can be volts away.
|
|
1078
|
-
const bn = this.blockOfNode[grid.regionEnd[r - 1]];
|
|
1079
|
-
for (const rx of interfaces[r - 1].reactions) {
|
|
1080
|
-
let a = rx.fixedA, s = 0;
|
|
1081
|
-
for (const p of rx.part) {
|
|
1082
|
-
if (p.side === 0) a -= p.nu * u[bn * M + 1 + p.i];
|
|
1083
|
-
else if (mode[p.i] === 2) {
|
|
1084
|
-
a -= p.nu * (Math.log(cFix[p.i] / mat.cRef[p.i]) + mat.mu0[p.i] / model.RT);
|
|
1085
|
-
s += p.nu * z[p.i];
|
|
1086
|
-
} else s = NaN;
|
|
1087
|
-
}
|
|
1088
|
-
if (s !== 0 && Number.isFinite(s)) {
|
|
1089
|
-
phiHat = a / s; // a − s·φ̂ = 0
|
|
1090
|
-
break;
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
const electrode = model.ports.find((port) => port.region === r && port.reactions.length > 0);
|
|
1095
|
-
if (!mat.phiFree && !responds && mat.ideal && electrode) {
|
|
1096
|
-
// Nor here, but an electrode spread through it does: start with the port's first reaction
|
|
1097
|
-
// that can be balanced at equilibrium with the port's level (the electrode at open circuit).
|
|
1098
|
-
const bare = 1 - electrode.surface.reduce((t, sp) => t + sp.theta0, 0);
|
|
1099
|
-
for (const rx of electrode.reactions) {
|
|
1100
|
-
let a = rx.fixedA, s = 0;
|
|
1101
|
-
for (const p of rx.part) {
|
|
1102
|
-
if (p.side === 1) a -= p.nu * this.portEta(electrode, p.i);
|
|
1103
|
-
else if (p.side === 2) {
|
|
1104
|
-
const sp = electrode.surface[p.s]; // (at its starting coverage)
|
|
1105
|
-
a -= p.nu * (sp.mu0 / model.RT + Math.log(sp.theta0 / bare));
|
|
1106
|
-
} else if (mode[p.i] === 2) {
|
|
1107
|
-
a -= p.nu * (Math.log(cFix[p.i] / mat.cRef[p.i]) + mat.mu0[p.i] / model.RT);
|
|
1108
|
-
s += p.nu * z[p.i];
|
|
1109
|
-
} else s = NaN;
|
|
1110
|
-
}
|
|
1111
|
-
if (s !== 0 && Number.isFinite(s)) {
|
|
1112
|
-
phiHat = a / s;
|
|
1113
|
-
break;
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
const neutralPhi = (ph) => {
|
|
1118
|
-
let lo = ph - 1, hi = ph + 1;
|
|
1119
|
-
while (charge(lo) < 0 && lo > -1e4) lo -= 2 * (hi - lo);
|
|
1120
|
-
while (charge(hi) > 0 && hi < 1e4) hi += 2 * (hi - lo);
|
|
1121
|
-
// Out of reach (every responding carrier has one sign and nothing balances it, as in an
|
|
1122
|
-
// undoped layer that holds only holes): keep the running φ̂ rather than run it to ±∞.
|
|
1123
|
-
if (charge(lo) < 0 || charge(hi) > 0) return ph;
|
|
1124
|
-
for (let it = 0; it < 200 && hi - lo > 1e-12; it++) {
|
|
1125
|
-
const m = 0.5 * (lo + hi);
|
|
1126
|
-
if (charge(m) > 0) lo = m;
|
|
1127
|
-
else hi = m;
|
|
1128
|
-
}
|
|
1129
|
-
return 0.5 * (lo + hi);
|
|
1130
|
-
};
|
|
1131
|
-
if (!mat.phiFree && responds) phiHat = neutralPhi(phiHat);
|
|
1132
|
-
if (!mat.ideal) this._materialAt(mat, reg.background, eta, mode, cFix, phiHat, zeta, cc);
|
|
1133
|
-
for (let g = grid.regionStart[r]; g <= grid.regionEnd[r]; g++) {
|
|
1134
|
-
if (profiled) {
|
|
1135
|
-
for (let i = 0; i < n; i++) if (reg.c0Profile[i]) cFix[i] = profileAt(reg.c0Profile[i], grid.x[g]);
|
|
1136
|
-
if (mustBalance) checkNeutral(` at x = ${grid.x[g].toPrecision(4)} m`);
|
|
1137
|
-
if (!mat.phiFree && responds) phiHat = neutralPhi(phiHat);
|
|
1138
|
-
if (!mat.ideal) this._materialAt(mat, reg.background, eta, mode, cFix, phiHat, zeta, cc);
|
|
1139
|
-
}
|
|
1140
|
-
const b = this.blockOfNode[g];
|
|
1141
|
-
u[b * M] = mat.conductor ? 0 : phiHat; // a metal's slot 0 is its segment flux
|
|
1142
|
-
for (let i = 0; i < n; i++) {
|
|
1143
|
-
if (mode[i] === 1) u[b * M + 1 + i] = eta[i];
|
|
1144
|
-
else if (mode[i] === 2) {
|
|
1145
|
-
const zt = mat.ideal ? Math.log(cFix[i] / mat.cRef[i]) : zeta[i];
|
|
1146
|
-
u[b * M + 1 + i] = zt + mat.mu0[i] / model.RT + z[i] * phiHat;
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
// Two strictly neutral regions meeting at a face whose links hold ions in equilibrium: where
|
|
1152
|
-
// c0 leaves the two sides out of it (a different fixed charge or immobile ion on each, so
|
|
1153
|
-
// c₊c₋ differs across it), the face reaches it at t = 0⁺, before anything diffuses, and a
|
|
1154
|
-
// first step made to take that jump met an exactly singular pivot. Start there: the ions the
|
|
1155
|
-
// face passes shared between its two nodes in equilibrium (μ̄ equal across it), each one's
|
|
1156
|
-
// amount over the two nodes kept, everything else as c0 has it. The one unknown is the jump
|
|
1157
|
-
// in φ̂ across it (a Donnan potential), set by neutrality on one side; the other's follows,
|
|
1158
|
-
// the two nodes' charge summing to what it was, zero.
|
|
1159
|
-
for (let f = 0; f < interfaces.length; f++) {
|
|
1160
|
-
const itf = interfaces[f], mL = materials[regions[f].material], mR = materials[regions[f + 1].material];
|
|
1161
|
-
const strict = (m) => m.epsr === 0 && !m.conductor && !m.phiFree && m.ideal;
|
|
1162
|
-
if (itf.phi.type !== 'neutral' || !strict(mL) || !strict(mR)) continue;
|
|
1163
|
-
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1], bL = this.blockOfNode[gL] * M, bR = this.blockOfNode[gR] * M;
|
|
1164
|
-
const pass = [];
|
|
1165
|
-
for (let i = 0; i < n; i++) if (itf.links[i].type === 'equilibrium' && mL.present[i] && mR.present[i]) pass.push(i);
|
|
1166
|
-
if (!pass.some((i) => z[i] !== 0)) continue;
|
|
1167
|
-
const RT = model.RT, vL = grid.vol[gL], vR = grid.vol[gR];
|
|
1168
|
-
const conc = (m, b, i) => m.cRef[i] * Math.exp(u[b + 1 + i] - m.mu0[i] / RT - z[i] * u[b]);
|
|
1169
|
-
const amount = pass.map((i) => vL * conc(mL, bL, i) + vR * conc(mR, bR, i));
|
|
1170
|
-
let qOther = regions[f].fixedCharge / FARADAY; // (the left node's charge from all it doesn't share)
|
|
1171
|
-
for (let i = 0; i < n; i++) if (mL.present[i] && !pass.includes(i) && z[i] !== 0) qOther += z[i] * conc(mL, bL, i);
|
|
1172
|
-
// At a jump D = φ̂_R − φ̂_L: c_L/c_R = (cRef_L/cRef_R)·exp((μ°_R − μ°_L)/RT + z D), the amount fixing both.
|
|
1173
|
-
const split = (D) =>
|
|
1174
|
-
pass.map((i, j) => {
|
|
1175
|
-
const K = (mL.cRef[i] / mR.cRef[i]) * Math.exp((mR.mu0[i] - mL.mu0[i]) / RT + z[i] * D), cR = amount[j] / (vL * K + vR);
|
|
1176
|
-
return [K * cR, cR];
|
|
1177
|
-
});
|
|
1178
|
-
const qL = (D) => split(D).reduce((q, [cL], j) => q + z[pass[j]] * cL, qOther); // (increasing in D)
|
|
1179
|
-
const D0 = u[bR] - u[bL];
|
|
1180
|
-
let lo = D0 - 1, hi = D0 + 1;
|
|
1181
|
-
while (qL(lo) > 0 && lo > D0 - 1e3) lo -= 2 * (hi - lo);
|
|
1182
|
-
while (qL(hi) < 0 && hi < D0 + 1e3) hi += 2 * (hi - lo);
|
|
1183
|
-
if (!(qL(lo) <= 0 && qL(hi) >= 0)) continue; // (nothing the face shares can balance it: leave c0)
|
|
1184
|
-
for (let it = 0; it < 200 && hi - lo > 1e-13 * Math.max(1, Math.abs(D0)); it++) {
|
|
1185
|
-
const m = 0.5 * (lo + hi);
|
|
1186
|
-
if (qL(m) < 0) lo = m;
|
|
1187
|
-
else hi = m;
|
|
1188
|
-
}
|
|
1189
|
-
const D = 0.5 * (lo + hi), cs = split(D);
|
|
1190
|
-
const keep = Float64Array.from({ length: n }, (_, i) => (mR.present[i] ? conc(mR, bR, i) : 0)); // (the right node's, before φ̂ moves)
|
|
1191
|
-
u[bR] = u[bL] + D;
|
|
1192
|
-
for (let i = 0; i < n; i++) if (mR.present[i] && !pass.includes(i)) u[bR + 1 + i] = Math.log(keep[i] / mR.cRef[i]) + mR.mu0[i] / RT + z[i] * u[bR];
|
|
1193
|
-
pass.forEach((i, j) => {
|
|
1194
|
-
u[bL + 1 + i] = Math.log(cs[j][0] / mL.cRef[i]) + mL.mu0[i] / RT + z[i] * u[bL];
|
|
1195
|
-
u[bR + 1 + i] = Math.log(cs[j][1] / mR.cRef[i]) + mR.mu0[i] / RT + z[i] * u[bR];
|
|
1196
|
-
});
|
|
1197
|
-
}
|
|
1198
|
-
// An insertion host's φ is a gauge: c0 sets only its neutral combination's level, not how that
|
|
1199
|
-
// splits between its ion and its electrons. Start its ion level with the same ion's beside it
|
|
1200
|
-
// across a face (the equilibrium a link or a transfer reaction heads for), so its electrons,
|
|
1201
|
-
// and a floating contact on them, start where the cell's open-circuit voltage puts them, not
|
|
1202
|
-
// volts away. (A shift of φ̂ and each η by z·Δ: the concentrations don't change.)
|
|
1203
|
-
regions.forEach((reg, r) => {
|
|
1204
|
-
const mat = materials[reg.material];
|
|
1205
|
-
if (!mat.phiFree) return;
|
|
1206
|
-
const sides = [[r - 1, grid.regionStart[r], r > 0 ? grid.regionEnd[r - 1] : -1], [r + 1, grid.regionEnd[r], r + 1 < regions.length ? grid.regionStart[r + 1] : -1]];
|
|
1207
|
-
for (const [nr, gIn, gOut] of sides) {
|
|
1208
|
-
if (gOut < 0) continue;
|
|
1209
|
-
const other = materials[regions[nr].material];
|
|
1210
|
-
if (other.phiFree || other.conductor) continue;
|
|
1211
|
-
const i = z.findIndex((zi, j) => zi !== 0 && mat.present[j] && other.present[j]);
|
|
1212
|
-
if (i < 0) continue;
|
|
1213
|
-
const d = (u[this.blockOfNode[gOut] * M + 1 + i] - u[this.blockOfNode[gIn] * M + 1 + i]) / z[i];
|
|
1214
|
-
if (!Number.isFinite(d)) continue;
|
|
1215
|
-
for (let g = grid.regionStart[r]; g <= grid.regionEnd[r]; g++) {
|
|
1216
|
-
const b = this.blockOfNode[g];
|
|
1217
|
-
u[b * M] += d;
|
|
1218
|
-
for (let j = 0; j < n; j++) if (mat.present[j]) u[b * M + 1 + j] += z[j] * d;
|
|
1219
|
-
}
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
});
|
|
1223
|
-
// A floating contact facing a held one starts where the start's own composition puts it: the
|
|
1224
|
-
// held voltage plus the difference between the two terminal species' levels beside each
|
|
1225
|
-
// contact (c0 sets them, and φ was chosen for neutrality, not to match either contact), so
|
|
1226
|
-
// that a transient's first reading means something.
|
|
1227
|
-
const level = (side) => {
|
|
1228
|
-
const ct = model.contacts[side], i = ct.terminal;
|
|
1229
|
-
if (i === null || ct.species[i].type !== 'equilibrium' || z[i] === 0) return NaN;
|
|
1230
|
-
const b = this.blockOfNode[side === 'left' ? 0 : grid.nNodes - 1];
|
|
1231
|
-
return (this.VT * u[b * M + 1 + i]) / z[i] - ct.species[i].offset;
|
|
1232
|
-
};
|
|
1233
|
-
for (const k of this.floating) {
|
|
1234
|
-
const t = this.terms[k], held = k === 0 ? 1 : 0;
|
|
1235
|
-
if (t.kind === 'port' || held >= this.terms.length || this.floating.includes(held)) continue;
|
|
1236
|
-
const shift = level(t.side) - level(this.terms[held].side);
|
|
1237
|
-
if (Number.isFinite(shift)) this.termV[k] = this.termV[held] + shift;
|
|
1238
|
-
}
|
|
1239
|
-
for (let g = 0; g < this.nNodes; g++) if (this.surfPort[g] >= 0) this._surfaceStart(g);
|
|
1240
|
-
this.computeConcentrations();
|
|
1241
|
-
// A floating capacitance starts uncharged (σ = 0 at the window's mean φ), as the starting
|
|
1242
|
-
// composition, neutral without it, assumes; with reactions too, its double layer then charges
|
|
1243
|
-
// toward their mixed potential.
|
|
1244
|
-
for (const k of this.floating) {
|
|
1245
|
-
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1246
|
-
const gate = t.kind === 'contact' ? model.contacts[t.side].phi : null;
|
|
1247
|
-
if (gate?.type === 'capacitive') {
|
|
1248
|
-
// A gate contact likewise: at its zero-charge level from the φ beside it (a pinned
|
|
1249
|
-
// one's charge is the field's, already zero against a neutral start).
|
|
1250
|
-
const g = t.side === 'left' ? 0 : this.nNodes - 1;
|
|
1251
|
-
if (!this.phiUndefined[g]) this.termV[k] = gate.zeroCharge + this.VT * u[this.blockOfNode[g] * M];
|
|
1252
|
-
continue;
|
|
1253
|
-
}
|
|
1254
|
-
if (!port?.capacitance) continue;
|
|
1255
|
-
let w = 0, sum = 0;
|
|
1256
|
-
port.nodes.forEach((g, j) => {
|
|
1257
|
-
if (this.phiUndefined[g]) return;
|
|
1258
|
-
const a = this.model.grid.vol[g] * this.portArea[t.index][j];
|
|
1259
|
-
w += a;
|
|
1260
|
-
sum += a * this.VT * u[this.blockOfNode[g] * M];
|
|
1261
|
-
});
|
|
1262
|
-
if (w > 0) this.termV[k] = port.capacitance.zeroCharge + sum / w;
|
|
1263
|
-
}
|
|
1264
|
-
// A floating port linked to its carrier (no reactions, no double layer: a wire into an
|
|
1265
|
-
// insertion host) starts at that carrier's level across its window, not at 0 V.
|
|
1266
|
-
for (const k of this.floating) {
|
|
1267
|
-
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1268
|
-
const i = port?.terminal, link = i === null || i === undefined ? null : port.species[i];
|
|
1269
|
-
if (!port || port.reactions.length > 0 || port.capacitance || !link || link.type === 'blocked' || z[i] === 0) continue;
|
|
1270
|
-
let w = 0, sum = 0;
|
|
1271
|
-
port.nodes.forEach((g, j) => {
|
|
1272
|
-
const a = this.model.grid.vol[g] * (this.portArea[t.index]?.[j] ?? 1); // (a window with no area: by volume)
|
|
1273
|
-
w += a;
|
|
1274
|
-
sum += (a * this.VT * u[this.blockOfNode[g] * M + 1 + i]) / z[i];
|
|
1275
|
-
});
|
|
1276
|
-
// (Levels at the start mean something only against a held terminal's, as for a contact.)
|
|
1277
|
-
const held = [0, 1].find((h) => !this.floating.includes(h) && Number.isFinite(level(this.terms[h].side)));
|
|
1278
|
-
if (w > 0 && held !== undefined) this.termV[k] = this.termV[held] + sum / w - (link.offset ?? 0) - level(this.terms[held].side);
|
|
1279
|
-
}
|
|
1280
|
-
// Gates start open as far as the starting voltage across their face holds them, α/(α + β).
|
|
1281
|
-
for (const gt of this.gateList) {
|
|
1282
|
-
const { gate, o } = gt, V = this._gateVoltage(gt), a = gateRate(gate.alpha, V)[0], b = gateRate(gate.beta, V)[0];
|
|
1283
|
-
u[o] = a + b > 0 ? a / (a + b) : 0;
|
|
1284
|
-
}
|
|
1285
|
-
// A floating electrode spread through a port (without a double layer) starts where its
|
|
1286
|
-
// reactions pass the current it's set (none, behind a resistance): at its mixed potential in
|
|
1287
|
-
// the start's composition, not level with a held terminal, which can be volts away and pass
|
|
1288
|
-
// an absurd current.
|
|
1289
|
-
for (const k of this.floating) {
|
|
1290
|
-
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1291
|
-
if (!port || port.reactions.length === 0 || port.capacitance) continue;
|
|
1292
|
-
const want = t.drive.kind === 'I' ? sourceAt(t.drive.src, this.time) : 0;
|
|
1293
|
-
const current = (V) => {
|
|
1294
|
-
this.termV[k] = V;
|
|
1295
|
-
let I = 0;
|
|
1296
|
-
port.reactions.forEach((rx) => {
|
|
1297
|
-
let q = 0;
|
|
1298
|
-
for (const p of rx.part) if (p.side === 0) q += p.nu * z[p.i];
|
|
1299
|
-
port.nodes.forEach((g, w) => (I += q * FARADAY * this.model.grid.vol[g] * this.portArea[t.index][w] * this._portRate(port, rx, g)));
|
|
1300
|
-
});
|
|
1301
|
-
return I;
|
|
1302
|
-
};
|
|
1303
|
-
// Current into the device rises with V (oxidation); bracket, then bisect.
|
|
1304
|
-
let lo = this.termV[k] - 1, hi = this.termV[k] + 1;
|
|
1305
|
-
while (current(lo) > want && lo > -1e3) lo -= 2 * (hi - lo);
|
|
1306
|
-
while (current(hi) < want && hi < 1e3) hi += 2 * (hi - lo);
|
|
1307
|
-
for (let it = 0; it < 200 && hi - lo > 1e-12; it++) {
|
|
1308
|
-
const m = 0.5 * (lo + hi);
|
|
1309
|
-
if (current(m) < want) lo = m;
|
|
1310
|
-
else hi = m;
|
|
1311
|
-
}
|
|
1312
|
-
this.termV[k] = 0.5 * (lo + hi);
|
|
1313
|
-
}
|
|
706
|
+
return initFromCompositionFn(this);
|
|
1314
707
|
}
|
|
1315
708
|
|
|
1316
|
-
//
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
let pref = rx.k0, a = rx.fixedA;
|
|
1320
|
-
for (const p of rx.part) {
|
|
1321
|
-
if (p.side === 1) {
|
|
1322
|
-
a -= p.nu * this.portEta(port, p.i);
|
|
1323
|
-
continue;
|
|
1324
|
-
}
|
|
1325
|
-
const o = b * M + (p.side === 2 ? 1 + n + p.s : 1 + p.i), e = p.nu < 0 ? -p.nu * (1 - rx.alpha) : p.nu * rx.alpha;
|
|
1326
|
-
a -= p.nu * (u[o] + uLo[o]);
|
|
1327
|
-
pref *= p.side === 2 ? powr(th[g * nSurf + p.s], e) : powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
1328
|
-
}
|
|
1329
|
-
if (rx.sites) pref *= powr(this.th0[g], rx.sites);
|
|
1330
|
-
return pref * bvFactor(a, rx.alpha).g;
|
|
709
|
+
// Newton's iteration on the equations at dt (src/newton.js).
|
|
710
|
+
newton(dt, opts) {
|
|
711
|
+
return newtonFn(this, dt, opts);
|
|
1331
712
|
}
|
|
1332
713
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
zeta[i] = mode[i] === 2 ? Math.log(cFix[i] / mat.cRef[i]) : eta[i] - mat.mu0[i] / RT - z[i] * ph;
|
|
1341
|
-
if (mat.modelOf[i] < 0) cc[i] = mode[i] === 2 ? cFix[i] : mat.cRef[i] * Math.exp(Math.min(700, zeta[i]));
|
|
1342
|
-
}
|
|
1343
|
-
for (const md of mat.models) {
|
|
1344
|
-
const w = this._work(md), idx = md.idx, k = idx.length;
|
|
1345
|
-
for (let a = 0; a < k; a++) {
|
|
1346
|
-
w.z[a] = zeta[idx[a]];
|
|
1347
|
-
w.fixed[a] = mode[idx[a]] === 2 ? 1 : 0;
|
|
1348
|
-
w.t[a] = cFix[idx[a]];
|
|
1349
|
-
}
|
|
1350
|
-
md.invert(w.z, w.fixed, w.t, background);
|
|
1351
|
-
md.evaluate(w.z, w.c, w.K, background);
|
|
1352
|
-
for (let a = 0; a < k; a++) {
|
|
1353
|
-
zeta[idx[a]] = w.z[a];
|
|
1354
|
-
cc[idx[a]] = w.c[a];
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
714
|
+
/**
|
|
715
|
+
* Advance by dt, splitting the interval into halves (recursively) wherever Newton fails.
|
|
716
|
+
* The end time is always t + dt; the result reports the substeps taken.
|
|
717
|
+
*/
|
|
718
|
+
// A time step, halved until it converges (src/timeSteps.js).
|
|
719
|
+
advance(dt, opts, depth = 0) {
|
|
720
|
+
return advanceFn(this, dt, opts, depth);
|
|
1357
721
|
}
|
|
1358
722
|
|
|
1359
|
-
//
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
const { n, M, u, uLo, nSurf, th } = this, RT = this.model.RT;
|
|
1363
|
-
for (let g = 0; g < this.nNodes; g++) {
|
|
1364
|
-
const k = this.surfPort[g];
|
|
1365
|
-
if (k < 0) continue;
|
|
1366
|
-
const surf = this.model.ports[k].surface, b = this.blockOfNode[g];
|
|
1367
|
-
let m = 0;
|
|
1368
|
-
for (let q = 0; q < surf.length; q++) m = Math.max(m, u[b * M + 1 + n + q] + uLo[b * M + 1 + n + q] - surf[q].mu0 / RT);
|
|
1369
|
-
let sum = Math.exp(-m);
|
|
1370
|
-
for (let q = 0; q < surf.length; q++) sum += (th[g * nSurf + q] = Math.exp(u[b * M + 1 + n + q] + uLo[b * M + 1 + n + q] - surf[q].mu0 / RT - m));
|
|
1371
|
-
for (let q = 0; q < surf.length; q++) th[g * nSurf + q] /= sum;
|
|
1372
|
-
this.th0[g] = Math.exp(-m) / sum;
|
|
1373
|
-
}
|
|
723
|
+
// One time step (src/timeSteps.js).
|
|
724
|
+
step(dt, opts = {}) {
|
|
725
|
+
return stepFn(this, dt, opts);
|
|
1374
726
|
}
|
|
1375
727
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
const b = this.blockOfNode[g];
|
|
1380
|
-
const phiHat = u[b * M];
|
|
1381
|
-
if (!this.nodeIdeal[g]) {
|
|
1382
|
-
this._nodeStatistics(g, b, phiHat);
|
|
1383
|
-
continue;
|
|
1384
|
-
}
|
|
1385
|
-
const im = this.nodeConductor[g];
|
|
1386
|
-
if (im >= 0) {
|
|
1387
|
-
// Excess carriers: zero in the bulk, the surface sheet at a charged face.
|
|
1388
|
-
for (let i = 0; i < n; i++) c[g * n + i] = 0;
|
|
1389
|
-
const f = this.sheetFace[g];
|
|
1390
|
-
if (f >= 0) c[g * n + im] = (this.sheetSign[g] * u[this.blockOfFace[f] * M] * this.model.grid.area[g]) / (z[im] * FARADAY * this.model.grid.vol[g]);
|
|
1391
|
-
continue;
|
|
1392
|
-
}
|
|
1393
|
-
for (let i = 0; i < n; i++) {
|
|
1394
|
-
const k = g * n + i;
|
|
1395
|
-
c[k] = this.present[k] ? this.cRef[k] * Math.exp(u[b * M + 1 + i] + uLo[b * M + 1 + i] - this.mu0hat[k] - z[i] * phiHat) : 0;
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
this._coverages();
|
|
728
|
+
// Adaptive time stepping to tEnd (src/timeSteps.js).
|
|
729
|
+
integrate(tEnd, opts = {}) {
|
|
730
|
+
return integrateFn(this, tEnd, opts);
|
|
1399
731
|
}
|
|
1400
732
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
}
|
|
1408
|
-
|
|
733
|
+
// Probes for a transient's trace: { x, species, quantity: 'c' | 'V', region } read after every
|
|
734
|
+
// accepted step, linearly between the two nodes of x's region around it. At an interface x
|
|
735
|
+
// belongs to both regions, so `region` (a name or index) picks the side; it defaults to the
|
|
736
|
+
// first region that holds x.
|
|
737
|
+
_probes(probes) {
|
|
738
|
+
if (probes === undefined) return null;
|
|
739
|
+
const { grid, regions, species } = this.model;
|
|
740
|
+
const fail = (m) => {
|
|
741
|
+
throw new DeviceError(m);
|
|
742
|
+
};
|
|
743
|
+
if (!Array.isArray(probes)) fail('advance: probes must be an array of { x, species, quantity, region }, or { interface, species } / { interface, gate }');
|
|
744
|
+
const plan = probes.map((p, k) => {
|
|
745
|
+
const path = `advance: probes[${k}]`;
|
|
746
|
+
if (p?.interface !== undefined) {
|
|
747
|
+
// At a face: a species' flux through it (mol/(m²·s), toward +x), or a gate's fraction open.
|
|
748
|
+
const f = p.interface, itf = this.model.interfaces[f];
|
|
749
|
+
if (!(Number.isInteger(f) && itf)) fail(`${path}.interface: no interface ${JSON.stringify(f)} (they're numbered from 0, left to right)`);
|
|
750
|
+
if (p.gate !== undefined) {
|
|
751
|
+
const q = itf.gates.findIndex((gt) => gt.name === p.gate);
|
|
752
|
+
if (q < 0) fail(`${path}.gate: interfaces[${f}] has no gate ${JSON.stringify(p.gate)}${itf.gates.length ? ` (it has ${itf.gates.map((gt) => gt.name).join(', ')})` : ''}`);
|
|
753
|
+
return { o: this._gateSlot(f, q) };
|
|
754
|
+
}
|
|
755
|
+
const i = species.findIndex((sp) => sp.name === p.species);
|
|
756
|
+
if (i < 0) fail(`${path}.species: no species ${JSON.stringify(p.species)}`);
|
|
757
|
+
return { o: this.blockOfFace[f] * this.M + 1 + i };
|
|
758
|
+
}
|
|
759
|
+
const quantity = p?.quantity ?? 'c';
|
|
760
|
+
if (quantity !== 'c' && quantity !== 'V' && quantity !== 'phi') fail(`${path}.quantity must be 'c' (mol/m³), 'V' (the species voltage) or 'phi' (φ, V)`);
|
|
761
|
+
const i = quantity === 'phi' ? -1 : species.findIndex((sp) => sp.name === p?.species);
|
|
762
|
+
if (quantity !== 'phi' && i < 0) fail(`${path}.species: no species ${JSON.stringify(p?.species)}`);
|
|
763
|
+
if (quantity === 'V' && species[i].z === 0) fail(`${path}: '${p.species}' is neutral, so it has no voltage; read 'c'`);
|
|
764
|
+
const x = grid.x, inside = (r) => p.x >= x[grid.regionStart[r]] && p.x <= x[grid.regionEnd[r]];
|
|
765
|
+
let r;
|
|
766
|
+
if (p.region !== undefined) {
|
|
767
|
+
r = typeof p.region === 'number' ? p.region : regions.findIndex((reg) => reg.name === p.region);
|
|
768
|
+
if (!(r >= 0 && r < regions.length)) fail(`${path}.region: no region ${JSON.stringify(p.region)}`);
|
|
769
|
+
if (!inside(r)) fail(`${path}.x (${p.x} m) is outside region ${JSON.stringify(p.region)}`);
|
|
770
|
+
} else {
|
|
771
|
+
// At a face, the side where the probe has something to read; both, and it must be told.
|
|
772
|
+
const has = (rr) => {
|
|
773
|
+
const mat = this.model.materials[regions[rr].material];
|
|
774
|
+
return i < 0 ? !mat.conductor && !mat.phiFree : mat.present[i];
|
|
775
|
+
};
|
|
776
|
+
const sides = regions.flatMap((_, rr) => (inside(rr) ? [rr] : []));
|
|
777
|
+
if (!(Number.isFinite(p.x) && sides.length)) fail(`${path}.x must be a position in the device, in m (got ${p.x})`);
|
|
778
|
+
const able = sides.filter(has);
|
|
779
|
+
if (able.length > 1) fail(`${path}.x (${p.x} m) is on the face between ${able.map((rr) => JSON.stringify(regions[rr].name)).join(' and ')}: give region, the side to read`);
|
|
780
|
+
r = able[0] ?? sides[0];
|
|
781
|
+
}
|
|
782
|
+
if (i >= 0 && !this.model.materials[regions[r].material].present[i]) fail(`${path}: '${p.species}' is absent from region ${JSON.stringify(regions[r].name)}, so there's nothing to read at x = ${p.x} m`);
|
|
783
|
+
let g = grid.regionStart[r];
|
|
784
|
+
while (g + 1 < grid.regionEnd[r] && x[g + 1] < p.x) g++;
|
|
785
|
+
const w = x[g + 1] > x[g] ? (p.x - x[g]) / (x[g + 1] - x[g]) : 0;
|
|
786
|
+
return { i, g, w, quantity };
|
|
787
|
+
});
|
|
788
|
+
const out = plan.map(() => []);
|
|
789
|
+
const { n, M } = this;
|
|
790
|
+
const at = (q, g) => {
|
|
791
|
+
if (q.quantity === 'phi') return this.phiUndefined[g] ? NaN : this.VT * (this.u[this.blockOfNode[g] * M] + this.uLo[this.blockOfNode[g] * M]);
|
|
792
|
+
if (!this.present[g * n + q.i]) return NaN;
|
|
793
|
+
if (q.quantity === 'c') return this.c[g * n + q.i];
|
|
794
|
+
const o = this.blockOfNode[g] * M + 1 + q.i;
|
|
795
|
+
return (this.VT * (this.u[o] + this.uLo[o])) / this.z[q.i];
|
|
796
|
+
};
|
|
797
|
+
return {
|
|
798
|
+
out,
|
|
799
|
+
read: () => plan.map((q) => (q.o !== undefined ? (this.loc[q.o] < 0 ? 0 : this.u[q.o]) : (1 - q.w) * at(q, q.g) + q.w * at(q, q.g + 1))),
|
|
800
|
+
push: (values) => values.forEach((v, k) => out[k].push(v)),
|
|
801
|
+
};
|
|
1409
802
|
}
|
|
1410
803
|
|
|
1411
|
-
//
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
804
|
+
// Start from another solver's state for the same device on another grid (a refined one): each
|
|
805
|
+
// node's unknowns interpolated linearly in x within its region, each face's copied, and the
|
|
806
|
+
// terminals' voltages. A warm start for a steady solve; nothing else is carried over.
|
|
807
|
+
_warmFrom(src) {
|
|
808
|
+
const { M, u, uLo } = this, grid = this.model.grid, sg = src.model.grid;
|
|
809
|
+
if (src.M !== M || sg.regionStart.length !== grid.regionStart.length) throw new Error('_warmFrom: a different device');
|
|
810
|
+
const value = (b, k) => src.u[b * M + k] + src.uLo[b * M + k];
|
|
811
|
+
for (let r = 0; r < grid.regionStart.length; r++) {
|
|
812
|
+
let a = sg.regionStart[r];
|
|
813
|
+
for (let g = grid.regionStart[r]; g <= grid.regionEnd[r]; g++) {
|
|
814
|
+
const x = grid.x[g];
|
|
815
|
+
while (a + 1 < sg.regionEnd[r] && sg.x[a + 1] < x) a++;
|
|
816
|
+
const b = Math.min(a + 1, sg.regionEnd[r]), h = sg.x[b] - sg.x[a];
|
|
817
|
+
const w = h > 0 ? Math.min(1, Math.max(0, (x - sg.x[a]) / h)) : 0;
|
|
818
|
+
const ba = src.blockOfNode[a], bb = src.blockOfNode[b], bg = this.blockOfNode[g];
|
|
819
|
+
for (let k = 0; k < M; k++) {
|
|
820
|
+
u[bg * M + k] = (1 - w) * value(ba, k) + w * value(bb, k);
|
|
821
|
+
uLo[bg * M + k] = 0;
|
|
822
|
+
}
|
|
1428
823
|
}
|
|
1429
824
|
}
|
|
1430
|
-
for (
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
for (let a = 0; a < k; a++) {
|
|
1435
|
-
const i = idx[a], ci = w.c[a];
|
|
1436
|
-
c[g * n + i] = ci;
|
|
1437
|
-
ex[g * n + i] = ci > 0 ? zeta[i] - Math.log(ci / this.cRef[g * n + i]) : 0;
|
|
1438
|
-
for (let q = 0; q < k; q++) K[Kg + i * n + idx[q]] = w.K[a * k + q];
|
|
825
|
+
for (let f = 0; f < this.nFaces; f++) {
|
|
826
|
+
for (let k = 0; k < M; k++) {
|
|
827
|
+
u[this.blockOfFace[f] * M + k] = value(src.blockOfFace[f], k);
|
|
828
|
+
uLo[this.blockOfFace[f] * M + k] = 0;
|
|
1439
829
|
}
|
|
1440
830
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
const { n, z } = this;
|
|
1446
|
-
if (this.nodeIdeal[g]) return z[i] * this.c[g * n + i];
|
|
1447
|
-
let s = 0;
|
|
1448
|
-
const o = g * n * n + i * n;
|
|
1449
|
-
for (let j = 0; j < n; j++) s += this.K[o + j] * z[j];
|
|
1450
|
-
return s;
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
/** zᵀKz at node g (per RT): the charge response that sets the screening length. */
|
|
1454
|
-
screening(g) {
|
|
1455
|
-
let s = 0;
|
|
1456
|
-
for (let i = 0; i < this.n; i++) s += this.z[i] * this._Kz(g, i);
|
|
1457
|
-
return s;
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
// Add w·∂(ln c_i)/∂(slot) at node g into d (slot 0 = φ̂, 1 + j = η_j).
|
|
1461
|
-
_dlnc(g, i, w, d) {
|
|
1462
|
-
const { n, z } = this;
|
|
1463
|
-
if (this.nodeIdeal[g]) {
|
|
1464
|
-
d[1 + i] += w;
|
|
1465
|
-
d[0] -= w * z[i];
|
|
1466
|
-
return;
|
|
1467
|
-
}
|
|
1468
|
-
const ci = this.c[g * n + i];
|
|
1469
|
-
if (!(ci > 0)) return;
|
|
1470
|
-
const o = g * n * n + i * n, f = w / ci;
|
|
1471
|
-
for (let j = 0; j < n; j++) d[1 + j] += f * this.K[o + j];
|
|
1472
|
-
d[0] -= f * this._Kz(g, i);
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// Add ∂(row rb, slot rs)/∂(block cb, slot cs) to the Jacobian (nothing if either slot isn't
|
|
1476
|
-
// an unknown).
|
|
1477
|
-
_j(rb, rs, cb, cs, v) {
|
|
1478
|
-
const M = this.M, r = this.loc[rb * M + rs], c = this.loc[cb * M + cs];
|
|
1479
|
-
if (r < 0 || c < 0) return;
|
|
1480
|
-
const sys = this.sys, sz = sys.sizes;
|
|
1481
|
-
if (cb === rb) sys.B[sys.offB[rb] + r * sz[rb] + c] += v;
|
|
1482
|
-
else if (cb === rb - 1) sys.A[sys.offA[rb] + r * sz[cb] + c] += v;
|
|
1483
|
-
else if (cb === rb + 1) sys.C[sys.offC[rb] + r * sz[cb] + c] += v;
|
|
1484
|
-
else throw new Error(`internal: non-tridiagonal coupling ${rb}→${cb}`);
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
/** Assemble residual and Jacobian for a backward-Euler step of size dt. */
|
|
1488
|
-
assemble(dt) {
|
|
1489
|
-
const { model, res, sys } = this;
|
|
1490
|
-
const { grid, materials, regions } = model;
|
|
1491
|
-
sys.clear();
|
|
1492
|
-
res.fill(0);
|
|
1493
|
-
this.dtNow = dt;
|
|
1494
|
-
if (this.lin) this.lin.reset(1 / dt);
|
|
1495
|
-
if (this.steady && dt === Infinity) snapFlat(this);
|
|
1496
|
-
this.computeConcentrations();
|
|
1497
|
-
|
|
1498
|
-
// Regions, each assembled by the kernel for its kind: a conductor (its carrier only), a
|
|
1499
|
-
// dilute region (ideal statistics, the fast path) or a concentrated one (any statistics).
|
|
1500
|
-
for (let r = 0; r < regions.length; r++) {
|
|
1501
|
-
const reg = regions[r], mat = materials[reg.material];
|
|
1502
|
-
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
|
|
1503
|
-
if (mat.conductor) {
|
|
1504
|
-
for (let g = g0; g <= g1; g++) this._nodeConductor(g, dt);
|
|
1505
|
-
for (let s = g0; s < g1; s++) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s] / grid.segArea[s]);
|
|
1506
|
-
continue;
|
|
1507
|
-
}
|
|
1508
|
-
if (mat.ideal) this._nodesDilute(g0, g1, dt);
|
|
1509
|
-
else for (let g = g0; g <= g1; g++) this._nodeConcentrated(g, dt);
|
|
1510
|
-
const rxs = this.rxsIn[reg.material];
|
|
1511
|
-
if (rxs.length > 0) for (let g = g0; g <= g1; g++) this._bulkReactions(g, rxs);
|
|
1512
|
-
for (let s = g0; s < g1; s++) {
|
|
1513
|
-
const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s]; // (a length over the cross-section: fluxes are totals)
|
|
1514
|
-
this._segmentDisplacement(s, bL, bR, mat, h);
|
|
1515
|
-
if (reg.mixing > 0) this._segmentMixing(s, bL, bR, reg.mixing, h, mat);
|
|
1516
|
-
if (!mat.ideal) this._segmentConcentrated(s, bL, bR, mat, h, reg.velocity);
|
|
1517
|
-
}
|
|
1518
|
-
if (mat.ideal) this._segmentsDilute(g0, g1, r, mat, reg.velocity);
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
// Faces: the flux node carries D, the linked species' fluxes and the reaction rates.
|
|
1522
|
-
for (let f = 0; f < this.nFaces; f++) this._face(f);
|
|
1523
|
-
|
|
1524
|
-
// Terminals: ports (after every other term at their nodes, so a held level can read its
|
|
1525
|
-
// flux), then contacts, then the circuit rows of the floating ones.
|
|
1526
|
-
this._terminals(dt);
|
|
1527
|
-
// The laws' rows (src/lawRows.js): a time step's circuit laws; a steady system's levels held
|
|
1528
|
-
// flat, affinities, conserved amounts, islands' balances, and the pins held aside of them.
|
|
1529
|
-
applyLaws(this, dt);
|
|
1530
|
-
this.transformed = this.combining && dt !== Infinity;
|
|
1531
|
-
if (this.transformed) this._chargeRows(dt);
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
// A conductor node: only its carrier's balance (the segment flux J has its own row, in
|
|
1535
|
-
// _segmentConductor). Its bulk stores nothing; a charged face's sheet sits in the edge node.
|
|
1536
|
-
_nodeConductor(g, dt) {
|
|
1537
|
-
const { n, M, res, c, cOld, z } = this, R = this.rix;
|
|
1538
|
-
const b = this.blockOfNode[g], v = this.model.grid.vol[g], im = this.nodeConductor[g];
|
|
1539
|
-
const r = 1 + im, k = g * n + im;
|
|
1540
|
-
res[R[b * M + r]] += (v * (c[k] - cOld[k])) / dt;
|
|
1541
|
-
const f = this.sheetFace[g];
|
|
1542
|
-
if (f >= 0) this._j(b, r, this.blockOfFace[f], 0, (this.sheetSign[g] * this.model.grid.area[g]) / (z[im] * FARADAY * dt));
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
// Whether a port holds species i at node g, a device end node (see portHeldEnd).
|
|
1546
|
-
_portHeldAt(g, i) {
|
|
1547
|
-
return g === 0 ? this.portHeldEnd[i] === 1 : g === this.nNodes - 1 && this.portHeldEnd[this.n + i] === 1;
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
// Dilute nodes g0…g1 (ideal statistics, c = c_ref e^ζ): storage of each species and the space
|
|
1551
|
-
// charge in the Gauss row, written straight into the diagonal blocks by local index. A
|
|
1552
|
-
// dielectric is the case with no species.
|
|
1553
|
-
_nodesDilute(g0, g1, dt) {
|
|
1554
|
-
const { n, M, res, c, cOld, z, sys, loc, present, rhoFixed, blockOfNode, lin } = this, R = this.rix, F = FARADAY;
|
|
1555
|
-
const JB = sys.B, sizes = sys.sizes, offB = sys.offB, vol = this.model.grid.vol;
|
|
1556
|
-
for (let g = g0; g <= g1; g++) {
|
|
1557
|
-
const b = blockOfNode[g], v = vol[g];
|
|
1558
|
-
const m = sizes[b], oB = offB[b], lb = b * M, p = loc[lb]; // p: φ's row, −1 where undefined
|
|
1559
|
-
if (this.combining && this.chargeNode[g] === 1 && dt !== Infinity) {
|
|
1560
|
-
// Storage and neutrality go in later, in _chargeRows; a neutral species' storage here.
|
|
1561
|
-
for (let i = 0; i < n; i++) if (present[g * n + i] && z[i] === 0) this._storage(g, i, dt);
|
|
1562
|
-
continue;
|
|
1563
|
-
}
|
|
1564
|
-
let q = rhoFixed[g], dq = 0;
|
|
1565
|
-
const late = this.combining && this.lateStorage[g] === 1 && dt !== Infinity; // (the ions' storage: in _contact)
|
|
1566
|
-
for (let i = 0; i < n; i++) {
|
|
1567
|
-
const k = g * n + i;
|
|
1568
|
-
if (!present[k]) continue;
|
|
1569
|
-
const ck = c[k], l = loc[lb + 1 + i], stores = !(late && z[i] !== 0 && !this._portHeldAt(g, i));
|
|
1570
|
-
if (stores) res[R[lb + 1 + i]] += (v * (ck - cOld[k])) / dt;
|
|
1571
|
-
q += F * z[i] * ck;
|
|
1572
|
-
dq += F * z[i] * z[i] * ck;
|
|
1573
|
-
if (lin) {
|
|
1574
|
-
lin.node(R[lb + 1 + i], R[lb], z[i], stores ? v * ck : 0, -v * F * z[i] * ck);
|
|
1575
|
-
continue;
|
|
1576
|
-
}
|
|
1577
|
-
if (stores) {
|
|
1578
|
-
JB[oB + l * m + l] += (v * ck) / dt;
|
|
1579
|
-
if (p >= 0 && z[i] !== 0) JB[oB + l * m + p] += (-v * z[i] * ck) / dt;
|
|
1580
|
-
}
|
|
1581
|
-
if (p >= 0 && z[i] !== 0) JB[oB + p * m + l] += -v * F * z[i] * ck;
|
|
1582
|
-
}
|
|
1583
|
-
if (p >= 0) {
|
|
1584
|
-
res[R[lb]] -= v * q;
|
|
1585
|
-
if (!lin) JB[oB + p * m + p] += v * dq;
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
// A concentrated node (any statistics): ∂c_i/∂η_j = K_ij and ∂c_i/∂φ̂ = −(Kz)_i.
|
|
1591
|
-
_nodeConcentrated(g, dt) {
|
|
1592
|
-
const { n, M, res, c, z } = this, R = this.rix, F = FARADAY;
|
|
1593
|
-
const b = this.blockOfNode[g], v = this.model.grid.vol[g];
|
|
1594
|
-
if (this.combining && this.chargeNode[g] === 1 && dt !== Infinity) {
|
|
1595
|
-
// Storage and neutrality go in later, in _chargeRows; a neutral species' storage here.
|
|
1596
|
-
for (let i = 0; i < n; i++) if (this.present[g * n + i] && z[i] === 0) this._storage(g, i, dt);
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
let q = this.rhoFixed[g], dq = 0;
|
|
1600
|
-
const lin = this.lin;
|
|
1601
|
-
for (let i = 0; i < n; i++) {
|
|
1602
|
-
const k = g * n + i, r = 1 + i;
|
|
1603
|
-
if (!this.present[k]) continue;
|
|
1604
|
-
const Kz = this._Kz(g, i);
|
|
1605
|
-
if (lin) {
|
|
1606
|
-
// In difference form (see DifferenceTerms): storage of i and its charge, in each
|
|
1607
|
-
// δη_j − z_j δφ̂ through K_ij.
|
|
1608
|
-
res[R[b * M + r]] += (v * (c[k] - this.cOld[k])) / dt;
|
|
1609
|
-
q += F * z[i] * c[k];
|
|
1610
|
-
const Kg = g * n * n + i * n;
|
|
1611
|
-
for (let j = 0; j < n; j++) {
|
|
1612
|
-
const Kij = this.nodeIdeal[g] ? (i === j ? c[k] : 0) : this.K[Kg + j];
|
|
1613
|
-
if (Kij !== 0) lin.node(R[b * M + 1 + j], R[b * M], z[j], v * Kij, -v * F * z[i] * Kij, R[b * M + r]);
|
|
1614
|
-
}
|
|
1615
|
-
continue;
|
|
1616
|
-
}
|
|
1617
|
-
this._storage(g, i, dt, Kz);
|
|
1618
|
-
q += F * z[i] * c[k];
|
|
1619
|
-
dq += F * z[i] * Kz;
|
|
1620
|
-
if (Kz !== 0) this._j(b, 0, b, r, -v * F * Kz); // K symmetric: ∂(Σ z c)/∂η_i = (Kz)_i
|
|
1621
|
-
}
|
|
1622
|
-
// (where no charge responds and no field reaches, φ isn't defined, nor an unknown)
|
|
1623
|
-
if (!this.phiUndefined[g]) {
|
|
1624
|
-
res[R[b * M]] -= v * q;
|
|
1625
|
-
if (!lin) this._j(b, 0, b, 0, v * dq);
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
// Species i's storage at node g, v(c − c_old)/dt, in its balance row and the Jacobian
|
|
1630
|
-
// (∂c_i/∂η_j = K_ij, ∂c_i/∂φ̂ = −(Kz)_i; for ideal statistics K is diagonal, c_i).
|
|
1631
|
-
_storage(g, i, dt, Kz = this._Kz(g, i)) {
|
|
1632
|
-
const { n, M, res, c, cOld } = this, R = this.rix;
|
|
1633
|
-
const b = this.blockOfNode[g], v = this.model.grid.vol[g], k = g * n + i, r = 1 + i;
|
|
1634
|
-
res[R[b * M + r]] += (v * (c[k] - cOld[k])) / dt;
|
|
1635
|
-
if (this.nodeIdeal[g]) this._j(b, r, b, r, (v * c[k]) / dt);
|
|
1636
|
-
else {
|
|
1637
|
-
const Kg = g * n * n;
|
|
1638
|
-
for (let j = 0; j < n; j++) {
|
|
1639
|
-
const Kij = this.K[Kg + i * n + j];
|
|
1640
|
-
if (Kij !== 0) this._j(b, r, b, 1 + j, (v * Kij) / dt);
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
if (Kz !== 0 && !this.phiUndefined[g]) this._j(b, r, b, 0, (-v * Kz) / dt);
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
// Strictly neutral nodes on a transient step, solved in better-conditioned terms. There, a
|
|
1647
|
-
// change of φ̂ with every η_i shifted by z_i times it leaves every concentration as it was, so
|
|
1648
|
-
// storage and neutrality don't see it; but in (φ̂, η) they see it as pairs of huge entries that
|
|
1649
|
-
// cancel, and round-off in that cancellation (storage/flux ~ h²/(D·dt), times the range of
|
|
1650
|
-
// concentrations: a trace ion beside 3 M KCl) swamps the fluxes that do fix it. So, at each node
|
|
1651
|
-
// that stays neutral (see chargeNode), with storage and neutrality left out of assembly:
|
|
1652
|
-
// - rows: the balance of the most abundant charged species (by z²c) becomes Σ (z_i/z_k) × each
|
|
1653
|
-
// balance, which without storage is current continuity;
|
|
1654
|
-
// - columns: the unknowns become φ̂' and η'_i = η_i − z_i φ̂ (newton() maps the update back),
|
|
1655
|
-
// in which storage and neutrality have no φ̂' term at all;
|
|
1656
|
-
// - then storage (on the other balances) and neutrality go in, exactly, in those terms.
|
|
1657
|
-
// A start-of-step net charge (round-off in a solved state) isn't carried over: the neutrality
|
|
1658
|
-
// row holds the new state neutral.
|
|
1659
|
-
_chargeRows(dt) {
|
|
1660
|
-
const { n, M, res, c, cOld, z, sys, loc, present, termB, termC } = this, R = this.rix, F = FARADAY, live = this._liveTerms();
|
|
1661
|
-
const { A, B, C, sizes, offA, offB, offC } = sys, vol = this.model.grid.vol, last = this.nB - 1;
|
|
1662
|
-
for (const g of this.chargeNodes) {
|
|
1663
|
-
const b = this.blockOfNode[g], lb = b * M, m = sizes[b], mp = b > 0 ? sizes[b - 1] : 0, mn = b < last ? sizes[b + 1] : 0;
|
|
1664
|
-
const p = loc[lb], gn = g * n;
|
|
1665
|
-
let k = -1, best = -1;
|
|
1666
|
-
for (let i = 0; i < n; i++) {
|
|
1667
|
-
if (!present[gn + i] || z[i] === 0) continue;
|
|
1668
|
-
const w = z[i] * z[i] * c[gn + i];
|
|
1669
|
-
if (w > best) {
|
|
1670
|
-
k = i;
|
|
1671
|
-
best = w;
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
if (k < 0 || p < 0) continue;
|
|
1675
|
-
// Rows: the pivot's balance becomes the z-weighted sum (all still without storage).
|
|
1676
|
-
const lk = loc[lb + 1 + k], rk = R[lb + 1 + k];
|
|
1677
|
-
for (let i = 0; i < n; i++) {
|
|
1678
|
-
if (i === k || !present[gn + i] || z[i] === 0) continue;
|
|
1679
|
-
const li = loc[lb + 1 + i], w = z[i] / z[k], ri = R[lb + 1 + i];
|
|
1680
|
-
for (let q = 0, o = offA[b] + lk * mp, s = offA[b] + li * mp; q < mp; q++) A[o + q] += w * A[s + q];
|
|
1681
|
-
for (let q = 0, o = offB[b] + lk * m, s = offB[b] + li * m; q < m; q++) B[o + q] += w * B[s + q];
|
|
1682
|
-
for (let q = 0, o = offC[b] + lk * mn, s = offC[b] + li * mn; q < mn; q++) C[o + q] += w * C[s + q];
|
|
1683
|
-
res[rk] += w * res[ri];
|
|
1684
|
-
for (const t of live) termB[t][rk] += w * termB[t][ri];
|
|
1685
|
-
if (this.lin) this.lin.combine(rk, ri, w);
|
|
1686
|
-
}
|
|
1687
|
-
// Columns: φ̂' = φ̂ with η fixed becomes φ̂ with η_i shifted by z_i, wherever node g's
|
|
1688
|
-
// unknowns appear (its own rows, its neighbours', a terminal's current).
|
|
1689
|
-
for (let i = 0; i < n; i++) {
|
|
1690
|
-
if (!present[gn + i] || z[i] === 0) continue;
|
|
1691
|
-
const li = loc[lb + 1 + i], zi = z[i];
|
|
1692
|
-
for (let r = 0, o = offB[b]; r < m; r++) B[o + r * m + p] += zi * B[o + r * m + li];
|
|
1693
|
-
if (b > 0) for (let r = 0, mr = mp, o = offC[b - 1]; r < mr; r++) C[o + r * m + p] += zi * C[o + r * m + li];
|
|
1694
|
-
if (b < last) for (let r = 0, mr = mn, o = offA[b + 1]; r < mr; r++) A[o + r * m + p] += zi * A[o + r * m + li];
|
|
1695
|
-
for (const t of live) termC[t][R[lb]] += zi * termC[t][R[lb + 1 + i]];
|
|
1696
|
-
}
|
|
1697
|
-
// Storage on the other charged balances, and neutrality: no φ̂' terms, exactly.
|
|
1698
|
-
const v = vol[g], oB = offB[b];
|
|
1699
|
-
let q = this.rhoFixed[g];
|
|
1700
|
-
if (this.nodeIdeal[g]) {
|
|
1701
|
-
for (let i = 0; i < n; i++) {
|
|
1702
|
-
if (!present[gn + i] || z[i] === 0) continue;
|
|
1703
|
-
const ci = c[gn + i], li = loc[lb + 1 + i];
|
|
1704
|
-
if (i !== k) {
|
|
1705
|
-
res[R[lb + 1 + i]] += (v * (ci - cOld[gn + i])) / dt;
|
|
1706
|
-
B[oB + li * m + li] += (v * ci) / dt;
|
|
1707
|
-
}
|
|
1708
|
-
q += F * z[i] * ci;
|
|
1709
|
-
B[oB + p * m + li] += -v * F * z[i] * ci;
|
|
1710
|
-
}
|
|
1711
|
-
} else {
|
|
1712
|
-
for (let i = 0; i < n; i++) {
|
|
1713
|
-
if (!present[gn + i]) continue;
|
|
1714
|
-
if (i !== k && z[i] !== 0) this._storage(g, i, dt, 0);
|
|
1715
|
-
q += F * z[i] * c[gn + i];
|
|
1716
|
-
const Kz = this._Kz(g, i); // (nonzero for a neutral species only on a shared lattice)
|
|
1717
|
-
if (Kz !== 0) this._j(b, 0, b, 1 + i, -v * F * Kz);
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
res[R[lb]] -= v * q;
|
|
1721
|
-
// The pivot's row (charge continuity) left storage out, the ions' net charge being constant
|
|
1722
|
-
// under neutrality. Against a capacitance it isn't: neutrality makes it −(aσ + ρ_fixed)/F,
|
|
1723
|
-
// so their storage is that less what the step started with, Σ z c_old, over dt (in units
|
|
1724
|
-
// of the pivot's balance).
|
|
1725
|
-
if (this.qgTerms[g].length > 0) {
|
|
1726
|
-
const f = -v / (FARADAY * z[k] * dt);
|
|
1727
|
-
let qOld = this.rhoFixed[g];
|
|
1728
|
-
for (let i = 0; i < n; i++) if (present[gn + i]) qOld += F * z[i] * cOld[gn + i];
|
|
1729
|
-
res[rk] += f * (this.qg[g] + qOld);
|
|
1730
|
-
B[offB[b] + lk * m + p] += -f * this.qgSlope[g] * this.VT;
|
|
1731
|
-
for (const [kq, w] of this.qgTerms[g]) termB[kq][rk] += f * this.portArea[kq - 2][w] * this.model.ports[kq - 2].capacitance.C;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
// Newton's update in the transformed unknowns (see _chargeRows) back in η: η_i = η'_i + z_i φ̂'.
|
|
1737
|
-
_untransform(delta) {
|
|
1738
|
-
const { n, M, z, loc, present } = this, R = this.rix;
|
|
1739
|
-
for (const g of this.chargeNodes) {
|
|
1740
|
-
const lb = this.blockOfNode[g] * M;
|
|
1741
|
-
if (loc[lb] < 0) continue;
|
|
1742
|
-
const dphi = delta[R[lb]];
|
|
1743
|
-
for (let i = 0; i < n; i++) if (present[g * n + i] && z[i] !== 0) delta[R[lb + 1 + i]] += z[i] * dphi;
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
// Bulk reactions at node g: r = k_f Π c_R^ν · (−expm1(−a)), with a = A/RT from the
|
|
1748
|
-
// (compensated) η. Reactants are consumed (+v·ν·r in their balance), products made (−v·ν·r).
|
|
1749
|
-
_bulkReactions(g, rxs) {
|
|
1750
|
-
const { n, M, u, uLo, res, c } = this, R = this.rix;
|
|
1751
|
-
const b = this.blockOfNode[g], v = this.model.grid.vol[g];
|
|
1752
|
-
const dr = this.dA;
|
|
1753
|
-
for (let x = 0; x < rxs.length; x++) {
|
|
1754
|
-
const rx = rxs[x];
|
|
1755
|
-
const law = rx.srh?.[this.nodeMaterial[g]];
|
|
1756
|
-
if (law) {
|
|
1757
|
-
// SRH: dr is ∂rate/∂slot directly, from its sensitivities to ln n, ln p and a.
|
|
1758
|
-
let aHi = rx.fixedA, aLo = 0;
|
|
1759
|
-
for (let p = 0; p < rx.sp.length; p++) {
|
|
1760
|
-
aHi += rx.nu[p] * u[b * M + 1 + rx.sp[p]];
|
|
1761
|
-
aLo += rx.nu[p] * uLo[b * M + 1 + rx.sp[p]];
|
|
1762
|
-
}
|
|
1763
|
-
const { rate, Cn, Cp, Ca } = this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], aHi + aLo, ...this._srhDegen(g, g, law));
|
|
1764
|
-
dr.fill(0);
|
|
1765
|
-
this._dlnc(g, law.n.i, Cn, dr);
|
|
1766
|
-
this._dlnc(g, law.p.i, Cp, dr);
|
|
1767
|
-
for (let p = 0; p < rx.sp.length; p++) dr[1 + rx.sp[p]] += Ca * rx.nu[p];
|
|
1768
|
-
for (let p = 0; p < rx.sp.length; p++) {
|
|
1769
|
-
const row = 1 + rx.sp[p], w = v * rx.nu[p];
|
|
1770
|
-
res[R[b * M + row]] += w * rate;
|
|
1771
|
-
for (let k = 0; k < M; k++) if (dr[k] !== 0) this._j(b, row, b, k, w * dr[k]);
|
|
1772
|
-
}
|
|
1773
|
-
continue;
|
|
1774
|
-
}
|
|
1775
|
-
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g], aHi = rx.fixedA, aLo = 0;
|
|
1776
|
-
dr.fill(0); // ∂ ln P / ∂slot
|
|
1777
|
-
// Participants: reactants with ν > 0, then products with ν < 0.
|
|
1778
|
-
for (let p = 0; p < rx.sp.length; p++) {
|
|
1779
|
-
const i = rx.sp[p], nu = rx.nu[p];
|
|
1780
|
-
if (nu > 0) {
|
|
1781
|
-
const ci = c[g * n + i];
|
|
1782
|
-
P *= nu === 1 ? ci : powi(ci, nu);
|
|
1783
|
-
this._dlnc(g, i, nu, dr);
|
|
1784
|
-
}
|
|
1785
|
-
aHi += nu * u[b * M + 1 + i];
|
|
1786
|
-
aLo += nu * uLo[b * M + 1 + i];
|
|
1787
|
-
}
|
|
1788
|
-
const a = aHi + aLo;
|
|
1789
|
-
const f = -Math.expm1(-a); // 1 − e^{−a}
|
|
1790
|
-
const rate = P * f;
|
|
1791
|
-
const Pd = P * (1 - f); // P·df/da, df/da = e^{−a}
|
|
1792
|
-
// ∂rate/∂slot = rate·∂lnP + P·f′·∂a
|
|
1793
|
-
for (let k = 0; k < M; k++) dr[k] *= rate;
|
|
1794
|
-
for (let p = 0; p < rx.sp.length; p++) dr[1 + rx.sp[p]] += Pd * rx.nu[p];
|
|
1795
|
-
for (let p = 0; p < rx.sp.length; p++) {
|
|
1796
|
-
const row = 1 + rx.sp[p], w = v * rx.nu[p];
|
|
1797
|
-
res[R[b * M + row]] += w * rate;
|
|
1798
|
-
for (let k = 0; k < M; k++) if (dr[k] !== 0) this._j(b, row, b, k, w * dr[k]);
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
// Displacement along a segment where φ is defined: D = −ε(φ_R − φ_L)/h into the two Gauss rows.
|
|
1804
|
-
_segmentDisplacement(s, bL, bR, mat, h) {
|
|
1805
|
-
const { M, u, res, sys, loc } = this, R = this.rix;
|
|
1806
|
-
const k = this.phiUndefined[s] ? 0 : (mat.epsr * EPS0 * this.VT) / h; // ε = 0: no displacement
|
|
1807
|
-
if (k === 0) return;
|
|
1808
|
-
const D = -k * (u[bR * M] - u[bL * M]);
|
|
1809
|
-
const mL = sys.sizes[bL], mR = sys.sizes[bR], pL = loc[bL * M], pR = loc[bR * M];
|
|
1810
|
-
res[R[bL * M]] += D;
|
|
1811
|
-
res[R[bR * M]] -= D;
|
|
1812
|
-
if (this.lin) return this.lin.displacement(R[bL * M], R[bR * M], k);
|
|
1813
|
-
sys.B[sys.offB[bL] + pL * mL + pL] += k;
|
|
1814
|
-
sys.C[sys.offC[bL] + pL * mR + pR] -= k;
|
|
1815
|
-
sys.A[sys.offA[bR] + pR * mL + pL] -= k;
|
|
1816
|
-
sys.B[sys.offB[bR] + pR * mR + pR] += k;
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
// Scharfetter–Gummel fluxes along a dilute region's segments (nodes g0…g1), written
|
|
1820
|
-
// by local index. The flux
|
|
1821
|
-
// N = g[B(Δ)c_L − B(−Δ)c_R] is rewritten with B(−Δ) = B(Δ)e^Δ and c_R e^Δ = c_L e^{Δη} as
|
|
1822
|
-
// N = −g·B(Δ)·c_L·expm1(Δη). This is precise relative to the quasi-Fermi difference Δη, so tiny
|
|
1823
|
-
// fluxes (e.g. majority carriers carrying a small current) don't vanish in the cancellation of
|
|
1824
|
-
// two huge drift and diffusion terms.
|
|
1825
|
-
_segmentsDilute(g0, g1, region, mat, vel) {
|
|
1826
|
-
const { n, M, u, uLo, res, c, z, sys, loc, lin } = this, R = this.rix;
|
|
1827
|
-
const { A: JA, B: JB, C: JC, sizes, offA, offB, offC } = sys;
|
|
1828
|
-
const { segLength, segArea } = this.model.grid;
|
|
1829
|
-
for (let s = g0; s < g1; s++) {
|
|
1830
|
-
const bL = s + region, bR = bL + 1, h = segLength[s] / segArea[s]; // (fluxes are totals through the cross-section)
|
|
1831
|
-
const mL = sizes[bL], mR = sizes[bR], pL = loc[bL * M], pR = loc[bR * M];
|
|
1832
|
-
const phiL = u[bL * M], phiR = u[bR * M];
|
|
1833
|
-
for (let i = 0; i < n; i++) {
|
|
1834
|
-
if (!mat.present[i] || mat.D[i] === 0) continue;
|
|
1835
|
-
const r = 1 + i, zi = z[i];
|
|
1836
|
-
const cL = c[s * n + i];
|
|
1837
|
-
const g = mat.D[i] / h;
|
|
1838
|
-
const pe = (vel * h) / mat.D[i]; // advection: a Péclet shift of the drift potential
|
|
1839
|
-
const d = zi * (phiR - phiL) - pe;
|
|
1840
|
-
const deta = u[bR * M + r] - u[bL * M + r] + (uLo[bR * M + r] - uLo[bL * M + r]) - pe;
|
|
1841
|
-
const E = Math.expm1(deta);
|
|
1842
|
-
const gBc = g * bernoulli(d) * cL;
|
|
1843
|
-
const N = -gBc * E;
|
|
1844
|
-
const dNdd = -g * bernoulliDerivative(d) * cL * E;
|
|
1845
|
-
const dNdEtaL = gBc;
|
|
1846
|
-
const dNdEtaR = -gBc * (E + 1);
|
|
1847
|
-
const dNdPhiL = -zi * dNdd + zi * gBc * E; // via Δ, and via c_L ∝ e^{−zφ̂_L}
|
|
1848
|
-
const dNdPhiR = zi * dNdd;
|
|
1849
|
-
res[R[bL * M + r]] += N;
|
|
1850
|
-
res[R[bR * M + r]] -= N;
|
|
1851
|
-
if (lin) {
|
|
1852
|
-
lin.segment(R[bL * M + r], R[bR * M + r], R[bL * M], R[bR * M], zi, -gBc * E, dNdEtaR, zi * dNdd);
|
|
1853
|
-
continue;
|
|
1854
|
-
}
|
|
1855
|
-
// Row r of block bL (couplings to itself in B, to bR in C) and of bR (to bL in A).
|
|
1856
|
-
const lL = loc[bL * M + r], lR = loc[bR * M + r];
|
|
1857
|
-
const oBL = offB[bL] + lL * mL, oCL = offC[bL] + lL * mR, oAR = offA[bR] + lR * mL, oBR = offB[bR] + lR * mR;
|
|
1858
|
-
JB[oBL + lL] += dNdEtaL;
|
|
1859
|
-
JC[oCL + lR] += dNdEtaR;
|
|
1860
|
-
JA[oAR + lL] -= dNdEtaL;
|
|
1861
|
-
JB[oBR + lR] -= dNdEtaR;
|
|
1862
|
-
if (zi !== 0 && pL >= 0) {
|
|
1863
|
-
JB[oBL + pL] += dNdPhiL;
|
|
1864
|
-
JC[oCL + pR] += dNdPhiR;
|
|
1865
|
-
JA[oAR + pL] -= dNdPhiL;
|
|
1866
|
-
JB[oBR + pR] -= dNdPhiR;
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
// A face: its flux node's rows are the interface laws (φ law, species links, reactions), and
|
|
1873
|
-
// its fluxes enter the two edge nodes' balances.
|
|
1874
|
-
_face(f) {
|
|
1875
|
-
const { model, n, M, u, uLo, res, z, VT } = this, R = this.rix, F = FARADAY;
|
|
1876
|
-
const grid = model.grid, interfaces = model.interfaces;
|
|
1877
|
-
const bf = this.blockOfFace[f], bL = bf - 1, bR = bf + 1;
|
|
1878
|
-
const itf = interfaces[f];
|
|
1879
|
-
// φ law: pinned jump (dipole), Helmholtz capacitor, or no charge at all (neutral).
|
|
1880
|
-
const law = itf.phi.type;
|
|
1881
|
-
const condSide = itf.conductor && law !== 'neutral' ? itf.conductor.side : null;
|
|
1882
|
-
if (condSide) {
|
|
1883
|
-
// Against a metal: the other side's φ is tied to the metal's Fermi level V_F = V_T η/z.
|
|
1884
|
-
const im = itf.conductor.i, bm = condSide === 'left' ? bL : bR, bo = condSide === 'left' ? bR : bL;
|
|
1885
|
-
const sg = condSide === 'left' ? 1 : -1;
|
|
1886
|
-
const vf = (u[bm * M + 1 + im] + uLo[bm * M + 1 + im]) / z[im]; // V_F / V_T
|
|
1887
|
-
const gap = vf - itf.zeroCharge / VT - (u[bo * M] + uLo[bo * M]); // (V_F − zeroCharge − φ_edge)/V_T
|
|
1888
|
-
if (law === 'pinned') {
|
|
1889
|
-
res[R[bf * M]] = gap;
|
|
1890
|
-
this._j(bf, 0, bm, 1 + im, 1 / z[im]);
|
|
1891
|
-
this._j(bf, 0, bo, 0, -1);
|
|
1892
|
-
} else {
|
|
1893
|
-
// D toward the other side = C (V_F − zeroCharge − φ_edge); toward +x that's sg times it.
|
|
1894
|
-
const kC = itf.phi.C * VT;
|
|
1895
|
-
res[R[bf * M]] = u[bf * M] - sg * kC * gap;
|
|
1896
|
-
this._j(bf, 0, bf, 0, 1);
|
|
1897
|
-
this._j(bf, 0, bm, 1 + im, (-sg * kC) / z[im]);
|
|
1898
|
-
this._j(bf, 0, bo, 0, sg * kC);
|
|
1899
|
-
}
|
|
1900
|
-
} else if (law === 'neutral') {
|
|
1901
|
-
// D = 0 (not an unknown); the jump is whatever each side's neutrality needs
|
|
1902
|
-
} else {
|
|
1903
|
-
const jump = u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M]) - itf.dipole / VT;
|
|
1904
|
-
if (law === 'pinned') {
|
|
1905
|
-
res[R[bf * M]] = jump;
|
|
1906
|
-
this._j(bf, 0, bR, 0, 1);
|
|
1907
|
-
this._j(bf, 0, bL, 0, -1);
|
|
1908
|
-
} else {
|
|
1909
|
-
// D = −C (φ_R − φ_L − dipole): displacement toward +x drops across the layer.
|
|
1910
|
-
const kC = itf.phi.C * VT;
|
|
1911
|
-
res[R[bf * M]] = u[bf * M] + kC * jump;
|
|
1912
|
-
this._j(bf, 0, bf, 0, 1);
|
|
1913
|
-
this._j(bf, 0, bR, 0, kC);
|
|
1914
|
-
this._j(bf, 0, bL, 0, -kC);
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
// D_f enters each side's Gauss row; a metal side holds it as surface carriers instead. The
|
|
1918
|
-
// flux node's unknowns are per area, so what they carry is that times the face's area.
|
|
1919
|
-
const gL = grid.regionEnd[f], gR = gL + 1, Af = grid.area[gL];
|
|
1920
|
-
if (this.nodeConductor[gL] < 0) {
|
|
1921
|
-
res[R[bL * M]] += Af * u[bf * M];
|
|
1922
|
-
this._j(bL, 0, bf, 0, Af);
|
|
1923
|
-
}
|
|
1924
|
-
if (this.nodeConductor[gR] < 0) {
|
|
1925
|
-
res[R[bR * M]] -= Af * (u[bf * M] + itf.sheetCharge);
|
|
1926
|
-
this._j(bR, 0, bf, 0, -Af);
|
|
1927
|
-
}
|
|
1928
|
-
for (let i = 0; i < n; i++) {
|
|
1929
|
-
const r = 1 + i, o = bf * M + r;
|
|
1930
|
-
const type = itf.links[i].type;
|
|
1931
|
-
const deta = u[bL * M + r] - u[bR * M + r] + (uLo[bL * M + r] - uLo[bR * M + r]); // η_L − η_R
|
|
1932
|
-
if (type === 'blocked') continue;
|
|
1933
|
-
if (type === 'equilibrium') {
|
|
1934
|
-
res[R[o]] = -deta; // μ̄ continuous
|
|
1935
|
-
this._j(bf, r, bR, r, 1);
|
|
1936
|
-
this._j(bf, r, bL, r, -1);
|
|
1937
|
-
} else if (type === 'permeability') {
|
|
1938
|
-
// Electrodiffusion through a thin membrane in a constant field: Scharfetter–Gummel across
|
|
1939
|
-
// the face with D/h → P, its drift taken over the face's whole jump in the standard level
|
|
1940
|
-
// zφ̂ + μ°/RT − ln c_ref (the φ jump of a capacitive face, and any step between the two
|
|
1941
|
-
// materials). Between like solutions it's Goldman–Hodgkin–Katz's flux,
|
|
1942
|
-
// N = P·zu·(c_L − c_R e^{zu})/(e^{zu} − 1), u = (φ_R − φ_L)/V_T,
|
|
1943
|
-
// and it's exactly zero where μ̄ is level.
|
|
1944
|
-
const gl = grid.regionEnd[f], gr = gl + 1, kl = gl * n + i, kr = gr * n + i, zi = z[i], link = itf.links[i];
|
|
1945
|
-
const shift = this.mu0hat[kr] - this.mu0hat[kl] - Math.log(this.cRef[kr] / this.cRef[kl]);
|
|
1946
|
-
const d = zi * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M])) + shift;
|
|
1947
|
-
const E = Math.expm1(-deta); // η_R − η_L
|
|
1948
|
-
// Gated: P × Π x^p, each x taken in [0, 1] (Newton's iterates can stray past its ends).
|
|
1949
|
-
const gated = link.gates ?? [], gx = (q) => u[this._gateSlot(f, q)];
|
|
1950
|
-
const P = link.P * (gated.length ? this._open(gated, gx) : 1);
|
|
1951
|
-
const gBc = P * bernoulli(d) * this.c[kl];
|
|
1952
|
-
const dNdd = -P * bernoulliDerivative(d) * this.c[kl] * E;
|
|
1953
|
-
res[R[o]] = u[o] + gBc * E;
|
|
1954
|
-
this._j(bf, r, bf, r, 1);
|
|
1955
|
-
for (const [q, p] of gated) {
|
|
1956
|
-
const dO = this._dOpen(gated, q, p, gx);
|
|
1957
|
-
if (dO !== 0) this._j(bf, r, bf, 1 + n + itf.reactions.length + q, link.P * bernoulli(d) * this.c[kl] * E * dO);
|
|
1958
|
-
}
|
|
1959
|
-
this._j(bf, r, bL, r, -gBc);
|
|
1960
|
-
this._j(bf, r, bR, r, gBc * (E + 1));
|
|
1961
|
-
if (zi !== 0) {
|
|
1962
|
-
this._j(bf, r, bL, 0, zi * dNdd - zi * gBc * E);
|
|
1963
|
-
this._j(bf, r, bR, 0, -zi * dNdd);
|
|
1964
|
-
}
|
|
1965
|
-
} else {
|
|
1966
|
-
// conductance: J = G (V_L − V_R), V = V_T η / z ⇒ N = G V_T (η_L − η_R) / (z² F); gated,
|
|
1967
|
-
// G times Π x^p
|
|
1968
|
-
const gated = itf.links[i].gates ?? [], gx = (q) => u[this._gateSlot(f, q)];
|
|
1969
|
-
const gG0 = (itf.links[i].G * VT) / (z[i] * z[i] * F), gG = gG0 * (gated.length ? this._open(gated, gx) : 1);
|
|
1970
|
-
res[R[o]] = u[o] - gG * deta;
|
|
1971
|
-
this._j(bf, r, bf, r, 1);
|
|
1972
|
-
this._j(bf, r, bL, r, -gG);
|
|
1973
|
-
this._j(bf, r, bR, r, gG);
|
|
1974
|
-
for (const [q, p] of gated) {
|
|
1975
|
-
const dO = this._dOpen(gated, q, p, gx);
|
|
1976
|
-
if (dO !== 0) this._j(bf, r, bf, 1 + n + itf.reactions.length + q, -gG0 * deta * dO);
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
res[R[bL * M + r]] += Af * u[o];
|
|
1980
|
-
this._j(bL, r, bf, r, Af);
|
|
1981
|
-
res[R[bR * M + r]] -= Af * u[o];
|
|
1982
|
-
this._j(bR, r, bf, r, -Af);
|
|
1983
|
-
}
|
|
1984
|
-
itf.reactions.forEach((rx, k) => this._faceReaction(rx, k, f, bf, bL, bR, Af));
|
|
1985
|
-
if (itf.gates.length > 0) this._faceGates(f, bf, bL, bR);
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
// Full index of face f's gate q (its fraction open).
|
|
1989
|
-
_gateSlot(f, q) {
|
|
1990
|
-
return this.blockOfFace[f] * this.M + 1 + this.n + this.model.interfaces[f].reactions.length + q;
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
// The voltage a gate follows: across its face, or across its port's capacitance at its node.
|
|
1994
|
-
_gateVoltage(gt) {
|
|
1995
|
-
return gt.port === undefined ? this._faceVoltage(gt.f) : this._portVoltage(gt.port, gt.g);
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
// The voltage across port k's capacitance at node g, φ − (V − zeroCharge) (V): a membrane's,
|
|
1999
|
-
// inside (the region) minus outside.
|
|
2000
|
-
_portVoltage(k, g) {
|
|
2001
|
-
const { M, u, uLo, VT } = this, b = this.blockOfNode[g], port = this.model.ports[k];
|
|
2002
|
-
return VT * (u[b * M] + uLo[b * M]) - (this.termV[2 + k] - port.capacitance.zeroCharge);
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
// The voltage across face f, φ_right − φ_left (V).
|
|
2006
|
-
_faceVoltage(f) {
|
|
2007
|
-
const { M, u, uLo, VT } = this, bf = this.blockOfFace[f], bL = bf - 1, bR = bf + 1;
|
|
2008
|
-
return VT * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M]));
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
// Face f's gates, each a row (x − x_old)/dt = α(1 − x) − βx in the voltage across the face.
|
|
2012
|
-
_faceGates(f, bf, bL, bR) {
|
|
2013
|
-
const { res, VT } = this, R = this.rix, itf = this.model.interfaces[f], dt = this.dtNow;
|
|
2014
|
-
const V = this._faceVoltage(f), k0 = 1 + this.n + itf.reactions.length;
|
|
2015
|
-
itf.gates.forEach((gate, q) => {
|
|
2016
|
-
const o = this._gateSlot(f, q), x = this.u[o];
|
|
2017
|
-
const [a, da] = gateRate(gate.alpha, V), [b, db] = gateRate(gate.beta, V);
|
|
2018
|
-
const store = Number.isFinite(dt) ? 1 / dt : 0, j = this.gateIndex[f] + q;
|
|
2019
|
-
res[R[o]] = store * (x - this.gateOld[j]) - (a * (1 - x) - b * x);
|
|
2020
|
-
this._j(bf, k0 + q, bf, k0 + q, store + a + b);
|
|
2021
|
-
const dV = -(da * (1 - x) - db * x) * VT; // per unit φ̂_R, and minus that per unit φ̂_L
|
|
2022
|
-
this._j(bf, k0 + q, bR, 0, dV);
|
|
2023
|
-
this._j(bf, k0 + q, bL, 0, -dV);
|
|
2024
|
-
});
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
// Scharfetter–Gummel with non-ideal statistics. The excess ex = ζ − ln(c/c_ref) acts as an
|
|
2028
|
-
// extra potential, linear along the segment like φ, so Δ = zΔφ̂ + Δex and
|
|
2029
|
-
// N = −(D/h)·B(Δ)·c_L·expm1(η_R − η_L),
|
|
2030
|
-
// still exactly zero at equilibrium. c_L and ex depend on every ζ at their node through K.
|
|
2031
|
-
_segmentConcentrated(s, bL, bR, mat, h, vel) {
|
|
2032
|
-
const R = this.rix;
|
|
2033
|
-
const { n, M, res, z, K } = this;
|
|
2034
|
-
const KL = s * n * n, KR = (s + 1) * n * n;
|
|
2035
|
-
for (let i = 0; i < n; i++) {
|
|
2036
|
-
if (!mat.present[i] || mat.D[i] === 0) continue;
|
|
2037
|
-
const r = 1 + i, zi = z[i];
|
|
2038
|
-
const { N, E, gBc, dNdd, fL, cR } = this._fluxConcentrated(s, bL, bR, mat, h, vel, i);
|
|
2039
|
-
res[R[bL * M + r]] += N;
|
|
2040
|
-
res[R[bR * M + r]] -= N;
|
|
2041
|
-
if (this.lin) {
|
|
2042
|
-
// In difference form (see DifferenceTerms): b on Δη_i and z_i·∂N/∂d on Δφ̂, and the rest
|
|
2043
|
-
// as side terms in each δη_j − z_j δφ̂ at either end (c_L and the excess potentials).
|
|
2044
|
-
const rL = R[bL * M + r], rR = R[bR * M + r], pL = R[bL * M], pR = R[bR * M];
|
|
2045
|
-
this.lin.segment(rL, rR, pL, pR, zi, 0, -gBc * (E + 1), zi * dNdd);
|
|
2046
|
-
for (let j = 0; j < n; j++) {
|
|
2047
|
-
const wL = fL * K[KL + i * n + j] - (j === i ? dNdd : 0);
|
|
2048
|
-
const wR = cR > 0 ? dNdd * ((j === i ? 1 : 0) - K[KR + i * n + j] / cR) : 0;
|
|
2049
|
-
if (wL !== 0) this.lin.side(rL, rR, R[bL * M + 1 + j], pL, z[j], wL);
|
|
2050
|
-
if (wR !== 0) this.lin.side(rL, rR, R[bR * M + 1 + j], pR, z[j], wR);
|
|
2051
|
-
}
|
|
2052
|
-
continue;
|
|
2053
|
-
}
|
|
2054
|
-
const { jL, jR } = this;
|
|
2055
|
-
for (let k = 0; k < M; k++) {
|
|
2056
|
-
if (jL[k] !== 0) {
|
|
2057
|
-
this._j(bL, r, bL, k, jL[k]);
|
|
2058
|
-
this._j(bR, r, bL, k, -jL[k]);
|
|
2059
|
-
}
|
|
2060
|
-
if (jR[k] !== 0) {
|
|
2061
|
-
this._j(bL, r, bR, k, jR[k]);
|
|
2062
|
-
this._j(bR, r, bR, k, -jR[k]);
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
// Species i's flux across segment s of a non-ideal region, toward +x, and its derivatives in
|
|
2069
|
-
// this.jL and this.jR (over the unknowns of the blocks bL and bR).
|
|
2070
|
-
_fluxConcentrated(s, bL, bR, mat, h, vel, i) {
|
|
2071
|
-
const { n, M, u, uLo, c, z, K, ex, jL, jR } = this;
|
|
2072
|
-
const gL = s, gR = s + 1, KL = gL * n * n, KR = gR * n * n, r = 1 + i, zi = z[i];
|
|
2073
|
-
const cL = c[gL * n + i], cR = c[gR * n + i];
|
|
2074
|
-
const g = mat.D[i] / h;
|
|
2075
|
-
const pe = (vel * h) / mat.D[i];
|
|
2076
|
-
const d = zi * (u[bR * M] - u[bL * M]) + ex[gR * n + i] - ex[gL * n + i] - pe;
|
|
2077
|
-
const deta = u[bR * M + r] - u[bL * M + r] + (uLo[bR * M + r] - uLo[bL * M + r]) - pe;
|
|
2078
|
-
const E = Math.expm1(deta);
|
|
2079
|
-
const B = bernoulli(d), Bp = bernoulliDerivative(d);
|
|
2080
|
-
const gBc = g * B * cL;
|
|
2081
|
-
const N = -gBc * E;
|
|
2082
|
-
const dNdd = -g * Bp * cL * E;
|
|
2083
|
-
// Left node: ∂N/∂ζ_Lj = −gE(B + B′)K_L,ij − dNdd·δ_ij, plus the direct η_L,i term.
|
|
2084
|
-
const fL = -g * E * (B + Bp);
|
|
2085
|
-
jL[0] = -fL * this._Kz(gL, i);
|
|
2086
|
-
for (let j = 0; j < n; j++) jL[1 + j] = fL * K[KL + i * n + j];
|
|
2087
|
-
jL[r] += -dNdd + gBc * (E + 1);
|
|
2088
|
-
// Right node: ∂N/∂ζ_Rj = dNdd·(δ_ij − K_R,ij/c_R), plus the direct η_R,i term.
|
|
2089
|
-
jR.fill(0);
|
|
2090
|
-
if (cR > 0) {
|
|
2091
|
-
const fR = -dNdd / cR;
|
|
2092
|
-
jR[0] = -fR * this._Kz(gR, i);
|
|
2093
|
-
for (let j = 0; j < n; j++) jR[1 + j] = fR * K[KR + i * n + j];
|
|
2094
|
-
} else {
|
|
2095
|
-
jR[0] = dNdd * zi; // K/c → δ as c → 0
|
|
2096
|
-
jR[r] -= dNdd;
|
|
2097
|
-
}
|
|
2098
|
-
jR[r] += dNdd - gBc * (E + 1);
|
|
2099
|
-
return { N, E, gBc, dNdd, fL, cR };
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
// Ohmic conduction in a metal, in mixed form. The flux J (slot 0 of the left node) obeys
|
|
2103
|
-
// η_R − η_L + J/g = 0, g = σ RT/(z²F² h) (Ohm's law, J = −g Δη),
|
|
2104
|
-
// and enters the two nodes' carrier balances as outflow and inflow.
|
|
2105
|
-
_segmentConductor(s, bL, bR, mat, h) {
|
|
2106
|
-
const R = this.rix;
|
|
2107
|
-
const { M, u, uLo, res, z } = this;
|
|
2108
|
-
const i = mat.conductor.i, r = 1 + i;
|
|
2109
|
-
const g = (mat.conductor.sigma * this.model.RT) / (z[i] * z[i] * FARADAY * FARADAY * h);
|
|
2110
|
-
const J = u[bL * M];
|
|
2111
|
-
res[R[bL * M]] = u[bR * M + r] - u[bL * M + r] + (uLo[bR * M + r] - uLo[bL * M + r]) + J / g;
|
|
2112
|
-
this._j(bL, 0, bR, r, 1);
|
|
2113
|
-
this._j(bL, 0, bL, r, -1);
|
|
2114
|
-
this._j(bL, 0, bL, 0, 1 / g);
|
|
2115
|
-
res[R[bL * M + r]] += J;
|
|
2116
|
-
res[R[bR * M + r]] -= J;
|
|
2117
|
-
this._j(bL, r, bL, 0, 1);
|
|
2118
|
-
this._j(bR, r, bL, 0, -1);
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
// Eddy mixing: N = −(D_mix/RT) P ∇μ̄ with P = C − (Cz)(Cz)ᵀ/(zᵀCz), C = diag(c). It mixes
|
|
2122
|
-
// composition without carrying current (zᵀP = 0) and vanishes exactly at equilibrium. On a
|
|
2123
|
-
// segment, N_i = −(D_mix/h) Σ_j P̄_ij Δη_j, with P̄ from the logarithmic mean of each c, so a
|
|
2124
|
-
// neutral species gets exactly −D_mix Δc/h. Only mobile species (D > 0) take part.
|
|
2125
|
-
_segmentMixing(s, bL, bR, Dm, h, mat) {
|
|
2126
|
-
const R = this.rix;
|
|
2127
|
-
const { n, M, u, uLo, res, c, z } = this;
|
|
2128
|
-
const gL = s, gR = s + 1, k0 = Dm / h;
|
|
2129
|
-
const on = mat.mobile ?? (mat.mobile = [...Array(n).keys()].filter((i) => mat.present[i] && mat.D[i] > 0));
|
|
2130
|
-
if (on.length === 0) return;
|
|
2131
|
-
const w = this.mixWork ?? (this.mixWork = [0, 1, 2, 3].map(() => new Float64Array(n)));
|
|
2132
|
-
const [cb, dLa, dLb, de] = w;
|
|
2133
|
-
let S = 0, Q = 0;
|
|
2134
|
-
for (const k of on) {
|
|
2135
|
-
const a = c[gL * n + k], b = c[gR * n + k];
|
|
2136
|
-
// l = ln(c_R/c_L), from the compensated potentials where statistics are ideal
|
|
2137
|
-
const l = this.nodeIdeal[gL]
|
|
2138
|
-
? u[bR * M + 1 + k] - u[bL * M + 1 + k] + (uLo[bR * M + 1 + k] - uLo[bL * M + 1 + k]) - z[k] * (u[bR * M] - u[bL * M])
|
|
2139
|
-
: Math.log(b / a);
|
|
2140
|
-
// L = a·f(l), f = expm1(l)/l; ∂L/∂a = f − f′, ∂L/∂b = a f′/b
|
|
2141
|
-
let f, fp;
|
|
2142
|
-
if (Math.abs(l) < 1e-3) {
|
|
2143
|
-
f = 1 + l / 2 + (l * l) / 6 + (l * l * l) / 24;
|
|
2144
|
-
fp = 0.5 + l / 3 + (l * l) / 8 + (l * l * l) / 30;
|
|
2145
|
-
} else {
|
|
2146
|
-
const em = Math.expm1(l);
|
|
2147
|
-
f = em / l;
|
|
2148
|
-
fp = (l * (em + 1) - em) / (l * l);
|
|
2149
|
-
}
|
|
2150
|
-
cb[k] = a * f;
|
|
2151
|
-
dLa[k] = f - fp;
|
|
2152
|
-
dLb[k] = b > 0 ? (a * fp) / b : 0.5;
|
|
2153
|
-
de[k] = u[bR * M + 1 + k] - u[bL * M + 1 + k] + (uLo[bR * M + 1 + k] - uLo[bL * M + 1 + k]);
|
|
2154
|
-
S += z[k] * z[k] * cb[k];
|
|
2155
|
-
Q += z[k] * cb[k] * de[k];
|
|
2156
|
-
}
|
|
2157
|
-
const q = S > 0 ? Q / S : 0;
|
|
2158
|
-
const { jL, jR } = this;
|
|
2159
|
-
const dcL = this.dA, dcR = this.dB; // ∂c_k/∂slot at each end
|
|
2160
|
-
for (const i of on) {
|
|
2161
|
-
const r = 1 + i;
|
|
2162
|
-
const N = -k0 * cb[i] * (de[i] - z[i] * q);
|
|
2163
|
-
jL.fill(0);
|
|
2164
|
-
jR.fill(0);
|
|
2165
|
-
for (const j of on) {
|
|
2166
|
-
// direct dependence on Δη_j
|
|
2167
|
-
const P = cb[i] * ((i === j ? 1 : 0) - (S > 0 ? (z[i] * z[j] * cb[j]) / S : 0));
|
|
2168
|
-
jL[1 + j] += k0 * P;
|
|
2169
|
-
jR[1 + j] -= k0 * P;
|
|
2170
|
-
}
|
|
2171
|
-
// through c̄_k: ∂a_i/∂c̄_k = δ_ik (Δη_i − z_i q) − c̄_i z_i (z_k Δη_k − q z_k²)/S
|
|
2172
|
-
for (const k of on) {
|
|
2173
|
-
let da = (i === k ? de[i] - z[i] * q : 0) - (S > 0 ? (cb[i] * z[i] * (z[k] * de[k] - q * z[k] * z[k])) / S : 0);
|
|
2174
|
-
if (da === 0) continue;
|
|
2175
|
-
da *= -k0;
|
|
2176
|
-
this._dc(gL, k, dcL);
|
|
2177
|
-
this._dc(gR, k, dcR);
|
|
2178
|
-
for (let t = 0; t < M; t++) {
|
|
2179
|
-
jL[t] += da * dLa[k] * dcL[t];
|
|
2180
|
-
jR[t] += da * dLb[k] * dcR[t];
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
res[R[bL * M + r]] += N;
|
|
2184
|
-
res[R[bR * M + r]] -= N;
|
|
2185
|
-
for (let t = 0; t < M; t++) {
|
|
2186
|
-
if (jL[t] !== 0) {
|
|
2187
|
-
this._j(bL, r, bL, t, jL[t]);
|
|
2188
|
-
this._j(bR, r, bL, t, -jL[t]);
|
|
2189
|
-
}
|
|
2190
|
-
if (jR[t] !== 0) {
|
|
2191
|
-
this._j(bL, r, bR, t, jR[t]);
|
|
2192
|
-
this._j(bR, r, bR, t, -jR[t]);
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
// ∂c_k/∂slot at node g into d (slot 0 = φ̂, 1 + j = η_j).
|
|
2199
|
-
_dc(g, k, d) {
|
|
2200
|
-
const { n } = this;
|
|
2201
|
-
d.fill(0);
|
|
2202
|
-
if (this.nodeIdeal[g]) {
|
|
2203
|
-
const ck = this.c[g * n + k];
|
|
2204
|
-
d[1 + k] = ck;
|
|
2205
|
-
d[0] = -this.z[k] * ck;
|
|
2206
|
-
return;
|
|
2207
|
-
}
|
|
2208
|
-
const o = g * n * n + k * n;
|
|
2209
|
-
for (let j = 0; j < n; j++) d[1 + j] = this.K[o + j];
|
|
2210
|
-
d[0] = -this._Kz(g, k);
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
// A bulk reaction's rate at node g (mol/(m³·s), forward), as assembled: for the solution.
|
|
2214
|
-
bulkRate(rx, g) {
|
|
2215
|
-
const { n, M, u, uLo, c } = this, b = this.blockOfNode[g];
|
|
2216
|
-
let aHi = rx.fixedA, aLo = 0;
|
|
2217
|
-
for (let p = 0; p < rx.sp.length; p++) {
|
|
2218
|
-
aHi += rx.nu[p] * u[b * M + 1 + rx.sp[p]];
|
|
2219
|
-
aLo += rx.nu[p] * uLo[b * M + 1 + rx.sp[p]];
|
|
2220
|
-
}
|
|
2221
|
-
const law = rx.srh?.[this.nodeMaterial[g]];
|
|
2222
|
-
if (law) return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], aHi + aLo, ...this._srhDegen(g, g, law)).rate;
|
|
2223
|
-
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g];
|
|
2224
|
-
for (let p = 0; p < rx.sp.length; p++) if (rx.nu[p] > 0) P *= rx.nu[p] === 1 ? c[g * n + rx.sp[p]] : powi(c[g * n + rx.sp[p]], rx.nu[p]);
|
|
2225
|
-
return P * -Math.expm1(-(aHi + aLo));
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
// A bulk reaction's forward one-way rate at node g (mol/(m³·s)); its backward one is that
|
|
2229
|
-
// times e^{−a}, so their difference is bulkRate's.
|
|
2230
|
-
bulkForward(rx, g) {
|
|
2231
|
-
const { n, M, u, uLo, c } = this, b = this.blockOfNode[g];
|
|
2232
|
-
const law = rx.srh?.[this.nodeMaterial[g]];
|
|
2233
|
-
if (law) {
|
|
2234
|
-
let a = rx.fixedA;
|
|
2235
|
-
for (let p = 0; p < rx.sp.length; p++) a += rx.nu[p] * (u[b * M + 1 + rx.sp[p]] + uLo[b * M + 1 + rx.sp[p]]);
|
|
2236
|
-
return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], a, ...this._srhDegen(g, g, law)).forward;
|
|
2237
|
-
}
|
|
2238
|
-
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g];
|
|
2239
|
-
for (let p = 0; p < rx.sp.length; p++) if (rx.nu[p] > 0) P *= rx.nu[p] === 1 ? c[g * n + rx.sp[p]] : powi(c[g * n + rx.sp[p]], rx.nu[p]);
|
|
2240
|
-
return P;
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
// The larger of a bulk reaction's two one-way rates at node g (mol/(m³·s)): the scale its net
|
|
2244
|
-
// rate is read against (in equilibrium the net rate is their round-off).
|
|
2245
|
-
bulkOneWay(rx, g) {
|
|
2246
|
-
const { n, M, u, uLo, c } = this, b = this.blockOfNode[g];
|
|
2247
|
-
let a = rx.fixedA;
|
|
2248
|
-
for (let p = 0; p < rx.sp.length; p++) a += rx.nu[p] * (u[b * M + 1 + rx.sp[p]] + uLo[b * M + 1 + rx.sp[p]]);
|
|
2249
|
-
const law = rx.srh?.[this.nodeMaterial[g]];
|
|
2250
|
-
if (law) return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], a, ...this._srhDegen(g, g, law)).oneWay;
|
|
2251
|
-
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g];
|
|
2252
|
-
for (let p = 0; p < rx.sp.length; p++) if (rx.nu[p] > 0) P *= rx.nu[p] === 1 ? c[g * n + rx.sp[p]] : powi(c[g * n + rx.sp[p]], rx.nu[p]);
|
|
2253
|
-
return P * Math.max(1, Math.exp(-a));
|
|
2254
|
-
}
|
|
2255
|
-
|
|
2256
|
-
// SRH kinetics (see srhLaw in device.js): the rate, and its sensitivities to ln n, ln p and
|
|
2257
|
-
// a = A/RT. With no n₁ given, the trap is midgap: n₁ = p₁ = n_i = √(n p e^{−a}).
|
|
2258
|
-
// SRH at electron and hole concentrations cn, cp and affinity a. n₁p₁ = n p e^{−a} (n_i² for
|
|
2259
|
-
// Boltzmann carriers), and under degeneracy each carrier's emission scales with its own
|
|
2260
|
-
// γ = e^{−ex} (its excess chemical potential ex, c/K = ∂ln c/∂ζ its slope): n₁ = γ_n n₁₀ and
|
|
2261
|
-
// p₁ = γ_p p₁₀, n₁₀ the law's n1, or n_i at midgap. (γ_n γ_p split evenly onto both, as it was,
|
|
2262
|
-
// put a degenerate n⁺ region's γ_n on its holes' emission too.) The rate's derivatives in ln n,
|
|
2263
|
-
// ln p and a. For large negative a (strong extraction), np·(1 − e^{−a}) as −n p e^{−a}.
|
|
2264
|
-
_srhRate(law, cn, cp, a, xn = 0, xp = 0, sn = 1, sp = 1) {
|
|
2265
|
-
const { tn, tp } = law, lnnp = Math.log(cn) + Math.log(cp);
|
|
2266
|
-
let lnn1, kN = 0, kP = 0, kA = 0; // ln n₁ = kN ln n + kP ln p + kA a + const
|
|
2267
|
-
if (Number.isNaN(law.n1)) {
|
|
2268
|
-
lnn1 = (lnnp - a - xn + xp) / 2; // γ_n n_i, with n_i² = n p e^{−a}/(γ_n γ_p)
|
|
2269
|
-
kN = (2 - sn) / 2;
|
|
2270
|
-
kP = sp / 2;
|
|
2271
|
-
kA = -0.5;
|
|
2272
|
-
} else {
|
|
2273
|
-
lnn1 = Math.log(law.n1) - xn;
|
|
2274
|
-
kN = 1 - sn;
|
|
2275
|
-
}
|
|
2276
|
-
const n1 = Math.exp(lnn1), p1 = Math.exp(lnnp - a - lnn1), np = cn * cp;
|
|
2277
|
-
const den = tp * (cn + n1) + tn * (cp + p1);
|
|
2278
|
-
// np(1 − e^{−a}), with e^{−a} folded into np where it would overflow
|
|
2279
|
-
const f = -Math.expm1(-a), num = a > -700 ? np * f : np - Math.exp(lnnp - a);
|
|
2280
|
-
const rate = num / den;
|
|
2281
|
-
const dDn = tp * cn + tp * n1 * kN + tn * p1 * (1 - kN);
|
|
2282
|
-
const dDp = tp * n1 * kP + tn * cp + tn * p1 * (1 - kP);
|
|
2283
|
-
const dDa = tp * n1 * kA + tn * p1 * (-1 - kA);
|
|
2284
|
-
return { rate, forward: np / den, oneWay: a > -700 ? (np / den) * Math.max(1, Math.exp(-a)) : Math.exp(lnnp - a) / den, Cn: rate * (1 - dDn / den), Cp: rate * (1 - dDp / den), Ca: (a > -700 ? (np / den) * (1 - f) : Math.exp(lnnp - a) / den) - (rate * dDa) / den };
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
// _srhRate's degeneracy arguments: the electrons' at node gn, the holes' at gq.
|
|
2288
|
-
_srhDegen(gn, gq, law) {
|
|
2289
|
-
const [xn, sn] = this._degen(gn, law.n.i), [xp, sp] = this._degen(gq, law.p.i);
|
|
2290
|
-
return [xn, xp, sn, sp];
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
// A carrier's excess chemical potential at node g and ∂ln c/∂ζ there (0 and 1 if ideal).
|
|
2294
|
-
_degen(g, i) {
|
|
2295
|
-
if (this.nodeIdeal[g]) return [0, 1];
|
|
2296
|
-
const n = this.n, c = this.c[g * n + i], K = this.K[g * n * n + i * n + i];
|
|
2297
|
-
return [this.ex[g * n + i], c > 0 && K > 0 ? c / K : 1];
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
// A face reaction (see normalizeFaceReactions). Its rate r_k is an unknown of the face block,
|
|
2301
|
-
// with the row r_k − rate(u_L, u_R) = 0, and each participant's edge node takes ν·r_k (made
|
|
2302
|
-
// there when ν > 0). The rate couples the two edge nodes only through the face block between
|
|
2303
|
-
// them, which keeps the system block-tridiagonal.
|
|
2304
|
-
_faceReaction(rx, k, f, bf, bL, bR, Af) {
|
|
2305
|
-
const { n, M, u, uLo, c, res } = this;
|
|
2306
|
-
const R = this.rix;
|
|
2307
|
-
const grid = this.model.grid;
|
|
2308
|
-
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1];
|
|
2309
|
-
const al = rx.alpha;
|
|
2310
|
-
const dL = this.dA.fill(0), dR = this.dB.fill(0); // ∂ ln(prefactor)/∂slot, per side
|
|
2311
|
-
let pref = rx.k0, aHi = rx.fixedA, aLo = 0, rate;
|
|
2312
|
-
for (const p of rx.part) {
|
|
2313
|
-
const g = p.side ? gR : gL, b = p.side ? bR : bL, o = b * M + 1 + p.i;
|
|
2314
|
-
aHi -= p.nu * u[o];
|
|
2315
|
-
aLo -= p.nu * uLo[o];
|
|
2316
|
-
if (rx.srh || rx.vmax || this.nodeConductor[g] === p.i) continue; // a conductor's carrier has activity 1
|
|
2317
|
-
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
2318
|
-
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
2319
|
-
this._dlnc(g, p.i, e, p.side ? dR : dL);
|
|
2320
|
-
}
|
|
2321
|
-
if (rx.vmax) {
|
|
2322
|
-
// Saturating: r = vmax Π (c/(c + K))^{|ν|} (1 − e^{−a}); ∂ln S/∂ln c = |ν| K/(c + K).
|
|
2323
|
-
let S = 1;
|
|
2324
|
-
rx.part.forEach((p, q) => {
|
|
2325
|
-
if (p.nu >= 0) return;
|
|
2326
|
-
const g = p.side ? gR : gL, cc = c[g * n + p.i], K = rx.K[q];
|
|
2327
|
-
S *= powi(cc / (cc + K), -p.nu);
|
|
2328
|
-
this._dlnc(g, p.i, (-p.nu * K) / (cc + K), p.side ? dR : dL);
|
|
2329
|
-
});
|
|
2330
|
-
const a = aHi + aLo, back = Math.exp(-a);
|
|
2331
|
-
rate = rx.vmax * S * -Math.expm1(-a);
|
|
2332
|
-
for (let s = 0; s < M; s++) {
|
|
2333
|
-
dL[s] *= rate;
|
|
2334
|
-
dR[s] *= rate;
|
|
2335
|
-
}
|
|
2336
|
-
for (const p of rx.part) (p.side ? dR : dL)[1 + p.i] -= rx.vmax * S * back * p.nu;
|
|
2337
|
-
} else if (rx.srh) {
|
|
2338
|
-
// SRH, with n and p each at its own side's edge node.
|
|
2339
|
-
const { n: pn, p: pp } = rx.srh, gn = pn.side ? gR : gL, gq = pp.side ? gR : gL;
|
|
2340
|
-
const s = this._srhRate(rx.srh, c[gn * n + pn.i], c[gq * n + pp.i], aHi + aLo, ...this._srhDegen(gn, gq, { n: pn, p: pp }));
|
|
2341
|
-
rate = s.rate;
|
|
2342
|
-
this._dlnc(gn, pn.i, s.Cn, pn.side ? dR : dL);
|
|
2343
|
-
this._dlnc(gq, pp.i, s.Cp, pp.side ? dR : dL);
|
|
2344
|
-
for (const p of rx.part) (p.side ? dR : dL)[1 + p.i] -= s.Ca * p.nu;
|
|
2345
|
-
} else {
|
|
2346
|
-
const { g: bv, gp } = bvFactor(aHi + aLo, al);
|
|
2347
|
-
rate = pref * bv;
|
|
2348
|
-
// ∂rate/∂slot = rate·∂ln(prefactor) + pref·g′·∂a, with ∂a/∂η_i = −ν
|
|
2349
|
-
for (let s = 0; s < M; s++) {
|
|
2350
|
-
dL[s] *= rate;
|
|
2351
|
-
dR[s] *= rate;
|
|
2352
|
-
}
|
|
2353
|
-
for (const p of rx.part) (p.side ? dR : dL)[1 + p.i] -= pref * gp * p.nu;
|
|
2354
|
-
}
|
|
2355
|
-
const slot = 1 + n + k, o = bf * M + slot;
|
|
2356
|
-
res[R[o]] = u[o] - rate;
|
|
2357
|
-
this._j(bf, slot, bf, slot, 1);
|
|
2358
|
-
for (let s = 0; s < M; s++) {
|
|
2359
|
-
if (dL[s] !== 0) this._j(bf, slot, bL, s, -dL[s]);
|
|
2360
|
-
if (dR[s] !== 0) this._j(bf, slot, bR, s, -dR[s]);
|
|
2361
|
-
}
|
|
2362
|
-
for (const p of rx.part) {
|
|
2363
|
-
const b = p.side ? bR : bL;
|
|
2364
|
-
res[R[b * M + 1 + p.i]] -= Af * p.nu * u[o];
|
|
2365
|
-
this._j(b, 1 + p.i, bf, slot, -Af * p.nu);
|
|
2366
|
-
}
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
// The terminals whose ∂res/∂V (termB) and ∂I/∂x (termC) are kept, exactly: the floating ones,
|
|
2370
|
-
// which the Newton solve borders on, or all of them (allTerminals: for the impedance, which reads
|
|
2371
|
-
// its terminal's). A held terminal's are otherwise left as they fall (written but not kept
|
|
2372
|
-
// consistent), which spares a device with dozens of ports (an axon's nodes of Ranvier) the cost
|
|
2373
|
-
// of carrying each one's two dense vectors through every assembly.
|
|
2374
|
-
_liveTerms() {
|
|
2375
|
-
if (this.allTerminals) return this._allTerms ?? (this._allTerms = this.terms.map((_, k) => k));
|
|
2376
|
-
return this.floating;
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
|
-
// Every terminal's terms: its voltage held or floating, how the residual depends on it
|
|
2380
|
-
// (termB), its current into the device (termI) and how that depends on the state (termC).
|
|
2381
|
-
_terminals(dt) {
|
|
2382
|
-
this._refreshSources();
|
|
2383
|
-
for (const k of this._liveTerms()) {
|
|
2384
|
-
this.termB[k].fill(0);
|
|
2385
|
-
this.termC[k].fill(0);
|
|
2386
|
-
}
|
|
2387
|
-
this.termDI.fill(0);
|
|
2388
|
-
for (const x of this.termDX) x.fill(0);
|
|
2389
|
-
this.termI.fill(0);
|
|
2390
|
-
this.qg.fill(0);
|
|
2391
|
-
this.qgSlope.fill(0);
|
|
2392
|
-
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt, false));
|
|
2393
|
-
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt, true));
|
|
2394
|
-
this._contact('left', dt);
|
|
2395
|
-
this._contact('right', dt);
|
|
2396
|
-
for (const k of this.floating) this._circuit(k);
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
// A floating terminal's circuit law: I − I_set = 0 (driven by a current), or I − (V_src − V)/R = 0
|
|
2400
|
-
// (a source behind a resistance).
|
|
2401
|
-
_circuit(k) {
|
|
2402
|
-
const d = this.terms[k].drive, src = sourceAt(d.src, this.sourceTime, this.sourceBefore);
|
|
2403
|
-
if (d.kind === 'I') this.termRes[k] = this.termI[k] - src;
|
|
2404
|
-
else {
|
|
2405
|
-
this.termRes[k] = this.termI[k] - (src - this.termV[k]) / d.R;
|
|
2406
|
-
this.termDI[k] += 1 / d.R;
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
// into += w × (the Jacobian row of block b, slot s), over compact columns.
|
|
2411
|
-
_captureRow(b, s, w, into) {
|
|
2412
|
-
const l = this.loc[b * this.M + s];
|
|
2413
|
-
if (l < 0 || w === 0) return;
|
|
2414
|
-
if (this.lin) this.lin.capture(this.rix[b * this.M + s], w, this.lin.rate, into);
|
|
2415
|
-
const { sys, nB } = this, { sizes, offA, offB, offC, offX } = sys, m = sizes[b];
|
|
2416
|
-
for (const [X, nb, off] of [['A', b - 1, offA], ['B', b, offB], ['C', b + 1, offC]]) {
|
|
2417
|
-
if (nb < 0 || nb >= nB) continue;
|
|
2418
|
-
const mc = sizes[nb], o = off[b] + l * mc, arr = sys[X], c0 = offX[nb];
|
|
2419
|
-
for (let c = 0; c < mc; c++) if (arr[o + c] !== 0) into[c0 + c] += w * arr[o + c];
|
|
2420
|
-
}
|
|
2421
|
-
return m;
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
// An internal port (terminal k): its exchange with each node of its window, as a source per
|
|
2425
|
-
// volume. A held ('equilibrium') level replaces the node's balance row; the source is then that
|
|
2426
|
-
// row's residual, read just before. Its current into the device is Σ z F × the sources. In two
|
|
2427
|
-
// passes over the ports: everything else, then the held levels (`holds`), so that a level reads
|
|
2428
|
-
// every other port's terms at its nodes, and none is added into its row after.
|
|
2429
|
-
_port(port, flux, k, dt, holds) {
|
|
2430
|
-
const R = this.rix;
|
|
2431
|
-
const { n, M, u, uLo, res, z, VT } = this;
|
|
2432
|
-
const F = FARADAY, vol = this.model.grid.vol;
|
|
2433
|
-
const B = this.termB[k], C = this.termC[k];
|
|
2434
|
-
if (!holds) flux.fill(0);
|
|
2435
|
-
for (let i = 0; i < n; i++) {
|
|
2436
|
-
const link = port.species[i];
|
|
2437
|
-
if (link.type === 'blocked' || (link.type === 'equilibrium') !== holds) continue;
|
|
2438
|
-
const r = 1 + i, target = this.portEta(port, i), zF = z[i] * F;
|
|
2439
|
-
for (const g of port.nodes) {
|
|
2440
|
-
const b = this.blockOfNode[g], o = b * M + r, v = vol[g];
|
|
2441
|
-
const deta = target - u[o] - uLo[o]; // (μ̄_out − μ̄)/RT
|
|
2442
|
-
if (link.type === 'equilibrium') {
|
|
2443
|
-
// a device end node's level is its contact's business, where the contact links it
|
|
2444
|
-
if ((g === 0 && this.model.contacts.left.species[i].type !== 'blocked') || (g === this.nNodes - 1 && this.model.contacts.right.species[i].type !== 'blocked')) continue;
|
|
2445
|
-
flux[i] += res[R[o]];
|
|
2446
|
-
this._captureRow(b, r, zF, C);
|
|
2447
|
-
this._replaceRow(b, r);
|
|
2448
|
-
for (const Bk of this.termB) Bk[R[o]] = 0; // (other ports' terms in the row replaced)
|
|
2449
|
-
this._j(b, r, b, r, 1);
|
|
2450
|
-
res[R[o]] = -deta;
|
|
2451
|
-
B[R[o]] += -z[i] / VT;
|
|
2452
|
-
continue;
|
|
2453
|
-
}
|
|
2454
|
-
// conductance: s = G V_T (η_out − η)/(z² F); exchange: s = k (η_out − η). Gated, G times
|
|
2455
|
-
// Π x^p, the gates at this node (each taken in [0, 1]).
|
|
2456
|
-
const kk0 = link.type === 'conductance' ? (link.G * VT) / (z[i] * z[i] * F) : link.k;
|
|
2457
|
-
const gated = link.gates ?? [], q0 = 1 + n + port.surface.length;
|
|
2458
|
-
const open = gated.length ? this._open(gated, (q) => u[b * M + q0 + q]) : 1, kk = kk0 * open;
|
|
2459
|
-
res[R[o]] -= v * kk * deta;
|
|
2460
|
-
this._j(b, r, b, r, v * kk);
|
|
2461
|
-
flux[i] += v * kk * deta;
|
|
2462
|
-
if (z[i] !== 0) {
|
|
2463
|
-
B[R[o]] += (-v * kk * z[i]) / VT;
|
|
2464
|
-
C[R[o]] += -zF * v * kk;
|
|
2465
|
-
this.termDI[k] += (zF * v * kk * z[i]) / VT;
|
|
2466
|
-
}
|
|
2467
|
-
for (const [q, p] of gated) {
|
|
2468
|
-
const d = this._dOpen(gated, q, p, (qq) => u[b * M + q0 + qq]);
|
|
2469
|
-
if (d === 0) continue;
|
|
2470
|
-
this._j(b, r, b, q0 + q, -v * kk0 * deta * d);
|
|
2471
|
-
if (z[i] !== 0) C[R[b * M + q0 + q]] += zF * v * kk0 * deta * d;
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
}
|
|
2475
|
-
if (holds) {
|
|
2476
|
-
for (let i = 0; i < n; i++) this.termI[k] += z[i] * F * flux[i];
|
|
2477
|
-
return;
|
|
2478
|
-
}
|
|
2479
|
-
port.reactions.forEach((rx, x) => this._portReaction(port, rx, this.portArea[k - 2], this.portRates[k - 2][x], flux, k));
|
|
2480
|
-
if (port.capacitance) this._portCapacitance(port, k, dt);
|
|
2481
|
-
if (port.gates.length > 0) this._portGates(port, k, dt);
|
|
2482
|
-
if (port.surface.length > 0 && Number.isFinite(dt)) this._surfaceStorage(port, dt);
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
// A port's reaction at each node of its window: Butler–Volmer per area, as at a face, against
|
|
2486
|
-
// the electrode's carrier at the port's level (activity 1), times the electrode's area per
|
|
2487
|
-
// volume. Each node's rate is explicit (no unknown of its own): it enters the balances of the
|
|
2488
|
-
// species it makes and consumes, and the port's current.
|
|
2489
|
-
_portReaction(port, rx, area, rates, flux, k) {
|
|
2490
|
-
const { n, M, u, uLo, c, res, z, VT, nSurf, th } = this, R = this.rix, F = FARADAY, vol = this.model.grid.vol, RT = this.model.RT;
|
|
2491
|
-
const B = this.termB[k], C = this.termC[k], al = rx.alpha, d = this.dA, surf = port.surface;
|
|
2492
|
-
// The carrier's part of the affinity, and how it moves with the port's voltage.
|
|
2493
|
-
let aV = rx.fixedA, daV = 0, q = 0; // q: charge the forward reaction brings into the device, per event
|
|
2494
|
-
for (const p of rx.part) {
|
|
2495
|
-
if (p.side === 1) {
|
|
2496
|
-
aV -= p.nu * this.portEta(port, p.i);
|
|
2497
|
-
daV -= (p.nu * z[p.i]) / VT;
|
|
2498
|
-
} else if (p.side === 0) q += p.nu * z[p.i];
|
|
2499
|
-
}
|
|
2500
|
-
const slot = (p) => (p.side === 2 ? 1 + n + p.s : 1 + p.i);
|
|
2501
|
-
port.nodes.forEach((g, w) => {
|
|
2502
|
-
const b = this.blockOfNode[g], s = vol[g] * area[w];
|
|
2503
|
-
d.fill(0);
|
|
2504
|
-
let pref = rx.k0, aHi = aV, aLo = 0;
|
|
2505
|
-
for (const p of rx.part) {
|
|
2506
|
-
if (p.side === 1) continue;
|
|
2507
|
-
const o = b * M + slot(p);
|
|
2508
|
-
aHi -= p.nu * u[o];
|
|
2509
|
-
aLo -= p.nu * uLo[o];
|
|
2510
|
-
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
2511
|
-
if (p.side === 2) {
|
|
2512
|
-
// a surface species at its coverage θ = θ₀e^ζ, with ∂ln θ_s/∂η_x = δ_sx − θ_x
|
|
2513
|
-
pref *= powr(th[g * nSurf + p.s], e);
|
|
2514
|
-
d[1 + n + p.s] += e;
|
|
2515
|
-
for (let x = 0; x < surf.length; x++) d[1 + n + x] -= e * th[g * nSurf + x];
|
|
2516
|
-
} else {
|
|
2517
|
-
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
2518
|
-
this._dlnc(g, p.i, e, d);
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
if (rx.sites) {
|
|
2522
|
-
// the bare sites its transition state holds: θ₀ = 1 − Σθ, with ∂ln θ₀/∂η_s = −θ_s
|
|
2523
|
-
for (let x = 0; x < surf.length; x++) d[1 + n + x] -= rx.sites * th[g * nSurf + x];
|
|
2524
|
-
pref *= powr(this.th0[g], rx.sites);
|
|
2525
|
-
}
|
|
2526
|
-
const { g: bv, gp } = bvFactor(aHi + aLo, al);
|
|
2527
|
-
const rate = pref * bv;
|
|
2528
|
-
rates[w] = rate;
|
|
2529
|
-
// ∂rate/∂slot = rate·∂ln(prefactor) + pref·g′·∂a, with ∂a/∂η = −ν
|
|
2530
|
-
for (let t = 0; t < M; t++) d[t] *= rate;
|
|
2531
|
-
for (const p of rx.part) if (p.side !== 1) d[slot(p)] -= pref * gp * p.nu;
|
|
2532
|
-
const dV = pref * gp * daV;
|
|
2533
|
-
for (const p of rx.part) {
|
|
2534
|
-
if (p.side === 1) continue;
|
|
2535
|
-
// the region's species per volume of the window; a surface's per area of electrode
|
|
2536
|
-
const row = slot(p), o = b * M + row, f = p.side === 2 ? 1 : s;
|
|
2537
|
-
res[R[o]] -= f * p.nu * rate;
|
|
2538
|
-
if (p.side === 0) flux[p.i] += s * p.nu * rate;
|
|
2539
|
-
for (let t = 0; t < M; t++) if (d[t] !== 0) this._j(b, row, b, t, -f * p.nu * d[t]);
|
|
2540
|
-
B[R[o]] -= f * p.nu * dV;
|
|
2541
|
-
}
|
|
2542
|
-
for (let t = 0; t < M; t++) if (d[t] !== 0) C[R[b * M + t]] += q * F * s * d[t];
|
|
2543
|
-
this.termDI[k] += q * F * s * dV;
|
|
2544
|
-
});
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
|
-
// A capacitance spread through a port's window: per area of electrode, σ = C (V − zeroCharge − φ)
|
|
2548
|
-
// on the port's side, so each node's charge balance (its φ row) gains vol·a·σ, and the port
|
|
2549
|
-
// passes the charging current d(Σ vol·a·σ)/dt (none in a steady state). An end node whose φ its
|
|
2550
|
-
// contact sets is left to the contact.
|
|
2551
|
-
// A membrane port's gates, at each node of its window: (x − x_old)/dt = α(1 − x) − βx in the
|
|
2552
|
-
// voltage across its capacitance there, φ − (V − zeroCharge).
|
|
2553
|
-
_portGates(port, k, dt) {
|
|
2554
|
-
const { n, res, VT } = this, R = this.rix, B = this.termB[k], store = Number.isFinite(dt) ? 1 / dt : 0, q0 = 1 + n + port.surface.length;
|
|
2555
|
-
port.nodes.forEach((g, w) => {
|
|
2556
|
-
const b = this.blockOfNode[g], V = this._portVoltage(k - 2, g);
|
|
2557
|
-
port.gates.forEach((gate, q) => {
|
|
2558
|
-
const o = b * this.M + q0 + q, x = this.u[o], j = this.portGateIndex[k - 2] + w * port.gates.length + q;
|
|
2559
|
-
const [a, da] = gateRate(gate.alpha, V), [bb, db] = gateRate(gate.beta, V);
|
|
2560
|
-
res[R[o]] = store * (x - this.gateOld[j]) - (a * (1 - x) - bb * x);
|
|
2561
|
-
this._j(b, q0 + q, b, q0 + q, store + a + bb);
|
|
2562
|
-
const s = -(da * (1 - x) - db * x); // ∂/∂V_m, with V_m = V_T φ̂ − V + zeroCharge
|
|
2563
|
-
this._j(b, q0 + q, b, 0, s * VT);
|
|
2564
|
-
B[R[o]] += -s;
|
|
2565
|
-
});
|
|
2566
|
-
});
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
// Π x^p over a link's gates (each x taken in [0, 1]), and its derivative in gate q.
|
|
2570
|
-
_open(gated, x) {
|
|
2571
|
-
let open = 1;
|
|
2572
|
-
for (const [q, p] of gated) open *= powi(Math.min(1, Math.max(0, x(q))), p);
|
|
2573
|
-
return open;
|
|
2574
|
-
}
|
|
2575
|
-
_dOpen(gated, q, p, x) {
|
|
2576
|
-
const xq = x(q);
|
|
2577
|
-
if (!(xq > 0 && xq < 1)) return 0;
|
|
2578
|
-
let others = 1;
|
|
2579
|
-
for (const [q2, p2] of gated) if (q2 !== q) others *= powi(Math.min(1, Math.max(0, x(q2))), p2);
|
|
2580
|
-
return p * powi(xq, p - 1) * others;
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
_portCapacitance(port, k, dt) {
|
|
2584
|
-
const { M, u, uLo, res, VT } = this, R = this.rix, vol = this.model.grid.vol, area = this.portArea[k - 2];
|
|
2585
|
-
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
2586
|
-
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
2587
|
-
// Its current, the charging (Q − Q at the step's start)/dt; or, steady at no current, the
|
|
2588
|
-
// charge it keeps in place of that (see captureLaws in src/lawRows.js).
|
|
2589
|
-
const rate = Number.isFinite(dt) ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
2590
|
-
let Q = 0, dQdV = 0;
|
|
2591
|
-
port.nodes.forEach((g, w) => {
|
|
2592
|
-
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
2593
|
-
const b = this.blockOfNode[g], s = vol[g] * area[w] * Cs;
|
|
2594
|
-
const q = s * (V - zeroCharge - VT * (u[b * M] + uLo[b * M]));
|
|
2595
|
-
this.qg[g] += area[w] * Cs * (V - zeroCharge - VT * (u[b * M] + uLo[b * M]));
|
|
2596
|
-
this.qgSlope[g] += area[w] * Cs;
|
|
2597
|
-
Q += q;
|
|
2598
|
-
dQdV += s;
|
|
2599
|
-
res[R[b * M]] -= q;
|
|
2600
|
-
this._j(b, 0, b, 0, s * VT);
|
|
2601
|
-
B[R[b * M]] -= s;
|
|
2602
|
-
if (rate > 0) Ct[R[b * M]] += -s * VT * rate;
|
|
2603
|
-
});
|
|
2604
|
-
this.portQ[k - 2] = Q;
|
|
2605
|
-
if (rate > 0) {
|
|
2606
|
-
this.termI[k] += (Q - (dt < Infinity ? this.portQStart[k - 2] : this.chargeRefs.get(k))) * rate;
|
|
2607
|
-
this.termDI[k] += dQdV * rate;
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2610
|
-
|
|
2611
|
-
// An electrode surface's storage, per area of electrode: Γ (θ_s − θ_s,old)/dt in each surface
|
|
2612
|
-
// species' row, with ∂θ_s/∂η_x = θ_s (δ_sx − θ_x).
|
|
2613
|
-
_surfaceStorage(port, dt) {
|
|
2614
|
-
const { n, M, res, nSurf, th, thOld } = this, R = this.rix, surf = port.surface, G = surf[0].capacity;
|
|
2615
|
-
for (const g of port.nodes) {
|
|
2616
|
-
const b = this.blockOfNode[g];
|
|
2617
|
-
// Near full coverage θ − θ_old cancels (both ~1, the change ~θ₀): the dominant species'
|
|
2618
|
-
// change comes from the small ones instead, Δθ = −Δθ₀ − Σ_others Δθ.
|
|
2619
|
-
let top = 0;
|
|
2620
|
-
for (let q = 1; q < surf.length; q++) if (th[g * nSurf + q] > th[g * nSurf + top]) top = q;
|
|
2621
|
-
for (let q = 0; q < surf.length; q++) {
|
|
2622
|
-
const tq = th[g * nSurf + q];
|
|
2623
|
-
let change = tq - thOld[g * nSurf + q];
|
|
2624
|
-
if (q === top && tq > 0.5) {
|
|
2625
|
-
change = -(this.th0[g] - this.th0Old[g]);
|
|
2626
|
-
for (let x = 0; x < surf.length; x++) if (x !== q) change -= th[g * nSurf + x] - thOld[g * nSurf + x];
|
|
2627
|
-
}
|
|
2628
|
-
res[R[b * M + 1 + n + q]] += (G * change) / dt;
|
|
2629
|
-
for (let x = 0; x < surf.length; x++) this._j(b, 1 + n + q, b, 1 + n + x, ((G * tq) / dt) * ((q === x ? 1 : 0) - th[g * nSurf + x]));
|
|
2630
|
-
}
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
// One contact (terminal 0 or 1): record the flux through its outer face and the current into
|
|
2635
|
-
// the device (read from the end box before anything here is added: whatever the box needs
|
|
2636
|
-
// comes through the contact), then add its exchange terms, its equilibrium links and its φ law.
|
|
2637
|
-
_contact(side, dt) {
|
|
2638
|
-
const R = this.rix;
|
|
2639
|
-
const { model, n, M, u, uLo, res, c, z, VT } = this;
|
|
2640
|
-
const F = FARADAY;
|
|
2641
|
-
const ct = model.contacts[side];
|
|
2642
|
-
const k = side === 'left' ? 0 : 1, Vt = this.termV[k];
|
|
2643
|
-
const B = this.termB[k], C = this.termC[k];
|
|
2644
|
-
const g = side === 'left' ? 0 : this.nNodes - 1;
|
|
2645
|
-
const b = this.blockOfNode[g];
|
|
2646
|
-
const sgn = side === 'left' ? 1 : -1;
|
|
2647
|
-
const flux = this.contactFlux[side];
|
|
2648
|
-
const Ac = model.grid.area[g]; // the links' laws are per area: what they pass is that times A here
|
|
2649
|
-
const dyn = Number.isFinite(dt);
|
|
2650
|
-
|
|
2651
|
-
// Before anything is added, each balance residual is the flux into the device here. (A
|
|
2652
|
-
// closed end solved in charge rows has its ions' storage left out until later: none of them
|
|
2653
|
-
// passes, so their flux is zero.)
|
|
2654
|
-
const closedIons = this.combining && dyn && this.chargeNode[g] === 1;
|
|
2655
|
-
let I = 0;
|
|
2656
|
-
const held = this.portHeldEnd.subarray(k * n, (k + 1) * n);
|
|
2657
|
-
for (let i = 0; i < n; i++) {
|
|
2658
|
-
const active = this.loc[b * M + 1 + i] >= 0 && !(closedIons && z[i] !== 0) && !held[i];
|
|
2659
|
-
const nIn = active ? res[R[b * M + 1 + i]] : 0;
|
|
2660
|
-
flux[i] = sgn * nIn; // (toward +x)
|
|
2661
|
-
if (active && z[i] !== 0) {
|
|
2662
|
-
I += z[i] * F * nIn;
|
|
2663
|
-
this._captureRow(b, 1 + i, z[i] * F, C);
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
// The ions' storage, left out until now (see lateStorage): in each balance, and in what
|
|
2667
|
-
// each species brings in, but not in the current.
|
|
2668
|
-
if (this.combining && dyn && this.lateStorage[g] === 1) {
|
|
2669
|
-
const v = model.grid.vol[g], { cOld } = this;
|
|
2670
|
-
for (let i = 0; i < n; i++) {
|
|
2671
|
-
if (z[i] === 0 || this.loc[b * M + 1 + i] < 0 || held[i]) continue;
|
|
2672
|
-
const ci = c[g * n + i], st = (v * (ci - cOld[g * n + i])) / dt;
|
|
2673
|
-
res[R[b * M + 1 + i]] += st;
|
|
2674
|
-
flux[i] += sgn * st;
|
|
2675
|
-
this._j(b, 1 + i, b, 1 + i, (v * ci) / dt);
|
|
2676
|
-
this._j(b, 1 + i, b, 0, (-v * z[i] * ci) / dt);
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
|
|
2680
|
-
// Conductance links: J (toward the device) = G (V_out − V_i), V_out = V_t + offset; exchange
|
|
2681
|
-
// links (neutral species): N_in = k (μ_out − μ)/RT; velocity links, v (c_eq − c).
|
|
2682
|
-
for (let i = 0; i < n; i++) {
|
|
2683
|
-
const link = ct.species[i];
|
|
2684
|
-
const o = b * M + 1 + i;
|
|
2685
|
-
if (link.type === 'exchange') {
|
|
2686
|
-
res[R[o]] -= Ac * link.k * (link.mu / model.RT - u[o] - uLo[o]);
|
|
2687
|
-
this._j(b, 1 + i, b, 1 + i, Ac * link.k);
|
|
2688
|
-
} else if (link.type === 'conductance') {
|
|
2689
|
-
const Vi = (VT * (u[o] + uLo[o])) / z[i];
|
|
2690
|
-
res[R[o]] -= (Ac * link.G * (Vt + link.offset - Vi)) / (z[i] * F);
|
|
2691
|
-
this._j(b, 1 + i, b, 1 + i, (Ac * link.G * VT) / (z[i] * z[i] * F));
|
|
2692
|
-
B[R[o]] += (-Ac * link.G) / (z[i] * F);
|
|
2693
|
-
} else if (link.type === 'velocity') {
|
|
2694
|
-
// N_in = v c (e^(η_out − η) − 1): v (c_eq − c), c_eq in equilibrium with the outside.
|
|
2695
|
-
const ci = c[g * n + i], E = Math.exp(Math.min(this.contactEta(side, i) - u[o] - uLo[o], 700));
|
|
2696
|
-
const N = Ac * link.v * ci * (E - 1), d = this.dA;
|
|
2697
|
-
res[R[o]] -= N;
|
|
2698
|
-
d.fill(0);
|
|
2699
|
-
this._dlnc(g, i, 1, d);
|
|
2700
|
-
for (let s = 0; s <= n; s++) if (d[s] !== 0) this._j(b, 1 + i, b, s, -N * d[s]);
|
|
2701
|
-
this._j(b, 1 + i, b, 1 + i, Ac * link.v * ci * E);
|
|
2702
|
-
if (z[i] !== 0) B[R[o]] += (-Ac * link.v * ci * E * z[i]) / VT;
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
// Equilibrium links: Dirichlet on the known outside level.
|
|
2707
|
-
for (let i = 0; i < n; i++) {
|
|
2708
|
-
const link = ct.species[i];
|
|
2709
|
-
if (link.type !== 'equilibrium') continue;
|
|
2710
|
-
const o = b * M + 1 + i;
|
|
2711
|
-
this._replaceRow(b, 1 + i);
|
|
2712
|
-
this._j(b, 1 + i, b, 1 + i, 1);
|
|
2713
|
-
res[R[o]] = u[o] - this.contactEta(side, i) + uLo[o]; // (the big terms first: u + uLo rounds to u)
|
|
2714
|
-
if (z[i] !== 0) B[R[o]] += -z[i] / VT;
|
|
2715
|
-
}
|
|
2716
|
-
|
|
2717
|
-
// φ law (none where φ is undefined: nothing at the end node responds to it). The displacement
|
|
2718
|
-
// into the device, D_in = sgn·D, adds (D_in − D_in at the step's start)/dt to the current.
|
|
2719
|
-
const link = ct.phi, Dstart = sgn * this.contactDStart[side];
|
|
2720
|
-
// (In a steady solve, what a law in this circuit row counts of D_in: see applyConstraints in
|
|
2721
|
-
// src/lawRows.js.)
|
|
2722
|
-
const q = !dyn && this.steady && this.contactLaw[k] ? (this.contactQ[k] ??= { row: new Float64Array(this.sys.size + 1), D: 0, dV: 0 }) : null;
|
|
2723
|
-
if (q) {
|
|
2724
|
-
q.row.fill(0);
|
|
2725
|
-
q.D = 0;
|
|
2726
|
-
q.dV = 0;
|
|
2727
|
-
}
|
|
2728
|
-
if (this.phiUndefined[g]) {
|
|
2729
|
-
this.contactD[side] = 0;
|
|
2730
|
-
} else if (link.type === 'capacitive') {
|
|
2731
|
-
// Gate, or metal across a Stern layer, at φ_g = V_t − zeroCharge. D toward +x.
|
|
2732
|
-
const Din = Ac * link.C * (Vt - link.zeroCharge - VT * u[b * M]);
|
|
2733
|
-
res[R[b * M]] -= Din;
|
|
2734
|
-
this._j(b, 0, b, 0, Ac * link.C * VT);
|
|
2735
|
-
B[R[b * M]] += -Ac * link.C;
|
|
2736
|
-
this.contactD[side] = sgn * Din;
|
|
2737
|
-
if (q && this.loc[b * M] >= 0) [q.D, q.row[R[b * M]], q.dV] = [Din, -Ac * link.C * VT, Ac * link.C];
|
|
2738
|
-
// Its current, the charging (D_in − D_in at the step's start)/dt; or, steady at no current,
|
|
2739
|
-
// the charge it keeps in place of that (see captureLaws in src/lawRows.js).
|
|
2740
|
-
const rate = dyn ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
2741
|
-
if (rate > 0) {
|
|
2742
|
-
I += (Din - (dyn ? Dstart : this.chargeRefs.get(k))) * rate;
|
|
2743
|
-
C[R[b * M]] += -Ac * link.C * VT * rate;
|
|
2744
|
-
this.termDI[k] += Ac * link.C * rate;
|
|
2745
|
-
}
|
|
2746
|
-
} else if (link.type === 'pinned' || link.type === 'bulk') {
|
|
2747
|
-
// The Poisson residual is the outside's charge, D_in.
|
|
2748
|
-
const Din = res[R[b * M]];
|
|
2749
|
-
this.contactD[side] = sgn * Din;
|
|
2750
|
-
if (q) {
|
|
2751
|
-
q.D = Din;
|
|
2752
|
-
this._captureRow(b, 0, 1, q.row);
|
|
2753
|
-
}
|
|
2754
|
-
if (dyn && !(this.combining && this.lateStorage[g] === 1)) {
|
|
2755
|
-
I += (Din - Dstart) / dt;
|
|
2756
|
-
this._captureRow(b, 0, 1 / dt, C);
|
|
2757
|
-
} else if (!dyn && this.steady && this.chargeRefs.has(k)) {
|
|
2758
|
-
I += Din - this.chargeRefs.get(k);
|
|
2759
|
-
this._captureRow(b, 0, 1, C);
|
|
2760
|
-
}
|
|
2761
|
-
this._replaceRow(b, 0);
|
|
2762
|
-
if (link.type === 'pinned') {
|
|
2763
|
-
// φ_edge = V_t − zeroCharge (the C → ∞ limit).
|
|
2764
|
-
this._j(b, 0, b, 0, 1);
|
|
2765
|
-
res[R[b * M]] = u[b * M] - (Vt - link.zeroCharge) / VT + uLo[b * M];
|
|
2766
|
-
B[R[b * M]] += -1 / VT;
|
|
2767
|
-
} else {
|
|
2768
|
-
// Plain bulk: local neutrality.
|
|
2769
|
-
let q = this.rhoFixed[g], dq = 0;
|
|
2770
|
-
for (let i = 0; i < n; i++) {
|
|
2771
|
-
const kk = g * n + i;
|
|
2772
|
-
if (!this.present[kk]) continue;
|
|
2773
|
-
const Kz = this._Kz(g, i);
|
|
2774
|
-
q += F * z[i] * c[kk];
|
|
2775
|
-
dq -= F * z[i] * Kz;
|
|
2776
|
-
if (Kz !== 0) this._j(b, 0, b, 1 + i, F * Kz);
|
|
2777
|
-
}
|
|
2778
|
-
this._j(b, 0, b, 0, dq);
|
|
2779
|
-
res[R[b * M]] = q;
|
|
2780
|
-
}
|
|
2781
|
-
} else {
|
|
2782
|
-
this.contactD[side] = 0;
|
|
2783
|
-
}
|
|
2784
|
-
this.termI[k] = I;
|
|
2785
|
-
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
// Zero one row of the Jacobian (all three blocks), ready to be replaced.
|
|
2791
|
-
_replaceRow(b, rs) {
|
|
2792
|
-
const l = this.loc[b * this.M + rs];
|
|
2793
|
-
if (l < 0) return;
|
|
2794
|
-
if (this.lin) this.lin.kill(this.rix[b * this.M + rs]);
|
|
2795
|
-
const { sys, nB } = this, sz = sys.sizes, m = sz[b];
|
|
2796
|
-
sys.B.fill(0, sys.offB[b] + l * m, sys.offB[b] + (l + 1) * m);
|
|
2797
|
-
if (b > 0) sys.A.fill(0, sys.offA[b] + l * sz[b - 1], sys.offA[b] + (l + 1) * sz[b - 1]);
|
|
2798
|
-
if (b < nB - 1) sys.C.fill(0, sys.offC[b] + l * sz[b + 1], sys.offC[b] + (l + 1) * sz[b + 1]);
|
|
2799
|
-
}
|
|
2800
|
-
|
|
2801
|
-
// Scale every row by its largest Jacobian entry (in place, residual too), dividing by it: its
|
|
2802
|
-
// reciprocal overflows where it's subnormal (a co-ion crowded out of a double layer, ~1e-310).
|
|
2803
|
-
_equilibrate() {
|
|
2804
|
-
const { nB, sys, res, termB } = this, live = this._liveTerms();
|
|
2805
|
-
const { A, B, C, sizes, offA, offB, offC, offX } = sys;
|
|
2806
|
-
for (let b = 0; b < nB; b++) {
|
|
2807
|
-
const m = sizes[b], mp = b > 0 ? sizes[b - 1] : 0, mn = b < nB - 1 ? sizes[b + 1] : 0;
|
|
2808
|
-
for (let r = 0; r < m; r++) {
|
|
2809
|
-
const oa = offA[b] + r * mp, ob = offB[b] + r * m, oc = offC[b] + r * mn;
|
|
2810
|
-
let mx = 0;
|
|
2811
|
-
for (let k = 0; k < mp; k++) mx = Math.max(mx, Math.abs(A[oa + k]));
|
|
2812
|
-
for (let k = 0; k < m; k++) mx = Math.max(mx, Math.abs(B[ob + k]));
|
|
2813
|
-
for (let k = 0; k < mn; k++) mx = Math.max(mx, Math.abs(C[oc + k]));
|
|
2814
|
-
if (mx === 0) {
|
|
2815
|
-
this.rowDiv[offX[b] + r] = 1;
|
|
2816
|
-
continue;
|
|
2817
|
-
}
|
|
2818
|
-
const d = mx;
|
|
2819
|
-
this.rowDiv[offX[b] + r] = d;
|
|
2820
|
-
for (let k = 0; k < mp; k++) A[oa + k] /= d;
|
|
2821
|
-
for (let k = 0; k < m; k++) B[ob + k] /= d;
|
|
2822
|
-
for (let k = 0; k < mn; k++) C[oc + k] /= d;
|
|
2823
|
-
res[offX[b] + r] /= d;
|
|
2824
|
-
for (const k of live) termB[k][offX[b] + r] /= d; // (each terminal's ∂/∂V)
|
|
2825
|
-
}
|
|
2826
|
-
}
|
|
2827
|
-
}
|
|
2828
|
-
|
|
2829
|
-
// The Jacobian at dt with the dilute kernels' terms kept aside in difference form (see
|
|
2830
|
-
// DifferenceTerms), the rest assembled into a matrix of its own. The residual and terminal terms
|
|
2831
|
-
// come out as from assemble(dt); the Newton matrix is left as it was.
|
|
2832
|
-
_assembleDifference(dt) {
|
|
2833
|
-
const keep = this.sys;
|
|
2834
|
-
const rest = this.sysRest ?? (this.sysRest = new BlockTridiagonal(keep.n, keep.sizes));
|
|
2835
|
-
const n = this.n, nN = this.nNodes;
|
|
2836
|
-
const lin = this.diffTerms ?? (this.diffTerms = new DifferenceTerms(keep.size, nN * n * n, nN * n, 2 * nN * n * n));
|
|
2837
|
-
this.sys = rest;
|
|
2838
|
-
this.lin = lin;
|
|
2839
|
-
try {
|
|
2840
|
-
this.assemble(dt);
|
|
2841
|
-
} finally {
|
|
2842
|
-
this.sys = keep;
|
|
2843
|
-
this.lin = null;
|
|
2844
|
-
}
|
|
2845
|
-
return { rest, lin };
|
|
2846
|
-
}
|
|
2847
|
-
|
|
2848
|
-
// A Newton solve refined by GMRES: the factorised Jacobian as the preconditioner, and J·x
|
|
2849
|
-
// exact, with the dilute kernels' terms in difference form (see DifferenceTerms), as the
|
|
2850
|
-
// impedance does. The assembled J holds a flux's dependence on η at its two ends as two
|
|
2851
|
-
// entries; where they're huge (an inversion layer's 0.1 nm cells, a conductance ~1e11 against
|
|
2852
|
-
// the layer's own storage and the trickle from the bulk), eliminating them loses the layer's
|
|
2853
|
-
// overall level to round-off, and Newton rattles there. delta and deltaV come in as the plain
|
|
2854
|
-
// solve's and leave refined (in the transformed unknowns, as solveBordered gives them); false
|
|
2855
|
-
// if refining didn't help, leaving them as they came.
|
|
2856
|
-
_refine(dt, delta, deltaV) {
|
|
2857
|
-
const { res, termRes, termB, termC, termDI, rowDiv, floating: fl } = this;
|
|
2858
|
-
const N = this.sys.size, K = fl.length, pins = this.pins, R = this.rix;
|
|
2859
|
-
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: this.termDX.map((x) => Float64Array.from(x)), transformed: this.transformed, pinRes: this.pins.map((p) => p.res) };
|
|
2860
|
-
const restoreArrays = () => {
|
|
2861
|
-
res.set(save.res);
|
|
2862
|
-
termRes.set(save.termRes);
|
|
2863
|
-
save.termB.forEach((b, k) => termB[k].set(b));
|
|
2864
|
-
save.termC.forEach((c, k) => termC[k].set(c));
|
|
2865
|
-
termDI.set(save.termDI);
|
|
2866
|
-
save.termDX.forEach((x, k) => this.termDX[k].set(x));
|
|
2867
|
-
this.transformed = save.transformed;
|
|
2868
|
-
this.pins.forEach((p, a) => (p.res = save.pinRes[a]));
|
|
2869
|
-
};
|
|
2870
|
-
// The same system, its terms kept apart: the rest, and each floating terminal's (unscaled).
|
|
2871
|
-
const { rest, lin } = this._assembleDifference(dt);
|
|
2872
|
-
const Bk = fl.map((k) => Float64Array.from(termB[k])), Ck = fl.map((k) => Float64Array.from(termC[k])), Dk = fl.map((k) => termDI[k]);
|
|
2873
|
-
restoreArrays();
|
|
2874
|
-
const v = new Float64Array(N + 1), jv = new Float64Array(N);
|
|
2875
|
-
const op = (x) => {
|
|
2876
|
-
for (let j = 0; j < N; j++) v[j] = x[j];
|
|
2877
|
-
v[N] = 0;
|
|
2878
|
-
rest.multiply(v, jv);
|
|
2879
|
-
if (save.transformed) this._untransform(v); // (the dilute terms in the plain unknowns)
|
|
2880
|
-
lin.apply(v, jv, 1, 1 / dt);
|
|
2881
|
-
// A flat stretch's rows hold each level to its first node's (see flatRows in src/lawRows.js).
|
|
2882
|
-
for (const p of pins) {
|
|
2883
|
-
if (!p.col) continue;
|
|
2884
|
-
for (const o of p.col) if (o !== p.row && this.loc[o] >= 0) jv[R[o]] -= x[R[p.row]];
|
|
2885
|
-
}
|
|
2886
|
-
const out = new Float64Array(N + K);
|
|
2887
|
-
for (let j = 0; j < N; j++) {
|
|
2888
|
-
let t = jv[j];
|
|
2889
|
-
for (let a = 0; a < K; a++) t += Bk[a][j] * x[N + a];
|
|
2890
|
-
out[j] = t / rowDiv[j];
|
|
2891
|
-
}
|
|
2892
|
-
for (let a = 0; a < K; a++) {
|
|
2893
|
-
let t = Dk[a] * x[N + a];
|
|
2894
|
-
for (let b = 0; b < K; b++) if (b !== a) t += this.termDX[fl[a]][fl[b]] * x[N + b];
|
|
2895
|
-
for (let j = 0; j < N; j++) t += Ck[a][j] * x[j];
|
|
2896
|
-
out[N + a] = t;
|
|
2897
|
-
}
|
|
2898
|
-
// A pinned row is its constraint's (a conserved amount, an island's summed balance).
|
|
2899
|
-
for (const p of pins) {
|
|
2900
|
-
let t = 0;
|
|
2901
|
-
for (let j = 0; j < p.len; j++) t += p.w[j] * x[p.idx[j]];
|
|
2902
|
-
out[R[p.row]] = t;
|
|
2903
|
-
}
|
|
2904
|
-
return precondition(out);
|
|
2905
|
-
};
|
|
2906
|
-
const rhs = new Float64Array(N + 1), out = new Float64Array(N + 1), outV = new Float64Array(K);
|
|
2907
|
-
const precondition = (r) => {
|
|
2908
|
-
for (let j = 0; j < N; j++) rhs[j] = r[j];
|
|
2909
|
-
fl.forEach((k, a) => (termRes[k] = r[N + a]));
|
|
2910
|
-
for (const p of pins) {
|
|
2911
|
-
p.res = rhs[R[p.row]];
|
|
2912
|
-
rhs[R[p.row]] = 0;
|
|
2913
|
-
}
|
|
2914
|
-
solveBordered(this, rhs, out, outV, pins);
|
|
2915
|
-
const z = new Float64Array(N + K);
|
|
2916
|
-
for (let j = 0; j < N; j++) z[j] = out[j];
|
|
2917
|
-
for (let a = 0; a < K; a++) z[N + a] = outV[a];
|
|
2918
|
-
return z;
|
|
2919
|
-
};
|
|
2920
|
-
try {
|
|
2921
|
-
const b = new Float64Array(N + K);
|
|
2922
|
-
for (let j = 0; j < N; j++) b[j] = save.res[j];
|
|
2923
|
-
fl.forEach((k, a) => (b[N + a] = save.termRes[k]));
|
|
2924
|
-
pins.forEach((p, a) => (b[R[p.row]] = save.pinRes[a]));
|
|
2925
|
-
const pb = precondition(b), x = Float64Array.from(pb);
|
|
2926
|
-
// A lost mode or two, GMRES finds in a couple of iterations. The refined update is kept
|
|
2927
|
-
// where GMRES converged and its correction is under a thermal unit; otherwise newton()
|
|
2928
|
-
// stops refining.
|
|
2929
|
-
const x0 = Float64Array.from(x);
|
|
2930
|
-
const ok = gmresReal(op, pb, x, { m: 8, restarts: 1, tol: 1e-8 });
|
|
2931
|
-
// (Measured on the potentials, as Newton's own step is: a rate the plain solve left
|
|
2932
|
-
// unsolved, at a perturbed pivot, can need a correction of 41.7 mol/(m²·s).)
|
|
2933
|
-
const dx = new Float64Array(N + 1), dV = new Float64Array(K);
|
|
2934
|
-
for (let j = 0; j < N; j++) dx[j] = x[j] - x0[j];
|
|
2935
|
-
for (let a = 0; a < K; a++) dV[a] = x[N + a] - x0[N + a];
|
|
2936
|
-
if (this.transformed) this._untransform(dx);
|
|
2937
|
-
const c = this._maxPotentialStep(dx, dV);
|
|
2938
|
-
if (!ok || !(c < 1)) return false;
|
|
2939
|
-
for (let j = 0; j < N; j++) delta[j] = x[j];
|
|
2940
|
-
for (let a = 0; a < K; a++) deltaV[a] = x[N + a];
|
|
2941
|
-
return true;
|
|
2942
|
-
} finally {
|
|
2943
|
-
restoreArrays();
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
|
|
2947
|
-
// Largest update among the potential-like unknowns (φ̂ and η at grid nodes).
|
|
2948
|
-
_maxPotentialStep(delta, deltaV = this.deltaV) { // (deltaV null: the device's potentials only)
|
|
2949
|
-
const { n, M } = this;
|
|
2950
|
-
const R = this.rix; // (the sink entry of delta is 0)
|
|
2951
|
-
let mx = 0;
|
|
2952
|
-
if (deltaV) for (let a = 0; a < this.floating.length; a++) mx = Math.max(mx, Math.abs(deltaV[a]) / this.VT);
|
|
2953
|
-
for (let g = 0; g < this.nNodes; g++) {
|
|
2954
|
-
const b = this.blockOfNode[g];
|
|
2955
|
-
if (this.nodeConductor[g] < 0) mx = Math.max(mx, Math.abs(delta[R[b * M]])); // (a metal's slot 0 is a flux)
|
|
2956
|
-
for (let i = 0; i < n; i++) {
|
|
2957
|
-
if (this.present[g * n + i]) mx = Math.max(mx, Math.abs(delta[R[b * M + 1 + i]]));
|
|
2958
|
-
}
|
|
2959
|
-
const k = this.surfPort[g]; // an electrode surface's coverages, as η
|
|
2960
|
-
if (k >= 0) for (let q = 0; q < this.model.ports[k].surface.length; q++) mx = Math.max(mx, Math.abs(delta[R[b * M + 1 + n + q]]));
|
|
2961
|
-
}
|
|
2962
|
-
for (const gt of this.gateList) mx = Math.max(mx, Math.abs(delta[R[gt.o]])); // a gate's fraction open
|
|
2963
|
-
return mx;
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
/**
|
|
2967
|
-
* Newton iteration for one backward-Euler step from the current cOld.
|
|
2968
|
-
* @returns {{converged: boolean, iterations: number, history: number[]}}
|
|
2969
|
-
*/
|
|
2970
|
-
newton(dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands = false } = {}) {
|
|
2971
|
-
const { delta, res } = this;
|
|
2972
|
-
const deltaV = this.deltaV ?? (this.deltaV = new Float64Array(this.floating.length));
|
|
2973
|
-
const history = [], ownHistory = [];
|
|
2974
|
-
// Refined solves (see _refine), once the plain ones stall.
|
|
2975
|
-
let refine = false, refined = 0, noRefine = false;
|
|
2976
|
-
// Islands (see applyIslands in src/lawRows.js): their summed rows go in only once the plain
|
|
2977
|
-
// solves converge.
|
|
2978
|
-
// Far from the solution, Newton exact along a weakly held level takes it by 1e5 thermal
|
|
2979
|
-
// units (a floating base, cold at bias), where the plain solves, which barely see that level,
|
|
2980
|
-
// converge. Converged, an island at the wrong level shows: it takes in a different current
|
|
2981
|
-
// than it passes on, so the terminal currents don't add up to zero. Then the summed rows go
|
|
2982
|
-
// in and Newton carries on from there. Converged again, a current that tiny against the
|
|
2983
|
-
// conduction around it rides on differences (~1e-28 thermal units for 5 fA/m² through a bulk
|
|
2984
|
-
// of 1e4 mol/m³ at 1e-4 m²/s) that the last update leaves known only to its own round-off:
|
|
2985
|
-
// where the currents still don't add up, one more update polishes them. Where the islands'
|
|
2986
|
-
// own system is held by static pivots elsewhere (a GaAs stack's minority carriers, at 1e-17
|
|
2987
|
-
// A/m² of noise), their rows can throw Newton off instead: if it doesn't converge again
|
|
2988
|
-
// within a dozen iterations, the plain solution stands.
|
|
2989
|
-
this.islandsOn = islands;
|
|
2990
|
-
let rounds = 0, kept = null, perturbed = false;
|
|
2991
|
-
// Converged by a small update, but solved at a perturbed pivot, or in a steady solve (where a
|
|
2992
|
-
// factorisation that lost a row's digits left a reaction's rate 3.6e7 off it, the update
|
|
2993
|
-
// small, an I = 0 electrode at a voltage 0.4 V from its steady one): the residual itself,
|
|
2994
|
-
// assembled at the state, must be small too (each row scaled by its largest Jacobian entry).
|
|
2995
|
-
const unsolved = () => {
|
|
2996
|
-
if (!perturbed && dt !== Infinity) return false;
|
|
2997
|
-
this.assemble(dt);
|
|
2998
|
-
this._equilibrate();
|
|
2999
|
-
let r = 0;
|
|
3000
|
-
for (let k = 0; k < this.SINK; k++) r = Math.max(r, Math.abs(this.res[k]));
|
|
3001
|
-
return !(r < 1e-6);
|
|
3002
|
-
};
|
|
3003
|
-
const islandsOff = (it, result) => {
|
|
3004
|
-
if (dt !== Infinity || !this.steady || this.islands.length === 0 || rounds >= 2 || !(this._kirchhoff() > 1e-9)) return false;
|
|
3005
|
-
this.islandsOn = true;
|
|
3006
|
-
rounds++;
|
|
3007
|
-
kept = { it, result, u: Float64Array.from(this.u), uLo: Float64Array.from(this.uLo), termV: Float64Array.from(this.termV) };
|
|
3008
|
-
return true;
|
|
3009
|
-
};
|
|
3010
|
-
// A steady solve that fails: record how nearly singular the system was, and where; or, failing
|
|
3011
|
-
// with the islands' rows in, go back to the solution without them.
|
|
3012
|
-
const fail = (r) => {
|
|
3013
|
-
if (kept) {
|
|
3014
|
-
this.u.set(kept.u);
|
|
3015
|
-
this.uLo.set(kept.uLo);
|
|
3016
|
-
this.termV.set(kept.termV);
|
|
3017
|
-
this.islandsOn = false;
|
|
3018
|
-
this.computeConcentrations();
|
|
3019
|
-
return { ...kept.result, iterations: r.iterations, history };
|
|
3020
|
-
}
|
|
3021
|
-
if (dt === Infinity) this._noteConditioning();
|
|
3022
|
-
return r;
|
|
3023
|
-
};
|
|
3024
|
-
for (let it = 1; it <= maxIter; it++) {
|
|
3025
|
-
if (kept && it - kept.it > 12) return fail({ converged: false, iterations: it - 1, history });
|
|
3026
|
-
if (kept && it === kept.it + 2) {
|
|
3027
|
-
// Refined from the islands' second update (their first moves the level, perhaps by
|
|
3028
|
-
// more than a refinement may), since the plain solves have shown a mode they lose.
|
|
3029
|
-
refine = true;
|
|
3030
|
-
noRefine = false;
|
|
3031
|
-
}
|
|
3032
|
-
try {
|
|
3033
|
-
this.assemble(dt);
|
|
3034
|
-
} catch (err) {
|
|
3035
|
-
// A statistics model can fail far from the solution (e.g. Debye–Hückel beyond its range).
|
|
3036
|
-
if (!(err instanceof SolverError)) throw err;
|
|
3037
|
-
return fail({ converged: false, iterations: it, history, error: err.message });
|
|
3038
|
-
}
|
|
3039
|
-
this._equilibrate();
|
|
3040
|
-
let rmax = 0;
|
|
3041
|
-
for (let k = 0; k < this.SINK; k++) rmax = Math.max(rmax, Math.abs(res[k]));
|
|
3042
|
-
try {
|
|
3043
|
-
this._factor();
|
|
3044
|
-
} catch (err) {
|
|
3045
|
-
return fail({ converged: false, iterations: it, history, error: err.message });
|
|
3046
|
-
}
|
|
3047
|
-
// A pivot perturbed (one that cancelled to exactly nothing) can leave a row unsolved, its
|
|
3048
|
-
// update small while its residual isn't: the same couple written twice on an electrode at
|
|
3049
|
-
// open circuit converged with one spelling's rate row 41.7 mol/(m²·s) off, the couple
|
|
3050
|
-
// 1.5 RT from equilibrium. Refine the solves (exact J·v) wherever one was, and before
|
|
3051
|
-
// taking a small update for convergence there, look at the residual itself (see unsolved).
|
|
3052
|
-
perturbed = this.sys.perturbedPivots > 0;
|
|
3053
|
-
if (perturbed && !noRefine) refine = true;
|
|
3054
|
-
try {
|
|
3055
|
-
const pins = this.pins;
|
|
3056
|
-
deltaV.fill(0);
|
|
3057
|
-
solveBordered(this, res, delta, deltaV, pins);
|
|
3058
|
-
if (refine && !this._refine(dt, delta, deltaV)) {
|
|
3059
|
-
refine = false; // (GMRES didn't converge, or strayed: the plain solves it is)
|
|
3060
|
-
noRefine = true;
|
|
3061
|
-
}
|
|
3062
|
-
if (this.transformed) this._untransform(delta);
|
|
3063
|
-
} catch (err) {
|
|
3064
|
-
if (!(err instanceof SolverError)) throw err;
|
|
3065
|
-
return fail({ converged: false, iterations: it, history, error: err.message });
|
|
3066
|
-
}
|
|
3067
|
-
// Convergence counts every unknown, floating terminal voltages too. Damping and the
|
|
3068
|
-
// divergence check count only the device's own potentials: a terminal voltage enters
|
|
3069
|
-
// linearly (conductance links, held levels, a capacitive face), so a large swing in one is
|
|
3070
|
-
// safe, and damping it would throttle the whole update (a current-driven port switching
|
|
3071
|
-
// off, its voltage collapsing by hundreds of volts, crawled 10 thermal units at a time).
|
|
3072
|
-
const step = this._maxPotentialStep(delta, deltaV);
|
|
3073
|
-
const own = this._maxPotentialStep(delta, null);
|
|
3074
|
-
history.push(step);
|
|
3075
|
-
ownHistory.push(own);
|
|
3076
|
-
if (!Number.isFinite(step)) return fail({ converged: false, iterations: it, history, error: 'non-finite update' });
|
|
3077
|
-
// Give up early on clear divergence; the caller will take a smaller step instead.
|
|
3078
|
-
if ((it > 1 && own > 1e4) || (it > 6 && own > 10 * ownHistory[0])) {
|
|
3079
|
-
this.computeConcentrations();
|
|
3080
|
-
return fail({ converged: false, iterations: it, history, error: 'diverging' });
|
|
3081
|
-
}
|
|
3082
|
-
const alpha = own > maxStep ? maxStep / own : 1;
|
|
3083
|
-
this._addToState(delta, -alpha);
|
|
3084
|
-
this.floating.forEach((k, a) => (this.termV[k] -= alpha * deltaV[a]));
|
|
3085
|
-
// A time step's Newton contracts fast from its predictor (1e-2, 1e-9, 1e-15 thermal
|
|
3086
|
-
// units). The next update would be about θ = step/(the last one) times this one, so where
|
|
3087
|
-
// θ/(1 − θ) of it (Hairer and Wanner's estimate) is under tol, this update converged it:
|
|
3088
|
-
// the iteration that would only show that is skipped, a third of a typical step's work.
|
|
3089
|
-
const last = history[history.length - 2], theta = step / last;
|
|
3090
|
-
const contracted = dt !== Infinity && it >= 2 && step < 1e-6 && theta < 0.1 && (theta / (1 - theta)) * step < tol;
|
|
3091
|
-
if (alpha === 1 && (step < tol || contracted)) {
|
|
3092
|
-
if (unsolved()) return fail({ converged: false, iterations: it, history, error: 'a row left unsolved at a pivot that cancelled exactly' });
|
|
3093
|
-
const done = { converged: true, iterations: it, history, residual: rmax };
|
|
3094
|
-
if (islandsOff(it, done)) continue;
|
|
3095
|
-
this.computeConcentrations();
|
|
3096
|
-
return done;
|
|
3097
|
-
}
|
|
3098
|
-
// Converged as far as round-off allows: the updates are already tiny (below 1e-6 thermal
|
|
3099
|
-
// units, ~26 nV) and have stopped shrinking. A badly conditioned system's floor can sit
|
|
3100
|
-
// above tol: a strictly neutral material on a short step, where φ is fixed only through
|
|
3101
|
-
// fluxes that the storage term dwarfs, rattles at ~1e-9 after converging quadratically.
|
|
3102
|
-
// (Raising this floor lets a weakly held population drift: a MOS capacitor without a
|
|
3103
|
-
// channel port then showed a DC leak through its oxide.)
|
|
3104
|
-
const [p1, p2] = [history[history.length - 2], history[history.length - 3]], floor = 1e-6;
|
|
3105
|
-
// Stalled near the solution (not shrinking quadratically), or growing undamped twice
|
|
3106
|
-
// running: the factorised Jacobian may have lost a slow mode to cancellation, and
|
|
3107
|
-
// round-off's floor would accept it wrong, or its noise keep Newton from the solution (a
|
|
3108
|
-
// bipolar stack's floating base, its holes held ~1e14 more weakly than they move within
|
|
3109
|
-
// it). Refine the solves from here.
|
|
3110
|
-
if (refine) refined++;
|
|
3111
|
-
else if (!noRefine && alpha === 1 && it >= 3 && step > 0.25 * p1 && (step < 1e-4 || (step > p1 && p1 > p2))) refine = true;
|
|
3112
|
-
if (alpha === 1 && it >= 4 && (!refine || refined >= 1 || noRefine) && step < floor && p1 < floor && step > 0.25 * p1 && p1 > 0.25 * p2) {
|
|
3113
|
-
if (unsolved()) return fail({ converged: false, iterations: it, history, error: 'a row left unsolved at a pivot that cancelled exactly' });
|
|
3114
|
-
const done = { converged: true, iterations: it, history, residual: rmax, roundoff: true };
|
|
3115
|
-
if (islandsOff(it, done)) continue;
|
|
3116
|
-
this.computeConcentrations();
|
|
3117
|
-
return done;
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
3120
|
-
this.computeConcentrations();
|
|
3121
|
-
return fail({ converged: false, iterations: maxIter, history });
|
|
3122
|
-
}
|
|
3123
|
-
|
|
3124
|
-
// How far the terminal currents (as last assembled) are from adding up to zero, relative to the
|
|
3125
|
-
// largest of them: Kirchhoff's law, which a steady state keeps.
|
|
3126
|
-
_kirchhoff() {
|
|
3127
|
-
let sum = 0, mx = 0;
|
|
3128
|
-
for (const I of this.termI) {
|
|
3129
|
-
sum += I;
|
|
3130
|
-
mx = Math.max(mx, Math.abs(I));
|
|
3131
|
-
}
|
|
3132
|
-
return mx > 0 ? Math.abs(sum) / mx : 0;
|
|
3133
|
-
}
|
|
3134
|
-
|
|
3135
|
-
// How many digits the last factorisation lost to cancellation, and where (the worst seen since
|
|
3136
|
-
// the steady solve began). Only for diagnosing failures: a saturated species, for instance,
|
|
3137
|
-
// can lose digits harmlessly.
|
|
3138
|
-
_noteConditioning() {
|
|
3139
|
-
let c;
|
|
3140
|
-
try {
|
|
3141
|
-
c = this.sys.cancellation();
|
|
3142
|
-
} catch {
|
|
3143
|
-
return;
|
|
3144
|
-
}
|
|
3145
|
-
if (!(c.digits > (this.conditioning?.digits ?? -1))) return;
|
|
3146
|
-
const { grid } = this.model;
|
|
3147
|
-
let x = NaN, where = '', nodes = [];
|
|
3148
|
-
for (let g = 0; g < this.nNodes; g++) {
|
|
3149
|
-
if (this.blockOfNode[g] !== c.block) continue;
|
|
3150
|
-
x = grid.x[g];
|
|
3151
|
-
where = this.model.regions[grid.nodeRegion[g]].name;
|
|
3152
|
-
nodes = [g];
|
|
3153
|
-
}
|
|
3154
|
-
for (let f = 0; f < this.nFaces; f++) {
|
|
3155
|
-
if (this.blockOfFace[f] !== c.block) continue;
|
|
3156
|
-
x = grid.x[grid.regionEnd[f]];
|
|
3157
|
-
where = `interfaces[${f}]`;
|
|
3158
|
-
nodes = [grid.regionEnd[f], grid.regionEnd[f] + 1];
|
|
3159
|
-
}
|
|
3160
|
-
this.conditioning = { digits: c.digits, x, where, nodes };
|
|
3161
|
-
}
|
|
3162
|
-
|
|
3163
|
-
/**
|
|
3164
|
-
* Advance by dt, splitting the interval into halves (recursively) wherever Newton fails.
|
|
3165
|
-
* The end time is always t + dt; the result reports the substeps taken.
|
|
3166
|
-
*/
|
|
3167
|
-
// A time step, halved until it converges (src/timeSteps.js).
|
|
3168
|
-
advance(dt, opts, depth = 0) {
|
|
3169
|
-
return advanceFn(this, dt, opts, depth);
|
|
3170
|
-
}
|
|
3171
|
-
|
|
3172
|
-
// u += scale·d in compensated arithmetic (Knuth two-sum, then renormalise hi/lo).
|
|
3173
|
-
_addToState(d, scale) {
|
|
3174
|
-
const { u, uLo, fullOf } = this;
|
|
3175
|
-
for (let j = 0; j < fullOf.length; j++) {
|
|
3176
|
-
const k = fullOf[j], a = u[k], b = scale * d[j];
|
|
3177
|
-
const s = a + b, bb = s - a;
|
|
3178
|
-
const err = a - (s - bb) + (b - bb);
|
|
3179
|
-
const lo = uLo[k] + err;
|
|
3180
|
-
const hi = s + lo;
|
|
3181
|
-
u[k] = hi;
|
|
3182
|
-
uLo[k] = lo - (hi - s);
|
|
3183
|
-
}
|
|
3184
|
-
}
|
|
3185
|
-
|
|
3186
|
-
// One time step (src/timeSteps.js).
|
|
3187
|
-
step(dt, opts = {}) {
|
|
3188
|
-
return stepFn(this, dt, opts);
|
|
3189
|
-
}
|
|
3190
|
-
|
|
3191
|
-
// Adaptive time stepping to tEnd (src/timeSteps.js).
|
|
3192
|
-
integrate(tEnd, opts = {}) {
|
|
3193
|
-
return integrateFn(this, tEnd, opts);
|
|
3194
|
-
}
|
|
3195
|
-
|
|
3196
|
-
// Probes for a transient's trace: { x, species, quantity: 'c' | 'V', region } read after every
|
|
3197
|
-
// accepted step, linearly between the two nodes of x's region around it. At an interface x
|
|
3198
|
-
// belongs to both regions, so `region` (a name or index) picks the side; it defaults to the
|
|
3199
|
-
// first region that holds x.
|
|
3200
|
-
_probes(probes) {
|
|
3201
|
-
if (probes === undefined) return null;
|
|
3202
|
-
const { grid, regions, species } = this.model;
|
|
3203
|
-
const fail = (m) => {
|
|
3204
|
-
throw new DeviceError(m);
|
|
3205
|
-
};
|
|
3206
|
-
if (!Array.isArray(probes)) fail('advance: probes must be an array of { x, species, quantity, region }, or { interface, species } / { interface, gate }');
|
|
3207
|
-
const plan = probes.map((p, k) => {
|
|
3208
|
-
const path = `advance: probes[${k}]`;
|
|
3209
|
-
if (p?.interface !== undefined) {
|
|
3210
|
-
// At a face: a species' flux through it (mol/(m²·s), toward +x), or a gate's fraction open.
|
|
3211
|
-
const f = p.interface, itf = this.model.interfaces[f];
|
|
3212
|
-
if (!(Number.isInteger(f) && itf)) fail(`${path}.interface: no interface ${JSON.stringify(f)} (they're numbered from 0, left to right)`);
|
|
3213
|
-
if (p.gate !== undefined) {
|
|
3214
|
-
const q = itf.gates.findIndex((gt) => gt.name === p.gate);
|
|
3215
|
-
if (q < 0) fail(`${path}.gate: interfaces[${f}] has no gate ${JSON.stringify(p.gate)}${itf.gates.length ? ` (it has ${itf.gates.map((gt) => gt.name).join(', ')})` : ''}`);
|
|
3216
|
-
return { o: this._gateSlot(f, q) };
|
|
3217
|
-
}
|
|
3218
|
-
const i = species.findIndex((sp) => sp.name === p.species);
|
|
3219
|
-
if (i < 0) fail(`${path}.species: no species ${JSON.stringify(p.species)}`);
|
|
3220
|
-
return { o: this.blockOfFace[f] * this.M + 1 + i };
|
|
3221
|
-
}
|
|
3222
|
-
const quantity = p?.quantity ?? 'c';
|
|
3223
|
-
if (quantity !== 'c' && quantity !== 'V' && quantity !== 'phi') fail(`${path}.quantity must be 'c' (mol/m³), 'V' (the species voltage) or 'phi' (φ, V)`);
|
|
3224
|
-
const i = quantity === 'phi' ? -1 : species.findIndex((sp) => sp.name === p?.species);
|
|
3225
|
-
if (quantity !== 'phi' && i < 0) fail(`${path}.species: no species ${JSON.stringify(p?.species)}`);
|
|
3226
|
-
if (quantity === 'V' && species[i].z === 0) fail(`${path}: '${p.species}' is neutral, so it has no voltage; read 'c'`);
|
|
3227
|
-
const x = grid.x, inside = (r) => p.x >= x[grid.regionStart[r]] && p.x <= x[grid.regionEnd[r]];
|
|
3228
|
-
let r;
|
|
3229
|
-
if (p.region !== undefined) {
|
|
3230
|
-
r = typeof p.region === 'number' ? p.region : regions.findIndex((reg) => reg.name === p.region);
|
|
3231
|
-
if (!(r >= 0 && r < regions.length)) fail(`${path}.region: no region ${JSON.stringify(p.region)}`);
|
|
3232
|
-
if (!inside(r)) fail(`${path}.x (${p.x} m) is outside region ${JSON.stringify(p.region)}`);
|
|
3233
|
-
} else {
|
|
3234
|
-
// At a face, the side where the probe has something to read; both, and it must be told.
|
|
3235
|
-
const has = (rr) => {
|
|
3236
|
-
const mat = this.model.materials[regions[rr].material];
|
|
3237
|
-
return i < 0 ? !mat.conductor && !mat.phiFree : mat.present[i];
|
|
3238
|
-
};
|
|
3239
|
-
const sides = regions.flatMap((_, rr) => (inside(rr) ? [rr] : []));
|
|
3240
|
-
if (!(Number.isFinite(p.x) && sides.length)) fail(`${path}.x must be a position in the device, in m (got ${p.x})`);
|
|
3241
|
-
const able = sides.filter(has);
|
|
3242
|
-
if (able.length > 1) fail(`${path}.x (${p.x} m) is on the face between ${able.map((rr) => JSON.stringify(regions[rr].name)).join(' and ')}: give region, the side to read`);
|
|
3243
|
-
r = able[0] ?? sides[0];
|
|
3244
|
-
}
|
|
3245
|
-
if (i >= 0 && !this.model.materials[regions[r].material].present[i]) fail(`${path}: '${p.species}' is absent from region ${JSON.stringify(regions[r].name)}, so there's nothing to read at x = ${p.x} m`);
|
|
3246
|
-
let g = grid.regionStart[r];
|
|
3247
|
-
while (g + 1 < grid.regionEnd[r] && x[g + 1] < p.x) g++;
|
|
3248
|
-
const w = x[g + 1] > x[g] ? (p.x - x[g]) / (x[g + 1] - x[g]) : 0;
|
|
3249
|
-
return { i, g, w, quantity };
|
|
3250
|
-
});
|
|
3251
|
-
const out = plan.map(() => []);
|
|
3252
|
-
const { n, M } = this;
|
|
3253
|
-
const at = (q, g) => {
|
|
3254
|
-
if (q.quantity === 'phi') return this.phiUndefined[g] ? NaN : this.VT * (this.u[this.blockOfNode[g] * M] + this.uLo[this.blockOfNode[g] * M]);
|
|
3255
|
-
if (!this.present[g * n + q.i]) return NaN;
|
|
3256
|
-
if (q.quantity === 'c') return this.c[g * n + q.i];
|
|
3257
|
-
const o = this.blockOfNode[g] * M + 1 + q.i;
|
|
3258
|
-
return (this.VT * (this.u[o] + this.uLo[o])) / this.z[q.i];
|
|
3259
|
-
};
|
|
3260
|
-
return {
|
|
3261
|
-
out,
|
|
3262
|
-
read: () => plan.map((q) => (q.o !== undefined ? (this.loc[q.o] < 0 ? 0 : this.u[q.o]) : (1 - q.w) * at(q, q.g) + q.w * at(q, q.g + 1))),
|
|
3263
|
-
push: (values) => values.forEach((v, k) => out[k].push(v)),
|
|
3264
|
-
};
|
|
3265
|
-
}
|
|
3266
|
-
|
|
3267
|
-
// Start from another solver's state for the same device on another grid (a refined one): each
|
|
3268
|
-
// node's unknowns interpolated linearly in x within its region, each face's copied, and the
|
|
3269
|
-
// terminals' voltages. A warm start for a steady solve; nothing else is carried over.
|
|
3270
|
-
_warmFrom(src) {
|
|
3271
|
-
const { M, u, uLo } = this, grid = this.model.grid, sg = src.model.grid;
|
|
3272
|
-
if (src.M !== M || sg.regionStart.length !== grid.regionStart.length) throw new Error('_warmFrom: a different device');
|
|
3273
|
-
const value = (b, k) => src.u[b * M + k] + src.uLo[b * M + k];
|
|
3274
|
-
for (let r = 0; r < grid.regionStart.length; r++) {
|
|
3275
|
-
let a = sg.regionStart[r];
|
|
3276
|
-
for (let g = grid.regionStart[r]; g <= grid.regionEnd[r]; g++) {
|
|
3277
|
-
const x = grid.x[g];
|
|
3278
|
-
while (a + 1 < sg.regionEnd[r] && sg.x[a + 1] < x) a++;
|
|
3279
|
-
const b = Math.min(a + 1, sg.regionEnd[r]), h = sg.x[b] - sg.x[a];
|
|
3280
|
-
const w = h > 0 ? Math.min(1, Math.max(0, (x - sg.x[a]) / h)) : 0;
|
|
3281
|
-
const ba = src.blockOfNode[a], bb = src.blockOfNode[b], bg = this.blockOfNode[g];
|
|
3282
|
-
for (let k = 0; k < M; k++) {
|
|
3283
|
-
u[bg * M + k] = (1 - w) * value(ba, k) + w * value(bb, k);
|
|
3284
|
-
uLo[bg * M + k] = 0;
|
|
3285
|
-
}
|
|
3286
|
-
}
|
|
3287
|
-
}
|
|
3288
|
-
for (let f = 0; f < this.nFaces; f++) {
|
|
3289
|
-
for (let k = 0; k < M; k++) {
|
|
3290
|
-
u[this.blockOfFace[f] * M + k] = value(src.blockOfFace[f], k);
|
|
3291
|
-
uLo[this.blockOfFace[f] * M + k] = 0;
|
|
3292
|
-
}
|
|
3293
|
-
}
|
|
3294
|
-
this.termV.set(src.termV);
|
|
3295
|
-
this.solvedV = src.solvedV;
|
|
3296
|
-
this.time = src.time;
|
|
3297
|
-
this.computeConcentrations();
|
|
831
|
+
this.termV.set(src.termV);
|
|
832
|
+
this.solvedV = src.solvedV;
|
|
833
|
+
this.time = src.time;
|
|
834
|
+
this.computeConcentrations();
|
|
3298
835
|
}
|
|
3299
836
|
|
|
3300
837
|
_snapshot() {
|
|
@@ -3363,453 +900,6 @@ export class Solver {
|
|
|
3363
900
|
return I + (Number.isFinite(dt) ? (this.contactD.right - this.contactDOld.right) / dt : 0);
|
|
3364
901
|
}
|
|
3365
902
|
|
|
3366
|
-
/**
|
|
3367
|
-
* Small-signal impedance about the current (steady) state, seen at one terminal. Linearising
|
|
3368
|
-
* gives (J + iωM)·δx + Σ B_k δV_k = rhs for the grid's rows, and (C_k + iωC′_k)·δx +
|
|
3369
|
-
* (∂I_k/∂V_k)·δV_k = δI_set,k for each floating terminal's circuit row, with J the steady
|
|
3370
|
-
* Jacobian and M the storage part (from J(dt) = J + M/dt), B_k = ∂res/∂V_k and C_k = ∂I_k/∂x.
|
|
3371
|
-
* At the measured terminal, a held voltage is perturbed (rhs = −B δV) and its current read
|
|
3372
|
-
* from C, or a driven current is perturbed and its voltage solved for. The other terminals
|
|
3373
|
-
* keep their drives: held ones at AC ground, driven ones at AC open circuit.
|
|
3374
|
-
* Z = δV/δI with I into the device (positive real part for a passive device).
|
|
3375
|
-
* @param {ArrayLike<number>} frequencies Hz
|
|
3376
|
-
*/
|
|
3377
|
-
impedance(frequencies, opts) {
|
|
3378
|
-
const was = this.allTerminals;
|
|
3379
|
-
this.allTerminals = true; // (it reads its terminal's ∂res/∂V and ∂I/∂x)
|
|
3380
|
-
try {
|
|
3381
|
-
return this._impedance(frequencies, opts);
|
|
3382
|
-
} finally {
|
|
3383
|
-
this.allTerminals = was;
|
|
3384
|
-
}
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
|
-
_impedance(frequencies, { profiles = false, terminal = 'right' } = {}) {
|
|
3388
|
-
const { M, nB, sys, VT } = this;
|
|
3389
|
-
const kT = this.terms.findIndex((t) => t.name === terminal);
|
|
3390
|
-
if (kT < 0) throw new SolverError(`impedance: no terminal named '${terminal}' (${this.terms.map((t) => t.name).join(', ')})`);
|
|
3391
|
-
const dT = this.terms[kT].drive;
|
|
3392
|
-
if (dT.R > 0) throw new SolverError(`impedance: terminal '${terminal}' has a series resistance; that belongs to the external circuit`);
|
|
3393
|
-
// A small signal returns through the terminals held at a voltage: with every other one driven
|
|
3394
|
-
// by a current (held open), none comes back, and the impedance is infinite (else round-off).
|
|
3395
|
-
const others = this.terms.filter((t, k) => k !== kT);
|
|
3396
|
-
if (others.length && others.every((t) => t.drive.kind === 'I')) {
|
|
3397
|
-
throw new SolverError(
|
|
3398
|
-
`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}': ` +
|
|
3399
|
-
`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`,
|
|
3400
|
-
);
|
|
3401
|
-
}
|
|
3402
|
-
const N = nB * M, fl = this.floating, K = fl.length;
|
|
3403
|
-
this.sourceTime = this.time;
|
|
3404
|
-
this.computeConcentrations();
|
|
3405
|
-
this.cOld.set(this.c);
|
|
3406
|
-
this.thOld.set(this.th);
|
|
3407
|
-
this.th0Old.set(this.th0);
|
|
3408
|
-
this.gateList.forEach((gt, j) => (this.gateOld[j] = this.u[gt.o]));
|
|
3409
|
-
this.assemble(Infinity);
|
|
3410
|
-
this.contactDStart = { ...this.contactD };
|
|
3411
|
-
this.portQStart = Float64Array.from(this.portQ);
|
|
3412
|
-
this.assemble(Infinity);
|
|
3413
|
-
// The steady parts, then the storage parts from an assembly at a tiny dt.
|
|
3414
|
-
const J = { A: sys.A.slice(), B: sys.B.slice(), C: sys.C.slice() };
|
|
3415
|
-
const Bt = this.termB.map((v) => v.slice()), Ct = this.termC.map((v) => v.slice()), DI = Float64Array.from(this.termDI);
|
|
3416
|
-
const dts = 1e-30;
|
|
3417
|
-
this.assemble(dts);
|
|
3418
|
-
const S = {};
|
|
3419
|
-
for (const X of ['A', 'B', 'C']) S[X] = sys[X].map((v, k) => (v - J[X][k]) * dts);
|
|
3420
|
-
const Cs = this.termC.map((v, k) => v.map((x, j) => (x - Ct[k][j]) * dts));
|
|
3421
|
-
const DIs = this.termDI.map((x, k) => (x - DI[k]) * dts);
|
|
3422
|
-
// Where the measured contact's current may be read across a segment: anywhere without
|
|
3423
|
-
// ports; with them, only between it and the nearest window (a port's exchange takes current
|
|
3424
|
-
// in or out along it). Never for a port's own terminal.
|
|
3425
|
-
let cuts = kT < 2 ? this._currentCuts() : null;
|
|
3426
|
-
if (cuts && this.model.ports.length > 0) {
|
|
3427
|
-
const windows = this.model.ports.flatMap((p) => Array.from(p.nodes));
|
|
3428
|
-
const lo = Math.min(...windows), hi = Math.max(...windows);
|
|
3429
|
-
cuts = cuts.filter((cut) => (kT === 0 ? cut.s + 1 <= lo : cut.s >= hi));
|
|
3430
|
-
}
|
|
3431
|
-
// J·v for GMRES, exact: the assembled J holds a flux's dependence on η_L and η_R as two
|
|
3432
|
-
// entries, and where both are huge (an inversion layer) and v nearly uniform, J·v loses the
|
|
3433
|
-
// flux to round-off. So the dilute kernels' terms are kept in difference form (see
|
|
3434
|
-
// DifferenceTerms), with the rest from a matrix of their own, steady (Jr) and storage (Sr).
|
|
3435
|
-
this._assembleDifference(Infinity);
|
|
3436
|
-
const Jr = { A: this.sysRest.A.slice(), B: this.sysRest.B.slice(), C: this.sysRest.C.slice() };
|
|
3437
|
-
const { lin } = this._assembleDifference(dts);
|
|
3438
|
-
const Sr = {};
|
|
3439
|
-
for (const X of ['A', 'B', 'C']) Sr[X] = this.sysRest[X].map((v, k) => (v - Jr[X][k]) * dts);
|
|
3440
|
-
this.assemble(Infinity);
|
|
3441
|
-
|
|
3442
|
-
const { sizes, offA, offB, offC, offX } = sys;
|
|
3443
|
-
const csys = new ComplexBlockTridiagonal(nB, sizes);
|
|
3444
|
-
const Nc = csys.size;
|
|
3445
|
-
const rr = new Float64Array(Nc), ri = new Float64Array(Nc);
|
|
3446
|
-
const yr = new Float64Array(Nc), yi = new Float64Array(Nc);
|
|
3447
|
-
const Xr = fl.map(() => new Float64Array(Nc)), Xi = fl.map(() => new Float64Array(Nc));
|
|
3448
|
-
const xr = new Float64Array(N), xi = new Float64Array(N);
|
|
3449
|
-
const rRes = new Float64Array(Nc), iRes = new Float64Array(Nc);
|
|
3450
|
-
const jr = new Float64Array(Nc), ji = new Float64Array(Nc), sr = new Float64Array(Nc), si = new Float64Array(Nc);
|
|
3451
|
-
const out = { f: Float64Array.from(frequencies), Z: { re: new Float64Array(frequencies.length), im: new Float64Array(frequencies.length) } };
|
|
3452
|
-
if (profiles) out.profiles = [];
|
|
3453
|
-
// (C + iωC′)·(ar + i ai) for terminal k
|
|
3454
|
-
const dot = (k, w, ar, ai) => {
|
|
3455
|
-
let re = 0, im = 0;
|
|
3456
|
-
const c = Ct[k], cs = Cs[k];
|
|
3457
|
-
for (let j = 0; j < Nc; j++) {
|
|
3458
|
-
re += c[j] * ar[j] - w * cs[j] * ai[j];
|
|
3459
|
-
im += c[j] * ai[j] + w * cs[j] * ar[j];
|
|
3460
|
-
}
|
|
3461
|
-
return [re, im];
|
|
3462
|
-
};
|
|
3463
|
-
Array.from(frequencies).forEach((f, q) => {
|
|
3464
|
-
const w = 2 * Math.PI * f;
|
|
3465
|
-
if (!(w > 0)) throw new SolverError('impedance: frequencies must be positive');
|
|
3466
|
-
// Rows scaled by their largest entry (and the terminal columns with them).
|
|
3467
|
-
const scale = new Float64Array(Nc);
|
|
3468
|
-
for (let blk = 0; blk < nB; blk++) {
|
|
3469
|
-
const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
|
|
3470
|
-
for (let r = 0; r < m; r++) {
|
|
3471
|
-
const rows = [['A', offA[blk] + r * mp, mp], ['B', offB[blk] + r * m, m], ['C', offC[blk] + r * mn, mn]];
|
|
3472
|
-
let mx = 0;
|
|
3473
|
-
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]));
|
|
3474
|
-
const sc = mx > 0 ? 1 / mx : 1;
|
|
3475
|
-
scale[offX[blk] + r] = sc;
|
|
3476
|
-
for (const [X, o, len] of rows) {
|
|
3477
|
-
const JX = J[X], SX = S[X], Xre = csys[X + 'r'], Xim = csys[X + 'i'];
|
|
3478
|
-
for (let c = 0; c < len; c++) {
|
|
3479
|
-
Xre[o + c] = JX[o + c] * sc;
|
|
3480
|
-
Xim[o + c] = w * SX[o + c] * sc;
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
}
|
|
3484
|
-
}
|
|
3485
|
-
csys.factor();
|
|
3486
|
-
const sx = (X, vr, out) => {
|
|
3487
|
-
for (let blk = 0; blk < nB; blk++) {
|
|
3488
|
-
const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
|
|
3489
|
-
for (let r = 0; r < m; r++) {
|
|
3490
|
-
let acc = 0;
|
|
3491
|
-
for (let c = 0; c < mp; c++) acc += X.A[offA[blk] + r * mp + c] * vr[offX[blk - 1] + c];
|
|
3492
|
-
for (let c = 0; c < m; c++) acc += X.B[offB[blk] + r * m + c] * vr[offX[blk] + c];
|
|
3493
|
-
for (let c = 0; c < mn; c++) acc += X.C[offC[blk] + r * mn + c] * vr[offX[blk + 1] + c];
|
|
3494
|
-
out[offX[blk] + r] = acc;
|
|
3495
|
-
}
|
|
3496
|
-
}
|
|
3497
|
-
};
|
|
3498
|
-
// A x = −σ ∂res/∂V_k, x = outr + i outi (σ = 1 for the response to the measured terminal,
|
|
3499
|
-
// −1 for a floating one's unit δV): GMRES on the system preconditioned by the factorised one
|
|
3500
|
-
// (whose error is concentrated in a few slow modes, which it finds in a few iterations).
|
|
3501
|
-
const zero = new Float64Array(Nc);
|
|
3502
|
-
const precondition = (ar, ai) => {
|
|
3503
|
-
const outR = new Float64Array(Nc), outI = new Float64Array(Nc);
|
|
3504
|
-
for (let j = 0; j < Nc; j++) {
|
|
3505
|
-
rRes[j] = ar[j] * scale[j];
|
|
3506
|
-
iRes[j] = ai[j] * scale[j];
|
|
3507
|
-
}
|
|
3508
|
-
csys.solve(rRes, iRes, outR, outI);
|
|
3509
|
-
return [outR, outI];
|
|
3510
|
-
};
|
|
3511
|
-
// (J + iωS)·v, exactly (see DifferenceTerms), into (ar, ai).
|
|
3512
|
-
const jv = (vr, vi, ar, ai) => {
|
|
3513
|
-
sx(Jr, vr, jr);
|
|
3514
|
-
sx(Jr, vi, ji);
|
|
3515
|
-
sx(Sr, vi, si);
|
|
3516
|
-
sx(Sr, vr, sr);
|
|
3517
|
-
for (let j = 0; j < Nc; j++) {
|
|
3518
|
-
ar[j] = jr[j] - w * si[j];
|
|
3519
|
-
ai[j] = ji[j] + w * sr[j];
|
|
3520
|
-
}
|
|
3521
|
-
lin.apply(vr, ar, 1, 0);
|
|
3522
|
-
lin.apply(vi, ar, 0, -w);
|
|
3523
|
-
lin.apply(vi, ai, 1, 0);
|
|
3524
|
-
lin.apply(vr, ai, 0, w);
|
|
3525
|
-
};
|
|
3526
|
-
const op = ([vr, vi]) => {
|
|
3527
|
-
const ar = new Float64Array(Nc), ai = new Float64Array(Nc);
|
|
3528
|
-
jv(vr, vi, ar, ai);
|
|
3529
|
-
return precondition(ar, ai);
|
|
3530
|
-
};
|
|
3531
|
-
const solve = (br, bi, outr, outi, k, sigma) => {
|
|
3532
|
-
const rhs = k >= 0 ? precondition(Bt[k].map((v) => -sigma * v), zero) : [new Float64Array(Nc), new Float64Array(Nc)];
|
|
3533
|
-
outr.set(rhs[0]);
|
|
3534
|
-
outi.set(rhs[1]);
|
|
3535
|
-
// (GMRES keeps its best iterate, which is never worse than the factorised solve.)
|
|
3536
|
-
const { residual } = gmres(op, rhs, [outr, outi]);
|
|
3537
|
-
accuracy = Math.max(accuracy, residual);
|
|
3538
|
-
};
|
|
3539
|
-
let accuracy = 1e-14; // (relative, of the solves: the readouts' error is weighed with it)
|
|
3540
|
-
// y: the response with the floating terminals held; X_k: to a unit δV_k.
|
|
3541
|
-
const heldT = dT.kind === 'V';
|
|
3542
|
-
for (let j = 0; j < Nc; j++) {
|
|
3543
|
-
rr[j] = heldT ? -Bt[kT][j] * scale[j] : 0;
|
|
3544
|
-
ri[j] = 0;
|
|
3545
|
-
}
|
|
3546
|
-
solve(rr, ri, yr, yi, heldT ? kT : -1, heldT ? 1 : 0);
|
|
3547
|
-
fl.forEach((k, a) => {
|
|
3548
|
-
for (let j = 0; j < Nc; j++) rr[j] = Bt[k][j] * scale[j];
|
|
3549
|
-
ri.fill(0);
|
|
3550
|
-
solve(rr, ri, Xr[a], Xi[a], k, -1);
|
|
3551
|
-
});
|
|
3552
|
-
// The floating terminals' circuit rows, a small complex system for their δV.
|
|
3553
|
-
const Ar = Array.from({ length: K }, () => new Float64Array(K + 1)), Ai = Array.from({ length: K }, () => new Float64Array(K + 1));
|
|
3554
|
-
fl.forEach((k, a) => {
|
|
3555
|
-
for (let b = 0; b < K; b++) {
|
|
3556
|
-
const [re, im] = dot(k, w, Xr[b], Xi[b]);
|
|
3557
|
-
Ar[a][b] = -re;
|
|
3558
|
-
Ai[a][b] = -im;
|
|
3559
|
-
}
|
|
3560
|
-
Ar[a][a] += DI[k];
|
|
3561
|
-
Ai[a][a] += w * DIs[k];
|
|
3562
|
-
const [yre, yim] = dot(k, w, yr, yi);
|
|
3563
|
-
Ar[a][K] = (k === kT ? 1 : 0) - yre;
|
|
3564
|
-
Ai[a][K] = -yim;
|
|
3565
|
-
});
|
|
3566
|
-
const dV = complexSolve(Ar, Ai, K);
|
|
3567
|
-
// δx = y − Σ X_k δV_k
|
|
3568
|
-
for (let j = 0; j < Nc; j++) {
|
|
3569
|
-
let re = yr[j], im = yi[j];
|
|
3570
|
-
for (let a = 0; a < K; a++) {
|
|
3571
|
-
re -= Xr[a][j] * dV.re[a] - Xi[a][j] * dV.im[a];
|
|
3572
|
-
im -= Xr[a][j] * dV.im[a] + Xi[a][j] * dV.re[a];
|
|
3573
|
-
}
|
|
3574
|
-
yr[j] = re;
|
|
3575
|
-
yi[j] = im;
|
|
3576
|
-
xr[this.fullOf[j]] = re;
|
|
3577
|
-
xi[this.fullOf[j]] = im;
|
|
3578
|
-
}
|
|
3579
|
-
// The current into terminal kT per volt, (C + iωC′)·δx + (∂I/∂V)(1 + iω′), with its error.
|
|
3580
|
-
// At the contact it's a sum whose terms can be far larger than itself (in a junction's
|
|
3581
|
-
// neutral ends, a conductance times a δη that cancels its neighbours' to 1e-17), and its
|
|
3582
|
-
// round-off reads as a constant conductance. Between the two contacts of a device without
|
|
3583
|
-
// ports, the total current (conduction and displacement) is the same through every cut, so
|
|
3584
|
-
// it's also read across each segment, and the reading with the least error taken. Each
|
|
3585
|
-
// reading's error is its terms' round-off and the solves' error in δx (to `accuracy` of its
|
|
3586
|
-
// largest entry) through its coefficients: a contact's or a cut's flux is a difference of
|
|
3587
|
-
// levels that the solved rows don't pin.
|
|
3588
|
-
// `parts`: the response as a sum, each part compact (r, i) and full-index (fr, fi), with the
|
|
3589
|
-
// absolute error of its entries.
|
|
3590
|
-
const read = (parts) => {
|
|
3591
|
-
const eps = 1.1e-16, c = Ct[kT], cs = Cs[kT];
|
|
3592
|
-
let Ir = DI[kT], Ii = w * DIs[kT], err = eps * (Math.abs(DI[kT]) + w * Math.abs(DIs[kT]));
|
|
3593
|
-
for (const p of parts) {
|
|
3594
|
-
const [re, im] = dot(kT, w, p.r, p.i);
|
|
3595
|
-
Ir += re;
|
|
3596
|
-
Ii += im;
|
|
3597
|
-
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);
|
|
3598
|
-
}
|
|
3599
|
-
if (!cuts) return { Ir, Ii, err };
|
|
3600
|
-
const sgn = kT === 0 ? 1 : -1;
|
|
3601
|
-
for (const cut of cuts) {
|
|
3602
|
-
let re = 0, im = 0, e = 0;
|
|
3603
|
-
for (const p of parts) {
|
|
3604
|
-
for (let a = 0; a < cut.idx.length; a++) {
|
|
3605
|
-
const x = cut.idx[a], gk = cut.g[a], dk = w * cut.d[a];
|
|
3606
|
-
// (g + iωd)(xr + i xi)
|
|
3607
|
-
re += gk * p.fr[x] - dk * p.fi[x];
|
|
3608
|
-
im += gk * p.fi[x] + dk * p.fr[x];
|
|
3609
|
-
e += (Math.abs(gk) + Math.abs(dk)) * (eps * Math.hypot(p.fr[x], p.fi[x]) + p.floor);
|
|
3610
|
-
}
|
|
3611
|
-
}
|
|
3612
|
-
if (e < err) [err, Ir, Ii] = [e, sgn * re, sgn * im];
|
|
3613
|
-
}
|
|
3614
|
-
return { Ir, Ii, err };
|
|
3615
|
-
};
|
|
3616
|
-
const largest = (r, i) => {
|
|
3617
|
-
let m = 0;
|
|
3618
|
-
for (let j = 0; j < r.length; j++) m = Math.max(m, Math.hypot(r[j], i[j]));
|
|
3619
|
-
return m;
|
|
3620
|
-
};
|
|
3621
|
-
const full = (r, i) => {
|
|
3622
|
-
const fr = new Float64Array(N), fi = new Float64Array(N);
|
|
3623
|
-
for (let j = 0; j < Nc; j++) {
|
|
3624
|
-
fr[this.fullOf[j]] = r[j];
|
|
3625
|
-
fi[this.fullOf[j]] = i[j];
|
|
3626
|
-
}
|
|
3627
|
-
return [fr, fi];
|
|
3628
|
-
};
|
|
3629
|
-
let Zr, Zi;
|
|
3630
|
-
if (heldT) {
|
|
3631
|
-
// δI per volt: (C + iωC′)·δx + (∂I/∂V)(1 + iω′) at the terminal…
|
|
3632
|
-
let { Ir, Ii, err } = read([{ r: yr, i: yi, fr: xr, fi: xi, floor: accuracy * largest(yr, yi) }]);
|
|
3633
|
-
// …and where the solves leave that unresolved (the estimate past 1% of the current), the
|
|
3634
|
-
// response is split: a uniform shift of each region (z_i s on every η_i, s on φ̂), which
|
|
3635
|
-
// changes no flux and no concentration, so that J·e vanishes inside each region and is
|
|
3636
|
-
// taken at its edges; and what's left, small, solved for to GMRES's floor, its error now
|
|
3637
|
-
// relative to itself. An electrolyte at its open circuit needs it: the response to its
|
|
3638
|
-
// terminal is nearly such a shift, and its current the slope of levels uniform to 1e-14,
|
|
3639
|
-
// more than a double can hold. (A port's exchange sees the shift: its window's rows are
|
|
3640
|
-
// kept with the edges'.) So does a MOS capacitor's back contact, its channel held by a port.
|
|
3641
|
-
if (K === 0 && cuts && err > 1e-2 * Math.hypot(Ir, Ii)) {
|
|
3642
|
-
const [er, ei] = this._regionShift(yr, yi);
|
|
3643
|
-
const tr = new Float64Array(Nc), ti = new Float64Array(Nc);
|
|
3644
|
-
jv(er, ei, tr, ti);
|
|
3645
|
-
const edge = this._regionEdgeRows();
|
|
3646
|
-
const ar = new Float64Array(Nc), ai = new Float64Array(Nc);
|
|
3647
|
-
for (let j = 0; j < Nc; j++) {
|
|
3648
|
-
ar[j] = -Bt[kT][j] - (edge[j] ? tr[j] : 0);
|
|
3649
|
-
ai[j] = edge[j] ? -ti[j] : 0;
|
|
3650
|
-
}
|
|
3651
|
-
const rhs = precondition(ar, ai);
|
|
3652
|
-
const dr = yr.map((v, j) => v - er[j]), di = yi.map((v, j) => v - ei[j]);
|
|
3653
|
-
const { residual } = gmres(op, rhs, [dr, di], { tol: 1e-11 });
|
|
3654
|
-
const [efr, efi] = full(er, ei), [dfr, dfi] = full(dr, di);
|
|
3655
|
-
const r2 = read([
|
|
3656
|
-
{ r: er, i: ei, fr: efr, fi: efi, floor: 0 },
|
|
3657
|
-
{ r: dr, i: di, fr: dfr, fi: dfi, floor: Math.max(1e-14, residual) * largest(dr, di) },
|
|
3658
|
-
]);
|
|
3659
|
-
if (r2.err < err) ({ Ir, Ii, err } = r2);
|
|
3660
|
-
if (profiles) {
|
|
3661
|
-
for (let j = 0; j < N; j++) {
|
|
3662
|
-
xr[j] = efr[j] + dfr[j];
|
|
3663
|
-
xi[j] = efi[j] + dfi[j];
|
|
3664
|
-
}
|
|
3665
|
-
}
|
|
3666
|
-
}
|
|
3667
|
-
const d2 = Ir * Ir + Ii * Ii; // Z = 1/δI
|
|
3668
|
-
Zr = Ir / d2;
|
|
3669
|
-
Zi = -Ii / d2;
|
|
3670
|
-
} else {
|
|
3671
|
-
// δV per unit current
|
|
3672
|
-
const a = fl.indexOf(kT);
|
|
3673
|
-
Zr = dV.re[a];
|
|
3674
|
-
Zi = dV.im[a];
|
|
3675
|
-
}
|
|
3676
|
-
out.Z.re[q] = Zr;
|
|
3677
|
-
out.Z.im[q] = Zi;
|
|
3678
|
-
if (profiles) out.profiles.push(this._smallSignalProfiles(xr, xi));
|
|
3679
|
-
});
|
|
3680
|
-
return out;
|
|
3681
|
-
}
|
|
3682
|
-
|
|
3683
|
-
// A uniform shift of each region, fitted to a small-signal response (compact, complex): φ̂ by
|
|
3684
|
-
// s_r and each species' η by z_i s_r, with s_r the region's mean δφ̂ (or, where φ̂ isn't an
|
|
3685
|
-
// unknown, its carriers' mean δη/z). Coverages and flux unknowns aren't shifted.
|
|
3686
|
-
_regionShift(yr, yi) {
|
|
3687
|
-
const { n, M, z, loc, model } = this, R = this.rix, { grid } = model;
|
|
3688
|
-
const er = new Float64Array(yr.length), ei = new Float64Array(yr.length);
|
|
3689
|
-
for (let r = 0; r < model.regions.length; r++) {
|
|
3690
|
-
let sr = 0, si = 0, k = 0;
|
|
3691
|
-
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
|
|
3692
|
-
for (let g = g0; g <= g1; g++) {
|
|
3693
|
-
const o = this.blockOfNode[g] * M;
|
|
3694
|
-
if (loc[o] >= 0) {
|
|
3695
|
-
sr += yr[R[o]];
|
|
3696
|
-
si += yi[R[o]];
|
|
3697
|
-
k++;
|
|
3698
|
-
}
|
|
3699
|
-
}
|
|
3700
|
-
if (k === 0) {
|
|
3701
|
-
for (let g = g0; g <= g1; g++) {
|
|
3702
|
-
const o = this.blockOfNode[g] * M;
|
|
3703
|
-
for (let i = 0; i < n; i++) {
|
|
3704
|
-
if (z[i] === 0 || loc[o + 1 + i] < 0) continue;
|
|
3705
|
-
sr += yr[R[o + 1 + i]] / z[i];
|
|
3706
|
-
si += yi[R[o + 1 + i]] / z[i];
|
|
3707
|
-
k++;
|
|
3708
|
-
}
|
|
3709
|
-
}
|
|
3710
|
-
}
|
|
3711
|
-
if (k === 0) continue;
|
|
3712
|
-
sr /= k;
|
|
3713
|
-
si /= k;
|
|
3714
|
-
for (let g = g0; g <= g1; g++) {
|
|
3715
|
-
const o = this.blockOfNode[g] * M;
|
|
3716
|
-
if (loc[o] >= 0) {
|
|
3717
|
-
er[R[o]] = sr;
|
|
3718
|
-
ei[R[o]] = si;
|
|
3719
|
-
}
|
|
3720
|
-
for (let i = 0; i < n; i++) {
|
|
3721
|
-
if (loc[o + 1 + i] < 0) continue;
|
|
3722
|
-
er[R[o + 1 + i]] = z[i] * sr;
|
|
3723
|
-
ei[R[o + 1 + i]] = z[i] * si;
|
|
3724
|
-
}
|
|
3725
|
-
}
|
|
3726
|
-
}
|
|
3727
|
-
return [er, ei];
|
|
3728
|
-
}
|
|
3729
|
-
|
|
3730
|
-
// Compact rows a region's uniform shift can touch: faces, the nodes beside them, the two ends
|
|
3731
|
-
// and the ports' windows (whatever holds a level from outside). Elsewhere inside a region,
|
|
3732
|
-
// every row is unchanged by it.
|
|
3733
|
-
_regionEdgeRows() {
|
|
3734
|
-
const { M, loc, nB } = this, R = this.rix;
|
|
3735
|
-
const edge = new Uint8Array(this.sys.size);
|
|
3736
|
-
const mark = (b) => {
|
|
3737
|
-
if (b < 0 || b >= nB) return;
|
|
3738
|
-
for (let r = 0; r < M; r++) if (loc[b * M + r] >= 0) edge[R[b * M + r]] = 1;
|
|
3739
|
-
};
|
|
3740
|
-
mark(0);
|
|
3741
|
-
mark(nB - 1);
|
|
3742
|
-
for (let f = 0; f < this.nFaces; f++) {
|
|
3743
|
-
const b = this.blockOfFace[f];
|
|
3744
|
-
mark(b - 1);
|
|
3745
|
-
mark(b);
|
|
3746
|
-
mark(b + 1);
|
|
3747
|
-
}
|
|
3748
|
-
for (const port of this.model.ports) for (const g of port.nodes) mark(this.blockOfNode[g]);
|
|
3749
|
-
return edge;
|
|
3750
|
-
}
|
|
3751
|
-
|
|
3752
|
-
// The cuts where the impedance may read a small-signal current. Each is the linearised total
|
|
3753
|
-
// current toward +x through one segment s of a region, Σ (g + iω d)·δu over the unknowns idx:
|
|
3754
|
-
// the Scharfetter–Gummel fluxes' derivatives (as in _segmentsDilute, or _segmentConcentrated
|
|
3755
|
-
// for non-ideal statistics) and the displacement's. Without ports it's the same through every cut; with them, only between a
|
|
3756
|
-
// contact and the nearest port's window (see _impedance).
|
|
3757
|
-
_currentCuts() {
|
|
3758
|
-
const { model, n, M, u, uLo, c, z } = this, F = FARADAY;
|
|
3759
|
-
const { grid, materials, regions } = model;
|
|
3760
|
-
const cuts = [];
|
|
3761
|
-
for (let r = 0; r < regions.length; r++) {
|
|
3762
|
-
const reg = regions[r], mat = materials[reg.material];
|
|
3763
|
-
if (mat.conductor || reg.mixing > 0) continue;
|
|
3764
|
-
const vel = reg.velocity;
|
|
3765
|
-
for (let s = grid.regionStart[r]; s < grid.regionEnd[r]; s++) {
|
|
3766
|
-
const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s];
|
|
3767
|
-
const terms = new Map();
|
|
3768
|
-
const add = (x, g, d) => {
|
|
3769
|
-
const t = terms.get(x) || [0, 0];
|
|
3770
|
-
t[0] += g;
|
|
3771
|
-
t[1] += d;
|
|
3772
|
-
terms.set(x, t);
|
|
3773
|
-
};
|
|
3774
|
-
const phiL = u[bL * M], phiR = u[bR * M], hasPhi = !this.phiUndefined[s];
|
|
3775
|
-
for (let i = 0; i < n; i++) {
|
|
3776
|
-
if (z[i] === 0 || !mat.present[i] || mat.D[i] === 0) continue;
|
|
3777
|
-
if (!mat.ideal) {
|
|
3778
|
-
this._fluxConcentrated(s, bL, bR, mat, h, vel, i);
|
|
3779
|
-
for (let k = 0; k < M; k++) {
|
|
3780
|
-
if (k === 0 && !hasPhi) continue;
|
|
3781
|
-
if (this.jL[k] !== 0) add(bL * M + k, z[i] * F * this.jL[k], 0);
|
|
3782
|
-
if (this.jR[k] !== 0) add(bR * M + k, z[i] * F * this.jR[k], 0);
|
|
3783
|
-
}
|
|
3784
|
-
continue;
|
|
3785
|
-
}
|
|
3786
|
-
const zi = z[i], r1 = 1 + i, cL = c[s * n + i], g = mat.D[i] / h;
|
|
3787
|
-
const pe = (vel * h) / mat.D[i];
|
|
3788
|
-
const d = zi * (phiR - phiL) - pe;
|
|
3789
|
-
const E = Math.expm1(u[bR * M + r1] - u[bL * M + r1] + (uLo[bR * M + r1] - uLo[bL * M + r1]) - pe);
|
|
3790
|
-
const gBc = g * bernoulli(d) * cL;
|
|
3791
|
-
const dNdd = -g * bernoulliDerivative(d) * cL * E;
|
|
3792
|
-
add(bL * M + r1, zi * F * gBc, 0);
|
|
3793
|
-
add(bR * M + r1, -zi * F * gBc * (E + 1), 0);
|
|
3794
|
-
if (hasPhi) {
|
|
3795
|
-
add(bL * M, zi * F * (-zi * dNdd + zi * gBc * E), 0);
|
|
3796
|
-
add(bR * M, zi * F * zi * dNdd, 0);
|
|
3797
|
-
}
|
|
3798
|
-
}
|
|
3799
|
-
// The displacement toward +x, ε(φ_L − φ_R)/h (in φ̂ = φ/V_T).
|
|
3800
|
-
const k = hasPhi ? (mat.epsr * EPS0 * this.VT) / h : 0;
|
|
3801
|
-
if (k > 0) {
|
|
3802
|
-
add(bL * M, 0, k);
|
|
3803
|
-
add(bR * M, 0, -k);
|
|
3804
|
-
}
|
|
3805
|
-
if (terms.size === 0) continue;
|
|
3806
|
-
const idx = [...terms.keys()];
|
|
3807
|
-
cuts.push({ s, idx, g: idx.map((x) => terms.get(x)[0]), d: idx.map((x) => terms.get(x)[1]) });
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3810
|
-
return cuts;
|
|
3811
|
-
}
|
|
3812
|
-
|
|
3813
903
|
// Each species' flux toward +x across every segment, at the current state (concentrations
|
|
3814
904
|
// computed): totals through the cross-section, mol/s (mol/(m²·s) for a planar device). A face's
|
|
3815
905
|
// segment, between its pair of nodes, carries the face's flux; a conductor's, its carrier's.
|
|
@@ -3831,11 +921,17 @@ export class Solver {
|
|
|
3831
921
|
const pe = (vel * h) / mat.D[i];
|
|
3832
922
|
const d = z[i] * (u[bR * M] - u[bL * M]) + ex[(s + 1) * n + i] - ex[s * n + i] - pe;
|
|
3833
923
|
const deta = u[bR * M + 1 + i] - u[bL * M + 1 + i] + (uLo[bR * M + 1 + i] - uLo[bL * M + 1 + i]) - pe;
|
|
3834
|
-
out[i][s] =
|
|
924
|
+
out[i][s] = sgFlux(mat.D[i] / h, d, deta, c[s * n + i], c[(s + 1) * n + i], this.sg)[0];
|
|
3835
925
|
}
|
|
3836
926
|
if (reg.mixing > 0) {
|
|
3837
|
-
// Eddy mixing (see _segmentMixing): −(D_mix/h) c̄_i (Δη_i − z_i q), c̄ the logarithmic
|
|
927
|
+
// Eddy mixing (see _segmentMixing): −(D_mix/h) c̄_i (Δη_i − z_i q), c̄ the logarithmic
|
|
928
|
+
// mean; between crowded ends, _mixCrowded's.
|
|
3838
929
|
const on = [...Array(n).keys()].filter((i) => mat.present[i] && mat.D[i] > 0);
|
|
930
|
+
if (!this.nodeIdeal[s] || !this.nodeIdeal[s + 1]) {
|
|
931
|
+
const { N } = this._mixBase(s, s + 1, bL, bR, on);
|
|
932
|
+
on.forEach((i, p) => (out[i][s] += (reg.mixing / h) * N[p]));
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
3839
935
|
const cb = [], de = [];
|
|
3840
936
|
let S = 0, Q = 0;
|
|
3841
937
|
for (const k of on) {
|
|
@@ -3872,89 +968,11 @@ export class Solver {
|
|
|
3872
968
|
const gated = link.gates ?? [];
|
|
3873
969
|
const P = link.P * (gated.length ? this._open(gated, (q) => u[this._gateSlot(f, q)]) : 1);
|
|
3874
970
|
const right = P * bernoulli(d) * this.c[kl];
|
|
3875
|
-
|
|
971
|
+
// (far up a drift, right × e^Δη is 0·∞: the backward one is then sgFlux's, whole)
|
|
972
|
+
return [right, d < SG_FAR ? right * Math.exp(deta) : sgFlux(P, d, deta, this.c[kl], this.c[kr], this.sg)[3]];
|
|
3876
973
|
});
|
|
3877
974
|
}
|
|
3878
975
|
|
|
3879
|
-
// Complex profiles of δφ (V), δμ̄ (J/mol) and δc (mol/m³) from a small-signal solution.
|
|
3880
|
-
_smallSignalProfiles(xr, xi) {
|
|
3881
|
-
const { n, M, z, VT, model } = this;
|
|
3882
|
-
const nN = this.nNodes, RT = model.RT;
|
|
3883
|
-
const pair = () => ({ re: new Float64Array(nN), im: new Float64Array(nN) });
|
|
3884
|
-
const phi = pair(), mu = {}, c = {};
|
|
3885
|
-
for (const sp of model.species) {
|
|
3886
|
-
mu[sp.name] = pair();
|
|
3887
|
-
c[sp.name] = pair();
|
|
3888
|
-
}
|
|
3889
|
-
for (let g = 0; g < nN; g++) {
|
|
3890
|
-
const b = this.blockOfNode[g];
|
|
3891
|
-
const undef = this.phiUndefined[g];
|
|
3892
|
-
phi.re[g] = undef ? NaN : VT * xr[b * M];
|
|
3893
|
-
phi.im[g] = undef ? NaN : VT * xi[b * M];
|
|
3894
|
-
for (let i = 0; i < n; i++) {
|
|
3895
|
-
const name = model.species[i].name;
|
|
3896
|
-
if (!this.present[g * n + i]) {
|
|
3897
|
-
mu[name].re[g] = mu[name].im[g] = c[name].re[g] = c[name].im[g] = NaN;
|
|
3898
|
-
continue;
|
|
3899
|
-
}
|
|
3900
|
-
mu[name].re[g] = RT * xr[b * M + 1 + i];
|
|
3901
|
-
mu[name].im[g] = RT * xi[b * M + 1 + i];
|
|
3902
|
-
if (this.nodeConductor[g] >= 0) {
|
|
3903
|
-
// a metal has a Fermi level but no concentration (as a solution reports it)
|
|
3904
|
-
c[name].re[g] = c[name].im[g] = NaN;
|
|
3905
|
-
continue;
|
|
3906
|
-
}
|
|
3907
|
-
// δc_i = Σ_j K_ij (δη_j − z_j δφ̂)
|
|
3908
|
-
let sr = 0, si = 0;
|
|
3909
|
-
for (let j = 0; j < n; j++) {
|
|
3910
|
-
const Kij = this.nodeIdeal[g] ? (i === j ? this.c[g * n + i] : 0) : this.K[g * n * n + i * n + j];
|
|
3911
|
-
if (Kij === 0) continue;
|
|
3912
|
-
sr += Kij * (xr[b * M + 1 + j] - z[j] * xr[b * M]);
|
|
3913
|
-
si += Kij * (xi[b * M + 1 + j] - z[j] * xi[b * M]);
|
|
3914
|
-
}
|
|
3915
|
-
c[name].re[g] = sr;
|
|
3916
|
-
c[name].im[g] = si;
|
|
3917
|
-
}
|
|
3918
|
-
}
|
|
3919
|
-
return { phi, mu, c };
|
|
3920
|
-
}
|
|
3921
|
-
|
|
3922
|
-
// The contact and port readouts (fluxes, displacements, the last segment's current) at the
|
|
3923
|
-
// current state, from only the boxes they're read from. Each of those boxes gets the same terms
|
|
3924
|
-
// in the same order as in assemble(), so the readouts are identical; other residuals and the
|
|
3925
|
-
// Jacobian are left partial (the next assemble() starts afresh). Concentrations must be current.
|
|
3926
|
-
_assembleBookkeeping(dt) {
|
|
3927
|
-
const { model } = this;
|
|
3928
|
-
this.dtNow = dt;
|
|
3929
|
-
const { grid, materials, regions } = model;
|
|
3930
|
-
this.res.fill(0);
|
|
3931
|
-
for (let r = 0; r < regions.length; r++) {
|
|
3932
|
-
const { nodes, segs } = this.bookkeeping[r];
|
|
3933
|
-
if (nodes.length === 0 && segs.length === 0) continue;
|
|
3934
|
-
const reg = regions[r], mat = materials[reg.material];
|
|
3935
|
-
if (mat.conductor) {
|
|
3936
|
-
for (const g of nodes) this._nodeConductor(g, dt);
|
|
3937
|
-
for (const s of segs) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s] / grid.segArea[s]);
|
|
3938
|
-
continue;
|
|
3939
|
-
}
|
|
3940
|
-
for (const g of nodes) {
|
|
3941
|
-
if (mat.ideal) this._nodesDilute(g, g, dt);
|
|
3942
|
-
else this._nodeConcentrated(g, dt);
|
|
3943
|
-
}
|
|
3944
|
-
const rxs = this.rxsIn[reg.material];
|
|
3945
|
-
if (rxs.length > 0) for (const g of nodes) this._bulkReactions(g, rxs);
|
|
3946
|
-
for (const s of segs) {
|
|
3947
|
-
const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s]; // (a length over the cross-section: fluxes are totals)
|
|
3948
|
-
this._segmentDisplacement(s, bL, bR, mat, h);
|
|
3949
|
-
if (reg.mixing > 0) this._segmentMixing(s, bL, bR, reg.mixing, h, mat);
|
|
3950
|
-
if (!mat.ideal) this._segmentConcentrated(s, bL, bR, mat, h, reg.velocity);
|
|
3951
|
-
}
|
|
3952
|
-
if (mat.ideal) for (const s of segs) this._segmentsDilute(s, s + 1, r, mat, reg.velocity);
|
|
3953
|
-
}
|
|
3954
|
-
for (const f of this.bookkeepingFaces) this._face(f);
|
|
3955
|
-
this._terminals(dt);
|
|
3956
|
-
}
|
|
3957
|
-
|
|
3958
976
|
// Add this step's contact fluxes (at the converged state) to each stretch's intake.
|
|
3959
977
|
// (A BDF2 step also moves each amount by Σ v (c* − c_n), its history term.)
|
|
3960
978
|
_accumulateBoundaryIntake(dt, cN) {
|
|
@@ -3984,503 +1002,11 @@ export class Solver {
|
|
|
3984
1002
|
this.boundaryIntake[k] += q * dt + hist;
|
|
3985
1003
|
});
|
|
3986
1004
|
}
|
|
1005
|
+
}
|
|
3987
1006
|
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
for (let i = 0; i < this.n; i++) if (mat.present[i] && mat.D[i] > 0) dMin = Math.min(dMin, mat.D[i]);
|
|
3994
|
-
}
|
|
3995
|
-
const L = model.grid.length;
|
|
3996
|
-
return Number.isFinite(dMin) ? (L * L) / dMin : 1;
|
|
3997
|
-
}
|
|
3998
|
-
|
|
3999
|
-
// The fastest diffusion time: across the smallest cell, at the largest D.
|
|
4000
|
-
fastestTime() {
|
|
4001
|
-
let dMax = 0;
|
|
4002
|
-
for (const mat of this.model.materials) {
|
|
4003
|
-
for (let i = 0; i < this.n; i++) if (mat.present[i]) dMax = Math.max(dMax, mat.D[i]);
|
|
4004
|
-
}
|
|
4005
|
-
let h = Infinity;
|
|
4006
|
-
for (const s of this.model.grid.segLength) if (s > 0) h = Math.min(h, s);
|
|
4007
|
-
return dMax > 0 && Number.isFinite(h) ? (h * h) / dMax : this.slowestTime();
|
|
4008
|
-
}
|
|
4009
|
-
|
|
4010
|
-
/**
|
|
4011
|
-
* Steady state: where a transient from the present state ends, found as the solution of the
|
|
4012
|
-
* steady equations (dt = ∞, no storage term), with no slow modes to wait out, however slow the
|
|
4013
|
-
* physics (e.g. exponentially scarce minority carriers). What the storage term held is held by
|
|
4014
|
-
* the steady system's own rows instead (see src/laws.js): each conserved amount in place of a balance
|
|
4015
|
-
* row that says nothing new, levels that carry nothing held flat, islands' summed balances.
|
|
4016
|
-
* Where Newton from here fails: continuation along the drives, then the pseudo-transient ramp.
|
|
4017
|
-
* The clock is not advanced, and open-system conservation bookkeeping restarts here.
|
|
4018
|
-
*/
|
|
4019
|
-
// Steady state from the present one; `atSteady` says whether the state is one (until a step or
|
|
4020
|
-
// a change of drive).
|
|
4021
|
-
solveSteady(opts = {}) {
|
|
4022
|
-
this.violent = this.history.length === 0 && !this.stepped ? violentStart(this) : []; // (for a failure to say: see src/timeSteps.js)
|
|
4023
|
-
this.unreached = null; // (a driven current no held voltage reaches: see _floatingContinuation)
|
|
4024
|
-
this.charging = null; // (a driven current that only charges: see _solveSteadyAll)
|
|
4025
|
-
captureLaws(this);
|
|
4026
|
-
this.steady = true; // (the steady system's rows; sources at the present time)
|
|
4027
|
-
this.steadyAt = this.time; // (which a pseudo-transient's steps don't move on)
|
|
4028
|
-
try {
|
|
4029
|
-
return this._solveSteadyAll(opts);
|
|
4030
|
-
} finally {
|
|
4031
|
-
this.steady = false;
|
|
4032
|
-
}
|
|
4033
|
-
}
|
|
4034
|
-
|
|
4035
|
-
_solveSteadyAll(opts) {
|
|
4036
|
-
// A terminal driven by a current that can only store what comes in (a battery's cathode
|
|
4037
|
-
// behind it, closed but for that terminal; a gate, or a capacitance with no species through
|
|
4038
|
-
// it) has no steady state but at no current, where it keeps what it holds.
|
|
4039
|
-
const { species, regions } = this.model;
|
|
4040
|
-
for (const k of this.floating) {
|
|
4041
|
-
const I = sourceAt(this.terms[k].drive.src, this.time);
|
|
4042
|
-
if (this.terms[k].drive.kind !== 'I' || I === 0) continue;
|
|
4043
|
-
const laws = this.constraints.filter((cs) => cs.terminals.includes(k));
|
|
4044
|
-
if (laws.length === 0 && !chargeOnly(this, k)) continue;
|
|
4045
|
-
const what = laws.flatMap((cs) => cs.parts.map(({ stretch }) => {
|
|
4046
|
-
const st = this.stretches[stretch], [r0, r1] = st.regions;
|
|
4047
|
-
return `${species[st.species].name} in ${regions[r0].name}${r1 > r0 ? `…${regions[r1].name}` : ''}`;
|
|
4048
|
-
}));
|
|
4049
|
-
this.charging = { terminal: this.terms[k].name, current: I, what };
|
|
4050
|
-
return { converged: false, steps: 0, iterations: 0, history: [] };
|
|
4051
|
-
}
|
|
4052
|
-
const r = this._steadyFromHere(opts);
|
|
4053
|
-
this.atSteady = r.converged;
|
|
4054
|
-
return r;
|
|
4055
|
-
}
|
|
4056
|
-
|
|
4057
|
-
// Steady state from the present one, in three stages: Newton from here; failing that, from a
|
|
4058
|
-
// solved state along the drives (the light ramped up, a current's terminal held at the voltages
|
|
4059
|
-
// that pass it, the voltages ramped from where they were last solved); failing that, the
|
|
4060
|
-
// pseudo-transient ramp, through time.
|
|
4061
|
-
_steadyFromHere(opts) {
|
|
4062
|
-
this.conditioning = null;
|
|
4063
|
-
this.computeConcentrations();
|
|
4064
|
-
captureLocal(this); // immobile combinations keep, node by node, what they hold now
|
|
4065
|
-
this.sourceTime = this.time;
|
|
4066
|
-
// Continuation applies where the right terminal is held at a voltage, against a held left one.
|
|
4067
|
-
const [dl, dr] = [this.terms[0].drive, this.terms[1].drive];
|
|
4068
|
-
const held = (d) => d.kind === 'V' && !(d.R > 0);
|
|
4069
|
-
const target = sourceAt(dr.src, this.time), level = sourceAt(dl.src, this.time);
|
|
4070
|
-
const allowed = opts.continuation !== false;
|
|
4071
|
-
const canContinue = allowed && held(dl) && held(dr) && target !== level;
|
|
4072
|
-
const direct = this._directSteady();
|
|
4073
|
-
const u0 = Float64Array.from(this.u), u0Lo = Float64Array.from(this.uLo), v0 = Float64Array.from(this.termV);
|
|
4074
|
-
const restart = () => {
|
|
4075
|
-
this.u.set(u0);
|
|
4076
|
-
this.uLo.set(u0Lo);
|
|
4077
|
-
this.termV.set(v0);
|
|
4078
|
-
this.computeConcentrations();
|
|
4079
|
-
};
|
|
4080
|
-
const solved = (q) => {
|
|
4081
|
-
if (q.converged) {
|
|
4082
|
-
this.solvedV = [level, target];
|
|
4083
|
-
this.stepped = false;
|
|
4084
|
-
}
|
|
4085
|
-
return q;
|
|
4086
|
-
};
|
|
4087
|
-
const after = (q, r) => ({ ...q, steps: q.steps + r.steps, iterations: q.iterations + r.iterations });
|
|
4088
|
-
|
|
4089
|
-
// 1. Newton from here. Where a continuation could follow, one direct attempt only, rather
|
|
4090
|
-
// than the pseudo-transient ramp straight away.
|
|
4091
|
-
const quick = (canContinue || this.hasGeneration) && direct && allowed;
|
|
4092
|
-
let r = this._steadySteps(quick ? { ...opts, maxSteps: 1 } : opts);
|
|
4093
|
-
if (r.converged) return solved(r);
|
|
4094
|
-
// A transient's state can be a worse start than a cold one: 1 ns after a lit MOS capacitor's
|
|
4095
|
-
// gate jumps, its surface is in deep depletion, minority carriers ten orders below where they
|
|
4096
|
-
// end, and Newton fails from there where it converges from the layout for the drives (and
|
|
4097
|
-
// the stages below took 9000 iterations from there, 2000 from the layout). What a steady
|
|
4098
|
-
// solve keeps it took from the state already, so from here on it goes as a cold device would.
|
|
4099
|
-
if (this.stepped && direct && allowed) {
|
|
4100
|
-
this._refreshSources();
|
|
4101
|
-
this.initFromComposition();
|
|
4102
|
-
u0.set(this.u);
|
|
4103
|
-
u0Lo.set(this.uLo);
|
|
4104
|
-
const q = this._steadySteps(quick ? { ...opts, maxSteps: 1 } : opts);
|
|
4105
|
-
if (q.converged) return solved(after(q, r));
|
|
4106
|
-
restart();
|
|
4107
|
-
r = after(q, r);
|
|
4108
|
-
}
|
|
4109
|
-
|
|
4110
|
-
// 2. Continuation. Generation (e.g. light) holding the device far from equilibrium: ramp it up
|
|
4111
|
-
// from nearly nothing, each solve warm from the last.
|
|
4112
|
-
let dimmer = false;
|
|
4113
|
-
if (this.hasGeneration && allowed) {
|
|
4114
|
-
restart();
|
|
4115
|
-
this.dimmer = false;
|
|
4116
|
-
const g = this._generationContinuation(opts, r);
|
|
4117
|
-
dimmer = this.dimmer;
|
|
4118
|
-
if (g.converged) return solved(g);
|
|
4119
|
-
r = g;
|
|
4120
|
-
}
|
|
4121
|
-
// A terminal driven by a current (open circuit, say): hold it at a voltage instead, march the
|
|
4122
|
-
// voltage until the current crosses its target, and float it from there.
|
|
4123
|
-
if (allowed && this.floating.some((k) => this.terms[k].drive.kind === 'I' && !this._bound(k))) {
|
|
4124
|
-
restart();
|
|
4125
|
-
const q = this._floatingContinuation(opts, r);
|
|
4126
|
-
if (q.converged) return q;
|
|
4127
|
-
r = q;
|
|
4128
|
-
}
|
|
4129
|
-
if (canContinue) {
|
|
4130
|
-
// The right terminal's voltage ramped to its target: from the contacts' voltages at the last
|
|
4131
|
-
// converged solve when the state is that solution (a warm start), the left one first where
|
|
4132
|
-
// it moved, the right held where it was; otherwise from level terminals, consistent with a
|
|
4133
|
-
// cold start.
|
|
4134
|
-
restart();
|
|
4135
|
-
const was = this.solvedV;
|
|
4136
|
-
let c;
|
|
4137
|
-
if (was) {
|
|
4138
|
-
c = was[0] === level ? { ...r, converged: true, ramped: false } : this._continuation(opts, 0, was[0], level, r, true, was[1]);
|
|
4139
|
-
// (Ramping the right from level terminals, solve there first: a sweep's start.)
|
|
4140
|
-
if (c.converged) c = this._continuation(opts, 1, was[1], target, c, c.ramped || was[1] !== level);
|
|
4141
|
-
} else c = this._continuation(opts, 1, level, target, r, false);
|
|
4142
|
-
if (c.converged) return solved(c);
|
|
4143
|
-
// 3. The pseudo-transient ramp, from the start.
|
|
4144
|
-
restart();
|
|
4145
|
-
return solved(after(this._steadySteps(opts), c));
|
|
4146
|
-
}
|
|
4147
|
-
// 3. The pseudo-transient ramp (where the first attempt wasn't it already): from the dimmer
|
|
4148
|
-
// light's solution where the light's continuation got anywhere (a lit floating base charges
|
|
4149
|
-
// from there as it would in time), else from the start.
|
|
4150
|
-
if (!quick) return r;
|
|
4151
|
-
if (!dimmer) restart();
|
|
4152
|
-
else this.computeConcentrations();
|
|
4153
|
-
return after(this._steadySteps(opts), r);
|
|
4154
|
-
}
|
|
4155
|
-
|
|
4156
|
-
// Whether terminal k, driven by a current, feeds a combination that it alone can change, or
|
|
4157
|
-
// only charges a capacitance (so that its steady states are one per amount or charge, not one
|
|
4158
|
-
// per voltage).
|
|
4159
|
-
_bound(k) {
|
|
4160
|
-
return this.constraints.some((cs) => cs.terminals.includes(k)) || chargeOnly(this, k);
|
|
4161
|
-
}
|
|
4162
|
-
|
|
4163
|
-
// A cold steady solve with a terminal driven by a current, which can fail where the same state
|
|
4164
|
-
// is easy warm (a solar cell's open circuit, from cold on a fine grid): the terminal held at a
|
|
4165
|
-
// voltage instead, whose steady solves have their own continuation, the voltage marched from
|
|
4166
|
-
// the start's until the current crosses its target, the crossing narrowed by bisection, and
|
|
4167
|
-
// the terminal floated again from beside it.
|
|
4168
|
-
_floatingContinuation(opts, r) {
|
|
4169
|
-
const k = this.floating.find((j) => this.terms[j].drive.kind === 'I' && !this._bound(j));
|
|
4170
|
-
const drive = this.terms[k].drive, target = sourceAt(drive.src, this.time);
|
|
4171
|
-
let steps = r.steps ?? 0, iterations = r.iterations ?? 0;
|
|
4172
|
-
const history = (r.history ?? []).slice();
|
|
4173
|
-
const hold = (V) => {
|
|
4174
|
-
this.terms[k].drive = { kind: 'V', src: { value: V }, R: 0 };
|
|
4175
|
-
this.redrive();
|
|
4176
|
-
const q = this._steadyFromHere(opts);
|
|
4177
|
-
steps += q.steps ?? 0;
|
|
4178
|
-
iterations += q.iterations ?? 0;
|
|
4179
|
-
history.push({ held: V, converged: q.converged });
|
|
4180
|
-
if (q.converged) seen.push([V, this.termI[k]]);
|
|
4181
|
-
return q.converged ? this.termI[k] - target : NaN;
|
|
4182
|
-
};
|
|
4183
|
-
const fail = () => ({ converged: false, steps, iterations, history });
|
|
4184
|
-
const seen = []; // [V, I] of each held solve that converged
|
|
4185
|
-
let Va = this.termV[k], fa, Vb, fb;
|
|
4186
|
-
try {
|
|
4187
|
-
fa = hold(Va);
|
|
4188
|
-
if (!Number.isFinite(fa)) {
|
|
4189
|
-
Va = 0;
|
|
4190
|
-
fa = hold(Va);
|
|
4191
|
-
}
|
|
4192
|
-
if (!Number.isFinite(fa)) return fail();
|
|
4193
|
-
// March, the step growing: up where the current is below its target, since raising a
|
|
4194
|
-
// terminal's voltage raises the current into a passive device (the other way, from the
|
|
4195
|
-
// start again, if 20 V brings no crossing).
|
|
4196
|
-
const start = Va, f0 = fa;
|
|
4197
|
-
[Vb, fb] = [Va, fa];
|
|
4198
|
-
for (const dir of [fa < 0 ? 1 : -1, fa < 0 ? -1 : 1]) {
|
|
4199
|
-
[Va, fa] = [start, f0];
|
|
4200
|
-
if (dir !== (f0 < 0 ? 1 : -1)) hold(start); // (back to the start, for a warm march)
|
|
4201
|
-
let dV = 0.02;
|
|
4202
|
-
Vb = Va + dir * dV;
|
|
4203
|
-
fb = hold(Vb);
|
|
4204
|
-
while (Number.isFinite(fb) && Math.sign(fb) === Math.sign(fa) && fa !== 0 && Math.abs(Vb - start) < 20) {
|
|
4205
|
-
[Va, fa] = [Vb, fb];
|
|
4206
|
-
dV *= 1.5;
|
|
4207
|
-
Vb = Va + dir * dV;
|
|
4208
|
-
fb = hold(Vb);
|
|
4209
|
-
}
|
|
4210
|
-
if (Number.isFinite(fb) && (Math.sign(fb) !== Math.sign(fa) || fa === 0)) break;
|
|
4211
|
-
}
|
|
4212
|
-
if (!Number.isFinite(fb) || (Math.sign(fb) === Math.sign(fa) && fa !== 0)) {
|
|
4213
|
-
// What the held voltages passed, at the two extremes reached.
|
|
4214
|
-
if (seen.length > 1) {
|
|
4215
|
-
seen.sort((a, b) => a[0] - b[0]);
|
|
4216
|
-
this.unreached = { terminal: this.terms[k].name, target, low: seen[0], high: seen.at(-1) };
|
|
4217
|
-
}
|
|
4218
|
-
return fail();
|
|
4219
|
-
}
|
|
4220
|
-
// Bisect (each held solve warm from the last) to a tenth of a millivolt.
|
|
4221
|
-
for (let it = 0; it < 40 && Math.abs(Vb - Va) > 1e-4; it++) {
|
|
4222
|
-
const Vm = (Va + Vb) / 2, fm = hold(Vm);
|
|
4223
|
-
if (!Number.isFinite(fm)) return fail();
|
|
4224
|
-
if (Math.sign(fm) === Math.sign(fa)) [Va, fa] = [Vm, fm];
|
|
4225
|
-
else [Vb, fb] = [Vm, fm];
|
|
4226
|
-
}
|
|
4227
|
-
hold(Math.abs(fa) < Math.abs(fb) ? Va : Vb);
|
|
4228
|
-
} finally {
|
|
4229
|
-
this.terms[k].drive = drive;
|
|
4230
|
-
this.redrive();
|
|
4231
|
-
}
|
|
4232
|
-
const q = this._steadySteps(opts);
|
|
4233
|
-
steps += q.steps ?? 0;
|
|
4234
|
-
iterations += q.iterations ?? 0;
|
|
4235
|
-
history.push(...(q.history ?? []));
|
|
4236
|
-
if (q.converged) return { ...q, steps, iterations, history };
|
|
4237
|
-
// Floated, the system can lose what held it doesn't (a closed redox cell driven near its
|
|
4238
|
-
// limit: its bordered row 33 digits short), but a current-driven steady state is the held
|
|
4239
|
-
// one at the voltage that passes the target: found by held solves, by regula falsi
|
|
4240
|
-
// (Illinois) within the bracket, to 1e-10 of the target, or at open circuit of the largest
|
|
4241
|
-
// current the march saw (a target of 0 is never met exactly).
|
|
4242
|
-
const scale = Math.max(Math.abs(target), ...seen.map(([, I]) => Math.abs(I)));
|
|
4243
|
-
try {
|
|
4244
|
-
let side = 0;
|
|
4245
|
-
for (let it = 0; it < 60; it++) {
|
|
4246
|
-
const Vm = Va - (fa * (Vb - Va)) / (fb - fa), fm = hold(Vm);
|
|
4247
|
-
if (!Number.isFinite(fm)) return fail();
|
|
4248
|
-
if (Math.abs(fm) <= 1e-10 * scale) return { converged: true, steps, iterations, history, residual: Math.abs(fm) };
|
|
4249
|
-
if (Math.sign(fm) === Math.sign(fb)) {
|
|
4250
|
-
[Vb, fb] = [Vm, fm];
|
|
4251
|
-
if (side === -1) fa /= 2;
|
|
4252
|
-
side = -1;
|
|
4253
|
-
} else {
|
|
4254
|
-
[Va, fa] = [Vm, fm];
|
|
4255
|
-
if (side === 1) fb /= 2;
|
|
4256
|
-
side = 1;
|
|
4257
|
-
}
|
|
4258
|
-
}
|
|
4259
|
-
return fail();
|
|
4260
|
-
} finally {
|
|
4261
|
-
this.terms[k].drive = drive;
|
|
4262
|
-
this.redrive();
|
|
4263
|
-
}
|
|
4264
|
-
}
|
|
4265
|
-
|
|
4266
|
-
// Steady solves with the generation reactions' rates scaled from 1e-12 up to 1, ×100 a step
|
|
4267
|
-
// while they converge, smaller steps where they don't.
|
|
4268
|
-
_generationContinuation(opts, r) {
|
|
4269
|
-
const sub = this._directSteady() ? { ...opts, maxSteps: 1 } : opts;
|
|
4270
|
-
let s = 1e-30, factor = 100, steps = r.steps, iterations = r.iterations;
|
|
4271
|
-
const history = r.history.slice();
|
|
4272
|
-
try {
|
|
4273
|
-
this.generationScale = s;
|
|
4274
|
-
// (nearly dark: a cold start can need the full ramp, as a dark device's does)
|
|
4275
|
-
let q = this._steadySteps(sub);
|
|
4276
|
-
steps += q.steps;
|
|
4277
|
-
iterations += q.iterations;
|
|
4278
|
-
if (!q.converged) return { converged: false, steps, iterations, history };
|
|
4279
|
-
this.dimmer = true; // (the state is now a solution under dimmer light: a better start than cold)
|
|
4280
|
-
while (s < 1) {
|
|
4281
|
-
const next = Math.min(1, s * factor);
|
|
4282
|
-
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
4283
|
-
this.generationScale = next;
|
|
4284
|
-
q = this._steadySteps(sub);
|
|
4285
|
-
steps += q.steps;
|
|
4286
|
-
iterations += q.iterations;
|
|
4287
|
-
history.push({ generation: next, converged: q.converged });
|
|
4288
|
-
if (q.converged) {
|
|
4289
|
-
s = next;
|
|
4290
|
-
factor = Math.min(100, factor * factor);
|
|
4291
|
-
} else {
|
|
4292
|
-
this.u.set(u1);
|
|
4293
|
-
this.uLo.set(u1Lo);
|
|
4294
|
-
this.termV.set(v1);
|
|
4295
|
-
this.computeConcentrations();
|
|
4296
|
-
factor = Math.sqrt(factor);
|
|
4297
|
-
if (factor < 1.5) return { converged: false, steps, iterations, history }; // (crawling: time steps do better)
|
|
4298
|
-
}
|
|
4299
|
-
}
|
|
4300
|
-
return { converged: true, steps, iterations, history };
|
|
4301
|
-
} finally {
|
|
4302
|
-
this.generationScale = 1;
|
|
4303
|
-
}
|
|
4304
|
-
}
|
|
4305
|
-
|
|
4306
|
-
// Terminal k (a contact) ramped from `level` to `target` through steady solves, each warm from
|
|
4307
|
-
// the last (`hold`: the other contact's voltage meanwhile, if not its own). A warm ramp that has
|
|
4308
|
-
// nowhere to go converges only if what it started from did (`ramped`): the state it carries
|
|
4309
|
-
// must be some ramp's solution, never just the one a failed solve left.
|
|
4310
|
-
_continuation(opts, k, level, target, r, warm, hold) {
|
|
4311
|
-
const sub = this._directSteady() ? { ...opts, maxSteps: 1 } : opts;
|
|
4312
|
-
let V = level, dV = (target - level) / 8, steps = r.steps, iterations = r.iterations;
|
|
4313
|
-
const history = r.history.slice();
|
|
4314
|
-
if (warm && V === target) return { ...r, converged: r.ramped === true, steps, iterations, history, ramped: r.ramped };
|
|
4315
|
-
try {
|
|
4316
|
-
this.sourceOverride.set(k, V);
|
|
4317
|
-
if (hold !== undefined) this.sourceOverride.set(1 - k, hold);
|
|
4318
|
-
let q;
|
|
4319
|
-
if (!warm) {
|
|
4320
|
-
q = this._steadySteps(opts);
|
|
4321
|
-
if (!q.converged) {
|
|
4322
|
-
// A cold device's start was laid out for the target (a set() before the first solve
|
|
4323
|
-
// builds it there): lay it out again, level.
|
|
4324
|
-
steps += q.steps;
|
|
4325
|
-
iterations += q.iterations;
|
|
4326
|
-
this._refreshSources();
|
|
4327
|
-
this.initFromComposition();
|
|
4328
|
-
q = this._steadySteps(opts);
|
|
4329
|
-
}
|
|
4330
|
-
// (lit, the level start may need the light ramped up too)
|
|
4331
|
-
if (!q.converged && this.hasGeneration) q = this._generationContinuation(opts, q);
|
|
4332
|
-
steps += q.steps;
|
|
4333
|
-
iterations += q.iterations;
|
|
4334
|
-
if (!q.converged) return { ...r, steps, iterations };
|
|
4335
|
-
}
|
|
4336
|
-
while (V !== target) {
|
|
4337
|
-
const next = Math.abs(target - V) <= Math.abs(dV) ? target : V + dV;
|
|
4338
|
-
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
4339
|
-
this.sourceOverride.set(k, next);
|
|
4340
|
-
q = this._steadySteps(sub);
|
|
4341
|
-
steps += q.steps;
|
|
4342
|
-
iterations += q.iterations;
|
|
4343
|
-
history.push({ continuation: next, converged: q.converged });
|
|
4344
|
-
if (q.converged) {
|
|
4345
|
-
V = next;
|
|
4346
|
-
dV *= 1.5;
|
|
4347
|
-
} else {
|
|
4348
|
-
this.u.set(u1);
|
|
4349
|
-
this.uLo.set(u1Lo);
|
|
4350
|
-
this.termV.set(v1);
|
|
4351
|
-
this.computeConcentrations();
|
|
4352
|
-
dV /= 4;
|
|
4353
|
-
if (Math.abs(dV) < 1e-6 * Math.abs(target - level)) return { converged: false, steps, iterations, history };
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4356
|
-
return { converged: true, steps, iterations, history, ramped: true };
|
|
4357
|
-
} finally {
|
|
4358
|
-
this.sourceOverride.delete(k);
|
|
4359
|
-
if (hold !== undefined) this.sourceOverride.delete(1 - k);
|
|
4360
|
-
this._refreshSources();
|
|
4361
|
-
}
|
|
4362
|
-
}
|
|
4363
|
-
|
|
4364
|
-
_steadySteps({ maxSteps = 80, tol = 1e-11 } = {}) {
|
|
4365
|
-
const time = this.time;
|
|
4366
|
-
const tau = this.slowestTime();
|
|
4367
|
-
const direct = this._directSteady();
|
|
4368
|
-
const giant = direct ? Infinity : 1e6 * tau;
|
|
4369
|
-
let dt = giant, grow = 10; // (after a failure, dt grows by less, then back up to ×10)
|
|
4370
|
-
let triedDirect = 0;
|
|
4371
|
-
let totalIter = 0, steps = 0, converged = false;
|
|
4372
|
-
const history = [];
|
|
4373
|
-
while (steps < maxSteps) {
|
|
4374
|
-
steps++;
|
|
4375
|
-
// Restore conserved amounts exactly before each huge step; the step then re-solves, so
|
|
4376
|
-
// the final state satisfies every equation.
|
|
4377
|
-
if (!direct && dt >= giant) this._renormalizeSpectators();
|
|
4378
|
-
let r = this.step(dt);
|
|
4379
|
-
totalIter += r.iterations;
|
|
4380
|
-
history.push({ dt, converged: r.converged, iterations: r.iterations });
|
|
4381
|
-
if (!r.converged && dt === Infinity && steps === 1) {
|
|
4382
|
-
// A large change can overshoot Newton's usual damping: retry with a tighter limit.
|
|
4383
|
-
r = this.step(dt, { maxStep: 3, maxIter: 80 });
|
|
4384
|
-
totalIter += r.iterations;
|
|
4385
|
-
history.push({ dt, converged: r.converged, iterations: r.iterations, maxStep: 3 });
|
|
4386
|
-
}
|
|
4387
|
-
if (!r.converged) {
|
|
4388
|
-
// Pseudo-transient continuation: down from a slow time scale until a step converges,
|
|
4389
|
-
// as far as the fastest (slow ions mustn't stop it short of what a cold start needs).
|
|
4390
|
-
dt = dt >= giant ? tau * 1e-6 : dt / 4;
|
|
4391
|
-
grow = 2;
|
|
4392
|
-
if (dt < Math.min(tau * 1e-15, this.fastestTime() * 1e-2)) break;
|
|
4393
|
-
continue;
|
|
4394
|
-
}
|
|
4395
|
-
if (dt === Infinity) {
|
|
4396
|
-
converged = true; // the steady equations themselves were solved
|
|
4397
|
-
break;
|
|
4398
|
-
}
|
|
4399
|
-
// Past the slowest diffusion time, the direct solve may already reach from here: try it
|
|
4400
|
-
// once a decade (the state kept if it fails), rather than step on to the giant dt through
|
|
4401
|
-
// steps that can fail for slow ions' sake.
|
|
4402
|
-
if (direct && dt < giant && dt >= tau && dt >= 10 * triedDirect) {
|
|
4403
|
-
triedDirect = dt;
|
|
4404
|
-
const snap = this._snapshot();
|
|
4405
|
-
const d = this.step(Infinity);
|
|
4406
|
-
totalIter += d.iterations;
|
|
4407
|
-
history.push({ dt: Infinity, converged: d.converged, iterations: d.iterations });
|
|
4408
|
-
if (d.converged) {
|
|
4409
|
-
converged = true;
|
|
4410
|
-
break;
|
|
4411
|
-
}
|
|
4412
|
-
this._restore(snap);
|
|
4413
|
-
}
|
|
4414
|
-
if (dt >= giant && this._maxPotentialStep(this._diff()) < tol) {
|
|
4415
|
-
converged = true;
|
|
4416
|
-
if (dt > giant) {
|
|
4417
|
-
// Finish at the base giant step, where the storage term pins amounts tightly.
|
|
4418
|
-
this._renormalizeSpectators();
|
|
4419
|
-
const f = this.step(giant);
|
|
4420
|
-
totalIter += f.iterations;
|
|
4421
|
-
converged = f.converged;
|
|
4422
|
-
}
|
|
4423
|
-
break;
|
|
4424
|
-
}
|
|
4425
|
-
if (dt < giant) {
|
|
4426
|
-
dt *= grow; // ramping up after a failure
|
|
4427
|
-
grow = Math.min(10, grow * 1.5);
|
|
4428
|
-
if (dt > 1e6 * tau) dt = giant; // then the direct steady solve (or the giant step)
|
|
4429
|
-
} else if (dt < 1e6 * giant) {
|
|
4430
|
-
dt *= 10; // conserved amounts present and still moving: let dt keep growing (capped)
|
|
4431
|
-
}
|
|
4432
|
-
}
|
|
4433
|
-
this.time = time;
|
|
4434
|
-
this.history = []; // a steady solve isn't a trajectory: transients restart from here
|
|
4435
|
-
this.dtNext = undefined;
|
|
4436
|
-
if (converged) {
|
|
4437
|
-
this.stretches.forEach((st, k) => {
|
|
4438
|
-
if (!st.connected) return;
|
|
4439
|
-
this.referenceAmounts[k] = this.amount(st);
|
|
4440
|
-
this.boundaryIntake[k] = 0;
|
|
4441
|
-
});
|
|
4442
|
-
}
|
|
4443
|
-
return { converged, steps, iterations: totalIter, history };
|
|
4444
|
-
}
|
|
4445
|
-
|
|
4446
|
-
// Huge steps pin each conserved amount only through a tiny storage term, so round-off can let
|
|
4447
|
-
// it creep. Shift each spectator's level uniformly to restore its amount exactly (in one step
|
|
4448
|
-
// for ideal statistics, where c ∝ e^η; by Newton on the shift otherwise).
|
|
4449
|
-
_renormalizeSpectators() {
|
|
4450
|
-
const { n, M, u } = this;
|
|
4451
|
-
let changed = false;
|
|
4452
|
-
this.stretches.forEach((st, k) => {
|
|
4453
|
-
// A floating conductor holds its charge on its faces, not as an amount: the huge steps keep
|
|
4454
|
-
// it, and there's nothing here to shift.
|
|
4455
|
-
if (!st.spectator || this.model.materials[this.model.regions[st.regions[0]].material].conductor) return;
|
|
4456
|
-
const want = this.referenceAmounts[k];
|
|
4457
|
-
const ideal = this.nodeIdeal.subarray(st.nodes[0], st.nodes[1] + 1).every((v) => v === 1);
|
|
4458
|
-
for (let it = 0; it < (ideal ? 1 : 30); it++) {
|
|
4459
|
-
const now = this.amount(st);
|
|
4460
|
-
if (!(now > 0) || now === want) return;
|
|
4461
|
-
let shift;
|
|
4462
|
-
if (ideal) shift = Math.log(want / now);
|
|
4463
|
-
else {
|
|
4464
|
-
let dA = 0;
|
|
4465
|
-
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
4466
|
-
const dc = this.nodeIdeal[g] ? this.c[g * n + st.species] : this.K[g * n * n + st.species * (n + 1)];
|
|
4467
|
-
dA += this.model.grid.vol[g] * dc;
|
|
4468
|
-
}
|
|
4469
|
-
shift = (want - now) / dA;
|
|
4470
|
-
if (!(Math.abs(shift) > 1e-16)) return;
|
|
4471
|
-
}
|
|
4472
|
-
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) u[this.blockOfNode[g] * M + 1 + st.species] += shift;
|
|
4473
|
-
changed = true;
|
|
4474
|
-
if (!ideal) this.computeConcentrations();
|
|
4475
|
-
}
|
|
4476
|
-
});
|
|
4477
|
-
if (changed) this.computeConcentrations();
|
|
4478
|
-
}
|
|
4479
|
-
|
|
4480
|
-
_diff() {
|
|
4481
|
-
const d = this.delta; // reuse as scratch
|
|
4482
|
-
const { fullOf } = this;
|
|
4483
|
-
for (let j = 0; j < fullOf.length; j++) d[j] = this.u[fullOf[j]] - this.uPrev[fullOf[j]];
|
|
4484
|
-
return d;
|
|
4485
|
-
}
|
|
1007
|
+
// The assembly's methods, the steady solves' and the small signal's, defined with their contracts
|
|
1008
|
+
// in src/assemble.js, src/regions.js, src/boundaries.js, src/steady.js and src/impedance.js, are
|
|
1009
|
+
// the Solver's own.
|
|
1010
|
+
for (const part of [Assembly, RegionTerms, BoundaryTerms, SteadySolves, SmallSignal]) {
|
|
1011
|
+
for (const [k, d] of Object.entries(Object.getOwnPropertyDescriptors(part.prototype))) if (k !== 'constructor') Object.defineProperty(Solver.prototype, k, d);
|
|
4486
1012
|
}
|