driftlet 0.6.0 → 0.8.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 +15 -6
- package/docs/device.md +138 -10
- package/docs/kit.md +58 -4
- package/docs/numerics.md +16 -0
- package/llms.txt +78 -15
- package/package.json +1 -1
- package/src/checks.js +59 -24
- package/src/describe.js +21 -3
- package/src/device.js +126 -17
- package/src/grid.js +73 -0
- package/src/index.js +1 -1
- package/src/junction.js +164 -0
- package/src/kit.js +2 -0
- package/src/polarization.js +84 -0
- package/src/recorder.js +1 -1
- package/src/solution.js +12 -4
- package/src/solver.js +240 -47
- package/src/types.js +15 -4
- package/types/grid.d.ts +29 -0
- package/types/index.d.ts +3 -3
- package/types/junction.d.ts +32 -0
- package/types/kit.d.ts +2 -0
- package/types/polarization.d.ts +31 -0
- package/types/recorder.d.ts +3 -3
- package/types/solver.d.ts +6 -2
- package/types/types.d.ts +41 -4
package/src/solver.js
CHANGED
|
@@ -374,6 +374,13 @@ export class Solver {
|
|
|
374
374
|
nu: Float64Array.from([...rx.reactants.map((p) => p.nu), ...rx.products.map((p) => -p.nu)]),
|
|
375
375
|
}));
|
|
376
376
|
|
|
377
|
+
// Each reacting port's electrode area per volume at its window's nodes (a profile's mean over
|
|
378
|
+
// each node's box, as for a rate constant), and its reactions' rates there, mol/(m²·s).
|
|
379
|
+
this.portArea = model.ports.map((port) =>
|
|
380
|
+
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)),
|
|
381
|
+
);
|
|
382
|
+
this.portRates = model.ports.map((port) => port.reactions.map(() => new Float64Array(port.nodes.length)));
|
|
383
|
+
|
|
377
384
|
// Whether any region is strictly neutral (ε = 0, not a conductor): see integrate().
|
|
378
385
|
this.strictlyNeutral = model.regions.some((reg) => {
|
|
379
386
|
const mat = materials[reg.material];
|
|
@@ -395,8 +402,14 @@ export class Solver {
|
|
|
395
402
|
for (const port of model.ports) for (const g of port.nodes) need[g] = 1;
|
|
396
403
|
// Nodes of strictly neutral (ε = 0) regions that stay neutral, where φ is defined: interior
|
|
397
404
|
// 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).
|
|
405
|
+
// charge), away from the contacts and from ports that hold a level (which replaces a balance
|
|
406
|
+
// row). On a transient step these are solved in better-conditioned terms (see _chargeRows).
|
|
407
|
+
// A port that only exchanges (an O₂ supply, a leak) is solved there too: left out, its window
|
|
408
|
+
// fell back on the ill-conditioned terms, and a closed neutral electrolyte whose potential is
|
|
409
|
+
// set only by a face reaction couldn't take a step.
|
|
410
|
+
const held = new Uint8Array(nNodes);
|
|
411
|
+
held[0] = held[nNodes - 1] = 1;
|
|
412
|
+
for (const port of model.ports) if (port.species.some((l) => l.type === 'equilibrium')) for (const g of port.nodes) held[g] = 1;
|
|
400
413
|
this.chargeNode = new Uint8Array(nNodes);
|
|
401
414
|
const neutralFace = (f) => f >= 0 && f < model.interfaces.length && model.interfaces[f].phi.type === 'neutral';
|
|
402
415
|
regions.forEach((reg, r) => {
|
|
@@ -405,7 +418,7 @@ export class Solver {
|
|
|
405
418
|
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
|
|
406
419
|
for (let g = g0; g <= g1; g++) {
|
|
407
420
|
const edgeOk = (g > g0 || neutralFace(r - 1)) && (g < g1 || neutralFace(r));
|
|
408
|
-
if (edgeOk && !
|
|
421
|
+
if (edgeOk && !held[g] && !this.phiUndefined[g]) this.chargeNode[g] = 1;
|
|
409
422
|
}
|
|
410
423
|
});
|
|
411
424
|
this.chargeNodes = Int32Array.from([...this.chargeNode.keys()].filter((g) => this.chargeNode[g]));
|
|
@@ -502,10 +515,15 @@ export class Solver {
|
|
|
502
515
|
if (rx.kf[m] > 0 && [...rx.reactants, ...rx.products].some((p) => p.i === i)) reactive = true;
|
|
503
516
|
}
|
|
504
517
|
}
|
|
505
|
-
const
|
|
506
|
-
const
|
|
518
|
+
const inside = (port) => port.region >= r0 && port.region <= r;
|
|
519
|
+
const linked = model.ports.flatMap((port, k) => (inside(port) && port.species[i].type !== 'blocked' ? [k] : []));
|
|
520
|
+
// made or consumed by a port's reactions (an electrode spread through its window)
|
|
521
|
+
const reacting = model.ports.flatMap((port, k) => (inside(port) && port.reactions.some((rx) => rx.part.some((p) => p.side === 0 && p.i === i)) ? [k] : []));
|
|
522
|
+
if (reacting.length > 0) reactive = true;
|
|
523
|
+
const connected = leftOpen || rightOpen || linked.length > 0;
|
|
507
524
|
this.stretches.push({
|
|
508
|
-
ports
|
|
525
|
+
linked, // ports that hold or exchange it
|
|
526
|
+
ports: [...new Set([...linked, ...reacting])], // ports that bring any in
|
|
509
527
|
species: i,
|
|
510
528
|
regions: [r0, r],
|
|
511
529
|
nodes: [grid.regionStart[r0], grid.regionEnd[r]],
|
|
@@ -635,6 +653,13 @@ export class Solver {
|
|
|
635
653
|
rows.push(row);
|
|
636
654
|
}
|
|
637
655
|
});
|
|
656
|
+
for (const port of model.ports) {
|
|
657
|
+
for (const rx of port.reactions) {
|
|
658
|
+
const row = new Float64Array(S); // (the electrode's carrier is outside the device)
|
|
659
|
+
for (const p of rx.part) if (p.side === 0) row[this.stretchOf[port.region * n + p.i]] += p.nu;
|
|
660
|
+
rows.push(row);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
638
663
|
model.regions.forEach((reg, q) => {
|
|
639
664
|
for (const rx of model.reactions) {
|
|
640
665
|
if (!(rx.kf[reg.material] > 0)) continue;
|
|
@@ -820,11 +845,11 @@ export class Solver {
|
|
|
820
845
|
});
|
|
821
846
|
}
|
|
822
847
|
|
|
823
|
-
/** η_i (μ̄/RT) of a port's outside level for species i. */
|
|
848
|
+
/** η_i (μ̄/RT) of a port's outside level for species i (its electrode's carrier: at V itself). */
|
|
824
849
|
portEta(port, i) {
|
|
825
850
|
const link = port.species[i];
|
|
826
851
|
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;
|
|
852
|
+
return this.z[i] === 0 ? link.mu / this.model.RT : (this.z[i] * (V + (link.offset ?? 0))) / this.VT;
|
|
828
853
|
}
|
|
829
854
|
|
|
830
855
|
/** η_i/RT that a fixed contact link imposes (or NaN if the link isn't fixed). */
|
|
@@ -890,7 +915,7 @@ export class Solver {
|
|
|
890
915
|
} else {
|
|
891
916
|
const left = st.regions[0] === 0 ? this.contactEta('left', i) : NaN;
|
|
892
917
|
eta[i] = Number.isFinite(left) ? left : this.contactEta('right', i);
|
|
893
|
-
if (!Number.isFinite(eta[i]) && st.
|
|
918
|
+
if (!Number.isFinite(eta[i]) && st.linked.length > 0) eta[i] = this.portEta(model.ports[st.linked[0]], i);
|
|
894
919
|
mode[i] = 1;
|
|
895
920
|
if (!Number.isFinite(eta[i])) {
|
|
896
921
|
// Fed only through reactions (no level held at a contact): start from c0.
|
|
@@ -961,6 +986,25 @@ export class Solver {
|
|
|
961
986
|
}
|
|
962
987
|
}
|
|
963
988
|
}
|
|
989
|
+
const electrode = model.ports.find((port) => port.region === r && port.reactions.length > 0);
|
|
990
|
+
if (!mat.phiFree && !responds && mat.ideal && electrode) {
|
|
991
|
+
// Nor here, but an electrode spread through it does: start with the port's first reaction
|
|
992
|
+
// that can be balanced at equilibrium with the port's level (the electrode at open circuit).
|
|
993
|
+
for (const rx of electrode.reactions) {
|
|
994
|
+
let a = rx.fixedA, s = 0;
|
|
995
|
+
for (const p of rx.part) {
|
|
996
|
+
if (p.side === 1) a -= p.nu * this.portEta(electrode, p.i);
|
|
997
|
+
else if (mode[p.i] === 2) {
|
|
998
|
+
a -= p.nu * (Math.log(cFix[p.i] / mat.cRef[p.i]) + mat.mu0[p.i] / model.RT);
|
|
999
|
+
s += p.nu * z[p.i];
|
|
1000
|
+
} else s = NaN;
|
|
1001
|
+
}
|
|
1002
|
+
if (s !== 0 && Number.isFinite(s)) {
|
|
1003
|
+
phiHat = a / s;
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
964
1008
|
const neutralPhi = (ph) => {
|
|
965
1009
|
let lo = ph - 1, hi = ph + 1;
|
|
966
1010
|
while (charge(lo) < 0 && lo > -1e4) lo -= 2 * (hi - lo);
|
|
@@ -995,7 +1039,67 @@ export class Solver {
|
|
|
995
1039
|
}
|
|
996
1040
|
}
|
|
997
1041
|
}
|
|
1042
|
+
// A floating contact facing a held one starts where the start's own composition puts it: the
|
|
1043
|
+
// held voltage plus the difference between the two terminal species' levels beside each
|
|
1044
|
+
// contact (c0 sets them, and φ was chosen for neutrality, not to match either contact), so
|
|
1045
|
+
// that a transient's first reading means something.
|
|
1046
|
+
const level = (side) => {
|
|
1047
|
+
const ct = model.contacts[side], i = ct.terminal;
|
|
1048
|
+
if (i === null || ct.species[i].type !== 'equilibrium' || z[i] === 0) return NaN;
|
|
1049
|
+
const b = this.blockOfNode[side === 'left' ? 0 : grid.nNodes - 1];
|
|
1050
|
+
return (this.VT * u[b * M + 1 + i]) / z[i] - ct.species[i].offset;
|
|
1051
|
+
};
|
|
1052
|
+
for (const k of this.floating) {
|
|
1053
|
+
const t = this.terms[k], held = k === 0 ? 1 : 0;
|
|
1054
|
+
if (t.kind === 'port' || held >= this.terms.length || this.floating.includes(held)) continue;
|
|
1055
|
+
const shift = level(t.side) - level(this.terms[held].side);
|
|
1056
|
+
if (Number.isFinite(shift)) this.termV[k] = this.termV[held] + shift;
|
|
1057
|
+
}
|
|
998
1058
|
this.computeConcentrations();
|
|
1059
|
+
// A floating electrode spread through a port starts where its reactions pass the current it's
|
|
1060
|
+
// set (none, behind a resistance): at its mixed potential in the start's composition, not
|
|
1061
|
+
// level with a held terminal, which can be volts away and pass an absurd current.
|
|
1062
|
+
for (const k of this.floating) {
|
|
1063
|
+
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1064
|
+
if (!port || port.reactions.length === 0) continue;
|
|
1065
|
+
const want = t.drive.kind === 'I' ? sourceAt(t.drive.src, this.time) : 0;
|
|
1066
|
+
const current = (V) => {
|
|
1067
|
+
this.termV[k] = V;
|
|
1068
|
+
let I = 0;
|
|
1069
|
+
port.reactions.forEach((rx) => {
|
|
1070
|
+
let q = 0;
|
|
1071
|
+
for (const p of rx.part) if (p.side === 0) q += p.nu * z[p.i];
|
|
1072
|
+
port.nodes.forEach((g, w) => (I += q * FARADAY * this.model.grid.vol[g] * this.portArea[t.index][w] * this._portRate(port, rx, g)));
|
|
1073
|
+
});
|
|
1074
|
+
return I;
|
|
1075
|
+
};
|
|
1076
|
+
// Current into the device rises with V (oxidation); bracket, then bisect.
|
|
1077
|
+
let lo = this.termV[k] - 1, hi = this.termV[k] + 1;
|
|
1078
|
+
while (current(lo) > want && lo > -1e3) lo -= 2 * (hi - lo);
|
|
1079
|
+
while (current(hi) < want && hi < 1e3) hi += 2 * (hi - lo);
|
|
1080
|
+
for (let it = 0; it < 200 && hi - lo > 1e-12; it++) {
|
|
1081
|
+
const m = 0.5 * (lo + hi);
|
|
1082
|
+
if (current(m) < want) lo = m;
|
|
1083
|
+
else hi = m;
|
|
1084
|
+
}
|
|
1085
|
+
this.termV[k] = 0.5 * (lo + hi);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// A port reaction's rate per area at node g (mol/(m²·s), forward), at the present state.
|
|
1090
|
+
_portRate(port, rx, g) {
|
|
1091
|
+
const { n, M, u, uLo, c } = this, b = this.blockOfNode[g];
|
|
1092
|
+
let pref = rx.k0, a = rx.fixedA;
|
|
1093
|
+
for (const p of rx.part) {
|
|
1094
|
+
if (p.side === 1) {
|
|
1095
|
+
a -= p.nu * this.portEta(port, p.i);
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
a -= p.nu * (u[b * M + 1 + p.i] + uLo[b * M + 1 + p.i]);
|
|
1099
|
+
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], p.nu < 0 ? -p.nu * (1 - rx.alpha) : p.nu * rx.alpha);
|
|
1100
|
+
}
|
|
1101
|
+
// (far from equilibrium, the plain difference: bvFactor's product would give 0·∞ there)
|
|
1102
|
+
return pref * (Math.abs(a) > 50 ? Math.exp(rx.alpha * a) - Math.exp(-(1 - rx.alpha) * a) : bvFactor(a, rx.alpha).g);
|
|
999
1103
|
}
|
|
1000
1104
|
|
|
1001
1105
|
// A non-ideal material's composition at φ̂ = ph: level-fixed species (mode 1) at their η,
|
|
@@ -1038,7 +1142,7 @@ export class Solver {
|
|
|
1038
1142
|
// Excess carriers: zero in the bulk, the surface sheet at a charged face.
|
|
1039
1143
|
for (let i = 0; i < n; i++) c[g * n + i] = 0;
|
|
1040
1144
|
const f = this.sheetFace[g];
|
|
1041
|
-
if (f >= 0) c[g * n + im] = (this.sheetSign[g] * u[this.blockOfFace[f] * M]) / (z[im] * FARADAY * this.model.grid.vol[g]);
|
|
1145
|
+
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]);
|
|
1042
1146
|
continue;
|
|
1043
1147
|
}
|
|
1044
1148
|
for (let i = 0; i < n; i++) {
|
|
@@ -1149,7 +1253,7 @@ export class Solver {
|
|
|
1149
1253
|
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
|
|
1150
1254
|
if (mat.conductor) {
|
|
1151
1255
|
for (let g = g0; g <= g1; g++) this._nodeConductor(g, dt);
|
|
1152
|
-
for (let s = g0; s < g1; s++) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s]);
|
|
1256
|
+
for (let s = g0; s < g1; s++) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s] / grid.segArea[s]);
|
|
1153
1257
|
continue;
|
|
1154
1258
|
}
|
|
1155
1259
|
if (mat.ideal) this._nodesDilute(g0, g1, dt);
|
|
@@ -1157,7 +1261,7 @@ export class Solver {
|
|
|
1157
1261
|
const rxs = this.rxsIn[reg.material];
|
|
1158
1262
|
if (rxs.length > 0) for (let g = g0; g <= g1; g++) this._bulkReactions(g, rxs);
|
|
1159
1263
|
for (let s = g0; s < g1; s++) {
|
|
1160
|
-
const bL = s + r, bR = bL + 1, h = grid.segLength[s];
|
|
1264
|
+
const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s]; // (a length over the cross-section: fluxes are totals)
|
|
1161
1265
|
this._segmentDisplacement(s, bL, bR, mat, h);
|
|
1162
1266
|
if (reg.mixing > 0) this._segmentMixing(s, bL, bR, reg.mixing, h, mat);
|
|
1163
1267
|
if (!mat.ideal) this._segmentConcentrated(s, bL, bR, mat, h, reg.velocity);
|
|
@@ -1185,7 +1289,7 @@ export class Solver {
|
|
|
1185
1289
|
const r = 1 + im, k = g * n + im;
|
|
1186
1290
|
res[R[b * M + r]] += (v * (c[k] - cOld[k])) / dt;
|
|
1187
1291
|
const f = this.sheetFace[g];
|
|
1188
|
-
if (f >= 0) this._j(b, r, this.blockOfFace[f], 0, this.sheetSign[g] / (z[im] * FARADAY * dt));
|
|
1292
|
+
if (f >= 0) this._j(b, r, this.blockOfFace[f], 0, (this.sheetSign[g] * this.model.grid.area[g]) / (z[im] * FARADAY * dt));
|
|
1189
1293
|
}
|
|
1190
1294
|
|
|
1191
1295
|
// Dilute nodes g0…g1 (ideal statistics, c = c_ref e^ζ): storage of each species and the space
|
|
@@ -1434,9 +1538,9 @@ export class Solver {
|
|
|
1434
1538
|
_segmentsDilute(g0, g1, region, mat, vel) {
|
|
1435
1539
|
const { n, M, u, uLo, res, c, z, sys, loc } = this, R = this.rix, F = FARADAY;
|
|
1436
1540
|
const { A: JA, B: JB, C: JC, sizes, offA, offB, offC } = sys;
|
|
1437
|
-
const segLength = this.model.grid
|
|
1541
|
+
const { segLength, segArea } = this.model.grid;
|
|
1438
1542
|
for (let s = g0; s < g1; s++) {
|
|
1439
|
-
const bL = s + region, bR = bL + 1, h = segLength[s];
|
|
1543
|
+
const bL = s + region, bR = bL + 1, h = segLength[s] / segArea[s]; // (fluxes are totals through the cross-section)
|
|
1440
1544
|
const mL = sizes[bL], mR = sizes[bR], pL = loc[bL * M], pR = loc[bR * M];
|
|
1441
1545
|
const phiL = u[bL * M], phiR = u[bR * M];
|
|
1442
1546
|
for (let i = 0; i < n; i++) {
|
|
@@ -1519,15 +1623,16 @@ export class Solver {
|
|
|
1519
1623
|
this._j(bf, 0, bL, 0, -kC);
|
|
1520
1624
|
}
|
|
1521
1625
|
}
|
|
1522
|
-
// D_f enters each side's Gauss row; a metal side holds it as surface carriers instead.
|
|
1523
|
-
|
|
1626
|
+
// D_f enters each side's Gauss row; a metal side holds it as surface carriers instead. The
|
|
1627
|
+
// flux node's unknowns are per area, so what they carry is that times the face's area.
|
|
1628
|
+
const gL = grid.regionEnd[f], gR = gL + 1, Af = grid.area[gL];
|
|
1524
1629
|
if (this.nodeConductor[gL] < 0) {
|
|
1525
|
-
res[R[bL * M]] += u[bf * M];
|
|
1526
|
-
this._j(bL, 0, bf, 0,
|
|
1630
|
+
res[R[bL * M]] += Af * u[bf * M];
|
|
1631
|
+
this._j(bL, 0, bf, 0, Af);
|
|
1527
1632
|
}
|
|
1528
1633
|
if (this.nodeConductor[gR] < 0) {
|
|
1529
|
-
res[R[bR * M]] -= u[bf * M] + itf.sheetCharge;
|
|
1530
|
-
this._j(bR, 0, bf, 0, -
|
|
1634
|
+
res[R[bR * M]] -= Af * (u[bf * M] + itf.sheetCharge);
|
|
1635
|
+
this._j(bR, 0, bf, 0, -Af);
|
|
1531
1636
|
}
|
|
1532
1637
|
for (let i = 0; i < n; i++) {
|
|
1533
1638
|
const r = 1 + i, o = bf * M + r;
|
|
@@ -1538,6 +1643,27 @@ export class Solver {
|
|
|
1538
1643
|
res[R[o]] = -deta; // μ̄ continuous
|
|
1539
1644
|
this._j(bf, r, bR, r, 1);
|
|
1540
1645
|
this._j(bf, r, bL, r, -1);
|
|
1646
|
+
} else if (type === 'permeability') {
|
|
1647
|
+
// Electrodiffusion through a thin membrane in a constant field: Scharfetter–Gummel across
|
|
1648
|
+
// the face with D/h → P, its drift taken over the face's whole jump in the standard level
|
|
1649
|
+
// zφ̂ + μ°/RT − ln c_ref (the φ jump of a capacitive face, and any step between the two
|
|
1650
|
+
// materials). Between like solutions it's Goldman–Hodgkin–Katz's flux,
|
|
1651
|
+
// N = P·zu·(c_L − c_R e^{zu})/(e^{zu} − 1), u = (φ_R − φ_L)/V_T,
|
|
1652
|
+
// and it's exactly zero where μ̄ is level.
|
|
1653
|
+
const gl = grid.regionEnd[f], gr = gl + 1, kl = gl * n + i, kr = gr * n + i, zi = z[i], P = itf.links[i].P;
|
|
1654
|
+
const shift = this.mu0hat[kr] - this.mu0hat[kl] - Math.log(this.cRef[kr] / this.cRef[kl]);
|
|
1655
|
+
const d = zi * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M])) + shift;
|
|
1656
|
+
const E = Math.expm1(-deta); // η_R − η_L
|
|
1657
|
+
const gBc = P * bernoulli(d) * this.c[kl];
|
|
1658
|
+
const dNdd = -P * bernoulliDerivative(d) * this.c[kl] * E;
|
|
1659
|
+
res[R[o]] = u[o] + gBc * E;
|
|
1660
|
+
this._j(bf, r, bf, r, 1);
|
|
1661
|
+
this._j(bf, r, bL, r, -gBc);
|
|
1662
|
+
this._j(bf, r, bR, r, gBc * (E + 1));
|
|
1663
|
+
if (zi !== 0) {
|
|
1664
|
+
this._j(bf, r, bL, 0, zi * dNdd - zi * gBc * E);
|
|
1665
|
+
this._j(bf, r, bR, 0, -zi * dNdd);
|
|
1666
|
+
}
|
|
1541
1667
|
} else {
|
|
1542
1668
|
// conductance: J = G (V_L − V_R), V = V_T η / z ⇒ N = G V_T (η_L − η_R) / (z² F)
|
|
1543
1669
|
const gG = (itf.links[i].G * VT) / (z[i] * z[i] * F);
|
|
@@ -1546,12 +1672,12 @@ export class Solver {
|
|
|
1546
1672
|
this._j(bf, r, bL, r, -gG);
|
|
1547
1673
|
this._j(bf, r, bR, r, gG);
|
|
1548
1674
|
}
|
|
1549
|
-
res[R[bL * M + r]] += u[o];
|
|
1550
|
-
this._j(bL, r, bf, r,
|
|
1551
|
-
res[R[bR * M + r]] -= u[o];
|
|
1552
|
-
this._j(bR, r, bf, r, -
|
|
1675
|
+
res[R[bL * M + r]] += Af * u[o];
|
|
1676
|
+
this._j(bL, r, bf, r, Af);
|
|
1677
|
+
res[R[bR * M + r]] -= Af * u[o];
|
|
1678
|
+
this._j(bR, r, bf, r, -Af);
|
|
1553
1679
|
}
|
|
1554
|
-
itf.reactions.forEach((rx, k) => this._faceReaction(rx, k, f, bf, bL, bR));
|
|
1680
|
+
itf.reactions.forEach((rx, k) => this._faceReaction(rx, k, f, bf, bL, bR, Af));
|
|
1555
1681
|
}
|
|
1556
1682
|
|
|
1557
1683
|
// Scharfetter–Gummel with non-ideal statistics. The excess ex = ζ − ln(c/c_ref) acts as an
|
|
@@ -1768,7 +1894,7 @@ export class Solver {
|
|
|
1768
1894
|
// with the row r_k − rate(u_L, u_R) = 0, and each participant's edge node takes ν·r_k (made
|
|
1769
1895
|
// there when ν > 0). The rate couples the two edge nodes only through the face block between
|
|
1770
1896
|
// them, which keeps the system block-tridiagonal.
|
|
1771
|
-
_faceReaction(rx, k, f, bf, bL, bR) {
|
|
1897
|
+
_faceReaction(rx, k, f, bf, bL, bR, Af) {
|
|
1772
1898
|
const { n, M, u, uLo, c, res } = this;
|
|
1773
1899
|
const R = this.rix;
|
|
1774
1900
|
const grid = this.model.grid;
|
|
@@ -1780,12 +1906,28 @@ export class Solver {
|
|
|
1780
1906
|
const g = p.side ? gR : gL, b = p.side ? bR : bL, o = b * M + 1 + p.i;
|
|
1781
1907
|
aHi -= p.nu * u[o];
|
|
1782
1908
|
aLo -= p.nu * uLo[o];
|
|
1783
|
-
if (rx.srh || this.nodeConductor[g] === p.i) continue; // a conductor's carrier has activity 1
|
|
1909
|
+
if (rx.srh || rx.vmax || this.nodeConductor[g] === p.i) continue; // a conductor's carrier has activity 1
|
|
1784
1910
|
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
1785
1911
|
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
1786
1912
|
this._dlnc(g, p.i, e, p.side ? dR : dL);
|
|
1787
1913
|
}
|
|
1788
|
-
if (rx.
|
|
1914
|
+
if (rx.vmax) {
|
|
1915
|
+
// Saturating: r = vmax Π (c/(c + K))^{|ν|} (1 − e^{−a}); ∂ln S/∂ln c = |ν| K/(c + K).
|
|
1916
|
+
let S = 1;
|
|
1917
|
+
rx.part.forEach((p, q) => {
|
|
1918
|
+
if (p.nu >= 0) return;
|
|
1919
|
+
const g = p.side ? gR : gL, cc = c[g * n + p.i], K = rx.K[q];
|
|
1920
|
+
S *= powi(cc / (cc + K), -p.nu);
|
|
1921
|
+
this._dlnc(g, p.i, (-p.nu * K) / (cc + K), p.side ? dR : dL);
|
|
1922
|
+
});
|
|
1923
|
+
const a = aHi + aLo, back = Math.exp(-a);
|
|
1924
|
+
rate = rx.vmax * S * -Math.expm1(-a);
|
|
1925
|
+
for (let s = 0; s < M; s++) {
|
|
1926
|
+
dL[s] *= rate;
|
|
1927
|
+
dR[s] *= rate;
|
|
1928
|
+
}
|
|
1929
|
+
for (const p of rx.part) (p.side ? dR : dL)[1 + p.i] -= rx.vmax * S * back * p.nu;
|
|
1930
|
+
} else if (rx.srh) {
|
|
1789
1931
|
// SRH, with n and p each at its own side's edge node.
|
|
1790
1932
|
const { n: pn, p: pp } = rx.srh, gn = pn.side ? gR : gL, gq = pp.side ? gR : gL;
|
|
1791
1933
|
const s = this._srhRate(rx.srh, c[gn * n + pn.i], c[gq * n + pp.i], aHi + aLo);
|
|
@@ -1812,8 +1954,8 @@ export class Solver {
|
|
|
1812
1954
|
}
|
|
1813
1955
|
for (const p of rx.part) {
|
|
1814
1956
|
const b = p.side ? bR : bL;
|
|
1815
|
-
res[R[b * M + 1 + p.i]] -= p.nu * u[o];
|
|
1816
|
-
this._j(b, 1 + p.i, bf, slot, -p.nu);
|
|
1957
|
+
res[R[b * M + 1 + p.i]] -= Af * p.nu * u[o];
|
|
1958
|
+
this._j(b, 1 + p.i, bf, slot, -Af * p.nu);
|
|
1817
1959
|
}
|
|
1818
1960
|
}
|
|
1819
1961
|
|
|
@@ -1895,9 +2037,58 @@ export class Solver {
|
|
|
1895
2037
|
}
|
|
1896
2038
|
}
|
|
1897
2039
|
}
|
|
2040
|
+
port.reactions.forEach((rx, x) => this._portReaction(port, rx, this.portArea[k - 2], this.portRates[k - 2][x], flux, k));
|
|
1898
2041
|
for (let i = 0; i < n; i++) this.termI[k] += z[i] * F * flux[i];
|
|
1899
2042
|
}
|
|
1900
2043
|
|
|
2044
|
+
// A port's reaction at each node of its window: Butler–Volmer per area, as at a face, against
|
|
2045
|
+
// the electrode's carrier at the port's level (activity 1), times the electrode's area per
|
|
2046
|
+
// volume. Each node's rate is explicit (no unknown of its own): it enters the balances of the
|
|
2047
|
+
// species it makes and consumes, and the port's current.
|
|
2048
|
+
_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;
|
|
2051
|
+
// The carrier's part of the affinity, and how it moves with the port's voltage.
|
|
2052
|
+
let aV = rx.fixedA, daV = 0, q = 0; // q: charge the forward reaction brings into the device, per event
|
|
2053
|
+
for (const p of rx.part) {
|
|
2054
|
+
if (p.side === 1) {
|
|
2055
|
+
aV -= p.nu * this.portEta(port, p.i);
|
|
2056
|
+
daV -= (p.nu * z[p.i]) / VT;
|
|
2057
|
+
} else q += p.nu * z[p.i];
|
|
2058
|
+
}
|
|
2059
|
+
port.nodes.forEach((g, w) => {
|
|
2060
|
+
const b = this.blockOfNode[g], s = vol[g] * area[w];
|
|
2061
|
+
d.fill(0);
|
|
2062
|
+
let pref = rx.k0, aHi = aV, aLo = 0;
|
|
2063
|
+
for (const p of rx.part) {
|
|
2064
|
+
if (p.side === 1) continue;
|
|
2065
|
+
const o = b * M + 1 + p.i;
|
|
2066
|
+
aHi -= p.nu * u[o];
|
|
2067
|
+
aLo -= p.nu * uLo[o];
|
|
2068
|
+
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
2069
|
+
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
2070
|
+
this._dlnc(g, p.i, e, d);
|
|
2071
|
+
}
|
|
2072
|
+
const { g: bv, gp } = bvFactor(aHi + aLo, al);
|
|
2073
|
+
const rate = pref * bv;
|
|
2074
|
+
rates[w] = rate;
|
|
2075
|
+
// ∂rate/∂slot = rate·∂ln(prefactor) + pref·g′·∂a, with ∂a/∂η_i = −ν
|
|
2076
|
+
for (let t = 0; t < M; t++) d[t] *= rate;
|
|
2077
|
+
for (const p of rx.part) if (p.side === 0) d[1 + p.i] -= pref * gp * p.nu;
|
|
2078
|
+
const dV = pref * gp * daV;
|
|
2079
|
+
for (const p of rx.part) {
|
|
2080
|
+
if (p.side === 1) continue;
|
|
2081
|
+
const row = 1 + p.i, o = b * M + row;
|
|
2082
|
+
res[R[o]] -= s * p.nu * rate;
|
|
2083
|
+
flux[p.i] += s * p.nu * rate;
|
|
2084
|
+
for (let t = 0; t < M; t++) if (d[t] !== 0) this._j(b, row, b, t, -s * p.nu * d[t]);
|
|
2085
|
+
B[R[o]] -= s * p.nu * dV;
|
|
2086
|
+
}
|
|
2087
|
+
for (let t = 0; t < M; t++) if (d[t] !== 0) C[R[b * M + t]] += q * F * s * d[t];
|
|
2088
|
+
this.termDI[k] += q * F * s * dV;
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
|
|
1901
2092
|
|
|
1902
2093
|
// One contact (terminal 0 or 1): record the flux through its outer face and the current into
|
|
1903
2094
|
// the device (read from the end box before anything here is added: whatever the box needs
|
|
@@ -1913,6 +2104,7 @@ export class Solver {
|
|
|
1913
2104
|
const b = this.blockOfNode[g];
|
|
1914
2105
|
const sgn = side === 'left' ? 1 : -1;
|
|
1915
2106
|
const flux = this.contactFlux[side];
|
|
2107
|
+
const Ac = model.grid.area[g]; // the links' laws are per area: what they pass is that times A here
|
|
1916
2108
|
const dyn = Number.isFinite(dt);
|
|
1917
2109
|
|
|
1918
2110
|
// Before anything is added, each balance residual is the flux into the device here.
|
|
@@ -1933,13 +2125,13 @@ export class Solver {
|
|
|
1933
2125
|
const link = ct.species[i];
|
|
1934
2126
|
const o = b * M + 1 + i;
|
|
1935
2127
|
if (link.type === 'exchange') {
|
|
1936
|
-
res[R[o]] -= link.k * (link.mu / model.RT - (u[o] + uLo[o]));
|
|
1937
|
-
this._j(b, 1 + i, b, 1 + i, link.k);
|
|
2128
|
+
res[R[o]] -= Ac * link.k * (link.mu / model.RT - (u[o] + uLo[o]));
|
|
2129
|
+
this._j(b, 1 + i, b, 1 + i, Ac * link.k);
|
|
1938
2130
|
} else if (link.type === 'conductance') {
|
|
1939
2131
|
const Vi = (VT * (u[o] + uLo[o])) / z[i];
|
|
1940
|
-
res[R[o]] -= (link.G * (Vt + link.offset - Vi)) / (z[i] * F);
|
|
1941
|
-
this._j(b, 1 + i, b, 1 + i, (link.G * VT) / (z[i] * z[i] * F));
|
|
1942
|
-
B[R[o]] += -link.G / (z[i] * F);
|
|
2132
|
+
res[R[o]] -= (Ac * link.G * (Vt + link.offset - Vi)) / (z[i] * F);
|
|
2133
|
+
this._j(b, 1 + i, b, 1 + i, (Ac * link.G * VT) / (z[i] * z[i] * F));
|
|
2134
|
+
B[R[o]] += (-Ac * link.G) / (z[i] * F);
|
|
1943
2135
|
}
|
|
1944
2136
|
}
|
|
1945
2137
|
|
|
@@ -1961,15 +2153,15 @@ export class Solver {
|
|
|
1961
2153
|
this.contactD[side] = 0;
|
|
1962
2154
|
} else if (link.type === 'capacitive') {
|
|
1963
2155
|
// Gate, or metal across a Stern layer, at φ_g = V_t − zeroCharge. D toward +x.
|
|
1964
|
-
const Din = link.C * (Vt - link.zeroCharge - VT * u[b * M]);
|
|
2156
|
+
const Din = Ac * link.C * (Vt - link.zeroCharge - VT * u[b * M]);
|
|
1965
2157
|
res[R[b * M]] -= Din;
|
|
1966
|
-
this._j(b, 0, b, 0, link.C * VT);
|
|
1967
|
-
B[R[b * M]] += -link.C;
|
|
2158
|
+
this._j(b, 0, b, 0, Ac * link.C * VT);
|
|
2159
|
+
B[R[b * M]] += -Ac * link.C;
|
|
1968
2160
|
this.contactD[side] = sgn * Din;
|
|
1969
2161
|
if (dyn) {
|
|
1970
2162
|
I += (Din - Dstart) / dt;
|
|
1971
|
-
C[R[b * M]] += (-link.C * VT) / dt;
|
|
1972
|
-
this.termDI[k] += link.C / dt;
|
|
2163
|
+
C[R[b * M]] += (-Ac * link.C * VT) / dt;
|
|
2164
|
+
this.termDI[k] += (Ac * link.C) / dt;
|
|
1973
2165
|
}
|
|
1974
2166
|
} else if (link.type === 'pinned' || link.type === 'bulk') {
|
|
1975
2167
|
// The Poisson residual is the outside's charge, D_in.
|
|
@@ -2447,10 +2639,10 @@ export class Solver {
|
|
|
2447
2639
|
if (!Array.isArray(probes)) fail('advance: probes must be an array of { x, species, quantity, region }');
|
|
2448
2640
|
const plan = probes.map((p, k) => {
|
|
2449
2641
|
const path = `advance: probes[${k}]`;
|
|
2450
|
-
const
|
|
2451
|
-
if (
|
|
2452
|
-
const
|
|
2453
|
-
if (quantity !== '
|
|
2642
|
+
const quantity = p?.quantity ?? 'c';
|
|
2643
|
+
if (quantity !== 'c' && quantity !== 'V' && quantity !== 'phi') fail(`${path}.quantity must be 'c' (mol/m³), 'V' (the species voltage) or 'phi' (φ, V)`);
|
|
2644
|
+
const i = quantity === 'phi' ? -1 : species.findIndex((sp) => sp.name === p?.species);
|
|
2645
|
+
if (quantity !== 'phi' && i < 0) fail(`${path}.species: no species ${JSON.stringify(p?.species)}`);
|
|
2454
2646
|
if (quantity === 'V' && species[i].z === 0) fail(`${path}: '${p.species}' is neutral, so it has no voltage; read 'c'`);
|
|
2455
2647
|
const x = grid.x, inside = (r) => p.x >= x[grid.regionStart[r]] && p.x <= x[grid.regionEnd[r]];
|
|
2456
2648
|
let r;
|
|
@@ -2470,6 +2662,7 @@ export class Solver {
|
|
|
2470
2662
|
const out = plan.map(() => []);
|
|
2471
2663
|
const { n, M } = this;
|
|
2472
2664
|
const at = (q, g) => {
|
|
2665
|
+
if (q.quantity === 'phi') return this.phiUndefined[g] ? NaN : this.VT * (this.u[this.blockOfNode[g] * M] + this.uLo[this.blockOfNode[g] * M]);
|
|
2473
2666
|
if (!this.present[g * n + q.i]) return NaN;
|
|
2474
2667
|
if (q.quantity === 'c') return this.c[g * n + q.i];
|
|
2475
2668
|
const o = this.blockOfNode[g] * M + 1 + q.i;
|
|
@@ -2856,7 +3049,7 @@ export class Solver {
|
|
|
2856
3049
|
const reg = regions[r], mat = materials[reg.material];
|
|
2857
3050
|
if (mat.conductor) {
|
|
2858
3051
|
for (const g of nodes) this._nodeConductor(g, dt);
|
|
2859
|
-
for (const s of segs) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s]);
|
|
3052
|
+
for (const s of segs) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s] / grid.segArea[s]);
|
|
2860
3053
|
continue;
|
|
2861
3054
|
}
|
|
2862
3055
|
for (const g of nodes) {
|
|
@@ -2866,7 +3059,7 @@ export class Solver {
|
|
|
2866
3059
|
const rxs = this.rxsIn[reg.material];
|
|
2867
3060
|
if (rxs.length > 0) for (const g of nodes) this._bulkReactions(g, rxs);
|
|
2868
3061
|
for (const s of segs) {
|
|
2869
|
-
const bL = s + r, bR = bL + 1, h = grid.segLength[s];
|
|
3062
|
+
const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s]; // (a length over the cross-section: fluxes are totals)
|
|
2870
3063
|
this._segmentDisplacement(s, bL, bR, mat, h);
|
|
2871
3064
|
if (reg.mixing > 0) this._segmentMixing(s, bL, bR, reg.mixing, h, mat);
|
|
2872
3065
|
if (!mat.ideal) this._segmentConcentrated(s, bL, bR, mat, h, reg.velocity);
|
package/src/types.js
CHANGED
|
@@ -154,9 +154,14 @@
|
|
|
154
154
|
* @property {Source} [V] held terminal voltage, V (default 0, if no I)
|
|
155
155
|
* @property {Source} [I] or: the current driven into the device, A/m² (e.g. 0 for a reference electrode)
|
|
156
156
|
* @property {number} [R] with V: a series resistance, Ω·m²
|
|
157
|
-
* @property {string} [terminal] species whose offset defaults to 0
|
|
157
|
+
* @property {string} [terminal] species whose offset defaults to 0; with reactions, the electrode's carrier
|
|
158
158
|
* @property {Record<string, 'blocked' | 'equilibrium' | { type: 'equilibrium', offset?: number, mu?: number }
|
|
159
|
-
* | { type: 'conductance', G: number, offset?: number } | { type: 'exchange', k: number, mu: number }>} species
|
|
159
|
+
* | { type: 'conductance', G: number, offset?: number } | { type: 'exchange', k: number, mu: number }>} [species]
|
|
160
|
+
* @property {{ equation: string, fixed?: Record<string, number>, k0: number, alpha: number }[]} [reactions] an
|
|
161
|
+
* electrode spread through the window: Butler–Volmer per area (k0 in mol/(m²·s)) between the region's
|
|
162
|
+
* species and the terminal species at the port's level
|
|
163
|
+
* @property {number | { x: number[], values: number[] }} [area] with reactions: electrode area per volume,
|
|
164
|
+
* m²/m³ (1/h for a film of thickness h), or a profile against the device's x
|
|
160
165
|
*/
|
|
161
166
|
|
|
162
167
|
/**
|
|
@@ -170,6 +175,10 @@
|
|
|
170
175
|
* @property {BulkReactionDef[]} [bulkReactions]
|
|
171
176
|
* @property {{ left?: ContactDef, right?: ContactDef }} [contacts]
|
|
172
177
|
* @property {PortDef[]} [ports] internal ports
|
|
178
|
+
* @property {'planar' | 'spherical' | 'cylindrical' | { type: 'planar' | 'spherical' | 'cylindrical', r0?: number }
|
|
179
|
+
* | { area: { x: number[], values: number[] } }} [geometry] the cross-section A(x): planar (A = 1 m², the
|
|
180
|
+
* default: currents read per m²), shells about a centre at r = r0 (m) left of x = 0 (cylindrical: per
|
|
181
|
+
* metre of length), or A in m² against x. Currents, fluxes, amounts and charges are then totals through A.
|
|
173
182
|
* @property {GridOptions} [grid]
|
|
174
183
|
*/
|
|
175
184
|
|
|
@@ -199,8 +208,10 @@
|
|
|
199
208
|
* then the ports by name): its voltage, and its current into the device (A/m²)
|
|
200
209
|
* @property {{ left: ContactResult, right: ContactResult }} contacts
|
|
201
210
|
* @property {{ left?: { V: number, D: number, charge: number }, right?: { V: number, D: number, charge: number } }} gates
|
|
202
|
-
* @property {{ name: string, V: number, flux: Record<string, number>, current: number
|
|
203
|
-
* internal port brings into the device
|
|
211
|
+
* @property {{ name: string, V: number, flux: Record<string, number>, current: number, x?: Float64Array,
|
|
212
|
+
* area?: Float64Array, rates?: Float64Array[] }[]} ports what each internal port brings into the device;
|
|
213
|
+
* with reactions, also its window's nodes (x, m), area per volume there, and each reaction's rate per area
|
|
214
|
+
* at each node (mol/(m²·s), forward)
|
|
204
215
|
* @property {{ left: string, right: string, dipole: number, sheetCharge: number, D: number, N: Record<string, number>, rates: number[] }[]} interfaces
|
|
205
216
|
* @property {{ rate: Float64Array, regions: number[], total: number }[]} bulkReactions each bulk reaction's
|
|
206
217
|
* forward rate per node (mol/(m³·s), NaN where it doesn't run), and per region and in all (mol/(m²·s))
|
package/types/grid.d.ts
CHANGED
|
@@ -26,3 +26,32 @@ export function buildGrid(regions: {
|
|
|
26
26
|
regionEnd: Int32Array<ArrayBuffer>;
|
|
27
27
|
length: number;
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Give a grid its cross-section: `area` (A at each node, m²), `segArea` (each segment's
|
|
31
|
+
* effective area, its length over ∫dx/A, so that a flux D·segArea·Δc/h is exact for steady
|
|
32
|
+
* diffusion between the nodes; A at its middle where A vanishes at an end, as at a sphere's
|
|
33
|
+
* centre) and `vol` (each node's box, ∫A dx). Planar (no geometry): A = 1, and nothing changes.
|
|
34
|
+
* @param {ReturnType<typeof buildGrid>} grid
|
|
35
|
+
* @param {{ type: 'planar' } | { type: 'spherical' | 'cylindrical', r0: number } | { type: 'profile', x: ArrayLike<number>, values: ArrayLike<number> }} geometry
|
|
36
|
+
*/
|
|
37
|
+
export function applyGeometry(grid: ReturnType<typeof buildGrid>, geometry: {
|
|
38
|
+
type: "planar";
|
|
39
|
+
} | {
|
|
40
|
+
type: "spherical" | "cylindrical";
|
|
41
|
+
r0: number;
|
|
42
|
+
} | {
|
|
43
|
+
type: "profile";
|
|
44
|
+
x: ArrayLike<number>;
|
|
45
|
+
values: ArrayLike<number>;
|
|
46
|
+
}): {
|
|
47
|
+
nNodes: number;
|
|
48
|
+
x: Float64Array<ArrayBuffer>;
|
|
49
|
+
nodeRegion: Int32Array<ArrayBuffer>;
|
|
50
|
+
segLength: Float64Array<ArrayBuffer>;
|
|
51
|
+
segRegion: Int32Array<ArrayBuffer>;
|
|
52
|
+
segFace: Int32Array<ArrayBuffer>;
|
|
53
|
+
vol: Float64Array<ArrayBuffer>;
|
|
54
|
+
regionStart: Int32Array<ArrayBuffer>;
|
|
55
|
+
regionEnd: Int32Array<ArrayBuffer>;
|
|
56
|
+
length: number;
|
|
57
|
+
};
|
package/types/index.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export class Device {
|
|
|
51
51
|
* (one array per probe, alongside `trace.t`): what a detector at x sees.
|
|
52
52
|
* @param {number} tEnd s
|
|
53
53
|
* @param {{ tol?: number, dt0?: number, dtMax?: number, budgetMs?: number, maxSteps?: number, method?: 'bdf2' | 'be',
|
|
54
|
-
* probes?: { x: number, species
|
|
54
|
+
* probes?: { x: number, species?: string, quantity?: 'c' | 'V' | 'phi', region?: string | number }[] }} [opts]
|
|
55
55
|
* @returns {Solution}
|
|
56
56
|
*/
|
|
57
57
|
advance(tEnd: number, opts?: {
|
|
@@ -63,8 +63,8 @@ export class Device {
|
|
|
63
63
|
method?: "bdf2" | "be";
|
|
64
64
|
probes?: {
|
|
65
65
|
x: number;
|
|
66
|
-
species
|
|
67
|
-
quantity?: "c" | "V";
|
|
66
|
+
species?: string;
|
|
67
|
+
quantity?: "c" | "V" | "phi";
|
|
68
68
|
region?: string | number;
|
|
69
69
|
}[];
|
|
70
70
|
}): Solution;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Henderson's liquid-junction potential, φ_R − φ_L (V): the junction taken to be the two solutions
|
|
3
|
+
* mixed in one proportion at every point (every ion's mixing fraction the same curve). Exact for a single binary salt, a close estimate otherwise (it's what
|
|
4
|
+
* JPCalc and most electrophysiology corrections use).
|
|
5
|
+
* @param {Record<string, number>} left mol/m³ by ion
|
|
6
|
+
* @param {Record<string, number>} right mol/m³ by ion
|
|
7
|
+
* @param {Record<string, { z: number, D: number }>} ions charge and diffusivity (m²/s), e.g. IONS
|
|
8
|
+
* @param {{ T?: number }} [opts] K (default 298.15)
|
|
9
|
+
*/
|
|
10
|
+
export function henderson(left: Record<string, number>, right: Record<string, number>, ions: Record<string, {
|
|
11
|
+
z: number;
|
|
12
|
+
D: number;
|
|
13
|
+
}>, { T }?: {
|
|
14
|
+
T?: number;
|
|
15
|
+
}): number;
|
|
16
|
+
/**
|
|
17
|
+
* Planck's liquid-junction potential, φ_R − φ_L (V): the steady state of constrained diffusion,
|
|
18
|
+
* a junction zone held between the two solutions, electroneutral, carrying no current. Solved by
|
|
19
|
+
* shooting on the Nernst–Planck equations across it (its length drops out). Exact for that
|
|
20
|
+
* junction; equal to Henderson's for a single binary salt.
|
|
21
|
+
* @param {Record<string, number>} left mol/m³ by ion
|
|
22
|
+
* @param {Record<string, number>} right mol/m³ by ion
|
|
23
|
+
* @param {Record<string, { z: number, D: number }>} ions charge and diffusivity (m²/s), e.g. IONS
|
|
24
|
+
* @param {{ T?: number, steps?: number }} [opts] K (default 298.15); RK4 steps across (default 4000)
|
|
25
|
+
*/
|
|
26
|
+
export function planck(left: Record<string, number>, right: Record<string, number>, ions: Record<string, {
|
|
27
|
+
z: number;
|
|
28
|
+
D: number;
|
|
29
|
+
}>, { T, steps }?: {
|
|
30
|
+
T?: number;
|
|
31
|
+
steps?: number;
|
|
32
|
+
}): number;
|
package/types/kit.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export function vacuumZeroCharge(def: object, workFunction: number, inside: Vacu
|
|
|
36
36
|
export { live } from "./live.js";
|
|
37
37
|
export { recorder } from "./recorder.js";
|
|
38
38
|
export { check } from "./checks.js";
|
|
39
|
+
export { polarization } from "./polarization.js";
|
|
39
40
|
/**
|
|
40
41
|
* One material seen from vacuum: a level inside it (the anchor) and how far above that level,
|
|
41
42
|
* in electron energy, the vacuum level just outside lies (the offset, V). So
|
|
@@ -61,5 +62,6 @@ export { parseEquation, stoichiometry, half, SHE, level } from "./equation.js";
|
|
|
61
62
|
export { traces, speciesRole, typeset } from "./traces.js";
|
|
62
63
|
export { pulse, square, triangle, ramp, injector, recombination, photogeneration } from "./sources.js";
|
|
63
64
|
export { describe, unitWarnings } from "./describe.js";
|
|
65
|
+
export { henderson, planck } from "./junction.js";
|
|
64
66
|
export { perovskiteCell, PEROVSKITE_SCANS, hysteresis } from "./devices.js";
|
|
65
67
|
export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal } from "./data.js";
|