driftlet 0.9.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 +3 -3
- package/docs/device.md +118 -90
- package/docs/kit.md +5 -0
- 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 +15 -9
- package/src/device.js +16 -5
- package/src/index.js +17 -4
- package/src/polarization.js +51 -16
- package/src/solution.js +21 -9
- package/src/solver.js +151 -40
- package/src/types.js +4 -0
- 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 +16 -4
- package/types/types.d.ts +6 -0
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.
|
|
@@ -224,10 +226,14 @@ export class Solver {
|
|
|
224
226
|
model.ports.forEach((port, k) => {
|
|
225
227
|
if (port.surface.length > 0) port.nodes.forEach((g, w) => ((this.surfPort[g] = k), (this.surfW[g] = w)));
|
|
226
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));
|
|
227
232
|
this.th = new Float64Array(nNodes * nSurf);
|
|
228
233
|
this.thOld = new Float64Array(nNodes * nSurf);
|
|
229
|
-
|
|
230
|
-
this.
|
|
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);
|
|
231
237
|
|
|
232
238
|
// Per-node material data, flattened [g·n + i].
|
|
233
239
|
this.present = new Uint8Array(nNodes * n);
|
|
@@ -348,6 +354,13 @@ export class Solver {
|
|
|
348
354
|
this.portQStart = new Float64Array(model.ports.length);
|
|
349
355
|
this.portQOld = new Float64Array(model.ports.length);
|
|
350
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])));
|
|
351
364
|
this.contactDOld = { left: 0, right: 0 };
|
|
352
365
|
this.contactDStart = { left: 0, right: 0 };
|
|
353
366
|
// Terminals (the two contacts, then the ports): each one's voltage (V), held by its source or
|
|
@@ -428,11 +441,15 @@ export class Solver {
|
|
|
428
441
|
// A port that only exchanges (an O₂ supply, a leak) is solved there too: left out, its window
|
|
429
442
|
// fell back on the ill-conditioned terms, and a closed neutral electrolyte whose potential is
|
|
430
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.
|
|
431
445
|
const held = new Uint8Array(nNodes);
|
|
432
|
-
|
|
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;
|
|
433
450
|
for (const port of model.ports) if (port.species.some((l) => l.type === 'equilibrium')) for (const g of port.nodes) held[g] = 1;
|
|
434
451
|
this.chargeNode = new Uint8Array(nNodes);
|
|
435
|
-
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');
|
|
436
453
|
regions.forEach((reg, r) => {
|
|
437
454
|
const mat = materials[reg.material];
|
|
438
455
|
if (mat.conductor || mat.epsr !== 0) return;
|
|
@@ -456,6 +473,8 @@ export class Solver {
|
|
|
456
473
|
this._findStretches();
|
|
457
474
|
this.initFromComposition();
|
|
458
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));
|
|
459
478
|
// ∫ (flux in − flux out) dt through the contacts, per stretch, since the reference.
|
|
460
479
|
this.boundaryIntake = new Float64Array(this.stretches.length);
|
|
461
480
|
}
|
|
@@ -582,12 +601,14 @@ export class Solver {
|
|
|
582
601
|
// its faces; both are left to giant time steps.
|
|
583
602
|
this.constraints = [];
|
|
584
603
|
const conductor = (st) => materials[regions[st.regions[0]].material].conductor;
|
|
585
|
-
const add = (parts, rowStretch) => {
|
|
604
|
+
const add = (parts, rowStretch, surface = []) => {
|
|
586
605
|
const st = this.stretches[rowStretch];
|
|
587
|
-
|
|
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;
|
|
588
608
|
for (const p of parts) this.stretches[p.stretch].conserved = true;
|
|
589
609
|
this.constraints.push({
|
|
590
610
|
parts,
|
|
611
|
+
surface,
|
|
591
612
|
row: this.blockOfNode[st.nodes[0]] * this.M + 1 + st.species,
|
|
592
613
|
idx: new Int32Array(nNodes * this.M),
|
|
593
614
|
w: new Float64Array(nNodes * this.M),
|
|
@@ -599,12 +620,15 @@ export class Solver {
|
|
|
599
620
|
this.stretches.forEach((st, k) => {
|
|
600
621
|
if (st.spectator && st.mobile) add([{ stretch: k, w: 1 }], k);
|
|
601
622
|
});
|
|
623
|
+
const S = this.stretches.length;
|
|
602
624
|
for (const w of this.moieties) {
|
|
603
|
-
const parts = [...w.keys()].filter((k) => w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
604
|
-
|
|
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)
|
|
605
629
|
if (parts.some((p) => !this.stretches[p.stretch].mobile || conductor(this.stretches[p.stretch]))) continue;
|
|
606
630
|
// The row replaced: the basis vector's own stretch (its weight is 1, and no other vector has one).
|
|
607
|
-
add(parts, parts.find((p) => w[p.stretch] === 1)?.stretch ?? parts[0].stretch);
|
|
631
|
+
add(parts, parts.find((p) => w[p.stretch] === 1)?.stretch ?? parts[0].stretch, surface);
|
|
608
632
|
}
|
|
609
633
|
// A combination of immobile stretches (trap states: X⁰ + X⁻ under e⁻ + X⁰ = X⁻) is conserved
|
|
610
634
|
// node by node, since nothing carries it anywhere: at each node its weighted sum stays what
|
|
@@ -667,26 +691,32 @@ export class Solver {
|
|
|
667
691
|
// stoichiometry (a row per reaction, over the stretches it touches) together with a unit row
|
|
668
692
|
// for each stretch fed from outside. Exact integer data, so plain elimination will do.
|
|
669
693
|
_conservedMoieties() {
|
|
670
|
-
const { model, n } = this, S = this.stretches.length;
|
|
694
|
+
const { model, n } = this, S = this.stretches.length, C = S + this.surfCols.length;
|
|
671
695
|
const rows = [];
|
|
672
696
|
model.interfaces.forEach((itf, f) => {
|
|
673
697
|
for (const rx of itf.reactions) {
|
|
674
|
-
const row = new Float64Array(
|
|
698
|
+
const row = new Float64Array(C);
|
|
675
699
|
for (const p of rx.part) row[this.stretchOf[(f + p.side) * n + p.i]] += p.nu;
|
|
676
700
|
rows.push(row);
|
|
677
701
|
}
|
|
678
702
|
});
|
|
679
|
-
|
|
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) => {
|
|
680
707
|
for (const rx of port.reactions) {
|
|
681
|
-
const row = new Float64Array(
|
|
682
|
-
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
|
+
}
|
|
683
713
|
rows.push(row);
|
|
684
714
|
}
|
|
685
|
-
}
|
|
715
|
+
});
|
|
686
716
|
model.regions.forEach((reg, q) => {
|
|
687
717
|
for (const rx of model.reactions) {
|
|
688
718
|
if (!(rx.kf[reg.material] > 0)) continue;
|
|
689
|
-
const row = new Float64Array(
|
|
719
|
+
const row = new Float64Array(C);
|
|
690
720
|
for (const p of rx.reactants) row[this.stretchOf[q * n + p.i]] -= p.nu;
|
|
691
721
|
for (const p of rx.products) row[this.stretchOf[q * n + p.i]] += p.nu;
|
|
692
722
|
rows.push(row);
|
|
@@ -694,34 +724,34 @@ export class Solver {
|
|
|
694
724
|
});
|
|
695
725
|
this.stretches.forEach((st, k) => {
|
|
696
726
|
if (!st.contactFed) return;
|
|
697
|
-
const row = new Float64Array(
|
|
727
|
+
const row = new Float64Array(C);
|
|
698
728
|
row[k] = 1;
|
|
699
729
|
rows.push(row);
|
|
700
730
|
});
|
|
701
731
|
// Reduced row echelon form; the free columns give the null space.
|
|
702
732
|
const pivots = [];
|
|
703
733
|
let r = 0;
|
|
704
|
-
for (let col = 0; col <
|
|
734
|
+
for (let col = 0; col < C && r < rows.length; col++) {
|
|
705
735
|
let best = r;
|
|
706
736
|
for (let i = r + 1; i < rows.length; i++) if (Math.abs(rows[i][col]) > Math.abs(rows[best][col])) best = i;
|
|
707
737
|
if (Math.abs(rows[best][col]) < 1e-9) continue;
|
|
708
738
|
[rows[r], rows[best]] = [rows[best], rows[r]];
|
|
709
739
|
const pr = rows[r], pv = pr[col];
|
|
710
|
-
for (let j = 0; j <
|
|
740
|
+
for (let j = 0; j < C; j++) pr[j] /= pv;
|
|
711
741
|
for (let i = 0; i < rows.length; i++) {
|
|
712
742
|
if (i === r || rows[i][col] === 0) continue;
|
|
713
743
|
const fct = rows[i][col];
|
|
714
|
-
for (let j = 0; j <
|
|
744
|
+
for (let j = 0; j < C; j++) rows[i][j] -= fct * pr[j];
|
|
715
745
|
}
|
|
716
746
|
pivots.push(col);
|
|
717
747
|
r++;
|
|
718
748
|
}
|
|
719
|
-
const isPivot = new Uint8Array(
|
|
749
|
+
const isPivot = new Uint8Array(C);
|
|
720
750
|
for (const col of pivots) isPivot[col] = 1;
|
|
721
751
|
const basis = [];
|
|
722
|
-
for (let free = 0; free <
|
|
752
|
+
for (let free = 0; free < C; free++) {
|
|
723
753
|
if (isPivot[free]) continue;
|
|
724
|
-
const w = new Float64Array(
|
|
754
|
+
const w = new Float64Array(C);
|
|
725
755
|
w[free] = 1;
|
|
726
756
|
pivots.forEach((col, i) => {
|
|
727
757
|
const v = -rows[i][free];
|
|
@@ -761,6 +791,19 @@ export class Solver {
|
|
|
761
791
|
}
|
|
762
792
|
}
|
|
763
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
|
+
}
|
|
764
807
|
cs.len = len;
|
|
765
808
|
cs.res = amount - reference;
|
|
766
809
|
const b0 = Math.floor(cs.row / M), r0 = cs.row % M;
|
|
@@ -839,6 +882,14 @@ export class Solver {
|
|
|
839
882
|
}
|
|
840
883
|
|
|
841
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
|
+
|
|
842
893
|
/** Total amount (mol per unit area) of a stretch's species in the current state. */
|
|
843
894
|
amount(stretch) {
|
|
844
895
|
const { n, c, model } = this;
|
|
@@ -1086,12 +1137,28 @@ export class Solver {
|
|
|
1086
1137
|
surf.forEach((sp, q) => (u[b * M + 1 + n + q] = sp.mu0 / model.RT + Math.log(sp.theta0 / bare)));
|
|
1087
1138
|
}
|
|
1088
1139
|
this.computeConcentrations();
|
|
1089
|
-
// A floating
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
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.
|
|
1092
1143
|
for (const k of this.floating) {
|
|
1093
1144
|
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1094
|
-
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;
|
|
1095
1162
|
const want = t.drive.kind === 'I' ? sourceAt(t.drive.src, this.time) : 0;
|
|
1096
1163
|
const current = (V) => {
|
|
1097
1164
|
this.termV[k] = V;
|
|
@@ -1130,8 +1197,7 @@ export class Solver {
|
|
|
1130
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);
|
|
1131
1198
|
}
|
|
1132
1199
|
if (rx.bare) pref *= this.th0[g];
|
|
1133
|
-
|
|
1134
|
-
return pref * (Math.abs(a) > 50 ? Math.exp(rx.alpha * a) - Math.exp(-(1 - rx.alpha) * a) : bvFactor(a, rx.alpha).g);
|
|
1200
|
+
return pref * bvFactor(a, rx.alpha).g;
|
|
1135
1201
|
}
|
|
1136
1202
|
|
|
1137
1203
|
// A non-ideal material's composition at φ̂ = ph: level-fixed species (mode 1) at their η,
|
|
@@ -1494,6 +1560,18 @@ export class Solver {
|
|
|
1494
1560
|
}
|
|
1495
1561
|
}
|
|
1496
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
|
+
}
|
|
1497
1575
|
}
|
|
1498
1576
|
}
|
|
1499
1577
|
|
|
@@ -2019,7 +2097,10 @@ export class Solver {
|
|
|
2019
2097
|
this.termDI[k] = 0;
|
|
2020
2098
|
this.termI[k] = 0;
|
|
2021
2099
|
}
|
|
2022
|
-
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));
|
|
2023
2104
|
this._contact('left', dt);
|
|
2024
2105
|
this._contact('right', dt);
|
|
2025
2106
|
for (const k of this.floating) this._circuit(k);
|
|
@@ -2051,16 +2132,18 @@ export class Solver {
|
|
|
2051
2132
|
|
|
2052
2133
|
// An internal port (terminal k): its exchange with each node of its window, as a source per
|
|
2053
2134
|
// volume. A held ('equilibrium') level replaces the node's balance row; the source is then that
|
|
2054
|
-
// row's residual, read just before. Its current into the device is Σ z F × the sources.
|
|
2055
|
-
|
|
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) {
|
|
2056
2139
|
const R = this.rix;
|
|
2057
2140
|
const { n, M, u, uLo, res, z, VT } = this;
|
|
2058
2141
|
const F = FARADAY, vol = this.model.grid.vol;
|
|
2059
2142
|
const B = this.termB[k], C = this.termC[k];
|
|
2060
|
-
flux.fill(0);
|
|
2143
|
+
if (!holds) flux.fill(0);
|
|
2061
2144
|
for (let i = 0; i < n; i++) {
|
|
2062
2145
|
const link = port.species[i];
|
|
2063
|
-
if (link.type === 'blocked') continue;
|
|
2146
|
+
if (link.type === 'blocked' || (link.type === 'equilibrium') !== holds) continue;
|
|
2064
2147
|
const r = 1 + i, target = this.portEta(port, i), zF = z[i] * F;
|
|
2065
2148
|
for (const g of port.nodes) {
|
|
2066
2149
|
const b = this.blockOfNode[g], o = b * M + r, v = vol[g];
|
|
@@ -2087,10 +2170,13 @@ export class Solver {
|
|
|
2087
2170
|
}
|
|
2088
2171
|
}
|
|
2089
2172
|
}
|
|
2173
|
+
if (holds) {
|
|
2174
|
+
for (let i = 0; i < n; i++) this.termI[k] += z[i] * F * flux[i];
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2090
2177
|
port.reactions.forEach((rx, x) => this._portReaction(port, rx, this.portArea[k - 2], this.portRates[k - 2][x], flux, k));
|
|
2091
2178
|
if (port.capacitance) this._portCapacitance(port, k, dt);
|
|
2092
2179
|
if (port.surface.length > 0 && Number.isFinite(dt)) this._surfaceStorage(port, dt);
|
|
2093
|
-
for (let i = 0; i < n; i++) this.termI[k] += z[i] * F * flux[i];
|
|
2094
2180
|
}
|
|
2095
2181
|
|
|
2096
2182
|
// A port's reaction at each node of its window: Butler–Volmer per area, as at a face, against
|
|
@@ -2167,6 +2253,8 @@ export class Solver {
|
|
|
2167
2253
|
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
2168
2254
|
const b = this.blockOfNode[g], s = vol[g] * area[w] * Cs;
|
|
2169
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;
|
|
2170
2258
|
Q += q;
|
|
2171
2259
|
dQdV += s;
|
|
2172
2260
|
res[R[b * M]] -= q;
|
|
@@ -2221,10 +2309,13 @@ export class Solver {
|
|
|
2221
2309
|
const Ac = model.grid.area[g]; // the links' laws are per area: what they pass is that times A here
|
|
2222
2310
|
const dyn = Number.isFinite(dt);
|
|
2223
2311
|
|
|
2224
|
-
// 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;
|
|
2225
2316
|
let I = 0;
|
|
2226
2317
|
for (let i = 0; i < n; i++) {
|
|
2227
|
-
const active = this.loc[b * M + 1 + i] >= 0;
|
|
2318
|
+
const active = this.loc[b * M + 1 + i] >= 0 && !(closedIons && z[i] !== 0);
|
|
2228
2319
|
const nIn = active ? res[R[b * M + 1 + i]] : 0;
|
|
2229
2320
|
flux[i] = sgn * nIn; // (toward +x)
|
|
2230
2321
|
if (active && z[i] !== 0) {
|
|
@@ -2583,7 +2674,7 @@ export class Solver {
|
|
|
2583
2674
|
/**
|
|
2584
2675
|
* Adaptive time stepping to tEnd: variable-step BDF2 (or backward Euler), with the local error
|
|
2585
2676
|
* estimated against an explicit predictor through the previous states, and controlled to
|
|
2586
|
-
* `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
|
|
2587
2678
|
* step is checked by step doubling. Stops early when `budgetMs` of wall time is used, so an
|
|
2588
2679
|
* animation can call it once per frame; the step size carries over between calls.
|
|
2589
2680
|
*/
|
|
@@ -2663,7 +2754,8 @@ export class Solver {
|
|
|
2663
2754
|
continue;
|
|
2664
2755
|
}
|
|
2665
2756
|
} else {
|
|
2666
|
-
|
|
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) });
|
|
2667
2759
|
iterations += r.iterations;
|
|
2668
2760
|
if (!r.converged) {
|
|
2669
2761
|
rejected++;
|
|
@@ -2683,6 +2775,11 @@ export class Solver {
|
|
|
2683
2775
|
}
|
|
2684
2776
|
steps++;
|
|
2685
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;
|
|
2686
2783
|
dt = clamped ? Math.max(dt, h * factor(err, p)) : h * factor(err, p);
|
|
2687
2784
|
if (atBreak) this.history = []; // the solution's slope jumps here: restart the order
|
|
2688
2785
|
if (half) {
|
|
@@ -2809,6 +2906,8 @@ export class Solver {
|
|
|
2809
2906
|
for (let i = 0; i < n; i++) {
|
|
2810
2907
|
if (this.present[g * n + i]) mx = Math.max(mx, Math.abs(u[b * M + 1 + i] - ref[b * M + 1 + i]));
|
|
2811
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]));
|
|
2812
2911
|
}
|
|
2813
2912
|
return mx;
|
|
2814
2913
|
}
|
|
@@ -3263,6 +3362,18 @@ export class Solver {
|
|
|
3263
3362
|
// Steady state from the present one; `atSteady` says whether the state is one (until a step or
|
|
3264
3363
|
// a change of drive).
|
|
3265
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
|
+
}
|
|
3266
3377
|
const r = this._steadyFromHere(opts);
|
|
3267
3378
|
this.atSteady = r.converged;
|
|
3268
3379
|
return r;
|
package/src/types.js
CHANGED
|
@@ -197,6 +197,10 @@
|
|
|
197
197
|
*/
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
|
+
* The device's totals through its cross-section (currents, fluxes through contacts and ports,
|
|
201
|
+
* charges, amounts, reaction totals, impedance) are per m² of a planar device, as below, and
|
|
202
|
+
* whole (A, mol/s, C, mol, Ω; per metre of a cylinder) for one with a `geometry`. Rates at a
|
|
203
|
+
* face or an electrode are always per m² of it.
|
|
200
204
|
* @typedef {object} Solution
|
|
201
205
|
* @property {Float64Array} x node positions, m (interface positions appear twice)
|
|
202
206
|
* @property {Int32Array} region region index per node
|
package/types/checks.d.ts
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* - **warnings**: the solution's warnings (unresolved double layers, steep profiles, weakly held
|
|
14
14
|
* parts) and the definition's likely unit slips.
|
|
15
15
|
* - **balance** (a steady state): each species' ledger, what comes in through each terminal and
|
|
16
|
-
* what each reaction (bulk or at a face) makes or uses, in mol/(m²·s)
|
|
17
|
-
* species, as a current F·|z|·rate. The terms must sum to
|
|
18
|
-
* and it says where every carrier went.
|
|
16
|
+
* what each reaction (bulk or at a face) makes or uses, in mol/(m²·s) (mol/s if the device
|
|
17
|
+
* isn't planar) and, for a charged species, as a current F·|z|·rate. The terms must sum to
|
|
18
|
+
* zero; under light it's J = F(G − R), and it says where every carrier went.
|
|
19
19
|
* - **conservation** (a transient): every closed, unreacting stretch keeps what it held plus
|
|
20
20
|
* what came in.
|
|
21
21
|
* - **grid** (a steady state, with `refine`, the default): the device solved again on a grid
|
package/types/index.d.ts
CHANGED
package/types/live.d.ts
CHANGED
package/types/polarization.d.ts
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* @param {{ face: number } | { port: string | number, x?: number }} where a face's index, or a port
|
|
10
10
|
* (name or index) and the position in its window (m, the device's x; default its first node)
|
|
11
11
|
* @param {number | ArrayLike<number>} V the metal's level(s), V
|
|
12
|
-
* @
|
|
12
|
+
* @param {{ surface?: 'held' | 'equilibrium' }} [opts] an electrode port's surface coverages: held as
|
|
13
|
+
* in `sol` (the default: this instant's curves), or in equilibrium at each V (the steady-state
|
|
14
|
+
* curve, with a passive film's active–passive peak), each species by the reaction that makes it
|
|
15
|
+
* @returns {{ V: Float64Array, current: Float64Array, bare?: Float64Array, reactions: { equation: string, level: number,
|
|
13
16
|
* rate: Float64Array, current: Float64Array }[] }} per reaction: its equilibrium level here (where
|
|
14
17
|
* its rate is zero), its rate (mol/(m²·s) of electrode, forward) and its current (A/m², anodic
|
|
15
18
|
* positive: into the solution); and their sum
|
|
@@ -19,9 +22,12 @@ export function polarization(device: import("./index.js").Device, sol: object, w
|
|
|
19
22
|
} | {
|
|
20
23
|
port: string | number;
|
|
21
24
|
x?: number;
|
|
22
|
-
}, V: number | ArrayLike<number
|
|
25
|
+
}, V: number | ArrayLike<number>, opts?: {
|
|
26
|
+
surface?: "held" | "equilibrium";
|
|
27
|
+
}): {
|
|
23
28
|
V: Float64Array;
|
|
24
29
|
current: Float64Array;
|
|
30
|
+
bare?: Float64Array;
|
|
25
31
|
reactions: {
|
|
26
32
|
equation: string;
|
|
27
33
|
level: number;
|
package/types/solver.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export class Solver {
|
|
|
17
17
|
blockOfFace: Int32Array<ArrayBuffer>;
|
|
18
18
|
surfPort: Int32Array<any>;
|
|
19
19
|
surfW: Int32Array<any>;
|
|
20
|
+
surfCols: any;
|
|
21
|
+
surfColOf: any;
|
|
20
22
|
th: Float64Array<ArrayBuffer>;
|
|
21
23
|
thOld: Float64Array<ArrayBuffer>;
|
|
22
24
|
th0: Float64Array<any>;
|
|
@@ -65,6 +67,9 @@ export class Solver {
|
|
|
65
67
|
portQStart: Float64Array<any>;
|
|
66
68
|
portQOld: Float64Array<any>;
|
|
67
69
|
portQEnd: any;
|
|
70
|
+
qg: Float64Array<any>;
|
|
71
|
+
qgSlope: Float64Array<any>;
|
|
72
|
+
qgTerms: any[][];
|
|
68
73
|
contactDOld: {
|
|
69
74
|
left: number;
|
|
70
75
|
right: number;
|
|
@@ -94,12 +99,17 @@ export class Solver {
|
|
|
94
99
|
generationScale: number;
|
|
95
100
|
hasGeneration: any;
|
|
96
101
|
rxsIn: any;
|
|
102
|
+
closedEnd: {
|
|
103
|
+
left: any;
|
|
104
|
+
right: any;
|
|
105
|
+
};
|
|
97
106
|
chargeNode: Uint8Array<any>;
|
|
98
107
|
chargeNodes: Int32Array<ArrayBuffer>;
|
|
99
108
|
combining: boolean;
|
|
100
109
|
bookkeeping: any;
|
|
101
110
|
bookkeepingFaces: number[];
|
|
102
111
|
referenceAmounts: number[];
|
|
112
|
+
surfaceRef: Float64Array<ArrayBuffer>;
|
|
103
113
|
boundaryIntake: Float64Array<ArrayBuffer>;
|
|
104
114
|
_activeSlots(): void;
|
|
105
115
|
active: Uint8Array<ArrayBuffer>;
|
|
@@ -115,18 +125,20 @@ export class Solver {
|
|
|
115
125
|
_findStretches(): void;
|
|
116
126
|
stretches: any[];
|
|
117
127
|
stretchOf: Int32Array<ArrayBuffer>;
|
|
118
|
-
moieties: Float64Array<
|
|
128
|
+
moieties: Float64Array<any>[];
|
|
119
129
|
constraints: any[];
|
|
120
130
|
localConstraints: any[];
|
|
121
131
|
constrained: boolean;
|
|
122
132
|
_captureLocal(): void;
|
|
123
133
|
_applyLocalConstraints(): void;
|
|
124
|
-
_conservedMoieties(): Float64Array<
|
|
134
|
+
_conservedMoieties(): Float64Array<any>[];
|
|
125
135
|
_directSteady(): boolean;
|
|
126
136
|
_applyConstraints(): void;
|
|
127
137
|
_solveBordered(rhs: any, delta: any, deltaV: any, pins: any): void;
|
|
128
138
|
dWork: Float64Array<ArrayBuffer>;
|
|
129
139
|
borderCols: any[];
|
|
140
|
+
/** Amount (mol per unit area) of surface column j's species, Γ Σ v·a·θ, in the current state. */
|
|
141
|
+
surfaceAmount(j: any): number;
|
|
130
142
|
/** Total amount (mol per unit area) of a stretch's species in the current state. */
|
|
131
143
|
amount(stretch: any): number;
|
|
132
144
|
redrive(): void;
|
|
@@ -184,7 +196,7 @@ export class Solver {
|
|
|
184
196
|
_terminals(dt: any): void;
|
|
185
197
|
_circuit(k: any): void;
|
|
186
198
|
_captureRow(b: any, s: any, w: any, into: any): number;
|
|
187
|
-
_port(port: any, flux: any, k: any, dt: any): void;
|
|
199
|
+
_port(port: any, flux: any, k: any, dt: any, holds: any): void;
|
|
188
200
|
_portReaction(port: any, rx: any, area: any, rates: any, flux: any, k: any): void;
|
|
189
201
|
_portCapacitance(port: any, k: any, dt: any): void;
|
|
190
202
|
_surfaceStorage(port: any, dt: any): void;
|
|
@@ -236,7 +248,7 @@ export class Solver {
|
|
|
236
248
|
/**
|
|
237
249
|
* Adaptive time stepping to tEnd: variable-step BDF2 (or backward Euler), with the local error
|
|
238
250
|
* estimated against an explicit predictor through the previous states, and controlled to
|
|
239
|
-
* `tol` thermal units per step in every potential (φ̂, each η, a
|
|
251
|
+
* `tol` thermal units per step in every potential (φ̂, each η, a surface's coverages as η). The first
|
|
240
252
|
* step is checked by step doubling. Stops early when `budgetMs` of wall time is used, so an
|
|
241
253
|
* animation can call it once per frame; the step size carries over between calls.
|
|
242
254
|
*/
|
package/types/types.d.ts
CHANGED
|
@@ -471,6 +471,12 @@ export type ContactResult = {
|
|
|
471
471
|
*/
|
|
472
472
|
current: number;
|
|
473
473
|
};
|
|
474
|
+
/**
|
|
475
|
+
* The device's totals through its cross-section (currents, fluxes through contacts and ports,
|
|
476
|
+
* charges, amounts, reaction totals, impedance) are per m² of a planar device, as below, and
|
|
477
|
+
* whole (A, mol/s, C, mol, Ω; per metre of a cylinder) for one with a `geometry`. Rates at a
|
|
478
|
+
* face or an electrode are always per m² of it.
|
|
479
|
+
*/
|
|
474
480
|
export type Solution = {
|
|
475
481
|
/**
|
|
476
482
|
* node positions, m (interface positions appear twice)
|