driftlet 0.7.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 +8 -4
- package/docs/device.md +79 -3
- package/docs/kit.md +26 -2
- 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 +12 -1
- package/src/device.js +95 -13
- package/src/grid.js +73 -0
- package/src/kit.js +1 -0
- package/src/polarization.js +84 -0
- package/src/solution.js +12 -4
- package/src/solver.js +180 -41
- package/src/types.js +15 -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 +6 -2
- package/types/types.d.ts +41 -4
package/src/grid.js
CHANGED
|
@@ -109,3 +109,76 @@ export function buildGrid(regions, opts = {}) {
|
|
|
109
109
|
length: x0,
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
// The cross-section A(x) (m²), for a device that isn't planar: the conservation laws hold for
|
|
114
|
+
// the totals through it, flux × A, and a box holds its volume ∫A dx. A planar device has A = 1
|
|
115
|
+
// everywhere, and its currents read as densities.
|
|
116
|
+
// spherical: A = 4π r², cylindrical: A = 2π r (per metre of length), r = r0 + x;
|
|
117
|
+
// a profile: A(x) piecewise linear through the points given, constant beyond them.
|
|
118
|
+
function crossSection(geometry) {
|
|
119
|
+
const { type } = geometry;
|
|
120
|
+
if (type === 'spherical' || type === 'cylindrical') {
|
|
121
|
+
const r0 = geometry.r0, sph = type === 'spherical';
|
|
122
|
+
const sq = (x) => (r0 + x) * (r0 + x), cube = (x) => (r0 + x) * (r0 + x) * (r0 + x);
|
|
123
|
+
return {
|
|
124
|
+
at: (x) => (sph ? 4 * Math.PI * sq(x) : 2 * Math.PI * (r0 + x)),
|
|
125
|
+
// ∫A dx and ∫dx/A from a to b
|
|
126
|
+
integral: (a, b) => (sph ? (4 * Math.PI * (cube(b) - cube(a))) / 3 : Math.PI * (sq(b) - sq(a))),
|
|
127
|
+
resistance: (a, b) => (sph ? (1 / (r0 + a) - 1 / (r0 + b)) / (4 * Math.PI) : Math.log((r0 + b) / (r0 + a)) / (2 * Math.PI)),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const xs = geometry.x, vs = geometry.values, last = xs.length - 1;
|
|
131
|
+
const at = (x) => {
|
|
132
|
+
if (x <= xs[0]) return vs[0];
|
|
133
|
+
if (x >= xs[last]) return vs[last];
|
|
134
|
+
let k = 0;
|
|
135
|
+
while (xs[k + 1] < x) k++;
|
|
136
|
+
return vs[k] + ((vs[k + 1] - vs[k]) * (x - xs[k])) / (xs[k + 1] - xs[k]);
|
|
137
|
+
};
|
|
138
|
+
// Over [a, b], split at the profile's points, where A is linear on each piece.
|
|
139
|
+
const pieces = (a, b, f) => {
|
|
140
|
+
let s = 0, lo = a;
|
|
141
|
+
for (const xk of [...xs.filter((v) => v > a && v < b), b]) {
|
|
142
|
+
s += f(lo, xk, at(lo), at(xk));
|
|
143
|
+
lo = xk;
|
|
144
|
+
}
|
|
145
|
+
return s;
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
at,
|
|
149
|
+
integral: (a, b) => pieces(a, b, (p, q, A1, A2) => ((A1 + A2) / 2) * (q - p)),
|
|
150
|
+
resistance: (a, b) =>
|
|
151
|
+
pieces(a, b, (p, q, A1, A2) => {
|
|
152
|
+
const d = A2 - A1, m = (A1 + A2) / 2;
|
|
153
|
+
return Math.abs(d) < 1e-6 * m ? (q - p) / m : ((q - p) * Math.log(A2 / A1)) / d;
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Give a grid its cross-section: `area` (A at each node, m²), `segArea` (each segment's
|
|
160
|
+
* effective area, its length over ∫dx/A, so that a flux D·segArea·Δc/h is exact for steady
|
|
161
|
+
* diffusion between the nodes; A at its middle where A vanishes at an end, as at a sphere's
|
|
162
|
+
* centre) and `vol` (each node's box, ∫A dx). Planar (no geometry): A = 1, and nothing changes.
|
|
163
|
+
* @param {ReturnType<typeof buildGrid>} grid
|
|
164
|
+
* @param {{ type: 'planar' } | { type: 'spherical' | 'cylindrical', r0: number } | { type: 'profile', x: ArrayLike<number>, values: ArrayLike<number> }} geometry
|
|
165
|
+
*/
|
|
166
|
+
export function applyGeometry(grid, geometry) {
|
|
167
|
+
const { nNodes, x, segLength } = grid;
|
|
168
|
+
grid.area = new Float64Array(nNodes).fill(1);
|
|
169
|
+
grid.segArea = new Float64Array(nNodes - 1).fill(1);
|
|
170
|
+
if (!geometry || geometry.type === 'planar') return grid;
|
|
171
|
+
const A = crossSection(geometry);
|
|
172
|
+
for (let g = 0; g < nNodes; g++) grid.area[g] = A.at(x[g]);
|
|
173
|
+
for (let s = 0; s < nNodes - 1; s++) {
|
|
174
|
+
const h = segLength[s];
|
|
175
|
+
if (h === 0) grid.segArea[s] = grid.area[s];
|
|
176
|
+
else if (grid.area[s] > 0 && grid.area[s + 1] > 0) grid.segArea[s] = h / A.resistance(x[s], x[s + 1]);
|
|
177
|
+
else grid.segArea[s] = A.at(x[s] + h / 2);
|
|
178
|
+
}
|
|
179
|
+
for (let g = 0; g < nNodes; g++) {
|
|
180
|
+
const lo = g > 0 ? x[g] - segLength[g - 1] / 2 : x[g], hi = g < nNodes - 1 ? x[g] + segLength[g] / 2 : x[g];
|
|
181
|
+
grid.vol[g] = hi > lo ? A.integral(lo, hi) : 0;
|
|
182
|
+
}
|
|
183
|
+
return grid;
|
|
184
|
+
}
|
package/src/kit.js
CHANGED
|
@@ -14,6 +14,7 @@ export { describe, unitWarnings } from './describe.js';
|
|
|
14
14
|
export { recorder } from './recorder.js';
|
|
15
15
|
export { henderson, planck } from './junction.js';
|
|
16
16
|
export { check } from './checks.js';
|
|
17
|
+
export { polarization } from './polarization.js';
|
|
17
18
|
export { perovskiteCell, PEROVSKITE_SCANS, hysteresis } from './devices.js';
|
|
18
19
|
export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal } from './data.js';
|
|
19
20
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Polarization curves at an electrode, for driftlet/kit: each reaction's rate with the solution
|
|
2
|
+
// beside the electrode held as it is and the metal's level moved, which is what an Evans diagram
|
|
3
|
+
// plots. Where the net current crosses zero is the spot's open-circuit (mixed) potential.
|
|
4
|
+
|
|
5
|
+
import { DeviceError } from './errors.js';
|
|
6
|
+
import { FARADAY } from './constants.js';
|
|
7
|
+
import { powi, powr } from './pow.js';
|
|
8
|
+
|
|
9
|
+
const fail = (message) => {
|
|
10
|
+
throw new DeviceError(message);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Each electrode reaction's rate at a face (a metal's) or at one spot of an electrode port, with
|
|
15
|
+
* the composition beside it as in `sol` and the metal's carrier at each level in `V` (in the
|
|
16
|
+
* terminals' convention, V = μ̄/(zF); against SHE, subtract φ for the usual tables' ions). The
|
|
17
|
+
* rates are the solver's own law, so at the electrode's actual level they are
|
|
18
|
+
* `sol.interfaces[f].rates` (or `sol.ports[k].rates` at that node).
|
|
19
|
+
* @param {import('./index.js').Device} device
|
|
20
|
+
* @param {object} sol a solution of it
|
|
21
|
+
* @param {{ face: number } | { port: string | number, x?: number }} where a face's index, or a port
|
|
22
|
+
* (name or index) and the position in its window (m, the device's x; default its first node)
|
|
23
|
+
* @param {number | ArrayLike<number>} V the metal's level(s), V
|
|
24
|
+
* @returns {{ V: Float64Array, current: Float64Array, reactions: { equation: string, level: number,
|
|
25
|
+
* rate: Float64Array, current: Float64Array }[] }} per reaction: its equilibrium level here (where
|
|
26
|
+
* its rate is zero), its rate (mol/(m²·s) of electrode, forward) and its current (A/m², anodic
|
|
27
|
+
* positive: into the solution); and their sum
|
|
28
|
+
*/
|
|
29
|
+
export function polarization(device, sol, where, V) {
|
|
30
|
+
const model = device.model, { species, grid, RT } = model, solver = device.solver;
|
|
31
|
+
const Vs = Float64Array.from(typeof V === 'number' ? [V] : V);
|
|
32
|
+
let reactions, nodeOf, carrier, names;
|
|
33
|
+
if (where?.face !== undefined) {
|
|
34
|
+
const f = where.face, itf = model.interfaces[f];
|
|
35
|
+
if (!itf) fail(`polarization: no face ${f} (the device has ${model.interfaces.length})`);
|
|
36
|
+
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1];
|
|
37
|
+
const metal = [gL, gR].findIndex((g) => solver.nodeConductor[g] >= 0);
|
|
38
|
+
if (metal < 0) fail(`polarization: face ${f} has no metal on either side`);
|
|
39
|
+
carrier = { side: metal, i: solver.nodeConductor[metal ? gR : gL] };
|
|
40
|
+
nodeOf = (p) => (p.side ? gR : gL);
|
|
41
|
+
reactions = itf.reactions;
|
|
42
|
+
names = device.def.interfaces?.[f]?.reactions;
|
|
43
|
+
} else if (where?.port !== undefined) {
|
|
44
|
+
const k = typeof where.port === 'number' ? where.port : model.ports.findIndex((p) => p.name === where.port);
|
|
45
|
+
const port = model.ports[k];
|
|
46
|
+
if (!port) fail(`polarization: no port ${JSON.stringify(where.port)}`);
|
|
47
|
+
if (port.reactions.length === 0) fail(`polarization: port ${port.name} has no reactions`);
|
|
48
|
+
let g = port.nodes[0];
|
|
49
|
+
if (where.x !== undefined) for (const h of port.nodes) if (Math.abs(grid.x[h] - where.x) < Math.abs(grid.x[g] - where.x)) g = h;
|
|
50
|
+
carrier = { side: 1, i: port.terminal };
|
|
51
|
+
nodeOf = () => g;
|
|
52
|
+
reactions = port.reactions;
|
|
53
|
+
names = device.def.ports?.[k]?.reactions;
|
|
54
|
+
} else fail('polarization: say where, { face: f } or { port: name, x }');
|
|
55
|
+
if (reactions.length === 0) fail('polarization: no reactions there');
|
|
56
|
+
|
|
57
|
+
const out = reactions.map((rx, k) => {
|
|
58
|
+
if (rx.srh) fail('polarization: SRH kinetics has no electrode level');
|
|
59
|
+
// a = A/RT = a0 + s·V: everything but the carrier at its level, and the carrier's part.
|
|
60
|
+
let a0 = rx.fixedA, s = 0, q = 0, pref = rx.vmax ? rx.vmax : rx.k0;
|
|
61
|
+
rx.part.forEach((p, x) => {
|
|
62
|
+
if (p.side === carrier.side && p.i === carrier.i) {
|
|
63
|
+
s -= (p.nu * species[p.i].z * FARADAY) / RT;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const g = nodeOf(p), name = species[p.i].name;
|
|
67
|
+
a0 -= (p.nu * sol.mu[name][g]) / RT;
|
|
68
|
+
q += p.side === carrier.side ? 0 : p.nu * species[p.i].z; // charge the forward reaction puts into the solution
|
|
69
|
+
const c = sol.c[name][g];
|
|
70
|
+
if (rx.vmax) {
|
|
71
|
+
if (p.nu < 0) pref *= powi(c / (c + rx.K[x]), -p.nu);
|
|
72
|
+
} else pref *= powr(c / solver.cRef[g * species.length + p.i], p.nu < 0 ? -p.nu * (1 - rx.alpha) : p.nu * rx.alpha);
|
|
73
|
+
});
|
|
74
|
+
if (s === 0) fail(`polarization: reaction ${k} there takes no ${species[carrier.i].name} from the metal`);
|
|
75
|
+
const rate = Float64Array.from(Vs, (v) => {
|
|
76
|
+
const a = a0 + s * v;
|
|
77
|
+
return rx.vmax ? pref * -Math.expm1(-a) : pref * (Math.exp(rx.alpha * a) - Math.exp(-(1 - rx.alpha) * a));
|
|
78
|
+
});
|
|
79
|
+
return { equation: names?.[k]?.equation ?? `reaction ${k}`, level: -a0 / s, rate, current: rate.map((r) => q * FARADAY * r) };
|
|
80
|
+
});
|
|
81
|
+
const current = new Float64Array(Vs.length);
|
|
82
|
+
for (const r of out) for (let j = 0; j < Vs.length; j++) current[j] += r.current[j];
|
|
83
|
+
return { V: Vs, current, reactions: out };
|
|
84
|
+
}
|
package/src/solution.js
CHANGED
|
@@ -91,7 +91,8 @@ export function makeSolution(solver, result = {}) {
|
|
|
91
91
|
const V = solver.termV[side === 'left' ? 0 : 1];
|
|
92
92
|
sol.contacts[side] = { V, flux, D, current: conduction + displacement };
|
|
93
93
|
if (ct.phi.type === 'capacitive' || ct.phi.type === 'pinned') {
|
|
94
|
-
// Charge
|
|
94
|
+
// Charge on the gate (or metal) plate, per area if the device is planar: +D at the left, −D
|
|
95
|
+
// at the right.
|
|
95
96
|
sol.gates[side] = { V, D, charge: side === 'left' ? D : -D };
|
|
96
97
|
}
|
|
97
98
|
}
|
|
@@ -105,7 +106,14 @@ export function makeSolution(solver, result = {}) {
|
|
|
105
106
|
flux[species[i].name] = solver.portFlux[k][i];
|
|
106
107
|
current += F * species[i].z * solver.portFlux[k][i];
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
+
const out = { name: port.name, V: solver.termV[2 + k], flux, current };
|
|
110
|
+
if (port.reactions.length > 0) {
|
|
111
|
+
// An electrode spread through the window: where it sits, and each reaction's rate there.
|
|
112
|
+
out.x = Float64Array.from(port.nodes, (g) => model.grid.x[g]);
|
|
113
|
+
out.area = Float64Array.from(solver.portArea[k]);
|
|
114
|
+
out.rates = solver.portRates[k].map((r) => Float64Array.from(r)); // mol/(m²·s), forward
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
109
117
|
});
|
|
110
118
|
sol.terminalVoltage = sol.contacts.right.V - sol.contacts.left.V;
|
|
111
119
|
// Every terminal (the contacts, then the ports by name): its voltage and its current into the
|
|
@@ -142,14 +150,14 @@ export function makeSolution(solver, result = {}) {
|
|
|
142
150
|
return { rate, regions, total };
|
|
143
151
|
});
|
|
144
152
|
|
|
145
|
-
// Total charge
|
|
153
|
+
// Total charge in the device (space charge plus sheet charges), per area if it's planar.
|
|
146
154
|
let q = 0;
|
|
147
155
|
for (let g = 0; g < nNodes; g++) {
|
|
148
156
|
let rho = solver.rhoFixed[g];
|
|
149
157
|
for (let i = 0; i < n; i++) rho += F * species[i].z * c[g * n + i];
|
|
150
158
|
q += grid.vol[g] * rho;
|
|
151
159
|
}
|
|
152
|
-
|
|
160
|
+
interfaces.forEach((itf, f) => (q += itf.sheetCharge * grid.area[grid.regionEnd[f]]));
|
|
153
161
|
sol.charge = q;
|
|
154
162
|
|
|
155
163
|
// Resolution warnings: where the model resolves a double layer (dipole or capacitive faces,
|
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);
|
|
@@ -1012,6 +1056,50 @@ export class Solver {
|
|
|
1012
1056
|
if (Number.isFinite(shift)) this.termV[k] = this.termV[held] + shift;
|
|
1013
1057
|
}
|
|
1014
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);
|
|
1015
1103
|
}
|
|
1016
1104
|
|
|
1017
1105
|
// A non-ideal material's composition at φ̂ = ph: level-fixed species (mode 1) at their η,
|
|
@@ -1054,7 +1142,7 @@ export class Solver {
|
|
|
1054
1142
|
// Excess carriers: zero in the bulk, the surface sheet at a charged face.
|
|
1055
1143
|
for (let i = 0; i < n; i++) c[g * n + i] = 0;
|
|
1056
1144
|
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]);
|
|
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]);
|
|
1058
1146
|
continue;
|
|
1059
1147
|
}
|
|
1060
1148
|
for (let i = 0; i < n; i++) {
|
|
@@ -1165,7 +1253,7 @@ export class Solver {
|
|
|
1165
1253
|
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
|
|
1166
1254
|
if (mat.conductor) {
|
|
1167
1255
|
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]);
|
|
1256
|
+
for (let s = g0; s < g1; s++) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s] / grid.segArea[s]);
|
|
1169
1257
|
continue;
|
|
1170
1258
|
}
|
|
1171
1259
|
if (mat.ideal) this._nodesDilute(g0, g1, dt);
|
|
@@ -1173,7 +1261,7 @@ export class Solver {
|
|
|
1173
1261
|
const rxs = this.rxsIn[reg.material];
|
|
1174
1262
|
if (rxs.length > 0) for (let g = g0; g <= g1; g++) this._bulkReactions(g, rxs);
|
|
1175
1263
|
for (let s = g0; s < g1; s++) {
|
|
1176
|
-
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)
|
|
1177
1265
|
this._segmentDisplacement(s, bL, bR, mat, h);
|
|
1178
1266
|
if (reg.mixing > 0) this._segmentMixing(s, bL, bR, reg.mixing, h, mat);
|
|
1179
1267
|
if (!mat.ideal) this._segmentConcentrated(s, bL, bR, mat, h, reg.velocity);
|
|
@@ -1201,7 +1289,7 @@ export class Solver {
|
|
|
1201
1289
|
const r = 1 + im, k = g * n + im;
|
|
1202
1290
|
res[R[b * M + r]] += (v * (c[k] - cOld[k])) / dt;
|
|
1203
1291
|
const f = this.sheetFace[g];
|
|
1204
|
-
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));
|
|
1205
1293
|
}
|
|
1206
1294
|
|
|
1207
1295
|
// Dilute nodes g0…g1 (ideal statistics, c = c_ref e^ζ): storage of each species and the space
|
|
@@ -1450,9 +1538,9 @@ export class Solver {
|
|
|
1450
1538
|
_segmentsDilute(g0, g1, region, mat, vel) {
|
|
1451
1539
|
const { n, M, u, uLo, res, c, z, sys, loc } = this, R = this.rix, F = FARADAY;
|
|
1452
1540
|
const { A: JA, B: JB, C: JC, sizes, offA, offB, offC } = sys;
|
|
1453
|
-
const segLength = this.model.grid
|
|
1541
|
+
const { segLength, segArea } = this.model.grid;
|
|
1454
1542
|
for (let s = g0; s < g1; s++) {
|
|
1455
|
-
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)
|
|
1456
1544
|
const mL = sizes[bL], mR = sizes[bR], pL = loc[bL * M], pR = loc[bR * M];
|
|
1457
1545
|
const phiL = u[bL * M], phiR = u[bR * M];
|
|
1458
1546
|
for (let i = 0; i < n; i++) {
|
|
@@ -1535,15 +1623,16 @@ export class Solver {
|
|
|
1535
1623
|
this._j(bf, 0, bL, 0, -kC);
|
|
1536
1624
|
}
|
|
1537
1625
|
}
|
|
1538
|
-
// D_f enters each side's Gauss row; a metal side holds it as surface carriers instead.
|
|
1539
|
-
|
|
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];
|
|
1540
1629
|
if (this.nodeConductor[gL] < 0) {
|
|
1541
|
-
res[R[bL * M]] += u[bf * M];
|
|
1542
|
-
this._j(bL, 0, bf, 0,
|
|
1630
|
+
res[R[bL * M]] += Af * u[bf * M];
|
|
1631
|
+
this._j(bL, 0, bf, 0, Af);
|
|
1543
1632
|
}
|
|
1544
1633
|
if (this.nodeConductor[gR] < 0) {
|
|
1545
|
-
res[R[bR * M]] -= u[bf * M] + itf.sheetCharge;
|
|
1546
|
-
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);
|
|
1547
1636
|
}
|
|
1548
1637
|
for (let i = 0; i < n; i++) {
|
|
1549
1638
|
const r = 1 + i, o = bf * M + r;
|
|
@@ -1583,12 +1672,12 @@ export class Solver {
|
|
|
1583
1672
|
this._j(bf, r, bL, r, -gG);
|
|
1584
1673
|
this._j(bf, r, bR, r, gG);
|
|
1585
1674
|
}
|
|
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, -
|
|
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);
|
|
1590
1679
|
}
|
|
1591
|
-
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));
|
|
1592
1681
|
}
|
|
1593
1682
|
|
|
1594
1683
|
// Scharfetter–Gummel with non-ideal statistics. The excess ex = ζ − ln(c/c_ref) acts as an
|
|
@@ -1805,7 +1894,7 @@ export class Solver {
|
|
|
1805
1894
|
// with the row r_k − rate(u_L, u_R) = 0, and each participant's edge node takes ν·r_k (made
|
|
1806
1895
|
// there when ν > 0). The rate couples the two edge nodes only through the face block between
|
|
1807
1896
|
// them, which keeps the system block-tridiagonal.
|
|
1808
|
-
_faceReaction(rx, k, f, bf, bL, bR) {
|
|
1897
|
+
_faceReaction(rx, k, f, bf, bL, bR, Af) {
|
|
1809
1898
|
const { n, M, u, uLo, c, res } = this;
|
|
1810
1899
|
const R = this.rix;
|
|
1811
1900
|
const grid = this.model.grid;
|
|
@@ -1865,8 +1954,8 @@ export class Solver {
|
|
|
1865
1954
|
}
|
|
1866
1955
|
for (const p of rx.part) {
|
|
1867
1956
|
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);
|
|
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);
|
|
1870
1959
|
}
|
|
1871
1960
|
}
|
|
1872
1961
|
|
|
@@ -1948,9 +2037,58 @@ export class Solver {
|
|
|
1948
2037
|
}
|
|
1949
2038
|
}
|
|
1950
2039
|
}
|
|
2040
|
+
port.reactions.forEach((rx, x) => this._portReaction(port, rx, this.portArea[k - 2], this.portRates[k - 2][x], flux, k));
|
|
1951
2041
|
for (let i = 0; i < n; i++) this.termI[k] += z[i] * F * flux[i];
|
|
1952
2042
|
}
|
|
1953
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
|
+
|
|
1954
2092
|
|
|
1955
2093
|
// One contact (terminal 0 or 1): record the flux through its outer face and the current into
|
|
1956
2094
|
// the device (read from the end box before anything here is added: whatever the box needs
|
|
@@ -1966,6 +2104,7 @@ export class Solver {
|
|
|
1966
2104
|
const b = this.blockOfNode[g];
|
|
1967
2105
|
const sgn = side === 'left' ? 1 : -1;
|
|
1968
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
|
|
1969
2108
|
const dyn = Number.isFinite(dt);
|
|
1970
2109
|
|
|
1971
2110
|
// Before anything is added, each balance residual is the flux into the device here.
|
|
@@ -1986,13 +2125,13 @@ export class Solver {
|
|
|
1986
2125
|
const link = ct.species[i];
|
|
1987
2126
|
const o = b * M + 1 + i;
|
|
1988
2127
|
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);
|
|
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);
|
|
1991
2130
|
} else if (link.type === 'conductance') {
|
|
1992
2131
|
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);
|
|
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);
|
|
1996
2135
|
}
|
|
1997
2136
|
}
|
|
1998
2137
|
|
|
@@ -2014,15 +2153,15 @@ export class Solver {
|
|
|
2014
2153
|
this.contactD[side] = 0;
|
|
2015
2154
|
} else if (link.type === 'capacitive') {
|
|
2016
2155
|
// 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]);
|
|
2156
|
+
const Din = Ac * link.C * (Vt - link.zeroCharge - VT * u[b * M]);
|
|
2018
2157
|
res[R[b * M]] -= Din;
|
|
2019
|
-
this._j(b, 0, b, 0, link.C * VT);
|
|
2020
|
-
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;
|
|
2021
2160
|
this.contactD[side] = sgn * Din;
|
|
2022
2161
|
if (dyn) {
|
|
2023
2162
|
I += (Din - Dstart) / dt;
|
|
2024
|
-
C[R[b * M]] += (-link.C * VT) / dt;
|
|
2025
|
-
this.termDI[k] += link.C / dt;
|
|
2163
|
+
C[R[b * M]] += (-Ac * link.C * VT) / dt;
|
|
2164
|
+
this.termDI[k] += (Ac * link.C) / dt;
|
|
2026
2165
|
}
|
|
2027
2166
|
} else if (link.type === 'pinned' || link.type === 'bulk') {
|
|
2028
2167
|
// The Poisson residual is the outside's charge, D_in.
|
|
@@ -2910,7 +3049,7 @@ export class Solver {
|
|
|
2910
3049
|
const reg = regions[r], mat = materials[reg.material];
|
|
2911
3050
|
if (mat.conductor) {
|
|
2912
3051
|
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]);
|
|
3052
|
+
for (const s of segs) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s] / grid.segArea[s]);
|
|
2914
3053
|
continue;
|
|
2915
3054
|
}
|
|
2916
3055
|
for (const g of nodes) {
|
|
@@ -2920,7 +3059,7 @@ export class Solver {
|
|
|
2920
3059
|
const rxs = this.rxsIn[reg.material];
|
|
2921
3060
|
if (rxs.length > 0) for (const g of nodes) this._bulkReactions(g, rxs);
|
|
2922
3061
|
for (const s of segs) {
|
|
2923
|
-
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)
|
|
2924
3063
|
this._segmentDisplacement(s, bL, bR, mat, h);
|
|
2925
3064
|
if (reg.mixing > 0) this._segmentMixing(s, bL, bR, reg.mixing, h, mat);
|
|
2926
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))
|