driftlet 1.2.4 → 1.3.2
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 +1 -1
- package/docs/conventions.md +8 -3
- package/docs/device.md +38 -9
- package/docs/internals.md +33 -14
- package/docs/numerics.md +65 -15
- package/docs/reliability.md +25 -16
- package/docs/statistics.md +13 -0
- package/llms.txt +11 -8
- package/package.json +1 -1
- package/src/assemble.js +206 -0
- package/src/bernoulli.js +32 -0
- package/src/boundaries.js +795 -0
- package/src/describe.js +2 -0
- package/src/device.js +32 -6
- package/src/impedance.js +857 -0
- package/src/index.js +25 -1
- package/src/newton.js +605 -0
- package/src/regions.js +1009 -0
- package/src/solution.js +24 -2
- package/src/solver.js +178 -3643
- package/src/start.js +508 -0
- package/src/steady.js +817 -0
- package/src/timeSteps.js +17 -2
- package/src/types.js +3 -0
- package/types/assemble.d.ts +18 -0
- package/types/bernoulli.d.ts +9 -0
- package/types/boundaries.d.ts +24 -0
- package/types/impedance.d.ts +53 -0
- package/types/newton.d.ts +7 -0
- package/types/regions.d.ts +61 -0
- package/types/solver.d.ts +14 -220
- package/types/start.d.ts +14 -0
- package/types/steady.d.ts +81 -0
- package/types/types.d.ts +6 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
// The equations at the boundaries: each face between regions (its φ law, the species' links
|
|
2
|
+
// across it, its reactions and gates), and the terminals (the ports inside the device, the two
|
|
3
|
+
// contacts, and the circuit rows of the floating ones), with what each passes into the device.
|
|
4
|
+
//
|
|
5
|
+
// CONTRACT
|
|
6
|
+
//
|
|
7
|
+
// Methods of the Solver, called by assemble() (src/assemble.js, whose contract sets the rows'
|
|
8
|
+
// conventions) and its bookkeeping, after the regions' terms (src/regions.js), whose
|
|
9
|
+
// concentrations and derivatives (_dlnc, _Kz) they use. Units: a face's slots are per area (its
|
|
10
|
+
// flux N_i toward +x, its D, its reactions' rates), and enter the edge nodes' rows times the
|
|
11
|
+
// face's area Af; a port's terms are per volume of its window (times vol·fill, the part of each
|
|
12
|
+
// node's box inside the window: port.fill, src/device.js), its reactions' per area of electrode
|
|
13
|
+
// (times vol and the electrode's area per volume of each node's box, portArea, fill in it); a contact's
|
|
14
|
+
// links are per area, times the area at the end node. V_T = RT/F; a terminal's voltage V (termV)
|
|
15
|
+
// is the outside's level per charge.
|
|
16
|
+
//
|
|
17
|
+
// A face f (_face; its flux block bf between edge nodes bL, bR):
|
|
18
|
+
// φ law: pinned, φ̂_R − φ̂_L − dipole/V_T = 0; capacitive, D + C V_T (φ̂_R − φ̂_L − dipole/V_T)
|
|
19
|
+
// = 0; neutral, no D. Against a metal (itf.conductor, not neutral), the other side's edge φ
|
|
20
|
+
// is tied to the metal's Fermi level V_F = V_T η/z instead: capacitive, D toward the other
|
|
21
|
+
// side = C (V_F − zeroCharge − φ) (pinned, V_F − zeroCharge − φ = 0, is written too, but
|
|
22
|
+
// normalizeDevice refuses it: a metal holds its surface charge in a finite capacitance). D enters the left Gauss row
|
|
23
|
+
// as +Af·D, the right one's as −Af·(D + sheetCharge); a metal side's charge is its carriers'
|
|
24
|
+
// (src/regions.js).
|
|
25
|
+
// links, per species: blocked, no flux; equilibrium, η_L = η_R (the flux whatever the
|
|
26
|
+
// balances need); permeability, N = −P B(d) c_L expm1(η_R − η_L), d = z Δφ̂ + the step in
|
|
27
|
+
// μ°/RT − ln c_ref (Goldman–Hodgkin–Katz between like solutions); conductance, N = G V_T
|
|
28
|
+
// (η_L − η_R)/(z² F). Gated, P or G times Π x^p over its gates, each x taken in [0, 1]. N
|
|
29
|
+
// enters the left node's balance as +Af·N, the right's as −Af·N.
|
|
30
|
+
// reactions (_faceReaction): its rate r an unknown, row r − rate = 0, with a = fixedA − Σ ν η
|
|
31
|
+
// over its participants (ν > 0 made, at either side's edge node); rate Butler–Volmer, k0
|
|
32
|
+
// Π (c/c_ref)^e g(a) (e = |ν|(1 − α) for what it consumes, να for what it makes; a metal's
|
|
33
|
+
// carrier at activity 1), or saturating (vmax), or SRH; −Af·ν·r into each participant's row.
|
|
34
|
+
// gates (_faceGates): (x − x_old)/dt − α(V)(1 − x) + β(V)x = 0, V = φ_R − φ_L (no storage at
|
|
35
|
+
// dt = ∞).
|
|
36
|
+
//
|
|
37
|
+
// The terminals (_terminals), in order: the sources refreshed (_refreshSources); the live
|
|
38
|
+
// terminals' termB, termC zeroed, and termDI, termDX, termI, qg, qgSlope; each port's terms but
|
|
39
|
+
// its held levels, then each port's held levels; the left contact, the right; each floating
|
|
40
|
+
// terminal's circuit row.
|
|
41
|
+
// port k (terminal 2 + k, _port), at each node of its window: an equilibrium link holds η at
|
|
42
|
+
// the port's level (portEta) at the nodes inside the window (port.inside), replacing the
|
|
43
|
+
// balance row, whose residual just before is the flux it passes (not at a device end node
|
|
44
|
+
// whose contact links that species); conductance
|
|
45
|
+
// and exchange links supply vol·fill·k(η_out − η), k = G V_T/(z² F) or the link's k (gated as at
|
|
46
|
+
// a face); reactions (_portReaction) as at a face against the electrode's carrier at the
|
|
47
|
+
// port's level, explicit (no unknown), −vol·area·ν·rate into the region's species' rows,
|
|
48
|
+
// −ν·rate into a surface species' (its rows are per area of electrode); a capacitance (_portCapacitance), σ =
|
|
49
|
+
// C (V − zeroCharge − φ) per electrode area, −vol·area·σ into each Gauss row (not at an end
|
|
50
|
+
// node whose contact sets φ, nor where φ is undefined), its charge portQ and per-node qg;
|
|
51
|
+
// gates as at a face, in V_m = φ − (V − zeroCharge); a surface's storage, Γ(θ − θ_old)/dt.
|
|
52
|
+
// termI: Σ z F × what it passes, plus its capacitance's charging, (Q − Q at the step's
|
|
53
|
+
// start)/dt (steady at no current: Q − its captured charge).
|
|
54
|
+
// contact (_contact): first, at the end node, each active balance residual (all of the box's
|
|
55
|
+
// other terms) is the flux the contact passes in: contactFlux (toward +x), and the current
|
|
56
|
+
// Σ z F × that; then, at a lateStorage node on a time step, the ions' storage (in the rows
|
|
57
|
+
// and contactFlux, not the current); then its links: exchange, A k (μ_out/RT − η); conductance,
|
|
58
|
+
// A G (V + offset − V_i)/(zF), V_i = V_T η/z; velocity, A v c (e^{η_out − η} − 1) (dilute,
|
|
59
|
+
// A v (c_eq − c)); equilibrium, the row replaced by η = η_out (contactEta), any other
|
|
60
|
+
// terminal's ∂/∂V in it cleared. Its φ law: capacitive, D_in = A C (V − zeroCharge − V_T φ̂)
|
|
61
|
+
// (φ̂ as u alone, as the displacement reads it) out of the Gauss row; pinned or bulk, D_in =
|
|
62
|
+
// the Gauss row's residual so far, the row replaced by φ̂ = (V − zeroCharge)/V_T or by
|
|
63
|
+
// neutrality (F Σ z c + ρ_fixed = 0, its ∂/∂η_i = F (Kz)_i, K being symmetric); the current gains (D_in − D_in at the step's start)/dt (not at a lateStorage
|
|
64
|
+
// node on a time step; steady at no current: D_in − its captured charge); contactD = ±D_in
|
|
65
|
+
// toward +x. termI: that current.
|
|
66
|
+
// A terminal whose current is read from rows (a contact's from its end box, a port's held
|
|
67
|
+
// level from the rows it replaces) takes their ∂/∂V's with it (_captureRow): another live
|
|
68
|
+
// terminal j's termB entries there into termDX[k][j] (a port's window in a contact's end
|
|
69
|
+
// box), its own into termDI[k].
|
|
70
|
+
// circuit (_circuit), floating k: termRes = I − I_set, or I − (V_src − V)/R.
|
|
71
|
+
//
|
|
72
|
+
// Invariants (each says what a reviewer should be able to check here):
|
|
73
|
+
// 1. Every Jacobian entry is its residual's derivative; termB[k] is ∂res/∂V_k, termC[k]
|
|
74
|
+
// ∂termI_k/∂(unknowns), and termDI[k], termDX[k][j] ∂termI_k/∂V_k and /∂V_j (with a circuit
|
|
75
|
+
// row's own), for the live terminals (_liveTerms).
|
|
76
|
+
// 2. termI[k] is the current into the device through terminal k: what its links and reactions
|
|
77
|
+
// pass, times z F, and its displacement current; a contact's equals what the end box needs
|
|
78
|
+
// from outside, read from its residual (so the current is conserved through the device).
|
|
79
|
+
// 3. A face's flux enters its two edge nodes with opposite signs, times Af.
|
|
80
|
+
// 4. Where μ̄ is level across a link (or a = 0 for a reaction), its flux (rate) is exactly 0.
|
|
81
|
+
// 5. The readouts (contactFlux, contactD, portFlux, portQ, portRates) are those of the state
|
|
82
|
+
// assembled, written at every assembly.
|
|
83
|
+
// Tests: test/contacts.test.js, test/ports.test.js, test/terminals.test.js, test/circuit.test.js,
|
|
84
|
+
// test/gates.test.js, test/membrane.test.js, test/kinetics.test.js, test/coverage.test.js,
|
|
85
|
+
// test/capacitance.test.js, test/metal.test.js, test/jacobian.test.js.
|
|
86
|
+
|
|
87
|
+
import { sgFlux } from './bernoulli.js';
|
|
88
|
+
import { FARADAY } from './constants.js';
|
|
89
|
+
import { sourceAt } from './device.js';
|
|
90
|
+
import { powi, powr } from './pow.js';
|
|
91
|
+
|
|
92
|
+
// Butler–Volmer factor g(a) = e^{αa} − e^{−(1−α)a} = e^{−(1−α)a}·expm1(a) (precise near a = 0),
|
|
93
|
+
// and its derivative g′(a) = α e^{αa} + (1−α) e^{−(1−α)a}.
|
|
94
|
+
function bvFactor(a, alpha) {
|
|
95
|
+
const e = Math.exp(-(1 - alpha) * a);
|
|
96
|
+
// (far from equilibrium, the plain difference: the product would give 0·∞ there)
|
|
97
|
+
const g = Math.abs(a) > 50 ? Math.exp(alpha * a) - e : e * Math.expm1(a);
|
|
98
|
+
return { g, gp: alpha * Math.exp(alpha * a) + (1 - alpha) * e };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A gate's rate law (see normalizeGates in device.js) at voltage V: [rate, ∂rate/∂V], 1/s and 1/(s·V).
|
|
102
|
+
export function gateRate(law, V) {
|
|
103
|
+
const { rate, midpoint, scale } = law;
|
|
104
|
+
if (law.type === 'exp') {
|
|
105
|
+
// (held finite where Newton's iterates stray to absurd voltages: β_m at −13 V would overflow)
|
|
106
|
+
const y = (V - midpoint) / scale, v = rate * Math.exp(Math.min(y, 700));
|
|
107
|
+
return [v, y > 700 ? 0 : v / scale];
|
|
108
|
+
}
|
|
109
|
+
if (law.type === 'sigmoid') {
|
|
110
|
+
const e = Math.exp((midpoint - V) / scale), d = 1 + e;
|
|
111
|
+
return Number.isFinite(e) ? [rate / d, (rate * e) / (scale * d * d)] : [0, 0];
|
|
112
|
+
}
|
|
113
|
+
// expLinear: rate·y/(1 − e^−y), smooth through y = 0 (rate there) and → rate·y for large y
|
|
114
|
+
const y = (V - midpoint) / scale;
|
|
115
|
+
if (Math.abs(y) < 1e-6) return [rate * (1 + y / 2), rate / (2 * scale)];
|
|
116
|
+
if (y < -700) return [0, 0];
|
|
117
|
+
const em = -Math.expm1(-y), ey = Math.exp(-y); // 1 − e^−y, e^−y
|
|
118
|
+
return [(rate * y) / em, (rate * (em - y * ey)) / (em * em * scale)];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class BoundaryTerms {
|
|
122
|
+
// A port reaction's rate per area at node g (mol/(m²·s), forward), at the present state.
|
|
123
|
+
_portRate(port, rx, g) {
|
|
124
|
+
const { n, M, u, uLo, c, nSurf, th } = this, b = this.blockOfNode[g], RT = this.model.RT;
|
|
125
|
+
let pref = rx.k0, a = rx.fixedA;
|
|
126
|
+
for (const p of rx.part) {
|
|
127
|
+
if (p.side === 1) {
|
|
128
|
+
a -= p.nu * this.portEta(port, p.i);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
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;
|
|
132
|
+
a -= p.nu * (u[o] + uLo[o]);
|
|
133
|
+
pref *= p.side === 2 ? powr(th[g * nSurf + p.s], e) : powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
134
|
+
}
|
|
135
|
+
if (rx.sites) pref *= powr(this.th0[g], rx.sites);
|
|
136
|
+
return pref * bvFactor(a, rx.alpha).g;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A face: its flux node's rows are the interface laws (φ law, species links, reactions), and
|
|
140
|
+
// its fluxes enter the two edge nodes' balances.
|
|
141
|
+
_face(f) {
|
|
142
|
+
const { model, n, M, u, uLo, res, z, VT } = this, R = this.rix, F = FARADAY;
|
|
143
|
+
const grid = model.grid, interfaces = model.interfaces;
|
|
144
|
+
const bf = this.blockOfFace[f], bL = bf - 1, bR = bf + 1;
|
|
145
|
+
const itf = interfaces[f];
|
|
146
|
+
// φ law: pinned jump (dipole), Helmholtz capacitor, or no charge at all (neutral).
|
|
147
|
+
const law = itf.phi.type;
|
|
148
|
+
const condSide = itf.conductor && law !== 'neutral' ? itf.conductor.side : null;
|
|
149
|
+
if (condSide) {
|
|
150
|
+
// Against a metal: the other side's φ is tied to the metal's Fermi level V_F = V_T η/z.
|
|
151
|
+
const im = itf.conductor.i, bm = condSide === 'left' ? bL : bR, bo = condSide === 'left' ? bR : bL;
|
|
152
|
+
const sg = condSide === 'left' ? 1 : -1;
|
|
153
|
+
const vf = (u[bm * M + 1 + im] + uLo[bm * M + 1 + im]) / z[im]; // V_F / V_T
|
|
154
|
+
const gap = vf - itf.zeroCharge / VT - (u[bo * M] + uLo[bo * M]); // (V_F − zeroCharge − φ_edge)/V_T
|
|
155
|
+
if (law === 'pinned') {
|
|
156
|
+
res[R[bf * M]] = gap;
|
|
157
|
+
this._j(bf, 0, bm, 1 + im, 1 / z[im]);
|
|
158
|
+
this._j(bf, 0, bo, 0, -1);
|
|
159
|
+
} else {
|
|
160
|
+
// D toward the other side = C (V_F − zeroCharge − φ_edge); toward +x that's sg times it.
|
|
161
|
+
const kC = itf.phi.C * VT;
|
|
162
|
+
res[R[bf * M]] = u[bf * M] - sg * kC * gap;
|
|
163
|
+
this._j(bf, 0, bf, 0, 1);
|
|
164
|
+
this._j(bf, 0, bm, 1 + im, (-sg * kC) / z[im]);
|
|
165
|
+
this._j(bf, 0, bo, 0, sg * kC);
|
|
166
|
+
}
|
|
167
|
+
} else if (law === 'neutral') {
|
|
168
|
+
// D = 0 (not an unknown); the jump is whatever each side's neutrality needs
|
|
169
|
+
} else {
|
|
170
|
+
const jump = u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M]) - itf.dipole / VT;
|
|
171
|
+
if (law === 'pinned') {
|
|
172
|
+
res[R[bf * M]] = jump;
|
|
173
|
+
this._j(bf, 0, bR, 0, 1);
|
|
174
|
+
this._j(bf, 0, bL, 0, -1);
|
|
175
|
+
} else {
|
|
176
|
+
// D = −C (φ_R − φ_L − dipole): displacement toward +x drops across the layer.
|
|
177
|
+
const kC = itf.phi.C * VT;
|
|
178
|
+
res[R[bf * M]] = u[bf * M] + kC * jump;
|
|
179
|
+
this._j(bf, 0, bf, 0, 1);
|
|
180
|
+
this._j(bf, 0, bR, 0, kC);
|
|
181
|
+
this._j(bf, 0, bL, 0, -kC);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// D_f enters each side's Gauss row; a metal side holds it as surface carriers instead. The
|
|
185
|
+
// flux node's unknowns are per area, so what they carry is that times the face's area.
|
|
186
|
+
const gL = grid.regionEnd[f], gR = gL + 1, Af = grid.area[gL];
|
|
187
|
+
if (this.nodeConductor[gL] < 0) {
|
|
188
|
+
res[R[bL * M]] += Af * u[bf * M];
|
|
189
|
+
this._j(bL, 0, bf, 0, Af);
|
|
190
|
+
}
|
|
191
|
+
if (this.nodeConductor[gR] < 0) {
|
|
192
|
+
res[R[bR * M]] -= Af * (u[bf * M] + itf.sheetCharge);
|
|
193
|
+
this._j(bR, 0, bf, 0, -Af);
|
|
194
|
+
}
|
|
195
|
+
for (let i = 0; i < n; i++) {
|
|
196
|
+
const r = 1 + i, o = bf * M + r;
|
|
197
|
+
const type = itf.links[i].type;
|
|
198
|
+
const deta = u[bL * M + r] - u[bR * M + r] + (uLo[bL * M + r] - uLo[bR * M + r]); // η_L − η_R
|
|
199
|
+
if (type === 'blocked') continue;
|
|
200
|
+
if (type === 'equilibrium') {
|
|
201
|
+
res[R[o]] = -deta; // μ̄ continuous
|
|
202
|
+
this._j(bf, r, bR, r, 1);
|
|
203
|
+
this._j(bf, r, bL, r, -1);
|
|
204
|
+
} else if (type === 'permeability') {
|
|
205
|
+
// Electrodiffusion through a thin membrane in a constant field: Scharfetter–Gummel across
|
|
206
|
+
// the face with D/h → P, its drift taken over the face's whole jump in the standard level
|
|
207
|
+
// zφ̂ + μ°/RT − ln c_ref (the φ jump of a capacitive face, and any step between the two
|
|
208
|
+
// materials). Between like solutions it's Goldman–Hodgkin–Katz's flux,
|
|
209
|
+
// N = P·zu·(c_L − c_R e^{zu})/(e^{zu} − 1), u = (φ_R − φ_L)/V_T,
|
|
210
|
+
// and it's exactly zero where μ̄ is level.
|
|
211
|
+
const gl = grid.regionEnd[f], gr = gl + 1, kl = gl * n + i, kr = gr * n + i, zi = z[i], link = itf.links[i];
|
|
212
|
+
const shift = this.mu0hat[kr] - this.mu0hat[kl] - Math.log(this.cRef[kr] / this.cRef[kl]);
|
|
213
|
+
const d = zi * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M])) + shift;
|
|
214
|
+
// Gated: P × Π x^p, each x taken in [0, 1] (Newton's iterates can stray past its ends).
|
|
215
|
+
const gated = link.gates ?? [], gx = (q) => u[this._gateSlot(f, q)];
|
|
216
|
+
const N1 = gated.length ? sgFlux(link.P, d, -deta, this.c[kl], this.c[kr], this.sg)[0] : 0; // (fully open)
|
|
217
|
+
const P = link.P * (gated.length ? this._open(gated, gx) : 1);
|
|
218
|
+
const sg = sgFlux(P, d, -deta, this.c[kl], this.c[kr], this.sg); // (−deta = η_R − η_L)
|
|
219
|
+
const N = sg[0], dNdd = sg[1];
|
|
220
|
+
res[R[o]] = u[o] - N;
|
|
221
|
+
this._j(bf, r, bf, r, 1);
|
|
222
|
+
for (const [q, p] of gated) {
|
|
223
|
+
const dO = this._dOpen(gated, q, p, gx);
|
|
224
|
+
if (dO !== 0) this._j(bf, r, bf, 1 + n + itf.reactions.length + q, -N1 * dO);
|
|
225
|
+
}
|
|
226
|
+
this._j(bf, r, bL, r, -sg[2]);
|
|
227
|
+
this._j(bf, r, bR, r, sg[3]);
|
|
228
|
+
if (zi !== 0) {
|
|
229
|
+
this._j(bf, r, bL, 0, zi * dNdd + zi * N);
|
|
230
|
+
this._j(bf, r, bR, 0, -zi * dNdd);
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
// conductance: J = G (V_L − V_R), V = V_T η / z ⇒ N = G V_T (η_L − η_R) / (z² F); gated,
|
|
234
|
+
// G times Π x^p
|
|
235
|
+
const gated = itf.links[i].gates ?? [], gx = (q) => u[this._gateSlot(f, q)];
|
|
236
|
+
const gG0 = (itf.links[i].G * VT) / (z[i] * z[i] * F), gG = gG0 * (gated.length ? this._open(gated, gx) : 1);
|
|
237
|
+
res[R[o]] = u[o] - gG * deta;
|
|
238
|
+
this._j(bf, r, bf, r, 1);
|
|
239
|
+
this._j(bf, r, bL, r, -gG);
|
|
240
|
+
this._j(bf, r, bR, r, gG);
|
|
241
|
+
for (const [q, p] of gated) {
|
|
242
|
+
const dO = this._dOpen(gated, q, p, gx);
|
|
243
|
+
if (dO !== 0) this._j(bf, r, bf, 1 + n + itf.reactions.length + q, -gG0 * deta * dO);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
res[R[bL * M + r]] += Af * u[o];
|
|
247
|
+
this._j(bL, r, bf, r, Af);
|
|
248
|
+
res[R[bR * M + r]] -= Af * u[o];
|
|
249
|
+
this._j(bR, r, bf, r, -Af);
|
|
250
|
+
}
|
|
251
|
+
itf.reactions.forEach((rx, k) => this._faceReaction(rx, k, f, bf, bL, bR, Af));
|
|
252
|
+
if (itf.gates.length > 0) this._faceGates(f, bf, bL, bR);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Full index of face f's gate q (its fraction open).
|
|
256
|
+
_gateSlot(f, q) {
|
|
257
|
+
return this.blockOfFace[f] * this.M + 1 + this.n + this.model.interfaces[f].reactions.length + q;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// The voltage a gate follows: across its face, or across its port's capacitance at its node.
|
|
261
|
+
_gateVoltage(gt) {
|
|
262
|
+
return gt.port === undefined ? this._faceVoltage(gt.f) : this._portVoltage(gt.port, gt.g);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// The voltage across port k's capacitance at node g, φ − (V − zeroCharge) (V): a membrane's,
|
|
266
|
+
// inside (the region) minus outside.
|
|
267
|
+
_portVoltage(k, g) {
|
|
268
|
+
const { M, u, uLo, VT } = this, b = this.blockOfNode[g], port = this.model.ports[k];
|
|
269
|
+
return VT * (u[b * M] + uLo[b * M]) - (this.termV[2 + k] - port.capacitance.zeroCharge);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// The voltage across face f, φ_right − φ_left (V).
|
|
273
|
+
_faceVoltage(f) {
|
|
274
|
+
const { M, u, uLo, VT } = this, bf = this.blockOfFace[f], bL = bf - 1, bR = bf + 1;
|
|
275
|
+
return VT * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M]));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Face f's gates, each a row (x − x_old)/dt = α(1 − x) − βx in the voltage across the face.
|
|
279
|
+
_faceGates(f, bf, bL, bR) {
|
|
280
|
+
const { res, VT } = this, R = this.rix, itf = this.model.interfaces[f], dt = this.dtNow;
|
|
281
|
+
const V = this._faceVoltage(f), k0 = 1 + this.n + itf.reactions.length;
|
|
282
|
+
itf.gates.forEach((gate, q) => {
|
|
283
|
+
const o = this._gateSlot(f, q), x = this.u[o];
|
|
284
|
+
const [a, da] = gateRate(gate.alpha, V), [b, db] = gateRate(gate.beta, V);
|
|
285
|
+
const store = Number.isFinite(dt) ? 1 / dt : 0, j = this.gateIndex[f] + q;
|
|
286
|
+
res[R[o]] = store * (x - this.gateOld[j]) - (a * (1 - x) - b * x);
|
|
287
|
+
this._j(bf, k0 + q, bf, k0 + q, store + a + b);
|
|
288
|
+
const dV = -(da * (1 - x) - db * x) * VT; // per unit φ̂_R, and minus that per unit φ̂_L
|
|
289
|
+
this._j(bf, k0 + q, bR, 0, dV);
|
|
290
|
+
this._j(bf, k0 + q, bL, 0, -dV);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// A face reaction (see normalizeFaceReactions). Its rate r_k is an unknown of the face block,
|
|
295
|
+
// with the row r_k − rate(u_L, u_R) = 0, and each participant's edge node takes ν·r_k (made
|
|
296
|
+
// there when ν > 0). The rate couples the two edge nodes only through the face block between
|
|
297
|
+
// them, which keeps the system block-tridiagonal.
|
|
298
|
+
_faceReaction(rx, k, f, bf, bL, bR, Af) {
|
|
299
|
+
const { n, M, u, uLo, c, res } = this;
|
|
300
|
+
const R = this.rix;
|
|
301
|
+
const grid = this.model.grid;
|
|
302
|
+
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1];
|
|
303
|
+
const al = rx.alpha;
|
|
304
|
+
const dL = this.dA.fill(0), dR = this.dB.fill(0); // ∂ ln(prefactor)/∂slot, per side
|
|
305
|
+
let pref = rx.k0, aHi = rx.fixedA, aLo = 0, rate;
|
|
306
|
+
for (const p of rx.part) {
|
|
307
|
+
const g = p.side ? gR : gL, b = p.side ? bR : bL, o = b * M + 1 + p.i;
|
|
308
|
+
aHi -= p.nu * u[o];
|
|
309
|
+
aLo -= p.nu * uLo[o];
|
|
310
|
+
if (rx.srh || rx.vmax || this.nodeConductor[g] === p.i) continue; // a conductor's carrier has activity 1
|
|
311
|
+
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
312
|
+
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
313
|
+
this._dlnc(g, p.i, e, p.side ? dR : dL);
|
|
314
|
+
}
|
|
315
|
+
if (rx.vmax) {
|
|
316
|
+
// Saturating: r = vmax Π (c/(c + K))^{|ν|} (1 − e^{−a}); ∂ln S/∂ln c = |ν| K/(c + K).
|
|
317
|
+
let S = 1;
|
|
318
|
+
rx.part.forEach((p, q) => {
|
|
319
|
+
if (p.nu >= 0) return;
|
|
320
|
+
const g = p.side ? gR : gL, cc = c[g * n + p.i], K = rx.K[q];
|
|
321
|
+
S *= powi(cc / (cc + K), -p.nu);
|
|
322
|
+
this._dlnc(g, p.i, (-p.nu * K) / (cc + K), p.side ? dR : dL);
|
|
323
|
+
});
|
|
324
|
+
const a = aHi + aLo, back = Math.exp(-a);
|
|
325
|
+
rate = rx.vmax * S * -Math.expm1(-a);
|
|
326
|
+
for (let s = 0; s < M; s++) {
|
|
327
|
+
dL[s] *= rate;
|
|
328
|
+
dR[s] *= rate;
|
|
329
|
+
}
|
|
330
|
+
for (const p of rx.part) (p.side ? dR : dL)[1 + p.i] -= rx.vmax * S * back * p.nu;
|
|
331
|
+
} else if (rx.srh) {
|
|
332
|
+
// SRH, with n and p each at its own side's edge node.
|
|
333
|
+
const { n: pn, p: pp } = rx.srh, gn = pn.side ? gR : gL, gq = pp.side ? gR : gL;
|
|
334
|
+
const s = this._srhRate(rx.srh, c[gn * n + pn.i], c[gq * n + pp.i], aHi + aLo, ...this._srhDegen(gn, gq, { n: pn, p: pp }));
|
|
335
|
+
rate = s.rate;
|
|
336
|
+
this._srhInto(gn, pn.i, s.Cn, s.Cxn, pn.side ? dR : dL);
|
|
337
|
+
this._srhInto(gq, pp.i, s.Cp, s.Cxp, pp.side ? dR : dL);
|
|
338
|
+
for (const p of rx.part) (p.side ? dR : dL)[1 + p.i] -= s.Ca * p.nu;
|
|
339
|
+
} else {
|
|
340
|
+
const { g: bv, gp } = bvFactor(aHi + aLo, al);
|
|
341
|
+
rate = pref * bv;
|
|
342
|
+
// ∂rate/∂slot = rate·∂ln(prefactor) + pref·g′·∂a, with ∂a/∂η_i = −ν
|
|
343
|
+
for (let s = 0; s < M; s++) {
|
|
344
|
+
dL[s] *= rate;
|
|
345
|
+
dR[s] *= rate;
|
|
346
|
+
}
|
|
347
|
+
for (const p of rx.part) (p.side ? dR : dL)[1 + p.i] -= pref * gp * p.nu;
|
|
348
|
+
}
|
|
349
|
+
const slot = 1 + n + k, o = bf * M + slot;
|
|
350
|
+
res[R[o]] = u[o] - rate;
|
|
351
|
+
this._j(bf, slot, bf, slot, 1);
|
|
352
|
+
for (let s = 0; s < M; s++) {
|
|
353
|
+
if (dL[s] !== 0) this._j(bf, slot, bL, s, -dL[s]);
|
|
354
|
+
if (dR[s] !== 0) this._j(bf, slot, bR, s, -dR[s]);
|
|
355
|
+
}
|
|
356
|
+
for (const p of rx.part) {
|
|
357
|
+
const b = p.side ? bR : bL;
|
|
358
|
+
res[R[b * M + 1 + p.i]] -= Af * p.nu * u[o];
|
|
359
|
+
this._j(b, 1 + p.i, bf, slot, -Af * p.nu);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// The terminals whose ∂res/∂V (termB) and ∂I/∂x (termC) are kept, exactly: the floating ones,
|
|
364
|
+
// which the Newton solve borders on, or all of them (allTerminals: for the impedance, which reads
|
|
365
|
+
// its terminal's). A held terminal's are otherwise left as they fall (written but not kept
|
|
366
|
+
// consistent), which spares a device with dozens of ports (an axon's nodes of Ranvier) the cost
|
|
367
|
+
// of carrying each one's two dense vectors through every assembly.
|
|
368
|
+
_liveTerms() {
|
|
369
|
+
if (this.allTerminals) return this._allTerms ?? (this._allTerms = this.terms.map((_, k) => k));
|
|
370
|
+
return this.floating;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Every terminal's terms: its voltage held or floating, how the residual depends on it
|
|
374
|
+
// (termB), its current into the device (termI) and how that depends on the state (termC).
|
|
375
|
+
_terminals(dt) {
|
|
376
|
+
this._refreshSources();
|
|
377
|
+
for (const k of this._liveTerms()) {
|
|
378
|
+
this.termB[k].fill(0);
|
|
379
|
+
this.termC[k].fill(0);
|
|
380
|
+
}
|
|
381
|
+
this.termDI.fill(0);
|
|
382
|
+
for (const x of this.termDX) x.fill(0);
|
|
383
|
+
this.termI.fill(0);
|
|
384
|
+
this.qg.fill(0);
|
|
385
|
+
this.qgSlope.fill(0);
|
|
386
|
+
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt, false));
|
|
387
|
+
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt, true));
|
|
388
|
+
this._contact('left', dt);
|
|
389
|
+
this._contact('right', dt);
|
|
390
|
+
for (const k of this.floating) this._circuit(k);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// A floating terminal's circuit law: I − I_set = 0 (driven by a current), or I − (V_src − V)/R = 0
|
|
394
|
+
// (a source behind a resistance).
|
|
395
|
+
_circuit(k) {
|
|
396
|
+
const d = this.terms[k].drive, src = sourceAt(d.src, this.sourceTime, this.sourceBefore);
|
|
397
|
+
if (d.kind === 'I') this.termRes[k] = this.termI[k] - src;
|
|
398
|
+
else {
|
|
399
|
+
this.termRes[k] = this.termI[k] - (src - this.termV[k]) / d.R;
|
|
400
|
+
this.termDI[k] += 1 / d.R;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// into += w × (the Jacobian row of block b, slot s), over compact columns; and where a terminal
|
|
405
|
+
// k reads that row as its current, its ∂/∂V's too: w × each live terminal's termB entry in it,
|
|
406
|
+
// into termDI[k] (its own) or termDX[k][j].
|
|
407
|
+
_captureRow(b, s, w, into, k = -1) {
|
|
408
|
+
const l = this.loc[b * this.M + s];
|
|
409
|
+
if (l < 0 || w === 0) return;
|
|
410
|
+
if (k >= 0) {
|
|
411
|
+
const o = this.rix[b * this.M + s];
|
|
412
|
+
for (const j of this._liveTerms()) {
|
|
413
|
+
const v = this.termB[j][o];
|
|
414
|
+
if (v === 0) continue;
|
|
415
|
+
if (j === k) this.termDI[k] += w * v;
|
|
416
|
+
else this.termDX[k][j] += w * v;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (this.lin) this.lin.capture(this.rix[b * this.M + s], w, this.lin.rate, into);
|
|
420
|
+
const { sys, nB } = this, { sizes, offA, offB, offC, offX } = sys, m = sizes[b];
|
|
421
|
+
for (const [X, nb, off] of [['A', b - 1, offA], ['B', b, offB], ['C', b + 1, offC]]) {
|
|
422
|
+
if (nb < 0 || nb >= nB) continue;
|
|
423
|
+
const mc = sizes[nb], o = off[b] + l * mc, arr = sys[X], c0 = offX[nb];
|
|
424
|
+
for (let c = 0; c < mc; c++) if (arr[o + c] !== 0) into[c0 + c] += w * arr[o + c];
|
|
425
|
+
}
|
|
426
|
+
return m;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// An internal port (terminal k): its exchange with each node of its window, as a source per
|
|
430
|
+
// volume. A held ('equilibrium') level replaces the node's balance row; the source is then that
|
|
431
|
+
// row's residual, read just before. Its current into the device is Σ z F × the sources. In two
|
|
432
|
+
// passes over the ports: everything else, then the held levels (`holds`), so that a level reads
|
|
433
|
+
// every other port's terms at its nodes, and none is added into its row after.
|
|
434
|
+
_port(port, flux, k, dt, holds) {
|
|
435
|
+
const R = this.rix;
|
|
436
|
+
const { n, M, u, uLo, res, z, VT } = this;
|
|
437
|
+
const F = FARADAY, vol = this.model.grid.vol;
|
|
438
|
+
const B = this.termB[k], C = this.termC[k];
|
|
439
|
+
if (!holds) flux.fill(0);
|
|
440
|
+
for (let i = 0; i < n; i++) {
|
|
441
|
+
const link = port.species[i];
|
|
442
|
+
if (link.type === 'blocked' || (link.type === 'equilibrium') !== holds) continue;
|
|
443
|
+
const r = 1 + i, target = this.portEta(port, i), zF = z[i] * F;
|
|
444
|
+
for (let w = 0; w < port.nodes.length; w++) {
|
|
445
|
+
const g = port.nodes[w], b = this.blockOfNode[g], o = b * M + r, v = vol[g] * port.fill[w];
|
|
446
|
+
const deta = target - u[o] - uLo[o]; // (μ̄_out − μ̄)/RT
|
|
447
|
+
if (link.type === 'equilibrium') {
|
|
448
|
+
if (!port.inside[w]) continue; // (a held level holds the nodes inside the window)
|
|
449
|
+
// a device end node's level is its contact's business, where the contact links it
|
|
450
|
+
if ((g === 0 && this.model.contacts.left.species[i].type !== 'blocked') || (g === this.nNodes - 1 && this.model.contacts.right.species[i].type !== 'blocked')) continue;
|
|
451
|
+
flux[i] += res[R[o]];
|
|
452
|
+
this._captureRow(b, r, zF, C, k);
|
|
453
|
+
this._replaceRow(b, r);
|
|
454
|
+
for (const Bk of this.termB) Bk[R[o]] = 0; // (other ports' terms in the row replaced)
|
|
455
|
+
this._j(b, r, b, r, 1);
|
|
456
|
+
res[R[o]] = -deta;
|
|
457
|
+
B[R[o]] += -z[i] / VT;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
// conductance: s = G V_T (η_out − η)/(z² F); exchange: s = k (η_out − η). Gated, G times
|
|
461
|
+
// Π x^p, the gates at this node (each taken in [0, 1]).
|
|
462
|
+
const kk0 = link.type === 'conductance' ? (link.G * VT) / (z[i] * z[i] * F) : link.k;
|
|
463
|
+
const gated = link.gates ?? [], q0 = 1 + n + port.surface.length;
|
|
464
|
+
const open = gated.length ? this._open(gated, (q) => u[b * M + q0 + q]) : 1, kk = kk0 * open;
|
|
465
|
+
res[R[o]] -= v * kk * deta;
|
|
466
|
+
this._j(b, r, b, r, v * kk);
|
|
467
|
+
flux[i] += v * kk * deta;
|
|
468
|
+
if (z[i] !== 0) {
|
|
469
|
+
B[R[o]] += (-v * kk * z[i]) / VT;
|
|
470
|
+
C[R[o]] += -zF * v * kk;
|
|
471
|
+
this.termDI[k] += (zF * v * kk * z[i]) / VT;
|
|
472
|
+
}
|
|
473
|
+
for (const [q, p] of gated) {
|
|
474
|
+
const d = this._dOpen(gated, q, p, (qq) => u[b * M + q0 + qq]);
|
|
475
|
+
if (d === 0) continue;
|
|
476
|
+
this._j(b, r, b, q0 + q, -v * kk0 * deta * d);
|
|
477
|
+
if (z[i] !== 0) C[R[b * M + q0 + q]] += zF * v * kk0 * deta * d;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (holds) {
|
|
482
|
+
for (let i = 0; i < n; i++) this.termI[k] += z[i] * F * flux[i];
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
port.reactions.forEach((rx, x) => this._portReaction(port, rx, this.portArea[k - 2], this.portRates[k - 2][x], flux, k));
|
|
486
|
+
if (port.capacitance) this._portCapacitance(port, k, dt);
|
|
487
|
+
if (port.gates.length > 0) this._portGates(port, k, dt);
|
|
488
|
+
if (port.surface.length > 0 && Number.isFinite(dt)) this._surfaceStorage(port, dt);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// A port's reaction at each node of its window: Butler–Volmer per area, as at a face, against
|
|
492
|
+
// the electrode's carrier at the port's level (activity 1), times the electrode's area per
|
|
493
|
+
// volume. Each node's rate is explicit (no unknown of its own): it enters the balances of the
|
|
494
|
+
// species it makes and consumes, and the port's current.
|
|
495
|
+
_portReaction(port, rx, area, rates, flux, k) {
|
|
496
|
+
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;
|
|
497
|
+
const B = this.termB[k], C = this.termC[k], al = rx.alpha, d = this.dA, surf = port.surface;
|
|
498
|
+
// The carrier's part of the affinity, and how it moves with the port's voltage.
|
|
499
|
+
let aV = rx.fixedA, daV = 0, q = 0; // q: charge the forward reaction brings into the device, per event
|
|
500
|
+
for (const p of rx.part) {
|
|
501
|
+
if (p.side === 1) {
|
|
502
|
+
aV -= p.nu * this.portEta(port, p.i);
|
|
503
|
+
daV -= (p.nu * z[p.i]) / VT;
|
|
504
|
+
} else if (p.side === 0) q += p.nu * z[p.i];
|
|
505
|
+
}
|
|
506
|
+
const slot = (p) => (p.side === 2 ? 1 + n + p.s : 1 + p.i);
|
|
507
|
+
port.nodes.forEach((g, w) => {
|
|
508
|
+
const b = this.blockOfNode[g], s = vol[g] * area[w];
|
|
509
|
+
d.fill(0);
|
|
510
|
+
let pref = rx.k0, aHi = aV, aLo = 0;
|
|
511
|
+
for (const p of rx.part) {
|
|
512
|
+
if (p.side === 1) continue;
|
|
513
|
+
const o = b * M + slot(p);
|
|
514
|
+
aHi -= p.nu * u[o];
|
|
515
|
+
aLo -= p.nu * uLo[o];
|
|
516
|
+
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
517
|
+
if (p.side === 2) {
|
|
518
|
+
// a surface species at its coverage θ = θ₀e^ζ, with ∂ln θ_s/∂η_x = δ_sx − θ_x
|
|
519
|
+
pref *= powr(th[g * nSurf + p.s], e);
|
|
520
|
+
d[1 + n + p.s] += e;
|
|
521
|
+
for (let x = 0; x < surf.length; x++) d[1 + n + x] -= e * th[g * nSurf + x];
|
|
522
|
+
} else {
|
|
523
|
+
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
524
|
+
this._dlnc(g, p.i, e, d);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (rx.sites) {
|
|
528
|
+
// the bare sites its transition state holds: θ₀ = 1 − Σθ, with ∂ln θ₀/∂η_s = −θ_s
|
|
529
|
+
for (let x = 0; x < surf.length; x++) d[1 + n + x] -= rx.sites * th[g * nSurf + x];
|
|
530
|
+
pref *= powr(this.th0[g], rx.sites);
|
|
531
|
+
}
|
|
532
|
+
const { g: bv, gp } = bvFactor(aHi + aLo, al);
|
|
533
|
+
const rate = pref * bv;
|
|
534
|
+
rates[w] = rate;
|
|
535
|
+
// ∂rate/∂slot = rate·∂ln(prefactor) + pref·g′·∂a, with ∂a/∂η = −ν
|
|
536
|
+
for (let t = 0; t < M; t++) d[t] *= rate;
|
|
537
|
+
for (const p of rx.part) if (p.side !== 1) d[slot(p)] -= pref * gp * p.nu;
|
|
538
|
+
const dV = pref * gp * daV;
|
|
539
|
+
for (const p of rx.part) {
|
|
540
|
+
if (p.side === 1) continue;
|
|
541
|
+
// the region's species per volume of the window; a surface's per area of electrode
|
|
542
|
+
const row = slot(p), o = b * M + row, f = p.side === 2 ? 1 : s;
|
|
543
|
+
res[R[o]] -= f * p.nu * rate;
|
|
544
|
+
if (p.side === 0) flux[p.i] += s * p.nu * rate;
|
|
545
|
+
for (let t = 0; t < M; t++) if (d[t] !== 0) this._j(b, row, b, t, -f * p.nu * d[t]);
|
|
546
|
+
B[R[o]] -= f * p.nu * dV;
|
|
547
|
+
}
|
|
548
|
+
for (let t = 0; t < M; t++) if (d[t] !== 0) C[R[b * M + t]] += q * F * s * d[t];
|
|
549
|
+
this.termDI[k] += q * F * s * dV;
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// A membrane port's gates, at each node of its window: (x − x_old)/dt = α(1 − x) − βx in the
|
|
554
|
+
// voltage across its capacitance there, φ − (V − zeroCharge).
|
|
555
|
+
_portGates(port, k, dt) {
|
|
556
|
+
const { n, res, VT } = this, R = this.rix, B = this.termB[k], store = Number.isFinite(dt) ? 1 / dt : 0, q0 = 1 + n + port.surface.length;
|
|
557
|
+
port.nodes.forEach((g, w) => {
|
|
558
|
+
const b = this.blockOfNode[g], V = this._portVoltage(k - 2, g);
|
|
559
|
+
port.gates.forEach((gate, q) => {
|
|
560
|
+
const o = b * this.M + q0 + q, x = this.u[o], j = this.portGateIndex[k - 2] + w * port.gates.length + q;
|
|
561
|
+
const [a, da] = gateRate(gate.alpha, V), [bb, db] = gateRate(gate.beta, V);
|
|
562
|
+
res[R[o]] = store * (x - this.gateOld[j]) - (a * (1 - x) - bb * x);
|
|
563
|
+
this._j(b, q0 + q, b, q0 + q, store + a + bb);
|
|
564
|
+
const s = -(da * (1 - x) - db * x); // ∂/∂V_m, with V_m = V_T φ̂ − V + zeroCharge
|
|
565
|
+
this._j(b, q0 + q, b, 0, s * VT);
|
|
566
|
+
B[R[o]] += -s;
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Π x^p over a link's gates (each x taken in [0, 1]), and its derivative in gate q.
|
|
572
|
+
_open(gated, x) {
|
|
573
|
+
let open = 1;
|
|
574
|
+
for (const [q, p] of gated) open *= powi(Math.min(1, Math.max(0, x(q))), p);
|
|
575
|
+
return open;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
_dOpen(gated, q, p, x) {
|
|
579
|
+
const xq = x(q);
|
|
580
|
+
if (!(xq > 0 && xq < 1)) return 0;
|
|
581
|
+
let others = 1;
|
|
582
|
+
for (const [q2, p2] of gated) if (q2 !== q) others *= powi(Math.min(1, Math.max(0, x(q2))), p2);
|
|
583
|
+
return p * powi(xq, p - 1) * others;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// A capacitance spread through a port's window: per area of electrode, σ = C (V − zeroCharge − φ)
|
|
587
|
+
// on the port's side, so each node's charge balance (its φ row) gains vol·a·σ, and the port
|
|
588
|
+
// passes the charging current d(Σ vol·a·σ)/dt (none in a steady state). An end node whose φ its
|
|
589
|
+
// contact sets is left to the contact.
|
|
590
|
+
_portCapacitance(port, k, dt) {
|
|
591
|
+
const { M, u, uLo, res, VT } = this, R = this.rix, vol = this.model.grid.vol, area = this.portArea[k - 2];
|
|
592
|
+
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
593
|
+
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
594
|
+
// Its current, the charging (Q − Q at the step's start)/dt; or, steady at no current, the
|
|
595
|
+
// charge it keeps in place of that (see captureLaws in src/lawRows.js).
|
|
596
|
+
const rate = Number.isFinite(dt) ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
597
|
+
let Q = 0, dQdV = 0;
|
|
598
|
+
port.nodes.forEach((g, w) => {
|
|
599
|
+
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
600
|
+
const b = this.blockOfNode[g], s = vol[g] * area[w] * Cs;
|
|
601
|
+
const q = s * (V - zeroCharge - VT * (u[b * M] + uLo[b * M]));
|
|
602
|
+
this.qg[g] += area[w] * Cs * (V - zeroCharge - VT * (u[b * M] + uLo[b * M]));
|
|
603
|
+
this.qgSlope[g] += area[w] * Cs;
|
|
604
|
+
Q += q;
|
|
605
|
+
dQdV += s;
|
|
606
|
+
res[R[b * M]] -= q;
|
|
607
|
+
this._j(b, 0, b, 0, s * VT);
|
|
608
|
+
B[R[b * M]] -= s;
|
|
609
|
+
if (rate > 0) Ct[R[b * M]] += -s * VT * rate;
|
|
610
|
+
});
|
|
611
|
+
this.portQ[k - 2] = Q;
|
|
612
|
+
if (rate > 0) {
|
|
613
|
+
this.termI[k] += (Q - (dt < Infinity ? this.portQStart[k - 2] : this.chargeRefs.get(k))) * rate;
|
|
614
|
+
this.termDI[k] += dQdV * rate;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// An electrode surface's storage, per area of electrode: Γ (θ_s − θ_s,old)/dt in each surface
|
|
619
|
+
// species' row, with ∂θ_s/∂η_x = θ_s (δ_sx − θ_x).
|
|
620
|
+
_surfaceStorage(port, dt) {
|
|
621
|
+
const { n, M, res, nSurf, th, thOld } = this, R = this.rix, surf = port.surface, G = surf[0].capacity;
|
|
622
|
+
for (const g of port.nodes) {
|
|
623
|
+
const b = this.blockOfNode[g];
|
|
624
|
+
// Near full coverage θ − θ_old cancels (both ~1, the change ~θ₀): the dominant species'
|
|
625
|
+
// change comes from the small ones instead, Δθ = −Δθ₀ − Σ_others Δθ.
|
|
626
|
+
let top = 0;
|
|
627
|
+
for (let q = 1; q < surf.length; q++) if (th[g * nSurf + q] > th[g * nSurf + top]) top = q;
|
|
628
|
+
for (let q = 0; q < surf.length; q++) {
|
|
629
|
+
const tq = th[g * nSurf + q];
|
|
630
|
+
let change = tq - thOld[g * nSurf + q];
|
|
631
|
+
if (q === top && tq > 0.5) {
|
|
632
|
+
change = -(this.th0[g] - this.th0Old[g]);
|
|
633
|
+
for (let x = 0; x < surf.length; x++) if (x !== q) change -= th[g * nSurf + x] - thOld[g * nSurf + x];
|
|
634
|
+
}
|
|
635
|
+
res[R[b * M + 1 + n + q]] += (G * change) / dt;
|
|
636
|
+
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]));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// One contact (terminal 0 or 1): record the flux through its outer face and the current into
|
|
642
|
+
// the device (read from the end box before anything here is added: whatever the box needs
|
|
643
|
+
// comes through the contact), then add its exchange terms, its equilibrium links and its φ law.
|
|
644
|
+
_contact(side, dt) {
|
|
645
|
+
const R = this.rix;
|
|
646
|
+
const { model, n, M, u, uLo, res, c, z, VT } = this;
|
|
647
|
+
const F = FARADAY;
|
|
648
|
+
const ct = model.contacts[side];
|
|
649
|
+
const k = side === 'left' ? 0 : 1, Vt = this.termV[k];
|
|
650
|
+
const B = this.termB[k], C = this.termC[k];
|
|
651
|
+
const g = side === 'left' ? 0 : this.nNodes - 1;
|
|
652
|
+
const b = this.blockOfNode[g];
|
|
653
|
+
const sgn = side === 'left' ? 1 : -1;
|
|
654
|
+
const flux = this.contactFlux[side];
|
|
655
|
+
const Ac = model.grid.area[g]; // the links' laws are per area: what they pass is that times A here
|
|
656
|
+
const dyn = Number.isFinite(dt);
|
|
657
|
+
|
|
658
|
+
// Before anything is added, each balance residual is the flux into the device here. (A
|
|
659
|
+
// closed end solved in charge rows has its ions' storage left out until later: none of them
|
|
660
|
+
// passes, so their flux is zero.)
|
|
661
|
+
const closedIons = this.combining && dyn && this.chargeNode[g] === 1;
|
|
662
|
+
let I = 0;
|
|
663
|
+
const held = this.portHeldEnd.subarray(k * n, (k + 1) * n);
|
|
664
|
+
for (let i = 0; i < n; i++) {
|
|
665
|
+
const active = this.loc[b * M + 1 + i] >= 0 && !(closedIons && z[i] !== 0) && !held[i];
|
|
666
|
+
const nIn = active ? res[R[b * M + 1 + i]] : 0;
|
|
667
|
+
flux[i] = sgn * nIn; // (toward +x)
|
|
668
|
+
if (active && z[i] !== 0) {
|
|
669
|
+
I += z[i] * F * nIn;
|
|
670
|
+
this._captureRow(b, 1 + i, z[i] * F, C, k);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// The ions' storage, left out until now (see lateStorage): in each balance, and in what
|
|
674
|
+
// each species brings in, but not in the current.
|
|
675
|
+
if (this.combining && dyn && this.lateStorage[g] === 1) {
|
|
676
|
+
const v = model.grid.vol[g], { cOld } = this;
|
|
677
|
+
for (let i = 0; i < n; i++) {
|
|
678
|
+
if (z[i] === 0 || this.loc[b * M + 1 + i] < 0 || held[i]) continue;
|
|
679
|
+
const ci = c[g * n + i], st = (v * (ci - cOld[g * n + i])) / dt;
|
|
680
|
+
res[R[b * M + 1 + i]] += st;
|
|
681
|
+
flux[i] += sgn * st;
|
|
682
|
+
this._j(b, 1 + i, b, 1 + i, (v * ci) / dt);
|
|
683
|
+
this._j(b, 1 + i, b, 0, (-v * z[i] * ci) / dt);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Conductance links: J (toward the device) = G (V_out − V_i), V_out = V_t + offset; exchange
|
|
688
|
+
// links (neutral species): N_in = k (μ_out − μ)/RT; velocity links, v (c_eq − c).
|
|
689
|
+
for (let i = 0; i < n; i++) {
|
|
690
|
+
const link = ct.species[i];
|
|
691
|
+
const o = b * M + 1 + i;
|
|
692
|
+
if (link.type === 'exchange') {
|
|
693
|
+
res[R[o]] -= Ac * link.k * (link.mu / model.RT - u[o] - uLo[o]);
|
|
694
|
+
this._j(b, 1 + i, b, 1 + i, Ac * link.k);
|
|
695
|
+
} else if (link.type === 'conductance') {
|
|
696
|
+
const Vi = (VT * (u[o] + uLo[o])) / z[i];
|
|
697
|
+
res[R[o]] -= (Ac * link.G * (Vt + link.offset - Vi)) / (z[i] * F);
|
|
698
|
+
this._j(b, 1 + i, b, 1 + i, (Ac * link.G * VT) / (z[i] * z[i] * F));
|
|
699
|
+
B[R[o]] += (-Ac * link.G) / (z[i] * F);
|
|
700
|
+
} else if (link.type === 'velocity') {
|
|
701
|
+
// N_in = v c (e^(η_out − η) − 1): v (c_eq − c), c_eq in equilibrium with the outside.
|
|
702
|
+
const ci = c[g * n + i], E = Math.exp(Math.min(this.contactEta(side, i) - u[o] - uLo[o], 700));
|
|
703
|
+
const N = Ac * link.v * ci * (E - 1), d = this.dA;
|
|
704
|
+
res[R[o]] -= N;
|
|
705
|
+
d.fill(0);
|
|
706
|
+
this._dlnc(g, i, 1, d);
|
|
707
|
+
for (let s = 0; s <= n; s++) if (d[s] !== 0) this._j(b, 1 + i, b, s, -N * d[s]);
|
|
708
|
+
this._j(b, 1 + i, b, 1 + i, Ac * link.v * ci * E);
|
|
709
|
+
if (z[i] !== 0) B[R[o]] += (-Ac * link.v * ci * E * z[i]) / VT;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Equilibrium links: Dirichlet on the known outside level.
|
|
714
|
+
for (let i = 0; i < n; i++) {
|
|
715
|
+
const link = ct.species[i];
|
|
716
|
+
if (link.type !== 'equilibrium') continue;
|
|
717
|
+
const o = b * M + 1 + i;
|
|
718
|
+
this._replaceRow(b, 1 + i);
|
|
719
|
+
for (const Bk of this.termB) Bk[R[o]] = 0; // (a port's terms in the row replaced)
|
|
720
|
+
this._j(b, 1 + i, b, 1 + i, 1);
|
|
721
|
+
res[R[o]] = u[o] - this.contactEta(side, i) + uLo[o]; // (the big terms first: u + uLo rounds to u)
|
|
722
|
+
if (z[i] !== 0) B[R[o]] += -z[i] / VT;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// φ law (none where φ is undefined: nothing at the end node responds to it). The displacement
|
|
726
|
+
// into the device, D_in = sgn·D, adds (D_in − D_in at the step's start)/dt to the current.
|
|
727
|
+
const link = ct.phi, Dstart = sgn * this.contactDStart[side];
|
|
728
|
+
// (In a steady solve, what a law in this circuit row counts of D_in: see applyConstraints in
|
|
729
|
+
// src/lawRows.js.)
|
|
730
|
+
const q = !dyn && this.steady && this.contactLaw[k] ? (this.contactQ[k] ??= { row: new Float64Array(this.sys.size + 1), D: 0, dV: 0 }) : null;
|
|
731
|
+
if (q) {
|
|
732
|
+
q.row.fill(0);
|
|
733
|
+
q.D = 0;
|
|
734
|
+
q.dV = 0;
|
|
735
|
+
}
|
|
736
|
+
if (this.phiUndefined[g]) {
|
|
737
|
+
this.contactD[side] = 0;
|
|
738
|
+
} else if (link.type === 'capacitive') {
|
|
739
|
+
// Gate, or metal across a Stern layer, at φ_g = V_t − zeroCharge. D toward +x.
|
|
740
|
+
const Din = Ac * link.C * (Vt - link.zeroCharge - VT * u[b * M]);
|
|
741
|
+
res[R[b * M]] -= Din;
|
|
742
|
+
this._j(b, 0, b, 0, Ac * link.C * VT);
|
|
743
|
+
B[R[b * M]] += -Ac * link.C;
|
|
744
|
+
this.contactD[side] = sgn * Din;
|
|
745
|
+
if (q && this.loc[b * M] >= 0) [q.D, q.row[R[b * M]], q.dV] = [Din, -Ac * link.C * VT, Ac * link.C];
|
|
746
|
+
// Its current, the charging (D_in − D_in at the step's start)/dt; or, steady at no current,
|
|
747
|
+
// the charge it keeps in place of that (see captureLaws in src/lawRows.js).
|
|
748
|
+
const rate = dyn ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
749
|
+
if (rate > 0) {
|
|
750
|
+
I += (Din - (dyn ? Dstart : this.chargeRefs.get(k))) * rate;
|
|
751
|
+
C[R[b * M]] += -Ac * link.C * VT * rate;
|
|
752
|
+
this.termDI[k] += Ac * link.C * rate;
|
|
753
|
+
}
|
|
754
|
+
} else if (link.type === 'pinned' || link.type === 'bulk') {
|
|
755
|
+
// The Poisson residual is the outside's charge, D_in.
|
|
756
|
+
const Din = res[R[b * M]];
|
|
757
|
+
this.contactD[side] = sgn * Din;
|
|
758
|
+
if (q) {
|
|
759
|
+
q.D = Din;
|
|
760
|
+
this._captureRow(b, 0, 1, q.row);
|
|
761
|
+
}
|
|
762
|
+
if (dyn && !(this.combining && this.lateStorage[g] === 1)) {
|
|
763
|
+
I += (Din - Dstart) / dt;
|
|
764
|
+
this._captureRow(b, 0, 1 / dt, C, k);
|
|
765
|
+
} else if (!dyn && this.steady && this.chargeRefs.has(k)) {
|
|
766
|
+
I += Din - this.chargeRefs.get(k);
|
|
767
|
+
this._captureRow(b, 0, 1, C, k);
|
|
768
|
+
}
|
|
769
|
+
this._replaceRow(b, 0);
|
|
770
|
+
for (const Bk of this.termB) Bk[R[b * M]] = 0;
|
|
771
|
+
if (link.type === 'pinned') {
|
|
772
|
+
// φ_edge = V_t − zeroCharge (the C → ∞ limit).
|
|
773
|
+
this._j(b, 0, b, 0, 1);
|
|
774
|
+
res[R[b * M]] = u[b * M] - (Vt - link.zeroCharge) / VT + uLo[b * M];
|
|
775
|
+
B[R[b * M]] += -1 / VT;
|
|
776
|
+
} else {
|
|
777
|
+
// Plain bulk: local neutrality.
|
|
778
|
+
let q = this.rhoFixed[g], dq = 0;
|
|
779
|
+
for (let i = 0; i < n; i++) {
|
|
780
|
+
const kk = g * n + i;
|
|
781
|
+
if (!this.present[kk]) continue;
|
|
782
|
+
const Kz = this._Kz(g, i);
|
|
783
|
+
q += F * z[i] * c[kk];
|
|
784
|
+
dq -= F * z[i] * Kz;
|
|
785
|
+
if (Kz !== 0) this._j(b, 0, b, 1 + i, F * Kz);
|
|
786
|
+
}
|
|
787
|
+
this._j(b, 0, b, 0, dq);
|
|
788
|
+
res[R[b * M]] = q;
|
|
789
|
+
}
|
|
790
|
+
} else {
|
|
791
|
+
this.contactD[side] = 0;
|
|
792
|
+
}
|
|
793
|
+
this.termI[k] = I;
|
|
794
|
+
}
|
|
795
|
+
}
|