driftlet 0.9.0 → 0.11.4
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 +47 -37
- package/docs/device.md +142 -96
- package/docs/kit.md +5 -0
- package/docs/numerics.md +197 -26
- package/docs/reliability.md +122 -0
- package/llms.txt +111 -7
- package/package.json +3 -1
- package/src/blockTridiagonal.js +17 -4
- package/src/checks.js +44 -33
- package/src/describe.js +15 -9
- package/src/device.js +16 -5
- package/src/difference.js +142 -0
- package/src/index.js +39 -10
- package/src/polarization.js +51 -16
- package/src/solution.js +29 -11
- package/src/solver.js +1115 -165
- package/src/types.js +4 -0
- package/types/blockTridiagonal.d.ts +9 -1
- package/types/checks.d.ts +3 -3
- package/types/difference.d.ts +30 -0
- package/types/index.d.ts +3 -1
- package/types/live.d.ts +1 -0
- package/types/polarization.d.ts +8 -2
- package/types/solver.d.ts +65 -12
- package/types/types.d.ts +6 -0
package/src/solver.js
CHANGED
|
@@ -2,7 +2,65 @@
|
|
|
2
2
|
// and its derivative g′(a) = α e^{αa} + (1−α) e^{−(1−α)a}.
|
|
3
3
|
function bvFactor(a, alpha) {
|
|
4
4
|
const e = Math.exp(-(1 - alpha) * a);
|
|
5
|
-
|
|
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;
|
|
6
64
|
}
|
|
7
65
|
|
|
8
66
|
// Solve the K×K complex system (Ar + i Ai) x = (last column), partial pivoting by modulus.
|
|
@@ -37,8 +95,13 @@ function complexSolve(Ar, Ai, K) {
|
|
|
37
95
|
}
|
|
38
96
|
|
|
39
97
|
// GMRES(m), restarted, for a complex system op(x) = b, with vectors as [re, im] pairs and op
|
|
40
|
-
// already preconditioned; x holds the starting guess and receives the answer.
|
|
41
|
-
|
|
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 } = {}) {
|
|
42
105
|
const N = b[0].length;
|
|
43
106
|
const cdot = (a, c) => {
|
|
44
107
|
let re = 0, im = 0;
|
|
@@ -50,15 +113,24 @@ function gmres(op, b, x, { m = 12, restarts = 4, tol = 1e-8 } = {}) {
|
|
|
50
113
|
};
|
|
51
114
|
const norm = (a) => Math.sqrt(cdot(a, a)[0]);
|
|
52
115
|
const bn = norm(b) || 1;
|
|
53
|
-
|
|
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++) {
|
|
54
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])];
|
|
55
|
-
const beta = norm(r);
|
|
56
|
-
if (
|
|
57
|
-
|
|
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))]];
|
|
58
130
|
const H = Array.from({ length: m + 1 }, () => Array.from({ length: m }, () => [0, 0]));
|
|
59
131
|
const cs = [], sn = [], g = Array.from({ length: m + 1 }, () => [0, 0]);
|
|
60
|
-
g[0] = [beta, 0];
|
|
61
|
-
let k = 0
|
|
132
|
+
g[0] = [beta * bn, 0];
|
|
133
|
+
let k = 0;
|
|
62
134
|
for (; k < m; k++) {
|
|
63
135
|
const w = op(V[k]);
|
|
64
136
|
for (let i = 0; i <= k; i++) {
|
|
@@ -89,7 +161,6 @@ function gmres(op, b, x, { m = 12, restarts = 4, tol = 1e-8 } = {}) {
|
|
|
89
161
|
g[k + 1] = [-(s[0] * gr + s[1] * gi), -(s[0] * gi - s[1] * gr)];
|
|
90
162
|
if (!(Math.hypot(g[k + 1][0], g[k + 1][1]) > tol * bn)) {
|
|
91
163
|
k++;
|
|
92
|
-
converged = true;
|
|
93
164
|
break;
|
|
94
165
|
}
|
|
95
166
|
}
|
|
@@ -112,8 +183,10 @@ function gmres(op, b, x, { m = 12, restarts = 4, tol = 1e-8 } = {}) {
|
|
|
112
183
|
x[1][j] += yr * V[i][1][j] + yi * V[i][0][j];
|
|
113
184
|
}
|
|
114
185
|
}
|
|
115
|
-
if (converged) return; // (by its own residual estimate)
|
|
116
186
|
}
|
|
187
|
+
x[0].set(best[0]);
|
|
188
|
+
x[1].set(best[1]);
|
|
189
|
+
return { initial, residual: least };
|
|
117
190
|
}
|
|
118
191
|
|
|
119
192
|
// Discretisation and nonlinear solver.
|
|
@@ -145,6 +218,7 @@ import { BlockTridiagonal, ComplexBlockTridiagonal } from './blockTridiagonal.js
|
|
|
145
218
|
import { bernoulli, bernoulliDerivative } from './bernoulli.js';
|
|
146
219
|
import { EPS0, FARADAY } from './constants.js';
|
|
147
220
|
import { nextBreakpoint, sourceAt } from './device.js';
|
|
221
|
+
import { DifferenceTerms } from './difference.js';
|
|
148
222
|
import { DeviceError } from './errors.js';
|
|
149
223
|
import { powi, powr } from './pow.js';
|
|
150
224
|
|
|
@@ -224,10 +298,14 @@ export class Solver {
|
|
|
224
298
|
model.ports.forEach((port, k) => {
|
|
225
299
|
if (port.surface.length > 0) port.nodes.forEach((g, w) => ((this.surfPort[g] = k), (this.surfW[g] = w)));
|
|
226
300
|
});
|
|
301
|
+
// Each surface species of each port, as a column of what's conserved: [port, species].
|
|
302
|
+
this.surfCols = model.ports.flatMap((port, k) => port.surface.map((_, q) => [k, q]));
|
|
303
|
+
this.surfColOf = model.ports.map((port, k) => this.surfCols.findIndex(([kk]) => kk === k));
|
|
227
304
|
this.th = new Float64Array(nNodes * nSurf);
|
|
228
305
|
this.thOld = new Float64Array(nNodes * nSurf);
|
|
229
|
-
|
|
230
|
-
this.
|
|
306
|
+
// The bare fraction θ₀, directly (1 − Σθ cancels as θ → 1); 1 where there's no surface.
|
|
307
|
+
this.th0 = new Float64Array(nNodes).fill(1);
|
|
308
|
+
this.th0Old = new Float64Array(nNodes).fill(1);
|
|
231
309
|
|
|
232
310
|
// Per-node material data, flattened [g·n + i].
|
|
233
311
|
this.present = new Uint8Array(nNodes * n);
|
|
@@ -337,6 +415,7 @@ export class Solver {
|
|
|
337
415
|
this.time = 0;
|
|
338
416
|
this.lastDt = Infinity;
|
|
339
417
|
this.atSteady = false; // whether the state is a converged steady solve
|
|
418
|
+
this.lin = null; // (set while assembling for DifferenceTerms)
|
|
340
419
|
// Contact bookkeeping, filled by assemble(): particle flux toward +x through each contact,
|
|
341
420
|
// and the displacement there (the metal's surface charge for a neutral link).
|
|
342
421
|
this.contactFlux = { left: new Float64Array(n), right: new Float64Array(n) };
|
|
@@ -348,6 +427,13 @@ export class Solver {
|
|
|
348
427
|
this.portQStart = new Float64Array(model.ports.length);
|
|
349
428
|
this.portQOld = new Float64Array(model.ports.length);
|
|
350
429
|
this.portQEnd = null;
|
|
430
|
+
// And per node, the charge per volume the capacitances hold against the window (Σ aσ, over
|
|
431
|
+
// every port with one there: two gates on one channel, say; the ions hold −Σ aσ), its slope
|
|
432
|
+
// Σ a·C per volt, and the ports [terminal, index in its window].
|
|
433
|
+
this.qg = new Float64Array(grid.nNodes);
|
|
434
|
+
this.qgSlope = new Float64Array(grid.nNodes);
|
|
435
|
+
this.qgTerms = Array.from({ length: grid.nNodes }, () => []);
|
|
436
|
+
model.ports.forEach((port, k) => port.capacitance && port.nodes.forEach((g, w) => this.qgTerms[g].push([2 + k, w])));
|
|
351
437
|
this.contactDOld = { left: 0, right: 0 };
|
|
352
438
|
this.contactDStart = { left: 0, right: 0 };
|
|
353
439
|
// Terminals (the two contacts, then the ports): each one's voltage (V), held by its source or
|
|
@@ -402,12 +488,6 @@ export class Solver {
|
|
|
402
488
|
);
|
|
403
489
|
this.portRates = model.ports.map((port) => port.reactions.map(() => new Float64Array(port.nodes.length)));
|
|
404
490
|
|
|
405
|
-
// Whether any region is strictly neutral (ε = 0, not a conductor): see integrate().
|
|
406
|
-
this.strictlyNeutral = model.regions.some((reg) => {
|
|
407
|
-
const mat = materials[reg.material];
|
|
408
|
-
return !mat.conductor && mat.epsr === 0;
|
|
409
|
-
});
|
|
410
|
-
|
|
411
491
|
// Scales the generation reactions' rates during a steady solve's continuation (else 1).
|
|
412
492
|
this.generationScale = 1;
|
|
413
493
|
this.hasGeneration = this.rxs.some((rx) => rx.generation);
|
|
@@ -428,11 +508,15 @@ export class Solver {
|
|
|
428
508
|
// A port that only exchanges (an O₂ supply, a leak) is solved there too: left out, its window
|
|
429
509
|
// fell back on the ill-conditioned terms, and a closed neutral electrolyte whose potential is
|
|
430
510
|
// set only by a face reaction couldn't take a step.
|
|
511
|
+
// An end node is solved there too if its contact is closed to every ion and leaves φ alone.
|
|
431
512
|
const held = new Uint8Array(nNodes);
|
|
432
|
-
|
|
513
|
+
const closed = (side) => model.contacts[side].phi.type === 'neutral' && model.contacts[side].species.every((l, i) => species[i].z === 0 ? l.type !== 'equilibrium' : l.type === 'blocked');
|
|
514
|
+
this.closedEnd = { left: closed('left'), right: closed('right') };
|
|
515
|
+
held[0] = this.closedEnd.left ? 0 : 1;
|
|
516
|
+
held[nNodes - 1] = this.closedEnd.right ? 0 : 1;
|
|
433
517
|
for (const port of model.ports) if (port.species.some((l) => l.type === 'equilibrium')) for (const g of port.nodes) held[g] = 1;
|
|
434
518
|
this.chargeNode = new Uint8Array(nNodes);
|
|
435
|
-
const neutralFace = (f) => f
|
|
519
|
+
const neutralFace = (f) => (f === -1 ? this.closedEnd.left : f === model.interfaces.length ? this.closedEnd.right : model.interfaces[f].phi.type === 'neutral');
|
|
436
520
|
regions.forEach((reg, r) => {
|
|
437
521
|
const mat = materials[reg.material];
|
|
438
522
|
if (mat.conductor || mat.epsr !== 0) return;
|
|
@@ -443,6 +527,18 @@ export class Solver {
|
|
|
443
527
|
}
|
|
444
528
|
});
|
|
445
529
|
this.chargeNodes = Int32Array.from([...this.chargeNode.keys()].filter((g) => this.chargeNode[g]));
|
|
530
|
+
// An end node of a strictly neutral region that its contact holds (leaving it neutral: φ law
|
|
531
|
+
// bulk or neutral): the contact's current is the ions' charge passing, and their storage
|
|
532
|
+
// carries none under neutrality, nor does the box's net charge change (a bulk law's "D").
|
|
533
|
+
// Read from the box's balance, both come with round-off ∝ c/dt that cancels only roughly,
|
|
534
|
+
// which on short steps swamps the flux (a floating bath on a dilute side rattled). So there,
|
|
535
|
+
// on a transient step, the ions' storage goes in only after the contact has read its current,
|
|
536
|
+
// and the box's charge isn't counted as displacement (see _contact).
|
|
537
|
+
this.lateStorage = new Uint8Array(nNodes);
|
|
538
|
+
[0, nNodes - 1].forEach((g, k) => {
|
|
539
|
+
const mat = materials[regions[grid.nodeRegion[g]].material], law = contacts[k === 0 ? 'left' : 'right'].phi.type;
|
|
540
|
+
if (!mat.conductor && mat.epsr === 0 && (law === 'bulk' || law === 'neutral') && !this.chargeNode[g] && !this.phiUndefined[g] && this.nodeIdeal[g]) this.lateStorage[g] = 1;
|
|
541
|
+
});
|
|
446
542
|
this.combining = false;
|
|
447
543
|
this.bookkeeping = regions.map((_, r) => {
|
|
448
544
|
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r], nodes = [], segs = [];
|
|
@@ -456,6 +552,8 @@ export class Solver {
|
|
|
456
552
|
this._findStretches();
|
|
457
553
|
this.initFromComposition();
|
|
458
554
|
this.referenceAmounts = this.stretches.map((st) => this.amount(st));
|
|
555
|
+
// And what each surface species held then (a conserved combination through one counts it).
|
|
556
|
+
this.surfaceRef = Float64Array.from(this.surfCols, (_, j) => this.surfaceAmount(j));
|
|
459
557
|
// ∫ (flux in − flux out) dt through the contacts, per stretch, since the reference.
|
|
460
558
|
this.boundaryIntake = new Float64Array(this.stretches.length);
|
|
461
559
|
}
|
|
@@ -486,6 +584,11 @@ export class Solver {
|
|
|
486
584
|
const sizes = new Int32Array(nB), loc = (this.loc = new Int32Array(nB * M).fill(-1));
|
|
487
585
|
for (let b = 0; b < nB; b++) for (let r = 0; r < M; r++) if (active[b * M + r]) loc[b * M + r] = sizes[b]++;
|
|
488
586
|
const sys = (this.sys = new BlockTridiagonal(nB, sizes));
|
|
587
|
+
// A pivot that cancels to exactly nothing (a population held only through conductances
|
|
588
|
+
// 1e-16 of its neighbours', as GaAs's minority carriers ~1 per m³ beside a face) is
|
|
589
|
+
// perturbed, not thrown on: Newton then converges, its small updates meaning small residuals
|
|
590
|
+
// (the factorisation only amplifies), refining its solves where they stall.
|
|
591
|
+
sys.staticPivots = true;
|
|
489
592
|
const N = sys.size;
|
|
490
593
|
this.SINK = N;
|
|
491
594
|
const rix = (this.rix = new Int32Array(nB * M).fill(N)), fullOf = (this.fullOf = new Int32Array(N));
|
|
@@ -496,6 +599,7 @@ export class Solver {
|
|
|
496
599
|
}
|
|
497
600
|
this.res = new Float64Array(N + 1);
|
|
498
601
|
this.delta = new Float64Array(N + 1);
|
|
602
|
+
this.rowScale = new Float64Array(N + 1).fill(1); // each row's scaling in the factorised system
|
|
499
603
|
}
|
|
500
604
|
|
|
501
605
|
_factor() {
|
|
@@ -545,6 +649,8 @@ export class Solver {
|
|
|
545
649
|
if (reacting.length > 0) reactive = true;
|
|
546
650
|
const connected = leftOpen || rightOpen || linked.length > 0;
|
|
547
651
|
this.stretches.push({
|
|
652
|
+
leftOpen,
|
|
653
|
+
rightOpen,
|
|
548
654
|
linked, // ports that hold or exchange it
|
|
549
655
|
ports: [...new Set([...linked, ...reacting])], // ports that bring any in
|
|
550
656
|
species: i,
|
|
@@ -575,6 +681,33 @@ export class Solver {
|
|
|
575
681
|
st.mobile = true;
|
|
576
682
|
for (let q = st.regions[0]; q <= st.regions[1]; q++) if (!(materials[regions[q].material].D[st.species] > 0)) st.mobile = false;
|
|
577
683
|
}
|
|
684
|
+
// Stretches whose steady state is known outright: a species that no reaction or port touches,
|
|
685
|
+
// reached by one contact only (the other end blocked), carries no flux at steady state, so its
|
|
686
|
+
// level is flat at that contact's. Steady solves pin it there (_flatRows) rather than find it
|
|
687
|
+
// through its own conduction, which can be all but nothing: a MOS capacitor's inversion
|
|
688
|
+
// electrons reach the back contact only through a bulk with ~1e3 of them per cm³. (Not
|
|
689
|
+
// through flow or mixing, nor a concentrated material's cross-diffusion, where zero flux
|
|
690
|
+
// isn't a flat level.)
|
|
691
|
+
this.flatStretches = this.stretches.filter((st) => {
|
|
692
|
+
if (st.reactive || st.ports.length > 0 || !st.mobile || st.leftOpen === st.rightOpen) return false;
|
|
693
|
+
for (let q = st.regions[0]; q <= st.regions[1]; q++) {
|
|
694
|
+
const reg = regions[q], mat = materials[reg.material];
|
|
695
|
+
if (mat.conductor || !mat.ideal || reg.velocity !== 0 || reg.mixing > 0) return false;
|
|
696
|
+
}
|
|
697
|
+
return true;
|
|
698
|
+
});
|
|
699
|
+
// Likewise a spectator that's mobile throughout: no flux at steady state, so its level is flat,
|
|
700
|
+
// at a value its amount fixes. Steady solves hold its levels equal (_levelRows) rather than
|
|
701
|
+
// find them through its own conduction: blocked SO₄²⁻ driven from a zinc cathode's extended
|
|
702
|
+
// space charge is down to 1e-25 mol/m³ there, and the chain of conductances through it lost
|
|
703
|
+
// the steady system 14 digits. (Within one region: across a face, the rows that would hold
|
|
704
|
+
// the level and pin the face's flux leave a diagonal block singular.)
|
|
705
|
+
this.levelStretches = this.stretches.filter((st) => {
|
|
706
|
+
if (!st.spectator || !st.mobile || st.ports.length > 0 || st.regions[0] !== st.regions[1]) return false;
|
|
707
|
+
const reg = regions[st.regions[0]], mat = materials[reg.material];
|
|
708
|
+
return !mat.conductor && mat.ideal && reg.velocity === 0 && !(reg.mixing > 0);
|
|
709
|
+
});
|
|
710
|
+
this.flattening = false;
|
|
578
711
|
// Conserved amounts solved directly, each in place of one (redundant) balance row: a
|
|
579
712
|
// spectator's own amount, and each conserved combination of reacting stretches (the total
|
|
580
713
|
// iron of Fe³⁺, Fe²⁺ and FeCl²⁺, say), whose weighted balances sum to zero at steady state.
|
|
@@ -582,12 +715,14 @@ export class Solver {
|
|
|
582
715
|
// its faces; both are left to giant time steps.
|
|
583
716
|
this.constraints = [];
|
|
584
717
|
const conductor = (st) => materials[regions[st.regions[0]].material].conductor;
|
|
585
|
-
const add = (parts, rowStretch) => {
|
|
718
|
+
const add = (parts, rowStretch, surface = []) => {
|
|
586
719
|
const st = this.stretches[rowStretch];
|
|
587
|
-
|
|
720
|
+
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
721
|
+
for (const p of surface) nNodes += model.ports[this.surfCols[p.col][0]].nodes.length * model.ports[this.surfCols[p.col][0]].surface.length;
|
|
588
722
|
for (const p of parts) this.stretches[p.stretch].conserved = true;
|
|
589
723
|
this.constraints.push({
|
|
590
724
|
parts,
|
|
725
|
+
surface,
|
|
591
726
|
row: this.blockOfNode[st.nodes[0]] * this.M + 1 + st.species,
|
|
592
727
|
idx: new Int32Array(nNodes * this.M),
|
|
593
728
|
w: new Float64Array(nNodes * this.M),
|
|
@@ -599,12 +734,15 @@ export class Solver {
|
|
|
599
734
|
this.stretches.forEach((st, k) => {
|
|
600
735
|
if (st.spectator && st.mobile) add([{ stretch: k, w: 1 }], k);
|
|
601
736
|
});
|
|
737
|
+
const S = this.stretches.length;
|
|
602
738
|
for (const w of this.moieties) {
|
|
603
|
-
const parts = [...w.keys()].filter((k) => w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
604
|
-
|
|
739
|
+
const parts = [...w.keys()].filter((k) => k < S && w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
740
|
+
const surface = [...w.keys()].filter((k) => k >= S && w[k] !== 0).map((k) => ({ col: k - S, w: w[k] }));
|
|
741
|
+
if (parts.length === 0) continue; // (a surface's own: left to giant time steps)
|
|
742
|
+
if (parts.length === 1 && surface.length === 0 && this.stretches[parts[0].stretch].spectator) continue; // (above)
|
|
605
743
|
if (parts.some((p) => !this.stretches[p.stretch].mobile || conductor(this.stretches[p.stretch]))) continue;
|
|
606
744
|
// The row replaced: the basis vector's own stretch (its weight is 1, and no other vector has one).
|
|
607
|
-
add(parts, parts.find((p) => w[p.stretch] === 1)?.stretch ?? parts[0].stretch);
|
|
745
|
+
add(parts, parts.find((p) => w[p.stretch] === 1)?.stretch ?? parts[0].stretch, surface);
|
|
608
746
|
}
|
|
609
747
|
// A combination of immobile stretches (trap states: X⁰ + X⁻ under e⁻ + X⁰ = X⁻) is conserved
|
|
610
748
|
// node by node, since nothing carries it anywhere: at each node its weighted sum stays what
|
|
@@ -628,6 +766,66 @@ export class Solver {
|
|
|
628
766
|
reference: new Float64Array(sts[0].nodes[1] - sts[0].nodes[0] + 1),
|
|
629
767
|
});
|
|
630
768
|
}
|
|
769
|
+
// Islands: the pieces of a stretch between its faces. One that nothing else feeds (no
|
|
770
|
+
// contact, port or reaction) holds its level only through its faces' fluxes, which can be
|
|
771
|
+
// ~1e-24 of its own conduction (a tiny conductance at the face, or a neighbour that barely
|
|
772
|
+
// conducts): eliminated, the level is lost to round-off, and Newton settles at a wrong one with
|
|
773
|
+
// nothing to show for it. In steady solves the piece's balance summed over its nodes, where
|
|
774
|
+
// the internal fluxes cancel exactly and the faces' alone are left, replaces its first node's
|
|
775
|
+
// (see _applyIslands), once the plain solves converge (see newton).
|
|
776
|
+
this.islands = [];
|
|
777
|
+
this.pinnedIslands = [];
|
|
778
|
+
this.islandsOn = false; // (newton() turns them on, see there)
|
|
779
|
+
this.pins = [];
|
|
780
|
+
const used = new Set(this.constraints.map((cs) => cs.row));
|
|
781
|
+
for (const st of this.stretches) {
|
|
782
|
+
const i = st.species, [s0, s1] = st.regions;
|
|
783
|
+
if (!st.mobile || conductor(st)) continue;
|
|
784
|
+
for (let q0 = s0, q1 = s0; q1 <= s1; q1++) {
|
|
785
|
+
const p0 = q0;
|
|
786
|
+
q0 = q1 + 1;
|
|
787
|
+
if (p0 === s0 && q1 === s1) continue; // (uncut)
|
|
788
|
+
if ((p0 === s0 && st.leftOpen) || (q1 === s1 && st.rightOpen)) continue;
|
|
789
|
+
const inside = (q) => q >= p0 && q <= q1;
|
|
790
|
+
if (model.ports.some((port) => inside(port.region) && (port.species[i].type !== 'blocked' || port.reactions.some((rx) => rx.part.some((p) => p.side === 0 && p.i === i))))) continue;
|
|
791
|
+
let reactive = false;
|
|
792
|
+
for (let q = p0; q <= q1; q++) {
|
|
793
|
+
for (const rx of model.reactions) if (rx.kf[regions[q].material] > 0 && [...rx.reactants, ...rx.products].some((p) => p.i === i)) reactive = true;
|
|
794
|
+
}
|
|
795
|
+
for (let f = Math.max(0, p0 - 1); f <= Math.min(model.interfaces.length - 1, q1); f++) {
|
|
796
|
+
for (const rx of model.interfaces[f].reactions) if (rx.part.some((p) => p.i === i && inside(f + p.side))) reactive = true;
|
|
797
|
+
}
|
|
798
|
+
const row = this.blockOfNode[grid.regionStart[p0]] * this.M + 1 + i;
|
|
799
|
+
if (reactive || used.has(row)) continue;
|
|
800
|
+
used.add(row);
|
|
801
|
+
// The faces' fluxes into the piece: −A_f u at its left face, +A_f u at its right. Across
|
|
802
|
+
// a face that holds μ̄ level, u is set only by the edge balances, and eliminated, it can
|
|
803
|
+
// come out of the piece's own (G Δη, its two η all but equal: a neighbour 1e20 times
|
|
804
|
+
// less conductive, and the piece's level was seen through one face of two, and Newton
|
|
805
|
+
// swung it back and forth across both). There the outside edge node's balance is added:
|
|
806
|
+
// u cancels, and the outside's first segment carries the flux instead.
|
|
807
|
+
const faces = [], outside = [];
|
|
808
|
+
if (p0 > s0) faces.push([p0 - 1, -1, grid.regionEnd[p0 - 1]]);
|
|
809
|
+
if (q1 < s1) faces.push([q1, 1, grid.regionStart[q1 + 1]]);
|
|
810
|
+
for (const [f, , g] of faces) {
|
|
811
|
+
if (model.interfaces[f].links[i].type !== 'equilibrium' || g === 0 || g === this.nNodes - 1) continue;
|
|
812
|
+
if (model.ports.some((port) => port.nodes.includes(g))) continue;
|
|
813
|
+
outside.push(this.blockOfNode[g] * this.M + 1 + i);
|
|
814
|
+
}
|
|
815
|
+
this.islands.push({
|
|
816
|
+
stretch: st,
|
|
817
|
+
row,
|
|
818
|
+
flux: faces.map(([f]) => this.blockOfFace[f] * this.M + 1 + i),
|
|
819
|
+
weight: faces.map(([f, sg]) => sg * grid.area[grid.regionEnd[f]]),
|
|
820
|
+
outside,
|
|
821
|
+
idx: new Int32Array(faces.length + 3 * this.M * outside.length),
|
|
822
|
+
w: new Float64Array(faces.length + 3 * this.M * outside.length),
|
|
823
|
+
len: 0,
|
|
824
|
+
res: 0,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
for (const isl of this.islands) isl.outside = isl.outside.filter((o) => !used.has(o));
|
|
631
829
|
this.constrained = false;
|
|
632
830
|
}
|
|
633
831
|
|
|
@@ -644,6 +842,71 @@ export class Solver {
|
|
|
644
842
|
}
|
|
645
843
|
}
|
|
646
844
|
|
|
845
|
+
// The islands' summed balances (steady solves only): Σ A_f u over the faces into each, its
|
|
846
|
+
// first node's row pinned in the factorised matrix and the sum bordered (_solveBordered) like a
|
|
847
|
+
// conserved amount's. The pieces of a stretch held flat by its contact are left as they are.
|
|
848
|
+
_applyIslands() {
|
|
849
|
+
const { u, res } = this, R = this.rix, out = [];
|
|
850
|
+
for (const isl of this.islands) {
|
|
851
|
+
if (this.flattening && this.flatStretches.includes(isl.stretch)) continue;
|
|
852
|
+
const b0 = Math.floor(isl.row / this.M), r0 = isl.row % this.M;
|
|
853
|
+
if (this.loc[isl.row] < 0) continue;
|
|
854
|
+
// Gathered by column (the faces' u, then each outside row's entries, which cancel those u
|
|
855
|
+
// exactly where they meet), then the nonzeros kept.
|
|
856
|
+
const acc = this.islandRow ?? (this.islandRow = new Float64Array(this.sys.size + 1));
|
|
857
|
+
const cols = [];
|
|
858
|
+
const add = (col, v) => {
|
|
859
|
+
if (acc[col] === 0) cols.push(col);
|
|
860
|
+
acc[col] += v;
|
|
861
|
+
};
|
|
862
|
+
let t = 0;
|
|
863
|
+
isl.flux.forEach((o, j) => {
|
|
864
|
+
if (this.loc[o] < 0) return;
|
|
865
|
+
t += isl.weight[j] * u[o];
|
|
866
|
+
add(R[o], isl.weight[j]);
|
|
867
|
+
});
|
|
868
|
+
for (const o of isl.outside) {
|
|
869
|
+
if (this.loc[o] < 0) continue;
|
|
870
|
+
t += res[R[o]];
|
|
871
|
+
this._rowInto(o, add);
|
|
872
|
+
}
|
|
873
|
+
let len = 0;
|
|
874
|
+
for (const col of cols) {
|
|
875
|
+
if (col < this.sys.size && acc[col] !== 0) {
|
|
876
|
+
isl.idx[len] = col;
|
|
877
|
+
isl.w[len++] = acc[col];
|
|
878
|
+
}
|
|
879
|
+
acc[col] = 0;
|
|
880
|
+
}
|
|
881
|
+
isl.len = len;
|
|
882
|
+
isl.res = t;
|
|
883
|
+
this._replaceRow(b0, r0);
|
|
884
|
+
for (const B of this.termB) B[R[isl.row]] = 0;
|
|
885
|
+
this._j(b0, r0, b0, r0, 1);
|
|
886
|
+
res[R[isl.row]] = 0;
|
|
887
|
+
out.push(isl);
|
|
888
|
+
}
|
|
889
|
+
return out;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Row o's Jacobian entries, as add(compact column, value): the assembled ones, and the dilute
|
|
893
|
+
// kernels' kept aside in difference form (see _assembleDifference) when they are.
|
|
894
|
+
_rowInto(o, add) {
|
|
895
|
+
const { sys, M, nB } = this, b = Math.floor(o / M), l = this.loc[o];
|
|
896
|
+
const { A, B, C, sizes, offA, offB, offC, offX } = sys, m = sizes[b];
|
|
897
|
+
for (let k = 0; k < m; k++) if (B[offB[b] + l * m + k] !== 0) add(offX[b] + k, B[offB[b] + l * m + k]);
|
|
898
|
+
if (b > 0) for (let k = 0, mp = sizes[b - 1]; k < mp; k++) if (A[offA[b] + l * mp + k] !== 0) add(offX[b - 1] + k, A[offA[b] + l * mp + k]);
|
|
899
|
+
if (b < nB - 1) for (let k = 0, mn = sizes[b + 1]; k < mn; k++) if (C[offC[b] + l * mn + k] !== 0) add(offX[b + 1] + k, C[offC[b] + l * mn + k]);
|
|
900
|
+
if (this.lin) {
|
|
901
|
+
const into = this.linRow ?? (this.linRow = new Float64Array(sys.size + 1));
|
|
902
|
+
this.lin.capture(this.rix[o], 1, 0, into);
|
|
903
|
+
for (let k = 0; k < sys.size; k++) {
|
|
904
|
+
if (into[k] !== 0) add(k, into[k]);
|
|
905
|
+
into[k] = 0;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
647
910
|
// The local constraints' rows (steady solves only): Σ w c_i − reference at each node.
|
|
648
911
|
_applyLocalConstraints() {
|
|
649
912
|
const { n, M, c, res, dA: d } = this;
|
|
@@ -667,26 +930,32 @@ export class Solver {
|
|
|
667
930
|
// stoichiometry (a row per reaction, over the stretches it touches) together with a unit row
|
|
668
931
|
// for each stretch fed from outside. Exact integer data, so plain elimination will do.
|
|
669
932
|
_conservedMoieties() {
|
|
670
|
-
const { model, n } = this, S = this.stretches.length;
|
|
933
|
+
const { model, n } = this, S = this.stretches.length, C = S + this.surfCols.length;
|
|
671
934
|
const rows = [];
|
|
672
935
|
model.interfaces.forEach((itf, f) => {
|
|
673
936
|
for (const rx of itf.reactions) {
|
|
674
|
-
const row = new Float64Array(
|
|
937
|
+
const row = new Float64Array(C);
|
|
675
938
|
for (const p of rx.part) row[this.stretchOf[(f + p.side) * n + p.i]] += p.nu;
|
|
676
939
|
rows.push(row);
|
|
677
940
|
}
|
|
678
941
|
});
|
|
679
|
-
|
|
942
|
+
// An electrode surface's species are columns too, after the stretches (S + j for surface
|
|
943
|
+
// column j), so a combination that passes through them (A⁺ + e⁻ = S, S + e⁻ = B⁻) is conserved
|
|
944
|
+
// with what the surface holds.
|
|
945
|
+
model.ports.forEach((port, k) => {
|
|
680
946
|
for (const rx of port.reactions) {
|
|
681
|
-
const row = new Float64Array(
|
|
682
|
-
for (const p of rx.part)
|
|
947
|
+
const row = new Float64Array(C); // (the electrode's carrier is outside the device)
|
|
948
|
+
for (const p of rx.part) {
|
|
949
|
+
if (p.side === 0) row[this.stretchOf[port.region * n + p.i]] += p.nu;
|
|
950
|
+
else if (p.side === 2) row[S + this.surfColOf[k] + p.s] += p.nu;
|
|
951
|
+
}
|
|
683
952
|
rows.push(row);
|
|
684
953
|
}
|
|
685
|
-
}
|
|
954
|
+
});
|
|
686
955
|
model.regions.forEach((reg, q) => {
|
|
687
956
|
for (const rx of model.reactions) {
|
|
688
957
|
if (!(rx.kf[reg.material] > 0)) continue;
|
|
689
|
-
const row = new Float64Array(
|
|
958
|
+
const row = new Float64Array(C);
|
|
690
959
|
for (const p of rx.reactants) row[this.stretchOf[q * n + p.i]] -= p.nu;
|
|
691
960
|
for (const p of rx.products) row[this.stretchOf[q * n + p.i]] += p.nu;
|
|
692
961
|
rows.push(row);
|
|
@@ -694,34 +963,34 @@ export class Solver {
|
|
|
694
963
|
});
|
|
695
964
|
this.stretches.forEach((st, k) => {
|
|
696
965
|
if (!st.contactFed) return;
|
|
697
|
-
const row = new Float64Array(
|
|
966
|
+
const row = new Float64Array(C);
|
|
698
967
|
row[k] = 1;
|
|
699
968
|
rows.push(row);
|
|
700
969
|
});
|
|
701
970
|
// Reduced row echelon form; the free columns give the null space.
|
|
702
971
|
const pivots = [];
|
|
703
972
|
let r = 0;
|
|
704
|
-
for (let col = 0; col <
|
|
973
|
+
for (let col = 0; col < C && r < rows.length; col++) {
|
|
705
974
|
let best = r;
|
|
706
975
|
for (let i = r + 1; i < rows.length; i++) if (Math.abs(rows[i][col]) > Math.abs(rows[best][col])) best = i;
|
|
707
976
|
if (Math.abs(rows[best][col]) < 1e-9) continue;
|
|
708
977
|
[rows[r], rows[best]] = [rows[best], rows[r]];
|
|
709
978
|
const pr = rows[r], pv = pr[col];
|
|
710
|
-
for (let j = 0; j <
|
|
979
|
+
for (let j = 0; j < C; j++) pr[j] /= pv;
|
|
711
980
|
for (let i = 0; i < rows.length; i++) {
|
|
712
981
|
if (i === r || rows[i][col] === 0) continue;
|
|
713
982
|
const fct = rows[i][col];
|
|
714
|
-
for (let j = 0; j <
|
|
983
|
+
for (let j = 0; j < C; j++) rows[i][j] -= fct * pr[j];
|
|
715
984
|
}
|
|
716
985
|
pivots.push(col);
|
|
717
986
|
r++;
|
|
718
987
|
}
|
|
719
|
-
const isPivot = new Uint8Array(
|
|
988
|
+
const isPivot = new Uint8Array(C);
|
|
720
989
|
for (const col of pivots) isPivot[col] = 1;
|
|
721
990
|
const basis = [];
|
|
722
|
-
for (let free = 0; free <
|
|
991
|
+
for (let free = 0; free < C; free++) {
|
|
723
992
|
if (isPivot[free]) continue;
|
|
724
|
-
const w = new Float64Array(
|
|
993
|
+
const w = new Float64Array(C);
|
|
725
994
|
w[free] = 1;
|
|
726
995
|
pivots.forEach((col, i) => {
|
|
727
996
|
const v = -rows[i][free];
|
|
@@ -761,6 +1030,19 @@ export class Solver {
|
|
|
761
1030
|
}
|
|
762
1031
|
}
|
|
763
1032
|
}
|
|
1033
|
+
// What the electrodes' surfaces hold of it: Γ Σ v·a·θ, with ∂θ_s/∂η_x = θ_s (δ_sx − θ_x).
|
|
1034
|
+
for (const { col, w } of cs.surface) {
|
|
1035
|
+
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity, ns = port.surface.length;
|
|
1036
|
+
reference += w * this.surfaceRef[col];
|
|
1037
|
+
port.nodes.forEach((g, j) => {
|
|
1038
|
+
const f = w * G * vol[g] * this.portArea[k][j], b = this.blockOfNode[g], tq = this.th[g * this.nSurf + q];
|
|
1039
|
+
amount += f * tq;
|
|
1040
|
+
for (let x = 0; x < ns; x++) {
|
|
1041
|
+
cs.idx[len] = this.rix[b * M + 1 + n + x];
|
|
1042
|
+
cs.w[len++] = f * tq * ((q === x ? 1 : 0) - this.th[g * this.nSurf + x]);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
764
1046
|
cs.len = len;
|
|
765
1047
|
cs.res = amount - reference;
|
|
766
1048
|
const b0 = Math.floor(cs.row / M), r0 = cs.row % M;
|
|
@@ -772,7 +1054,8 @@ export class Solver {
|
|
|
772
1054
|
|
|
773
1055
|
// Solve the full Newton system, J δ = rhs, with the extra unknowns and rows that don't fit the
|
|
774
1056
|
// block-tridiagonal matrix T: the floating terminals' voltages (with their circuit rows), and
|
|
775
|
-
// the
|
|
1057
|
+
// the rows pinned in T: the spectators' conserved amounts (see _applyConstraints) and the
|
|
1058
|
+
// islands' summed balances (see _applyIslands). By low-rank
|
|
776
1059
|
// updates of T (Woodbury): δ = y + Σ_q Q_q μ_q − Σ_k X_k δV_k with y = T⁻¹ rhs, Q_q = T⁻¹ e_q
|
|
777
1060
|
// (the response to a unit pin) and X_k = T⁻¹ B_k (to a unit change of V_k), then a small dense
|
|
778
1061
|
// system for the μ (pins) and δV (terminals):
|
|
@@ -839,6 +1122,21 @@ export class Solver {
|
|
|
839
1122
|
}
|
|
840
1123
|
|
|
841
1124
|
|
|
1125
|
+
// An electrode surface at its starting coverages at node g: η = μ°/RT + ln(θ/θ₀).
|
|
1126
|
+
_surfaceStart(g) {
|
|
1127
|
+
const { n, M, u, model } = this, surf = model.ports[this.surfPort[g]].surface, b = this.blockOfNode[g];
|
|
1128
|
+
const bare = 1 - surf.reduce((t, sp) => t + sp.theta0, 0);
|
|
1129
|
+
surf.forEach((sp, q) => (u[b * M + 1 + n + q] = sp.mu0 / model.RT + Math.log(sp.theta0 / bare)));
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/** Amount (mol per unit area) of surface column j's species, Γ Σ v·a·θ, in the current state. */
|
|
1133
|
+
surfaceAmount(j) {
|
|
1134
|
+
const [k, q] = this.surfCols[j], port = this.model.ports[k], vol = this.model.grid.vol;
|
|
1135
|
+
let s = 0;
|
|
1136
|
+
port.nodes.forEach((g, w) => (s += vol[g] * this.portArea[k][w] * this.th[g * this.nSurf + q]));
|
|
1137
|
+
return port.surface[0].capacity * s;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
842
1140
|
/** Total amount (mol per unit area) of a stretch's species in the current state. */
|
|
843
1141
|
amount(stretch) {
|
|
844
1142
|
const { n, c, model } = this;
|
|
@@ -1078,20 +1376,30 @@ export class Solver {
|
|
|
1078
1376
|
const shift = level(t.side) - level(this.terms[held].side);
|
|
1079
1377
|
if (Number.isFinite(shift)) this.termV[k] = this.termV[held] + shift;
|
|
1080
1378
|
}
|
|
1081
|
-
|
|
1082
|
-
for (let g = 0; g < this.nNodes; g++) {
|
|
1083
|
-
const k = this.surfPort[g];
|
|
1084
|
-
if (k < 0) continue;
|
|
1085
|
-
const surf = model.ports[k].surface, b = this.blockOfNode[g], bare = 1 - surf.reduce((t, sp) => t + sp.theta0, 0);
|
|
1086
|
-
surf.forEach((sp, q) => (u[b * M + 1 + n + q] = sp.mu0 / model.RT + Math.log(sp.theta0 / bare)));
|
|
1087
|
-
}
|
|
1379
|
+
for (let g = 0; g < this.nNodes; g++) if (this.surfPort[g] >= 0) this._surfaceStart(g);
|
|
1088
1380
|
this.computeConcentrations();
|
|
1089
|
-
// A floating
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1381
|
+
// A floating capacitance starts uncharged (σ = 0 at the window's mean φ), as the starting
|
|
1382
|
+
// composition, neutral without it, assumes; with reactions too, its double layer then charges
|
|
1383
|
+
// toward their mixed potential.
|
|
1384
|
+
for (const k of this.floating) {
|
|
1385
|
+
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1386
|
+
if (!port?.capacitance) continue;
|
|
1387
|
+
let w = 0, sum = 0;
|
|
1388
|
+
port.nodes.forEach((g, j) => {
|
|
1389
|
+
if (this.phiUndefined[g]) return;
|
|
1390
|
+
const a = this.model.grid.vol[g] * this.portArea[t.index][j];
|
|
1391
|
+
w += a;
|
|
1392
|
+
sum += a * this.VT * u[this.blockOfNode[g] * M];
|
|
1393
|
+
});
|
|
1394
|
+
if (w > 0) this.termV[k] = port.capacitance.zeroCharge + sum / w;
|
|
1395
|
+
}
|
|
1396
|
+
// A floating electrode spread through a port (without a double layer) starts where its
|
|
1397
|
+
// reactions pass the current it's set (none, behind a resistance): at its mixed potential in
|
|
1398
|
+
// the start's composition, not level with a held terminal, which can be volts away and pass
|
|
1399
|
+
// an absurd current.
|
|
1092
1400
|
for (const k of this.floating) {
|
|
1093
1401
|
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1094
|
-
if (!port || port.reactions.length === 0) continue;
|
|
1402
|
+
if (!port || port.reactions.length === 0 || port.capacitance) continue;
|
|
1095
1403
|
const want = t.drive.kind === 'I' ? sourceAt(t.drive.src, this.time) : 0;
|
|
1096
1404
|
const current = (V) => {
|
|
1097
1405
|
this.termV[k] = V;
|
|
@@ -1130,8 +1438,7 @@ export class Solver {
|
|
|
1130
1438
|
pref *= p.side === 2 ? Math.exp(e * (u[o] + uLo[o] - port.surface[p.s].mu0 / RT)) : powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
1131
1439
|
}
|
|
1132
1440
|
if (rx.bare) pref *= this.th0[g];
|
|
1133
|
-
|
|
1134
|
-
return pref * (Math.abs(a) > 50 ? Math.exp(rx.alpha * a) - Math.exp(-(1 - rx.alpha) * a) : bvFactor(a, rx.alpha).g);
|
|
1441
|
+
return pref * bvFactor(a, rx.alpha).g;
|
|
1135
1442
|
}
|
|
1136
1443
|
|
|
1137
1444
|
// A non-ideal material's composition at φ̂ = ph: level-fixed species (mode 1) at their η,
|
|
@@ -1294,6 +1601,7 @@ export class Solver {
|
|
|
1294
1601
|
const { grid, materials, regions } = model;
|
|
1295
1602
|
sys.clear();
|
|
1296
1603
|
res.fill(0);
|
|
1604
|
+
if (this.lin) this.lin.reset(1 / dt);
|
|
1297
1605
|
this.computeConcentrations();
|
|
1298
1606
|
|
|
1299
1607
|
// Regions, each assembled by the kernel for its kind: a conductor (its carrier only), a
|
|
@@ -1325,8 +1633,13 @@ export class Solver {
|
|
|
1325
1633
|
// Terminals: ports (after every other term at their nodes, so a held level can read its
|
|
1326
1634
|
// flux), then contacts, then the circuit rows of the floating ones.
|
|
1327
1635
|
this._terminals(dt);
|
|
1636
|
+
if (this.flattening && dt === Infinity) this._flatRows();
|
|
1637
|
+
if (this.flattening && this.constrained && dt === Infinity) this._levelRows();
|
|
1328
1638
|
if (this.constrained && dt === Infinity && this.constraints.length > 0) this._applyConstraints();
|
|
1329
1639
|
if (this.constrained && dt === Infinity && this.localConstraints.length > 0) this._applyLocalConstraints();
|
|
1640
|
+
// Rows kept aside for _solveBordered: the conserved amounts' and the islands'.
|
|
1641
|
+
this.pinnedIslands = this.constrained && dt === Infinity && this.islandsOn ? this._applyIslands() : [];
|
|
1642
|
+
this.pins = this.constrained && dt === Infinity ? [...this.constraints, ...this.pinnedIslands] : [];
|
|
1330
1643
|
this.transformed = this.combining && dt !== Infinity;
|
|
1331
1644
|
if (this.transformed) this._chargeRows(dt);
|
|
1332
1645
|
}
|
|
@@ -1346,7 +1659,7 @@ export class Solver {
|
|
|
1346
1659
|
// charge in the Gauss row, written straight into the diagonal blocks by local index. A
|
|
1347
1660
|
// dielectric is the case with no species.
|
|
1348
1661
|
_nodesDilute(g0, g1, dt) {
|
|
1349
|
-
const { n, M, res, c, cOld, z, sys, loc, present, rhoFixed, blockOfNode } = this, R = this.rix, F = FARADAY;
|
|
1662
|
+
const { n, M, res, c, cOld, z, sys, loc, present, rhoFixed, blockOfNode, lin } = this, R = this.rix, F = FARADAY;
|
|
1350
1663
|
const JB = sys.B, sizes = sys.sizes, offB = sys.offB, vol = this.model.grid.vol;
|
|
1351
1664
|
for (let g = g0; g <= g1; g++) {
|
|
1352
1665
|
const b = blockOfNode[g], v = vol[g];
|
|
@@ -1357,22 +1670,27 @@ export class Solver {
|
|
|
1357
1670
|
continue;
|
|
1358
1671
|
}
|
|
1359
1672
|
let q = rhoFixed[g], dq = 0;
|
|
1673
|
+
const late = this.combining && this.lateStorage[g] === 1 && dt !== Infinity; // (the ions' storage: in _contact)
|
|
1360
1674
|
for (let i = 0; i < n; i++) {
|
|
1361
1675
|
const k = g * n + i;
|
|
1362
1676
|
if (!present[k]) continue;
|
|
1363
|
-
const ck = c[k], l = loc[lb + 1 + i];
|
|
1364
|
-
res[R[lb + 1 + i]] += (v * (ck - cOld[k])) / dt;
|
|
1365
|
-
JB[oB + l * m + l] += (v * ck) / dt;
|
|
1677
|
+
const ck = c[k], l = loc[lb + 1 + i], stores = !(late && z[i] !== 0);
|
|
1678
|
+
if (stores) res[R[lb + 1 + i]] += (v * (ck - cOld[k])) / dt;
|
|
1366
1679
|
q += F * z[i] * ck;
|
|
1367
1680
|
dq += F * z[i] * z[i] * ck;
|
|
1368
|
-
if (
|
|
1369
|
-
|
|
1370
|
-
|
|
1681
|
+
if (lin) {
|
|
1682
|
+
lin.node(R[lb + 1 + i], R[lb], z[i], stores ? v * ck : 0, -v * F * z[i] * ck);
|
|
1683
|
+
continue;
|
|
1684
|
+
}
|
|
1685
|
+
if (stores) {
|
|
1686
|
+
JB[oB + l * m + l] += (v * ck) / dt;
|
|
1687
|
+
if (p >= 0 && z[i] !== 0) JB[oB + l * m + p] += (-v * z[i] * ck) / dt;
|
|
1371
1688
|
}
|
|
1689
|
+
if (p >= 0 && z[i] !== 0) JB[oB + p * m + l] += -v * F * z[i] * ck;
|
|
1372
1690
|
}
|
|
1373
1691
|
if (p >= 0) {
|
|
1374
1692
|
res[R[lb]] -= v * q;
|
|
1375
|
-
JB[oB + p * m + p] += v * dq;
|
|
1693
|
+
if (!lin) JB[oB + p * m + p] += v * dq;
|
|
1376
1694
|
}
|
|
1377
1695
|
}
|
|
1378
1696
|
}
|
|
@@ -1459,6 +1777,7 @@ export class Solver {
|
|
|
1459
1777
|
for (let q = 0, o = offC[b] + lk * mn, s = offC[b] + li * mn; q < mn; q++) C[o + q] += w * C[s + q];
|
|
1460
1778
|
res[rk] += w * res[ri];
|
|
1461
1779
|
for (let t = 0; t < termB.length; t++) termB[t][rk] += w * termB[t][ri];
|
|
1780
|
+
if (this.lin) this.lin.combine(rk, ri, w);
|
|
1462
1781
|
}
|
|
1463
1782
|
// Columns: φ̂' = φ̂ with η fixed becomes φ̂ with η_i shifted by z_i, wherever node g's
|
|
1464
1783
|
// unknowns appear (its own rows, its neighbours', a terminal's current).
|
|
@@ -1494,6 +1813,18 @@ export class Solver {
|
|
|
1494
1813
|
}
|
|
1495
1814
|
}
|
|
1496
1815
|
res[R[lb]] -= v * q;
|
|
1816
|
+
// The pivot's row (charge continuity) left storage out, the ions' net charge being constant
|
|
1817
|
+
// under neutrality. Against a capacitance it isn't: neutrality makes it −(aσ + ρ_fixed)/F,
|
|
1818
|
+
// so their storage is that less what the step started with, Σ z c_old, over dt (in units
|
|
1819
|
+
// of the pivot's balance).
|
|
1820
|
+
if (this.qgTerms[g].length > 0) {
|
|
1821
|
+
const f = -v / (FARADAY * z[k] * dt);
|
|
1822
|
+
let qOld = this.rhoFixed[g];
|
|
1823
|
+
for (let i = 0; i < n; i++) if (present[gn + i]) qOld += F * z[i] * cOld[gn + i];
|
|
1824
|
+
res[rk] += f * (this.qg[g] + qOld);
|
|
1825
|
+
B[offB[b] + lk * m + p] += -f * this.qgSlope[g] * this.VT;
|
|
1826
|
+
for (const [kq, w] of this.qgTerms[g]) termB[kq][rk] += f * this.portArea[kq - 2][w] * this.model.ports[kq - 2].capacitance.C;
|
|
1827
|
+
}
|
|
1497
1828
|
}
|
|
1498
1829
|
}
|
|
1499
1830
|
|
|
@@ -1573,6 +1904,7 @@ export class Solver {
|
|
|
1573
1904
|
const mL = sys.sizes[bL], mR = sys.sizes[bR], pL = loc[bL * M], pR = loc[bR * M];
|
|
1574
1905
|
res[R[bL * M]] += D;
|
|
1575
1906
|
res[R[bR * M]] -= D;
|
|
1907
|
+
if (this.lin) return this.lin.displacement(R[bL * M], R[bR * M], k);
|
|
1576
1908
|
sys.B[sys.offB[bL] + pL * mL + pL] += k;
|
|
1577
1909
|
sys.C[sys.offC[bL] + pL * mR + pR] -= k;
|
|
1578
1910
|
sys.A[sys.offA[bR] + pR * mL + pL] -= k;
|
|
@@ -1586,7 +1918,7 @@ export class Solver {
|
|
|
1586
1918
|
// fluxes (e.g. majority carriers carrying a small current) don't vanish in the cancellation of
|
|
1587
1919
|
// two huge drift and diffusion terms.
|
|
1588
1920
|
_segmentsDilute(g0, g1, region, mat, vel) {
|
|
1589
|
-
const { n, M, u, uLo, res, c, z, sys, loc } = this, R = this.rix
|
|
1921
|
+
const { n, M, u, uLo, res, c, z, sys, loc, lin } = this, R = this.rix;
|
|
1590
1922
|
const { A: JA, B: JB, C: JC, sizes, offA, offB, offC } = sys;
|
|
1591
1923
|
const { segLength, segArea } = this.model.grid;
|
|
1592
1924
|
for (let s = g0; s < g1; s++) {
|
|
@@ -1611,6 +1943,10 @@ export class Solver {
|
|
|
1611
1943
|
const dNdPhiR = zi * dNdd;
|
|
1612
1944
|
res[R[bL * M + r]] += N;
|
|
1613
1945
|
res[R[bR * M + r]] -= N;
|
|
1946
|
+
if (lin) {
|
|
1947
|
+
lin.segment(R[bL * M + r], R[bR * M + r], R[bL * M], R[bR * M], zi, -gBc * E, dNdEtaR, zi * dNdd);
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1614
1950
|
// Row r of block bL (couplings to itself in B, to bR in C) and of bR (to bL in A).
|
|
1615
1951
|
const lL = loc[bL * M + r], lR = loc[bR * M + r];
|
|
1616
1952
|
const oBL = offB[bL] + lL * mL, oCL = offC[bL] + lL * mR, oAR = offA[bR] + lR * mL, oBR = offB[bR] + lR * mR;
|
|
@@ -2019,7 +2355,10 @@ export class Solver {
|
|
|
2019
2355
|
this.termDI[k] = 0;
|
|
2020
2356
|
this.termI[k] = 0;
|
|
2021
2357
|
}
|
|
2022
|
-
this.
|
|
2358
|
+
this.qg.fill(0);
|
|
2359
|
+
this.qgSlope.fill(0);
|
|
2360
|
+
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt, false));
|
|
2361
|
+
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt, true));
|
|
2023
2362
|
this._contact('left', dt);
|
|
2024
2363
|
this._contact('right', dt);
|
|
2025
2364
|
for (const k of this.floating) this._circuit(k);
|
|
@@ -2040,6 +2379,7 @@ export class Solver {
|
|
|
2040
2379
|
_captureRow(b, s, w, into) {
|
|
2041
2380
|
const l = this.loc[b * this.M + s];
|
|
2042
2381
|
if (l < 0 || w === 0) return;
|
|
2382
|
+
if (this.lin) this.lin.capture(this.rix[b * this.M + s], w, this.lin.rate, into);
|
|
2043
2383
|
const { sys, nB } = this, { sizes, offA, offB, offC, offX } = sys, m = sizes[b];
|
|
2044
2384
|
for (const [X, nb, off] of [['A', b - 1, offA], ['B', b, offB], ['C', b + 1, offC]]) {
|
|
2045
2385
|
if (nb < 0 || nb >= nB) continue;
|
|
@@ -2051,16 +2391,18 @@ export class Solver {
|
|
|
2051
2391
|
|
|
2052
2392
|
// An internal port (terminal k): its exchange with each node of its window, as a source per
|
|
2053
2393
|
// volume. A held ('equilibrium') level replaces the node's balance row; the source is then that
|
|
2054
|
-
// row's residual, read just before. Its current into the device is Σ z F × the sources.
|
|
2055
|
-
|
|
2394
|
+
// row's residual, read just before. Its current into the device is Σ z F × the sources. In two
|
|
2395
|
+
// passes over the ports: everything else, then the held levels (`holds`), so that a level reads
|
|
2396
|
+
// every other port's terms at its nodes, and none is added into its row after.
|
|
2397
|
+
_port(port, flux, k, dt, holds) {
|
|
2056
2398
|
const R = this.rix;
|
|
2057
2399
|
const { n, M, u, uLo, res, z, VT } = this;
|
|
2058
2400
|
const F = FARADAY, vol = this.model.grid.vol;
|
|
2059
2401
|
const B = this.termB[k], C = this.termC[k];
|
|
2060
|
-
flux.fill(0);
|
|
2402
|
+
if (!holds) flux.fill(0);
|
|
2061
2403
|
for (let i = 0; i < n; i++) {
|
|
2062
2404
|
const link = port.species[i];
|
|
2063
|
-
if (link.type === 'blocked') continue;
|
|
2405
|
+
if (link.type === 'blocked' || (link.type === 'equilibrium') !== holds) continue;
|
|
2064
2406
|
const r = 1 + i, target = this.portEta(port, i), zF = z[i] * F;
|
|
2065
2407
|
for (const g of port.nodes) {
|
|
2066
2408
|
const b = this.blockOfNode[g], o = b * M + r, v = vol[g];
|
|
@@ -2087,10 +2429,13 @@ export class Solver {
|
|
|
2087
2429
|
}
|
|
2088
2430
|
}
|
|
2089
2431
|
}
|
|
2432
|
+
if (holds) {
|
|
2433
|
+
for (let i = 0; i < n; i++) this.termI[k] += z[i] * F * flux[i];
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2090
2436
|
port.reactions.forEach((rx, x) => this._portReaction(port, rx, this.portArea[k - 2], this.portRates[k - 2][x], flux, k));
|
|
2091
2437
|
if (port.capacitance) this._portCapacitance(port, k, dt);
|
|
2092
2438
|
if (port.surface.length > 0 && Number.isFinite(dt)) this._surfaceStorage(port, dt);
|
|
2093
|
-
for (let i = 0; i < n; i++) this.termI[k] += z[i] * F * flux[i];
|
|
2094
2439
|
}
|
|
2095
2440
|
|
|
2096
2441
|
// A port's reaction at each node of its window: Butler–Volmer per area, as at a face, against
|
|
@@ -2167,6 +2512,8 @@ export class Solver {
|
|
|
2167
2512
|
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
2168
2513
|
const b = this.blockOfNode[g], s = vol[g] * area[w] * Cs;
|
|
2169
2514
|
const q = s * (V - zeroCharge - VT * (u[b * M] + uLo[b * M]));
|
|
2515
|
+
this.qg[g] += area[w] * Cs * (V - zeroCharge - VT * (u[b * M] + uLo[b * M]));
|
|
2516
|
+
this.qgSlope[g] += area[w] * Cs;
|
|
2170
2517
|
Q += q;
|
|
2171
2518
|
dQdV += s;
|
|
2172
2519
|
res[R[b * M]] -= q;
|
|
@@ -2221,10 +2568,13 @@ export class Solver {
|
|
|
2221
2568
|
const Ac = model.grid.area[g]; // the links' laws are per area: what they pass is that times A here
|
|
2222
2569
|
const dyn = Number.isFinite(dt);
|
|
2223
2570
|
|
|
2224
|
-
// Before anything is added, each balance residual is the flux into the device here.
|
|
2571
|
+
// Before anything is added, each balance residual is the flux into the device here. (A
|
|
2572
|
+
// closed end solved in charge rows has its ions' storage left out until later: none of them
|
|
2573
|
+
// passes, so their flux is zero.)
|
|
2574
|
+
const closedIons = this.combining && dyn && this.chargeNode[g] === 1;
|
|
2225
2575
|
let I = 0;
|
|
2226
2576
|
for (let i = 0; i < n; i++) {
|
|
2227
|
-
const active = this.loc[b * M + 1 + i] >= 0;
|
|
2577
|
+
const active = this.loc[b * M + 1 + i] >= 0 && !(closedIons && z[i] !== 0);
|
|
2228
2578
|
const nIn = active ? res[R[b * M + 1 + i]] : 0;
|
|
2229
2579
|
flux[i] = sgn * nIn; // (toward +x)
|
|
2230
2580
|
if (active && z[i] !== 0) {
|
|
@@ -2232,6 +2582,19 @@ export class Solver {
|
|
|
2232
2582
|
this._captureRow(b, 1 + i, z[i] * F, C);
|
|
2233
2583
|
}
|
|
2234
2584
|
}
|
|
2585
|
+
// The ions' storage, left out until now (see lateStorage): in each balance, and in what
|
|
2586
|
+
// each species brings in, but not in the current.
|
|
2587
|
+
if (this.combining && dyn && this.lateStorage[g] === 1) {
|
|
2588
|
+
const v = model.grid.vol[g], { cOld } = this;
|
|
2589
|
+
for (let i = 0; i < n; i++) {
|
|
2590
|
+
if (z[i] === 0 || this.loc[b * M + 1 + i] < 0) continue;
|
|
2591
|
+
const ci = c[g * n + i], st = (v * (ci - cOld[g * n + i])) / dt;
|
|
2592
|
+
res[R[b * M + 1 + i]] += st;
|
|
2593
|
+
flux[i] += sgn * st;
|
|
2594
|
+
this._j(b, 1 + i, b, 1 + i, (v * ci) / dt);
|
|
2595
|
+
this._j(b, 1 + i, b, 0, (-v * z[i] * ci) / dt);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2235
2598
|
|
|
2236
2599
|
// Conductance links: J (toward the device) = G (V_out − V_i), V_out = V_t + offset; exchange
|
|
2237
2600
|
// links (neutral species): N_in = k (μ_out − μ)/RT.
|
|
@@ -2281,7 +2644,7 @@ export class Solver {
|
|
|
2281
2644
|
// The Poisson residual is the outside's charge, D_in.
|
|
2282
2645
|
const Din = res[R[b * M]];
|
|
2283
2646
|
this.contactD[side] = sgn * Din;
|
|
2284
|
-
if (dyn) {
|
|
2647
|
+
if (dyn && !(this.combining && this.lateStorage[g] === 1)) {
|
|
2285
2648
|
I += (Din - Dstart) / dt;
|
|
2286
2649
|
this._captureRow(b, 0, 1 / dt, C);
|
|
2287
2650
|
}
|
|
@@ -2314,10 +2677,59 @@ export class Solver {
|
|
|
2314
2677
|
|
|
2315
2678
|
|
|
2316
2679
|
|
|
2680
|
+
// Steady solves: each flat stretch's level held at its contact's throughout (see
|
|
2681
|
+
// flatStretches), in place of its balances, as an equilibrium link holds it at the end node.
|
|
2682
|
+
_flatRows() {
|
|
2683
|
+
const { M, u, uLo, res, z, VT, model } = this, R = this.rix;
|
|
2684
|
+
for (const st of this.flatStretches) {
|
|
2685
|
+
const i = st.species, side = st.leftOpen ? 'left' : 'right', k = side === 'left' ? 0 : 1;
|
|
2686
|
+
const link = model.contacts[side].species[i];
|
|
2687
|
+
const level = z[i] === 0 ? link.mu / model.RT : (z[i] * (this.termV[k] + link.offset)) / VT;
|
|
2688
|
+
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
2689
|
+
const b = this.blockOfNode[g], o = b * M + 1 + i;
|
|
2690
|
+
if (this.loc[o] < 0) continue;
|
|
2691
|
+
this._replaceRow(b, 1 + i);
|
|
2692
|
+
for (const B of this.termB) B[R[o]] = 0; // (the row's old terminal terms: the end node's own link)
|
|
2693
|
+
this._j(b, 1 + i, b, 1 + i, 1);
|
|
2694
|
+
res[R[o]] = u[o] + uLo[o] - level;
|
|
2695
|
+
if (z[i] !== 0) this.termB[k][R[o]] += -z[i] / VT;
|
|
2696
|
+
}
|
|
2697
|
+
// A face inside the stretch whose link holds the level continuous carries the species' flux
|
|
2698
|
+
// as an unknown that only the edge balances, now replaced, determined: it's zero.
|
|
2699
|
+
for (let f = st.regions[0]; f < st.regions[1]; f++) {
|
|
2700
|
+
if (model.interfaces[f].links[i].type !== 'equilibrium') continue;
|
|
2701
|
+
const b = this.blockOfFace[f], o = b * M + 1 + i;
|
|
2702
|
+
if (this.loc[o] < 0) continue;
|
|
2703
|
+
this._replaceRow(b, 1 + i);
|
|
2704
|
+
for (const B of this.termB) B[R[o]] = 0;
|
|
2705
|
+
this._j(b, 1 + i, b, 1 + i, 1);
|
|
2706
|
+
res[R[o]] = u[o] + uLo[o];
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2317
2711
|
// Zero one row of the Jacobian (all three blocks), ready to be replaced.
|
|
2712
|
+
// Steady solves: each level stretch's levels held equal (see levelStretches), node to node in
|
|
2713
|
+
// place of the balances. The first node's row is the amount's (see _applyConstraints).
|
|
2714
|
+
_levelRows() {
|
|
2715
|
+
const { M, u, uLo, res } = this, R = this.rix;
|
|
2716
|
+
for (const st of this.levelStretches) {
|
|
2717
|
+
const i = st.species;
|
|
2718
|
+
for (let g = st.nodes[0] + 1; g <= st.nodes[1]; g++) {
|
|
2719
|
+
const b = this.blockOfNode[g], o = b * M + 1 + i, p = o - M;
|
|
2720
|
+
if (this.loc[o] < 0 || this.loc[p] < 0) continue;
|
|
2721
|
+
this._replaceRow(b, 1 + i);
|
|
2722
|
+
for (const B of this.termB) B[R[o]] = 0;
|
|
2723
|
+
this._j(b, 1 + i, b, 1 + i, 1);
|
|
2724
|
+
this._j(b, 1 + i, b - 1, 1 + i, -1);
|
|
2725
|
+
res[R[o]] = u[o] + uLo[o] - (u[p] + uLo[p]);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2318
2729
|
_replaceRow(b, rs) {
|
|
2319
2730
|
const l = this.loc[b * this.M + rs];
|
|
2320
2731
|
if (l < 0) return;
|
|
2732
|
+
if (this.lin) this.lin.kill(this.rix[b * this.M + rs]);
|
|
2321
2733
|
const { sys, nB } = this, sz = sys.sizes, m = sz[b];
|
|
2322
2734
|
sys.B.fill(0, sys.offB[b] + l * m, sys.offB[b] + (l + 1) * m);
|
|
2323
2735
|
if (b > 0) sys.A.fill(0, sys.offA[b] + l * sz[b - 1], sys.offA[b] + (l + 1) * sz[b - 1]);
|
|
@@ -2336,8 +2748,12 @@ export class Solver {
|
|
|
2336
2748
|
for (let k = 0; k < mp; k++) mx = Math.max(mx, Math.abs(A[oa + k]));
|
|
2337
2749
|
for (let k = 0; k < m; k++) mx = Math.max(mx, Math.abs(B[ob + k]));
|
|
2338
2750
|
for (let k = 0; k < mn; k++) mx = Math.max(mx, Math.abs(C[oc + k]));
|
|
2339
|
-
if (mx === 0)
|
|
2751
|
+
if (mx === 0) {
|
|
2752
|
+
this.rowScale[offX[b] + r] = 1;
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2340
2755
|
const s = 1 / mx;
|
|
2756
|
+
this.rowScale[offX[b] + r] = s;
|
|
2341
2757
|
for (let k = 0; k < mp; k++) A[oa + k] *= s;
|
|
2342
2758
|
for (let k = 0; k < m; k++) B[ob + k] *= s;
|
|
2343
2759
|
for (let k = 0; k < mn; k++) C[oc + k] *= s;
|
|
@@ -2347,6 +2763,111 @@ export class Solver {
|
|
|
2347
2763
|
}
|
|
2348
2764
|
}
|
|
2349
2765
|
|
|
2766
|
+
// The Jacobian at dt with the dilute kernels' terms kept aside in difference form (see
|
|
2767
|
+
// DifferenceTerms), the rest assembled into a matrix of its own. The residual and terminal terms
|
|
2768
|
+
// come out as from assemble(dt); the Newton matrix is left as it was.
|
|
2769
|
+
_assembleDifference(dt) {
|
|
2770
|
+
const keep = this.sys;
|
|
2771
|
+
const rest = this.sysRest ?? (this.sysRest = new BlockTridiagonal(keep.n, keep.sizes));
|
|
2772
|
+
const lin = this.diffTerms ?? (this.diffTerms = new DifferenceTerms(keep.size, this.nNodes * this.n, this.nNodes * this.n));
|
|
2773
|
+
this.sys = rest;
|
|
2774
|
+
this.lin = lin;
|
|
2775
|
+
try {
|
|
2776
|
+
this.assemble(dt);
|
|
2777
|
+
} finally {
|
|
2778
|
+
this.sys = keep;
|
|
2779
|
+
this.lin = null;
|
|
2780
|
+
}
|
|
2781
|
+
return { rest, lin };
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// A Newton solve refined by GMRES: the factorised Jacobian as the preconditioner, and J·x
|
|
2785
|
+
// exact, with the dilute kernels' terms in difference form (see DifferenceTerms), as the
|
|
2786
|
+
// impedance does. The assembled J holds a flux's dependence on η at its two ends as two
|
|
2787
|
+
// entries; where they're huge (an inversion layer's 0.1 nm cells, a conductance ~1e11 against
|
|
2788
|
+
// the layer's own storage and the trickle from the bulk), eliminating them loses the layer's
|
|
2789
|
+
// overall level to round-off, and Newton rattles there. delta and deltaV come in as the plain
|
|
2790
|
+
// solve's and leave refined (in the transformed unknowns, as _solveBordered gives them); false
|
|
2791
|
+
// if refining didn't help, leaving them as they came.
|
|
2792
|
+
_refine(dt, delta, deltaV) {
|
|
2793
|
+
const { res, termRes, termB, termC, termDI, rowScale, floating: fl } = this;
|
|
2794
|
+
const N = this.sys.size, K = fl.length, pins = this.pins, R = this.rix;
|
|
2795
|
+
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), transformed: this.transformed, pinRes: this.pins.map((p) => p.res) };
|
|
2796
|
+
const restoreArrays = () => {
|
|
2797
|
+
res.set(save.res);
|
|
2798
|
+
termRes.set(save.termRes);
|
|
2799
|
+
save.termB.forEach((b, k) => termB[k].set(b));
|
|
2800
|
+
save.termC.forEach((c, k) => termC[k].set(c));
|
|
2801
|
+
termDI.set(save.termDI);
|
|
2802
|
+
this.transformed = save.transformed;
|
|
2803
|
+
this.pins.forEach((p, a) => (p.res = save.pinRes[a]));
|
|
2804
|
+
};
|
|
2805
|
+
// The same system, its terms kept apart: the rest, and each floating terminal's (unscaled).
|
|
2806
|
+
const { rest, lin } = this._assembleDifference(dt);
|
|
2807
|
+
const Bk = fl.map((k) => Float64Array.from(termB[k])), Ck = fl.map((k) => Float64Array.from(termC[k])), Dk = fl.map((k) => termDI[k]);
|
|
2808
|
+
restoreArrays();
|
|
2809
|
+
const v = new Float64Array(N + 1), jv = new Float64Array(N);
|
|
2810
|
+
const op = (x) => {
|
|
2811
|
+
for (let j = 0; j < N; j++) v[j] = x[j];
|
|
2812
|
+
v[N] = 0;
|
|
2813
|
+
rest.multiply(v, jv);
|
|
2814
|
+
if (save.transformed) this._untransform(v); // (the dilute terms in the plain unknowns)
|
|
2815
|
+
lin.apply(v, jv, 1, 1 / dt);
|
|
2816
|
+
const out = new Float64Array(N + K);
|
|
2817
|
+
for (let j = 0; j < N; j++) {
|
|
2818
|
+
let t = jv[j];
|
|
2819
|
+
for (let a = 0; a < K; a++) t += Bk[a][j] * x[N + a];
|
|
2820
|
+
out[j] = t * rowScale[j];
|
|
2821
|
+
}
|
|
2822
|
+
for (let a = 0; a < K; a++) {
|
|
2823
|
+
let t = Dk[a] * x[N + a];
|
|
2824
|
+
for (let j = 0; j < N; j++) t += Ck[a][j] * x[j];
|
|
2825
|
+
out[N + a] = t;
|
|
2826
|
+
}
|
|
2827
|
+
// A pinned row is its constraint's (a conserved amount, an island's summed balance).
|
|
2828
|
+
for (const p of pins) {
|
|
2829
|
+
let t = 0;
|
|
2830
|
+
for (let j = 0; j < p.len; j++) t += p.w[j] * x[p.idx[j]];
|
|
2831
|
+
out[R[p.row]] = t;
|
|
2832
|
+
}
|
|
2833
|
+
return precondition(out);
|
|
2834
|
+
};
|
|
2835
|
+
const rhs = new Float64Array(N + 1), out = new Float64Array(N + 1), outV = new Float64Array(K);
|
|
2836
|
+
const precondition = (r) => {
|
|
2837
|
+
for (let j = 0; j < N; j++) rhs[j] = r[j];
|
|
2838
|
+
fl.forEach((k, a) => (termRes[k] = r[N + a]));
|
|
2839
|
+
for (const p of pins) {
|
|
2840
|
+
p.res = rhs[R[p.row]];
|
|
2841
|
+
rhs[R[p.row]] = 0;
|
|
2842
|
+
}
|
|
2843
|
+
this._solveBordered(rhs, out, outV, pins);
|
|
2844
|
+
const z = new Float64Array(N + K);
|
|
2845
|
+
for (let j = 0; j < N; j++) z[j] = out[j];
|
|
2846
|
+
for (let a = 0; a < K; a++) z[N + a] = outV[a];
|
|
2847
|
+
return z;
|
|
2848
|
+
};
|
|
2849
|
+
try {
|
|
2850
|
+
const b = new Float64Array(N + K);
|
|
2851
|
+
for (let j = 0; j < N; j++) b[j] = save.res[j];
|
|
2852
|
+
fl.forEach((k, a) => (b[N + a] = save.termRes[k]));
|
|
2853
|
+
pins.forEach((p, a) => (b[R[p.row]] = save.pinRes[a]));
|
|
2854
|
+
const pb = precondition(b), x = Float64Array.from(pb);
|
|
2855
|
+
// A lost mode or two, GMRES finds in a couple of iterations. The refined update is kept
|
|
2856
|
+
// where GMRES converged and its correction is under a thermal unit; otherwise newton()
|
|
2857
|
+
// stops refining.
|
|
2858
|
+
const x0 = Float64Array.from(x);
|
|
2859
|
+
const ok = gmresReal(op, pb, x, { m: 8, restarts: 1, tol: 1e-8 });
|
|
2860
|
+
let c = 0;
|
|
2861
|
+
for (let j = 0; j < N + K; j++) c = Math.max(c, Math.abs(x[j] - x0[j]));
|
|
2862
|
+
if (!ok || !(c < 1)) return false;
|
|
2863
|
+
for (let j = 0; j < N; j++) delta[j] = x[j];
|
|
2864
|
+
for (let a = 0; a < K; a++) deltaV[a] = x[N + a];
|
|
2865
|
+
return true;
|
|
2866
|
+
} finally {
|
|
2867
|
+
restoreArrays();
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2350
2871
|
// Largest update among the potential-like unknowns (φ̂ and η at grid nodes).
|
|
2351
2872
|
_maxPotentialStep(delta, deltaV = this.deltaV) { // (deltaV null: the device's potentials only)
|
|
2352
2873
|
const { n, M } = this;
|
|
@@ -2369,16 +2890,55 @@ export class Solver {
|
|
|
2369
2890
|
* Newton iteration for one backward-Euler step from the current cOld.
|
|
2370
2891
|
* @returns {{converged: boolean, iterations: number, history: number[]}}
|
|
2371
2892
|
*/
|
|
2372
|
-
newton(dt, { maxIter = 60, tol = 1e-10, maxStep = 10 } = {}) {
|
|
2893
|
+
newton(dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands = false } = {}) {
|
|
2373
2894
|
const { delta, res } = this;
|
|
2374
2895
|
const deltaV = this.deltaV ?? (this.deltaV = new Float64Array(this.floating.length));
|
|
2375
2896
|
const history = [], ownHistory = [];
|
|
2376
|
-
//
|
|
2897
|
+
// Refined solves (see _refine), once the plain ones stall.
|
|
2898
|
+
let refine = false, refined = 0, noRefine = false;
|
|
2899
|
+
// Islands (see _applyIslands): their summed rows go in only once the plain solves converge.
|
|
2900
|
+
// Far from the solution, Newton exact along a weakly held level takes it by 1e5 thermal
|
|
2901
|
+
// units (a floating base, cold at bias), where the plain solves, which barely see that level,
|
|
2902
|
+
// converge. Converged, an island at the wrong level shows: it takes in a different current
|
|
2903
|
+
// than it passes on, so the terminal currents don't add up to zero. Then the summed rows go
|
|
2904
|
+
// in and Newton carries on from there. Converged again, a current that tiny against the
|
|
2905
|
+
// conduction around it rides on differences (~1e-28 thermal units for 5 fA/m² through a bulk
|
|
2906
|
+
// of 1e4 mol/m³ at 1e-4 m²/s) that the last update leaves known only to its own round-off:
|
|
2907
|
+
// where the currents still don't add up, one more update polishes them. Where the islands'
|
|
2908
|
+
// own system is held by static pivots elsewhere (a GaAs stack's minority carriers, at 1e-17
|
|
2909
|
+
// A/m² of noise), their rows can throw Newton off instead: if it doesn't converge again
|
|
2910
|
+
// within a dozen iterations, the plain solution stands.
|
|
2911
|
+
this.islandsOn = islands;
|
|
2912
|
+
let rounds = 0, kept = null;
|
|
2913
|
+
const islandsOff = (it, result) => {
|
|
2914
|
+
if (dt !== Infinity || !this.constrained || this.islands.length === 0 || rounds >= 2 || !(this._kirchhoff() > 1e-9)) return false;
|
|
2915
|
+
this.islandsOn = true;
|
|
2916
|
+
rounds++;
|
|
2917
|
+
kept = { it, result, u: Float64Array.from(this.u), uLo: Float64Array.from(this.uLo), termV: Float64Array.from(this.termV) };
|
|
2918
|
+
return true;
|
|
2919
|
+
};
|
|
2920
|
+
// A steady solve that fails: record how nearly singular the system was, and where; or, failing
|
|
2921
|
+
// with the islands' rows in, go back to the solution without them.
|
|
2377
2922
|
const fail = (r) => {
|
|
2923
|
+
if (kept) {
|
|
2924
|
+
this.u.set(kept.u);
|
|
2925
|
+
this.uLo.set(kept.uLo);
|
|
2926
|
+
this.termV.set(kept.termV);
|
|
2927
|
+
this.islandsOn = false;
|
|
2928
|
+
this.computeConcentrations();
|
|
2929
|
+
return { ...kept.result, iterations: r.iterations, history };
|
|
2930
|
+
}
|
|
2378
2931
|
if (dt === Infinity) this._noteConditioning();
|
|
2379
2932
|
return r;
|
|
2380
2933
|
};
|
|
2381
2934
|
for (let it = 1; it <= maxIter; it++) {
|
|
2935
|
+
if (kept && it - kept.it > 12) return fail({ converged: false, iterations: it - 1, history });
|
|
2936
|
+
if (kept && it === kept.it + 2) {
|
|
2937
|
+
// Refined from the islands' second update (their first moves the level, perhaps by
|
|
2938
|
+
// more than a refinement may), since the plain solves have shown a mode they lose.
|
|
2939
|
+
refine = true;
|
|
2940
|
+
noRefine = false;
|
|
2941
|
+
}
|
|
2382
2942
|
try {
|
|
2383
2943
|
this.assemble(dt);
|
|
2384
2944
|
} catch (err) {
|
|
@@ -2395,9 +2955,13 @@ export class Solver {
|
|
|
2395
2955
|
return fail({ converged: false, iterations: it, history, error: err.message });
|
|
2396
2956
|
}
|
|
2397
2957
|
try {
|
|
2398
|
-
const pins = this.
|
|
2958
|
+
const pins = this.pins;
|
|
2399
2959
|
deltaV.fill(0);
|
|
2400
2960
|
this._solveBordered(res, delta, deltaV, pins);
|
|
2961
|
+
if (refine && !this._refine(dt, delta, deltaV)) {
|
|
2962
|
+
refine = false; // (GMRES didn't converge, or strayed: the plain solves it is)
|
|
2963
|
+
noRefine = true;
|
|
2964
|
+
}
|
|
2401
2965
|
if (this.transformed) this._untransform(delta);
|
|
2402
2966
|
} catch (err) {
|
|
2403
2967
|
if (!(err instanceof SolverError)) throw err;
|
|
@@ -2414,7 +2978,7 @@ export class Solver {
|
|
|
2414
2978
|
ownHistory.push(own);
|
|
2415
2979
|
if (!Number.isFinite(step)) return fail({ converged: false, iterations: it, history, error: 'non-finite update' });
|
|
2416
2980
|
// Give up early on clear divergence; the caller will take a smaller step instead.
|
|
2417
|
-
if (own > 1e4 || (it > 6 && own > 10 * ownHistory[0])) {
|
|
2981
|
+
if ((it > 1 && own > 1e4) || (it > 6 && own > 10 * ownHistory[0])) {
|
|
2418
2982
|
this.computeConcentrations();
|
|
2419
2983
|
return fail({ converged: false, iterations: it, history, error: 'diverging' });
|
|
2420
2984
|
}
|
|
@@ -2422,8 +2986,10 @@ export class Solver {
|
|
|
2422
2986
|
this._addToState(delta, -alpha);
|
|
2423
2987
|
this.floating.forEach((k, a) => (this.termV[k] -= alpha * deltaV[a]));
|
|
2424
2988
|
if (alpha === 1 && step < tol) {
|
|
2989
|
+
const done = { converged: true, iterations: it, history, residual: rmax };
|
|
2990
|
+
if (islandsOff(it, done)) continue;
|
|
2425
2991
|
this.computeConcentrations();
|
|
2426
|
-
return
|
|
2992
|
+
return done;
|
|
2427
2993
|
}
|
|
2428
2994
|
// Converged as far as round-off allows: the updates are already tiny (below 1e-6 thermal
|
|
2429
2995
|
// units, ~26 nV) and have stopped shrinking. A badly conditioned system's floor can sit
|
|
@@ -2432,15 +2998,35 @@ export class Solver {
|
|
|
2432
2998
|
// (Raising this floor lets a weakly held population drift: a MOS capacitor without a
|
|
2433
2999
|
// channel port then showed a DC leak through its oxide.)
|
|
2434
3000
|
const [p1, p2] = [history[history.length - 2], history[history.length - 3]], floor = 1e-6;
|
|
2435
|
-
|
|
3001
|
+
// Stalled near the solution (not shrinking quadratically), or growing undamped twice
|
|
3002
|
+
// running: the factorised Jacobian may have lost a slow mode to cancellation, and
|
|
3003
|
+
// round-off's floor would accept it wrong, or its noise keep Newton from the solution (a
|
|
3004
|
+
// bipolar stack's floating base, its holes held ~1e14 more weakly than they move within
|
|
3005
|
+
// it). Refine the solves from here.
|
|
3006
|
+
if (refine) refined++;
|
|
3007
|
+
else if (!noRefine && alpha === 1 && it >= 3 && step > 0.25 * p1 && (step < 1e-4 || (step > p1 && p1 > p2))) refine = true;
|
|
3008
|
+
if (alpha === 1 && it >= 4 && (!refine || refined >= 1 || noRefine) && step < floor && p1 < floor && step > 0.25 * p1 && p1 > 0.25 * p2) {
|
|
3009
|
+
const done = { converged: true, iterations: it, history, residual: rmax, roundoff: true };
|
|
3010
|
+
if (islandsOff(it, done)) continue;
|
|
2436
3011
|
this.computeConcentrations();
|
|
2437
|
-
return
|
|
3012
|
+
return done;
|
|
2438
3013
|
}
|
|
2439
3014
|
}
|
|
2440
3015
|
this.computeConcentrations();
|
|
2441
3016
|
return fail({ converged: false, iterations: maxIter, history });
|
|
2442
3017
|
}
|
|
2443
3018
|
|
|
3019
|
+
// How far the terminal currents (as last assembled) are from adding up to zero, relative to the
|
|
3020
|
+
// largest of them: Kirchhoff's law, which a steady state keeps.
|
|
3021
|
+
_kirchhoff() {
|
|
3022
|
+
let sum = 0, mx = 0;
|
|
3023
|
+
for (const I of this.termI) {
|
|
3024
|
+
sum += I;
|
|
3025
|
+
mx = Math.max(mx, Math.abs(I));
|
|
3026
|
+
}
|
|
3027
|
+
return mx > 0 ? Math.abs(sum) / mx : 0;
|
|
3028
|
+
}
|
|
3029
|
+
|
|
2444
3030
|
// How many digits the last factorisation lost to cancellation, and where (the worst seen since
|
|
2445
3031
|
// the steady solve began). Only for diagnosing failures: a saturated species, for instance,
|
|
2446
3032
|
// can lose digits harmlessly.
|
|
@@ -2557,6 +3143,20 @@ export class Solver {
|
|
|
2557
3143
|
let result;
|
|
2558
3144
|
try {
|
|
2559
3145
|
result = this.newton(dtEff, opts);
|
|
3146
|
+
if (!result.converged && dtEff === Infinity && this.constrained && this.islands.length > 0) {
|
|
3147
|
+
// Plain solves that don't converge may be lost along an island's level (two regions
|
|
3148
|
+
// conducting 1e20 times less hold it): once more from the start, with the islands'
|
|
3149
|
+
// summed rows in throughout (see newton).
|
|
3150
|
+
this.u.set(this.uPrev);
|
|
3151
|
+
this.uLo.set(this.uPrevLo);
|
|
3152
|
+
this.termV.set(this.termVPrev);
|
|
3153
|
+
if (opts.guess) {
|
|
3154
|
+
this.u.set(opts.guess);
|
|
3155
|
+
this.uLo.fill(0);
|
|
3156
|
+
}
|
|
3157
|
+
const again = this.newton(dtEff, { ...opts, islands: true });
|
|
3158
|
+
result = { ...again, iterations: result.iterations + again.iterations };
|
|
3159
|
+
}
|
|
2560
3160
|
} finally {
|
|
2561
3161
|
this.combining = false;
|
|
2562
3162
|
}
|
|
@@ -2583,7 +3183,7 @@ export class Solver {
|
|
|
2583
3183
|
/**
|
|
2584
3184
|
* Adaptive time stepping to tEnd: variable-step BDF2 (or backward Euler), with the local error
|
|
2585
3185
|
* estimated against an explicit predictor through the previous states, and controlled to
|
|
2586
|
-
* `tol` thermal units per step in every potential (φ̂, each η, a
|
|
3186
|
+
* `tol` thermal units per step in every potential (φ̂, each η, a surface's coverages as η). The first
|
|
2587
3187
|
* step is checked by step doubling. Stops early when `budgetMs` of wall time is used, so an
|
|
2588
3188
|
* animation can call it once per frame; the step size carries over between calls.
|
|
2589
3189
|
*/
|
|
@@ -2599,11 +3199,11 @@ export class Solver {
|
|
|
2599
3199
|
if (!(tEnd > this.time)) return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace };
|
|
2600
3200
|
let dt = this.dtNext ?? opts.dt0 ?? (tEnd - this.time) * 1e-4;
|
|
2601
3201
|
if (!(dt > 0)) dt = (tEnd - this.time) * 1e-4;
|
|
2602
|
-
let grow = 0; // longer first steps tried after a Newton failure (see below)
|
|
2603
3202
|
// The shortest step worth trying: round-off relative to the time, or the device's fastest
|
|
2604
3203
|
// time scale, whichever is shorter (a cold start can need steps far below a long run's 1e-14).
|
|
2605
3204
|
const floor = Math.min(1e-14 * Math.max(tEnd, 1e-300), this.fastestTime() * 1e-2);
|
|
2606
3205
|
const pred = new Float64Array(this.u.length), guess = new Float64Array(this.u.length);
|
|
3206
|
+
const firstErrs = []; // the first step's error at each size tried (see below)
|
|
2607
3207
|
const factor = (err, p) => (err > 0 ? Math.min(2, Math.max(0.2, 0.9 * Math.exp(Math.log(tol / err) / (p + 1)))) : 2);
|
|
2608
3208
|
while (this.time < tEnd) {
|
|
2609
3209
|
if (steps + rejected >= maxSteps || clock() - start > budgetMs) break;
|
|
@@ -2628,6 +3228,7 @@ export class Solver {
|
|
|
2628
3228
|
this.landing = atBreak ? tb : clamped ? tEnd : undefined;
|
|
2629
3229
|
const snap = this._snapshot();
|
|
2630
3230
|
let err, p, half = null;
|
|
3231
|
+
if (this.history.length > 0) firstErrs.length = 0;
|
|
2631
3232
|
if (this.history.length === 0) {
|
|
2632
3233
|
// First step: backward Euler, checked against two half steps (whose result is kept).
|
|
2633
3234
|
const full = this.step(h);
|
|
@@ -2648,22 +3249,13 @@ export class Solver {
|
|
|
2648
3249
|
if (err === undefined) {
|
|
2649
3250
|
this._restore(snap);
|
|
2650
3251
|
rejected++;
|
|
2651
|
-
|
|
2652
|
-
// helps; but right after a jump in a strictly neutral (ε = 0) material, the shorter the
|
|
2653
|
-
// step the worse conditioned it is (the field is fixed only through fluxes, which the
|
|
2654
|
-
// storage term dwarfs), so try longer ones first.
|
|
2655
|
-
if (this.strictlyNeutral && grow < 3 && h * 16 < tEnd - this.time) {
|
|
2656
|
-
grow++;
|
|
2657
|
-
dt = h * 16;
|
|
2658
|
-
} else {
|
|
2659
|
-
grow = 3;
|
|
2660
|
-
dt = h / 4;
|
|
2661
|
-
}
|
|
3252
|
+
dt = h / 4;
|
|
2662
3253
|
if (dt < floor) { failed = true; break; }
|
|
2663
3254
|
continue;
|
|
2664
3255
|
}
|
|
2665
3256
|
} else {
|
|
2666
|
-
|
|
3257
|
+
// (Order 2 once three consistent states can check it.)
|
|
3258
|
+
const r = this.step(h, { method: this.history.length >= 2 ? method : 'be', guess: this._extrapolate(guess, this.time + h) });
|
|
2667
3259
|
iterations += r.iterations;
|
|
2668
3260
|
if (!r.converged) {
|
|
2669
3261
|
rejected++;
|
|
@@ -2675,14 +3267,25 @@ export class Solver {
|
|
|
2675
3267
|
err = this._errorNorm(pred) * p.scale;
|
|
2676
3268
|
p = p.order;
|
|
2677
3269
|
}
|
|
2678
|
-
|
|
3270
|
+
// A first step whose error doesn't shrink with it: a jump in a level held at a boundary
|
|
3271
|
+
// (a bath's, after a voltage step) starts a profile self-similar in x/√t, so a first step
|
|
3272
|
+
// of any length errs alike, down to the grid's own diffusion time, below Newton's reach.
|
|
3273
|
+
// Three tries in a row, each within a factor 2 of the last, say so: the error is the
|
|
3274
|
+
// jump's, not the step's. Take the step (backward Euler damps it as it should) and grow.
|
|
3275
|
+
if (half && err > tol) firstErrs.push(err);
|
|
3276
|
+
const jump = half && firstErrs.length >= 3 && firstErrs.slice(-3).every((e, j, a) => j === 0 || (e > 0.5 * a[j - 1] && e < 2 * a[j - 1]));
|
|
3277
|
+
if (err > tol && !jump) {
|
|
2679
3278
|
this._restore(snap);
|
|
2680
3279
|
rejected++;
|
|
2681
3280
|
dt = h * factor(err, p);
|
|
2682
3281
|
continue;
|
|
2683
3282
|
}
|
|
2684
3283
|
steps++;
|
|
2685
|
-
|
|
3284
|
+
// The state the first step started from needn't satisfy the algebraic equations (φ in a
|
|
3285
|
+
// strictly neutral region, an interface's unknowns, after a start or a jump), and they
|
|
3286
|
+
// jump in the first instant; a predictor through it would see that jump as error on every
|
|
3287
|
+
// step after. The history starts from the half step.
|
|
3288
|
+
if (half) this.history.length = 1;
|
|
2686
3289
|
dt = clamped ? Math.max(dt, h * factor(err, p)) : h * factor(err, p);
|
|
2687
3290
|
if (atBreak) this.history = []; // the solution's slope jumps here: restart the order
|
|
2688
3291
|
if (half) {
|
|
@@ -2809,6 +3412,8 @@ export class Solver {
|
|
|
2809
3412
|
for (let i = 0; i < n; i++) {
|
|
2810
3413
|
if (this.present[g * n + i]) mx = Math.max(mx, Math.abs(u[b * M + 1 + i] - ref[b * M + 1 + i]));
|
|
2811
3414
|
}
|
|
3415
|
+
const k = this.surfPort[g]; // an electrode surface's coverages, as η
|
|
3416
|
+
if (k >= 0) for (let q = 0; q < this.model.ports[k].surface.length; q++) mx = Math.max(mx, Math.abs(u[b * M + 1 + n + q] - ref[b * M + 1 + n + q]));
|
|
2812
3417
|
}
|
|
2813
3418
|
return mx;
|
|
2814
3419
|
}
|
|
@@ -2935,52 +3540,17 @@ export class Solver {
|
|
|
2935
3540
|
for (const X of ['A', 'B', 'C']) S[X] = sys[X].map((v, k) => (v - J[X][k]) * dts);
|
|
2936
3541
|
const Cs = this.termC.map((v, k) => v.map((x, j) => (x - Ct[k][j]) * dts));
|
|
2937
3542
|
const DIs = this.termDI.map((x, k) => (x - DI[k]) * dts);
|
|
2938
|
-
|
|
2939
|
-
// a flux's dependence on η_L and η_R as two
|
|
2940
|
-
// layer) and v nearly uniform, J·v loses the
|
|
2941
|
-
//
|
|
2942
|
-
//
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
const
|
|
2947
|
-
const
|
|
2948
|
-
|
|
2949
|
-
for (let j = 0; j < low.length; j++) {
|
|
2950
|
-
const full = this.fullOf[j];
|
|
2951
|
-
low[j] = nodeBlock[Math.floor(full / M)] && full % M !== 0 ? 1 : 0;
|
|
2952
|
-
}
|
|
2953
|
-
const resP = new Float64Array(this.res.length), resM = new Float64Array(this.res.length);
|
|
2954
|
-
const derivative = (v, k, sigma, out) => {
|
|
2955
|
-
let mx = Math.abs(sigma);
|
|
2956
|
-
for (let j = 0; j < v.length; j++) mx = Math.max(mx, Math.abs(v[j]));
|
|
2957
|
-
const h = 1e-4 / (mx || 1);
|
|
2958
|
-
for (const [sgn, into] of [[1, resP], [-1, resM]]) {
|
|
2959
|
-
this.u.set(u0);
|
|
2960
|
-
this.uLo.set(uLo0);
|
|
2961
|
-
this.termV.set(V0);
|
|
2962
|
-
for (let j = 0; j < v.length; j++) {
|
|
2963
|
-
const full = this.fullOf[j];
|
|
2964
|
-
if (low[j]) this.uLo[full] += sgn * h * v[j];
|
|
2965
|
-
else this.u[full] += sgn * h * v[j];
|
|
2966
|
-
}
|
|
2967
|
-
// A held terminal's voltage comes from its source at each assembly; a floating one's is
|
|
2968
|
-
// an unknown.
|
|
2969
|
-
if (k >= 0 && fl.includes(k)) this.termV[k] += sgn * h * sigma;
|
|
2970
|
-
else if (k >= 0) this.sourceOverride.set(k, V0[k] + sgn * h * sigma);
|
|
2971
|
-
this.assemble(Infinity);
|
|
2972
|
-
if (k >= 0) this.sourceOverride.delete(k);
|
|
2973
|
-
into.set(this.res);
|
|
2974
|
-
}
|
|
2975
|
-
for (let j = 0; j < out.length; j++) out[j] = (resP[j] - resM[j]) / (2 * h);
|
|
2976
|
-
};
|
|
2977
|
-
const restore = () => {
|
|
2978
|
-
this.u.set(u0);
|
|
2979
|
-
this.uLo.set(uLo0);
|
|
2980
|
-
this.termV.set(V0);
|
|
2981
|
-
this._refreshSources();
|
|
2982
|
-
this.computeConcentrations();
|
|
2983
|
-
};
|
|
3543
|
+
const cuts = this._currentCuts();
|
|
3544
|
+
// J·v for GMRES, exact: the assembled J holds a flux's dependence on η_L and η_R as two
|
|
3545
|
+
// entries, and where both are huge (an inversion layer) and v nearly uniform, J·v loses the
|
|
3546
|
+
// flux to round-off. So the dilute kernels' terms are kept in difference form (see
|
|
3547
|
+
// DifferenceTerms), with the rest from a matrix of their own, steady (Jr) and storage (Sr).
|
|
3548
|
+
this._assembleDifference(Infinity);
|
|
3549
|
+
const Jr = { A: this.sysRest.A.slice(), B: this.sysRest.B.slice(), C: this.sysRest.C.slice() };
|
|
3550
|
+
const { lin } = this._assembleDifference(dts);
|
|
3551
|
+
const Sr = {};
|
|
3552
|
+
for (const X of ['A', 'B', 'C']) Sr[X] = this.sysRest[X].map((v, k) => (v - Jr[X][k]) * dts);
|
|
3553
|
+
this.assemble(Infinity);
|
|
2984
3554
|
|
|
2985
3555
|
const { sizes, offA, offB, offC, offX } = sys;
|
|
2986
3556
|
const csys = new ComplexBlockTridiagonal(nB, sizes);
|
|
@@ -3026,9 +3596,6 @@ export class Solver {
|
|
|
3026
3596
|
}
|
|
3027
3597
|
}
|
|
3028
3598
|
csys.factor();
|
|
3029
|
-
// Each solve uses that residual-based J·v (S·v is safe from the matrix): in an inversion
|
|
3030
|
-
// layer fed by minority carriers that number ~1e3 cm⁻³ in the bulk, the assembled J alone
|
|
3031
|
-
// gives the layer an exchange path many orders too fast.
|
|
3032
3599
|
const sx = (X, vr, out) => {
|
|
3033
3600
|
for (let blk = 0; blk < nB; blk++) {
|
|
3034
3601
|
const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
|
|
@@ -3054,25 +3621,35 @@ export class Solver {
|
|
|
3054
3621
|
csys.solve(rRes, iRes, outR, outI);
|
|
3055
3622
|
return [outR, outI];
|
|
3056
3623
|
};
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
sx(
|
|
3061
|
-
sx(
|
|
3062
|
-
|
|
3624
|
+
// (J + iωS)·v, exactly (see DifferenceTerms), into (ar, ai).
|
|
3625
|
+
const jv = (vr, vi, ar, ai) => {
|
|
3626
|
+
sx(Jr, vr, jr);
|
|
3627
|
+
sx(Jr, vi, ji);
|
|
3628
|
+
sx(Sr, vi, si);
|
|
3629
|
+
sx(Sr, vr, sr);
|
|
3063
3630
|
for (let j = 0; j < Nc; j++) {
|
|
3064
3631
|
ar[j] = jr[j] - w * si[j];
|
|
3065
3632
|
ai[j] = ji[j] + w * sr[j];
|
|
3066
3633
|
}
|
|
3634
|
+
lin.apply(vr, ar, 1, 0);
|
|
3635
|
+
lin.apply(vi, ar, 0, -w);
|
|
3636
|
+
lin.apply(vi, ai, 1, 0);
|
|
3637
|
+
lin.apply(vr, ai, 0, w);
|
|
3638
|
+
};
|
|
3639
|
+
const op = ([vr, vi]) => {
|
|
3640
|
+
const ar = new Float64Array(Nc), ai = new Float64Array(Nc);
|
|
3641
|
+
jv(vr, vi, ar, ai);
|
|
3067
3642
|
return precondition(ar, ai);
|
|
3068
3643
|
};
|
|
3069
3644
|
const solve = (br, bi, outr, outi, k, sigma) => {
|
|
3070
3645
|
const rhs = k >= 0 ? precondition(Bt[k].map((v) => -sigma * v), zero) : [new Float64Array(Nc), new Float64Array(Nc)];
|
|
3071
3646
|
outr.set(rhs[0]);
|
|
3072
3647
|
outi.set(rhs[1]);
|
|
3073
|
-
|
|
3074
|
-
|
|
3648
|
+
// (GMRES keeps its best iterate, which is never worse than the factorised solve.)
|
|
3649
|
+
const { residual } = gmres(op, rhs, [outr, outi]);
|
|
3650
|
+
accuracy = Math.max(accuracy, residual);
|
|
3075
3651
|
};
|
|
3652
|
+
let accuracy = 1e-14; // (relative, of the solves: the readouts' error is weighed with it)
|
|
3076
3653
|
// y: the response with the floating terminals held; X_k: to a unit δV_k.
|
|
3077
3654
|
const heldT = dT.kind === 'V';
|
|
3078
3655
|
for (let j = 0; j < Nc; j++) {
|
|
@@ -3112,12 +3689,93 @@ export class Solver {
|
|
|
3112
3689
|
xr[this.fullOf[j]] = re;
|
|
3113
3690
|
xi[this.fullOf[j]] = im;
|
|
3114
3691
|
}
|
|
3692
|
+
// The current into terminal kT per volt, (C + iωC′)·δx + (∂I/∂V)(1 + iω′), with its error.
|
|
3693
|
+
// At the contact it's a sum whose terms can be far larger than itself (in a junction's
|
|
3694
|
+
// neutral ends, a conductance times a δη that cancels its neighbours' to 1e-17), and its
|
|
3695
|
+
// round-off reads as a constant conductance. Between the two contacts of a device without
|
|
3696
|
+
// ports, the total current (conduction and displacement) is the same through every cut, so
|
|
3697
|
+
// it's also read across each segment, and the reading with the least error taken. Each
|
|
3698
|
+
// reading's error is its terms' round-off and the solves' error in δx (to `accuracy` of its
|
|
3699
|
+
// largest entry) through its coefficients: a contact's or a cut's flux is a difference of
|
|
3700
|
+
// levels that the solved rows don't pin.
|
|
3701
|
+
// `parts`: the response as a sum, each part compact (r, i) and full-index (fr, fi), with the
|
|
3702
|
+
// absolute error of its entries.
|
|
3703
|
+
const read = (parts) => {
|
|
3704
|
+
const eps = 1.1e-16, c = Ct[kT], cs = Cs[kT];
|
|
3705
|
+
let Ir = DI[kT], Ii = w * DIs[kT], err = eps * (Math.abs(DI[kT]) + w * Math.abs(DIs[kT]));
|
|
3706
|
+
for (const p of parts) {
|
|
3707
|
+
const [re, im] = dot(kT, w, p.r, p.i);
|
|
3708
|
+
Ir += re;
|
|
3709
|
+
Ii += im;
|
|
3710
|
+
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);
|
|
3711
|
+
}
|
|
3712
|
+
if (!cuts || kT >= 2) return { Ir, Ii, err };
|
|
3713
|
+
const sgn = kT === 0 ? 1 : -1;
|
|
3714
|
+
for (const cut of cuts) {
|
|
3715
|
+
let re = 0, im = 0, e = 0;
|
|
3716
|
+
for (const p of parts) {
|
|
3717
|
+
for (let a = 0; a < cut.idx.length; a++) {
|
|
3718
|
+
const x = cut.idx[a], gk = cut.g[a], dk = w * cut.d[a];
|
|
3719
|
+
// (g + iωd)(xr + i xi)
|
|
3720
|
+
re += gk * p.fr[x] - dk * p.fi[x];
|
|
3721
|
+
im += gk * p.fi[x] + dk * p.fr[x];
|
|
3722
|
+
e += (Math.abs(gk) + Math.abs(dk)) * (eps * Math.hypot(p.fr[x], p.fi[x]) + p.floor);
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
if (e < err) [err, Ir, Ii] = [e, sgn * re, sgn * im];
|
|
3726
|
+
}
|
|
3727
|
+
return { Ir, Ii, err };
|
|
3728
|
+
};
|
|
3729
|
+
const largest = (r, i) => {
|
|
3730
|
+
let m = 0;
|
|
3731
|
+
for (let j = 0; j < r.length; j++) m = Math.max(m, Math.hypot(r[j], i[j]));
|
|
3732
|
+
return m;
|
|
3733
|
+
};
|
|
3734
|
+
const full = (r, i) => {
|
|
3735
|
+
const fr = new Float64Array(N), fi = new Float64Array(N);
|
|
3736
|
+
for (let j = 0; j < Nc; j++) {
|
|
3737
|
+
fr[this.fullOf[j]] = r[j];
|
|
3738
|
+
fi[this.fullOf[j]] = i[j];
|
|
3739
|
+
}
|
|
3740
|
+
return [fr, fi];
|
|
3741
|
+
};
|
|
3115
3742
|
let Zr, Zi;
|
|
3116
3743
|
if (heldT) {
|
|
3117
|
-
// δI per volt: (C + iωC′)·δx + (∂I/∂V)(1 + iω′) at the terminal
|
|
3118
|
-
let
|
|
3119
|
-
|
|
3120
|
-
|
|
3744
|
+
// δI per volt: (C + iωC′)·δx + (∂I/∂V)(1 + iω′) at the terminal…
|
|
3745
|
+
let { Ir, Ii, err } = read([{ r: yr, i: yi, fr: xr, fi: xi, floor: accuracy * largest(yr, yi) }]);
|
|
3746
|
+
// …and where the solves leave that unresolved (the estimate past 1% of the current), the
|
|
3747
|
+
// response is split: a uniform shift of each region (z_i s on every η_i, s on φ̂), which
|
|
3748
|
+
// changes no flux and no concentration, so that J·e vanishes inside each region and is
|
|
3749
|
+
// taken at its edges; and what's left, small, solved for to GMRES's floor, its error now
|
|
3750
|
+
// relative to itself. An electrolyte at its open circuit needs it: the response to its
|
|
3751
|
+
// terminal is nearly such a shift, and its current the slope of levels uniform to 1e-14,
|
|
3752
|
+
// more than a double can hold. (Not with ports, whose exchange sees the shift.)
|
|
3753
|
+
if (K === 0 && cuts && err > 1e-2 * Math.hypot(Ir, Ii)) {
|
|
3754
|
+
const [er, ei] = this._regionShift(yr, yi);
|
|
3755
|
+
const tr = new Float64Array(Nc), ti = new Float64Array(Nc);
|
|
3756
|
+
jv(er, ei, tr, ti);
|
|
3757
|
+
const edge = this._regionEdgeRows();
|
|
3758
|
+
const ar = new Float64Array(Nc), ai = new Float64Array(Nc);
|
|
3759
|
+
for (let j = 0; j < Nc; j++) {
|
|
3760
|
+
ar[j] = -Bt[kT][j] - (edge[j] ? tr[j] : 0);
|
|
3761
|
+
ai[j] = edge[j] ? -ti[j] : 0;
|
|
3762
|
+
}
|
|
3763
|
+
const rhs = precondition(ar, ai);
|
|
3764
|
+
const dr = yr.map((v, j) => v - er[j]), di = yi.map((v, j) => v - ei[j]);
|
|
3765
|
+
const { residual } = gmres(op, rhs, [dr, di], { tol: 1e-11 });
|
|
3766
|
+
const [efr, efi] = full(er, ei), [dfr, dfi] = full(dr, di);
|
|
3767
|
+
const r2 = read([
|
|
3768
|
+
{ r: er, i: ei, fr: efr, fi: efi, floor: 0 },
|
|
3769
|
+
{ r: dr, i: di, fr: dfr, fi: dfi, floor: Math.max(1e-14, residual) * largest(dr, di) },
|
|
3770
|
+
]);
|
|
3771
|
+
if (r2.err < err) ({ Ir, Ii, err } = r2);
|
|
3772
|
+
if (profiles) {
|
|
3773
|
+
for (let j = 0; j < N; j++) {
|
|
3774
|
+
xr[j] = efr[j] + dfr[j];
|
|
3775
|
+
xi[j] = efi[j] + dfi[j];
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3121
3779
|
const d2 = Ir * Ir + Ii * Ii; // Z = 1/δI
|
|
3122
3780
|
Zr = Ir / d2;
|
|
3123
3781
|
Zi = -Ii / d2;
|
|
@@ -3134,6 +3792,125 @@ export class Solver {
|
|
|
3134
3792
|
return out;
|
|
3135
3793
|
}
|
|
3136
3794
|
|
|
3795
|
+
// A uniform shift of each region, fitted to a small-signal response (compact, complex): φ̂ by
|
|
3796
|
+
// s_r and each species' η by z_i s_r, with s_r the region's mean δφ̂ (or, where φ̂ isn't an
|
|
3797
|
+
// unknown, its carriers' mean δη/z). Coverages and flux unknowns aren't shifted.
|
|
3798
|
+
_regionShift(yr, yi) {
|
|
3799
|
+
const { n, M, z, loc, model } = this, R = this.rix, { grid } = model;
|
|
3800
|
+
const er = new Float64Array(yr.length), ei = new Float64Array(yr.length);
|
|
3801
|
+
for (let r = 0; r < model.regions.length; r++) {
|
|
3802
|
+
let sr = 0, si = 0, k = 0;
|
|
3803
|
+
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
|
|
3804
|
+
for (let g = g0; g <= g1; g++) {
|
|
3805
|
+
const o = this.blockOfNode[g] * M;
|
|
3806
|
+
if (loc[o] >= 0) {
|
|
3807
|
+
sr += yr[R[o]];
|
|
3808
|
+
si += yi[R[o]];
|
|
3809
|
+
k++;
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
if (k === 0) {
|
|
3813
|
+
for (let g = g0; g <= g1; g++) {
|
|
3814
|
+
const o = this.blockOfNode[g] * M;
|
|
3815
|
+
for (let i = 0; i < n; i++) {
|
|
3816
|
+
if (z[i] === 0 || loc[o + 1 + i] < 0) continue;
|
|
3817
|
+
sr += yr[R[o + 1 + i]] / z[i];
|
|
3818
|
+
si += yi[R[o + 1 + i]] / z[i];
|
|
3819
|
+
k++;
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
if (k === 0) continue;
|
|
3824
|
+
sr /= k;
|
|
3825
|
+
si /= k;
|
|
3826
|
+
for (let g = g0; g <= g1; g++) {
|
|
3827
|
+
const o = this.blockOfNode[g] * M;
|
|
3828
|
+
if (loc[o] >= 0) {
|
|
3829
|
+
er[R[o]] = sr;
|
|
3830
|
+
ei[R[o]] = si;
|
|
3831
|
+
}
|
|
3832
|
+
for (let i = 0; i < n; i++) {
|
|
3833
|
+
if (loc[o + 1 + i] < 0) continue;
|
|
3834
|
+
er[R[o + 1 + i]] = z[i] * sr;
|
|
3835
|
+
ei[R[o + 1 + i]] = z[i] * si;
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
return [er, ei];
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
// Compact rows a region's uniform shift can touch: faces, the nodes beside them, and the two
|
|
3843
|
+
// ends (whatever holds a level from outside). Inside a region, every row is unchanged by it.
|
|
3844
|
+
_regionEdgeRows() {
|
|
3845
|
+
const { M, loc, nB } = this, R = this.rix;
|
|
3846
|
+
const edge = new Uint8Array(this.sys.size);
|
|
3847
|
+
const mark = (b) => {
|
|
3848
|
+
if (b < 0 || b >= nB) return;
|
|
3849
|
+
for (let r = 0; r < M; r++) if (loc[b * M + r] >= 0) edge[R[b * M + r]] = 1;
|
|
3850
|
+
};
|
|
3851
|
+
mark(0);
|
|
3852
|
+
mark(nB - 1);
|
|
3853
|
+
for (let f = 0; f < this.nFaces; f++) {
|
|
3854
|
+
const b = this.blockOfFace[f];
|
|
3855
|
+
mark(b - 1);
|
|
3856
|
+
mark(b);
|
|
3857
|
+
mark(b + 1);
|
|
3858
|
+
}
|
|
3859
|
+
return edge;
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
// The cuts where the impedance may read a small-signal current (null where the current
|
|
3863
|
+
// differs between cuts: a device with ports). Each is the linearised total current toward +x
|
|
3864
|
+
// through one segment of an ideal region, Σ (g + iω d)·δu over the unknowns idx: the
|
|
3865
|
+
// Scharfetter–Gummel fluxes' derivatives (as in _segmentsDilute) and the displacement's.
|
|
3866
|
+
_currentCuts() {
|
|
3867
|
+
const { model, n, M, u, uLo, c, z } = this, F = FARADAY;
|
|
3868
|
+
if (model.ports.length > 0) return null;
|
|
3869
|
+
const { grid, materials, regions } = model;
|
|
3870
|
+
const cuts = [];
|
|
3871
|
+
for (let r = 0; r < regions.length; r++) {
|
|
3872
|
+
const reg = regions[r], mat = materials[reg.material];
|
|
3873
|
+
if (mat.conductor || !mat.ideal || reg.mixing > 0) continue;
|
|
3874
|
+
const vel = reg.velocity;
|
|
3875
|
+
for (let s = grid.regionStart[r]; s < grid.regionEnd[r]; s++) {
|
|
3876
|
+
const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s];
|
|
3877
|
+
const terms = new Map();
|
|
3878
|
+
const add = (x, g, d) => {
|
|
3879
|
+
const t = terms.get(x) || [0, 0];
|
|
3880
|
+
t[0] += g;
|
|
3881
|
+
t[1] += d;
|
|
3882
|
+
terms.set(x, t);
|
|
3883
|
+
};
|
|
3884
|
+
const phiL = u[bL * M], phiR = u[bR * M], hasPhi = !this.phiUndefined[s];
|
|
3885
|
+
for (let i = 0; i < n; i++) {
|
|
3886
|
+
if (z[i] === 0 || !mat.present[i] || mat.D[i] === 0) continue;
|
|
3887
|
+
const zi = z[i], r1 = 1 + i, cL = c[s * n + i], g = mat.D[i] / h;
|
|
3888
|
+
const pe = (vel * h) / mat.D[i];
|
|
3889
|
+
const d = zi * (phiR - phiL) - pe;
|
|
3890
|
+
const E = Math.expm1(u[bR * M + r1] - u[bL * M + r1] + (uLo[bR * M + r1] - uLo[bL * M + r1]) - pe);
|
|
3891
|
+
const gBc = g * bernoulli(d) * cL;
|
|
3892
|
+
const dNdd = -g * bernoulliDerivative(d) * cL * E;
|
|
3893
|
+
add(bL * M + r1, zi * F * gBc, 0);
|
|
3894
|
+
add(bR * M + r1, -zi * F * gBc * (E + 1), 0);
|
|
3895
|
+
if (hasPhi) {
|
|
3896
|
+
add(bL * M, zi * F * (-zi * dNdd + zi * gBc * E), 0);
|
|
3897
|
+
add(bR * M, zi * F * zi * dNdd, 0);
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
// The displacement toward +x, ε(φ_L − φ_R)/h (in φ̂ = φ/V_T).
|
|
3901
|
+
const k = hasPhi ? (mat.epsr * EPS0 * this.VT) / h : 0;
|
|
3902
|
+
if (k > 0) {
|
|
3903
|
+
add(bL * M, 0, k);
|
|
3904
|
+
add(bR * M, 0, -k);
|
|
3905
|
+
}
|
|
3906
|
+
if (terms.size === 0) continue;
|
|
3907
|
+
const idx = [...terms.keys()];
|
|
3908
|
+
cuts.push({ idx, g: idx.map((x) => terms.get(x)[0]), d: idx.map((x) => terms.get(x)[1]) });
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
return cuts;
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3137
3914
|
// Complex profiles of δφ (V), δμ̄ (J/mol) and δc (mol/m³) from a small-signal solution.
|
|
3138
3915
|
_smallSignalProfiles(xr, xi) {
|
|
3139
3916
|
const { n, M, z, VT, model } = this;
|
|
@@ -3263,6 +4040,28 @@ export class Solver {
|
|
|
3263
4040
|
// Steady state from the present one; `atSteady` says whether the state is one (until a step or
|
|
3264
4041
|
// a change of drive).
|
|
3265
4042
|
solveSteady(opts = {}) {
|
|
4043
|
+
this.flattening = true;
|
|
4044
|
+
this.unreached = null; // (a driven current no held voltage reaches: see _floatingContinuation)
|
|
4045
|
+
try {
|
|
4046
|
+
return this._solveSteadyAll(opts);
|
|
4047
|
+
} finally {
|
|
4048
|
+
this.flattening = false;
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
_solveSteadyAll(opts) {
|
|
4053
|
+
// A terminal that passes current only by charging (a gate, a capacitance with no species
|
|
4054
|
+
// through it) has no steady state under a current drive: it charges for ever, or at I = 0
|
|
4055
|
+
// keeps whatever charge it started with, which a steady solve doesn't know.
|
|
4056
|
+
const { species } = this.model, ions = (links) => links.some((l, i) => l.type !== 'blocked' && species[i].z !== 0);
|
|
4057
|
+
for (const k of this.floating) {
|
|
4058
|
+
const t = this.terms[k];
|
|
4059
|
+
if (t.drive.kind !== 'I') continue;
|
|
4060
|
+
const only = t.kind === 'port' ? (p) => p.reactions.length === 0 && !ions(p.species) : (c) => !ions(c.species);
|
|
4061
|
+
if (only(t.kind === 'port' ? this.model.ports[t.index] : this.model.contacts[t.side])) {
|
|
4062
|
+
throw new DeviceError(`${t.kind === 'port' ? `ports[${t.index}]` : `contacts.${t.side}`} passes current only by charging, so driven by a current it has no steady state; hold it at V (or a source behind R), or advance() in time`);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
3266
4065
|
const r = this._steadyFromHere(opts);
|
|
3267
4066
|
this.atSteady = r.converged;
|
|
3268
4067
|
return r;
|
|
@@ -3288,25 +4087,44 @@ export class Solver {
|
|
|
3288
4087
|
if (r.converged) return r;
|
|
3289
4088
|
// Generation (e.g. light) holding the device far from equilibrium: ramp it up from nearly
|
|
3290
4089
|
// nothing, each solve warm from the last.
|
|
4090
|
+
let dimmer = false;
|
|
3291
4091
|
if (this.hasGeneration && opts.continuation !== false) {
|
|
3292
4092
|
this.u.set(u0);
|
|
3293
4093
|
this.uLo.set(u0Lo);
|
|
3294
4094
|
this.termV.set(v0);
|
|
3295
4095
|
this.computeConcentrations();
|
|
4096
|
+
this.dimmer = false;
|
|
3296
4097
|
const g = this._generationContinuation(opts, r);
|
|
4098
|
+
dimmer = this.dimmer;
|
|
3297
4099
|
if (g.converged) {
|
|
3298
4100
|
this.solvedV = target;
|
|
3299
4101
|
return g;
|
|
3300
4102
|
}
|
|
3301
4103
|
r = g;
|
|
3302
4104
|
}
|
|
4105
|
+
// A terminal driven by a current (open circuit, say): hold it at a voltage instead, march the
|
|
4106
|
+
// voltage until the current crosses its target, and float it from there.
|
|
4107
|
+
if (opts.continuation !== false && this.floating.some((k) => this.terms[k].drive.kind === 'I')) {
|
|
4108
|
+
this.u.set(u0);
|
|
4109
|
+
this.uLo.set(u0Lo);
|
|
4110
|
+
this.termV.set(v0);
|
|
4111
|
+
this.computeConcentrations();
|
|
4112
|
+
const q = this._floatingContinuation(opts, r);
|
|
4113
|
+
if (q.converged) return q;
|
|
4114
|
+
r = q;
|
|
4115
|
+
}
|
|
3303
4116
|
if (!canContinue) {
|
|
3304
4117
|
if (quick) {
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
4118
|
+
// The full pseudo-transient ramp: from the dimmer light's solution where the light's
|
|
4119
|
+
// continuation got anywhere (a lit floating base charges from there as it would in
|
|
4120
|
+
// time), else from the start.
|
|
4121
|
+
if (!dimmer) {
|
|
4122
|
+
this.u.set(u0);
|
|
4123
|
+
this.uLo.set(u0Lo);
|
|
4124
|
+
this.termV.set(v0);
|
|
4125
|
+
}
|
|
3308
4126
|
this.computeConcentrations();
|
|
3309
|
-
const full = this._solveSteady(opts);
|
|
4127
|
+
const full = this._solveSteady(opts);
|
|
3310
4128
|
return { ...full, steps: full.steps + r.steps, iterations: full.iterations + r.iterations };
|
|
3311
4129
|
}
|
|
3312
4130
|
return r;
|
|
@@ -3334,18 +4152,121 @@ export class Solver {
|
|
|
3334
4152
|
return { ...r, steps: r.steps + c.steps, iterations: r.iterations + c.iterations };
|
|
3335
4153
|
}
|
|
3336
4154
|
|
|
4155
|
+
// A cold steady solve with a terminal driven by a current, which can fail where the same state
|
|
4156
|
+
// is easy warm (a solar cell's open circuit, from cold on a fine grid): the terminal held at a
|
|
4157
|
+
// voltage instead, whose steady solves have their own continuation, the voltage marched from
|
|
4158
|
+
// the start's until the current crosses its target, the crossing narrowed by bisection, and
|
|
4159
|
+
// the terminal floated again from beside it.
|
|
4160
|
+
_floatingContinuation(opts, r) {
|
|
4161
|
+
const k = this.floating.find((j) => this.terms[j].drive.kind === 'I');
|
|
4162
|
+
const drive = this.terms[k].drive, target = sourceAt(drive.src, this.time);
|
|
4163
|
+
let steps = r.steps ?? 0, iterations = r.iterations ?? 0;
|
|
4164
|
+
const history = (r.history ?? []).slice();
|
|
4165
|
+
const hold = (V) => {
|
|
4166
|
+
this.terms[k].drive = { kind: 'V', src: { value: V }, R: 0 };
|
|
4167
|
+
this.redrive();
|
|
4168
|
+
const q = this._steadyFromHere(opts);
|
|
4169
|
+
steps += q.steps ?? 0;
|
|
4170
|
+
iterations += q.iterations ?? 0;
|
|
4171
|
+
history.push({ held: V, converged: q.converged });
|
|
4172
|
+
if (q.converged) seen.push([V, this.termI[k]]);
|
|
4173
|
+
return q.converged ? this.termI[k] - target : NaN;
|
|
4174
|
+
};
|
|
4175
|
+
const fail = () => ({ converged: false, steps, iterations, history });
|
|
4176
|
+
const seen = []; // [V, I] of each held solve that converged
|
|
4177
|
+
let Va = this.termV[k], fa, Vb, fb;
|
|
4178
|
+
try {
|
|
4179
|
+
fa = hold(Va);
|
|
4180
|
+
if (!Number.isFinite(fa)) {
|
|
4181
|
+
Va = 0;
|
|
4182
|
+
fa = hold(Va);
|
|
4183
|
+
}
|
|
4184
|
+
if (!Number.isFinite(fa)) return fail();
|
|
4185
|
+
// March, the step growing: up where the current is below its target, since raising a
|
|
4186
|
+
// terminal's voltage raises the current into a passive device (the other way, from the
|
|
4187
|
+
// start again, if 20 V brings no crossing).
|
|
4188
|
+
const start = Va, f0 = fa;
|
|
4189
|
+
[Vb, fb] = [Va, fa];
|
|
4190
|
+
for (const dir of [fa < 0 ? 1 : -1, fa < 0 ? -1 : 1]) {
|
|
4191
|
+
[Va, fa] = [start, f0];
|
|
4192
|
+
if (dir !== (f0 < 0 ? 1 : -1)) hold(start); // (back to the start, for a warm march)
|
|
4193
|
+
let dV = 0.02;
|
|
4194
|
+
Vb = Va + dir * dV;
|
|
4195
|
+
fb = hold(Vb);
|
|
4196
|
+
while (Number.isFinite(fb) && Math.sign(fb) === Math.sign(fa) && fa !== 0 && Math.abs(Vb - start) < 20) {
|
|
4197
|
+
[Va, fa] = [Vb, fb];
|
|
4198
|
+
dV *= 1.5;
|
|
4199
|
+
Vb = Va + dir * dV;
|
|
4200
|
+
fb = hold(Vb);
|
|
4201
|
+
}
|
|
4202
|
+
if (Number.isFinite(fb) && (Math.sign(fb) !== Math.sign(fa) || fa === 0)) break;
|
|
4203
|
+
}
|
|
4204
|
+
if (!Number.isFinite(fb) || (Math.sign(fb) === Math.sign(fa) && fa !== 0)) {
|
|
4205
|
+
// What the held voltages passed, at the two extremes reached.
|
|
4206
|
+
if (seen.length > 1) {
|
|
4207
|
+
seen.sort((a, b) => a[0] - b[0]);
|
|
4208
|
+
this.unreached = { terminal: this.terms[k].name, target, low: seen[0], high: seen.at(-1) };
|
|
4209
|
+
}
|
|
4210
|
+
return fail();
|
|
4211
|
+
}
|
|
4212
|
+
// Bisect (each held solve warm from the last) to a tenth of a millivolt.
|
|
4213
|
+
for (let it = 0; it < 40 && Math.abs(Vb - Va) > 1e-4; it++) {
|
|
4214
|
+
const Vm = (Va + Vb) / 2, fm = hold(Vm);
|
|
4215
|
+
if (!Number.isFinite(fm)) return fail();
|
|
4216
|
+
if (Math.sign(fm) === Math.sign(fa)) [Va, fa] = [Vm, fm];
|
|
4217
|
+
else [Vb, fb] = [Vm, fm];
|
|
4218
|
+
}
|
|
4219
|
+
hold(Math.abs(fa) < Math.abs(fb) ? Va : Vb);
|
|
4220
|
+
} finally {
|
|
4221
|
+
this.terms[k].drive = drive;
|
|
4222
|
+
this.redrive();
|
|
4223
|
+
}
|
|
4224
|
+
const q = this._solveSteady(opts);
|
|
4225
|
+
steps += q.steps ?? 0;
|
|
4226
|
+
iterations += q.iterations ?? 0;
|
|
4227
|
+
history.push(...(q.history ?? []));
|
|
4228
|
+
if (q.converged) return { ...q, steps, iterations, history };
|
|
4229
|
+
// Floated, the system can lose what held it doesn't (a closed redox cell driven near its
|
|
4230
|
+
// limit: its bordered row 33 digits short), but a current-driven steady state is the held
|
|
4231
|
+
// one at the voltage that passes the target: found by held solves, by regula falsi
|
|
4232
|
+
// (Illinois) within the bracket, to 1e-10 of the target.
|
|
4233
|
+
try {
|
|
4234
|
+
let side = 0;
|
|
4235
|
+
for (let it = 0; it < 60; it++) {
|
|
4236
|
+
const Vm = Va - (fa * (Vb - Va)) / (fb - fa), fm = hold(Vm);
|
|
4237
|
+
if (!Number.isFinite(fm)) return fail();
|
|
4238
|
+
if (Math.abs(fm) <= 1e-10 * Math.abs(target)) return { converged: true, steps, iterations, history, residual: Math.abs(fm) };
|
|
4239
|
+
if (Math.sign(fm) === Math.sign(fb)) {
|
|
4240
|
+
[Vb, fb] = [Vm, fm];
|
|
4241
|
+
if (side === -1) fa /= 2;
|
|
4242
|
+
side = -1;
|
|
4243
|
+
} else {
|
|
4244
|
+
[Va, fa] = [Vm, fm];
|
|
4245
|
+
if (side === 1) fb /= 2;
|
|
4246
|
+
side = 1;
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
return fail();
|
|
4250
|
+
} finally {
|
|
4251
|
+
this.terms[k].drive = drive;
|
|
4252
|
+
this.redrive();
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
|
|
3337
4256
|
// Steady solves with the generation reactions' rates scaled from 1e-12 up to 1, ×100 a step
|
|
3338
4257
|
// while they converge, smaller steps where they don't.
|
|
3339
4258
|
_generationContinuation(opts, r) {
|
|
3340
4259
|
const sub = this._directSteady() ? { ...opts, maxSteps: 1 } : opts;
|
|
3341
|
-
let s = 1e-
|
|
4260
|
+
let s = 1e-30, factor = 100, steps = r.steps, iterations = r.iterations;
|
|
3342
4261
|
const history = r.history.slice();
|
|
3343
4262
|
try {
|
|
3344
4263
|
this.generationScale = s;
|
|
4264
|
+
// (nearly dark: a cold start can need the full ramp, as a dark device's does)
|
|
3345
4265
|
let q = this._solveSteady(sub);
|
|
3346
4266
|
steps += q.steps;
|
|
3347
4267
|
iterations += q.iterations;
|
|
3348
4268
|
if (!q.converged) return { converged: false, steps, iterations, history };
|
|
4269
|
+
this.dimmer = true; // (the state is now a solution under dimmer light: a better start than cold)
|
|
3349
4270
|
while (s < 1) {
|
|
3350
4271
|
const next = Math.min(1, s * factor);
|
|
3351
4272
|
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
@@ -3363,7 +4284,7 @@ export class Solver {
|
|
|
3363
4284
|
this.termV.set(v1);
|
|
3364
4285
|
this.computeConcentrations();
|
|
3365
4286
|
factor = Math.sqrt(factor);
|
|
3366
|
-
if (factor < 1.
|
|
4287
|
+
if (factor < 1.5) return { converged: false, steps, iterations, history }; // (crawling: time steps do better)
|
|
3367
4288
|
}
|
|
3368
4289
|
}
|
|
3369
4290
|
return { converged: true, steps, iterations, history };
|
|
@@ -3381,6 +4302,17 @@ export class Solver {
|
|
|
3381
4302
|
let q;
|
|
3382
4303
|
if (!warm) {
|
|
3383
4304
|
q = this._solveSteady(opts);
|
|
4305
|
+
if (!q.converged) {
|
|
4306
|
+
// A cold device's start was laid out for the target (a set() before the first solve
|
|
4307
|
+
// builds it there): lay it out again, level.
|
|
4308
|
+
steps += q.steps;
|
|
4309
|
+
iterations += q.iterations;
|
|
4310
|
+
this._refreshSources();
|
|
4311
|
+
this.initFromComposition();
|
|
4312
|
+
q = this._solveSteady(opts);
|
|
4313
|
+
}
|
|
4314
|
+
// (lit, the level start may need the light ramped up too)
|
|
4315
|
+
if (!q.converged && this.hasGeneration) q = this._generationContinuation(opts, q);
|
|
3384
4316
|
steps += q.steps;
|
|
3385
4317
|
iterations += q.iterations;
|
|
3386
4318
|
if (!q.converged) return { ...r, steps, iterations };
|
|
@@ -3429,7 +4361,8 @@ export class Solver {
|
|
|
3429
4361
|
const direct = this._directSteady();
|
|
3430
4362
|
const giant = direct ? Infinity : 1e6 * tau;
|
|
3431
4363
|
if (direct) this._renormalizeSpectators(); // a starting point with the right amounts
|
|
3432
|
-
let dt = giant;
|
|
4364
|
+
let dt = giant, grow = 10; // (after a failure, dt grows by less, then back up to ×10)
|
|
4365
|
+
let triedDirect = 0;
|
|
3433
4366
|
let totalIter = 0, steps = 0, converged = false;
|
|
3434
4367
|
const history = [];
|
|
3435
4368
|
while (steps < maxSteps) {
|
|
@@ -3450,6 +4383,7 @@ export class Solver {
|
|
|
3450
4383
|
// Pseudo-transient continuation: down from a slow time scale until a step converges,
|
|
3451
4384
|
// as far as the fastest (slow ions mustn't stop it short of what a cold start needs).
|
|
3452
4385
|
dt = dt >= giant ? tau * 1e-6 : dt / 4;
|
|
4386
|
+
grow = 2;
|
|
3453
4387
|
if (dt < Math.min(tau * 1e-15, this.fastestTime() * 1e-2)) break;
|
|
3454
4388
|
continue;
|
|
3455
4389
|
}
|
|
@@ -3457,6 +4391,21 @@ export class Solver {
|
|
|
3457
4391
|
converged = true; // the steady equations themselves were solved
|
|
3458
4392
|
break;
|
|
3459
4393
|
}
|
|
4394
|
+
// Past the slowest diffusion time, the direct solve may already reach from here: try it
|
|
4395
|
+
// once a decade (the state kept if it fails), rather than step on to the giant dt through
|
|
4396
|
+
// steps that can fail for slow ions' sake.
|
|
4397
|
+
if (direct && dt < giant && dt >= tau && dt >= 10 * triedDirect) {
|
|
4398
|
+
triedDirect = dt;
|
|
4399
|
+
const snap = this._snapshot();
|
|
4400
|
+
const d = this.step(Infinity);
|
|
4401
|
+
totalIter += d.iterations;
|
|
4402
|
+
history.push({ dt: Infinity, converged: d.converged, iterations: d.iterations });
|
|
4403
|
+
if (d.converged) {
|
|
4404
|
+
converged = true;
|
|
4405
|
+
break;
|
|
4406
|
+
}
|
|
4407
|
+
this._restore(snap);
|
|
4408
|
+
}
|
|
3460
4409
|
if (dt >= giant && this._maxPotentialStep(this._diff()) < tol) {
|
|
3461
4410
|
converged = true;
|
|
3462
4411
|
if (dt > giant) {
|
|
@@ -3469,7 +4418,8 @@ export class Solver {
|
|
|
3469
4418
|
break;
|
|
3470
4419
|
}
|
|
3471
4420
|
if (dt < giant) {
|
|
3472
|
-
dt *=
|
|
4421
|
+
dt *= grow; // ramping up after a failure
|
|
4422
|
+
grow = Math.min(10, grow * 1.5);
|
|
3473
4423
|
if (dt > 1e6 * tau) dt = giant; // then the direct steady solve (or the giant step)
|
|
3474
4424
|
} else if (dt < 1e6 * giant) {
|
|
3475
4425
|
dt *= 10; // conserved amounts present and still moving: let dt keep growing (capped)
|