driftlet 0.7.0 → 0.9.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 +11 -5
- package/docs/device.md +163 -3
- package/docs/kit.md +27 -3
- package/docs/numerics.md +16 -0
- package/llms.txt +14 -9
- package/package.json +1 -1
- package/src/checks.js +10 -9
- package/src/describe.js +14 -1
- package/src/device.js +153 -13
- package/src/grid.js +73 -0
- package/src/index.js +1 -0
- package/src/kit.js +1 -0
- package/src/polarization.js +96 -0
- package/src/solution.js +30 -4
- package/src/solver.js +325 -48
- package/src/stack.js +3 -2
- package/src/types.js +22 -4
- package/types/grid.d.ts +29 -0
- package/types/kit.d.ts +1 -0
- package/types/polarization.d.ts +31 -0
- package/types/solver.d.ts +26 -3
- package/types/stack.d.ts +1 -1
- package/types/types.d.ts +65 -4
package/src/solver.js
CHANGED
|
@@ -195,13 +195,16 @@ export class Solver {
|
|
|
195
195
|
// Slots per block: φ̂ (or D, or a conductor's segment flux J), one per species, and at a face
|
|
196
196
|
// one per face reaction (its rate), as many as the busiest face has.
|
|
197
197
|
const nRx = model.interfaces.reduce((m, itf) => Math.max(m, itf.reactions.length), 0);
|
|
198
|
-
|
|
198
|
+
// (and at an electrode port's nodes, one per surface species: its coverage's η)
|
|
199
|
+
const nSurf = model.ports.reduce((m, port) => Math.max(m, port.surface.length), 0);
|
|
200
|
+
const M = n + 1 + Math.max(nRx, nSurf);
|
|
199
201
|
const nNodes = grid.nNodes;
|
|
200
202
|
const nFaces = regions.length - 1;
|
|
201
203
|
const nB = nNodes + nFaces;
|
|
202
204
|
this.n = n;
|
|
203
205
|
this.M = M;
|
|
204
206
|
this.nRx = nRx;
|
|
207
|
+
this.nSurf = nSurf;
|
|
205
208
|
this.nNodes = nNodes;
|
|
206
209
|
this.nFaces = nFaces;
|
|
207
210
|
this.nB = nB;
|
|
@@ -214,6 +217,18 @@ export class Solver {
|
|
|
214
217
|
this.blockOfFace = new Int32Array(nFaces);
|
|
215
218
|
for (let f = 0; f < nFaces; f++) this.blockOfFace[f] = grid.regionEnd[f] + f + 1;
|
|
216
219
|
|
|
220
|
+
// Electrode ports' surfaces: each node's port (−1: none) and its index in the port's window;
|
|
221
|
+
// the coverages there, θ [g·nSurf + s], now and at the step's start.
|
|
222
|
+
this.surfPort = new Int32Array(nNodes).fill(-1);
|
|
223
|
+
this.surfW = new Int32Array(nNodes);
|
|
224
|
+
model.ports.forEach((port, k) => {
|
|
225
|
+
if (port.surface.length > 0) port.nodes.forEach((g, w) => ((this.surfPort[g] = k), (this.surfW[g] = w)));
|
|
226
|
+
});
|
|
227
|
+
this.th = new Float64Array(nNodes * nSurf);
|
|
228
|
+
this.thOld = new Float64Array(nNodes * nSurf);
|
|
229
|
+
this.th0 = new Float64Array(nNodes); // the bare fraction θ₀, directly (1 − Σθ cancels as θ → 1)
|
|
230
|
+
this.th0Old = new Float64Array(nNodes);
|
|
231
|
+
|
|
217
232
|
// Per-node material data, flattened [g·n + i].
|
|
218
233
|
this.present = new Uint8Array(nNodes * n);
|
|
219
234
|
this.cRef = new Float64Array(nNodes * n);
|
|
@@ -327,6 +342,12 @@ export class Solver {
|
|
|
327
342
|
this.contactFlux = { left: new Float64Array(n), right: new Float64Array(n) };
|
|
328
343
|
this.portFlux = model.ports.map(() => new Float64Array(n)); // into the device, mol/(m²·s)
|
|
329
344
|
this.contactD = { left: 0, right: 0 };
|
|
345
|
+
// A capacitive port's charge (its side's, Σ vol·a·σ over its window), as the contacts' D: now,
|
|
346
|
+
// at the step's start and at the previous step's end.
|
|
347
|
+
this.portQ = new Float64Array(model.ports.length);
|
|
348
|
+
this.portQStart = new Float64Array(model.ports.length);
|
|
349
|
+
this.portQOld = new Float64Array(model.ports.length);
|
|
350
|
+
this.portQEnd = null;
|
|
330
351
|
this.contactDOld = { left: 0, right: 0 };
|
|
331
352
|
this.contactDStart = { left: 0, right: 0 };
|
|
332
353
|
// Terminals (the two contacts, then the ports): each one's voltage (V), held by its source or
|
|
@@ -374,6 +395,13 @@ export class Solver {
|
|
|
374
395
|
nu: Float64Array.from([...rx.reactants.map((p) => p.nu), ...rx.products.map((p) => -p.nu)]),
|
|
375
396
|
}));
|
|
376
397
|
|
|
398
|
+
// Each reacting port's electrode area per volume at its window's nodes (a profile's mean over
|
|
399
|
+
// each node's box, as for a rate constant), and its reactions' rates there, mol/(m²·s).
|
|
400
|
+
this.portArea = model.ports.map((port) =>
|
|
401
|
+
port.area === null ? null : Float64Array.from(port.nodes, (g) => port.area.value ?? profileMean(port.area, gx[g] - (g > 0 ? segLength[g - 1] : 0) / 2, gx[g] + (g < segLength.length ? segLength[g] : 0) / 2)),
|
|
402
|
+
);
|
|
403
|
+
this.portRates = model.ports.map((port) => port.reactions.map(() => new Float64Array(port.nodes.length)));
|
|
404
|
+
|
|
377
405
|
// Whether any region is strictly neutral (ε = 0, not a conductor): see integrate().
|
|
378
406
|
this.strictlyNeutral = model.regions.some((reg) => {
|
|
379
407
|
const mat = materials[reg.material];
|
|
@@ -395,8 +423,14 @@ export class Solver {
|
|
|
395
423
|
for (const port of model.ports) for (const g of port.nodes) need[g] = 1;
|
|
396
424
|
// Nodes of strictly neutral (ε = 0) regions that stay neutral, where φ is defined: interior
|
|
397
425
|
// ones, and edges at neutral faces (a capacitive or pinned face's edge node holds the face's
|
|
398
|
-
// charge), away from contacts and ports
|
|
399
|
-
// conditioned terms (see _chargeRows).
|
|
426
|
+
// charge), away from the contacts and from ports that hold a level (which replaces a balance
|
|
427
|
+
// row). On a transient step these are solved in better-conditioned terms (see _chargeRows).
|
|
428
|
+
// A port that only exchanges (an O₂ supply, a leak) is solved there too: left out, its window
|
|
429
|
+
// fell back on the ill-conditioned terms, and a closed neutral electrolyte whose potential is
|
|
430
|
+
// set only by a face reaction couldn't take a step.
|
|
431
|
+
const held = new Uint8Array(nNodes);
|
|
432
|
+
held[0] = held[nNodes - 1] = 1;
|
|
433
|
+
for (const port of model.ports) if (port.species.some((l) => l.type === 'equilibrium')) for (const g of port.nodes) held[g] = 1;
|
|
400
434
|
this.chargeNode = new Uint8Array(nNodes);
|
|
401
435
|
const neutralFace = (f) => f >= 0 && f < model.interfaces.length && model.interfaces[f].phi.type === 'neutral';
|
|
402
436
|
regions.forEach((reg, r) => {
|
|
@@ -405,7 +439,7 @@ export class Solver {
|
|
|
405
439
|
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
|
|
406
440
|
for (let g = g0; g <= g1; g++) {
|
|
407
441
|
const edgeOk = (g > g0 || neutralFace(r - 1)) && (g < g1 || neutralFace(r));
|
|
408
|
-
if (edgeOk && !
|
|
442
|
+
if (edgeOk && !held[g] && !this.phiUndefined[g]) this.chargeNode[g] = 1;
|
|
409
443
|
}
|
|
410
444
|
});
|
|
411
445
|
this.chargeNodes = Int32Array.from([...this.chargeNode.keys()].filter((g) => this.chargeNode[g]));
|
|
@@ -440,6 +474,8 @@ export class Solver {
|
|
|
440
474
|
}
|
|
441
475
|
active[b * M] = this.phiUndefined[g] ? 0 : 1;
|
|
442
476
|
for (let i = 0; i < n; i++) active[b * M + 1 + i] = this.present[g * n + i];
|
|
477
|
+
const k = this.surfPort[g];
|
|
478
|
+
if (k >= 0) for (let q = 0; q < model.ports[k].surface.length; q++) active[b * M + 1 + n + q] = 1;
|
|
443
479
|
}
|
|
444
480
|
model.interfaces.forEach((itf, f) => {
|
|
445
481
|
const bf = this.blockOfFace[f];
|
|
@@ -502,10 +538,15 @@ export class Solver {
|
|
|
502
538
|
if (rx.kf[m] > 0 && [...rx.reactants, ...rx.products].some((p) => p.i === i)) reactive = true;
|
|
503
539
|
}
|
|
504
540
|
}
|
|
505
|
-
const
|
|
506
|
-
const
|
|
541
|
+
const inside = (port) => port.region >= r0 && port.region <= r;
|
|
542
|
+
const linked = model.ports.flatMap((port, k) => (inside(port) && port.species[i].type !== 'blocked' ? [k] : []));
|
|
543
|
+
// made or consumed by a port's reactions (an electrode spread through its window)
|
|
544
|
+
const reacting = model.ports.flatMap((port, k) => (inside(port) && port.reactions.some((rx) => rx.part.some((p) => p.side === 0 && p.i === i)) ? [k] : []));
|
|
545
|
+
if (reacting.length > 0) reactive = true;
|
|
546
|
+
const connected = leftOpen || rightOpen || linked.length > 0;
|
|
507
547
|
this.stretches.push({
|
|
508
|
-
ports
|
|
548
|
+
linked, // ports that hold or exchange it
|
|
549
|
+
ports: [...new Set([...linked, ...reacting])], // ports that bring any in
|
|
509
550
|
species: i,
|
|
510
551
|
regions: [r0, r],
|
|
511
552
|
nodes: [grid.regionStart[r0], grid.regionEnd[r]],
|
|
@@ -635,6 +676,13 @@ export class Solver {
|
|
|
635
676
|
rows.push(row);
|
|
636
677
|
}
|
|
637
678
|
});
|
|
679
|
+
for (const port of model.ports) {
|
|
680
|
+
for (const rx of port.reactions) {
|
|
681
|
+
const row = new Float64Array(S); // (the electrode's carrier is outside the device)
|
|
682
|
+
for (const p of rx.part) if (p.side === 0) row[this.stretchOf[port.region * n + p.i]] += p.nu;
|
|
683
|
+
rows.push(row);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
638
686
|
model.regions.forEach((reg, q) => {
|
|
639
687
|
for (const rx of model.reactions) {
|
|
640
688
|
if (!(rx.kf[reg.material] > 0)) continue;
|
|
@@ -820,11 +868,11 @@ export class Solver {
|
|
|
820
868
|
});
|
|
821
869
|
}
|
|
822
870
|
|
|
823
|
-
/** η_i (μ̄/RT) of a port's outside level for species i. */
|
|
871
|
+
/** η_i (μ̄/RT) of a port's outside level for species i (its electrode's carrier: at V itself). */
|
|
824
872
|
portEta(port, i) {
|
|
825
873
|
const link = port.species[i];
|
|
826
874
|
const V = this.termV[2 + this.model.ports.indexOf(port)];
|
|
827
|
-
return this.z[i] === 0 ? link.mu / this.model.RT : (this.z[i] * (V + link.offset)) / this.VT;
|
|
875
|
+
return this.z[i] === 0 ? link.mu / this.model.RT : (this.z[i] * (V + (link.offset ?? 0))) / this.VT;
|
|
828
876
|
}
|
|
829
877
|
|
|
830
878
|
/** η_i/RT that a fixed contact link imposes (or NaN if the link isn't fixed). */
|
|
@@ -890,7 +938,7 @@ export class Solver {
|
|
|
890
938
|
} else {
|
|
891
939
|
const left = st.regions[0] === 0 ? this.contactEta('left', i) : NaN;
|
|
892
940
|
eta[i] = Number.isFinite(left) ? left : this.contactEta('right', i);
|
|
893
|
-
if (!Number.isFinite(eta[i]) && st.
|
|
941
|
+
if (!Number.isFinite(eta[i]) && st.linked.length > 0) eta[i] = this.portEta(model.ports[st.linked[0]], i);
|
|
894
942
|
mode[i] = 1;
|
|
895
943
|
if (!Number.isFinite(eta[i])) {
|
|
896
944
|
// Fed only through reactions (no level held at a contact): start from c0.
|
|
@@ -961,6 +1009,25 @@ export class Solver {
|
|
|
961
1009
|
}
|
|
962
1010
|
}
|
|
963
1011
|
}
|
|
1012
|
+
const electrode = model.ports.find((port) => port.region === r && port.reactions.length > 0);
|
|
1013
|
+
if (!mat.phiFree && !responds && mat.ideal && electrode) {
|
|
1014
|
+
// Nor here, but an electrode spread through it does: start with the port's first reaction
|
|
1015
|
+
// that can be balanced at equilibrium with the port's level (the electrode at open circuit).
|
|
1016
|
+
for (const rx of electrode.reactions) {
|
|
1017
|
+
let a = rx.fixedA, s = 0;
|
|
1018
|
+
for (const p of rx.part) {
|
|
1019
|
+
if (p.side === 1) a -= p.nu * this.portEta(electrode, p.i);
|
|
1020
|
+
else if (mode[p.i] === 2) {
|
|
1021
|
+
a -= p.nu * (Math.log(cFix[p.i] / mat.cRef[p.i]) + mat.mu0[p.i] / model.RT);
|
|
1022
|
+
s += p.nu * z[p.i];
|
|
1023
|
+
} else s = NaN;
|
|
1024
|
+
}
|
|
1025
|
+
if (s !== 0 && Number.isFinite(s)) {
|
|
1026
|
+
phiHat = a / s;
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
964
1031
|
const neutralPhi = (ph) => {
|
|
965
1032
|
let lo = ph - 1, hi = ph + 1;
|
|
966
1033
|
while (charge(lo) < 0 && lo > -1e4) lo -= 2 * (hi - lo);
|
|
@@ -1011,7 +1078,60 @@ export class Solver {
|
|
|
1011
1078
|
const shift = level(t.side) - level(this.terms[held].side);
|
|
1012
1079
|
if (Number.isFinite(shift)) this.termV[k] = this.termV[held] + shift;
|
|
1013
1080
|
}
|
|
1081
|
+
// Electrode surfaces at their starting coverages: η = μ°/RT + ln(θ/θ₀).
|
|
1082
|
+
for (let g = 0; g < this.nNodes; g++) {
|
|
1083
|
+
const k = this.surfPort[g];
|
|
1084
|
+
if (k < 0) continue;
|
|
1085
|
+
const surf = model.ports[k].surface, b = this.blockOfNode[g], bare = 1 - surf.reduce((t, sp) => t + sp.theta0, 0);
|
|
1086
|
+
surf.forEach((sp, q) => (u[b * M + 1 + n + q] = sp.mu0 / model.RT + Math.log(sp.theta0 / bare)));
|
|
1087
|
+
}
|
|
1014
1088
|
this.computeConcentrations();
|
|
1089
|
+
// A floating electrode spread through a port starts where its reactions pass the current it's
|
|
1090
|
+
// set (none, behind a resistance): at its mixed potential in the start's composition, not
|
|
1091
|
+
// level with a held terminal, which can be volts away and pass an absurd current.
|
|
1092
|
+
for (const k of this.floating) {
|
|
1093
|
+
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1094
|
+
if (!port || port.reactions.length === 0) continue;
|
|
1095
|
+
const want = t.drive.kind === 'I' ? sourceAt(t.drive.src, this.time) : 0;
|
|
1096
|
+
const current = (V) => {
|
|
1097
|
+
this.termV[k] = V;
|
|
1098
|
+
let I = 0;
|
|
1099
|
+
port.reactions.forEach((rx) => {
|
|
1100
|
+
let q = 0;
|
|
1101
|
+
for (const p of rx.part) if (p.side === 0) q += p.nu * z[p.i];
|
|
1102
|
+
port.nodes.forEach((g, w) => (I += q * FARADAY * this.model.grid.vol[g] * this.portArea[t.index][w] * this._portRate(port, rx, g)));
|
|
1103
|
+
});
|
|
1104
|
+
return I;
|
|
1105
|
+
};
|
|
1106
|
+
// Current into the device rises with V (oxidation); bracket, then bisect.
|
|
1107
|
+
let lo = this.termV[k] - 1, hi = this.termV[k] + 1;
|
|
1108
|
+
while (current(lo) > want && lo > -1e3) lo -= 2 * (hi - lo);
|
|
1109
|
+
while (current(hi) < want && hi < 1e3) hi += 2 * (hi - lo);
|
|
1110
|
+
for (let it = 0; it < 200 && hi - lo > 1e-12; it++) {
|
|
1111
|
+
const m = 0.5 * (lo + hi);
|
|
1112
|
+
if (current(m) < want) lo = m;
|
|
1113
|
+
else hi = m;
|
|
1114
|
+
}
|
|
1115
|
+
this.termV[k] = 0.5 * (lo + hi);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// A port reaction's rate per area at node g (mol/(m²·s), forward), at the present state.
|
|
1120
|
+
_portRate(port, rx, g) {
|
|
1121
|
+
const { n, M, u, uLo, c, nSurf, th } = this, b = this.blockOfNode[g], RT = this.model.RT;
|
|
1122
|
+
let pref = rx.k0, a = rx.fixedA;
|
|
1123
|
+
for (const p of rx.part) {
|
|
1124
|
+
if (p.side === 1) {
|
|
1125
|
+
a -= p.nu * this.portEta(port, p.i);
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
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;
|
|
1129
|
+
a -= p.nu * (u[o] + uLo[o]);
|
|
1130
|
+
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
|
+
}
|
|
1132
|
+
if (rx.bare) pref *= this.th0[g];
|
|
1133
|
+
// (far from equilibrium, the plain difference: bvFactor's product would give 0·∞ there)
|
|
1134
|
+
return pref * (Math.abs(a) > 50 ? Math.exp(rx.alpha * a) - Math.exp(-(1 - rx.alpha) * a) : bvFactor(a, rx.alpha).g);
|
|
1015
1135
|
}
|
|
1016
1136
|
|
|
1017
1137
|
// A non-ideal material's composition at φ̂ = ph: level-fixed species (mode 1) at their η,
|
|
@@ -1040,6 +1160,23 @@ export class Solver {
|
|
|
1040
1160
|
}
|
|
1041
1161
|
}
|
|
1042
1162
|
|
|
1163
|
+
// Each electrode surface's coverages from its η: Langmuir on shared sites, θ_s = e^{ζ_s}/(1 + Σ e^{ζ}),
|
|
1164
|
+
// ζ = η − μ°/RT (so θ_s/θ₀ = e^{ζ_s}), at every node of a surfaced window.
|
|
1165
|
+
_coverages() {
|
|
1166
|
+
const { n, M, u, uLo, nSurf, th } = this, RT = this.model.RT;
|
|
1167
|
+
for (let g = 0; g < this.nNodes; g++) {
|
|
1168
|
+
const k = this.surfPort[g];
|
|
1169
|
+
if (k < 0) continue;
|
|
1170
|
+
const surf = this.model.ports[k].surface, b = this.blockOfNode[g];
|
|
1171
|
+
let m = 0;
|
|
1172
|
+
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);
|
|
1173
|
+
let sum = Math.exp(-m);
|
|
1174
|
+
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));
|
|
1175
|
+
for (let q = 0; q < surf.length; q++) th[g * nSurf + q] /= sum;
|
|
1176
|
+
this.th0[g] = Math.exp(-m) / sum;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1043
1180
|
computeConcentrations() {
|
|
1044
1181
|
const { n, M, u, uLo, c, z } = this;
|
|
1045
1182
|
for (let g = 0; g < this.nNodes; g++) {
|
|
@@ -1054,7 +1191,7 @@ export class Solver {
|
|
|
1054
1191
|
// Excess carriers: zero in the bulk, the surface sheet at a charged face.
|
|
1055
1192
|
for (let i = 0; i < n; i++) c[g * n + i] = 0;
|
|
1056
1193
|
const f = this.sheetFace[g];
|
|
1057
|
-
if (f >= 0) c[g * n + im] = (this.sheetSign[g] * u[this.blockOfFace[f] * M]) / (z[im] * FARADAY * this.model.grid.vol[g]);
|
|
1194
|
+
if (f >= 0) c[g * n + im] = (this.sheetSign[g] * u[this.blockOfFace[f] * M] * this.model.grid.area[g]) / (z[im] * FARADAY * this.model.grid.vol[g]);
|
|
1058
1195
|
continue;
|
|
1059
1196
|
}
|
|
1060
1197
|
for (let i = 0; i < n; i++) {
|
|
@@ -1062,6 +1199,7 @@ export class Solver {
|
|
|
1062
1199
|
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;
|
|
1063
1200
|
}
|
|
1064
1201
|
}
|
|
1202
|
+
this._coverages();
|
|
1065
1203
|
}
|
|
1066
1204
|
|
|
1067
1205
|
_work(md) {
|
|
@@ -1165,7 +1303,7 @@ export class Solver {
|
|
|
1165
1303
|
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
|
|
1166
1304
|
if (mat.conductor) {
|
|
1167
1305
|
for (let g = g0; g <= g1; g++) this._nodeConductor(g, dt);
|
|
1168
|
-
for (let s = g0; s < g1; s++) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s]);
|
|
1306
|
+
for (let s = g0; s < g1; s++) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s] / grid.segArea[s]);
|
|
1169
1307
|
continue;
|
|
1170
1308
|
}
|
|
1171
1309
|
if (mat.ideal) this._nodesDilute(g0, g1, dt);
|
|
@@ -1173,7 +1311,7 @@ export class Solver {
|
|
|
1173
1311
|
const rxs = this.rxsIn[reg.material];
|
|
1174
1312
|
if (rxs.length > 0) for (let g = g0; g <= g1; g++) this._bulkReactions(g, rxs);
|
|
1175
1313
|
for (let s = g0; s < g1; s++) {
|
|
1176
|
-
const bL = s + r, bR = bL + 1, h = grid.segLength[s];
|
|
1314
|
+
const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s]; // (a length over the cross-section: fluxes are totals)
|
|
1177
1315
|
this._segmentDisplacement(s, bL, bR, mat, h);
|
|
1178
1316
|
if (reg.mixing > 0) this._segmentMixing(s, bL, bR, reg.mixing, h, mat);
|
|
1179
1317
|
if (!mat.ideal) this._segmentConcentrated(s, bL, bR, mat, h, reg.velocity);
|
|
@@ -1201,7 +1339,7 @@ export class Solver {
|
|
|
1201
1339
|
const r = 1 + im, k = g * n + im;
|
|
1202
1340
|
res[R[b * M + r]] += (v * (c[k] - cOld[k])) / dt;
|
|
1203
1341
|
const f = this.sheetFace[g];
|
|
1204
|
-
if (f >= 0) this._j(b, r, this.blockOfFace[f], 0, this.sheetSign[g] / (z[im] * FARADAY * dt));
|
|
1342
|
+
if (f >= 0) this._j(b, r, this.blockOfFace[f], 0, (this.sheetSign[g] * this.model.grid.area[g]) / (z[im] * FARADAY * dt));
|
|
1205
1343
|
}
|
|
1206
1344
|
|
|
1207
1345
|
// Dilute nodes g0…g1 (ideal statistics, c = c_ref e^ζ): storage of each species and the space
|
|
@@ -1450,9 +1588,9 @@ export class Solver {
|
|
|
1450
1588
|
_segmentsDilute(g0, g1, region, mat, vel) {
|
|
1451
1589
|
const { n, M, u, uLo, res, c, z, sys, loc } = this, R = this.rix, F = FARADAY;
|
|
1452
1590
|
const { A: JA, B: JB, C: JC, sizes, offA, offB, offC } = sys;
|
|
1453
|
-
const segLength = this.model.grid
|
|
1591
|
+
const { segLength, segArea } = this.model.grid;
|
|
1454
1592
|
for (let s = g0; s < g1; s++) {
|
|
1455
|
-
const bL = s + region, bR = bL + 1, h = segLength[s];
|
|
1593
|
+
const bL = s + region, bR = bL + 1, h = segLength[s] / segArea[s]; // (fluxes are totals through the cross-section)
|
|
1456
1594
|
const mL = sizes[bL], mR = sizes[bR], pL = loc[bL * M], pR = loc[bR * M];
|
|
1457
1595
|
const phiL = u[bL * M], phiR = u[bR * M];
|
|
1458
1596
|
for (let i = 0; i < n; i++) {
|
|
@@ -1535,15 +1673,16 @@ export class Solver {
|
|
|
1535
1673
|
this._j(bf, 0, bL, 0, -kC);
|
|
1536
1674
|
}
|
|
1537
1675
|
}
|
|
1538
|
-
// D_f enters each side's Gauss row; a metal side holds it as surface carriers instead.
|
|
1539
|
-
|
|
1676
|
+
// D_f enters each side's Gauss row; a metal side holds it as surface carriers instead. The
|
|
1677
|
+
// flux node's unknowns are per area, so what they carry is that times the face's area.
|
|
1678
|
+
const gL = grid.regionEnd[f], gR = gL + 1, Af = grid.area[gL];
|
|
1540
1679
|
if (this.nodeConductor[gL] < 0) {
|
|
1541
|
-
res[R[bL * M]] += u[bf * M];
|
|
1542
|
-
this._j(bL, 0, bf, 0,
|
|
1680
|
+
res[R[bL * M]] += Af * u[bf * M];
|
|
1681
|
+
this._j(bL, 0, bf, 0, Af);
|
|
1543
1682
|
}
|
|
1544
1683
|
if (this.nodeConductor[gR] < 0) {
|
|
1545
|
-
res[R[bR * M]] -= u[bf * M] + itf.sheetCharge;
|
|
1546
|
-
this._j(bR, 0, bf, 0, -
|
|
1684
|
+
res[R[bR * M]] -= Af * (u[bf * M] + itf.sheetCharge);
|
|
1685
|
+
this._j(bR, 0, bf, 0, -Af);
|
|
1547
1686
|
}
|
|
1548
1687
|
for (let i = 0; i < n; i++) {
|
|
1549
1688
|
const r = 1 + i, o = bf * M + r;
|
|
@@ -1583,12 +1722,12 @@ export class Solver {
|
|
|
1583
1722
|
this._j(bf, r, bL, r, -gG);
|
|
1584
1723
|
this._j(bf, r, bR, r, gG);
|
|
1585
1724
|
}
|
|
1586
|
-
res[R[bL * M + r]] += u[o];
|
|
1587
|
-
this._j(bL, r, bf, r,
|
|
1588
|
-
res[R[bR * M + r]] -= u[o];
|
|
1589
|
-
this._j(bR, r, bf, r, -
|
|
1725
|
+
res[R[bL * M + r]] += Af * u[o];
|
|
1726
|
+
this._j(bL, r, bf, r, Af);
|
|
1727
|
+
res[R[bR * M + r]] -= Af * u[o];
|
|
1728
|
+
this._j(bR, r, bf, r, -Af);
|
|
1590
1729
|
}
|
|
1591
|
-
itf.reactions.forEach((rx, k) => this._faceReaction(rx, k, f, bf, bL, bR));
|
|
1730
|
+
itf.reactions.forEach((rx, k) => this._faceReaction(rx, k, f, bf, bL, bR, Af));
|
|
1592
1731
|
}
|
|
1593
1732
|
|
|
1594
1733
|
// Scharfetter–Gummel with non-ideal statistics. The excess ex = ζ − ln(c/c_ref) acts as an
|
|
@@ -1805,7 +1944,7 @@ export class Solver {
|
|
|
1805
1944
|
// with the row r_k − rate(u_L, u_R) = 0, and each participant's edge node takes ν·r_k (made
|
|
1806
1945
|
// there when ν > 0). The rate couples the two edge nodes only through the face block between
|
|
1807
1946
|
// them, which keeps the system block-tridiagonal.
|
|
1808
|
-
_faceReaction(rx, k, f, bf, bL, bR) {
|
|
1947
|
+
_faceReaction(rx, k, f, bf, bL, bR, Af) {
|
|
1809
1948
|
const { n, M, u, uLo, c, res } = this;
|
|
1810
1949
|
const R = this.rix;
|
|
1811
1950
|
const grid = this.model.grid;
|
|
@@ -1865,8 +2004,8 @@ export class Solver {
|
|
|
1865
2004
|
}
|
|
1866
2005
|
for (const p of rx.part) {
|
|
1867
2006
|
const b = p.side ? bR : bL;
|
|
1868
|
-
res[R[b * M + 1 + p.i]] -= p.nu * u[o];
|
|
1869
|
-
this._j(b, 1 + p.i, bf, slot, -p.nu);
|
|
2007
|
+
res[R[b * M + 1 + p.i]] -= Af * p.nu * u[o];
|
|
2008
|
+
this._j(b, 1 + p.i, bf, slot, -Af * p.nu);
|
|
1870
2009
|
}
|
|
1871
2010
|
}
|
|
1872
2011
|
|
|
@@ -1880,7 +2019,7 @@ export class Solver {
|
|
|
1880
2019
|
this.termDI[k] = 0;
|
|
1881
2020
|
this.termI[k] = 0;
|
|
1882
2021
|
}
|
|
1883
|
-
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k));
|
|
2022
|
+
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt));
|
|
1884
2023
|
this._contact('left', dt);
|
|
1885
2024
|
this._contact('right', dt);
|
|
1886
2025
|
for (const k of this.floating) this._circuit(k);
|
|
@@ -1913,7 +2052,7 @@ export class Solver {
|
|
|
1913
2052
|
// An internal port (terminal k): its exchange with each node of its window, as a source per
|
|
1914
2053
|
// volume. A held ('equilibrium') level replaces the node's balance row; the source is then that
|
|
1915
2054
|
// row's residual, read just before. Its current into the device is Σ z F × the sources.
|
|
1916
|
-
_port(port, flux, k) {
|
|
2055
|
+
_port(port, flux, k, dt) {
|
|
1917
2056
|
const R = this.rix;
|
|
1918
2057
|
const { n, M, u, uLo, res, z, VT } = this;
|
|
1919
2058
|
const F = FARADAY, vol = this.model.grid.vol;
|
|
@@ -1948,9 +2087,122 @@ export class Solver {
|
|
|
1948
2087
|
}
|
|
1949
2088
|
}
|
|
1950
2089
|
}
|
|
2090
|
+
port.reactions.forEach((rx, x) => this._portReaction(port, rx, this.portArea[k - 2], this.portRates[k - 2][x], flux, k));
|
|
2091
|
+
if (port.capacitance) this._portCapacitance(port, k, dt);
|
|
2092
|
+
if (port.surface.length > 0 && Number.isFinite(dt)) this._surfaceStorage(port, dt);
|
|
1951
2093
|
for (let i = 0; i < n; i++) this.termI[k] += z[i] * F * flux[i];
|
|
1952
2094
|
}
|
|
1953
2095
|
|
|
2096
|
+
// A port's reaction at each node of its window: Butler–Volmer per area, as at a face, against
|
|
2097
|
+
// the electrode's carrier at the port's level (activity 1), times the electrode's area per
|
|
2098
|
+
// volume. Each node's rate is explicit (no unknown of its own): it enters the balances of the
|
|
2099
|
+
// species it makes and consumes, and the port's current.
|
|
2100
|
+
_portReaction(port, rx, area, rates, flux, k) {
|
|
2101
|
+
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;
|
|
2102
|
+
const B = this.termB[k], C = this.termC[k], al = rx.alpha, d = this.dA, surf = port.surface;
|
|
2103
|
+
// The carrier's part of the affinity, and how it moves with the port's voltage.
|
|
2104
|
+
let aV = rx.fixedA, daV = 0, q = 0; // q: charge the forward reaction brings into the device, per event
|
|
2105
|
+
for (const p of rx.part) {
|
|
2106
|
+
if (p.side === 1) {
|
|
2107
|
+
aV -= p.nu * this.portEta(port, p.i);
|
|
2108
|
+
daV -= (p.nu * z[p.i]) / VT;
|
|
2109
|
+
} else if (p.side === 0) q += p.nu * z[p.i];
|
|
2110
|
+
}
|
|
2111
|
+
const slot = (p) => (p.side === 2 ? 1 + n + p.s : 1 + p.i);
|
|
2112
|
+
port.nodes.forEach((g, w) => {
|
|
2113
|
+
const b = this.blockOfNode[g], s = vol[g] * area[w];
|
|
2114
|
+
d.fill(0);
|
|
2115
|
+
let pref = rx.k0, aHi = aV, aLo = 0;
|
|
2116
|
+
for (const p of rx.part) {
|
|
2117
|
+
if (p.side === 1) continue;
|
|
2118
|
+
const o = b * M + slot(p);
|
|
2119
|
+
aHi -= p.nu * u[o];
|
|
2120
|
+
aLo -= p.nu * uLo[o];
|
|
2121
|
+
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
2122
|
+
if (p.side === 2) {
|
|
2123
|
+
// a surface species at activity θ/θ₀ = e^ζ
|
|
2124
|
+
pref *= Math.exp(e * (u[o] + uLo[o] - surf[p.s].mu0 / RT));
|
|
2125
|
+
d[1 + n + p.s] += e;
|
|
2126
|
+
} else {
|
|
2127
|
+
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
2128
|
+
this._dlnc(g, p.i, e, d);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
if (rx.bare) {
|
|
2132
|
+
// on bare metal only: the free fraction θ₀ = 1 − Σθ, with ∂ln θ₀/∂η_s = −θ_s
|
|
2133
|
+
for (let x = 0; x < surf.length; x++) d[1 + n + x] -= th[g * nSurf + x];
|
|
2134
|
+
pref *= this.th0[g];
|
|
2135
|
+
}
|
|
2136
|
+
const { g: bv, gp } = bvFactor(aHi + aLo, al);
|
|
2137
|
+
const rate = pref * bv;
|
|
2138
|
+
rates[w] = rate;
|
|
2139
|
+
// ∂rate/∂slot = rate·∂ln(prefactor) + pref·g′·∂a, with ∂a/∂η = −ν
|
|
2140
|
+
for (let t = 0; t < M; t++) d[t] *= rate;
|
|
2141
|
+
for (const p of rx.part) if (p.side !== 1) d[slot(p)] -= pref * gp * p.nu;
|
|
2142
|
+
const dV = pref * gp * daV;
|
|
2143
|
+
for (const p of rx.part) {
|
|
2144
|
+
if (p.side === 1) continue;
|
|
2145
|
+
// the region's species per volume of the window; a surface's per area of electrode
|
|
2146
|
+
const row = slot(p), o = b * M + row, f = p.side === 2 ? 1 : s;
|
|
2147
|
+
res[R[o]] -= f * p.nu * rate;
|
|
2148
|
+
if (p.side === 0) flux[p.i] += s * p.nu * rate;
|
|
2149
|
+
for (let t = 0; t < M; t++) if (d[t] !== 0) this._j(b, row, b, t, -f * p.nu * d[t]);
|
|
2150
|
+
B[R[o]] -= f * p.nu * dV;
|
|
2151
|
+
}
|
|
2152
|
+
for (let t = 0; t < M; t++) if (d[t] !== 0) C[R[b * M + t]] += q * F * s * d[t];
|
|
2153
|
+
this.termDI[k] += q * F * s * dV;
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// A capacitance spread through a port's window: per area of electrode, σ = C (V − zeroCharge − φ)
|
|
2158
|
+
// on the port's side, so each node's charge balance (its φ row) gains vol·a·σ, and the port
|
|
2159
|
+
// passes the charging current d(Σ vol·a·σ)/dt (none in a steady state). An end node whose φ its
|
|
2160
|
+
// contact sets is left to the contact.
|
|
2161
|
+
_portCapacitance(port, k, dt) {
|
|
2162
|
+
const { M, u, uLo, res, VT } = this, R = this.rix, vol = this.model.grid.vol, area = this.portArea[k - 2];
|
|
2163
|
+
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
2164
|
+
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
2165
|
+
let Q = 0, dQdV = 0;
|
|
2166
|
+
port.nodes.forEach((g, w) => {
|
|
2167
|
+
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
2168
|
+
const b = this.blockOfNode[g], s = vol[g] * area[w] * Cs;
|
|
2169
|
+
const q = s * (V - zeroCharge - VT * (u[b * M] + uLo[b * M]));
|
|
2170
|
+
Q += q;
|
|
2171
|
+
dQdV += s;
|
|
2172
|
+
res[R[b * M]] -= q;
|
|
2173
|
+
this._j(b, 0, b, 0, s * VT);
|
|
2174
|
+
B[R[b * M]] -= s;
|
|
2175
|
+
if (Number.isFinite(dt)) Ct[R[b * M]] += (-s * VT) / dt;
|
|
2176
|
+
});
|
|
2177
|
+
this.portQ[k - 2] = Q;
|
|
2178
|
+
if (Number.isFinite(dt)) {
|
|
2179
|
+
this.termI[k] += (Q - this.portQStart[k - 2]) / dt;
|
|
2180
|
+
this.termDI[k] += dQdV / dt;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// An electrode surface's storage, per area of electrode: Γ (θ_s − θ_s,old)/dt in each surface
|
|
2185
|
+
// species' row, with ∂θ_s/∂η_x = θ_s (δ_sx − θ_x).
|
|
2186
|
+
_surfaceStorage(port, dt) {
|
|
2187
|
+
const { n, M, res, nSurf, th, thOld } = this, R = this.rix, surf = port.surface, G = surf[0].capacity;
|
|
2188
|
+
for (const g of port.nodes) {
|
|
2189
|
+
const b = this.blockOfNode[g];
|
|
2190
|
+
// Near full coverage θ − θ_old cancels (both ~1, the change ~θ₀): the dominant species'
|
|
2191
|
+
// change comes from the small ones instead, Δθ = −Δθ₀ − Σ_others Δθ.
|
|
2192
|
+
let top = 0;
|
|
2193
|
+
for (let q = 1; q < surf.length; q++) if (th[g * nSurf + q] > th[g * nSurf + top]) top = q;
|
|
2194
|
+
for (let q = 0; q < surf.length; q++) {
|
|
2195
|
+
const tq = th[g * nSurf + q];
|
|
2196
|
+
let change = tq - thOld[g * nSurf + q];
|
|
2197
|
+
if (q === top && tq > 0.5) {
|
|
2198
|
+
change = -(this.th0[g] - this.th0Old[g]);
|
|
2199
|
+
for (let x = 0; x < surf.length; x++) if (x !== q) change -= th[g * nSurf + x] - thOld[g * nSurf + x];
|
|
2200
|
+
}
|
|
2201
|
+
res[R[b * M + 1 + n + q]] += (G * change) / dt;
|
|
2202
|
+
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]));
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
1954
2206
|
|
|
1955
2207
|
// One contact (terminal 0 or 1): record the flux through its outer face and the current into
|
|
1956
2208
|
// the device (read from the end box before anything here is added: whatever the box needs
|
|
@@ -1966,6 +2218,7 @@ export class Solver {
|
|
|
1966
2218
|
const b = this.blockOfNode[g];
|
|
1967
2219
|
const sgn = side === 'left' ? 1 : -1;
|
|
1968
2220
|
const flux = this.contactFlux[side];
|
|
2221
|
+
const Ac = model.grid.area[g]; // the links' laws are per area: what they pass is that times A here
|
|
1969
2222
|
const dyn = Number.isFinite(dt);
|
|
1970
2223
|
|
|
1971
2224
|
// Before anything is added, each balance residual is the flux into the device here.
|
|
@@ -1986,13 +2239,13 @@ export class Solver {
|
|
|
1986
2239
|
const link = ct.species[i];
|
|
1987
2240
|
const o = b * M + 1 + i;
|
|
1988
2241
|
if (link.type === 'exchange') {
|
|
1989
|
-
res[R[o]] -= link.k * (link.mu / model.RT - (u[o] + uLo[o]));
|
|
1990
|
-
this._j(b, 1 + i, b, 1 + i, link.k);
|
|
2242
|
+
res[R[o]] -= Ac * link.k * (link.mu / model.RT - (u[o] + uLo[o]));
|
|
2243
|
+
this._j(b, 1 + i, b, 1 + i, Ac * link.k);
|
|
1991
2244
|
} else if (link.type === 'conductance') {
|
|
1992
2245
|
const Vi = (VT * (u[o] + uLo[o])) / z[i];
|
|
1993
|
-
res[R[o]] -= (link.G * (Vt + link.offset - Vi)) / (z[i] * F);
|
|
1994
|
-
this._j(b, 1 + i, b, 1 + i, (link.G * VT) / (z[i] * z[i] * F));
|
|
1995
|
-
B[R[o]] += -link.G / (z[i] * F);
|
|
2246
|
+
res[R[o]] -= (Ac * link.G * (Vt + link.offset - Vi)) / (z[i] * F);
|
|
2247
|
+
this._j(b, 1 + i, b, 1 + i, (Ac * link.G * VT) / (z[i] * z[i] * F));
|
|
2248
|
+
B[R[o]] += (-Ac * link.G) / (z[i] * F);
|
|
1996
2249
|
}
|
|
1997
2250
|
}
|
|
1998
2251
|
|
|
@@ -2014,15 +2267,15 @@ export class Solver {
|
|
|
2014
2267
|
this.contactD[side] = 0;
|
|
2015
2268
|
} else if (link.type === 'capacitive') {
|
|
2016
2269
|
// Gate, or metal across a Stern layer, at φ_g = V_t − zeroCharge. D toward +x.
|
|
2017
|
-
const Din = link.C * (Vt - link.zeroCharge - VT * u[b * M]);
|
|
2270
|
+
const Din = Ac * link.C * (Vt - link.zeroCharge - VT * u[b * M]);
|
|
2018
2271
|
res[R[b * M]] -= Din;
|
|
2019
|
-
this._j(b, 0, b, 0, link.C * VT);
|
|
2020
|
-
B[R[b * M]] += -link.C;
|
|
2272
|
+
this._j(b, 0, b, 0, Ac * link.C * VT);
|
|
2273
|
+
B[R[b * M]] += -Ac * link.C;
|
|
2021
2274
|
this.contactD[side] = sgn * Din;
|
|
2022
2275
|
if (dyn) {
|
|
2023
2276
|
I += (Din - Dstart) / dt;
|
|
2024
|
-
C[R[b * M]] += (-link.C * VT) / dt;
|
|
2025
|
-
this.termDI[k] += link.C / dt;
|
|
2277
|
+
C[R[b * M]] += (-Ac * link.C * VT) / dt;
|
|
2278
|
+
this.termDI[k] += (Ac * link.C) / dt;
|
|
2026
2279
|
}
|
|
2027
2280
|
} else if (link.type === 'pinned' || link.type === 'bulk') {
|
|
2028
2281
|
// The Poisson residual is the outside's charge, D_in.
|
|
@@ -2106,6 +2359,8 @@ export class Solver {
|
|
|
2106
2359
|
for (let i = 0; i < n; i++) {
|
|
2107
2360
|
if (this.present[g * n + i]) mx = Math.max(mx, Math.abs(delta[R[b * M + 1 + i]]));
|
|
2108
2361
|
}
|
|
2362
|
+
const k = this.surfPort[g]; // an electrode surface's coverages, as η
|
|
2363
|
+
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]]));
|
|
2109
2364
|
}
|
|
2110
2365
|
return mx;
|
|
2111
2366
|
}
|
|
@@ -2262,14 +2517,15 @@ export class Solver {
|
|
|
2262
2517
|
this.uPrevLo.set(this.uLo);
|
|
2263
2518
|
this.termVPrev.set(this.termV);
|
|
2264
2519
|
this.computeConcentrations();
|
|
2265
|
-
const cN = Float64Array.from(this.c);
|
|
2520
|
+
const cN = Float64Array.from(this.c), thN = Float64Array.from(this.th), th0N = Float64Array.from(this.th0);
|
|
2266
2521
|
// Contact displacement before the step: as it was at the end of the previous step (under
|
|
2267
2522
|
// the parameters then), so a gate-voltage change shows up as displacement current.
|
|
2268
|
-
if (!this.contactDEnd) {
|
|
2523
|
+
if (!this.contactDEnd || !this.portQEnd) {
|
|
2269
2524
|
this._assembleBookkeeping(dt);
|
|
2270
2525
|
this.contactDEnd = { ...this.contactD };
|
|
2526
|
+
this.portQEnd = Float64Array.from(this.portQ);
|
|
2271
2527
|
}
|
|
2272
|
-
const DN = { ...this.contactDEnd };
|
|
2528
|
+
const DN = { ...this.contactDEnd }, QN = Float64Array.from(this.portQEnd);
|
|
2273
2529
|
if (opts.guess) {
|
|
2274
2530
|
// Start Newton from a predicted state (e.g. extrapolated from the history).
|
|
2275
2531
|
this.u.set(opts.guess);
|
|
@@ -2280,11 +2536,17 @@ export class Solver {
|
|
|
2280
2536
|
const a0 = (1 + 2 * w) / (1 + w), b1 = (1 + w) / a0, b2 = (w * w) / (1 + w) / a0;
|
|
2281
2537
|
const { cOld } = this;
|
|
2282
2538
|
for (let k = 0; k < cOld.length; k++) cOld[k] = b1 * cN[k] - b2 * prev.c[k];
|
|
2539
|
+
for (let k = 0; k < thN.length; k++) this.thOld[k] = b1 * thN[k] - b2 * prev.th[k];
|
|
2540
|
+
for (let k = 0; k < th0N.length; k++) this.th0Old[k] = b1 * th0N[k] - b2 * prev.th0[k];
|
|
2283
2541
|
this.contactDStart = { left: b1 * DN.left - b2 * prev.D.left, right: b1 * DN.right - b2 * prev.D.right };
|
|
2542
|
+
this.portQStart = QN.map((q, k) => b1 * q - b2 * prev.Q[k]);
|
|
2284
2543
|
dtEff = dt / a0;
|
|
2285
2544
|
} else {
|
|
2286
2545
|
this.cOld.set(cN);
|
|
2546
|
+
this.thOld.set(thN);
|
|
2547
|
+
this.th0Old.set(th0N);
|
|
2287
2548
|
this.contactDStart = DN;
|
|
2549
|
+
this.portQStart = QN;
|
|
2288
2550
|
}
|
|
2289
2551
|
// Implicit: sources at the step's end, as seen from within the step (before any jump there).
|
|
2290
2552
|
// A step landing on a breakpoint ends on it exactly.
|
|
@@ -2300,11 +2562,12 @@ export class Solver {
|
|
|
2300
2562
|
}
|
|
2301
2563
|
result.bdf = bdf;
|
|
2302
2564
|
if (result.converged) {
|
|
2303
|
-
this.history.unshift({ t: this.time, dt, u: Float64Array.from(this.uPrev), c: cN, D: DN });
|
|
2565
|
+
this.history.unshift({ t: this.time, dt, u: Float64Array.from(this.uPrev), c: cN, th: thN, th0: th0N, D: DN, Q: QN });
|
|
2304
2566
|
if (this.history.length > 3) this.history.length = 3;
|
|
2305
2567
|
this.time = tEnd;
|
|
2306
2568
|
this.lastDt = dtEff;
|
|
2307
2569
|
this.contactDOld = this.contactDStart;
|
|
2570
|
+
this.portQOld = this.portQStart;
|
|
2308
2571
|
this._accumulateBoundaryIntake(dtEff, cN);
|
|
2309
2572
|
} else {
|
|
2310
2573
|
this.u.set(this.uPrev);
|
|
@@ -2588,12 +2851,17 @@ export class Solver {
|
|
|
2588
2851
|
u: Float64Array.from(this.u),
|
|
2589
2852
|
uLo: Float64Array.from(this.uLo),
|
|
2590
2853
|
cOld: Float64Array.from(this.cOld),
|
|
2854
|
+
thOld: Float64Array.from(this.thOld),
|
|
2855
|
+
th0Old: Float64Array.from(this.th0Old),
|
|
2591
2856
|
time: this.time,
|
|
2592
2857
|
lastDt: this.lastDt,
|
|
2593
2858
|
atSteady: this.atSteady,
|
|
2594
2859
|
contactDStart: this.contactDStart,
|
|
2595
2860
|
contactDOld: this.contactDOld,
|
|
2596
2861
|
contactDEnd: this.contactDEnd,
|
|
2862
|
+
portQStart: this.portQStart,
|
|
2863
|
+
portQOld: this.portQOld,
|
|
2864
|
+
portQEnd: this.portQEnd,
|
|
2597
2865
|
boundaryIntake: Float64Array.from(this.boundaryIntake),
|
|
2598
2866
|
history: this.history.slice(),
|
|
2599
2867
|
termV: Float64Array.from(this.termV),
|
|
@@ -2605,12 +2873,17 @@ export class Solver {
|
|
|
2605
2873
|
this.u.set(s.u);
|
|
2606
2874
|
this.uLo.set(s.uLo);
|
|
2607
2875
|
this.cOld.set(s.cOld);
|
|
2876
|
+
this.thOld.set(s.thOld);
|
|
2877
|
+
this.th0Old.set(s.th0Old);
|
|
2608
2878
|
this.time = s.time;
|
|
2609
2879
|
this.lastDt = s.lastDt;
|
|
2610
2880
|
this.atSteady = s.atSteady;
|
|
2611
2881
|
this.contactDStart = s.contactDStart;
|
|
2612
2882
|
this.contactDOld = s.contactDOld;
|
|
2613
2883
|
this.contactDEnd = s.contactDEnd;
|
|
2884
|
+
this.portQStart = s.portQStart;
|
|
2885
|
+
this.portQOld = s.portQOld;
|
|
2886
|
+
this.portQEnd = s.portQEnd;
|
|
2614
2887
|
this.boundaryIntake.set(s.boundaryIntake);
|
|
2615
2888
|
this.history = s.history.slice();
|
|
2616
2889
|
this.termV.set(s.termV);
|
|
@@ -2647,8 +2920,11 @@ export class Solver {
|
|
|
2647
2920
|
this.sourceTime = this.time;
|
|
2648
2921
|
this.computeConcentrations();
|
|
2649
2922
|
this.cOld.set(this.c);
|
|
2923
|
+
this.thOld.set(this.th);
|
|
2924
|
+
this.th0Old.set(this.th0);
|
|
2650
2925
|
this.assemble(Infinity);
|
|
2651
2926
|
this.contactDStart = { ...this.contactD };
|
|
2927
|
+
this.portQStart = Float64Array.from(this.portQ);
|
|
2652
2928
|
this.assemble(Infinity);
|
|
2653
2929
|
// The steady parts, then the storage parts from an assembly at a tiny dt.
|
|
2654
2930
|
const J = { A: sys.A.slice(), B: sys.B.slice(), C: sys.C.slice() };
|
|
@@ -2910,7 +3186,7 @@ export class Solver {
|
|
|
2910
3186
|
const reg = regions[r], mat = materials[reg.material];
|
|
2911
3187
|
if (mat.conductor) {
|
|
2912
3188
|
for (const g of nodes) this._nodeConductor(g, dt);
|
|
2913
|
-
for (const s of segs) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s]);
|
|
3189
|
+
for (const s of segs) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s] / grid.segArea[s]);
|
|
2914
3190
|
continue;
|
|
2915
3191
|
}
|
|
2916
3192
|
for (const g of nodes) {
|
|
@@ -2920,7 +3196,7 @@ export class Solver {
|
|
|
2920
3196
|
const rxs = this.rxsIn[reg.material];
|
|
2921
3197
|
if (rxs.length > 0) for (const g of nodes) this._bulkReactions(g, rxs);
|
|
2922
3198
|
for (const s of segs) {
|
|
2923
|
-
const bL = s + r, bR = bL + 1, h = grid.segLength[s];
|
|
3199
|
+
const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s]; // (a length over the cross-section: fluxes are totals)
|
|
2924
3200
|
this._segmentDisplacement(s, bL, bR, mat, h);
|
|
2925
3201
|
if (reg.mixing > 0) this._segmentMixing(s, bL, bR, reg.mixing, h, mat);
|
|
2926
3202
|
if (!mat.ideal) this._segmentConcentrated(s, bL, bR, mat, h, reg.velocity);
|
|
@@ -2936,6 +3212,7 @@ export class Solver {
|
|
|
2936
3212
|
_accumulateBoundaryIntake(dt, cN) {
|
|
2937
3213
|
this._assembleBookkeeping(dt);
|
|
2938
3214
|
this.contactDEnd = { ...this.contactD };
|
|
3215
|
+
this.portQEnd = Float64Array.from(this.portQ);
|
|
2939
3216
|
if (!Number.isFinite(dt)) return;
|
|
2940
3217
|
const last = this.model.regions.length - 1;
|
|
2941
3218
|
const { n, cOld } = this, vol = this.model.grid.vol;
|