driftlet 0.8.0 → 0.10.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 +5 -3
- package/docs/device.md +169 -57
- package/docs/kit.md +7 -2
- package/docs/numerics.md +17 -5
- package/llms.txt +106 -7
- package/package.json +2 -1
- package/src/checks.js +27 -23
- package/src/describe.js +18 -10
- package/src/device.js +90 -21
- package/src/index.js +18 -4
- package/src/polarization.js +59 -12
- package/src/solution.js +32 -2
- package/src/solver.js +308 -59
- package/src/stack.js +3 -2
- package/src/types.js +14 -3
- package/types/checks.d.ts +3 -3
- package/types/index.d.ts +1 -0
- package/types/live.d.ts +1 -0
- package/types/polarization.d.ts +8 -2
- package/types/solver.d.ts +35 -4
- package/types/stack.d.ts +1 -1
- package/types/types.d.ts +31 -1
package/src/solver.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// and its derivative g′(a) = α e^{αa} + (1−α) e^{−(1−α)a}.
|
|
3
3
|
function bvFactor(a, alpha) {
|
|
4
4
|
const e = Math.exp(-(1 - alpha) * a);
|
|
5
|
-
|
|
5
|
+
// (far from equilibrium, the plain difference: the product would give 0·∞ there)
|
|
6
|
+
const g = Math.abs(a) > 50 ? Math.exp(alpha * a) - e : e * Math.expm1(a);
|
|
7
|
+
return { g, gp: alpha * Math.exp(alpha * a) + (1 - alpha) * e };
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
// Solve the K×K complex system (Ar + i Ai) x = (last column), partial pivoting by modulus.
|
|
@@ -195,13 +197,16 @@ export class Solver {
|
|
|
195
197
|
// Slots per block: φ̂ (or D, or a conductor's segment flux J), one per species, and at a face
|
|
196
198
|
// one per face reaction (its rate), as many as the busiest face has.
|
|
197
199
|
const nRx = model.interfaces.reduce((m, itf) => Math.max(m, itf.reactions.length), 0);
|
|
198
|
-
|
|
200
|
+
// (and at an electrode port's nodes, one per surface species: its coverage's η)
|
|
201
|
+
const nSurf = model.ports.reduce((m, port) => Math.max(m, port.surface.length), 0);
|
|
202
|
+
const M = n + 1 + Math.max(nRx, nSurf);
|
|
199
203
|
const nNodes = grid.nNodes;
|
|
200
204
|
const nFaces = regions.length - 1;
|
|
201
205
|
const nB = nNodes + nFaces;
|
|
202
206
|
this.n = n;
|
|
203
207
|
this.M = M;
|
|
204
208
|
this.nRx = nRx;
|
|
209
|
+
this.nSurf = nSurf;
|
|
205
210
|
this.nNodes = nNodes;
|
|
206
211
|
this.nFaces = nFaces;
|
|
207
212
|
this.nB = nB;
|
|
@@ -214,6 +219,22 @@ export class Solver {
|
|
|
214
219
|
this.blockOfFace = new Int32Array(nFaces);
|
|
215
220
|
for (let f = 0; f < nFaces; f++) this.blockOfFace[f] = grid.regionEnd[f] + f + 1;
|
|
216
221
|
|
|
222
|
+
// Electrode ports' surfaces: each node's port (−1: none) and its index in the port's window;
|
|
223
|
+
// the coverages there, θ [g·nSurf + s], now and at the step's start.
|
|
224
|
+
this.surfPort = new Int32Array(nNodes).fill(-1);
|
|
225
|
+
this.surfW = new Int32Array(nNodes);
|
|
226
|
+
model.ports.forEach((port, k) => {
|
|
227
|
+
if (port.surface.length > 0) port.nodes.forEach((g, w) => ((this.surfPort[g] = k), (this.surfW[g] = w)));
|
|
228
|
+
});
|
|
229
|
+
// Each surface species of each port, as a column of what's conserved: [port, species].
|
|
230
|
+
this.surfCols = model.ports.flatMap((port, k) => port.surface.map((_, q) => [k, q]));
|
|
231
|
+
this.surfColOf = model.ports.map((port, k) => this.surfCols.findIndex(([kk]) => kk === k));
|
|
232
|
+
this.th = new Float64Array(nNodes * nSurf);
|
|
233
|
+
this.thOld = new Float64Array(nNodes * nSurf);
|
|
234
|
+
// The bare fraction θ₀, directly (1 − Σθ cancels as θ → 1); 1 where there's no surface.
|
|
235
|
+
this.th0 = new Float64Array(nNodes).fill(1);
|
|
236
|
+
this.th0Old = new Float64Array(nNodes).fill(1);
|
|
237
|
+
|
|
217
238
|
// Per-node material data, flattened [g·n + i].
|
|
218
239
|
this.present = new Uint8Array(nNodes * n);
|
|
219
240
|
this.cRef = new Float64Array(nNodes * n);
|
|
@@ -327,6 +348,19 @@ export class Solver {
|
|
|
327
348
|
this.contactFlux = { left: new Float64Array(n), right: new Float64Array(n) };
|
|
328
349
|
this.portFlux = model.ports.map(() => new Float64Array(n)); // into the device, mol/(m²·s)
|
|
329
350
|
this.contactD = { left: 0, right: 0 };
|
|
351
|
+
// A capacitive port's charge (its side's, Σ vol·a·σ over its window), as the contacts' D: now,
|
|
352
|
+
// at the step's start and at the previous step's end.
|
|
353
|
+
this.portQ = new Float64Array(model.ports.length);
|
|
354
|
+
this.portQStart = new Float64Array(model.ports.length);
|
|
355
|
+
this.portQOld = new Float64Array(model.ports.length);
|
|
356
|
+
this.portQEnd = null;
|
|
357
|
+
// And per node, the charge per volume the capacitances hold against the window (Σ aσ, over
|
|
358
|
+
// every port with one there: two gates on one channel, say; the ions hold −Σ aσ), its slope
|
|
359
|
+
// Σ a·C per volt, and the ports [terminal, index in its window].
|
|
360
|
+
this.qg = new Float64Array(grid.nNodes);
|
|
361
|
+
this.qgSlope = new Float64Array(grid.nNodes);
|
|
362
|
+
this.qgTerms = Array.from({ length: grid.nNodes }, () => []);
|
|
363
|
+
model.ports.forEach((port, k) => port.capacitance && port.nodes.forEach((g, w) => this.qgTerms[g].push([2 + k, w])));
|
|
330
364
|
this.contactDOld = { left: 0, right: 0 };
|
|
331
365
|
this.contactDStart = { left: 0, right: 0 };
|
|
332
366
|
// Terminals (the two contacts, then the ports): each one's voltage (V), held by its source or
|
|
@@ -407,11 +441,15 @@ export class Solver {
|
|
|
407
441
|
// A port that only exchanges (an O₂ supply, a leak) is solved there too: left out, its window
|
|
408
442
|
// fell back on the ill-conditioned terms, and a closed neutral electrolyte whose potential is
|
|
409
443
|
// set only by a face reaction couldn't take a step.
|
|
444
|
+
// An end node is solved there too if its contact is closed to every ion and leaves φ alone.
|
|
410
445
|
const held = new Uint8Array(nNodes);
|
|
411
|
-
|
|
446
|
+
const closed = (side) => model.contacts[side].phi.type === 'neutral' && model.contacts[side].species.every((l, i) => species[i].z === 0 ? l.type !== 'equilibrium' : l.type === 'blocked');
|
|
447
|
+
this.closedEnd = { left: closed('left'), right: closed('right') };
|
|
448
|
+
held[0] = this.closedEnd.left ? 0 : 1;
|
|
449
|
+
held[nNodes - 1] = this.closedEnd.right ? 0 : 1;
|
|
412
450
|
for (const port of model.ports) if (port.species.some((l) => l.type === 'equilibrium')) for (const g of port.nodes) held[g] = 1;
|
|
413
451
|
this.chargeNode = new Uint8Array(nNodes);
|
|
414
|
-
const neutralFace = (f) => f
|
|
452
|
+
const neutralFace = (f) => (f === -1 ? this.closedEnd.left : f === model.interfaces.length ? this.closedEnd.right : model.interfaces[f].phi.type === 'neutral');
|
|
415
453
|
regions.forEach((reg, r) => {
|
|
416
454
|
const mat = materials[reg.material];
|
|
417
455
|
if (mat.conductor || mat.epsr !== 0) return;
|
|
@@ -435,6 +473,8 @@ export class Solver {
|
|
|
435
473
|
this._findStretches();
|
|
436
474
|
this.initFromComposition();
|
|
437
475
|
this.referenceAmounts = this.stretches.map((st) => this.amount(st));
|
|
476
|
+
// And what each surface species held then (a conserved combination through one counts it).
|
|
477
|
+
this.surfaceRef = Float64Array.from(this.surfCols, (_, j) => this.surfaceAmount(j));
|
|
438
478
|
// ∫ (flux in − flux out) dt through the contacts, per stretch, since the reference.
|
|
439
479
|
this.boundaryIntake = new Float64Array(this.stretches.length);
|
|
440
480
|
}
|
|
@@ -453,6 +493,8 @@ export class Solver {
|
|
|
453
493
|
}
|
|
454
494
|
active[b * M] = this.phiUndefined[g] ? 0 : 1;
|
|
455
495
|
for (let i = 0; i < n; i++) active[b * M + 1 + i] = this.present[g * n + i];
|
|
496
|
+
const k = this.surfPort[g];
|
|
497
|
+
if (k >= 0) for (let q = 0; q < model.ports[k].surface.length; q++) active[b * M + 1 + n + q] = 1;
|
|
456
498
|
}
|
|
457
499
|
model.interfaces.forEach((itf, f) => {
|
|
458
500
|
const bf = this.blockOfFace[f];
|
|
@@ -559,12 +601,14 @@ export class Solver {
|
|
|
559
601
|
// its faces; both are left to giant time steps.
|
|
560
602
|
this.constraints = [];
|
|
561
603
|
const conductor = (st) => materials[regions[st.regions[0]].material].conductor;
|
|
562
|
-
const add = (parts, rowStretch) => {
|
|
604
|
+
const add = (parts, rowStretch, surface = []) => {
|
|
563
605
|
const st = this.stretches[rowStretch];
|
|
564
|
-
|
|
606
|
+
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
607
|
+
for (const p of surface) nNodes += model.ports[this.surfCols[p.col][0]].nodes.length * model.ports[this.surfCols[p.col][0]].surface.length;
|
|
565
608
|
for (const p of parts) this.stretches[p.stretch].conserved = true;
|
|
566
609
|
this.constraints.push({
|
|
567
610
|
parts,
|
|
611
|
+
surface,
|
|
568
612
|
row: this.blockOfNode[st.nodes[0]] * this.M + 1 + st.species,
|
|
569
613
|
idx: new Int32Array(nNodes * this.M),
|
|
570
614
|
w: new Float64Array(nNodes * this.M),
|
|
@@ -576,12 +620,15 @@ export class Solver {
|
|
|
576
620
|
this.stretches.forEach((st, k) => {
|
|
577
621
|
if (st.spectator && st.mobile) add([{ stretch: k, w: 1 }], k);
|
|
578
622
|
});
|
|
623
|
+
const S = this.stretches.length;
|
|
579
624
|
for (const w of this.moieties) {
|
|
580
|
-
const parts = [...w.keys()].filter((k) => w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
581
|
-
|
|
625
|
+
const parts = [...w.keys()].filter((k) => k < S && w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
626
|
+
const surface = [...w.keys()].filter((k) => k >= S && w[k] !== 0).map((k) => ({ col: k - S, w: w[k] }));
|
|
627
|
+
if (parts.length === 0) continue; // (a surface's own: left to giant time steps)
|
|
628
|
+
if (parts.length === 1 && surface.length === 0 && this.stretches[parts[0].stretch].spectator) continue; // (above)
|
|
582
629
|
if (parts.some((p) => !this.stretches[p.stretch].mobile || conductor(this.stretches[p.stretch]))) continue;
|
|
583
630
|
// The row replaced: the basis vector's own stretch (its weight is 1, and no other vector has one).
|
|
584
|
-
add(parts, parts.find((p) => w[p.stretch] === 1)?.stretch ?? parts[0].stretch);
|
|
631
|
+
add(parts, parts.find((p) => w[p.stretch] === 1)?.stretch ?? parts[0].stretch, surface);
|
|
585
632
|
}
|
|
586
633
|
// A combination of immobile stretches (trap states: X⁰ + X⁻ under e⁻ + X⁰ = X⁻) is conserved
|
|
587
634
|
// node by node, since nothing carries it anywhere: at each node its weighted sum stays what
|
|
@@ -644,26 +691,32 @@ export class Solver {
|
|
|
644
691
|
// stoichiometry (a row per reaction, over the stretches it touches) together with a unit row
|
|
645
692
|
// for each stretch fed from outside. Exact integer data, so plain elimination will do.
|
|
646
693
|
_conservedMoieties() {
|
|
647
|
-
const { model, n } = this, S = this.stretches.length;
|
|
694
|
+
const { model, n } = this, S = this.stretches.length, C = S + this.surfCols.length;
|
|
648
695
|
const rows = [];
|
|
649
696
|
model.interfaces.forEach((itf, f) => {
|
|
650
697
|
for (const rx of itf.reactions) {
|
|
651
|
-
const row = new Float64Array(
|
|
698
|
+
const row = new Float64Array(C);
|
|
652
699
|
for (const p of rx.part) row[this.stretchOf[(f + p.side) * n + p.i]] += p.nu;
|
|
653
700
|
rows.push(row);
|
|
654
701
|
}
|
|
655
702
|
});
|
|
656
|
-
|
|
703
|
+
// An electrode surface's species are columns too, after the stretches (S + j for surface
|
|
704
|
+
// column j), so a combination that passes through them (A⁺ + e⁻ = S, S + e⁻ = B⁻) is conserved
|
|
705
|
+
// with what the surface holds.
|
|
706
|
+
model.ports.forEach((port, k) => {
|
|
657
707
|
for (const rx of port.reactions) {
|
|
658
|
-
const row = new Float64Array(
|
|
659
|
-
for (const p of rx.part)
|
|
708
|
+
const row = new Float64Array(C); // (the electrode's carrier is outside the device)
|
|
709
|
+
for (const p of rx.part) {
|
|
710
|
+
if (p.side === 0) row[this.stretchOf[port.region * n + p.i]] += p.nu;
|
|
711
|
+
else if (p.side === 2) row[S + this.surfColOf[k] + p.s] += p.nu;
|
|
712
|
+
}
|
|
660
713
|
rows.push(row);
|
|
661
714
|
}
|
|
662
|
-
}
|
|
715
|
+
});
|
|
663
716
|
model.regions.forEach((reg, q) => {
|
|
664
717
|
for (const rx of model.reactions) {
|
|
665
718
|
if (!(rx.kf[reg.material] > 0)) continue;
|
|
666
|
-
const row = new Float64Array(
|
|
719
|
+
const row = new Float64Array(C);
|
|
667
720
|
for (const p of rx.reactants) row[this.stretchOf[q * n + p.i]] -= p.nu;
|
|
668
721
|
for (const p of rx.products) row[this.stretchOf[q * n + p.i]] += p.nu;
|
|
669
722
|
rows.push(row);
|
|
@@ -671,34 +724,34 @@ export class Solver {
|
|
|
671
724
|
});
|
|
672
725
|
this.stretches.forEach((st, k) => {
|
|
673
726
|
if (!st.contactFed) return;
|
|
674
|
-
const row = new Float64Array(
|
|
727
|
+
const row = new Float64Array(C);
|
|
675
728
|
row[k] = 1;
|
|
676
729
|
rows.push(row);
|
|
677
730
|
});
|
|
678
731
|
// Reduced row echelon form; the free columns give the null space.
|
|
679
732
|
const pivots = [];
|
|
680
733
|
let r = 0;
|
|
681
|
-
for (let col = 0; col <
|
|
734
|
+
for (let col = 0; col < C && r < rows.length; col++) {
|
|
682
735
|
let best = r;
|
|
683
736
|
for (let i = r + 1; i < rows.length; i++) if (Math.abs(rows[i][col]) > Math.abs(rows[best][col])) best = i;
|
|
684
737
|
if (Math.abs(rows[best][col]) < 1e-9) continue;
|
|
685
738
|
[rows[r], rows[best]] = [rows[best], rows[r]];
|
|
686
739
|
const pr = rows[r], pv = pr[col];
|
|
687
|
-
for (let j = 0; j <
|
|
740
|
+
for (let j = 0; j < C; j++) pr[j] /= pv;
|
|
688
741
|
for (let i = 0; i < rows.length; i++) {
|
|
689
742
|
if (i === r || rows[i][col] === 0) continue;
|
|
690
743
|
const fct = rows[i][col];
|
|
691
|
-
for (let j = 0; j <
|
|
744
|
+
for (let j = 0; j < C; j++) rows[i][j] -= fct * pr[j];
|
|
692
745
|
}
|
|
693
746
|
pivots.push(col);
|
|
694
747
|
r++;
|
|
695
748
|
}
|
|
696
|
-
const isPivot = new Uint8Array(
|
|
749
|
+
const isPivot = new Uint8Array(C);
|
|
697
750
|
for (const col of pivots) isPivot[col] = 1;
|
|
698
751
|
const basis = [];
|
|
699
|
-
for (let free = 0; free <
|
|
752
|
+
for (let free = 0; free < C; free++) {
|
|
700
753
|
if (isPivot[free]) continue;
|
|
701
|
-
const w = new Float64Array(
|
|
754
|
+
const w = new Float64Array(C);
|
|
702
755
|
w[free] = 1;
|
|
703
756
|
pivots.forEach((col, i) => {
|
|
704
757
|
const v = -rows[i][free];
|
|
@@ -738,6 +791,19 @@ export class Solver {
|
|
|
738
791
|
}
|
|
739
792
|
}
|
|
740
793
|
}
|
|
794
|
+
// What the electrodes' surfaces hold of it: Γ Σ v·a·θ, with ∂θ_s/∂η_x = θ_s (δ_sx − θ_x).
|
|
795
|
+
for (const { col, w } of cs.surface) {
|
|
796
|
+
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity, ns = port.surface.length;
|
|
797
|
+
reference += w * this.surfaceRef[col];
|
|
798
|
+
port.nodes.forEach((g, j) => {
|
|
799
|
+
const f = w * G * vol[g] * this.portArea[k][j], b = this.blockOfNode[g], tq = this.th[g * this.nSurf + q];
|
|
800
|
+
amount += f * tq;
|
|
801
|
+
for (let x = 0; x < ns; x++) {
|
|
802
|
+
cs.idx[len] = this.rix[b * M + 1 + n + x];
|
|
803
|
+
cs.w[len++] = f * tq * ((q === x ? 1 : 0) - this.th[g * this.nSurf + x]);
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
}
|
|
741
807
|
cs.len = len;
|
|
742
808
|
cs.res = amount - reference;
|
|
743
809
|
const b0 = Math.floor(cs.row / M), r0 = cs.row % M;
|
|
@@ -816,6 +882,14 @@ export class Solver {
|
|
|
816
882
|
}
|
|
817
883
|
|
|
818
884
|
|
|
885
|
+
/** Amount (mol per unit area) of surface column j's species, Γ Σ v·a·θ, in the current state. */
|
|
886
|
+
surfaceAmount(j) {
|
|
887
|
+
const [k, q] = this.surfCols[j], port = this.model.ports[k], vol = this.model.grid.vol;
|
|
888
|
+
let s = 0;
|
|
889
|
+
port.nodes.forEach((g, w) => (s += vol[g] * this.portArea[k][w] * this.th[g * this.nSurf + q]));
|
|
890
|
+
return port.surface[0].capacity * s;
|
|
891
|
+
}
|
|
892
|
+
|
|
819
893
|
/** Total amount (mol per unit area) of a stretch's species in the current state. */
|
|
820
894
|
amount(stretch) {
|
|
821
895
|
const { n, c, model } = this;
|
|
@@ -1055,13 +1129,36 @@ export class Solver {
|
|
|
1055
1129
|
const shift = level(t.side) - level(this.terms[held].side);
|
|
1056
1130
|
if (Number.isFinite(shift)) this.termV[k] = this.termV[held] + shift;
|
|
1057
1131
|
}
|
|
1132
|
+
// Electrode surfaces at their starting coverages: η = μ°/RT + ln(θ/θ₀).
|
|
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
|
+
}
|
|
1058
1139
|
this.computeConcentrations();
|
|
1059
|
-
// A floating
|
|
1060
|
-
//
|
|
1061
|
-
//
|
|
1140
|
+
// A floating capacitance starts uncharged (σ = 0 at the window's mean φ), as the starting
|
|
1141
|
+
// composition, neutral without it, assumes; with reactions too, its double layer then charges
|
|
1142
|
+
// toward their mixed potential.
|
|
1062
1143
|
for (const k of this.floating) {
|
|
1063
1144
|
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1064
|
-
if (!port
|
|
1145
|
+
if (!port?.capacitance) continue;
|
|
1146
|
+
let w = 0, sum = 0;
|
|
1147
|
+
port.nodes.forEach((g, j) => {
|
|
1148
|
+
if (this.phiUndefined[g]) return;
|
|
1149
|
+
const a = this.model.grid.vol[g] * this.portArea[t.index][j];
|
|
1150
|
+
w += a;
|
|
1151
|
+
sum += a * this.VT * u[this.blockOfNode[g] * M];
|
|
1152
|
+
});
|
|
1153
|
+
if (w > 0) this.termV[k] = port.capacitance.zeroCharge + sum / w;
|
|
1154
|
+
}
|
|
1155
|
+
// A floating electrode spread through a port (without a double layer) starts where its
|
|
1156
|
+
// reactions pass the current it's set (none, behind a resistance): at its mixed potential in
|
|
1157
|
+
// the start's composition, not level with a held terminal, which can be volts away and pass
|
|
1158
|
+
// an absurd current.
|
|
1159
|
+
for (const k of this.floating) {
|
|
1160
|
+
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1161
|
+
if (!port || port.reactions.length === 0 || port.capacitance) continue;
|
|
1065
1162
|
const want = t.drive.kind === 'I' ? sourceAt(t.drive.src, this.time) : 0;
|
|
1066
1163
|
const current = (V) => {
|
|
1067
1164
|
this.termV[k] = V;
|
|
@@ -1088,18 +1185,19 @@ export class Solver {
|
|
|
1088
1185
|
|
|
1089
1186
|
// A port reaction's rate per area at node g (mol/(m²·s), forward), at the present state.
|
|
1090
1187
|
_portRate(port, rx, g) {
|
|
1091
|
-
const { n, M, u, uLo, c } = this, b = this.blockOfNode[g];
|
|
1188
|
+
const { n, M, u, uLo, c, nSurf, th } = this, b = this.blockOfNode[g], RT = this.model.RT;
|
|
1092
1189
|
let pref = rx.k0, a = rx.fixedA;
|
|
1093
1190
|
for (const p of rx.part) {
|
|
1094
1191
|
if (p.side === 1) {
|
|
1095
1192
|
a -= p.nu * this.portEta(port, p.i);
|
|
1096
1193
|
continue;
|
|
1097
1194
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1195
|
+
const o = b * M + (p.side === 2 ? 1 + n + p.s : 1 + p.i), e = p.nu < 0 ? -p.nu * (1 - rx.alpha) : p.nu * rx.alpha;
|
|
1196
|
+
a -= p.nu * (u[o] + uLo[o]);
|
|
1197
|
+
pref *= p.side === 2 ? Math.exp(e * (u[o] + uLo[o] - port.surface[p.s].mu0 / RT)) : powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
1100
1198
|
}
|
|
1101
|
-
|
|
1102
|
-
return pref *
|
|
1199
|
+
if (rx.bare) pref *= this.th0[g];
|
|
1200
|
+
return pref * bvFactor(a, rx.alpha).g;
|
|
1103
1201
|
}
|
|
1104
1202
|
|
|
1105
1203
|
// A non-ideal material's composition at φ̂ = ph: level-fixed species (mode 1) at their η,
|
|
@@ -1128,6 +1226,23 @@ export class Solver {
|
|
|
1128
1226
|
}
|
|
1129
1227
|
}
|
|
1130
1228
|
|
|
1229
|
+
// Each electrode surface's coverages from its η: Langmuir on shared sites, θ_s = e^{ζ_s}/(1 + Σ e^{ζ}),
|
|
1230
|
+
// ζ = η − μ°/RT (so θ_s/θ₀ = e^{ζ_s}), at every node of a surfaced window.
|
|
1231
|
+
_coverages() {
|
|
1232
|
+
const { n, M, u, uLo, nSurf, th } = this, RT = this.model.RT;
|
|
1233
|
+
for (let g = 0; g < this.nNodes; g++) {
|
|
1234
|
+
const k = this.surfPort[g];
|
|
1235
|
+
if (k < 0) continue;
|
|
1236
|
+
const surf = this.model.ports[k].surface, b = this.blockOfNode[g];
|
|
1237
|
+
let m = 0;
|
|
1238
|
+
for (let q = 0; q < surf.length; q++) m = Math.max(m, u[b * M + 1 + n + q] + uLo[b * M + 1 + n + q] - surf[q].mu0 / RT);
|
|
1239
|
+
let sum = Math.exp(-m);
|
|
1240
|
+
for (let q = 0; q < surf.length; q++) sum += (th[g * nSurf + q] = Math.exp(u[b * M + 1 + n + q] + uLo[b * M + 1 + n + q] - surf[q].mu0 / RT - m));
|
|
1241
|
+
for (let q = 0; q < surf.length; q++) th[g * nSurf + q] /= sum;
|
|
1242
|
+
this.th0[g] = Math.exp(-m) / sum;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1131
1246
|
computeConcentrations() {
|
|
1132
1247
|
const { n, M, u, uLo, c, z } = this;
|
|
1133
1248
|
for (let g = 0; g < this.nNodes; g++) {
|
|
@@ -1150,6 +1265,7 @@ export class Solver {
|
|
|
1150
1265
|
c[k] = this.present[k] ? this.cRef[k] * Math.exp(u[b * M + 1 + i] + uLo[b * M + 1 + i] - this.mu0hat[k] - z[i] * phiHat) : 0;
|
|
1151
1266
|
}
|
|
1152
1267
|
}
|
|
1268
|
+
this._coverages();
|
|
1153
1269
|
}
|
|
1154
1270
|
|
|
1155
1271
|
_work(md) {
|
|
@@ -1444,6 +1560,18 @@ export class Solver {
|
|
|
1444
1560
|
}
|
|
1445
1561
|
}
|
|
1446
1562
|
res[R[lb]] -= v * q;
|
|
1563
|
+
// The pivot's row (charge continuity) left storage out, the ions' net charge being constant
|
|
1564
|
+
// under neutrality. Against a capacitance it isn't: neutrality makes it −(aσ + ρ_fixed)/F,
|
|
1565
|
+
// so their storage is that less what the step started with, Σ z c_old, over dt (in units
|
|
1566
|
+
// of the pivot's balance).
|
|
1567
|
+
if (this.qgTerms[g].length > 0) {
|
|
1568
|
+
const f = -v / (FARADAY * z[k] * dt);
|
|
1569
|
+
let qOld = this.rhoFixed[g];
|
|
1570
|
+
for (let i = 0; i < n; i++) if (present[gn + i]) qOld += F * z[i] * cOld[gn + i];
|
|
1571
|
+
res[rk] += f * (this.qg[g] + qOld);
|
|
1572
|
+
B[offB[b] + lk * m + p] += -f * this.qgSlope[g] * this.VT;
|
|
1573
|
+
for (const [kq, w] of this.qgTerms[g]) termB[kq][rk] += f * this.portArea[kq - 2][w] * this.model.ports[kq - 2].capacitance.C;
|
|
1574
|
+
}
|
|
1447
1575
|
}
|
|
1448
1576
|
}
|
|
1449
1577
|
|
|
@@ -1969,7 +2097,10 @@ export class Solver {
|
|
|
1969
2097
|
this.termDI[k] = 0;
|
|
1970
2098
|
this.termI[k] = 0;
|
|
1971
2099
|
}
|
|
1972
|
-
this.
|
|
2100
|
+
this.qg.fill(0);
|
|
2101
|
+
this.qgSlope.fill(0);
|
|
2102
|
+
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt, false));
|
|
2103
|
+
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt, true));
|
|
1973
2104
|
this._contact('left', dt);
|
|
1974
2105
|
this._contact('right', dt);
|
|
1975
2106
|
for (const k of this.floating) this._circuit(k);
|
|
@@ -2001,16 +2132,18 @@ export class Solver {
|
|
|
2001
2132
|
|
|
2002
2133
|
// An internal port (terminal k): its exchange with each node of its window, as a source per
|
|
2003
2134
|
// volume. A held ('equilibrium') level replaces the node's balance row; the source is then that
|
|
2004
|
-
// row's residual, read just before. Its current into the device is Σ z F × the sources.
|
|
2005
|
-
|
|
2135
|
+
// row's residual, read just before. Its current into the device is Σ z F × the sources. In two
|
|
2136
|
+
// passes over the ports: everything else, then the held levels (`holds`), so that a level reads
|
|
2137
|
+
// every other port's terms at its nodes, and none is added into its row after.
|
|
2138
|
+
_port(port, flux, k, dt, holds) {
|
|
2006
2139
|
const R = this.rix;
|
|
2007
2140
|
const { n, M, u, uLo, res, z, VT } = this;
|
|
2008
2141
|
const F = FARADAY, vol = this.model.grid.vol;
|
|
2009
2142
|
const B = this.termB[k], C = this.termC[k];
|
|
2010
|
-
flux.fill(0);
|
|
2143
|
+
if (!holds) flux.fill(0);
|
|
2011
2144
|
for (let i = 0; i < n; i++) {
|
|
2012
2145
|
const link = port.species[i];
|
|
2013
|
-
if (link.type === 'blocked') continue;
|
|
2146
|
+
if (link.type === 'blocked' || (link.type === 'equilibrium') !== holds) continue;
|
|
2014
2147
|
const r = 1 + i, target = this.portEta(port, i), zF = z[i] * F;
|
|
2015
2148
|
for (const g of port.nodes) {
|
|
2016
2149
|
const b = this.blockOfNode[g], o = b * M + r, v = vol[g];
|
|
@@ -2037,8 +2170,13 @@ export class Solver {
|
|
|
2037
2170
|
}
|
|
2038
2171
|
}
|
|
2039
2172
|
}
|
|
2173
|
+
if (holds) {
|
|
2174
|
+
for (let i = 0; i < n; i++) this.termI[k] += z[i] * F * flux[i];
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2040
2177
|
port.reactions.forEach((rx, x) => this._portReaction(port, rx, this.portArea[k - 2], this.portRates[k - 2][x], flux, k));
|
|
2041
|
-
|
|
2178
|
+
if (port.capacitance) this._portCapacitance(port, k, dt);
|
|
2179
|
+
if (port.surface.length > 0 && Number.isFinite(dt)) this._surfaceStorage(port, dt);
|
|
2042
2180
|
}
|
|
2043
2181
|
|
|
2044
2182
|
// A port's reaction at each node of its window: Butler–Volmer per area, as at a face, against
|
|
@@ -2046,49 +2184,113 @@ export class Solver {
|
|
|
2046
2184
|
// volume. Each node's rate is explicit (no unknown of its own): it enters the balances of the
|
|
2047
2185
|
// species it makes and consumes, and the port's current.
|
|
2048
2186
|
_portReaction(port, rx, area, rates, flux, k) {
|
|
2049
|
-
const { n, M, u, uLo, c, res, z, VT } = this, R = this.rix, F = FARADAY, vol = this.model.grid.vol;
|
|
2050
|
-
const B = this.termB[k], C = this.termC[k], al = rx.alpha, d = this.dA;
|
|
2187
|
+
const { n, M, u, uLo, c, res, z, VT, nSurf, th } = this, R = this.rix, F = FARADAY, vol = this.model.grid.vol, RT = this.model.RT;
|
|
2188
|
+
const B = this.termB[k], C = this.termC[k], al = rx.alpha, d = this.dA, surf = port.surface;
|
|
2051
2189
|
// The carrier's part of the affinity, and how it moves with the port's voltage.
|
|
2052
2190
|
let aV = rx.fixedA, daV = 0, q = 0; // q: charge the forward reaction brings into the device, per event
|
|
2053
2191
|
for (const p of rx.part) {
|
|
2054
2192
|
if (p.side === 1) {
|
|
2055
2193
|
aV -= p.nu * this.portEta(port, p.i);
|
|
2056
2194
|
daV -= (p.nu * z[p.i]) / VT;
|
|
2057
|
-
} else q += p.nu * z[p.i];
|
|
2195
|
+
} else if (p.side === 0) q += p.nu * z[p.i];
|
|
2058
2196
|
}
|
|
2197
|
+
const slot = (p) => (p.side === 2 ? 1 + n + p.s : 1 + p.i);
|
|
2059
2198
|
port.nodes.forEach((g, w) => {
|
|
2060
2199
|
const b = this.blockOfNode[g], s = vol[g] * area[w];
|
|
2061
2200
|
d.fill(0);
|
|
2062
2201
|
let pref = rx.k0, aHi = aV, aLo = 0;
|
|
2063
2202
|
for (const p of rx.part) {
|
|
2064
2203
|
if (p.side === 1) continue;
|
|
2065
|
-
const o = b * M +
|
|
2204
|
+
const o = b * M + slot(p);
|
|
2066
2205
|
aHi -= p.nu * u[o];
|
|
2067
2206
|
aLo -= p.nu * uLo[o];
|
|
2068
2207
|
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
2069
|
-
|
|
2070
|
-
|
|
2208
|
+
if (p.side === 2) {
|
|
2209
|
+
// a surface species at activity θ/θ₀ = e^ζ
|
|
2210
|
+
pref *= Math.exp(e * (u[o] + uLo[o] - surf[p.s].mu0 / RT));
|
|
2211
|
+
d[1 + n + p.s] += e;
|
|
2212
|
+
} else {
|
|
2213
|
+
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
2214
|
+
this._dlnc(g, p.i, e, d);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
if (rx.bare) {
|
|
2218
|
+
// on bare metal only: the free fraction θ₀ = 1 − Σθ, with ∂ln θ₀/∂η_s = −θ_s
|
|
2219
|
+
for (let x = 0; x < surf.length; x++) d[1 + n + x] -= th[g * nSurf + x];
|
|
2220
|
+
pref *= this.th0[g];
|
|
2071
2221
|
}
|
|
2072
2222
|
const { g: bv, gp } = bvFactor(aHi + aLo, al);
|
|
2073
2223
|
const rate = pref * bv;
|
|
2074
2224
|
rates[w] = rate;
|
|
2075
|
-
// ∂rate/∂slot = rate·∂ln(prefactor) + pref·g′·∂a, with ∂a/∂η
|
|
2225
|
+
// ∂rate/∂slot = rate·∂ln(prefactor) + pref·g′·∂a, with ∂a/∂η = −ν
|
|
2076
2226
|
for (let t = 0; t < M; t++) d[t] *= rate;
|
|
2077
|
-
for (const p of rx.part) if (p.side
|
|
2227
|
+
for (const p of rx.part) if (p.side !== 1) d[slot(p)] -= pref * gp * p.nu;
|
|
2078
2228
|
const dV = pref * gp * daV;
|
|
2079
2229
|
for (const p of rx.part) {
|
|
2080
2230
|
if (p.side === 1) continue;
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2231
|
+
// the region's species per volume of the window; a surface's per area of electrode
|
|
2232
|
+
const row = slot(p), o = b * M + row, f = p.side === 2 ? 1 : s;
|
|
2233
|
+
res[R[o]] -= f * p.nu * rate;
|
|
2234
|
+
if (p.side === 0) flux[p.i] += s * p.nu * rate;
|
|
2235
|
+
for (let t = 0; t < M; t++) if (d[t] !== 0) this._j(b, row, b, t, -f * p.nu * d[t]);
|
|
2236
|
+
B[R[o]] -= f * p.nu * dV;
|
|
2086
2237
|
}
|
|
2087
2238
|
for (let t = 0; t < M; t++) if (d[t] !== 0) C[R[b * M + t]] += q * F * s * d[t];
|
|
2088
2239
|
this.termDI[k] += q * F * s * dV;
|
|
2089
2240
|
});
|
|
2090
2241
|
}
|
|
2091
2242
|
|
|
2243
|
+
// A capacitance spread through a port's window: per area of electrode, σ = C (V − zeroCharge − φ)
|
|
2244
|
+
// on the port's side, so each node's charge balance (its φ row) gains vol·a·σ, and the port
|
|
2245
|
+
// passes the charging current d(Σ vol·a·σ)/dt (none in a steady state). An end node whose φ its
|
|
2246
|
+
// contact sets is left to the contact.
|
|
2247
|
+
_portCapacitance(port, k, dt) {
|
|
2248
|
+
const { M, u, uLo, res, VT } = this, R = this.rix, vol = this.model.grid.vol, area = this.portArea[k - 2];
|
|
2249
|
+
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
2250
|
+
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
2251
|
+
let Q = 0, dQdV = 0;
|
|
2252
|
+
port.nodes.forEach((g, w) => {
|
|
2253
|
+
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
2254
|
+
const b = this.blockOfNode[g], s = vol[g] * area[w] * Cs;
|
|
2255
|
+
const q = s * (V - zeroCharge - VT * (u[b * M] + uLo[b * M]));
|
|
2256
|
+
this.qg[g] += area[w] * Cs * (V - zeroCharge - VT * (u[b * M] + uLo[b * M]));
|
|
2257
|
+
this.qgSlope[g] += area[w] * Cs;
|
|
2258
|
+
Q += q;
|
|
2259
|
+
dQdV += s;
|
|
2260
|
+
res[R[b * M]] -= q;
|
|
2261
|
+
this._j(b, 0, b, 0, s * VT);
|
|
2262
|
+
B[R[b * M]] -= s;
|
|
2263
|
+
if (Number.isFinite(dt)) Ct[R[b * M]] += (-s * VT) / dt;
|
|
2264
|
+
});
|
|
2265
|
+
this.portQ[k - 2] = Q;
|
|
2266
|
+
if (Number.isFinite(dt)) {
|
|
2267
|
+
this.termI[k] += (Q - this.portQStart[k - 2]) / dt;
|
|
2268
|
+
this.termDI[k] += dQdV / dt;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// An electrode surface's storage, per area of electrode: Γ (θ_s − θ_s,old)/dt in each surface
|
|
2273
|
+
// species' row, with ∂θ_s/∂η_x = θ_s (δ_sx − θ_x).
|
|
2274
|
+
_surfaceStorage(port, dt) {
|
|
2275
|
+
const { n, M, res, nSurf, th, thOld } = this, R = this.rix, surf = port.surface, G = surf[0].capacity;
|
|
2276
|
+
for (const g of port.nodes) {
|
|
2277
|
+
const b = this.blockOfNode[g];
|
|
2278
|
+
// Near full coverage θ − θ_old cancels (both ~1, the change ~θ₀): the dominant species'
|
|
2279
|
+
// change comes from the small ones instead, Δθ = −Δθ₀ − Σ_others Δθ.
|
|
2280
|
+
let top = 0;
|
|
2281
|
+
for (let q = 1; q < surf.length; q++) if (th[g * nSurf + q] > th[g * nSurf + top]) top = q;
|
|
2282
|
+
for (let q = 0; q < surf.length; q++) {
|
|
2283
|
+
const tq = th[g * nSurf + q];
|
|
2284
|
+
let change = tq - thOld[g * nSurf + q];
|
|
2285
|
+
if (q === top && tq > 0.5) {
|
|
2286
|
+
change = -(this.th0[g] - this.th0Old[g]);
|
|
2287
|
+
for (let x = 0; x < surf.length; x++) if (x !== q) change -= th[g * nSurf + x] - thOld[g * nSurf + x];
|
|
2288
|
+
}
|
|
2289
|
+
res[R[b * M + 1 + n + q]] += (G * change) / dt;
|
|
2290
|
+
for (let x = 0; x < surf.length; x++) this._j(b, 1 + n + q, b, 1 + n + x, ((G * tq) / dt) * ((q === x ? 1 : 0) - th[g * nSurf + x]));
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2092
2294
|
|
|
2093
2295
|
// One contact (terminal 0 or 1): record the flux through its outer face and the current into
|
|
2094
2296
|
// the device (read from the end box before anything here is added: whatever the box needs
|
|
@@ -2107,10 +2309,13 @@ export class Solver {
|
|
|
2107
2309
|
const Ac = model.grid.area[g]; // the links' laws are per area: what they pass is that times A here
|
|
2108
2310
|
const dyn = Number.isFinite(dt);
|
|
2109
2311
|
|
|
2110
|
-
// Before anything is added, each balance residual is the flux into the device here.
|
|
2312
|
+
// Before anything is added, each balance residual is the flux into the device here. (A
|
|
2313
|
+
// closed end solved in charge rows has its ions' storage left out until later: none of them
|
|
2314
|
+
// passes, so their flux is zero.)
|
|
2315
|
+
const closedIons = this.combining && dyn && this.chargeNode[g] === 1;
|
|
2111
2316
|
let I = 0;
|
|
2112
2317
|
for (let i = 0; i < n; i++) {
|
|
2113
|
-
const active = this.loc[b * M + 1 + i] >= 0;
|
|
2318
|
+
const active = this.loc[b * M + 1 + i] >= 0 && !(closedIons && z[i] !== 0);
|
|
2114
2319
|
const nIn = active ? res[R[b * M + 1 + i]] : 0;
|
|
2115
2320
|
flux[i] = sgn * nIn; // (toward +x)
|
|
2116
2321
|
if (active && z[i] !== 0) {
|
|
@@ -2245,6 +2450,8 @@ export class Solver {
|
|
|
2245
2450
|
for (let i = 0; i < n; i++) {
|
|
2246
2451
|
if (this.present[g * n + i]) mx = Math.max(mx, Math.abs(delta[R[b * M + 1 + i]]));
|
|
2247
2452
|
}
|
|
2453
|
+
const k = this.surfPort[g]; // an electrode surface's coverages, as η
|
|
2454
|
+
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]]));
|
|
2248
2455
|
}
|
|
2249
2456
|
return mx;
|
|
2250
2457
|
}
|
|
@@ -2401,14 +2608,15 @@ export class Solver {
|
|
|
2401
2608
|
this.uPrevLo.set(this.uLo);
|
|
2402
2609
|
this.termVPrev.set(this.termV);
|
|
2403
2610
|
this.computeConcentrations();
|
|
2404
|
-
const cN = Float64Array.from(this.c);
|
|
2611
|
+
const cN = Float64Array.from(this.c), thN = Float64Array.from(this.th), th0N = Float64Array.from(this.th0);
|
|
2405
2612
|
// Contact displacement before the step: as it was at the end of the previous step (under
|
|
2406
2613
|
// the parameters then), so a gate-voltage change shows up as displacement current.
|
|
2407
|
-
if (!this.contactDEnd) {
|
|
2614
|
+
if (!this.contactDEnd || !this.portQEnd) {
|
|
2408
2615
|
this._assembleBookkeeping(dt);
|
|
2409
2616
|
this.contactDEnd = { ...this.contactD };
|
|
2617
|
+
this.portQEnd = Float64Array.from(this.portQ);
|
|
2410
2618
|
}
|
|
2411
|
-
const DN = { ...this.contactDEnd };
|
|
2619
|
+
const DN = { ...this.contactDEnd }, QN = Float64Array.from(this.portQEnd);
|
|
2412
2620
|
if (opts.guess) {
|
|
2413
2621
|
// Start Newton from a predicted state (e.g. extrapolated from the history).
|
|
2414
2622
|
this.u.set(opts.guess);
|
|
@@ -2419,11 +2627,17 @@ export class Solver {
|
|
|
2419
2627
|
const a0 = (1 + 2 * w) / (1 + w), b1 = (1 + w) / a0, b2 = (w * w) / (1 + w) / a0;
|
|
2420
2628
|
const { cOld } = this;
|
|
2421
2629
|
for (let k = 0; k < cOld.length; k++) cOld[k] = b1 * cN[k] - b2 * prev.c[k];
|
|
2630
|
+
for (let k = 0; k < thN.length; k++) this.thOld[k] = b1 * thN[k] - b2 * prev.th[k];
|
|
2631
|
+
for (let k = 0; k < th0N.length; k++) this.th0Old[k] = b1 * th0N[k] - b2 * prev.th0[k];
|
|
2422
2632
|
this.contactDStart = { left: b1 * DN.left - b2 * prev.D.left, right: b1 * DN.right - b2 * prev.D.right };
|
|
2633
|
+
this.portQStart = QN.map((q, k) => b1 * q - b2 * prev.Q[k]);
|
|
2423
2634
|
dtEff = dt / a0;
|
|
2424
2635
|
} else {
|
|
2425
2636
|
this.cOld.set(cN);
|
|
2637
|
+
this.thOld.set(thN);
|
|
2638
|
+
this.th0Old.set(th0N);
|
|
2426
2639
|
this.contactDStart = DN;
|
|
2640
|
+
this.portQStart = QN;
|
|
2427
2641
|
}
|
|
2428
2642
|
// Implicit: sources at the step's end, as seen from within the step (before any jump there).
|
|
2429
2643
|
// A step landing on a breakpoint ends on it exactly.
|
|
@@ -2439,11 +2653,12 @@ export class Solver {
|
|
|
2439
2653
|
}
|
|
2440
2654
|
result.bdf = bdf;
|
|
2441
2655
|
if (result.converged) {
|
|
2442
|
-
this.history.unshift({ t: this.time, dt, u: Float64Array.from(this.uPrev), c: cN, D: DN });
|
|
2656
|
+
this.history.unshift({ t: this.time, dt, u: Float64Array.from(this.uPrev), c: cN, th: thN, th0: th0N, D: DN, Q: QN });
|
|
2443
2657
|
if (this.history.length > 3) this.history.length = 3;
|
|
2444
2658
|
this.time = tEnd;
|
|
2445
2659
|
this.lastDt = dtEff;
|
|
2446
2660
|
this.contactDOld = this.contactDStart;
|
|
2661
|
+
this.portQOld = this.portQStart;
|
|
2447
2662
|
this._accumulateBoundaryIntake(dtEff, cN);
|
|
2448
2663
|
} else {
|
|
2449
2664
|
this.u.set(this.uPrev);
|
|
@@ -2459,7 +2674,7 @@ export class Solver {
|
|
|
2459
2674
|
/**
|
|
2460
2675
|
* Adaptive time stepping to tEnd: variable-step BDF2 (or backward Euler), with the local error
|
|
2461
2676
|
* estimated against an explicit predictor through the previous states, and controlled to
|
|
2462
|
-
* `tol` thermal units per step in every potential (φ̂, each η, a
|
|
2677
|
+
* `tol` thermal units per step in every potential (φ̂, each η, a surface's coverages as η). The first
|
|
2463
2678
|
* step is checked by step doubling. Stops early when `budgetMs` of wall time is used, so an
|
|
2464
2679
|
* animation can call it once per frame; the step size carries over between calls.
|
|
2465
2680
|
*/
|
|
@@ -2539,7 +2754,8 @@ export class Solver {
|
|
|
2539
2754
|
continue;
|
|
2540
2755
|
}
|
|
2541
2756
|
} else {
|
|
2542
|
-
|
|
2757
|
+
// (Order 2 once three consistent states can check it.)
|
|
2758
|
+
const r = this.step(h, { method: this.history.length >= 2 ? method : 'be', guess: this._extrapolate(guess, this.time + h) });
|
|
2543
2759
|
iterations += r.iterations;
|
|
2544
2760
|
if (!r.converged) {
|
|
2545
2761
|
rejected++;
|
|
@@ -2559,6 +2775,11 @@ export class Solver {
|
|
|
2559
2775
|
}
|
|
2560
2776
|
steps++;
|
|
2561
2777
|
grow = 0;
|
|
2778
|
+
// The state the first step started from needn't satisfy the algebraic equations (φ in a
|
|
2779
|
+
// strictly neutral region, an interface's unknowns, after a start or a jump), and they
|
|
2780
|
+
// jump in the first instant; a predictor through it would see that jump as error on every
|
|
2781
|
+
// step after. The history starts from the half step.
|
|
2782
|
+
if (half) this.history.length = 1;
|
|
2562
2783
|
dt = clamped ? Math.max(dt, h * factor(err, p)) : h * factor(err, p);
|
|
2563
2784
|
if (atBreak) this.history = []; // the solution's slope jumps here: restart the order
|
|
2564
2785
|
if (half) {
|
|
@@ -2685,6 +2906,8 @@ export class Solver {
|
|
|
2685
2906
|
for (let i = 0; i < n; i++) {
|
|
2686
2907
|
if (this.present[g * n + i]) mx = Math.max(mx, Math.abs(u[b * M + 1 + i] - ref[b * M + 1 + i]));
|
|
2687
2908
|
}
|
|
2909
|
+
const k = this.surfPort[g]; // an electrode surface's coverages, as η
|
|
2910
|
+
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]));
|
|
2688
2911
|
}
|
|
2689
2912
|
return mx;
|
|
2690
2913
|
}
|
|
@@ -2727,12 +2950,17 @@ export class Solver {
|
|
|
2727
2950
|
u: Float64Array.from(this.u),
|
|
2728
2951
|
uLo: Float64Array.from(this.uLo),
|
|
2729
2952
|
cOld: Float64Array.from(this.cOld),
|
|
2953
|
+
thOld: Float64Array.from(this.thOld),
|
|
2954
|
+
th0Old: Float64Array.from(this.th0Old),
|
|
2730
2955
|
time: this.time,
|
|
2731
2956
|
lastDt: this.lastDt,
|
|
2732
2957
|
atSteady: this.atSteady,
|
|
2733
2958
|
contactDStart: this.contactDStart,
|
|
2734
2959
|
contactDOld: this.contactDOld,
|
|
2735
2960
|
contactDEnd: this.contactDEnd,
|
|
2961
|
+
portQStart: this.portQStart,
|
|
2962
|
+
portQOld: this.portQOld,
|
|
2963
|
+
portQEnd: this.portQEnd,
|
|
2736
2964
|
boundaryIntake: Float64Array.from(this.boundaryIntake),
|
|
2737
2965
|
history: this.history.slice(),
|
|
2738
2966
|
termV: Float64Array.from(this.termV),
|
|
@@ -2744,12 +2972,17 @@ export class Solver {
|
|
|
2744
2972
|
this.u.set(s.u);
|
|
2745
2973
|
this.uLo.set(s.uLo);
|
|
2746
2974
|
this.cOld.set(s.cOld);
|
|
2975
|
+
this.thOld.set(s.thOld);
|
|
2976
|
+
this.th0Old.set(s.th0Old);
|
|
2747
2977
|
this.time = s.time;
|
|
2748
2978
|
this.lastDt = s.lastDt;
|
|
2749
2979
|
this.atSteady = s.atSteady;
|
|
2750
2980
|
this.contactDStart = s.contactDStart;
|
|
2751
2981
|
this.contactDOld = s.contactDOld;
|
|
2752
2982
|
this.contactDEnd = s.contactDEnd;
|
|
2983
|
+
this.portQStart = s.portQStart;
|
|
2984
|
+
this.portQOld = s.portQOld;
|
|
2985
|
+
this.portQEnd = s.portQEnd;
|
|
2753
2986
|
this.boundaryIntake.set(s.boundaryIntake);
|
|
2754
2987
|
this.history = s.history.slice();
|
|
2755
2988
|
this.termV.set(s.termV);
|
|
@@ -2786,8 +3019,11 @@ export class Solver {
|
|
|
2786
3019
|
this.sourceTime = this.time;
|
|
2787
3020
|
this.computeConcentrations();
|
|
2788
3021
|
this.cOld.set(this.c);
|
|
3022
|
+
this.thOld.set(this.th);
|
|
3023
|
+
this.th0Old.set(this.th0);
|
|
2789
3024
|
this.assemble(Infinity);
|
|
2790
3025
|
this.contactDStart = { ...this.contactD };
|
|
3026
|
+
this.portQStart = Float64Array.from(this.portQ);
|
|
2791
3027
|
this.assemble(Infinity);
|
|
2792
3028
|
// The steady parts, then the storage parts from an assembly at a tiny dt.
|
|
2793
3029
|
const J = { A: sys.A.slice(), B: sys.B.slice(), C: sys.C.slice() };
|
|
@@ -3075,6 +3311,7 @@ export class Solver {
|
|
|
3075
3311
|
_accumulateBoundaryIntake(dt, cN) {
|
|
3076
3312
|
this._assembleBookkeeping(dt);
|
|
3077
3313
|
this.contactDEnd = { ...this.contactD };
|
|
3314
|
+
this.portQEnd = Float64Array.from(this.portQ);
|
|
3078
3315
|
if (!Number.isFinite(dt)) return;
|
|
3079
3316
|
const last = this.model.regions.length - 1;
|
|
3080
3317
|
const { n, cOld } = this, vol = this.model.grid.vol;
|
|
@@ -3125,6 +3362,18 @@ export class Solver {
|
|
|
3125
3362
|
// Steady state from the present one; `atSteady` says whether the state is one (until a step or
|
|
3126
3363
|
// a change of drive).
|
|
3127
3364
|
solveSteady(opts = {}) {
|
|
3365
|
+
// A terminal that passes current only by charging (a gate, a capacitance with no species
|
|
3366
|
+
// through it) has no steady state under a current drive: it charges for ever, or at I = 0
|
|
3367
|
+
// keeps whatever charge it started with, which a steady solve doesn't know.
|
|
3368
|
+
const { species } = this.model, ions = (links) => links.some((l, i) => l.type !== 'blocked' && species[i].z !== 0);
|
|
3369
|
+
for (const k of this.floating) {
|
|
3370
|
+
const t = this.terms[k];
|
|
3371
|
+
if (t.drive.kind !== 'I') continue;
|
|
3372
|
+
const only = t.kind === 'port' ? (p) => p.reactions.length === 0 && !ions(p.species) : (c) => !ions(c.species);
|
|
3373
|
+
if (only(t.kind === 'port' ? this.model.ports[t.index] : this.model.contacts[t.side])) {
|
|
3374
|
+
throw new DeviceError(`${t.kind === 'port' ? `ports[${t.index}]` : `contacts.${t.side}`} passes current only by charging, so driven by a current it has no steady state; hold it at V (or a source behind R), or advance() in time`);
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3128
3377
|
const r = this._steadyFromHere(opts);
|
|
3129
3378
|
this.atSteady = r.converged;
|
|
3130
3379
|
return r;
|