driftlet 0.10.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 +46 -36
- package/docs/device.md +24 -6
- package/docs/numerics.md +180 -21
- package/docs/reliability.md +122 -0
- package/llms.txt +11 -6
- package/package.json +2 -1
- package/src/blockTridiagonal.js +17 -4
- package/src/checks.js +17 -10
- package/src/difference.js +142 -0
- package/src/index.js +22 -6
- package/src/solution.js +8 -2
- package/src/solver.js +964 -125
- package/types/blockTridiagonal.d.ts +9 -1
- package/types/difference.d.ts +30 -0
- package/types/index.d.ts +2 -1
- package/types/solver.d.ts +49 -8
package/src/solver.js
CHANGED
|
@@ -7,6 +7,62 @@ function bvFactor(a, alpha) {
|
|
|
7
7
|
return { g, gp: alpha * Math.exp(alpha * a) + (1 - alpha) * e };
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// GMRES(m), restarted, for a real system op(x) = b, op already preconditioned; x holds the
|
|
11
|
+
// starting guess and receives the answer. Returns whether it converged.
|
|
12
|
+
function gmresReal(op, b, x, { m = 10, restarts = 3, tol = 1e-12 } = {}) {
|
|
13
|
+
const N = b.length;
|
|
14
|
+
const dot = (a, c) => {
|
|
15
|
+
let t = 0;
|
|
16
|
+
for (let j = 0; j < N; j++) t += a[j] * c[j];
|
|
17
|
+
return t;
|
|
18
|
+
};
|
|
19
|
+
const bn = Math.sqrt(dot(b, b)) || 1;
|
|
20
|
+
for (let cycle = 0; cycle < restarts; cycle++) {
|
|
21
|
+
const ax = op(x), r = Float64Array.from(b, (v, j) => v - ax[j]);
|
|
22
|
+
const beta = Math.sqrt(dot(r, r));
|
|
23
|
+
if (!(beta > tol * bn)) return true;
|
|
24
|
+
const V = [r.map((v) => v / beta)], H = Array.from({ length: m + 1 }, () => new Float64Array(m));
|
|
25
|
+
const cs = [], sn = [], g = new Float64Array(m + 1);
|
|
26
|
+
g[0] = beta;
|
|
27
|
+
let k = 0, converged = false;
|
|
28
|
+
for (; k < m; k++) {
|
|
29
|
+
const w = op(V[k]);
|
|
30
|
+
for (let i = 0; i <= k; i++) {
|
|
31
|
+
const h = dot(V[i], w);
|
|
32
|
+
H[i][k] = h;
|
|
33
|
+
for (let j = 0; j < N; j++) w[j] -= h * V[i][j];
|
|
34
|
+
}
|
|
35
|
+
const hn = Math.sqrt(dot(w, w));
|
|
36
|
+
V.push(w.map((v) => v / (hn || 1)));
|
|
37
|
+
for (let i = 0; i < k; i++) {
|
|
38
|
+
const a = H[i][k], c = H[i + 1][k];
|
|
39
|
+
H[i][k] = cs[i] * a + sn[i] * c;
|
|
40
|
+
H[i + 1][k] = -sn[i] * a + cs[i] * c;
|
|
41
|
+
}
|
|
42
|
+
const t = Math.hypot(H[k][k], hn), c = t > 0 ? H[k][k] / t : 1, sv = t > 0 ? hn / t : 0;
|
|
43
|
+
cs.push(c);
|
|
44
|
+
sn.push(sv);
|
|
45
|
+
H[k][k] = t;
|
|
46
|
+
g[k + 1] = -sv * g[k];
|
|
47
|
+
g[k] *= c;
|
|
48
|
+
if (!(Math.abs(g[k + 1]) > tol * bn) || !(hn > 0)) {
|
|
49
|
+
k++;
|
|
50
|
+
converged = true;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const y = new Float64Array(k);
|
|
55
|
+
for (let i = k - 1; i >= 0; i--) {
|
|
56
|
+
let v = g[i];
|
|
57
|
+
for (let j = i + 1; j < k; j++) v -= H[i][j] * y[j];
|
|
58
|
+
y[i] = v / H[i][i];
|
|
59
|
+
}
|
|
60
|
+
for (let i = 0; i < k; i++) for (let j = 0; j < N; j++) x[j] += y[i] * V[i][j];
|
|
61
|
+
if (converged) return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
10
66
|
// Solve the K×K complex system (Ar + i Ai) x = (last column), partial pivoting by modulus.
|
|
11
67
|
function complexSolve(Ar, Ai, K) {
|
|
12
68
|
for (let col = 0; col < K; col++) {
|
|
@@ -39,8 +95,13 @@ function complexSolve(Ar, Ai, K) {
|
|
|
39
95
|
}
|
|
40
96
|
|
|
41
97
|
// GMRES(m), restarted, for a complex system op(x) = b, with vectors as [re, im] pairs and op
|
|
42
|
-
// already preconditioned; x holds the starting guess and receives the answer.
|
|
43
|
-
|
|
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 } = {}) {
|
|
44
105
|
const N = b[0].length;
|
|
45
106
|
const cdot = (a, c) => {
|
|
46
107
|
let re = 0, im = 0;
|
|
@@ -52,15 +113,24 @@ function gmres(op, b, x, { m = 12, restarts = 4, tol = 1e-8 } = {}) {
|
|
|
52
113
|
};
|
|
53
114
|
const norm = (a) => Math.sqrt(cdot(a, a)[0]);
|
|
54
115
|
const bn = norm(b) || 1;
|
|
55
|
-
|
|
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++) {
|
|
56
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])];
|
|
57
|
-
const beta = norm(r);
|
|
58
|
-
if (
|
|
59
|
-
|
|
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))]];
|
|
60
130
|
const H = Array.from({ length: m + 1 }, () => Array.from({ length: m }, () => [0, 0]));
|
|
61
131
|
const cs = [], sn = [], g = Array.from({ length: m + 1 }, () => [0, 0]);
|
|
62
|
-
g[0] = [beta, 0];
|
|
63
|
-
let k = 0
|
|
132
|
+
g[0] = [beta * bn, 0];
|
|
133
|
+
let k = 0;
|
|
64
134
|
for (; k < m; k++) {
|
|
65
135
|
const w = op(V[k]);
|
|
66
136
|
for (let i = 0; i <= k; i++) {
|
|
@@ -91,7 +161,6 @@ function gmres(op, b, x, { m = 12, restarts = 4, tol = 1e-8 } = {}) {
|
|
|
91
161
|
g[k + 1] = [-(s[0] * gr + s[1] * gi), -(s[0] * gi - s[1] * gr)];
|
|
92
162
|
if (!(Math.hypot(g[k + 1][0], g[k + 1][1]) > tol * bn)) {
|
|
93
163
|
k++;
|
|
94
|
-
converged = true;
|
|
95
164
|
break;
|
|
96
165
|
}
|
|
97
166
|
}
|
|
@@ -114,8 +183,10 @@ function gmres(op, b, x, { m = 12, restarts = 4, tol = 1e-8 } = {}) {
|
|
|
114
183
|
x[1][j] += yr * V[i][1][j] + yi * V[i][0][j];
|
|
115
184
|
}
|
|
116
185
|
}
|
|
117
|
-
if (converged) return; // (by its own residual estimate)
|
|
118
186
|
}
|
|
187
|
+
x[0].set(best[0]);
|
|
188
|
+
x[1].set(best[1]);
|
|
189
|
+
return { initial, residual: least };
|
|
119
190
|
}
|
|
120
191
|
|
|
121
192
|
// Discretisation and nonlinear solver.
|
|
@@ -147,6 +218,7 @@ import { BlockTridiagonal, ComplexBlockTridiagonal } from './blockTridiagonal.js
|
|
|
147
218
|
import { bernoulli, bernoulliDerivative } from './bernoulli.js';
|
|
148
219
|
import { EPS0, FARADAY } from './constants.js';
|
|
149
220
|
import { nextBreakpoint, sourceAt } from './device.js';
|
|
221
|
+
import { DifferenceTerms } from './difference.js';
|
|
150
222
|
import { DeviceError } from './errors.js';
|
|
151
223
|
import { powi, powr } from './pow.js';
|
|
152
224
|
|
|
@@ -343,6 +415,7 @@ export class Solver {
|
|
|
343
415
|
this.time = 0;
|
|
344
416
|
this.lastDt = Infinity;
|
|
345
417
|
this.atSteady = false; // whether the state is a converged steady solve
|
|
418
|
+
this.lin = null; // (set while assembling for DifferenceTerms)
|
|
346
419
|
// Contact bookkeeping, filled by assemble(): particle flux toward +x through each contact,
|
|
347
420
|
// and the displacement there (the metal's surface charge for a neutral link).
|
|
348
421
|
this.contactFlux = { left: new Float64Array(n), right: new Float64Array(n) };
|
|
@@ -415,12 +488,6 @@ export class Solver {
|
|
|
415
488
|
);
|
|
416
489
|
this.portRates = model.ports.map((port) => port.reactions.map(() => new Float64Array(port.nodes.length)));
|
|
417
490
|
|
|
418
|
-
// Whether any region is strictly neutral (ε = 0, not a conductor): see integrate().
|
|
419
|
-
this.strictlyNeutral = model.regions.some((reg) => {
|
|
420
|
-
const mat = materials[reg.material];
|
|
421
|
-
return !mat.conductor && mat.epsr === 0;
|
|
422
|
-
});
|
|
423
|
-
|
|
424
491
|
// Scales the generation reactions' rates during a steady solve's continuation (else 1).
|
|
425
492
|
this.generationScale = 1;
|
|
426
493
|
this.hasGeneration = this.rxs.some((rx) => rx.generation);
|
|
@@ -460,6 +527,18 @@ export class Solver {
|
|
|
460
527
|
}
|
|
461
528
|
});
|
|
462
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
|
+
});
|
|
463
542
|
this.combining = false;
|
|
464
543
|
this.bookkeeping = regions.map((_, r) => {
|
|
465
544
|
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r], nodes = [], segs = [];
|
|
@@ -505,6 +584,11 @@ export class Solver {
|
|
|
505
584
|
const sizes = new Int32Array(nB), loc = (this.loc = new Int32Array(nB * M).fill(-1));
|
|
506
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]++;
|
|
507
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;
|
|
508
592
|
const N = sys.size;
|
|
509
593
|
this.SINK = N;
|
|
510
594
|
const rix = (this.rix = new Int32Array(nB * M).fill(N)), fullOf = (this.fullOf = new Int32Array(N));
|
|
@@ -515,6 +599,7 @@ export class Solver {
|
|
|
515
599
|
}
|
|
516
600
|
this.res = new Float64Array(N + 1);
|
|
517
601
|
this.delta = new Float64Array(N + 1);
|
|
602
|
+
this.rowScale = new Float64Array(N + 1).fill(1); // each row's scaling in the factorised system
|
|
518
603
|
}
|
|
519
604
|
|
|
520
605
|
_factor() {
|
|
@@ -564,6 +649,8 @@ export class Solver {
|
|
|
564
649
|
if (reacting.length > 0) reactive = true;
|
|
565
650
|
const connected = leftOpen || rightOpen || linked.length > 0;
|
|
566
651
|
this.stretches.push({
|
|
652
|
+
leftOpen,
|
|
653
|
+
rightOpen,
|
|
567
654
|
linked, // ports that hold or exchange it
|
|
568
655
|
ports: [...new Set([...linked, ...reacting])], // ports that bring any in
|
|
569
656
|
species: i,
|
|
@@ -594,6 +681,33 @@ export class Solver {
|
|
|
594
681
|
st.mobile = true;
|
|
595
682
|
for (let q = st.regions[0]; q <= st.regions[1]; q++) if (!(materials[regions[q].material].D[st.species] > 0)) st.mobile = false;
|
|
596
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;
|
|
597
711
|
// Conserved amounts solved directly, each in place of one (redundant) balance row: a
|
|
598
712
|
// spectator's own amount, and each conserved combination of reacting stretches (the total
|
|
599
713
|
// iron of Fe³⁺, Fe²⁺ and FeCl²⁺, say), whose weighted balances sum to zero at steady state.
|
|
@@ -652,6 +766,66 @@ export class Solver {
|
|
|
652
766
|
reference: new Float64Array(sts[0].nodes[1] - sts[0].nodes[0] + 1),
|
|
653
767
|
});
|
|
654
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));
|
|
655
829
|
this.constrained = false;
|
|
656
830
|
}
|
|
657
831
|
|
|
@@ -668,6 +842,71 @@ export class Solver {
|
|
|
668
842
|
}
|
|
669
843
|
}
|
|
670
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
|
+
|
|
671
910
|
// The local constraints' rows (steady solves only): Σ w c_i − reference at each node.
|
|
672
911
|
_applyLocalConstraints() {
|
|
673
912
|
const { n, M, c, res, dA: d } = this;
|
|
@@ -815,7 +1054,8 @@ export class Solver {
|
|
|
815
1054
|
|
|
816
1055
|
// Solve the full Newton system, J δ = rhs, with the extra unknowns and rows that don't fit the
|
|
817
1056
|
// block-tridiagonal matrix T: the floating terminals' voltages (with their circuit rows), and
|
|
818
|
-
// the
|
|
1057
|
+
// the rows pinned in T: the spectators' conserved amounts (see _applyConstraints) and the
|
|
1058
|
+
// islands' summed balances (see _applyIslands). By low-rank
|
|
819
1059
|
// updates of T (Woodbury): δ = y + Σ_q Q_q μ_q − Σ_k X_k δV_k with y = T⁻¹ rhs, Q_q = T⁻¹ e_q
|
|
820
1060
|
// (the response to a unit pin) and X_k = T⁻¹ B_k (to a unit change of V_k), then a small dense
|
|
821
1061
|
// system for the μ (pins) and δV (terminals):
|
|
@@ -882,6 +1122,13 @@ export class Solver {
|
|
|
882
1122
|
}
|
|
883
1123
|
|
|
884
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
|
+
|
|
885
1132
|
/** Amount (mol per unit area) of surface column j's species, Γ Σ v·a·θ, in the current state. */
|
|
886
1133
|
surfaceAmount(j) {
|
|
887
1134
|
const [k, q] = this.surfCols[j], port = this.model.ports[k], vol = this.model.grid.vol;
|
|
@@ -1129,13 +1376,7 @@ export class Solver {
|
|
|
1129
1376
|
const shift = level(t.side) - level(this.terms[held].side);
|
|
1130
1377
|
if (Number.isFinite(shift)) this.termV[k] = this.termV[held] + shift;
|
|
1131
1378
|
}
|
|
1132
|
-
|
|
1133
|
-
for (let g = 0; g < this.nNodes; g++) {
|
|
1134
|
-
const k = this.surfPort[g];
|
|
1135
|
-
if (k < 0) continue;
|
|
1136
|
-
const surf = model.ports[k].surface, b = this.blockOfNode[g], bare = 1 - surf.reduce((t, sp) => t + sp.theta0, 0);
|
|
1137
|
-
surf.forEach((sp, q) => (u[b * M + 1 + n + q] = sp.mu0 / model.RT + Math.log(sp.theta0 / bare)));
|
|
1138
|
-
}
|
|
1379
|
+
for (let g = 0; g < this.nNodes; g++) if (this.surfPort[g] >= 0) this._surfaceStart(g);
|
|
1139
1380
|
this.computeConcentrations();
|
|
1140
1381
|
// A floating capacitance starts uncharged (σ = 0 at the window's mean φ), as the starting
|
|
1141
1382
|
// composition, neutral without it, assumes; with reactions too, its double layer then charges
|
|
@@ -1360,6 +1601,7 @@ export class Solver {
|
|
|
1360
1601
|
const { grid, materials, regions } = model;
|
|
1361
1602
|
sys.clear();
|
|
1362
1603
|
res.fill(0);
|
|
1604
|
+
if (this.lin) this.lin.reset(1 / dt);
|
|
1363
1605
|
this.computeConcentrations();
|
|
1364
1606
|
|
|
1365
1607
|
// Regions, each assembled by the kernel for its kind: a conductor (its carrier only), a
|
|
@@ -1391,8 +1633,13 @@ export class Solver {
|
|
|
1391
1633
|
// Terminals: ports (after every other term at their nodes, so a held level can read its
|
|
1392
1634
|
// flux), then contacts, then the circuit rows of the floating ones.
|
|
1393
1635
|
this._terminals(dt);
|
|
1636
|
+
if (this.flattening && dt === Infinity) this._flatRows();
|
|
1637
|
+
if (this.flattening && this.constrained && dt === Infinity) this._levelRows();
|
|
1394
1638
|
if (this.constrained && dt === Infinity && this.constraints.length > 0) this._applyConstraints();
|
|
1395
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] : [];
|
|
1396
1643
|
this.transformed = this.combining && dt !== Infinity;
|
|
1397
1644
|
if (this.transformed) this._chargeRows(dt);
|
|
1398
1645
|
}
|
|
@@ -1412,7 +1659,7 @@ export class Solver {
|
|
|
1412
1659
|
// charge in the Gauss row, written straight into the diagonal blocks by local index. A
|
|
1413
1660
|
// dielectric is the case with no species.
|
|
1414
1661
|
_nodesDilute(g0, g1, dt) {
|
|
1415
|
-
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;
|
|
1416
1663
|
const JB = sys.B, sizes = sys.sizes, offB = sys.offB, vol = this.model.grid.vol;
|
|
1417
1664
|
for (let g = g0; g <= g1; g++) {
|
|
1418
1665
|
const b = blockOfNode[g], v = vol[g];
|
|
@@ -1423,22 +1670,27 @@ export class Solver {
|
|
|
1423
1670
|
continue;
|
|
1424
1671
|
}
|
|
1425
1672
|
let q = rhoFixed[g], dq = 0;
|
|
1673
|
+
const late = this.combining && this.lateStorage[g] === 1 && dt !== Infinity; // (the ions' storage: in _contact)
|
|
1426
1674
|
for (let i = 0; i < n; i++) {
|
|
1427
1675
|
const k = g * n + i;
|
|
1428
1676
|
if (!present[k]) continue;
|
|
1429
|
-
const ck = c[k], l = loc[lb + 1 + i];
|
|
1430
|
-
res[R[lb + 1 + i]] += (v * (ck - cOld[k])) / dt;
|
|
1431
|
-
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;
|
|
1432
1679
|
q += F * z[i] * ck;
|
|
1433
1680
|
dq += F * z[i] * z[i] * ck;
|
|
1434
|
-
if (
|
|
1435
|
-
|
|
1436
|
-
|
|
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;
|
|
1437
1688
|
}
|
|
1689
|
+
if (p >= 0 && z[i] !== 0) JB[oB + p * m + l] += -v * F * z[i] * ck;
|
|
1438
1690
|
}
|
|
1439
1691
|
if (p >= 0) {
|
|
1440
1692
|
res[R[lb]] -= v * q;
|
|
1441
|
-
JB[oB + p * m + p] += v * dq;
|
|
1693
|
+
if (!lin) JB[oB + p * m + p] += v * dq;
|
|
1442
1694
|
}
|
|
1443
1695
|
}
|
|
1444
1696
|
}
|
|
@@ -1525,6 +1777,7 @@ export class Solver {
|
|
|
1525
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];
|
|
1526
1778
|
res[rk] += w * res[ri];
|
|
1527
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);
|
|
1528
1781
|
}
|
|
1529
1782
|
// Columns: φ̂' = φ̂ with η fixed becomes φ̂ with η_i shifted by z_i, wherever node g's
|
|
1530
1783
|
// unknowns appear (its own rows, its neighbours', a terminal's current).
|
|
@@ -1651,6 +1904,7 @@ export class Solver {
|
|
|
1651
1904
|
const mL = sys.sizes[bL], mR = sys.sizes[bR], pL = loc[bL * M], pR = loc[bR * M];
|
|
1652
1905
|
res[R[bL * M]] += D;
|
|
1653
1906
|
res[R[bR * M]] -= D;
|
|
1907
|
+
if (this.lin) return this.lin.displacement(R[bL * M], R[bR * M], k);
|
|
1654
1908
|
sys.B[sys.offB[bL] + pL * mL + pL] += k;
|
|
1655
1909
|
sys.C[sys.offC[bL] + pL * mR + pR] -= k;
|
|
1656
1910
|
sys.A[sys.offA[bR] + pR * mL + pL] -= k;
|
|
@@ -1664,7 +1918,7 @@ export class Solver {
|
|
|
1664
1918
|
// fluxes (e.g. majority carriers carrying a small current) don't vanish in the cancellation of
|
|
1665
1919
|
// two huge drift and diffusion terms.
|
|
1666
1920
|
_segmentsDilute(g0, g1, region, mat, vel) {
|
|
1667
|
-
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;
|
|
1668
1922
|
const { A: JA, B: JB, C: JC, sizes, offA, offB, offC } = sys;
|
|
1669
1923
|
const { segLength, segArea } = this.model.grid;
|
|
1670
1924
|
for (let s = g0; s < g1; s++) {
|
|
@@ -1689,6 +1943,10 @@ export class Solver {
|
|
|
1689
1943
|
const dNdPhiR = zi * dNdd;
|
|
1690
1944
|
res[R[bL * M + r]] += N;
|
|
1691
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
|
+
}
|
|
1692
1950
|
// Row r of block bL (couplings to itself in B, to bR in C) and of bR (to bL in A).
|
|
1693
1951
|
const lL = loc[bL * M + r], lR = loc[bR * M + r];
|
|
1694
1952
|
const oBL = offB[bL] + lL * mL, oCL = offC[bL] + lL * mR, oAR = offA[bR] + lR * mL, oBR = offB[bR] + lR * mR;
|
|
@@ -2121,6 +2379,7 @@ export class Solver {
|
|
|
2121
2379
|
_captureRow(b, s, w, into) {
|
|
2122
2380
|
const l = this.loc[b * this.M + s];
|
|
2123
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);
|
|
2124
2383
|
const { sys, nB } = this, { sizes, offA, offB, offC, offX } = sys, m = sizes[b];
|
|
2125
2384
|
for (const [X, nb, off] of [['A', b - 1, offA], ['B', b, offB], ['C', b + 1, offC]]) {
|
|
2126
2385
|
if (nb < 0 || nb >= nB) continue;
|
|
@@ -2323,6 +2582,19 @@ export class Solver {
|
|
|
2323
2582
|
this._captureRow(b, 1 + i, z[i] * F, C);
|
|
2324
2583
|
}
|
|
2325
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
|
+
}
|
|
2326
2598
|
|
|
2327
2599
|
// Conductance links: J (toward the device) = G (V_out − V_i), V_out = V_t + offset; exchange
|
|
2328
2600
|
// links (neutral species): N_in = k (μ_out − μ)/RT.
|
|
@@ -2372,7 +2644,7 @@ export class Solver {
|
|
|
2372
2644
|
// The Poisson residual is the outside's charge, D_in.
|
|
2373
2645
|
const Din = res[R[b * M]];
|
|
2374
2646
|
this.contactD[side] = sgn * Din;
|
|
2375
|
-
if (dyn) {
|
|
2647
|
+
if (dyn && !(this.combining && this.lateStorage[g] === 1)) {
|
|
2376
2648
|
I += (Din - Dstart) / dt;
|
|
2377
2649
|
this._captureRow(b, 0, 1 / dt, C);
|
|
2378
2650
|
}
|
|
@@ -2405,10 +2677,59 @@ export class Solver {
|
|
|
2405
2677
|
|
|
2406
2678
|
|
|
2407
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
|
+
|
|
2408
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
|
+
}
|
|
2409
2729
|
_replaceRow(b, rs) {
|
|
2410
2730
|
const l = this.loc[b * this.M + rs];
|
|
2411
2731
|
if (l < 0) return;
|
|
2732
|
+
if (this.lin) this.lin.kill(this.rix[b * this.M + rs]);
|
|
2412
2733
|
const { sys, nB } = this, sz = sys.sizes, m = sz[b];
|
|
2413
2734
|
sys.B.fill(0, sys.offB[b] + l * m, sys.offB[b] + (l + 1) * m);
|
|
2414
2735
|
if (b > 0) sys.A.fill(0, sys.offA[b] + l * sz[b - 1], sys.offA[b] + (l + 1) * sz[b - 1]);
|
|
@@ -2427,8 +2748,12 @@ export class Solver {
|
|
|
2427
2748
|
for (let k = 0; k < mp; k++) mx = Math.max(mx, Math.abs(A[oa + k]));
|
|
2428
2749
|
for (let k = 0; k < m; k++) mx = Math.max(mx, Math.abs(B[ob + k]));
|
|
2429
2750
|
for (let k = 0; k < mn; k++) mx = Math.max(mx, Math.abs(C[oc + k]));
|
|
2430
|
-
if (mx === 0)
|
|
2751
|
+
if (mx === 0) {
|
|
2752
|
+
this.rowScale[offX[b] + r] = 1;
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2431
2755
|
const s = 1 / mx;
|
|
2756
|
+
this.rowScale[offX[b] + r] = s;
|
|
2432
2757
|
for (let k = 0; k < mp; k++) A[oa + k] *= s;
|
|
2433
2758
|
for (let k = 0; k < m; k++) B[ob + k] *= s;
|
|
2434
2759
|
for (let k = 0; k < mn; k++) C[oc + k] *= s;
|
|
@@ -2438,6 +2763,111 @@ export class Solver {
|
|
|
2438
2763
|
}
|
|
2439
2764
|
}
|
|
2440
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
|
+
|
|
2441
2871
|
// Largest update among the potential-like unknowns (φ̂ and η at grid nodes).
|
|
2442
2872
|
_maxPotentialStep(delta, deltaV = this.deltaV) { // (deltaV null: the device's potentials only)
|
|
2443
2873
|
const { n, M } = this;
|
|
@@ -2460,16 +2890,55 @@ export class Solver {
|
|
|
2460
2890
|
* Newton iteration for one backward-Euler step from the current cOld.
|
|
2461
2891
|
* @returns {{converged: boolean, iterations: number, history: number[]}}
|
|
2462
2892
|
*/
|
|
2463
|
-
newton(dt, { maxIter = 60, tol = 1e-10, maxStep = 10 } = {}) {
|
|
2893
|
+
newton(dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands = false } = {}) {
|
|
2464
2894
|
const { delta, res } = this;
|
|
2465
2895
|
const deltaV = this.deltaV ?? (this.deltaV = new Float64Array(this.floating.length));
|
|
2466
2896
|
const history = [], ownHistory = [];
|
|
2467
|
-
//
|
|
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.
|
|
2468
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
|
+
}
|
|
2469
2931
|
if (dt === Infinity) this._noteConditioning();
|
|
2470
2932
|
return r;
|
|
2471
2933
|
};
|
|
2472
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
|
+
}
|
|
2473
2942
|
try {
|
|
2474
2943
|
this.assemble(dt);
|
|
2475
2944
|
} catch (err) {
|
|
@@ -2486,9 +2955,13 @@ export class Solver {
|
|
|
2486
2955
|
return fail({ converged: false, iterations: it, history, error: err.message });
|
|
2487
2956
|
}
|
|
2488
2957
|
try {
|
|
2489
|
-
const pins = this.
|
|
2958
|
+
const pins = this.pins;
|
|
2490
2959
|
deltaV.fill(0);
|
|
2491
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
|
+
}
|
|
2492
2965
|
if (this.transformed) this._untransform(delta);
|
|
2493
2966
|
} catch (err) {
|
|
2494
2967
|
if (!(err instanceof SolverError)) throw err;
|
|
@@ -2505,7 +2978,7 @@ export class Solver {
|
|
|
2505
2978
|
ownHistory.push(own);
|
|
2506
2979
|
if (!Number.isFinite(step)) return fail({ converged: false, iterations: it, history, error: 'non-finite update' });
|
|
2507
2980
|
// Give up early on clear divergence; the caller will take a smaller step instead.
|
|
2508
|
-
if (own > 1e4 || (it > 6 && own > 10 * ownHistory[0])) {
|
|
2981
|
+
if ((it > 1 && own > 1e4) || (it > 6 && own > 10 * ownHistory[0])) {
|
|
2509
2982
|
this.computeConcentrations();
|
|
2510
2983
|
return fail({ converged: false, iterations: it, history, error: 'diverging' });
|
|
2511
2984
|
}
|
|
@@ -2513,8 +2986,10 @@ export class Solver {
|
|
|
2513
2986
|
this._addToState(delta, -alpha);
|
|
2514
2987
|
this.floating.forEach((k, a) => (this.termV[k] -= alpha * deltaV[a]));
|
|
2515
2988
|
if (alpha === 1 && step < tol) {
|
|
2989
|
+
const done = { converged: true, iterations: it, history, residual: rmax };
|
|
2990
|
+
if (islandsOff(it, done)) continue;
|
|
2516
2991
|
this.computeConcentrations();
|
|
2517
|
-
return
|
|
2992
|
+
return done;
|
|
2518
2993
|
}
|
|
2519
2994
|
// Converged as far as round-off allows: the updates are already tiny (below 1e-6 thermal
|
|
2520
2995
|
// units, ~26 nV) and have stopped shrinking. A badly conditioned system's floor can sit
|
|
@@ -2523,15 +2998,35 @@ export class Solver {
|
|
|
2523
2998
|
// (Raising this floor lets a weakly held population drift: a MOS capacitor without a
|
|
2524
2999
|
// channel port then showed a DC leak through its oxide.)
|
|
2525
3000
|
const [p1, p2] = [history[history.length - 2], history[history.length - 3]], floor = 1e-6;
|
|
2526
|
-
|
|
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;
|
|
2527
3011
|
this.computeConcentrations();
|
|
2528
|
-
return
|
|
3012
|
+
return done;
|
|
2529
3013
|
}
|
|
2530
3014
|
}
|
|
2531
3015
|
this.computeConcentrations();
|
|
2532
3016
|
return fail({ converged: false, iterations: maxIter, history });
|
|
2533
3017
|
}
|
|
2534
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
|
+
|
|
2535
3030
|
// How many digits the last factorisation lost to cancellation, and where (the worst seen since
|
|
2536
3031
|
// the steady solve began). Only for diagnosing failures: a saturated species, for instance,
|
|
2537
3032
|
// can lose digits harmlessly.
|
|
@@ -2648,6 +3143,20 @@ export class Solver {
|
|
|
2648
3143
|
let result;
|
|
2649
3144
|
try {
|
|
2650
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
|
+
}
|
|
2651
3160
|
} finally {
|
|
2652
3161
|
this.combining = false;
|
|
2653
3162
|
}
|
|
@@ -2690,11 +3199,11 @@ export class Solver {
|
|
|
2690
3199
|
if (!(tEnd > this.time)) return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace };
|
|
2691
3200
|
let dt = this.dtNext ?? opts.dt0 ?? (tEnd - this.time) * 1e-4;
|
|
2692
3201
|
if (!(dt > 0)) dt = (tEnd - this.time) * 1e-4;
|
|
2693
|
-
let grow = 0; // longer first steps tried after a Newton failure (see below)
|
|
2694
3202
|
// The shortest step worth trying: round-off relative to the time, or the device's fastest
|
|
2695
3203
|
// time scale, whichever is shorter (a cold start can need steps far below a long run's 1e-14).
|
|
2696
3204
|
const floor = Math.min(1e-14 * Math.max(tEnd, 1e-300), this.fastestTime() * 1e-2);
|
|
2697
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)
|
|
2698
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);
|
|
2699
3208
|
while (this.time < tEnd) {
|
|
2700
3209
|
if (steps + rejected >= maxSteps || clock() - start > budgetMs) break;
|
|
@@ -2719,6 +3228,7 @@ export class Solver {
|
|
|
2719
3228
|
this.landing = atBreak ? tb : clamped ? tEnd : undefined;
|
|
2720
3229
|
const snap = this._snapshot();
|
|
2721
3230
|
let err, p, half = null;
|
|
3231
|
+
if (this.history.length > 0) firstErrs.length = 0;
|
|
2722
3232
|
if (this.history.length === 0) {
|
|
2723
3233
|
// First step: backward Euler, checked against two half steps (whose result is kept).
|
|
2724
3234
|
const full = this.step(h);
|
|
@@ -2739,17 +3249,7 @@ export class Solver {
|
|
|
2739
3249
|
if (err === undefined) {
|
|
2740
3250
|
this._restore(snap);
|
|
2741
3251
|
rejected++;
|
|
2742
|
-
|
|
2743
|
-
// helps; but right after a jump in a strictly neutral (ε = 0) material, the shorter the
|
|
2744
|
-
// step the worse conditioned it is (the field is fixed only through fluxes, which the
|
|
2745
|
-
// storage term dwarfs), so try longer ones first.
|
|
2746
|
-
if (this.strictlyNeutral && grow < 3 && h * 16 < tEnd - this.time) {
|
|
2747
|
-
grow++;
|
|
2748
|
-
dt = h * 16;
|
|
2749
|
-
} else {
|
|
2750
|
-
grow = 3;
|
|
2751
|
-
dt = h / 4;
|
|
2752
|
-
}
|
|
3252
|
+
dt = h / 4;
|
|
2753
3253
|
if (dt < floor) { failed = true; break; }
|
|
2754
3254
|
continue;
|
|
2755
3255
|
}
|
|
@@ -2767,14 +3267,20 @@ export class Solver {
|
|
|
2767
3267
|
err = this._errorNorm(pred) * p.scale;
|
|
2768
3268
|
p = p.order;
|
|
2769
3269
|
}
|
|
2770
|
-
|
|
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) {
|
|
2771
3278
|
this._restore(snap);
|
|
2772
3279
|
rejected++;
|
|
2773
3280
|
dt = h * factor(err, p);
|
|
2774
3281
|
continue;
|
|
2775
3282
|
}
|
|
2776
3283
|
steps++;
|
|
2777
|
-
grow = 0;
|
|
2778
3284
|
// The state the first step started from needn't satisfy the algebraic equations (φ in a
|
|
2779
3285
|
// strictly neutral region, an interface's unknowns, after a start or a jump), and they
|
|
2780
3286
|
// jump in the first instant; a predictor through it would see that jump as error on every
|
|
@@ -3034,52 +3540,17 @@ export class Solver {
|
|
|
3034
3540
|
for (const X of ['A', 'B', 'C']) S[X] = sys[X].map((v, k) => (v - J[X][k]) * dts);
|
|
3035
3541
|
const Cs = this.termC.map((v, k) => v.map((x, j) => (x - Ct[k][j]) * dts));
|
|
3036
3542
|
const DIs = this.termDI.map((x, k) => (x - DI[k]) * dts);
|
|
3037
|
-
|
|
3038
|
-
// a flux's dependence on η_L and η_R as two
|
|
3039
|
-
// layer) and v nearly uniform, J·v loses the
|
|
3040
|
-
//
|
|
3041
|
-
//
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
const
|
|
3046
|
-
const
|
|
3047
|
-
|
|
3048
|
-
for (let j = 0; j < low.length; j++) {
|
|
3049
|
-
const full = this.fullOf[j];
|
|
3050
|
-
low[j] = nodeBlock[Math.floor(full / M)] && full % M !== 0 ? 1 : 0;
|
|
3051
|
-
}
|
|
3052
|
-
const resP = new Float64Array(this.res.length), resM = new Float64Array(this.res.length);
|
|
3053
|
-
const derivative = (v, k, sigma, out) => {
|
|
3054
|
-
let mx = Math.abs(sigma);
|
|
3055
|
-
for (let j = 0; j < v.length; j++) mx = Math.max(mx, Math.abs(v[j]));
|
|
3056
|
-
const h = 1e-4 / (mx || 1);
|
|
3057
|
-
for (const [sgn, into] of [[1, resP], [-1, resM]]) {
|
|
3058
|
-
this.u.set(u0);
|
|
3059
|
-
this.uLo.set(uLo0);
|
|
3060
|
-
this.termV.set(V0);
|
|
3061
|
-
for (let j = 0; j < v.length; j++) {
|
|
3062
|
-
const full = this.fullOf[j];
|
|
3063
|
-
if (low[j]) this.uLo[full] += sgn * h * v[j];
|
|
3064
|
-
else this.u[full] += sgn * h * v[j];
|
|
3065
|
-
}
|
|
3066
|
-
// A held terminal's voltage comes from its source at each assembly; a floating one's is
|
|
3067
|
-
// an unknown.
|
|
3068
|
-
if (k >= 0 && fl.includes(k)) this.termV[k] += sgn * h * sigma;
|
|
3069
|
-
else if (k >= 0) this.sourceOverride.set(k, V0[k] + sgn * h * sigma);
|
|
3070
|
-
this.assemble(Infinity);
|
|
3071
|
-
if (k >= 0) this.sourceOverride.delete(k);
|
|
3072
|
-
into.set(this.res);
|
|
3073
|
-
}
|
|
3074
|
-
for (let j = 0; j < out.length; j++) out[j] = (resP[j] - resM[j]) / (2 * h);
|
|
3075
|
-
};
|
|
3076
|
-
const restore = () => {
|
|
3077
|
-
this.u.set(u0);
|
|
3078
|
-
this.uLo.set(uLo0);
|
|
3079
|
-
this.termV.set(V0);
|
|
3080
|
-
this._refreshSources();
|
|
3081
|
-
this.computeConcentrations();
|
|
3082
|
-
};
|
|
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);
|
|
3083
3554
|
|
|
3084
3555
|
const { sizes, offA, offB, offC, offX } = sys;
|
|
3085
3556
|
const csys = new ComplexBlockTridiagonal(nB, sizes);
|
|
@@ -3125,9 +3596,6 @@ export class Solver {
|
|
|
3125
3596
|
}
|
|
3126
3597
|
}
|
|
3127
3598
|
csys.factor();
|
|
3128
|
-
// Each solve uses that residual-based J·v (S·v is safe from the matrix): in an inversion
|
|
3129
|
-
// layer fed by minority carriers that number ~1e3 cm⁻³ in the bulk, the assembled J alone
|
|
3130
|
-
// gives the layer an exchange path many orders too fast.
|
|
3131
3599
|
const sx = (X, vr, out) => {
|
|
3132
3600
|
for (let blk = 0; blk < nB; blk++) {
|
|
3133
3601
|
const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
|
|
@@ -3153,25 +3621,35 @@ export class Solver {
|
|
|
3153
3621
|
csys.solve(rRes, iRes, outR, outI);
|
|
3154
3622
|
return [outR, outI];
|
|
3155
3623
|
};
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
sx(
|
|
3160
|
-
sx(
|
|
3161
|
-
|
|
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);
|
|
3162
3630
|
for (let j = 0; j < Nc; j++) {
|
|
3163
3631
|
ar[j] = jr[j] - w * si[j];
|
|
3164
3632
|
ai[j] = ji[j] + w * sr[j];
|
|
3165
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);
|
|
3166
3642
|
return precondition(ar, ai);
|
|
3167
3643
|
};
|
|
3168
3644
|
const solve = (br, bi, outr, outi, k, sigma) => {
|
|
3169
3645
|
const rhs = k >= 0 ? precondition(Bt[k].map((v) => -sigma * v), zero) : [new Float64Array(Nc), new Float64Array(Nc)];
|
|
3170
3646
|
outr.set(rhs[0]);
|
|
3171
3647
|
outi.set(rhs[1]);
|
|
3172
|
-
|
|
3173
|
-
|
|
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);
|
|
3174
3651
|
};
|
|
3652
|
+
let accuracy = 1e-14; // (relative, of the solves: the readouts' error is weighed with it)
|
|
3175
3653
|
// y: the response with the floating terminals held; X_k: to a unit δV_k.
|
|
3176
3654
|
const heldT = dT.kind === 'V';
|
|
3177
3655
|
for (let j = 0; j < Nc; j++) {
|
|
@@ -3211,12 +3689,93 @@ export class Solver {
|
|
|
3211
3689
|
xr[this.fullOf[j]] = re;
|
|
3212
3690
|
xi[this.fullOf[j]] = im;
|
|
3213
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
|
+
};
|
|
3214
3742
|
let Zr, Zi;
|
|
3215
3743
|
if (heldT) {
|
|
3216
|
-
// δI per volt: (C + iωC′)·δx + (∂I/∂V)(1 + iω′) at the terminal
|
|
3217
|
-
let
|
|
3218
|
-
|
|
3219
|
-
|
|
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
|
+
}
|
|
3220
3779
|
const d2 = Ir * Ir + Ii * Ii; // Z = 1/δI
|
|
3221
3780
|
Zr = Ir / d2;
|
|
3222
3781
|
Zi = -Ii / d2;
|
|
@@ -3233,6 +3792,125 @@ export class Solver {
|
|
|
3233
3792
|
return out;
|
|
3234
3793
|
}
|
|
3235
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
|
+
|
|
3236
3914
|
// Complex profiles of δφ (V), δμ̄ (J/mol) and δc (mol/m³) from a small-signal solution.
|
|
3237
3915
|
_smallSignalProfiles(xr, xi) {
|
|
3238
3916
|
const { n, M, z, VT, model } = this;
|
|
@@ -3362,6 +4040,16 @@ export class Solver {
|
|
|
3362
4040
|
// Steady state from the present one; `atSteady` says whether the state is one (until a step or
|
|
3363
4041
|
// a change of drive).
|
|
3364
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) {
|
|
3365
4053
|
// A terminal that passes current only by charging (a gate, a capacitance with no species
|
|
3366
4054
|
// through it) has no steady state under a current drive: it charges for ever, or at I = 0
|
|
3367
4055
|
// keeps whatever charge it started with, which a steady solve doesn't know.
|
|
@@ -3399,25 +4087,44 @@ export class Solver {
|
|
|
3399
4087
|
if (r.converged) return r;
|
|
3400
4088
|
// Generation (e.g. light) holding the device far from equilibrium: ramp it up from nearly
|
|
3401
4089
|
// nothing, each solve warm from the last.
|
|
4090
|
+
let dimmer = false;
|
|
3402
4091
|
if (this.hasGeneration && opts.continuation !== false) {
|
|
3403
4092
|
this.u.set(u0);
|
|
3404
4093
|
this.uLo.set(u0Lo);
|
|
3405
4094
|
this.termV.set(v0);
|
|
3406
4095
|
this.computeConcentrations();
|
|
4096
|
+
this.dimmer = false;
|
|
3407
4097
|
const g = this._generationContinuation(opts, r);
|
|
4098
|
+
dimmer = this.dimmer;
|
|
3408
4099
|
if (g.converged) {
|
|
3409
4100
|
this.solvedV = target;
|
|
3410
4101
|
return g;
|
|
3411
4102
|
}
|
|
3412
4103
|
r = g;
|
|
3413
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
|
+
}
|
|
3414
4116
|
if (!canContinue) {
|
|
3415
4117
|
if (quick) {
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
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
|
+
}
|
|
3419
4126
|
this.computeConcentrations();
|
|
3420
|
-
const full = this._solveSteady(opts);
|
|
4127
|
+
const full = this._solveSteady(opts);
|
|
3421
4128
|
return { ...full, steps: full.steps + r.steps, iterations: full.iterations + r.iterations };
|
|
3422
4129
|
}
|
|
3423
4130
|
return r;
|
|
@@ -3445,18 +4152,121 @@ export class Solver {
|
|
|
3445
4152
|
return { ...r, steps: r.steps + c.steps, iterations: r.iterations + c.iterations };
|
|
3446
4153
|
}
|
|
3447
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
|
+
|
|
3448
4256
|
// Steady solves with the generation reactions' rates scaled from 1e-12 up to 1, ×100 a step
|
|
3449
4257
|
// while they converge, smaller steps where they don't.
|
|
3450
4258
|
_generationContinuation(opts, r) {
|
|
3451
4259
|
const sub = this._directSteady() ? { ...opts, maxSteps: 1 } : opts;
|
|
3452
|
-
let s = 1e-
|
|
4260
|
+
let s = 1e-30, factor = 100, steps = r.steps, iterations = r.iterations;
|
|
3453
4261
|
const history = r.history.slice();
|
|
3454
4262
|
try {
|
|
3455
4263
|
this.generationScale = s;
|
|
4264
|
+
// (nearly dark: a cold start can need the full ramp, as a dark device's does)
|
|
3456
4265
|
let q = this._solveSteady(sub);
|
|
3457
4266
|
steps += q.steps;
|
|
3458
4267
|
iterations += q.iterations;
|
|
3459
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)
|
|
3460
4270
|
while (s < 1) {
|
|
3461
4271
|
const next = Math.min(1, s * factor);
|
|
3462
4272
|
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
@@ -3474,7 +4284,7 @@ export class Solver {
|
|
|
3474
4284
|
this.termV.set(v1);
|
|
3475
4285
|
this.computeConcentrations();
|
|
3476
4286
|
factor = Math.sqrt(factor);
|
|
3477
|
-
if (factor < 1.
|
|
4287
|
+
if (factor < 1.5) return { converged: false, steps, iterations, history }; // (crawling: time steps do better)
|
|
3478
4288
|
}
|
|
3479
4289
|
}
|
|
3480
4290
|
return { converged: true, steps, iterations, history };
|
|
@@ -3492,6 +4302,17 @@ export class Solver {
|
|
|
3492
4302
|
let q;
|
|
3493
4303
|
if (!warm) {
|
|
3494
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);
|
|
3495
4316
|
steps += q.steps;
|
|
3496
4317
|
iterations += q.iterations;
|
|
3497
4318
|
if (!q.converged) return { ...r, steps, iterations };
|
|
@@ -3540,7 +4361,8 @@ export class Solver {
|
|
|
3540
4361
|
const direct = this._directSteady();
|
|
3541
4362
|
const giant = direct ? Infinity : 1e6 * tau;
|
|
3542
4363
|
if (direct) this._renormalizeSpectators(); // a starting point with the right amounts
|
|
3543
|
-
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;
|
|
3544
4366
|
let totalIter = 0, steps = 0, converged = false;
|
|
3545
4367
|
const history = [];
|
|
3546
4368
|
while (steps < maxSteps) {
|
|
@@ -3561,6 +4383,7 @@ export class Solver {
|
|
|
3561
4383
|
// Pseudo-transient continuation: down from a slow time scale until a step converges,
|
|
3562
4384
|
// as far as the fastest (slow ions mustn't stop it short of what a cold start needs).
|
|
3563
4385
|
dt = dt >= giant ? tau * 1e-6 : dt / 4;
|
|
4386
|
+
grow = 2;
|
|
3564
4387
|
if (dt < Math.min(tau * 1e-15, this.fastestTime() * 1e-2)) break;
|
|
3565
4388
|
continue;
|
|
3566
4389
|
}
|
|
@@ -3568,6 +4391,21 @@ export class Solver {
|
|
|
3568
4391
|
converged = true; // the steady equations themselves were solved
|
|
3569
4392
|
break;
|
|
3570
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
|
+
}
|
|
3571
4409
|
if (dt >= giant && this._maxPotentialStep(this._diff()) < tol) {
|
|
3572
4410
|
converged = true;
|
|
3573
4411
|
if (dt > giant) {
|
|
@@ -3580,7 +4418,8 @@ export class Solver {
|
|
|
3580
4418
|
break;
|
|
3581
4419
|
}
|
|
3582
4420
|
if (dt < giant) {
|
|
3583
|
-
dt *=
|
|
4421
|
+
dt *= grow; // ramping up after a failure
|
|
4422
|
+
grow = Math.min(10, grow * 1.5);
|
|
3584
4423
|
if (dt > 1e6 * tau) dt = giant; // then the direct steady solve (or the giant step)
|
|
3585
4424
|
} else if (dt < 1e6 * giant) {
|
|
3586
4425
|
dt *= 10; // conserved amounts present and still moving: let dt keep growing (capped)
|