driftlet 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -36
- package/docs/device.md +129 -7
- package/docs/kit.md +12 -0
- package/docs/numerics.md +188 -22
- package/docs/reliability.md +122 -0
- package/llms.txt +55 -8
- package/package.json +2 -1
- package/src/blockTridiagonal.js +17 -4
- package/src/channels.js +73 -0
- package/src/checks.js +17 -10
- package/src/describe.js +4 -2
- package/src/device.js +55 -11
- package/src/difference.js +142 -0
- package/src/index.js +22 -6
- package/src/kit.js +1 -0
- package/src/recorder.js +1 -1
- package/src/solution.js +20 -3
- package/src/solver.js +1166 -146
- package/types/blockTridiagonal.d.ts +9 -1
- package/types/channels.d.ts +39 -0
- package/types/difference.d.ts +30 -0
- package/types/index.d.ts +2 -1
- package/types/kit.d.ts +1 -0
- package/types/recorder.d.ts +7 -3
- package/types/solver.d.ts +77 -11
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
|
|
|
@@ -189,17 +261,39 @@ export class SolverError extends Error {
|
|
|
189
261
|
}
|
|
190
262
|
}
|
|
191
263
|
|
|
264
|
+
// A gate's rate law (see normalizeGates in device.js) at voltage V: [rate, ∂rate/∂V], 1/s and 1/(s·V).
|
|
265
|
+
function gateRate(law, V) {
|
|
266
|
+
const { rate, midpoint, scale } = law;
|
|
267
|
+
if (law.type === 'exp') {
|
|
268
|
+
const v = rate * Math.exp((V - midpoint) / scale);
|
|
269
|
+
return [v, v / scale];
|
|
270
|
+
}
|
|
271
|
+
if (law.type === 'sigmoid') {
|
|
272
|
+
const e = Math.exp((midpoint - V) / scale), d = 1 + e;
|
|
273
|
+
return Number.isFinite(e) ? [rate / d, (rate * e) / (scale * d * d)] : [0, 0];
|
|
274
|
+
}
|
|
275
|
+
// expLinear: rate·y/(1 − e^−y), smooth through y = 0 (rate there) and → rate·y for large y
|
|
276
|
+
const y = (V - midpoint) / scale;
|
|
277
|
+
if (Math.abs(y) < 1e-6) return [rate * (1 + y / 2), rate / (2 * scale)];
|
|
278
|
+
if (y < -700) return [0, 0];
|
|
279
|
+
const em = -Math.expm1(-y), ey = Math.exp(-y); // 1 − e^−y, e^−y
|
|
280
|
+
return [(rate * y) / em, (rate * (em - y * ey)) / (em * em * scale)];
|
|
281
|
+
}
|
|
282
|
+
|
|
192
283
|
export class Solver {
|
|
193
284
|
constructor(model) {
|
|
194
285
|
this.model = model;
|
|
195
286
|
const { grid, species, materials, regions, contacts } = model;
|
|
196
287
|
const n = species.length;
|
|
197
288
|
// Slots per block: φ̂ (or D, or a conductor's segment flux J), one per species, and at a face
|
|
198
|
-
// one per face reaction (its rate), as many as the
|
|
199
|
-
|
|
289
|
+
// one per face reaction (its rate) and one per gate (its fraction open), as many as the
|
|
290
|
+
// busiest face has.
|
|
291
|
+
const nRx = model.interfaces.reduce((m, itf) => Math.max(m, itf.reactions.length + itf.gates.length), 0);
|
|
200
292
|
// (and at an electrode port's nodes, one per surface species: its coverage's η)
|
|
201
293
|
const nSurf = model.ports.reduce((m, port) => Math.max(m, port.surface.length), 0);
|
|
202
|
-
|
|
294
|
+
// (and a membrane port's gates, after them: each one's fraction open)
|
|
295
|
+
const nPortSlots = model.ports.reduce((m, port) => Math.max(m, port.surface.length + port.gates.length), 0);
|
|
296
|
+
const M = n + 1 + Math.max(nRx, nPortSlots);
|
|
203
297
|
const nNodes = grid.nNodes;
|
|
204
298
|
const nFaces = regions.length - 1;
|
|
205
299
|
const nB = nNodes + nFaces;
|
|
@@ -234,6 +328,20 @@ export class Solver {
|
|
|
234
328
|
// The bare fraction θ₀, directly (1 − Σθ cancels as θ → 1); 1 where there's no surface.
|
|
235
329
|
this.th0 = new Float64Array(nNodes).fill(1);
|
|
236
330
|
this.th0Old = new Float64Array(nNodes).fill(1);
|
|
331
|
+
// Gates: each one's slot (at a face, after its reaction rates; at a membrane port's nodes, after
|
|
332
|
+
// the surface's coverages) and value at the step's start. Faces' first, then ports', node by
|
|
333
|
+
// node through each window.
|
|
334
|
+
this.gatePort = new Int32Array(nNodes).fill(-1);
|
|
335
|
+
model.ports.forEach((port, k) => port.gates.length > 0 && port.nodes.forEach((g) => (this.gatePort[g] = k)));
|
|
336
|
+
this.gateList = [
|
|
337
|
+
...model.interfaces.flatMap((itf, f) => itf.gates.map((gate, q) => ({ f, q, gate, o: this.blockOfFace[f] * M + 1 + n + itf.reactions.length + q }))),
|
|
338
|
+
...model.ports.flatMap((port, k) => Array.from(port.nodes).flatMap((g) => port.gates.map((gate, q) => ({ port: k, g, q, gate, o: this.blockOfNode[g] * M + 1 + n + port.surface.length + q })))),
|
|
339
|
+
];
|
|
340
|
+
this.gateOld = new Float64Array(this.gateList.length);
|
|
341
|
+
this.gateIndex = new Int32Array(nFaces); // face f's first gate in gateList
|
|
342
|
+
let j = 0;
|
|
343
|
+
for (let f = 0; f < nFaces; j += model.interfaces[f].gates.length, f++) this.gateIndex[f] = j;
|
|
344
|
+
this.portGateIndex = model.ports.map((port) => ((j += port.nodes.length * port.gates.length), j - port.nodes.length * port.gates.length));
|
|
237
345
|
|
|
238
346
|
// Per-node material data, flattened [g·n + i].
|
|
239
347
|
this.present = new Uint8Array(nNodes * n);
|
|
@@ -343,6 +451,7 @@ export class Solver {
|
|
|
343
451
|
this.time = 0;
|
|
344
452
|
this.lastDt = Infinity;
|
|
345
453
|
this.atSteady = false; // whether the state is a converged steady solve
|
|
454
|
+
this.lin = null; // (set while assembling for DifferenceTerms)
|
|
346
455
|
// Contact bookkeeping, filled by assemble(): particle flux toward +x through each contact,
|
|
347
456
|
// and the displacement there (the metal's surface charge for a neutral link).
|
|
348
457
|
this.contactFlux = { left: new Float64Array(n), right: new Float64Array(n) };
|
|
@@ -415,12 +524,6 @@ export class Solver {
|
|
|
415
524
|
);
|
|
416
525
|
this.portRates = model.ports.map((port) => port.reactions.map(() => new Float64Array(port.nodes.length)));
|
|
417
526
|
|
|
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
527
|
// Scales the generation reactions' rates during a steady solve's continuation (else 1).
|
|
425
528
|
this.generationScale = 1;
|
|
426
529
|
this.hasGeneration = this.rxs.some((rx) => rx.generation);
|
|
@@ -460,6 +563,18 @@ export class Solver {
|
|
|
460
563
|
}
|
|
461
564
|
});
|
|
462
565
|
this.chargeNodes = Int32Array.from([...this.chargeNode.keys()].filter((g) => this.chargeNode[g]));
|
|
566
|
+
// An end node of a strictly neutral region that its contact holds (leaving it neutral: φ law
|
|
567
|
+
// bulk or neutral): the contact's current is the ions' charge passing, and their storage
|
|
568
|
+
// carries none under neutrality, nor does the box's net charge change (a bulk law's "D").
|
|
569
|
+
// Read from the box's balance, both come with round-off ∝ c/dt that cancels only roughly,
|
|
570
|
+
// which on short steps swamps the flux (a floating bath on a dilute side rattled). So there,
|
|
571
|
+
// on a transient step, the ions' storage goes in only after the contact has read its current,
|
|
572
|
+
// and the box's charge isn't counted as displacement (see _contact).
|
|
573
|
+
this.lateStorage = new Uint8Array(nNodes);
|
|
574
|
+
[0, nNodes - 1].forEach((g, k) => {
|
|
575
|
+
const mat = materials[regions[grid.nodeRegion[g]].material], law = contacts[k === 0 ? 'left' : 'right'].phi.type;
|
|
576
|
+
if (!mat.conductor && mat.epsr === 0 && (law === 'bulk' || law === 'neutral') && !this.chargeNode[g] && !this.phiUndefined[g] && this.nodeIdeal[g]) this.lateStorage[g] = 1;
|
|
577
|
+
});
|
|
463
578
|
this.combining = false;
|
|
464
579
|
this.bookkeeping = regions.map((_, r) => {
|
|
465
580
|
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r], nodes = [], segs = [];
|
|
@@ -495,16 +610,23 @@ export class Solver {
|
|
|
495
610
|
for (let i = 0; i < n; i++) active[b * M + 1 + i] = this.present[g * n + i];
|
|
496
611
|
const k = this.surfPort[g];
|
|
497
612
|
if (k >= 0) for (let q = 0; q < model.ports[k].surface.length; q++) active[b * M + 1 + n + q] = 1;
|
|
613
|
+
const kg = this.gatePort[g], pg = model.ports[kg];
|
|
614
|
+
if (kg >= 0) for (let q = 0; q < pg.gates.length; q++) active[b * M + 1 + n + pg.surface.length + q] = 1;
|
|
498
615
|
}
|
|
499
616
|
model.interfaces.forEach((itf, f) => {
|
|
500
617
|
const bf = this.blockOfFace[f];
|
|
501
618
|
active[bf * M] = itf.phi.type === 'neutral' ? 0 : 1;
|
|
502
619
|
for (let i = 0; i < n; i++) active[bf * M + 1 + i] = itf.links[i].type === 'blocked' ? 0 : 1;
|
|
503
|
-
for (let k = 0; k < itf.reactions.length; k++) active[bf * M + 1 + n + k] = 1;
|
|
620
|
+
for (let k = 0; k < itf.reactions.length + itf.gates.length; k++) active[bf * M + 1 + n + k] = 1;
|
|
504
621
|
});
|
|
505
622
|
const sizes = new Int32Array(nB), loc = (this.loc = new Int32Array(nB * M).fill(-1));
|
|
506
623
|
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
624
|
const sys = (this.sys = new BlockTridiagonal(nB, sizes));
|
|
625
|
+
// A pivot that cancels to exactly nothing (a population held only through conductances
|
|
626
|
+
// 1e-16 of its neighbours', as GaAs's minority carriers ~1 per m³ beside a face) is
|
|
627
|
+
// perturbed, not thrown on: Newton then converges, its small updates meaning small residuals
|
|
628
|
+
// (the factorisation only amplifies), refining its solves where they stall.
|
|
629
|
+
sys.staticPivots = true;
|
|
508
630
|
const N = sys.size;
|
|
509
631
|
this.SINK = N;
|
|
510
632
|
const rix = (this.rix = new Int32Array(nB * M).fill(N)), fullOf = (this.fullOf = new Int32Array(N));
|
|
@@ -515,6 +637,7 @@ export class Solver {
|
|
|
515
637
|
}
|
|
516
638
|
this.res = new Float64Array(N + 1);
|
|
517
639
|
this.delta = new Float64Array(N + 1);
|
|
640
|
+
this.rowScale = new Float64Array(N + 1).fill(1); // each row's scaling in the factorised system
|
|
518
641
|
}
|
|
519
642
|
|
|
520
643
|
_factor() {
|
|
@@ -564,6 +687,8 @@ export class Solver {
|
|
|
564
687
|
if (reacting.length > 0) reactive = true;
|
|
565
688
|
const connected = leftOpen || rightOpen || linked.length > 0;
|
|
566
689
|
this.stretches.push({
|
|
690
|
+
leftOpen,
|
|
691
|
+
rightOpen,
|
|
567
692
|
linked, // ports that hold or exchange it
|
|
568
693
|
ports: [...new Set([...linked, ...reacting])], // ports that bring any in
|
|
569
694
|
species: i,
|
|
@@ -594,6 +719,33 @@ export class Solver {
|
|
|
594
719
|
st.mobile = true;
|
|
595
720
|
for (let q = st.regions[0]; q <= st.regions[1]; q++) if (!(materials[regions[q].material].D[st.species] > 0)) st.mobile = false;
|
|
596
721
|
}
|
|
722
|
+
// Stretches whose steady state is known outright: a species that no reaction or port touches,
|
|
723
|
+
// reached by one contact only (the other end blocked), carries no flux at steady state, so its
|
|
724
|
+
// level is flat at that contact's. Steady solves pin it there (_flatRows) rather than find it
|
|
725
|
+
// through its own conduction, which can be all but nothing: a MOS capacitor's inversion
|
|
726
|
+
// electrons reach the back contact only through a bulk with ~1e3 of them per cm³. (Not
|
|
727
|
+
// through flow or mixing, nor a concentrated material's cross-diffusion, where zero flux
|
|
728
|
+
// isn't a flat level.)
|
|
729
|
+
this.flatStretches = this.stretches.filter((st) => {
|
|
730
|
+
if (st.reactive || st.ports.length > 0 || !st.mobile || st.leftOpen === st.rightOpen) return false;
|
|
731
|
+
for (let q = st.regions[0]; q <= st.regions[1]; q++) {
|
|
732
|
+
const reg = regions[q], mat = materials[reg.material];
|
|
733
|
+
if (mat.conductor || !mat.ideal || reg.velocity !== 0 || reg.mixing > 0) return false;
|
|
734
|
+
}
|
|
735
|
+
return true;
|
|
736
|
+
});
|
|
737
|
+
// Likewise a spectator that's mobile throughout: no flux at steady state, so its level is flat,
|
|
738
|
+
// at a value its amount fixes. Steady solves hold its levels equal (_levelRows) rather than
|
|
739
|
+
// find them through its own conduction: blocked SO₄²⁻ driven from a zinc cathode's extended
|
|
740
|
+
// space charge is down to 1e-25 mol/m³ there, and the chain of conductances through it lost
|
|
741
|
+
// the steady system 14 digits. (Within one region: across a face, the rows that would hold
|
|
742
|
+
// the level and pin the face's flux leave a diagonal block singular.)
|
|
743
|
+
this.levelStretches = this.stretches.filter((st) => {
|
|
744
|
+
if (!st.spectator || !st.mobile || st.ports.length > 0 || st.regions[0] !== st.regions[1]) return false;
|
|
745
|
+
const reg = regions[st.regions[0]], mat = materials[reg.material];
|
|
746
|
+
return !mat.conductor && mat.ideal && reg.velocity === 0 && !(reg.mixing > 0);
|
|
747
|
+
});
|
|
748
|
+
this.flattening = false;
|
|
597
749
|
// Conserved amounts solved directly, each in place of one (redundant) balance row: a
|
|
598
750
|
// spectator's own amount, and each conserved combination of reacting stretches (the total
|
|
599
751
|
// iron of Fe³⁺, Fe²⁺ and FeCl²⁺, say), whose weighted balances sum to zero at steady state.
|
|
@@ -652,6 +804,66 @@ export class Solver {
|
|
|
652
804
|
reference: new Float64Array(sts[0].nodes[1] - sts[0].nodes[0] + 1),
|
|
653
805
|
});
|
|
654
806
|
}
|
|
807
|
+
// Islands: the pieces of a stretch between its faces. One that nothing else feeds (no
|
|
808
|
+
// contact, port or reaction) holds its level only through its faces' fluxes, which can be
|
|
809
|
+
// ~1e-24 of its own conduction (a tiny conductance at the face, or a neighbour that barely
|
|
810
|
+
// conducts): eliminated, the level is lost to round-off, and Newton settles at a wrong one with
|
|
811
|
+
// nothing to show for it. In steady solves the piece's balance summed over its nodes, where
|
|
812
|
+
// the internal fluxes cancel exactly and the faces' alone are left, replaces its first node's
|
|
813
|
+
// (see _applyIslands), once the plain solves converge (see newton).
|
|
814
|
+
this.islands = [];
|
|
815
|
+
this.pinnedIslands = [];
|
|
816
|
+
this.islandsOn = false; // (newton() turns them on, see there)
|
|
817
|
+
this.pins = [];
|
|
818
|
+
const used = new Set(this.constraints.map((cs) => cs.row));
|
|
819
|
+
for (const st of this.stretches) {
|
|
820
|
+
const i = st.species, [s0, s1] = st.regions;
|
|
821
|
+
if (!st.mobile || conductor(st)) continue;
|
|
822
|
+
for (let q0 = s0, q1 = s0; q1 <= s1; q1++) {
|
|
823
|
+
const p0 = q0;
|
|
824
|
+
q0 = q1 + 1;
|
|
825
|
+
if (p0 === s0 && q1 === s1) continue; // (uncut)
|
|
826
|
+
if ((p0 === s0 && st.leftOpen) || (q1 === s1 && st.rightOpen)) continue;
|
|
827
|
+
const inside = (q) => q >= p0 && q <= q1;
|
|
828
|
+
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;
|
|
829
|
+
let reactive = false;
|
|
830
|
+
for (let q = p0; q <= q1; q++) {
|
|
831
|
+
for (const rx of model.reactions) if (rx.kf[regions[q].material] > 0 && [...rx.reactants, ...rx.products].some((p) => p.i === i)) reactive = true;
|
|
832
|
+
}
|
|
833
|
+
for (let f = Math.max(0, p0 - 1); f <= Math.min(model.interfaces.length - 1, q1); f++) {
|
|
834
|
+
for (const rx of model.interfaces[f].reactions) if (rx.part.some((p) => p.i === i && inside(f + p.side))) reactive = true;
|
|
835
|
+
}
|
|
836
|
+
const row = this.blockOfNode[grid.regionStart[p0]] * this.M + 1 + i;
|
|
837
|
+
if (reactive || used.has(row)) continue;
|
|
838
|
+
used.add(row);
|
|
839
|
+
// The faces' fluxes into the piece: −A_f u at its left face, +A_f u at its right. Across
|
|
840
|
+
// a face that holds μ̄ level, u is set only by the edge balances, and eliminated, it can
|
|
841
|
+
// come out of the piece's own (G Δη, its two η all but equal: a neighbour 1e20 times
|
|
842
|
+
// less conductive, and the piece's level was seen through one face of two, and Newton
|
|
843
|
+
// swung it back and forth across both). There the outside edge node's balance is added:
|
|
844
|
+
// u cancels, and the outside's first segment carries the flux instead.
|
|
845
|
+
const faces = [], outside = [];
|
|
846
|
+
if (p0 > s0) faces.push([p0 - 1, -1, grid.regionEnd[p0 - 1]]);
|
|
847
|
+
if (q1 < s1) faces.push([q1, 1, grid.regionStart[q1 + 1]]);
|
|
848
|
+
for (const [f, , g] of faces) {
|
|
849
|
+
if (model.interfaces[f].links[i].type !== 'equilibrium' || g === 0 || g === this.nNodes - 1) continue;
|
|
850
|
+
if (model.ports.some((port) => port.nodes.includes(g))) continue;
|
|
851
|
+
outside.push(this.blockOfNode[g] * this.M + 1 + i);
|
|
852
|
+
}
|
|
853
|
+
this.islands.push({
|
|
854
|
+
stretch: st,
|
|
855
|
+
row,
|
|
856
|
+
flux: faces.map(([f]) => this.blockOfFace[f] * this.M + 1 + i),
|
|
857
|
+
weight: faces.map(([f, sg]) => sg * grid.area[grid.regionEnd[f]]),
|
|
858
|
+
outside,
|
|
859
|
+
idx: new Int32Array(faces.length + 3 * this.M * outside.length),
|
|
860
|
+
w: new Float64Array(faces.length + 3 * this.M * outside.length),
|
|
861
|
+
len: 0,
|
|
862
|
+
res: 0,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
for (const isl of this.islands) isl.outside = isl.outside.filter((o) => !used.has(o));
|
|
655
867
|
this.constrained = false;
|
|
656
868
|
}
|
|
657
869
|
|
|
@@ -668,6 +880,71 @@ export class Solver {
|
|
|
668
880
|
}
|
|
669
881
|
}
|
|
670
882
|
|
|
883
|
+
// The islands' summed balances (steady solves only): Σ A_f u over the faces into each, its
|
|
884
|
+
// first node's row pinned in the factorised matrix and the sum bordered (_solveBordered) like a
|
|
885
|
+
// conserved amount's. The pieces of a stretch held flat by its contact are left as they are.
|
|
886
|
+
_applyIslands() {
|
|
887
|
+
const { u, res } = this, R = this.rix, out = [];
|
|
888
|
+
for (const isl of this.islands) {
|
|
889
|
+
if (this.flattening && this.flatStretches.includes(isl.stretch)) continue;
|
|
890
|
+
const b0 = Math.floor(isl.row / this.M), r0 = isl.row % this.M;
|
|
891
|
+
if (this.loc[isl.row] < 0) continue;
|
|
892
|
+
// Gathered by column (the faces' u, then each outside row's entries, which cancel those u
|
|
893
|
+
// exactly where they meet), then the nonzeros kept.
|
|
894
|
+
const acc = this.islandRow ?? (this.islandRow = new Float64Array(this.sys.size + 1));
|
|
895
|
+
const cols = [];
|
|
896
|
+
const add = (col, v) => {
|
|
897
|
+
if (acc[col] === 0) cols.push(col);
|
|
898
|
+
acc[col] += v;
|
|
899
|
+
};
|
|
900
|
+
let t = 0;
|
|
901
|
+
isl.flux.forEach((o, j) => {
|
|
902
|
+
if (this.loc[o] < 0) return;
|
|
903
|
+
t += isl.weight[j] * u[o];
|
|
904
|
+
add(R[o], isl.weight[j]);
|
|
905
|
+
});
|
|
906
|
+
for (const o of isl.outside) {
|
|
907
|
+
if (this.loc[o] < 0) continue;
|
|
908
|
+
t += res[R[o]];
|
|
909
|
+
this._rowInto(o, add);
|
|
910
|
+
}
|
|
911
|
+
let len = 0;
|
|
912
|
+
for (const col of cols) {
|
|
913
|
+
if (col < this.sys.size && acc[col] !== 0) {
|
|
914
|
+
isl.idx[len] = col;
|
|
915
|
+
isl.w[len++] = acc[col];
|
|
916
|
+
}
|
|
917
|
+
acc[col] = 0;
|
|
918
|
+
}
|
|
919
|
+
isl.len = len;
|
|
920
|
+
isl.res = t;
|
|
921
|
+
this._replaceRow(b0, r0);
|
|
922
|
+
for (const B of this.termB) B[R[isl.row]] = 0;
|
|
923
|
+
this._j(b0, r0, b0, r0, 1);
|
|
924
|
+
res[R[isl.row]] = 0;
|
|
925
|
+
out.push(isl);
|
|
926
|
+
}
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Row o's Jacobian entries, as add(compact column, value): the assembled ones, and the dilute
|
|
931
|
+
// kernels' kept aside in difference form (see _assembleDifference) when they are.
|
|
932
|
+
_rowInto(o, add) {
|
|
933
|
+
const { sys, M, nB } = this, b = Math.floor(o / M), l = this.loc[o];
|
|
934
|
+
const { A, B, C, sizes, offA, offB, offC, offX } = sys, m = sizes[b];
|
|
935
|
+
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]);
|
|
936
|
+
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]);
|
|
937
|
+
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]);
|
|
938
|
+
if (this.lin) {
|
|
939
|
+
const into = this.linRow ?? (this.linRow = new Float64Array(sys.size + 1));
|
|
940
|
+
this.lin.capture(this.rix[o], 1, 0, into);
|
|
941
|
+
for (let k = 0; k < sys.size; k++) {
|
|
942
|
+
if (into[k] !== 0) add(k, into[k]);
|
|
943
|
+
into[k] = 0;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
671
948
|
// The local constraints' rows (steady solves only): Σ w c_i − reference at each node.
|
|
672
949
|
_applyLocalConstraints() {
|
|
673
950
|
const { n, M, c, res, dA: d } = this;
|
|
@@ -815,7 +1092,8 @@ export class Solver {
|
|
|
815
1092
|
|
|
816
1093
|
// Solve the full Newton system, J δ = rhs, with the extra unknowns and rows that don't fit the
|
|
817
1094
|
// block-tridiagonal matrix T: the floating terminals' voltages (with their circuit rows), and
|
|
818
|
-
// the
|
|
1095
|
+
// the rows pinned in T: the spectators' conserved amounts (see _applyConstraints) and the
|
|
1096
|
+
// islands' summed balances (see _applyIslands). By low-rank
|
|
819
1097
|
// updates of T (Woodbury): δ = y + Σ_q Q_q μ_q − Σ_k X_k δV_k with y = T⁻¹ rhs, Q_q = T⁻¹ e_q
|
|
820
1098
|
// (the response to a unit pin) and X_k = T⁻¹ B_k (to a unit change of V_k), then a small dense
|
|
821
1099
|
// system for the μ (pins) and δV (terminals):
|
|
@@ -882,6 +1160,13 @@ export class Solver {
|
|
|
882
1160
|
}
|
|
883
1161
|
|
|
884
1162
|
|
|
1163
|
+
// An electrode surface at its starting coverages at node g: η = μ°/RT + ln(θ/θ₀).
|
|
1164
|
+
_surfaceStart(g) {
|
|
1165
|
+
const { n, M, u, model } = this, surf = model.ports[this.surfPort[g]].surface, b = this.blockOfNode[g];
|
|
1166
|
+
const bare = 1 - surf.reduce((t, sp) => t + sp.theta0, 0);
|
|
1167
|
+
surf.forEach((sp, q) => (u[b * M + 1 + n + q] = sp.mu0 / model.RT + Math.log(sp.theta0 / bare)));
|
|
1168
|
+
}
|
|
1169
|
+
|
|
885
1170
|
/** Amount (mol per unit area) of surface column j's species, Γ Σ v·a·θ, in the current state. */
|
|
886
1171
|
surfaceAmount(j) {
|
|
887
1172
|
const [k, q] = this.surfCols[j], port = this.model.ports[k], vol = this.model.grid.vol;
|
|
@@ -1129,13 +1414,7 @@ export class Solver {
|
|
|
1129
1414
|
const shift = level(t.side) - level(this.terms[held].side);
|
|
1130
1415
|
if (Number.isFinite(shift)) this.termV[k] = this.termV[held] + shift;
|
|
1131
1416
|
}
|
|
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
|
-
}
|
|
1417
|
+
for (let g = 0; g < this.nNodes; g++) if (this.surfPort[g] >= 0) this._surfaceStart(g);
|
|
1139
1418
|
this.computeConcentrations();
|
|
1140
1419
|
// A floating capacitance starts uncharged (σ = 0 at the window's mean φ), as the starting
|
|
1141
1420
|
// composition, neutral without it, assumes; with reactions too, its double layer then charges
|
|
@@ -1152,6 +1431,11 @@ export class Solver {
|
|
|
1152
1431
|
});
|
|
1153
1432
|
if (w > 0) this.termV[k] = port.capacitance.zeroCharge + sum / w;
|
|
1154
1433
|
}
|
|
1434
|
+
// Gates start open as far as the starting voltage across their face holds them, α/(α + β).
|
|
1435
|
+
for (const gt of this.gateList) {
|
|
1436
|
+
const { gate, o } = gt, V = this._gateVoltage(gt), a = gateRate(gate.alpha, V)[0], b = gateRate(gate.beta, V)[0];
|
|
1437
|
+
u[o] = a + b > 0 ? a / (a + b) : 0;
|
|
1438
|
+
}
|
|
1155
1439
|
// A floating electrode spread through a port (without a double layer) starts where its
|
|
1156
1440
|
// reactions pass the current it's set (none, behind a resistance): at its mixed potential in
|
|
1157
1441
|
// the start's composition, not level with a held terminal, which can be volts away and pass
|
|
@@ -1360,6 +1644,8 @@ export class Solver {
|
|
|
1360
1644
|
const { grid, materials, regions } = model;
|
|
1361
1645
|
sys.clear();
|
|
1362
1646
|
res.fill(0);
|
|
1647
|
+
this.dtNow = dt;
|
|
1648
|
+
if (this.lin) this.lin.reset(1 / dt);
|
|
1363
1649
|
this.computeConcentrations();
|
|
1364
1650
|
|
|
1365
1651
|
// Regions, each assembled by the kernel for its kind: a conductor (its carrier only), a
|
|
@@ -1391,8 +1677,13 @@ export class Solver {
|
|
|
1391
1677
|
// Terminals: ports (after every other term at their nodes, so a held level can read its
|
|
1392
1678
|
// flux), then contacts, then the circuit rows of the floating ones.
|
|
1393
1679
|
this._terminals(dt);
|
|
1680
|
+
if (this.flattening && dt === Infinity) this._flatRows();
|
|
1681
|
+
if (this.flattening && this.constrained && dt === Infinity) this._levelRows();
|
|
1394
1682
|
if (this.constrained && dt === Infinity && this.constraints.length > 0) this._applyConstraints();
|
|
1395
1683
|
if (this.constrained && dt === Infinity && this.localConstraints.length > 0) this._applyLocalConstraints();
|
|
1684
|
+
// Rows kept aside for _solveBordered: the conserved amounts' and the islands'.
|
|
1685
|
+
this.pinnedIslands = this.constrained && dt === Infinity && this.islandsOn ? this._applyIslands() : [];
|
|
1686
|
+
this.pins = this.constrained && dt === Infinity ? [...this.constraints, ...this.pinnedIslands] : [];
|
|
1396
1687
|
this.transformed = this.combining && dt !== Infinity;
|
|
1397
1688
|
if (this.transformed) this._chargeRows(dt);
|
|
1398
1689
|
}
|
|
@@ -1412,7 +1703,7 @@ export class Solver {
|
|
|
1412
1703
|
// charge in the Gauss row, written straight into the diagonal blocks by local index. A
|
|
1413
1704
|
// dielectric is the case with no species.
|
|
1414
1705
|
_nodesDilute(g0, g1, dt) {
|
|
1415
|
-
const { n, M, res, c, cOld, z, sys, loc, present, rhoFixed, blockOfNode } = this, R = this.rix, F = FARADAY;
|
|
1706
|
+
const { n, M, res, c, cOld, z, sys, loc, present, rhoFixed, blockOfNode, lin } = this, R = this.rix, F = FARADAY;
|
|
1416
1707
|
const JB = sys.B, sizes = sys.sizes, offB = sys.offB, vol = this.model.grid.vol;
|
|
1417
1708
|
for (let g = g0; g <= g1; g++) {
|
|
1418
1709
|
const b = blockOfNode[g], v = vol[g];
|
|
@@ -1423,22 +1714,27 @@ export class Solver {
|
|
|
1423
1714
|
continue;
|
|
1424
1715
|
}
|
|
1425
1716
|
let q = rhoFixed[g], dq = 0;
|
|
1717
|
+
const late = this.combining && this.lateStorage[g] === 1 && dt !== Infinity; // (the ions' storage: in _contact)
|
|
1426
1718
|
for (let i = 0; i < n; i++) {
|
|
1427
1719
|
const k = g * n + i;
|
|
1428
1720
|
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;
|
|
1721
|
+
const ck = c[k], l = loc[lb + 1 + i], stores = !(late && z[i] !== 0);
|
|
1722
|
+
if (stores) res[R[lb + 1 + i]] += (v * (ck - cOld[k])) / dt;
|
|
1432
1723
|
q += F * z[i] * ck;
|
|
1433
1724
|
dq += F * z[i] * z[i] * ck;
|
|
1434
|
-
if (
|
|
1435
|
-
|
|
1436
|
-
|
|
1725
|
+
if (lin) {
|
|
1726
|
+
lin.node(R[lb + 1 + i], R[lb], z[i], stores ? v * ck : 0, -v * F * z[i] * ck);
|
|
1727
|
+
continue;
|
|
1437
1728
|
}
|
|
1729
|
+
if (stores) {
|
|
1730
|
+
JB[oB + l * m + l] += (v * ck) / dt;
|
|
1731
|
+
if (p >= 0 && z[i] !== 0) JB[oB + l * m + p] += (-v * z[i] * ck) / dt;
|
|
1732
|
+
}
|
|
1733
|
+
if (p >= 0 && z[i] !== 0) JB[oB + p * m + l] += -v * F * z[i] * ck;
|
|
1438
1734
|
}
|
|
1439
1735
|
if (p >= 0) {
|
|
1440
1736
|
res[R[lb]] -= v * q;
|
|
1441
|
-
JB[oB + p * m + p] += v * dq;
|
|
1737
|
+
if (!lin) JB[oB + p * m + p] += v * dq;
|
|
1442
1738
|
}
|
|
1443
1739
|
}
|
|
1444
1740
|
}
|
|
@@ -1500,7 +1796,7 @@ export class Solver {
|
|
|
1500
1796
|
// A start-of-step net charge (round-off in a solved state) isn't carried over: the neutrality
|
|
1501
1797
|
// row holds the new state neutral.
|
|
1502
1798
|
_chargeRows(dt) {
|
|
1503
|
-
const { n, M, res, c, cOld, z, sys, loc, present, termB, termC } = this, R = this.rix, F = FARADAY;
|
|
1799
|
+
const { n, M, res, c, cOld, z, sys, loc, present, termB, termC } = this, R = this.rix, F = FARADAY, live = this._liveTerms();
|
|
1504
1800
|
const { A, B, C, sizes, offA, offB, offC } = sys, vol = this.model.grid.vol, last = this.nB - 1;
|
|
1505
1801
|
for (const g of this.chargeNodes) {
|
|
1506
1802
|
const b = this.blockOfNode[g], lb = b * M, m = sizes[b], mp = b > 0 ? sizes[b - 1] : 0, mn = b < last ? sizes[b + 1] : 0;
|
|
@@ -1524,7 +1820,8 @@ export class Solver {
|
|
|
1524
1820
|
for (let q = 0, o = offB[b] + lk * m, s = offB[b] + li * m; q < m; q++) B[o + q] += w * B[s + q];
|
|
1525
1821
|
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
1822
|
res[rk] += w * res[ri];
|
|
1527
|
-
for (
|
|
1823
|
+
for (const t of live) termB[t][rk] += w * termB[t][ri];
|
|
1824
|
+
if (this.lin) this.lin.combine(rk, ri, w);
|
|
1528
1825
|
}
|
|
1529
1826
|
// Columns: φ̂' = φ̂ with η fixed becomes φ̂ with η_i shifted by z_i, wherever node g's
|
|
1530
1827
|
// unknowns appear (its own rows, its neighbours', a terminal's current).
|
|
@@ -1534,7 +1831,7 @@ export class Solver {
|
|
|
1534
1831
|
for (let r = 0, o = offB[b]; r < m; r++) B[o + r * m + p] += zi * B[o + r * m + li];
|
|
1535
1832
|
if (b > 0) for (let r = 0, mr = mp, o = offC[b - 1]; r < mr; r++) C[o + r * m + p] += zi * C[o + r * m + li];
|
|
1536
1833
|
if (b < last) for (let r = 0, mr = mn, o = offA[b + 1]; r < mr; r++) A[o + r * m + p] += zi * A[o + r * m + li];
|
|
1537
|
-
for (
|
|
1834
|
+
for (const t of live) termC[t][R[lb]] += zi * termC[t][R[lb + 1 + i]];
|
|
1538
1835
|
}
|
|
1539
1836
|
// Storage on the other charged balances, and neutrality: no φ̂' terms, exactly.
|
|
1540
1837
|
const v = vol[g], oB = offB[b];
|
|
@@ -1651,6 +1948,7 @@ export class Solver {
|
|
|
1651
1948
|
const mL = sys.sizes[bL], mR = sys.sizes[bR], pL = loc[bL * M], pR = loc[bR * M];
|
|
1652
1949
|
res[R[bL * M]] += D;
|
|
1653
1950
|
res[R[bR * M]] -= D;
|
|
1951
|
+
if (this.lin) return this.lin.displacement(R[bL * M], R[bR * M], k);
|
|
1654
1952
|
sys.B[sys.offB[bL] + pL * mL + pL] += k;
|
|
1655
1953
|
sys.C[sys.offC[bL] + pL * mR + pR] -= k;
|
|
1656
1954
|
sys.A[sys.offA[bR] + pR * mL + pL] -= k;
|
|
@@ -1664,7 +1962,7 @@ export class Solver {
|
|
|
1664
1962
|
// fluxes (e.g. majority carriers carrying a small current) don't vanish in the cancellation of
|
|
1665
1963
|
// two huge drift and diffusion terms.
|
|
1666
1964
|
_segmentsDilute(g0, g1, region, mat, vel) {
|
|
1667
|
-
const { n, M, u, uLo, res, c, z, sys, loc } = this, R = this.rix
|
|
1965
|
+
const { n, M, u, uLo, res, c, z, sys, loc, lin } = this, R = this.rix;
|
|
1668
1966
|
const { A: JA, B: JB, C: JC, sizes, offA, offB, offC } = sys;
|
|
1669
1967
|
const { segLength, segArea } = this.model.grid;
|
|
1670
1968
|
for (let s = g0; s < g1; s++) {
|
|
@@ -1689,6 +1987,10 @@ export class Solver {
|
|
|
1689
1987
|
const dNdPhiR = zi * dNdd;
|
|
1690
1988
|
res[R[bL * M + r]] += N;
|
|
1691
1989
|
res[R[bR * M + r]] -= N;
|
|
1990
|
+
if (lin) {
|
|
1991
|
+
lin.segment(R[bL * M + r], R[bR * M + r], R[bL * M], R[bR * M], zi, -gBc * E, dNdEtaR, zi * dNdd);
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1692
1994
|
// Row r of block bL (couplings to itself in B, to bR in C) and of bR (to bL in A).
|
|
1693
1995
|
const lL = loc[bL * M + r], lR = loc[bR * M + r];
|
|
1694
1996
|
const oBL = offB[bL] + lL * mL, oCL = offC[bL] + lL * mR, oAR = offA[bR] + lR * mL, oBR = offB[bR] + lR * mR;
|
|
@@ -1778,14 +2080,21 @@ export class Solver {
|
|
|
1778
2080
|
// materials). Between like solutions it's Goldman–Hodgkin–Katz's flux,
|
|
1779
2081
|
// N = P·zu·(c_L − c_R e^{zu})/(e^{zu} − 1), u = (φ_R − φ_L)/V_T,
|
|
1780
2082
|
// and it's exactly zero where μ̄ is level.
|
|
1781
|
-
const gl = grid.regionEnd[f], gr = gl + 1, kl = gl * n + i, kr = gr * n + i, zi = z[i],
|
|
2083
|
+
const gl = grid.regionEnd[f], gr = gl + 1, kl = gl * n + i, kr = gr * n + i, zi = z[i], link = itf.links[i];
|
|
1782
2084
|
const shift = this.mu0hat[kr] - this.mu0hat[kl] - Math.log(this.cRef[kr] / this.cRef[kl]);
|
|
1783
2085
|
const d = zi * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M])) + shift;
|
|
1784
2086
|
const E = Math.expm1(-deta); // η_R − η_L
|
|
2087
|
+
// Gated: P × Π x^p, each x taken in [0, 1] (Newton's iterates can stray past its ends).
|
|
2088
|
+
const gated = link.gates ?? [], gx = (q) => u[this._gateSlot(f, q)];
|
|
2089
|
+
const P = link.P * (gated.length ? this._open(gated, gx) : 1);
|
|
1785
2090
|
const gBc = P * bernoulli(d) * this.c[kl];
|
|
1786
2091
|
const dNdd = -P * bernoulliDerivative(d) * this.c[kl] * E;
|
|
1787
2092
|
res[R[o]] = u[o] + gBc * E;
|
|
1788
2093
|
this._j(bf, r, bf, r, 1);
|
|
2094
|
+
for (const [q, p] of gated) {
|
|
2095
|
+
const dO = this._dOpen(gated, q, p, gx);
|
|
2096
|
+
if (dO !== 0) this._j(bf, r, bf, 1 + n + itf.reactions.length + q, link.P * bernoulli(d) * this.c[kl] * E * dO);
|
|
2097
|
+
}
|
|
1789
2098
|
this._j(bf, r, bL, r, -gBc);
|
|
1790
2099
|
this._j(bf, r, bR, r, gBc * (E + 1));
|
|
1791
2100
|
if (zi !== 0) {
|
|
@@ -1793,12 +2102,18 @@ export class Solver {
|
|
|
1793
2102
|
this._j(bf, r, bR, 0, -zi * dNdd);
|
|
1794
2103
|
}
|
|
1795
2104
|
} else {
|
|
1796
|
-
// conductance: J = G (V_L − V_R), V = V_T η / z ⇒ N = G V_T (η_L − η_R) / (z² F)
|
|
1797
|
-
|
|
2105
|
+
// conductance: J = G (V_L − V_R), V = V_T η / z ⇒ N = G V_T (η_L − η_R) / (z² F); gated,
|
|
2106
|
+
// G times Π x^p
|
|
2107
|
+
const gated = itf.links[i].gates ?? [], gx = (q) => u[this._gateSlot(f, q)];
|
|
2108
|
+
const gG0 = (itf.links[i].G * VT) / (z[i] * z[i] * F), gG = gG0 * (gated.length ? this._open(gated, gx) : 1);
|
|
1798
2109
|
res[R[o]] = u[o] - gG * deta;
|
|
1799
2110
|
this._j(bf, r, bf, r, 1);
|
|
1800
2111
|
this._j(bf, r, bL, r, -gG);
|
|
1801
2112
|
this._j(bf, r, bR, r, gG);
|
|
2113
|
+
for (const [q, p] of gated) {
|
|
2114
|
+
const dO = this._dOpen(gated, q, p, gx);
|
|
2115
|
+
if (dO !== 0) this._j(bf, r, bf, 1 + n + itf.reactions.length + q, -gG0 * deta * dO);
|
|
2116
|
+
}
|
|
1802
2117
|
}
|
|
1803
2118
|
res[R[bL * M + r]] += Af * u[o];
|
|
1804
2119
|
this._j(bL, r, bf, r, Af);
|
|
@@ -1806,6 +2121,46 @@ export class Solver {
|
|
|
1806
2121
|
this._j(bR, r, bf, r, -Af);
|
|
1807
2122
|
}
|
|
1808
2123
|
itf.reactions.forEach((rx, k) => this._faceReaction(rx, k, f, bf, bL, bR, Af));
|
|
2124
|
+
if (itf.gates.length > 0) this._faceGates(f, bf, bL, bR);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// Full index of face f's gate q (its fraction open).
|
|
2128
|
+
_gateSlot(f, q) {
|
|
2129
|
+
return this.blockOfFace[f] * this.M + 1 + this.n + this.model.interfaces[f].reactions.length + q;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// The voltage a gate follows: across its face, or across its port's capacitance at its node.
|
|
2133
|
+
_gateVoltage(gt) {
|
|
2134
|
+
return gt.port === undefined ? this._faceVoltage(gt.f) : this._portVoltage(gt.port, gt.g);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// The voltage across port k's capacitance at node g, φ − (V − zeroCharge) (V): a membrane's,
|
|
2138
|
+
// inside (the region) minus outside.
|
|
2139
|
+
_portVoltage(k, g) {
|
|
2140
|
+
const { M, u, uLo, VT } = this, b = this.blockOfNode[g], port = this.model.ports[k];
|
|
2141
|
+
return VT * (u[b * M] + uLo[b * M]) - (this.termV[2 + k] - port.capacitance.zeroCharge);
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// The voltage across face f, φ_right − φ_left (V).
|
|
2145
|
+
_faceVoltage(f) {
|
|
2146
|
+
const { M, u, uLo, VT } = this, bf = this.blockOfFace[f], bL = bf - 1, bR = bf + 1;
|
|
2147
|
+
return VT * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M]));
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// Face f's gates, each a row (x − x_old)/dt = α(1 − x) − βx in the voltage across the face.
|
|
2151
|
+
_faceGates(f, bf, bL, bR) {
|
|
2152
|
+
const { res, VT } = this, R = this.rix, itf = this.model.interfaces[f], dt = this.dtNow;
|
|
2153
|
+
const V = this._faceVoltage(f), k0 = 1 + this.n + itf.reactions.length;
|
|
2154
|
+
itf.gates.forEach((gate, q) => {
|
|
2155
|
+
const o = this._gateSlot(f, q), x = this.u[o];
|
|
2156
|
+
const [a, da] = gateRate(gate.alpha, V), [b, db] = gateRate(gate.beta, V);
|
|
2157
|
+
const store = Number.isFinite(dt) ? 1 / dt : 0, j = this.gateIndex[f] + q;
|
|
2158
|
+
res[R[o]] = store * (x - this.gateOld[j]) - (a * (1 - x) - b * x);
|
|
2159
|
+
this._j(bf, k0 + q, bf, k0 + q, store + a + b);
|
|
2160
|
+
const dV = -(da * (1 - x) - db * x) * VT; // per unit φ̂_R, and minus that per unit φ̂_L
|
|
2161
|
+
this._j(bf, k0 + q, bR, 0, dV);
|
|
2162
|
+
this._j(bf, k0 + q, bL, 0, -dV);
|
|
2163
|
+
});
|
|
1809
2164
|
}
|
|
1810
2165
|
|
|
1811
2166
|
// Scharfetter–Gummel with non-ideal statistics. The excess ex = ζ − ln(c/c_ref) acts as an
|
|
@@ -2087,16 +2442,26 @@ export class Solver {
|
|
|
2087
2442
|
}
|
|
2088
2443
|
}
|
|
2089
2444
|
|
|
2445
|
+
// The terminals whose ∂res/∂V (termB) and ∂I/∂x (termC) are kept, exactly: the floating ones,
|
|
2446
|
+
// which the Newton solve borders on, or all of them (allTerminals: for the impedance, which reads
|
|
2447
|
+
// its terminal's). A held terminal's are otherwise left as they fall (written but not kept
|
|
2448
|
+
// consistent), which spares a device with dozens of ports (an axon's nodes of Ranvier) the cost
|
|
2449
|
+
// of carrying each one's two dense vectors through every assembly.
|
|
2450
|
+
_liveTerms() {
|
|
2451
|
+
if (this.allTerminals) return this._allTerms ?? (this._allTerms = this.terms.map((_, k) => k));
|
|
2452
|
+
return this.floating;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2090
2455
|
// Every terminal's terms: its voltage held or floating, how the residual depends on it
|
|
2091
2456
|
// (termB), its current into the device (termI) and how that depends on the state (termC).
|
|
2092
2457
|
_terminals(dt) {
|
|
2093
2458
|
this._refreshSources();
|
|
2094
|
-
for (
|
|
2459
|
+
for (const k of this._liveTerms()) {
|
|
2095
2460
|
this.termB[k].fill(0);
|
|
2096
2461
|
this.termC[k].fill(0);
|
|
2097
|
-
this.termDI[k] = 0;
|
|
2098
|
-
this.termI[k] = 0;
|
|
2099
2462
|
}
|
|
2463
|
+
this.termDI.fill(0);
|
|
2464
|
+
this.termI.fill(0);
|
|
2100
2465
|
this.qg.fill(0);
|
|
2101
2466
|
this.qgSlope.fill(0);
|
|
2102
2467
|
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt, false));
|
|
@@ -2121,6 +2486,7 @@ export class Solver {
|
|
|
2121
2486
|
_captureRow(b, s, w, into) {
|
|
2122
2487
|
const l = this.loc[b * this.M + s];
|
|
2123
2488
|
if (l < 0 || w === 0) return;
|
|
2489
|
+
if (this.lin) this.lin.capture(this.rix[b * this.M + s], w, this.lin.rate, into);
|
|
2124
2490
|
const { sys, nB } = this, { sizes, offA, offB, offC, offX } = sys, m = sizes[b];
|
|
2125
2491
|
for (const [X, nb, off] of [['A', b - 1, offA], ['B', b, offB], ['C', b + 1, offC]]) {
|
|
2126
2492
|
if (nb < 0 || nb >= nB) continue;
|
|
@@ -2158,8 +2524,11 @@ export class Solver {
|
|
|
2158
2524
|
B[R[o]] += -z[i] / VT;
|
|
2159
2525
|
continue;
|
|
2160
2526
|
}
|
|
2161
|
-
// conductance: s = G V_T (η_out − η)/(z² F); exchange: s = k (η_out − η)
|
|
2162
|
-
|
|
2527
|
+
// conductance: s = G V_T (η_out − η)/(z² F); exchange: s = k (η_out − η). Gated, G times
|
|
2528
|
+
// Π x^p, the gates at this node (each taken in [0, 1]).
|
|
2529
|
+
const kk0 = link.type === 'conductance' ? (link.G * VT) / (z[i] * z[i] * F) : link.k;
|
|
2530
|
+
const gated = link.gates ?? [], q0 = 1 + n + port.surface.length;
|
|
2531
|
+
const open = gated.length ? this._open(gated, (q) => u[b * M + q0 + q]) : 1, kk = kk0 * open;
|
|
2163
2532
|
res[R[o]] -= v * kk * deta;
|
|
2164
2533
|
this._j(b, r, b, r, v * kk);
|
|
2165
2534
|
flux[i] += v * kk * deta;
|
|
@@ -2168,6 +2537,12 @@ export class Solver {
|
|
|
2168
2537
|
C[R[o]] += -zF * v * kk;
|
|
2169
2538
|
this.termDI[k] += (zF * v * kk * z[i]) / VT;
|
|
2170
2539
|
}
|
|
2540
|
+
for (const [q, p] of gated) {
|
|
2541
|
+
const d = this._dOpen(gated, q, p, (qq) => u[b * M + q0 + qq]);
|
|
2542
|
+
if (d === 0) continue;
|
|
2543
|
+
this._j(b, r, b, q0 + q, -v * kk0 * deta * d);
|
|
2544
|
+
if (z[i] !== 0) C[R[b * M + q0 + q]] += zF * v * kk0 * deta * d;
|
|
2545
|
+
}
|
|
2171
2546
|
}
|
|
2172
2547
|
}
|
|
2173
2548
|
if (holds) {
|
|
@@ -2176,6 +2551,7 @@ export class Solver {
|
|
|
2176
2551
|
}
|
|
2177
2552
|
port.reactions.forEach((rx, x) => this._portReaction(port, rx, this.portArea[k - 2], this.portRates[k - 2][x], flux, k));
|
|
2178
2553
|
if (port.capacitance) this._portCapacitance(port, k, dt);
|
|
2554
|
+
if (port.gates.length > 0) this._portGates(port, k, dt);
|
|
2179
2555
|
if (port.surface.length > 0 && Number.isFinite(dt)) this._surfaceStorage(port, dt);
|
|
2180
2556
|
}
|
|
2181
2557
|
|
|
@@ -2244,6 +2620,38 @@ export class Solver {
|
|
|
2244
2620
|
// on the port's side, so each node's charge balance (its φ row) gains vol·a·σ, and the port
|
|
2245
2621
|
// passes the charging current d(Σ vol·a·σ)/dt (none in a steady state). An end node whose φ its
|
|
2246
2622
|
// contact sets is left to the contact.
|
|
2623
|
+
// A membrane port's gates, at each node of its window: (x − x_old)/dt = α(1 − x) − βx in the
|
|
2624
|
+
// voltage across its capacitance there, φ − (V − zeroCharge).
|
|
2625
|
+
_portGates(port, k, dt) {
|
|
2626
|
+
const { n, res, VT } = this, R = this.rix, B = this.termB[k], store = Number.isFinite(dt) ? 1 / dt : 0, q0 = 1 + n + port.surface.length;
|
|
2627
|
+
port.nodes.forEach((g, w) => {
|
|
2628
|
+
const b = this.blockOfNode[g], V = this._portVoltage(k - 2, g);
|
|
2629
|
+
port.gates.forEach((gate, q) => {
|
|
2630
|
+
const o = b * this.M + q0 + q, x = this.u[o], j = this.portGateIndex[k - 2] + w * port.gates.length + q;
|
|
2631
|
+
const [a, da] = gateRate(gate.alpha, V), [bb, db] = gateRate(gate.beta, V);
|
|
2632
|
+
res[R[o]] = store * (x - this.gateOld[j]) - (a * (1 - x) - bb * x);
|
|
2633
|
+
this._j(b, q0 + q, b, q0 + q, store + a + bb);
|
|
2634
|
+
const s = -(da * (1 - x) - db * x); // ∂/∂V_m, with V_m = V_T φ̂ − V + zeroCharge
|
|
2635
|
+
this._j(b, q0 + q, b, 0, s * VT);
|
|
2636
|
+
B[R[o]] += -s;
|
|
2637
|
+
});
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
// Π x^p over a link's gates (each x taken in [0, 1]), and its derivative in gate q.
|
|
2642
|
+
_open(gated, x) {
|
|
2643
|
+
let open = 1;
|
|
2644
|
+
for (const [q, p] of gated) open *= powi(Math.min(1, Math.max(0, x(q))), p);
|
|
2645
|
+
return open;
|
|
2646
|
+
}
|
|
2647
|
+
_dOpen(gated, q, p, x) {
|
|
2648
|
+
const xq = x(q);
|
|
2649
|
+
if (!(xq > 0 && xq < 1)) return 0;
|
|
2650
|
+
let others = 1;
|
|
2651
|
+
for (const [q2, p2] of gated) if (q2 !== q) others *= powi(Math.min(1, Math.max(0, x(q2))), p2);
|
|
2652
|
+
return p * powi(xq, p - 1) * others;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2247
2655
|
_portCapacitance(port, k, dt) {
|
|
2248
2656
|
const { M, u, uLo, res, VT } = this, R = this.rix, vol = this.model.grid.vol, area = this.portArea[k - 2];
|
|
2249
2657
|
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
@@ -2323,6 +2731,19 @@ export class Solver {
|
|
|
2323
2731
|
this._captureRow(b, 1 + i, z[i] * F, C);
|
|
2324
2732
|
}
|
|
2325
2733
|
}
|
|
2734
|
+
// The ions' storage, left out until now (see lateStorage): in each balance, and in what
|
|
2735
|
+
// each species brings in, but not in the current.
|
|
2736
|
+
if (this.combining && dyn && this.lateStorage[g] === 1) {
|
|
2737
|
+
const v = model.grid.vol[g], { cOld } = this;
|
|
2738
|
+
for (let i = 0; i < n; i++) {
|
|
2739
|
+
if (z[i] === 0 || this.loc[b * M + 1 + i] < 0) continue;
|
|
2740
|
+
const ci = c[g * n + i], st = (v * (ci - cOld[g * n + i])) / dt;
|
|
2741
|
+
res[R[b * M + 1 + i]] += st;
|
|
2742
|
+
flux[i] += sgn * st;
|
|
2743
|
+
this._j(b, 1 + i, b, 1 + i, (v * ci) / dt);
|
|
2744
|
+
this._j(b, 1 + i, b, 0, (-v * z[i] * ci) / dt);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2326
2747
|
|
|
2327
2748
|
// Conductance links: J (toward the device) = G (V_out − V_i), V_out = V_t + offset; exchange
|
|
2328
2749
|
// links (neutral species): N_in = k (μ_out − μ)/RT.
|
|
@@ -2372,7 +2793,7 @@ export class Solver {
|
|
|
2372
2793
|
// The Poisson residual is the outside's charge, D_in.
|
|
2373
2794
|
const Din = res[R[b * M]];
|
|
2374
2795
|
this.contactD[side] = sgn * Din;
|
|
2375
|
-
if (dyn) {
|
|
2796
|
+
if (dyn && !(this.combining && this.lateStorage[g] === 1)) {
|
|
2376
2797
|
I += (Din - Dstart) / dt;
|
|
2377
2798
|
this._captureRow(b, 0, 1 / dt, C);
|
|
2378
2799
|
}
|
|
@@ -2405,10 +2826,59 @@ export class Solver {
|
|
|
2405
2826
|
|
|
2406
2827
|
|
|
2407
2828
|
|
|
2829
|
+
// Steady solves: each flat stretch's level held at its contact's throughout (see
|
|
2830
|
+
// flatStretches), in place of its balances, as an equilibrium link holds it at the end node.
|
|
2831
|
+
_flatRows() {
|
|
2832
|
+
const { M, u, uLo, res, z, VT, model } = this, R = this.rix;
|
|
2833
|
+
for (const st of this.flatStretches) {
|
|
2834
|
+
const i = st.species, side = st.leftOpen ? 'left' : 'right', k = side === 'left' ? 0 : 1;
|
|
2835
|
+
const link = model.contacts[side].species[i];
|
|
2836
|
+
const level = z[i] === 0 ? link.mu / model.RT : (z[i] * (this.termV[k] + link.offset)) / VT;
|
|
2837
|
+
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
2838
|
+
const b = this.blockOfNode[g], o = b * M + 1 + i;
|
|
2839
|
+
if (this.loc[o] < 0) continue;
|
|
2840
|
+
this._replaceRow(b, 1 + i);
|
|
2841
|
+
for (const B of this.termB) B[R[o]] = 0; // (the row's old terminal terms: the end node's own link)
|
|
2842
|
+
this._j(b, 1 + i, b, 1 + i, 1);
|
|
2843
|
+
res[R[o]] = u[o] + uLo[o] - level;
|
|
2844
|
+
if (z[i] !== 0) this.termB[k][R[o]] += -z[i] / VT;
|
|
2845
|
+
}
|
|
2846
|
+
// A face inside the stretch whose link holds the level continuous carries the species' flux
|
|
2847
|
+
// as an unknown that only the edge balances, now replaced, determined: it's zero.
|
|
2848
|
+
for (let f = st.regions[0]; f < st.regions[1]; f++) {
|
|
2849
|
+
if (model.interfaces[f].links[i].type !== 'equilibrium') continue;
|
|
2850
|
+
const b = this.blockOfFace[f], o = b * M + 1 + i;
|
|
2851
|
+
if (this.loc[o] < 0) continue;
|
|
2852
|
+
this._replaceRow(b, 1 + i);
|
|
2853
|
+
for (const B of this.termB) B[R[o]] = 0;
|
|
2854
|
+
this._j(b, 1 + i, b, 1 + i, 1);
|
|
2855
|
+
res[R[o]] = u[o] + uLo[o];
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2408
2860
|
// Zero one row of the Jacobian (all three blocks), ready to be replaced.
|
|
2861
|
+
// Steady solves: each level stretch's levels held equal (see levelStretches), node to node in
|
|
2862
|
+
// place of the balances. The first node's row is the amount's (see _applyConstraints).
|
|
2863
|
+
_levelRows() {
|
|
2864
|
+
const { M, u, uLo, res } = this, R = this.rix;
|
|
2865
|
+
for (const st of this.levelStretches) {
|
|
2866
|
+
const i = st.species;
|
|
2867
|
+
for (let g = st.nodes[0] + 1; g <= st.nodes[1]; g++) {
|
|
2868
|
+
const b = this.blockOfNode[g], o = b * M + 1 + i, p = o - M;
|
|
2869
|
+
if (this.loc[o] < 0 || this.loc[p] < 0) continue;
|
|
2870
|
+
this._replaceRow(b, 1 + i);
|
|
2871
|
+
for (const B of this.termB) B[R[o]] = 0;
|
|
2872
|
+
this._j(b, 1 + i, b, 1 + i, 1);
|
|
2873
|
+
this._j(b, 1 + i, b - 1, 1 + i, -1);
|
|
2874
|
+
res[R[o]] = u[o] + uLo[o] - (u[p] + uLo[p]);
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2409
2878
|
_replaceRow(b, rs) {
|
|
2410
2879
|
const l = this.loc[b * this.M + rs];
|
|
2411
2880
|
if (l < 0) return;
|
|
2881
|
+
if (this.lin) this.lin.kill(this.rix[b * this.M + rs]);
|
|
2412
2882
|
const { sys, nB } = this, sz = sys.sizes, m = sz[b];
|
|
2413
2883
|
sys.B.fill(0, sys.offB[b] + l * m, sys.offB[b] + (l + 1) * m);
|
|
2414
2884
|
if (b > 0) sys.A.fill(0, sys.offA[b] + l * sz[b - 1], sys.offA[b] + (l + 1) * sz[b - 1]);
|
|
@@ -2417,7 +2887,7 @@ export class Solver {
|
|
|
2417
2887
|
|
|
2418
2888
|
// Scale every row by its largest Jacobian entry (in place, residual too).
|
|
2419
2889
|
_equilibrate() {
|
|
2420
|
-
const { nB, sys, res, termB } = this;
|
|
2890
|
+
const { nB, sys, res, termB } = this, live = this._liveTerms();
|
|
2421
2891
|
const { A, B, C, sizes, offA, offB, offC, offX } = sys;
|
|
2422
2892
|
for (let b = 0; b < nB; b++) {
|
|
2423
2893
|
const m = sizes[b], mp = b > 0 ? sizes[b - 1] : 0, mn = b < nB - 1 ? sizes[b + 1] : 0;
|
|
@@ -2427,14 +2897,123 @@ export class Solver {
|
|
|
2427
2897
|
for (let k = 0; k < mp; k++) mx = Math.max(mx, Math.abs(A[oa + k]));
|
|
2428
2898
|
for (let k = 0; k < m; k++) mx = Math.max(mx, Math.abs(B[ob + k]));
|
|
2429
2899
|
for (let k = 0; k < mn; k++) mx = Math.max(mx, Math.abs(C[oc + k]));
|
|
2430
|
-
if (mx === 0)
|
|
2900
|
+
if (mx === 0) {
|
|
2901
|
+
this.rowScale[offX[b] + r] = 1;
|
|
2902
|
+
continue;
|
|
2903
|
+
}
|
|
2431
2904
|
const s = 1 / mx;
|
|
2905
|
+
this.rowScale[offX[b] + r] = s;
|
|
2432
2906
|
for (let k = 0; k < mp; k++) A[oa + k] *= s;
|
|
2433
2907
|
for (let k = 0; k < m; k++) B[ob + k] *= s;
|
|
2434
2908
|
for (let k = 0; k < mn; k++) C[oc + k] *= s;
|
|
2435
2909
|
res[offX[b] + r] *= s;
|
|
2436
|
-
for (
|
|
2910
|
+
for (const k of live) termB[k][offX[b] + r] *= s; // (each terminal's ∂/∂V)
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
// The Jacobian at dt with the dilute kernels' terms kept aside in difference form (see
|
|
2916
|
+
// DifferenceTerms), the rest assembled into a matrix of its own. The residual and terminal terms
|
|
2917
|
+
// come out as from assemble(dt); the Newton matrix is left as it was.
|
|
2918
|
+
_assembleDifference(dt) {
|
|
2919
|
+
const keep = this.sys;
|
|
2920
|
+
const rest = this.sysRest ?? (this.sysRest = new BlockTridiagonal(keep.n, keep.sizes));
|
|
2921
|
+
const lin = this.diffTerms ?? (this.diffTerms = new DifferenceTerms(keep.size, this.nNodes * this.n, this.nNodes * this.n));
|
|
2922
|
+
this.sys = rest;
|
|
2923
|
+
this.lin = lin;
|
|
2924
|
+
try {
|
|
2925
|
+
this.assemble(dt);
|
|
2926
|
+
} finally {
|
|
2927
|
+
this.sys = keep;
|
|
2928
|
+
this.lin = null;
|
|
2929
|
+
}
|
|
2930
|
+
return { rest, lin };
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// A Newton solve refined by GMRES: the factorised Jacobian as the preconditioner, and J·x
|
|
2934
|
+
// exact, with the dilute kernels' terms in difference form (see DifferenceTerms), as the
|
|
2935
|
+
// impedance does. The assembled J holds a flux's dependence on η at its two ends as two
|
|
2936
|
+
// entries; where they're huge (an inversion layer's 0.1 nm cells, a conductance ~1e11 against
|
|
2937
|
+
// the layer's own storage and the trickle from the bulk), eliminating them loses the layer's
|
|
2938
|
+
// overall level to round-off, and Newton rattles there. delta and deltaV come in as the plain
|
|
2939
|
+
// solve's and leave refined (in the transformed unknowns, as _solveBordered gives them); false
|
|
2940
|
+
// if refining didn't help, leaving them as they came.
|
|
2941
|
+
_refine(dt, delta, deltaV) {
|
|
2942
|
+
const { res, termRes, termB, termC, termDI, rowScale, floating: fl } = this;
|
|
2943
|
+
const N = this.sys.size, K = fl.length, pins = this.pins, R = this.rix;
|
|
2944
|
+
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) };
|
|
2945
|
+
const restoreArrays = () => {
|
|
2946
|
+
res.set(save.res);
|
|
2947
|
+
termRes.set(save.termRes);
|
|
2948
|
+
save.termB.forEach((b, k) => termB[k].set(b));
|
|
2949
|
+
save.termC.forEach((c, k) => termC[k].set(c));
|
|
2950
|
+
termDI.set(save.termDI);
|
|
2951
|
+
this.transformed = save.transformed;
|
|
2952
|
+
this.pins.forEach((p, a) => (p.res = save.pinRes[a]));
|
|
2953
|
+
};
|
|
2954
|
+
// The same system, its terms kept apart: the rest, and each floating terminal's (unscaled).
|
|
2955
|
+
const { rest, lin } = this._assembleDifference(dt);
|
|
2956
|
+
const Bk = fl.map((k) => Float64Array.from(termB[k])), Ck = fl.map((k) => Float64Array.from(termC[k])), Dk = fl.map((k) => termDI[k]);
|
|
2957
|
+
restoreArrays();
|
|
2958
|
+
const v = new Float64Array(N + 1), jv = new Float64Array(N);
|
|
2959
|
+
const op = (x) => {
|
|
2960
|
+
for (let j = 0; j < N; j++) v[j] = x[j];
|
|
2961
|
+
v[N] = 0;
|
|
2962
|
+
rest.multiply(v, jv);
|
|
2963
|
+
if (save.transformed) this._untransform(v); // (the dilute terms in the plain unknowns)
|
|
2964
|
+
lin.apply(v, jv, 1, 1 / dt);
|
|
2965
|
+
const out = new Float64Array(N + K);
|
|
2966
|
+
for (let j = 0; j < N; j++) {
|
|
2967
|
+
let t = jv[j];
|
|
2968
|
+
for (let a = 0; a < K; a++) t += Bk[a][j] * x[N + a];
|
|
2969
|
+
out[j] = t * rowScale[j];
|
|
2970
|
+
}
|
|
2971
|
+
for (let a = 0; a < K; a++) {
|
|
2972
|
+
let t = Dk[a] * x[N + a];
|
|
2973
|
+
for (let j = 0; j < N; j++) t += Ck[a][j] * x[j];
|
|
2974
|
+
out[N + a] = t;
|
|
2975
|
+
}
|
|
2976
|
+
// A pinned row is its constraint's (a conserved amount, an island's summed balance).
|
|
2977
|
+
for (const p of pins) {
|
|
2978
|
+
let t = 0;
|
|
2979
|
+
for (let j = 0; j < p.len; j++) t += p.w[j] * x[p.idx[j]];
|
|
2980
|
+
out[R[p.row]] = t;
|
|
2437
2981
|
}
|
|
2982
|
+
return precondition(out);
|
|
2983
|
+
};
|
|
2984
|
+
const rhs = new Float64Array(N + 1), out = new Float64Array(N + 1), outV = new Float64Array(K);
|
|
2985
|
+
const precondition = (r) => {
|
|
2986
|
+
for (let j = 0; j < N; j++) rhs[j] = r[j];
|
|
2987
|
+
fl.forEach((k, a) => (termRes[k] = r[N + a]));
|
|
2988
|
+
for (const p of pins) {
|
|
2989
|
+
p.res = rhs[R[p.row]];
|
|
2990
|
+
rhs[R[p.row]] = 0;
|
|
2991
|
+
}
|
|
2992
|
+
this._solveBordered(rhs, out, outV, pins);
|
|
2993
|
+
const z = new Float64Array(N + K);
|
|
2994
|
+
for (let j = 0; j < N; j++) z[j] = out[j];
|
|
2995
|
+
for (let a = 0; a < K; a++) z[N + a] = outV[a];
|
|
2996
|
+
return z;
|
|
2997
|
+
};
|
|
2998
|
+
try {
|
|
2999
|
+
const b = new Float64Array(N + K);
|
|
3000
|
+
for (let j = 0; j < N; j++) b[j] = save.res[j];
|
|
3001
|
+
fl.forEach((k, a) => (b[N + a] = save.termRes[k]));
|
|
3002
|
+
pins.forEach((p, a) => (b[R[p.row]] = save.pinRes[a]));
|
|
3003
|
+
const pb = precondition(b), x = Float64Array.from(pb);
|
|
3004
|
+
// A lost mode or two, GMRES finds in a couple of iterations. The refined update is kept
|
|
3005
|
+
// where GMRES converged and its correction is under a thermal unit; otherwise newton()
|
|
3006
|
+
// stops refining.
|
|
3007
|
+
const x0 = Float64Array.from(x);
|
|
3008
|
+
const ok = gmresReal(op, pb, x, { m: 8, restarts: 1, tol: 1e-8 });
|
|
3009
|
+
let c = 0;
|
|
3010
|
+
for (let j = 0; j < N + K; j++) c = Math.max(c, Math.abs(x[j] - x0[j]));
|
|
3011
|
+
if (!ok || !(c < 1)) return false;
|
|
3012
|
+
for (let j = 0; j < N; j++) delta[j] = x[j];
|
|
3013
|
+
for (let a = 0; a < K; a++) deltaV[a] = x[N + a];
|
|
3014
|
+
return true;
|
|
3015
|
+
} finally {
|
|
3016
|
+
restoreArrays();
|
|
2438
3017
|
}
|
|
2439
3018
|
}
|
|
2440
3019
|
|
|
@@ -2453,6 +3032,7 @@ export class Solver {
|
|
|
2453
3032
|
const k = this.surfPort[g]; // an electrode surface's coverages, as η
|
|
2454
3033
|
if (k >= 0) for (let q = 0; q < this.model.ports[k].surface.length; q++) mx = Math.max(mx, Math.abs(delta[R[b * M + 1 + n + q]]));
|
|
2455
3034
|
}
|
|
3035
|
+
for (const gt of this.gateList) mx = Math.max(mx, Math.abs(delta[R[gt.o]])); // a gate's fraction open
|
|
2456
3036
|
return mx;
|
|
2457
3037
|
}
|
|
2458
3038
|
|
|
@@ -2460,16 +3040,55 @@ export class Solver {
|
|
|
2460
3040
|
* Newton iteration for one backward-Euler step from the current cOld.
|
|
2461
3041
|
* @returns {{converged: boolean, iterations: number, history: number[]}}
|
|
2462
3042
|
*/
|
|
2463
|
-
newton(dt, { maxIter = 60, tol = 1e-10, maxStep = 10 } = {}) {
|
|
3043
|
+
newton(dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands = false } = {}) {
|
|
2464
3044
|
const { delta, res } = this;
|
|
2465
3045
|
const deltaV = this.deltaV ?? (this.deltaV = new Float64Array(this.floating.length));
|
|
2466
3046
|
const history = [], ownHistory = [];
|
|
2467
|
-
//
|
|
3047
|
+
// Refined solves (see _refine), once the plain ones stall.
|
|
3048
|
+
let refine = false, refined = 0, noRefine = false;
|
|
3049
|
+
// Islands (see _applyIslands): their summed rows go in only once the plain solves converge.
|
|
3050
|
+
// Far from the solution, Newton exact along a weakly held level takes it by 1e5 thermal
|
|
3051
|
+
// units (a floating base, cold at bias), where the plain solves, which barely see that level,
|
|
3052
|
+
// converge. Converged, an island at the wrong level shows: it takes in a different current
|
|
3053
|
+
// than it passes on, so the terminal currents don't add up to zero. Then the summed rows go
|
|
3054
|
+
// in and Newton carries on from there. Converged again, a current that tiny against the
|
|
3055
|
+
// conduction around it rides on differences (~1e-28 thermal units for 5 fA/m² through a bulk
|
|
3056
|
+
// of 1e4 mol/m³ at 1e-4 m²/s) that the last update leaves known only to its own round-off:
|
|
3057
|
+
// where the currents still don't add up, one more update polishes them. Where the islands'
|
|
3058
|
+
// own system is held by static pivots elsewhere (a GaAs stack's minority carriers, at 1e-17
|
|
3059
|
+
// A/m² of noise), their rows can throw Newton off instead: if it doesn't converge again
|
|
3060
|
+
// within a dozen iterations, the plain solution stands.
|
|
3061
|
+
this.islandsOn = islands;
|
|
3062
|
+
let rounds = 0, kept = null;
|
|
3063
|
+
const islandsOff = (it, result) => {
|
|
3064
|
+
if (dt !== Infinity || !this.constrained || this.islands.length === 0 || rounds >= 2 || !(this._kirchhoff() > 1e-9)) return false;
|
|
3065
|
+
this.islandsOn = true;
|
|
3066
|
+
rounds++;
|
|
3067
|
+
kept = { it, result, u: Float64Array.from(this.u), uLo: Float64Array.from(this.uLo), termV: Float64Array.from(this.termV) };
|
|
3068
|
+
return true;
|
|
3069
|
+
};
|
|
3070
|
+
// A steady solve that fails: record how nearly singular the system was, and where; or, failing
|
|
3071
|
+
// with the islands' rows in, go back to the solution without them.
|
|
2468
3072
|
const fail = (r) => {
|
|
3073
|
+
if (kept) {
|
|
3074
|
+
this.u.set(kept.u);
|
|
3075
|
+
this.uLo.set(kept.uLo);
|
|
3076
|
+
this.termV.set(kept.termV);
|
|
3077
|
+
this.islandsOn = false;
|
|
3078
|
+
this.computeConcentrations();
|
|
3079
|
+
return { ...kept.result, iterations: r.iterations, history };
|
|
3080
|
+
}
|
|
2469
3081
|
if (dt === Infinity) this._noteConditioning();
|
|
2470
3082
|
return r;
|
|
2471
3083
|
};
|
|
2472
3084
|
for (let it = 1; it <= maxIter; it++) {
|
|
3085
|
+
if (kept && it - kept.it > 12) return fail({ converged: false, iterations: it - 1, history });
|
|
3086
|
+
if (kept && it === kept.it + 2) {
|
|
3087
|
+
// Refined from the islands' second update (their first moves the level, perhaps by
|
|
3088
|
+
// more than a refinement may), since the plain solves have shown a mode they lose.
|
|
3089
|
+
refine = true;
|
|
3090
|
+
noRefine = false;
|
|
3091
|
+
}
|
|
2473
3092
|
try {
|
|
2474
3093
|
this.assemble(dt);
|
|
2475
3094
|
} catch (err) {
|
|
@@ -2486,9 +3105,13 @@ export class Solver {
|
|
|
2486
3105
|
return fail({ converged: false, iterations: it, history, error: err.message });
|
|
2487
3106
|
}
|
|
2488
3107
|
try {
|
|
2489
|
-
const pins = this.
|
|
3108
|
+
const pins = this.pins;
|
|
2490
3109
|
deltaV.fill(0);
|
|
2491
3110
|
this._solveBordered(res, delta, deltaV, pins);
|
|
3111
|
+
if (refine && !this._refine(dt, delta, deltaV)) {
|
|
3112
|
+
refine = false; // (GMRES didn't converge, or strayed: the plain solves it is)
|
|
3113
|
+
noRefine = true;
|
|
3114
|
+
}
|
|
2492
3115
|
if (this.transformed) this._untransform(delta);
|
|
2493
3116
|
} catch (err) {
|
|
2494
3117
|
if (!(err instanceof SolverError)) throw err;
|
|
@@ -2505,7 +3128,7 @@ export class Solver {
|
|
|
2505
3128
|
ownHistory.push(own);
|
|
2506
3129
|
if (!Number.isFinite(step)) return fail({ converged: false, iterations: it, history, error: 'non-finite update' });
|
|
2507
3130
|
// Give up early on clear divergence; the caller will take a smaller step instead.
|
|
2508
|
-
if (own > 1e4 || (it > 6 && own > 10 * ownHistory[0])) {
|
|
3131
|
+
if ((it > 1 && own > 1e4) || (it > 6 && own > 10 * ownHistory[0])) {
|
|
2509
3132
|
this.computeConcentrations();
|
|
2510
3133
|
return fail({ converged: false, iterations: it, history, error: 'diverging' });
|
|
2511
3134
|
}
|
|
@@ -2513,8 +3136,10 @@ export class Solver {
|
|
|
2513
3136
|
this._addToState(delta, -alpha);
|
|
2514
3137
|
this.floating.forEach((k, a) => (this.termV[k] -= alpha * deltaV[a]));
|
|
2515
3138
|
if (alpha === 1 && step < tol) {
|
|
3139
|
+
const done = { converged: true, iterations: it, history, residual: rmax };
|
|
3140
|
+
if (islandsOff(it, done)) continue;
|
|
2516
3141
|
this.computeConcentrations();
|
|
2517
|
-
return
|
|
3142
|
+
return done;
|
|
2518
3143
|
}
|
|
2519
3144
|
// Converged as far as round-off allows: the updates are already tiny (below 1e-6 thermal
|
|
2520
3145
|
// units, ~26 nV) and have stopped shrinking. A badly conditioned system's floor can sit
|
|
@@ -2523,15 +3148,35 @@ export class Solver {
|
|
|
2523
3148
|
// (Raising this floor lets a weakly held population drift: a MOS capacitor without a
|
|
2524
3149
|
// channel port then showed a DC leak through its oxide.)
|
|
2525
3150
|
const [p1, p2] = [history[history.length - 2], history[history.length - 3]], floor = 1e-6;
|
|
2526
|
-
|
|
3151
|
+
// Stalled near the solution (not shrinking quadratically), or growing undamped twice
|
|
3152
|
+
// running: the factorised Jacobian may have lost a slow mode to cancellation, and
|
|
3153
|
+
// round-off's floor would accept it wrong, or its noise keep Newton from the solution (a
|
|
3154
|
+
// bipolar stack's floating base, its holes held ~1e14 more weakly than they move within
|
|
3155
|
+
// it). Refine the solves from here.
|
|
3156
|
+
if (refine) refined++;
|
|
3157
|
+
else if (!noRefine && alpha === 1 && it >= 3 && step > 0.25 * p1 && (step < 1e-4 || (step > p1 && p1 > p2))) refine = true;
|
|
3158
|
+
if (alpha === 1 && it >= 4 && (!refine || refined >= 1 || noRefine) && step < floor && p1 < floor && step > 0.25 * p1 && p1 > 0.25 * p2) {
|
|
3159
|
+
const done = { converged: true, iterations: it, history, residual: rmax, roundoff: true };
|
|
3160
|
+
if (islandsOff(it, done)) continue;
|
|
2527
3161
|
this.computeConcentrations();
|
|
2528
|
-
return
|
|
3162
|
+
return done;
|
|
2529
3163
|
}
|
|
2530
3164
|
}
|
|
2531
3165
|
this.computeConcentrations();
|
|
2532
3166
|
return fail({ converged: false, iterations: maxIter, history });
|
|
2533
3167
|
}
|
|
2534
3168
|
|
|
3169
|
+
// How far the terminal currents (as last assembled) are from adding up to zero, relative to the
|
|
3170
|
+
// largest of them: Kirchhoff's law, which a steady state keeps.
|
|
3171
|
+
_kirchhoff() {
|
|
3172
|
+
let sum = 0, mx = 0;
|
|
3173
|
+
for (const I of this.termI) {
|
|
3174
|
+
sum += I;
|
|
3175
|
+
mx = Math.max(mx, Math.abs(I));
|
|
3176
|
+
}
|
|
3177
|
+
return mx > 0 ? Math.abs(sum) / mx : 0;
|
|
3178
|
+
}
|
|
3179
|
+
|
|
2535
3180
|
// How many digits the last factorisation lost to cancellation, and where (the worst seen since
|
|
2536
3181
|
// the steady solve began). Only for diagnosing failures: a saturated species, for instance,
|
|
2537
3182
|
// can lose digits harmlessly.
|
|
@@ -2609,6 +3254,7 @@ export class Solver {
|
|
|
2609
3254
|
this.termVPrev.set(this.termV);
|
|
2610
3255
|
this.computeConcentrations();
|
|
2611
3256
|
const cN = Float64Array.from(this.c), thN = Float64Array.from(this.th), th0N = Float64Array.from(this.th0);
|
|
3257
|
+
const gN = Float64Array.from(this.gateList, (gt) => this.u[gt.o] + this.uLo[gt.o]);
|
|
2612
3258
|
// Contact displacement before the step: as it was at the end of the previous step (under
|
|
2613
3259
|
// the parameters then), so a gate-voltage change shows up as displacement current.
|
|
2614
3260
|
if (!this.contactDEnd || !this.portQEnd) {
|
|
@@ -2629,6 +3275,7 @@ export class Solver {
|
|
|
2629
3275
|
for (let k = 0; k < cOld.length; k++) cOld[k] = b1 * cN[k] - b2 * prev.c[k];
|
|
2630
3276
|
for (let k = 0; k < thN.length; k++) this.thOld[k] = b1 * thN[k] - b2 * prev.th[k];
|
|
2631
3277
|
for (let k = 0; k < th0N.length; k++) this.th0Old[k] = b1 * th0N[k] - b2 * prev.th0[k];
|
|
3278
|
+
for (let k = 0; k < gN.length; k++) this.gateOld[k] = b1 * gN[k] - b2 * prev.g[k];
|
|
2632
3279
|
this.contactDStart = { left: b1 * DN.left - b2 * prev.D.left, right: b1 * DN.right - b2 * prev.D.right };
|
|
2633
3280
|
this.portQStart = QN.map((q, k) => b1 * q - b2 * prev.Q[k]);
|
|
2634
3281
|
dtEff = dt / a0;
|
|
@@ -2636,6 +3283,7 @@ export class Solver {
|
|
|
2636
3283
|
this.cOld.set(cN);
|
|
2637
3284
|
this.thOld.set(thN);
|
|
2638
3285
|
this.th0Old.set(th0N);
|
|
3286
|
+
this.gateOld.set(gN);
|
|
2639
3287
|
this.contactDStart = DN;
|
|
2640
3288
|
this.portQStart = QN;
|
|
2641
3289
|
}
|
|
@@ -2648,12 +3296,26 @@ export class Solver {
|
|
|
2648
3296
|
let result;
|
|
2649
3297
|
try {
|
|
2650
3298
|
result = this.newton(dtEff, opts);
|
|
3299
|
+
if (!result.converged && dtEff === Infinity && this.constrained && this.islands.length > 0) {
|
|
3300
|
+
// Plain solves that don't converge may be lost along an island's level (two regions
|
|
3301
|
+
// conducting 1e20 times less hold it): once more from the start, with the islands'
|
|
3302
|
+
// summed rows in throughout (see newton).
|
|
3303
|
+
this.u.set(this.uPrev);
|
|
3304
|
+
this.uLo.set(this.uPrevLo);
|
|
3305
|
+
this.termV.set(this.termVPrev);
|
|
3306
|
+
if (opts.guess) {
|
|
3307
|
+
this.u.set(opts.guess);
|
|
3308
|
+
this.uLo.fill(0);
|
|
3309
|
+
}
|
|
3310
|
+
const again = this.newton(dtEff, { ...opts, islands: true });
|
|
3311
|
+
result = { ...again, iterations: result.iterations + again.iterations };
|
|
3312
|
+
}
|
|
2651
3313
|
} finally {
|
|
2652
3314
|
this.combining = false;
|
|
2653
3315
|
}
|
|
2654
3316
|
result.bdf = bdf;
|
|
2655
3317
|
if (result.converged) {
|
|
2656
|
-
this.history.unshift({ t: this.time, dt, u: Float64Array.from(this.uPrev), c: cN, th: thN, th0: th0N, D: DN, Q: QN });
|
|
3318
|
+
this.history.unshift({ t: this.time, dt, u: Float64Array.from(this.uPrev), c: cN, th: thN, th0: th0N, g: gN, D: DN, Q: QN });
|
|
2657
3319
|
if (this.history.length > 3) this.history.length = 3;
|
|
2658
3320
|
this.time = tEnd;
|
|
2659
3321
|
this.lastDt = dtEff;
|
|
@@ -2690,11 +3352,11 @@ export class Solver {
|
|
|
2690
3352
|
if (!(tEnd > this.time)) return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace };
|
|
2691
3353
|
let dt = this.dtNext ?? opts.dt0 ?? (tEnd - this.time) * 1e-4;
|
|
2692
3354
|
if (!(dt > 0)) dt = (tEnd - this.time) * 1e-4;
|
|
2693
|
-
let grow = 0; // longer first steps tried after a Newton failure (see below)
|
|
2694
3355
|
// The shortest step worth trying: round-off relative to the time, or the device's fastest
|
|
2695
3356
|
// time scale, whichever is shorter (a cold start can need steps far below a long run's 1e-14).
|
|
2696
3357
|
const floor = Math.min(1e-14 * Math.max(tEnd, 1e-300), this.fastestTime() * 1e-2);
|
|
2697
3358
|
const pred = new Float64Array(this.u.length), guess = new Float64Array(this.u.length);
|
|
3359
|
+
const firstErrs = []; // the first step's error at each size tried (see below)
|
|
2698
3360
|
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
3361
|
while (this.time < tEnd) {
|
|
2700
3362
|
if (steps + rejected >= maxSteps || clock() - start > budgetMs) break;
|
|
@@ -2719,6 +3381,7 @@ export class Solver {
|
|
|
2719
3381
|
this.landing = atBreak ? tb : clamped ? tEnd : undefined;
|
|
2720
3382
|
const snap = this._snapshot();
|
|
2721
3383
|
let err, p, half = null;
|
|
3384
|
+
if (this.history.length > 0) firstErrs.length = 0;
|
|
2722
3385
|
if (this.history.length === 0) {
|
|
2723
3386
|
// First step: backward Euler, checked against two half steps (whose result is kept).
|
|
2724
3387
|
const full = this.step(h);
|
|
@@ -2739,17 +3402,7 @@ export class Solver {
|
|
|
2739
3402
|
if (err === undefined) {
|
|
2740
3403
|
this._restore(snap);
|
|
2741
3404
|
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
|
-
}
|
|
3405
|
+
dt = h / 4;
|
|
2753
3406
|
if (dt < floor) { failed = true; break; }
|
|
2754
3407
|
continue;
|
|
2755
3408
|
}
|
|
@@ -2767,14 +3420,20 @@ export class Solver {
|
|
|
2767
3420
|
err = this._errorNorm(pred) * p.scale;
|
|
2768
3421
|
p = p.order;
|
|
2769
3422
|
}
|
|
2770
|
-
|
|
3423
|
+
// A first step whose error doesn't shrink with it: a jump in a level held at a boundary
|
|
3424
|
+
// (a bath's, after a voltage step) starts a profile self-similar in x/√t, so a first step
|
|
3425
|
+
// of any length errs alike, down to the grid's own diffusion time, below Newton's reach.
|
|
3426
|
+
// Three tries in a row, each within a factor 2 of the last, say so: the error is the
|
|
3427
|
+
// jump's, not the step's. Take the step (backward Euler damps it as it should) and grow.
|
|
3428
|
+
if (half && err > tol) firstErrs.push(err);
|
|
3429
|
+
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]));
|
|
3430
|
+
if (err > tol && !jump) {
|
|
2771
3431
|
this._restore(snap);
|
|
2772
3432
|
rejected++;
|
|
2773
3433
|
dt = h * factor(err, p);
|
|
2774
3434
|
continue;
|
|
2775
3435
|
}
|
|
2776
3436
|
steps++;
|
|
2777
|
-
grow = 0;
|
|
2778
3437
|
// The state the first step started from needn't satisfy the algebraic equations (φ in a
|
|
2779
3438
|
// strictly neutral region, an interface's unknowns, after a start or a jump), and they
|
|
2780
3439
|
// jump in the first instant; a predictor through it would see that jump as error on every
|
|
@@ -2857,9 +3516,22 @@ export class Solver {
|
|
|
2857
3516
|
const fail = (m) => {
|
|
2858
3517
|
throw new DeviceError(m);
|
|
2859
3518
|
};
|
|
2860
|
-
if (!Array.isArray(probes)) fail('advance: probes must be an array of { x, species, quantity, region }');
|
|
3519
|
+
if (!Array.isArray(probes)) fail('advance: probes must be an array of { x, species, quantity, region }, or { interface, species } / { interface, gate }');
|
|
2861
3520
|
const plan = probes.map((p, k) => {
|
|
2862
3521
|
const path = `advance: probes[${k}]`;
|
|
3522
|
+
if (p?.interface !== undefined) {
|
|
3523
|
+
// At a face: a species' flux through it (mol/(m²·s), toward +x), or a gate's fraction open.
|
|
3524
|
+
const f = p.interface, itf = this.model.interfaces[f];
|
|
3525
|
+
if (!(Number.isInteger(f) && itf)) fail(`${path}.interface: no interface ${JSON.stringify(f)} (they're numbered from 0, left to right)`);
|
|
3526
|
+
if (p.gate !== undefined) {
|
|
3527
|
+
const q = itf.gates.findIndex((gt) => gt.name === p.gate);
|
|
3528
|
+
if (q < 0) fail(`${path}.gate: interfaces[${f}] has no gate ${JSON.stringify(p.gate)}${itf.gates.length ? ` (it has ${itf.gates.map((gt) => gt.name).join(', ')})` : ''}`);
|
|
3529
|
+
return { o: this._gateSlot(f, q) };
|
|
3530
|
+
}
|
|
3531
|
+
const i = species.findIndex((sp) => sp.name === p.species);
|
|
3532
|
+
if (i < 0) fail(`${path}.species: no species ${JSON.stringify(p.species)}`);
|
|
3533
|
+
return { o: this.blockOfFace[f] * this.M + 1 + i };
|
|
3534
|
+
}
|
|
2863
3535
|
const quantity = p?.quantity ?? 'c';
|
|
2864
3536
|
if (quantity !== 'c' && quantity !== 'V' && quantity !== 'phi') fail(`${path}.quantity must be 'c' (mol/m³), 'V' (the species voltage) or 'phi' (φ, V)`);
|
|
2865
3537
|
const i = quantity === 'phi' ? -1 : species.findIndex((sp) => sp.name === p?.species);
|
|
@@ -2891,7 +3563,7 @@ export class Solver {
|
|
|
2891
3563
|
};
|
|
2892
3564
|
return {
|
|
2893
3565
|
out,
|
|
2894
|
-
read: () => plan.map((q) => (1 - q.w) * at(q, q.g) + q.w * at(q, q.g + 1)),
|
|
3566
|
+
read: () => plan.map((q) => (q.o !== undefined ? (this.loc[q.o] < 0 ? 0 : this.u[q.o]) : (1 - q.w) * at(q, q.g) + q.w * at(q, q.g + 1))),
|
|
2895
3567
|
push: (values) => values.forEach((v, k) => out[k].push(v)),
|
|
2896
3568
|
};
|
|
2897
3569
|
}
|
|
@@ -2909,6 +3581,7 @@ export class Solver {
|
|
|
2909
3581
|
const k = this.surfPort[g]; // an electrode surface's coverages, as η
|
|
2910
3582
|
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]));
|
|
2911
3583
|
}
|
|
3584
|
+
for (const { o } of this.gateList) mx = Math.max(mx, Math.abs(u[o] - ref[o])); // (a fraction: 1e-3 of m³ is 0.3% of P)
|
|
2912
3585
|
return mx;
|
|
2913
3586
|
}
|
|
2914
3587
|
|
|
@@ -2952,6 +3625,7 @@ export class Solver {
|
|
|
2952
3625
|
cOld: Float64Array.from(this.cOld),
|
|
2953
3626
|
thOld: Float64Array.from(this.thOld),
|
|
2954
3627
|
th0Old: Float64Array.from(this.th0Old),
|
|
3628
|
+
gateOld: Float64Array.from(this.gateOld),
|
|
2955
3629
|
time: this.time,
|
|
2956
3630
|
lastDt: this.lastDt,
|
|
2957
3631
|
atSteady: this.atSteady,
|
|
@@ -2974,6 +3648,7 @@ export class Solver {
|
|
|
2974
3648
|
this.cOld.set(s.cOld);
|
|
2975
3649
|
this.thOld.set(s.thOld);
|
|
2976
3650
|
this.th0Old.set(s.th0Old);
|
|
3651
|
+
this.gateOld.set(s.gateOld);
|
|
2977
3652
|
this.time = s.time;
|
|
2978
3653
|
this.lastDt = s.lastDt;
|
|
2979
3654
|
this.atSteady = s.atSteady;
|
|
@@ -3009,7 +3684,17 @@ export class Solver {
|
|
|
3009
3684
|
* Z = δV/δI with I into the device (positive real part for a passive device).
|
|
3010
3685
|
* @param {ArrayLike<number>} frequencies Hz
|
|
3011
3686
|
*/
|
|
3012
|
-
impedance(frequencies,
|
|
3687
|
+
impedance(frequencies, opts) {
|
|
3688
|
+
const was = this.allTerminals;
|
|
3689
|
+
this.allTerminals = true; // (it reads its terminal's ∂res/∂V and ∂I/∂x)
|
|
3690
|
+
try {
|
|
3691
|
+
return this._impedance(frequencies, opts);
|
|
3692
|
+
} finally {
|
|
3693
|
+
this.allTerminals = was;
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
_impedance(frequencies, { profiles = false, terminal = 'right' } = {}) {
|
|
3013
3698
|
const { M, nB, sys, VT } = this;
|
|
3014
3699
|
const kT = this.terms.findIndex((t) => t.name === terminal);
|
|
3015
3700
|
if (kT < 0) throw new SolverError(`impedance: no terminal named '${terminal}' (${this.terms.map((t) => t.name).join(', ')})`);
|
|
@@ -3021,6 +3706,7 @@ export class Solver {
|
|
|
3021
3706
|
this.cOld.set(this.c);
|
|
3022
3707
|
this.thOld.set(this.th);
|
|
3023
3708
|
this.th0Old.set(this.th0);
|
|
3709
|
+
this.gateList.forEach((gt, j) => (this.gateOld[j] = this.u[gt.o]));
|
|
3024
3710
|
this.assemble(Infinity);
|
|
3025
3711
|
this.contactDStart = { ...this.contactD };
|
|
3026
3712
|
this.portQStart = Float64Array.from(this.portQ);
|
|
@@ -3034,52 +3720,17 @@ export class Solver {
|
|
|
3034
3720
|
for (const X of ['A', 'B', 'C']) S[X] = sys[X].map((v, k) => (v - J[X][k]) * dts);
|
|
3035
3721
|
const Cs = this.termC.map((v, k) => v.map((x, j) => (x - Ct[k][j]) * dts));
|
|
3036
3722
|
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
|
-
};
|
|
3723
|
+
const cuts = this._currentCuts();
|
|
3724
|
+
// J·v for GMRES, exact: the assembled J holds a flux's dependence on η_L and η_R as two
|
|
3725
|
+
// entries, and where both are huge (an inversion layer) and v nearly uniform, J·v loses the
|
|
3726
|
+
// flux to round-off. So the dilute kernels' terms are kept in difference form (see
|
|
3727
|
+
// DifferenceTerms), with the rest from a matrix of their own, steady (Jr) and storage (Sr).
|
|
3728
|
+
this._assembleDifference(Infinity);
|
|
3729
|
+
const Jr = { A: this.sysRest.A.slice(), B: this.sysRest.B.slice(), C: this.sysRest.C.slice() };
|
|
3730
|
+
const { lin } = this._assembleDifference(dts);
|
|
3731
|
+
const Sr = {};
|
|
3732
|
+
for (const X of ['A', 'B', 'C']) Sr[X] = this.sysRest[X].map((v, k) => (v - Jr[X][k]) * dts);
|
|
3733
|
+
this.assemble(Infinity);
|
|
3083
3734
|
|
|
3084
3735
|
const { sizes, offA, offB, offC, offX } = sys;
|
|
3085
3736
|
const csys = new ComplexBlockTridiagonal(nB, sizes);
|
|
@@ -3125,9 +3776,6 @@ export class Solver {
|
|
|
3125
3776
|
}
|
|
3126
3777
|
}
|
|
3127
3778
|
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
3779
|
const sx = (X, vr, out) => {
|
|
3132
3780
|
for (let blk = 0; blk < nB; blk++) {
|
|
3133
3781
|
const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
|
|
@@ -3153,25 +3801,35 @@ export class Solver {
|
|
|
3153
3801
|
csys.solve(rRes, iRes, outR, outI);
|
|
3154
3802
|
return [outR, outI];
|
|
3155
3803
|
};
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
sx(
|
|
3160
|
-
sx(
|
|
3161
|
-
|
|
3804
|
+
// (J + iωS)·v, exactly (see DifferenceTerms), into (ar, ai).
|
|
3805
|
+
const jv = (vr, vi, ar, ai) => {
|
|
3806
|
+
sx(Jr, vr, jr);
|
|
3807
|
+
sx(Jr, vi, ji);
|
|
3808
|
+
sx(Sr, vi, si);
|
|
3809
|
+
sx(Sr, vr, sr);
|
|
3162
3810
|
for (let j = 0; j < Nc; j++) {
|
|
3163
3811
|
ar[j] = jr[j] - w * si[j];
|
|
3164
3812
|
ai[j] = ji[j] + w * sr[j];
|
|
3165
3813
|
}
|
|
3814
|
+
lin.apply(vr, ar, 1, 0);
|
|
3815
|
+
lin.apply(vi, ar, 0, -w);
|
|
3816
|
+
lin.apply(vi, ai, 1, 0);
|
|
3817
|
+
lin.apply(vr, ai, 0, w);
|
|
3818
|
+
};
|
|
3819
|
+
const op = ([vr, vi]) => {
|
|
3820
|
+
const ar = new Float64Array(Nc), ai = new Float64Array(Nc);
|
|
3821
|
+
jv(vr, vi, ar, ai);
|
|
3166
3822
|
return precondition(ar, ai);
|
|
3167
3823
|
};
|
|
3168
3824
|
const solve = (br, bi, outr, outi, k, sigma) => {
|
|
3169
3825
|
const rhs = k >= 0 ? precondition(Bt[k].map((v) => -sigma * v), zero) : [new Float64Array(Nc), new Float64Array(Nc)];
|
|
3170
3826
|
outr.set(rhs[0]);
|
|
3171
3827
|
outi.set(rhs[1]);
|
|
3172
|
-
|
|
3173
|
-
|
|
3828
|
+
// (GMRES keeps its best iterate, which is never worse than the factorised solve.)
|
|
3829
|
+
const { residual } = gmres(op, rhs, [outr, outi]);
|
|
3830
|
+
accuracy = Math.max(accuracy, residual);
|
|
3174
3831
|
};
|
|
3832
|
+
let accuracy = 1e-14; // (relative, of the solves: the readouts' error is weighed with it)
|
|
3175
3833
|
// y: the response with the floating terminals held; X_k: to a unit δV_k.
|
|
3176
3834
|
const heldT = dT.kind === 'V';
|
|
3177
3835
|
for (let j = 0; j < Nc; j++) {
|
|
@@ -3211,12 +3869,93 @@ export class Solver {
|
|
|
3211
3869
|
xr[this.fullOf[j]] = re;
|
|
3212
3870
|
xi[this.fullOf[j]] = im;
|
|
3213
3871
|
}
|
|
3872
|
+
// The current into terminal kT per volt, (C + iωC′)·δx + (∂I/∂V)(1 + iω′), with its error.
|
|
3873
|
+
// At the contact it's a sum whose terms can be far larger than itself (in a junction's
|
|
3874
|
+
// neutral ends, a conductance times a δη that cancels its neighbours' to 1e-17), and its
|
|
3875
|
+
// round-off reads as a constant conductance. Between the two contacts of a device without
|
|
3876
|
+
// ports, the total current (conduction and displacement) is the same through every cut, so
|
|
3877
|
+
// it's also read across each segment, and the reading with the least error taken. Each
|
|
3878
|
+
// reading's error is its terms' round-off and the solves' error in δx (to `accuracy` of its
|
|
3879
|
+
// largest entry) through its coefficients: a contact's or a cut's flux is a difference of
|
|
3880
|
+
// levels that the solved rows don't pin.
|
|
3881
|
+
// `parts`: the response as a sum, each part compact (r, i) and full-index (fr, fi), with the
|
|
3882
|
+
// absolute error of its entries.
|
|
3883
|
+
const read = (parts) => {
|
|
3884
|
+
const eps = 1.1e-16, c = Ct[kT], cs = Cs[kT];
|
|
3885
|
+
let Ir = DI[kT], Ii = w * DIs[kT], err = eps * (Math.abs(DI[kT]) + w * Math.abs(DIs[kT]));
|
|
3886
|
+
for (const p of parts) {
|
|
3887
|
+
const [re, im] = dot(kT, w, p.r, p.i);
|
|
3888
|
+
Ir += re;
|
|
3889
|
+
Ii += im;
|
|
3890
|
+
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);
|
|
3891
|
+
}
|
|
3892
|
+
if (!cuts || kT >= 2) return { Ir, Ii, err };
|
|
3893
|
+
const sgn = kT === 0 ? 1 : -1;
|
|
3894
|
+
for (const cut of cuts) {
|
|
3895
|
+
let re = 0, im = 0, e = 0;
|
|
3896
|
+
for (const p of parts) {
|
|
3897
|
+
for (let a = 0; a < cut.idx.length; a++) {
|
|
3898
|
+
const x = cut.idx[a], gk = cut.g[a], dk = w * cut.d[a];
|
|
3899
|
+
// (g + iωd)(xr + i xi)
|
|
3900
|
+
re += gk * p.fr[x] - dk * p.fi[x];
|
|
3901
|
+
im += gk * p.fi[x] + dk * p.fr[x];
|
|
3902
|
+
e += (Math.abs(gk) + Math.abs(dk)) * (eps * Math.hypot(p.fr[x], p.fi[x]) + p.floor);
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
if (e < err) [err, Ir, Ii] = [e, sgn * re, sgn * im];
|
|
3906
|
+
}
|
|
3907
|
+
return { Ir, Ii, err };
|
|
3908
|
+
};
|
|
3909
|
+
const largest = (r, i) => {
|
|
3910
|
+
let m = 0;
|
|
3911
|
+
for (let j = 0; j < r.length; j++) m = Math.max(m, Math.hypot(r[j], i[j]));
|
|
3912
|
+
return m;
|
|
3913
|
+
};
|
|
3914
|
+
const full = (r, i) => {
|
|
3915
|
+
const fr = new Float64Array(N), fi = new Float64Array(N);
|
|
3916
|
+
for (let j = 0; j < Nc; j++) {
|
|
3917
|
+
fr[this.fullOf[j]] = r[j];
|
|
3918
|
+
fi[this.fullOf[j]] = i[j];
|
|
3919
|
+
}
|
|
3920
|
+
return [fr, fi];
|
|
3921
|
+
};
|
|
3214
3922
|
let Zr, Zi;
|
|
3215
3923
|
if (heldT) {
|
|
3216
|
-
// δI per volt: (C + iωC′)·δx + (∂I/∂V)(1 + iω′) at the terminal
|
|
3217
|
-
let
|
|
3218
|
-
|
|
3219
|
-
|
|
3924
|
+
// δI per volt: (C + iωC′)·δx + (∂I/∂V)(1 + iω′) at the terminal…
|
|
3925
|
+
let { Ir, Ii, err } = read([{ r: yr, i: yi, fr: xr, fi: xi, floor: accuracy * largest(yr, yi) }]);
|
|
3926
|
+
// …and where the solves leave that unresolved (the estimate past 1% of the current), the
|
|
3927
|
+
// response is split: a uniform shift of each region (z_i s on every η_i, s on φ̂), which
|
|
3928
|
+
// changes no flux and no concentration, so that J·e vanishes inside each region and is
|
|
3929
|
+
// taken at its edges; and what's left, small, solved for to GMRES's floor, its error now
|
|
3930
|
+
// relative to itself. An electrolyte at its open circuit needs it: the response to its
|
|
3931
|
+
// terminal is nearly such a shift, and its current the slope of levels uniform to 1e-14,
|
|
3932
|
+
// more than a double can hold. (Not with ports, whose exchange sees the shift.)
|
|
3933
|
+
if (K === 0 && cuts && err > 1e-2 * Math.hypot(Ir, Ii)) {
|
|
3934
|
+
const [er, ei] = this._regionShift(yr, yi);
|
|
3935
|
+
const tr = new Float64Array(Nc), ti = new Float64Array(Nc);
|
|
3936
|
+
jv(er, ei, tr, ti);
|
|
3937
|
+
const edge = this._regionEdgeRows();
|
|
3938
|
+
const ar = new Float64Array(Nc), ai = new Float64Array(Nc);
|
|
3939
|
+
for (let j = 0; j < Nc; j++) {
|
|
3940
|
+
ar[j] = -Bt[kT][j] - (edge[j] ? tr[j] : 0);
|
|
3941
|
+
ai[j] = edge[j] ? -ti[j] : 0;
|
|
3942
|
+
}
|
|
3943
|
+
const rhs = precondition(ar, ai);
|
|
3944
|
+
const dr = yr.map((v, j) => v - er[j]), di = yi.map((v, j) => v - ei[j]);
|
|
3945
|
+
const { residual } = gmres(op, rhs, [dr, di], { tol: 1e-11 });
|
|
3946
|
+
const [efr, efi] = full(er, ei), [dfr, dfi] = full(dr, di);
|
|
3947
|
+
const r2 = read([
|
|
3948
|
+
{ r: er, i: ei, fr: efr, fi: efi, floor: 0 },
|
|
3949
|
+
{ r: dr, i: di, fr: dfr, fi: dfi, floor: Math.max(1e-14, residual) * largest(dr, di) },
|
|
3950
|
+
]);
|
|
3951
|
+
if (r2.err < err) ({ Ir, Ii, err } = r2);
|
|
3952
|
+
if (profiles) {
|
|
3953
|
+
for (let j = 0; j < N; j++) {
|
|
3954
|
+
xr[j] = efr[j] + dfr[j];
|
|
3955
|
+
xi[j] = efi[j] + dfi[j];
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3220
3959
|
const d2 = Ir * Ir + Ii * Ii; // Z = 1/δI
|
|
3221
3960
|
Zr = Ir / d2;
|
|
3222
3961
|
Zi = -Ii / d2;
|
|
@@ -3233,6 +3972,125 @@ export class Solver {
|
|
|
3233
3972
|
return out;
|
|
3234
3973
|
}
|
|
3235
3974
|
|
|
3975
|
+
// A uniform shift of each region, fitted to a small-signal response (compact, complex): φ̂ by
|
|
3976
|
+
// s_r and each species' η by z_i s_r, with s_r the region's mean δφ̂ (or, where φ̂ isn't an
|
|
3977
|
+
// unknown, its carriers' mean δη/z). Coverages and flux unknowns aren't shifted.
|
|
3978
|
+
_regionShift(yr, yi) {
|
|
3979
|
+
const { n, M, z, loc, model } = this, R = this.rix, { grid } = model;
|
|
3980
|
+
const er = new Float64Array(yr.length), ei = new Float64Array(yr.length);
|
|
3981
|
+
for (let r = 0; r < model.regions.length; r++) {
|
|
3982
|
+
let sr = 0, si = 0, k = 0;
|
|
3983
|
+
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
|
|
3984
|
+
for (let g = g0; g <= g1; g++) {
|
|
3985
|
+
const o = this.blockOfNode[g] * M;
|
|
3986
|
+
if (loc[o] >= 0) {
|
|
3987
|
+
sr += yr[R[o]];
|
|
3988
|
+
si += yi[R[o]];
|
|
3989
|
+
k++;
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
if (k === 0) {
|
|
3993
|
+
for (let g = g0; g <= g1; g++) {
|
|
3994
|
+
const o = this.blockOfNode[g] * M;
|
|
3995
|
+
for (let i = 0; i < n; i++) {
|
|
3996
|
+
if (z[i] === 0 || loc[o + 1 + i] < 0) continue;
|
|
3997
|
+
sr += yr[R[o + 1 + i]] / z[i];
|
|
3998
|
+
si += yi[R[o + 1 + i]] / z[i];
|
|
3999
|
+
k++;
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
if (k === 0) continue;
|
|
4004
|
+
sr /= k;
|
|
4005
|
+
si /= k;
|
|
4006
|
+
for (let g = g0; g <= g1; g++) {
|
|
4007
|
+
const o = this.blockOfNode[g] * M;
|
|
4008
|
+
if (loc[o] >= 0) {
|
|
4009
|
+
er[R[o]] = sr;
|
|
4010
|
+
ei[R[o]] = si;
|
|
4011
|
+
}
|
|
4012
|
+
for (let i = 0; i < n; i++) {
|
|
4013
|
+
if (loc[o + 1 + i] < 0) continue;
|
|
4014
|
+
er[R[o + 1 + i]] = z[i] * sr;
|
|
4015
|
+
ei[R[o + 1 + i]] = z[i] * si;
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
return [er, ei];
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
// Compact rows a region's uniform shift can touch: faces, the nodes beside them, and the two
|
|
4023
|
+
// ends (whatever holds a level from outside). Inside a region, every row is unchanged by it.
|
|
4024
|
+
_regionEdgeRows() {
|
|
4025
|
+
const { M, loc, nB } = this, R = this.rix;
|
|
4026
|
+
const edge = new Uint8Array(this.sys.size);
|
|
4027
|
+
const mark = (b) => {
|
|
4028
|
+
if (b < 0 || b >= nB) return;
|
|
4029
|
+
for (let r = 0; r < M; r++) if (loc[b * M + r] >= 0) edge[R[b * M + r]] = 1;
|
|
4030
|
+
};
|
|
4031
|
+
mark(0);
|
|
4032
|
+
mark(nB - 1);
|
|
4033
|
+
for (let f = 0; f < this.nFaces; f++) {
|
|
4034
|
+
const b = this.blockOfFace[f];
|
|
4035
|
+
mark(b - 1);
|
|
4036
|
+
mark(b);
|
|
4037
|
+
mark(b + 1);
|
|
4038
|
+
}
|
|
4039
|
+
return edge;
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
// The cuts where the impedance may read a small-signal current (null where the current
|
|
4043
|
+
// differs between cuts: a device with ports). Each is the linearised total current toward +x
|
|
4044
|
+
// through one segment of an ideal region, Σ (g + iω d)·δu over the unknowns idx: the
|
|
4045
|
+
// Scharfetter–Gummel fluxes' derivatives (as in _segmentsDilute) and the displacement's.
|
|
4046
|
+
_currentCuts() {
|
|
4047
|
+
const { model, n, M, u, uLo, c, z } = this, F = FARADAY;
|
|
4048
|
+
if (model.ports.length > 0) return null;
|
|
4049
|
+
const { grid, materials, regions } = model;
|
|
4050
|
+
const cuts = [];
|
|
4051
|
+
for (let r = 0; r < regions.length; r++) {
|
|
4052
|
+
const reg = regions[r], mat = materials[reg.material];
|
|
4053
|
+
if (mat.conductor || !mat.ideal || reg.mixing > 0) continue;
|
|
4054
|
+
const vel = reg.velocity;
|
|
4055
|
+
for (let s = grid.regionStart[r]; s < grid.regionEnd[r]; s++) {
|
|
4056
|
+
const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s];
|
|
4057
|
+
const terms = new Map();
|
|
4058
|
+
const add = (x, g, d) => {
|
|
4059
|
+
const t = terms.get(x) || [0, 0];
|
|
4060
|
+
t[0] += g;
|
|
4061
|
+
t[1] += d;
|
|
4062
|
+
terms.set(x, t);
|
|
4063
|
+
};
|
|
4064
|
+
const phiL = u[bL * M], phiR = u[bR * M], hasPhi = !this.phiUndefined[s];
|
|
4065
|
+
for (let i = 0; i < n; i++) {
|
|
4066
|
+
if (z[i] === 0 || !mat.present[i] || mat.D[i] === 0) continue;
|
|
4067
|
+
const zi = z[i], r1 = 1 + i, cL = c[s * n + i], g = mat.D[i] / h;
|
|
4068
|
+
const pe = (vel * h) / mat.D[i];
|
|
4069
|
+
const d = zi * (phiR - phiL) - pe;
|
|
4070
|
+
const E = Math.expm1(u[bR * M + r1] - u[bL * M + r1] + (uLo[bR * M + r1] - uLo[bL * M + r1]) - pe);
|
|
4071
|
+
const gBc = g * bernoulli(d) * cL;
|
|
4072
|
+
const dNdd = -g * bernoulliDerivative(d) * cL * E;
|
|
4073
|
+
add(bL * M + r1, zi * F * gBc, 0);
|
|
4074
|
+
add(bR * M + r1, -zi * F * gBc * (E + 1), 0);
|
|
4075
|
+
if (hasPhi) {
|
|
4076
|
+
add(bL * M, zi * F * (-zi * dNdd + zi * gBc * E), 0);
|
|
4077
|
+
add(bR * M, zi * F * zi * dNdd, 0);
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
// The displacement toward +x, ε(φ_L − φ_R)/h (in φ̂ = φ/V_T).
|
|
4081
|
+
const k = hasPhi ? (mat.epsr * EPS0 * this.VT) / h : 0;
|
|
4082
|
+
if (k > 0) {
|
|
4083
|
+
add(bL * M, 0, k);
|
|
4084
|
+
add(bR * M, 0, -k);
|
|
4085
|
+
}
|
|
4086
|
+
if (terms.size === 0) continue;
|
|
4087
|
+
const idx = [...terms.keys()];
|
|
4088
|
+
cuts.push({ idx, g: idx.map((x) => terms.get(x)[0]), d: idx.map((x) => terms.get(x)[1]) });
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
return cuts;
|
|
4092
|
+
}
|
|
4093
|
+
|
|
3236
4094
|
// Complex profiles of δφ (V), δμ̄ (J/mol) and δc (mol/m³) from a small-signal solution.
|
|
3237
4095
|
_smallSignalProfiles(xr, xi) {
|
|
3238
4096
|
const { n, M, z, VT, model } = this;
|
|
@@ -3277,6 +4135,7 @@ export class Solver {
|
|
|
3277
4135
|
// Jacobian are left partial (the next assemble() starts afresh). Concentrations must be current.
|
|
3278
4136
|
_assembleBookkeeping(dt) {
|
|
3279
4137
|
const { model } = this;
|
|
4138
|
+
this.dtNow = dt;
|
|
3280
4139
|
const { grid, materials, regions } = model;
|
|
3281
4140
|
this.res.fill(0);
|
|
3282
4141
|
for (let r = 0; r < regions.length; r++) {
|
|
@@ -3362,6 +4221,16 @@ export class Solver {
|
|
|
3362
4221
|
// Steady state from the present one; `atSteady` says whether the state is one (until a step or
|
|
3363
4222
|
// a change of drive).
|
|
3364
4223
|
solveSteady(opts = {}) {
|
|
4224
|
+
this.flattening = true;
|
|
4225
|
+
this.unreached = null; // (a driven current no held voltage reaches: see _floatingContinuation)
|
|
4226
|
+
try {
|
|
4227
|
+
return this._solveSteadyAll(opts);
|
|
4228
|
+
} finally {
|
|
4229
|
+
this.flattening = false;
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
_solveSteadyAll(opts) {
|
|
3365
4234
|
// A terminal that passes current only by charging (a gate, a capacitance with no species
|
|
3366
4235
|
// through it) has no steady state under a current drive: it charges for ever, or at I = 0
|
|
3367
4236
|
// keeps whatever charge it started with, which a steady solve doesn't know.
|
|
@@ -3399,25 +4268,44 @@ export class Solver {
|
|
|
3399
4268
|
if (r.converged) return r;
|
|
3400
4269
|
// Generation (e.g. light) holding the device far from equilibrium: ramp it up from nearly
|
|
3401
4270
|
// nothing, each solve warm from the last.
|
|
4271
|
+
let dimmer = false;
|
|
3402
4272
|
if (this.hasGeneration && opts.continuation !== false) {
|
|
3403
4273
|
this.u.set(u0);
|
|
3404
4274
|
this.uLo.set(u0Lo);
|
|
3405
4275
|
this.termV.set(v0);
|
|
3406
4276
|
this.computeConcentrations();
|
|
4277
|
+
this.dimmer = false;
|
|
3407
4278
|
const g = this._generationContinuation(opts, r);
|
|
4279
|
+
dimmer = this.dimmer;
|
|
3408
4280
|
if (g.converged) {
|
|
3409
4281
|
this.solvedV = target;
|
|
3410
4282
|
return g;
|
|
3411
4283
|
}
|
|
3412
4284
|
r = g;
|
|
3413
4285
|
}
|
|
4286
|
+
// A terminal driven by a current (open circuit, say): hold it at a voltage instead, march the
|
|
4287
|
+
// voltage until the current crosses its target, and float it from there.
|
|
4288
|
+
if (opts.continuation !== false && this.floating.some((k) => this.terms[k].drive.kind === 'I')) {
|
|
4289
|
+
this.u.set(u0);
|
|
4290
|
+
this.uLo.set(u0Lo);
|
|
4291
|
+
this.termV.set(v0);
|
|
4292
|
+
this.computeConcentrations();
|
|
4293
|
+
const q = this._floatingContinuation(opts, r);
|
|
4294
|
+
if (q.converged) return q;
|
|
4295
|
+
r = q;
|
|
4296
|
+
}
|
|
3414
4297
|
if (!canContinue) {
|
|
3415
4298
|
if (quick) {
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
4299
|
+
// The full pseudo-transient ramp: from the dimmer light's solution where the light's
|
|
4300
|
+
// continuation got anywhere (a lit floating base charges from there as it would in
|
|
4301
|
+
// time), else from the start.
|
|
4302
|
+
if (!dimmer) {
|
|
4303
|
+
this.u.set(u0);
|
|
4304
|
+
this.uLo.set(u0Lo);
|
|
4305
|
+
this.termV.set(v0);
|
|
4306
|
+
}
|
|
3419
4307
|
this.computeConcentrations();
|
|
3420
|
-
const full = this._solveSteady(opts);
|
|
4308
|
+
const full = this._solveSteady(opts);
|
|
3421
4309
|
return { ...full, steps: full.steps + r.steps, iterations: full.iterations + r.iterations };
|
|
3422
4310
|
}
|
|
3423
4311
|
return r;
|
|
@@ -3445,18 +4333,121 @@ export class Solver {
|
|
|
3445
4333
|
return { ...r, steps: r.steps + c.steps, iterations: r.iterations + c.iterations };
|
|
3446
4334
|
}
|
|
3447
4335
|
|
|
4336
|
+
// A cold steady solve with a terminal driven by a current, which can fail where the same state
|
|
4337
|
+
// is easy warm (a solar cell's open circuit, from cold on a fine grid): the terminal held at a
|
|
4338
|
+
// voltage instead, whose steady solves have their own continuation, the voltage marched from
|
|
4339
|
+
// the start's until the current crosses its target, the crossing narrowed by bisection, and
|
|
4340
|
+
// the terminal floated again from beside it.
|
|
4341
|
+
_floatingContinuation(opts, r) {
|
|
4342
|
+
const k = this.floating.find((j) => this.terms[j].drive.kind === 'I');
|
|
4343
|
+
const drive = this.terms[k].drive, target = sourceAt(drive.src, this.time);
|
|
4344
|
+
let steps = r.steps ?? 0, iterations = r.iterations ?? 0;
|
|
4345
|
+
const history = (r.history ?? []).slice();
|
|
4346
|
+
const hold = (V) => {
|
|
4347
|
+
this.terms[k].drive = { kind: 'V', src: { value: V }, R: 0 };
|
|
4348
|
+
this.redrive();
|
|
4349
|
+
const q = this._steadyFromHere(opts);
|
|
4350
|
+
steps += q.steps ?? 0;
|
|
4351
|
+
iterations += q.iterations ?? 0;
|
|
4352
|
+
history.push({ held: V, converged: q.converged });
|
|
4353
|
+
if (q.converged) seen.push([V, this.termI[k]]);
|
|
4354
|
+
return q.converged ? this.termI[k] - target : NaN;
|
|
4355
|
+
};
|
|
4356
|
+
const fail = () => ({ converged: false, steps, iterations, history });
|
|
4357
|
+
const seen = []; // [V, I] of each held solve that converged
|
|
4358
|
+
let Va = this.termV[k], fa, Vb, fb;
|
|
4359
|
+
try {
|
|
4360
|
+
fa = hold(Va);
|
|
4361
|
+
if (!Number.isFinite(fa)) {
|
|
4362
|
+
Va = 0;
|
|
4363
|
+
fa = hold(Va);
|
|
4364
|
+
}
|
|
4365
|
+
if (!Number.isFinite(fa)) return fail();
|
|
4366
|
+
// March, the step growing: up where the current is below its target, since raising a
|
|
4367
|
+
// terminal's voltage raises the current into a passive device (the other way, from the
|
|
4368
|
+
// start again, if 20 V brings no crossing).
|
|
4369
|
+
const start = Va, f0 = fa;
|
|
4370
|
+
[Vb, fb] = [Va, fa];
|
|
4371
|
+
for (const dir of [fa < 0 ? 1 : -1, fa < 0 ? -1 : 1]) {
|
|
4372
|
+
[Va, fa] = [start, f0];
|
|
4373
|
+
if (dir !== (f0 < 0 ? 1 : -1)) hold(start); // (back to the start, for a warm march)
|
|
4374
|
+
let dV = 0.02;
|
|
4375
|
+
Vb = Va + dir * dV;
|
|
4376
|
+
fb = hold(Vb);
|
|
4377
|
+
while (Number.isFinite(fb) && Math.sign(fb) === Math.sign(fa) && fa !== 0 && Math.abs(Vb - start) < 20) {
|
|
4378
|
+
[Va, fa] = [Vb, fb];
|
|
4379
|
+
dV *= 1.5;
|
|
4380
|
+
Vb = Va + dir * dV;
|
|
4381
|
+
fb = hold(Vb);
|
|
4382
|
+
}
|
|
4383
|
+
if (Number.isFinite(fb) && (Math.sign(fb) !== Math.sign(fa) || fa === 0)) break;
|
|
4384
|
+
}
|
|
4385
|
+
if (!Number.isFinite(fb) || (Math.sign(fb) === Math.sign(fa) && fa !== 0)) {
|
|
4386
|
+
// What the held voltages passed, at the two extremes reached.
|
|
4387
|
+
if (seen.length > 1) {
|
|
4388
|
+
seen.sort((a, b) => a[0] - b[0]);
|
|
4389
|
+
this.unreached = { terminal: this.terms[k].name, target, low: seen[0], high: seen.at(-1) };
|
|
4390
|
+
}
|
|
4391
|
+
return fail();
|
|
4392
|
+
}
|
|
4393
|
+
// Bisect (each held solve warm from the last) to a tenth of a millivolt.
|
|
4394
|
+
for (let it = 0; it < 40 && Math.abs(Vb - Va) > 1e-4; it++) {
|
|
4395
|
+
const Vm = (Va + Vb) / 2, fm = hold(Vm);
|
|
4396
|
+
if (!Number.isFinite(fm)) return fail();
|
|
4397
|
+
if (Math.sign(fm) === Math.sign(fa)) [Va, fa] = [Vm, fm];
|
|
4398
|
+
else [Vb, fb] = [Vm, fm];
|
|
4399
|
+
}
|
|
4400
|
+
hold(Math.abs(fa) < Math.abs(fb) ? Va : Vb);
|
|
4401
|
+
} finally {
|
|
4402
|
+
this.terms[k].drive = drive;
|
|
4403
|
+
this.redrive();
|
|
4404
|
+
}
|
|
4405
|
+
const q = this._solveSteady(opts);
|
|
4406
|
+
steps += q.steps ?? 0;
|
|
4407
|
+
iterations += q.iterations ?? 0;
|
|
4408
|
+
history.push(...(q.history ?? []));
|
|
4409
|
+
if (q.converged) return { ...q, steps, iterations, history };
|
|
4410
|
+
// Floated, the system can lose what held it doesn't (a closed redox cell driven near its
|
|
4411
|
+
// limit: its bordered row 33 digits short), but a current-driven steady state is the held
|
|
4412
|
+
// one at the voltage that passes the target: found by held solves, by regula falsi
|
|
4413
|
+
// (Illinois) within the bracket, to 1e-10 of the target.
|
|
4414
|
+
try {
|
|
4415
|
+
let side = 0;
|
|
4416
|
+
for (let it = 0; it < 60; it++) {
|
|
4417
|
+
const Vm = Va - (fa * (Vb - Va)) / (fb - fa), fm = hold(Vm);
|
|
4418
|
+
if (!Number.isFinite(fm)) return fail();
|
|
4419
|
+
if (Math.abs(fm) <= 1e-10 * Math.abs(target)) return { converged: true, steps, iterations, history, residual: Math.abs(fm) };
|
|
4420
|
+
if (Math.sign(fm) === Math.sign(fb)) {
|
|
4421
|
+
[Vb, fb] = [Vm, fm];
|
|
4422
|
+
if (side === -1) fa /= 2;
|
|
4423
|
+
side = -1;
|
|
4424
|
+
} else {
|
|
4425
|
+
[Va, fa] = [Vm, fm];
|
|
4426
|
+
if (side === 1) fb /= 2;
|
|
4427
|
+
side = 1;
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
return fail();
|
|
4431
|
+
} finally {
|
|
4432
|
+
this.terms[k].drive = drive;
|
|
4433
|
+
this.redrive();
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
|
|
3448
4437
|
// Steady solves with the generation reactions' rates scaled from 1e-12 up to 1, ×100 a step
|
|
3449
4438
|
// while they converge, smaller steps where they don't.
|
|
3450
4439
|
_generationContinuation(opts, r) {
|
|
3451
4440
|
const sub = this._directSteady() ? { ...opts, maxSteps: 1 } : opts;
|
|
3452
|
-
let s = 1e-
|
|
4441
|
+
let s = 1e-30, factor = 100, steps = r.steps, iterations = r.iterations;
|
|
3453
4442
|
const history = r.history.slice();
|
|
3454
4443
|
try {
|
|
3455
4444
|
this.generationScale = s;
|
|
4445
|
+
// (nearly dark: a cold start can need the full ramp, as a dark device's does)
|
|
3456
4446
|
let q = this._solveSteady(sub);
|
|
3457
4447
|
steps += q.steps;
|
|
3458
4448
|
iterations += q.iterations;
|
|
3459
4449
|
if (!q.converged) return { converged: false, steps, iterations, history };
|
|
4450
|
+
this.dimmer = true; // (the state is now a solution under dimmer light: a better start than cold)
|
|
3460
4451
|
while (s < 1) {
|
|
3461
4452
|
const next = Math.min(1, s * factor);
|
|
3462
4453
|
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
@@ -3474,7 +4465,7 @@ export class Solver {
|
|
|
3474
4465
|
this.termV.set(v1);
|
|
3475
4466
|
this.computeConcentrations();
|
|
3476
4467
|
factor = Math.sqrt(factor);
|
|
3477
|
-
if (factor < 1.
|
|
4468
|
+
if (factor < 1.5) return { converged: false, steps, iterations, history }; // (crawling: time steps do better)
|
|
3478
4469
|
}
|
|
3479
4470
|
}
|
|
3480
4471
|
return { converged: true, steps, iterations, history };
|
|
@@ -3492,6 +4483,17 @@ export class Solver {
|
|
|
3492
4483
|
let q;
|
|
3493
4484
|
if (!warm) {
|
|
3494
4485
|
q = this._solveSteady(opts);
|
|
4486
|
+
if (!q.converged) {
|
|
4487
|
+
// A cold device's start was laid out for the target (a set() before the first solve
|
|
4488
|
+
// builds it there): lay it out again, level.
|
|
4489
|
+
steps += q.steps;
|
|
4490
|
+
iterations += q.iterations;
|
|
4491
|
+
this._refreshSources();
|
|
4492
|
+
this.initFromComposition();
|
|
4493
|
+
q = this._solveSteady(opts);
|
|
4494
|
+
}
|
|
4495
|
+
// (lit, the level start may need the light ramped up too)
|
|
4496
|
+
if (!q.converged && this.hasGeneration) q = this._generationContinuation(opts, q);
|
|
3495
4497
|
steps += q.steps;
|
|
3496
4498
|
iterations += q.iterations;
|
|
3497
4499
|
if (!q.converged) return { ...r, steps, iterations };
|
|
@@ -3540,7 +4542,8 @@ export class Solver {
|
|
|
3540
4542
|
const direct = this._directSteady();
|
|
3541
4543
|
const giant = direct ? Infinity : 1e6 * tau;
|
|
3542
4544
|
if (direct) this._renormalizeSpectators(); // a starting point with the right amounts
|
|
3543
|
-
let dt = giant;
|
|
4545
|
+
let dt = giant, grow = 10; // (after a failure, dt grows by less, then back up to ×10)
|
|
4546
|
+
let triedDirect = 0;
|
|
3544
4547
|
let totalIter = 0, steps = 0, converged = false;
|
|
3545
4548
|
const history = [];
|
|
3546
4549
|
while (steps < maxSteps) {
|
|
@@ -3561,6 +4564,7 @@ export class Solver {
|
|
|
3561
4564
|
// Pseudo-transient continuation: down from a slow time scale until a step converges,
|
|
3562
4565
|
// as far as the fastest (slow ions mustn't stop it short of what a cold start needs).
|
|
3563
4566
|
dt = dt >= giant ? tau * 1e-6 : dt / 4;
|
|
4567
|
+
grow = 2;
|
|
3564
4568
|
if (dt < Math.min(tau * 1e-15, this.fastestTime() * 1e-2)) break;
|
|
3565
4569
|
continue;
|
|
3566
4570
|
}
|
|
@@ -3568,6 +4572,21 @@ export class Solver {
|
|
|
3568
4572
|
converged = true; // the steady equations themselves were solved
|
|
3569
4573
|
break;
|
|
3570
4574
|
}
|
|
4575
|
+
// Past the slowest diffusion time, the direct solve may already reach from here: try it
|
|
4576
|
+
// once a decade (the state kept if it fails), rather than step on to the giant dt through
|
|
4577
|
+
// steps that can fail for slow ions' sake.
|
|
4578
|
+
if (direct && dt < giant && dt >= tau && dt >= 10 * triedDirect) {
|
|
4579
|
+
triedDirect = dt;
|
|
4580
|
+
const snap = this._snapshot();
|
|
4581
|
+
const d = this.step(Infinity);
|
|
4582
|
+
totalIter += d.iterations;
|
|
4583
|
+
history.push({ dt: Infinity, converged: d.converged, iterations: d.iterations });
|
|
4584
|
+
if (d.converged) {
|
|
4585
|
+
converged = true;
|
|
4586
|
+
break;
|
|
4587
|
+
}
|
|
4588
|
+
this._restore(snap);
|
|
4589
|
+
}
|
|
3571
4590
|
if (dt >= giant && this._maxPotentialStep(this._diff()) < tol) {
|
|
3572
4591
|
converged = true;
|
|
3573
4592
|
if (dt > giant) {
|
|
@@ -3580,7 +4599,8 @@ export class Solver {
|
|
|
3580
4599
|
break;
|
|
3581
4600
|
}
|
|
3582
4601
|
if (dt < giant) {
|
|
3583
|
-
dt *=
|
|
4602
|
+
dt *= grow; // ramping up after a failure
|
|
4603
|
+
grow = Math.min(10, grow * 1.5);
|
|
3584
4604
|
if (dt > 1e6 * tau) dt = giant; // then the direct steady solve (or the giant step)
|
|
3585
4605
|
} else if (dt < 1e6 * giant) {
|
|
3586
4606
|
dt *= 10; // conserved amounts present and still moving: let dt keep growing (capped)
|