driftlet 0.11.4 → 0.13.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 +50 -74
- package/docs/device.md +143 -9
- package/docs/kit.md +25 -2
- package/docs/numerics.md +8 -1
- package/docs/reliability.md +60 -3
- package/docs/statistics.md +10 -0
- package/llms.txt +572 -259
- package/package.json +1 -1
- package/src/channels.js +96 -0
- package/src/describe.js +4 -2
- package/src/device.js +108 -21
- package/src/index.js +6 -1
- package/src/kit.js +1 -0
- package/src/recorder.js +1 -1
- package/src/solution.js +28 -3
- package/src/solver.js +292 -67
- package/src/stack.js +6 -4
- package/src/types.js +2 -1
- package/types/channels.d.ts +50 -0
- package/types/kit.d.ts +1 -0
- package/types/recorder.d.ts +7 -3
- package/types/solver.d.ts +32 -5
- package/types/stack.d.ts +6 -5
- package/types/types.d.ts +5 -0
package/src/solver.js
CHANGED
|
@@ -261,17 +261,45 @@ export class SolverError extends Error {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// Lagrange weights at x through the points s0, s1, s2.
|
|
265
|
+
function lagrange3(x, s0, s1, s2) {
|
|
266
|
+
return [((x - s1) * (x - s2)) / ((s0 - s1) * (s0 - s2)), ((x - s0) * (x - s2)) / ((s1 - s0) * (s1 - s2)), ((x - s0) * (x - s1)) / ((s2 - s0) * (s2 - s1))];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// A gate's rate law (see normalizeGates in device.js) at voltage V: [rate, ∂rate/∂V], 1/s and 1/(s·V).
|
|
270
|
+
function gateRate(law, V) {
|
|
271
|
+
const { rate, midpoint, scale } = law;
|
|
272
|
+
if (law.type === 'exp') {
|
|
273
|
+
// (held finite where Newton's iterates stray to absurd voltages: β_m at −13 V would overflow)
|
|
274
|
+
const y = (V - midpoint) / scale, v = rate * Math.exp(Math.min(y, 700));
|
|
275
|
+
return [v, y > 700 ? 0 : v / scale];
|
|
276
|
+
}
|
|
277
|
+
if (law.type === 'sigmoid') {
|
|
278
|
+
const e = Math.exp((midpoint - V) / scale), d = 1 + e;
|
|
279
|
+
return Number.isFinite(e) ? [rate / d, (rate * e) / (scale * d * d)] : [0, 0];
|
|
280
|
+
}
|
|
281
|
+
// expLinear: rate·y/(1 − e^−y), smooth through y = 0 (rate there) and → rate·y for large y
|
|
282
|
+
const y = (V - midpoint) / scale;
|
|
283
|
+
if (Math.abs(y) < 1e-6) return [rate * (1 + y / 2), rate / (2 * scale)];
|
|
284
|
+
if (y < -700) return [0, 0];
|
|
285
|
+
const em = -Math.expm1(-y), ey = Math.exp(-y); // 1 − e^−y, e^−y
|
|
286
|
+
return [(rate * y) / em, (rate * (em - y * ey)) / (em * em * scale)];
|
|
287
|
+
}
|
|
288
|
+
|
|
264
289
|
export class Solver {
|
|
265
290
|
constructor(model) {
|
|
266
291
|
this.model = model;
|
|
267
292
|
const { grid, species, materials, regions, contacts } = model;
|
|
268
293
|
const n = species.length;
|
|
269
294
|
// Slots per block: φ̂ (or D, or a conductor's segment flux J), one per species, and at a face
|
|
270
|
-
// one per face reaction (its rate), as many as the
|
|
271
|
-
|
|
295
|
+
// one per face reaction (its rate) and one per gate (its fraction open), as many as the
|
|
296
|
+
// busiest face has.
|
|
297
|
+
const nRx = model.interfaces.reduce((m, itf) => Math.max(m, itf.reactions.length + itf.gates.length), 0);
|
|
272
298
|
// (and at an electrode port's nodes, one per surface species: its coverage's η)
|
|
273
299
|
const nSurf = model.ports.reduce((m, port) => Math.max(m, port.surface.length), 0);
|
|
274
|
-
|
|
300
|
+
// (and a membrane port's gates, after them: each one's fraction open)
|
|
301
|
+
const nPortSlots = model.ports.reduce((m, port) => Math.max(m, port.surface.length + port.gates.length), 0);
|
|
302
|
+
const M = n + 1 + Math.max(nRx, nPortSlots);
|
|
275
303
|
const nNodes = grid.nNodes;
|
|
276
304
|
const nFaces = regions.length - 1;
|
|
277
305
|
const nB = nNodes + nFaces;
|
|
@@ -306,6 +334,20 @@ export class Solver {
|
|
|
306
334
|
// The bare fraction θ₀, directly (1 − Σθ cancels as θ → 1); 1 where there's no surface.
|
|
307
335
|
this.th0 = new Float64Array(nNodes).fill(1);
|
|
308
336
|
this.th0Old = new Float64Array(nNodes).fill(1);
|
|
337
|
+
// Gates: each one's slot (at a face, after its reaction rates; at a membrane port's nodes, after
|
|
338
|
+
// the surface's coverages) and value at the step's start. Faces' first, then ports', node by
|
|
339
|
+
// node through each window.
|
|
340
|
+
this.gatePort = new Int32Array(nNodes).fill(-1);
|
|
341
|
+
model.ports.forEach((port, k) => port.gates.length > 0 && port.nodes.forEach((g) => (this.gatePort[g] = k)));
|
|
342
|
+
this.gateList = [
|
|
343
|
+
...model.interfaces.flatMap((itf, f) => itf.gates.map((gate, q) => ({ f, q, gate, o: this.blockOfFace[f] * M + 1 + n + itf.reactions.length + q }))),
|
|
344
|
+
...model.ports.flatMap((port, k) => Array.from(port.nodes).flatMap((g) => port.gates.map((gate, q) => ({ port: k, g, q, gate, o: this.blockOfNode[g] * M + 1 + n + port.surface.length + q })))),
|
|
345
|
+
];
|
|
346
|
+
this.gateOld = new Float64Array(this.gateList.length);
|
|
347
|
+
this.gateIndex = new Int32Array(nFaces); // face f's first gate in gateList
|
|
348
|
+
let j = 0;
|
|
349
|
+
for (let f = 0; f < nFaces; j += model.interfaces[f].gates.length, f++) this.gateIndex[f] = j;
|
|
350
|
+
this.portGateIndex = model.ports.map((port) => ((j += port.nodes.length * port.gates.length), j - port.nodes.length * port.gates.length));
|
|
309
351
|
|
|
310
352
|
// Per-node material data, flattened [g·n + i].
|
|
311
353
|
this.present = new Uint8Array(nNodes * n);
|
|
@@ -574,12 +616,14 @@ export class Solver {
|
|
|
574
616
|
for (let i = 0; i < n; i++) active[b * M + 1 + i] = this.present[g * n + i];
|
|
575
617
|
const k = this.surfPort[g];
|
|
576
618
|
if (k >= 0) for (let q = 0; q < model.ports[k].surface.length; q++) active[b * M + 1 + n + q] = 1;
|
|
619
|
+
const kg = this.gatePort[g], pg = model.ports[kg];
|
|
620
|
+
if (kg >= 0) for (let q = 0; q < pg.gates.length; q++) active[b * M + 1 + n + pg.surface.length + q] = 1;
|
|
577
621
|
}
|
|
578
622
|
model.interfaces.forEach((itf, f) => {
|
|
579
623
|
const bf = this.blockOfFace[f];
|
|
580
624
|
active[bf * M] = itf.phi.type === 'neutral' ? 0 : 1;
|
|
581
625
|
for (let i = 0; i < n; i++) active[bf * M + 1 + i] = itf.links[i].type === 'blocked' ? 0 : 1;
|
|
582
|
-
for (let k = 0; k < itf.reactions.length; k++) active[bf * M + 1 + n + k] = 1;
|
|
626
|
+
for (let k = 0; k < itf.reactions.length + itf.gates.length; k++) active[bf * M + 1 + n + k] = 1;
|
|
583
627
|
});
|
|
584
628
|
const sizes = new Int32Array(nB), loc = (this.loc = new Int32Array(nB * M).fill(-1));
|
|
585
629
|
for (let b = 0; b < nB; b++) for (let r = 0; r < M; r++) if (active[b * M + r]) loc[b * M + r] = sizes[b]++;
|
|
@@ -1176,7 +1220,7 @@ export class Solver {
|
|
|
1176
1220
|
/** η_i/RT that a fixed contact link imposes (or NaN if the link isn't fixed). */
|
|
1177
1221
|
contactEta(side, i) {
|
|
1178
1222
|
const link = this.model.contacts[side].species[i];
|
|
1179
|
-
if (link.type !== 'equilibrium') return NaN;
|
|
1223
|
+
if (link.type !== 'equilibrium' && link.type !== 'velocity') return NaN;
|
|
1180
1224
|
const z = this.z[i];
|
|
1181
1225
|
return z === 0 ? link.mu / this.model.RT : (z * (this.termV[side === 'left' ? 0 : 1] + link.offset)) / this.VT;
|
|
1182
1226
|
}
|
|
@@ -1393,6 +1437,11 @@ export class Solver {
|
|
|
1393
1437
|
});
|
|
1394
1438
|
if (w > 0) this.termV[k] = port.capacitance.zeroCharge + sum / w;
|
|
1395
1439
|
}
|
|
1440
|
+
// Gates start open as far as the starting voltage across their face holds them, α/(α + β).
|
|
1441
|
+
for (const gt of this.gateList) {
|
|
1442
|
+
const { gate, o } = gt, V = this._gateVoltage(gt), a = gateRate(gate.alpha, V)[0], b = gateRate(gate.beta, V)[0];
|
|
1443
|
+
u[o] = a + b > 0 ? a / (a + b) : 0;
|
|
1444
|
+
}
|
|
1396
1445
|
// A floating electrode spread through a port (without a double layer) starts where its
|
|
1397
1446
|
// reactions pass the current it's set (none, behind a resistance): at its mixed potential in
|
|
1398
1447
|
// the start's composition, not level with a held terminal, which can be volts away and pass
|
|
@@ -1601,6 +1650,7 @@ export class Solver {
|
|
|
1601
1650
|
const { grid, materials, regions } = model;
|
|
1602
1651
|
sys.clear();
|
|
1603
1652
|
res.fill(0);
|
|
1653
|
+
this.dtNow = dt;
|
|
1604
1654
|
if (this.lin) this.lin.reset(1 / dt);
|
|
1605
1655
|
this.computeConcentrations();
|
|
1606
1656
|
|
|
@@ -1752,7 +1802,7 @@ export class Solver {
|
|
|
1752
1802
|
// A start-of-step net charge (round-off in a solved state) isn't carried over: the neutrality
|
|
1753
1803
|
// row holds the new state neutral.
|
|
1754
1804
|
_chargeRows(dt) {
|
|
1755
|
-
const { n, M, res, c, cOld, z, sys, loc, present, termB, termC } = this, R = this.rix, F = FARADAY;
|
|
1805
|
+
const { n, M, res, c, cOld, z, sys, loc, present, termB, termC } = this, R = this.rix, F = FARADAY, live = this._liveTerms();
|
|
1756
1806
|
const { A, B, C, sizes, offA, offB, offC } = sys, vol = this.model.grid.vol, last = this.nB - 1;
|
|
1757
1807
|
for (const g of this.chargeNodes) {
|
|
1758
1808
|
const b = this.blockOfNode[g], lb = b * M, m = sizes[b], mp = b > 0 ? sizes[b - 1] : 0, mn = b < last ? sizes[b + 1] : 0;
|
|
@@ -1776,7 +1826,7 @@ export class Solver {
|
|
|
1776
1826
|
for (let q = 0, o = offB[b] + lk * m, s = offB[b] + li * m; q < m; q++) B[o + q] += w * B[s + q];
|
|
1777
1827
|
for (let q = 0, o = offC[b] + lk * mn, s = offC[b] + li * mn; q < mn; q++) C[o + q] += w * C[s + q];
|
|
1778
1828
|
res[rk] += w * res[ri];
|
|
1779
|
-
for (
|
|
1829
|
+
for (const t of live) termB[t][rk] += w * termB[t][ri];
|
|
1780
1830
|
if (this.lin) this.lin.combine(rk, ri, w);
|
|
1781
1831
|
}
|
|
1782
1832
|
// Columns: φ̂' = φ̂ with η fixed becomes φ̂ with η_i shifted by z_i, wherever node g's
|
|
@@ -1787,7 +1837,7 @@ export class Solver {
|
|
|
1787
1837
|
for (let r = 0, o = offB[b]; r < m; r++) B[o + r * m + p] += zi * B[o + r * m + li];
|
|
1788
1838
|
if (b > 0) for (let r = 0, mr = mp, o = offC[b - 1]; r < mr; r++) C[o + r * m + p] += zi * C[o + r * m + li];
|
|
1789
1839
|
if (b < last) for (let r = 0, mr = mn, o = offA[b + 1]; r < mr; r++) A[o + r * m + p] += zi * A[o + r * m + li];
|
|
1790
|
-
for (
|
|
1840
|
+
for (const t of live) termC[t][R[lb]] += zi * termC[t][R[lb + 1 + i]];
|
|
1791
1841
|
}
|
|
1792
1842
|
// Storage on the other charged balances, and neutrality: no φ̂' terms, exactly.
|
|
1793
1843
|
const v = vol[g], oB = offB[b];
|
|
@@ -2036,14 +2086,21 @@ export class Solver {
|
|
|
2036
2086
|
// materials). Between like solutions it's Goldman–Hodgkin–Katz's flux,
|
|
2037
2087
|
// N = P·zu·(c_L − c_R e^{zu})/(e^{zu} − 1), u = (φ_R − φ_L)/V_T,
|
|
2038
2088
|
// and it's exactly zero where μ̄ is level.
|
|
2039
|
-
const gl = grid.regionEnd[f], gr = gl + 1, kl = gl * n + i, kr = gr * n + i, zi = z[i],
|
|
2089
|
+
const gl = grid.regionEnd[f], gr = gl + 1, kl = gl * n + i, kr = gr * n + i, zi = z[i], link = itf.links[i];
|
|
2040
2090
|
const shift = this.mu0hat[kr] - this.mu0hat[kl] - Math.log(this.cRef[kr] / this.cRef[kl]);
|
|
2041
2091
|
const d = zi * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M])) + shift;
|
|
2042
2092
|
const E = Math.expm1(-deta); // η_R − η_L
|
|
2093
|
+
// Gated: P × Π x^p, each x taken in [0, 1] (Newton's iterates can stray past its ends).
|
|
2094
|
+
const gated = link.gates ?? [], gx = (q) => u[this._gateSlot(f, q)];
|
|
2095
|
+
const P = link.P * (gated.length ? this._open(gated, gx) : 1);
|
|
2043
2096
|
const gBc = P * bernoulli(d) * this.c[kl];
|
|
2044
2097
|
const dNdd = -P * bernoulliDerivative(d) * this.c[kl] * E;
|
|
2045
2098
|
res[R[o]] = u[o] + gBc * E;
|
|
2046
2099
|
this._j(bf, r, bf, r, 1);
|
|
2100
|
+
for (const [q, p] of gated) {
|
|
2101
|
+
const dO = this._dOpen(gated, q, p, gx);
|
|
2102
|
+
if (dO !== 0) this._j(bf, r, bf, 1 + n + itf.reactions.length + q, link.P * bernoulli(d) * this.c[kl] * E * dO);
|
|
2103
|
+
}
|
|
2047
2104
|
this._j(bf, r, bL, r, -gBc);
|
|
2048
2105
|
this._j(bf, r, bR, r, gBc * (E + 1));
|
|
2049
2106
|
if (zi !== 0) {
|
|
@@ -2051,12 +2108,18 @@ export class Solver {
|
|
|
2051
2108
|
this._j(bf, r, bR, 0, -zi * dNdd);
|
|
2052
2109
|
}
|
|
2053
2110
|
} else {
|
|
2054
|
-
// conductance: J = G (V_L − V_R), V = V_T η / z ⇒ N = G V_T (η_L − η_R) / (z² F)
|
|
2055
|
-
|
|
2111
|
+
// conductance: J = G (V_L − V_R), V = V_T η / z ⇒ N = G V_T (η_L − η_R) / (z² F); gated,
|
|
2112
|
+
// G times Π x^p
|
|
2113
|
+
const gated = itf.links[i].gates ?? [], gx = (q) => u[this._gateSlot(f, q)];
|
|
2114
|
+
const gG0 = (itf.links[i].G * VT) / (z[i] * z[i] * F), gG = gG0 * (gated.length ? this._open(gated, gx) : 1);
|
|
2056
2115
|
res[R[o]] = u[o] - gG * deta;
|
|
2057
2116
|
this._j(bf, r, bf, r, 1);
|
|
2058
2117
|
this._j(bf, r, bL, r, -gG);
|
|
2059
2118
|
this._j(bf, r, bR, r, gG);
|
|
2119
|
+
for (const [q, p] of gated) {
|
|
2120
|
+
const dO = this._dOpen(gated, q, p, gx);
|
|
2121
|
+
if (dO !== 0) this._j(bf, r, bf, 1 + n + itf.reactions.length + q, -gG0 * deta * dO);
|
|
2122
|
+
}
|
|
2060
2123
|
}
|
|
2061
2124
|
res[R[bL * M + r]] += Af * u[o];
|
|
2062
2125
|
this._j(bL, r, bf, r, Af);
|
|
@@ -2064,6 +2127,46 @@ export class Solver {
|
|
|
2064
2127
|
this._j(bR, r, bf, r, -Af);
|
|
2065
2128
|
}
|
|
2066
2129
|
itf.reactions.forEach((rx, k) => this._faceReaction(rx, k, f, bf, bL, bR, Af));
|
|
2130
|
+
if (itf.gates.length > 0) this._faceGates(f, bf, bL, bR);
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// Full index of face f's gate q (its fraction open).
|
|
2134
|
+
_gateSlot(f, q) {
|
|
2135
|
+
return this.blockOfFace[f] * this.M + 1 + this.n + this.model.interfaces[f].reactions.length + q;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// The voltage a gate follows: across its face, or across its port's capacitance at its node.
|
|
2139
|
+
_gateVoltage(gt) {
|
|
2140
|
+
return gt.port === undefined ? this._faceVoltage(gt.f) : this._portVoltage(gt.port, gt.g);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// The voltage across port k's capacitance at node g, φ − (V − zeroCharge) (V): a membrane's,
|
|
2144
|
+
// inside (the region) minus outside.
|
|
2145
|
+
_portVoltage(k, g) {
|
|
2146
|
+
const { M, u, uLo, VT } = this, b = this.blockOfNode[g], port = this.model.ports[k];
|
|
2147
|
+
return VT * (u[b * M] + uLo[b * M]) - (this.termV[2 + k] - port.capacitance.zeroCharge);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// The voltage across face f, φ_right − φ_left (V).
|
|
2151
|
+
_faceVoltage(f) {
|
|
2152
|
+
const { M, u, uLo, VT } = this, bf = this.blockOfFace[f], bL = bf - 1, bR = bf + 1;
|
|
2153
|
+
return VT * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M]));
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// Face f's gates, each a row (x − x_old)/dt = α(1 − x) − βx in the voltage across the face.
|
|
2157
|
+
_faceGates(f, bf, bL, bR) {
|
|
2158
|
+
const { res, VT } = this, R = this.rix, itf = this.model.interfaces[f], dt = this.dtNow;
|
|
2159
|
+
const V = this._faceVoltage(f), k0 = 1 + this.n + itf.reactions.length;
|
|
2160
|
+
itf.gates.forEach((gate, q) => {
|
|
2161
|
+
const o = this._gateSlot(f, q), x = this.u[o];
|
|
2162
|
+
const [a, da] = gateRate(gate.alpha, V), [b, db] = gateRate(gate.beta, V);
|
|
2163
|
+
const store = Number.isFinite(dt) ? 1 / dt : 0, j = this.gateIndex[f] + q;
|
|
2164
|
+
res[R[o]] = store * (x - this.gateOld[j]) - (a * (1 - x) - b * x);
|
|
2165
|
+
this._j(bf, k0 + q, bf, k0 + q, store + a + b);
|
|
2166
|
+
const dV = -(da * (1 - x) - db * x) * VT; // per unit φ̂_R, and minus that per unit φ̂_L
|
|
2167
|
+
this._j(bf, k0 + q, bR, 0, dV);
|
|
2168
|
+
this._j(bf, k0 + q, bL, 0, -dV);
|
|
2169
|
+
});
|
|
2067
2170
|
}
|
|
2068
2171
|
|
|
2069
2172
|
// Scharfetter–Gummel with non-ideal statistics. The excess ex = ζ − ln(c/c_ref) acts as an
|
|
@@ -2345,16 +2448,26 @@ export class Solver {
|
|
|
2345
2448
|
}
|
|
2346
2449
|
}
|
|
2347
2450
|
|
|
2451
|
+
// The terminals whose ∂res/∂V (termB) and ∂I/∂x (termC) are kept, exactly: the floating ones,
|
|
2452
|
+
// which the Newton solve borders on, or all of them (allTerminals: for the impedance, which reads
|
|
2453
|
+
// its terminal's). A held terminal's are otherwise left as they fall (written but not kept
|
|
2454
|
+
// consistent), which spares a device with dozens of ports (an axon's nodes of Ranvier) the cost
|
|
2455
|
+
// of carrying each one's two dense vectors through every assembly.
|
|
2456
|
+
_liveTerms() {
|
|
2457
|
+
if (this.allTerminals) return this._allTerms ?? (this._allTerms = this.terms.map((_, k) => k));
|
|
2458
|
+
return this.floating;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2348
2461
|
// Every terminal's terms: its voltage held or floating, how the residual depends on it
|
|
2349
2462
|
// (termB), its current into the device (termI) and how that depends on the state (termC).
|
|
2350
2463
|
_terminals(dt) {
|
|
2351
2464
|
this._refreshSources();
|
|
2352
|
-
for (
|
|
2465
|
+
for (const k of this._liveTerms()) {
|
|
2353
2466
|
this.termB[k].fill(0);
|
|
2354
2467
|
this.termC[k].fill(0);
|
|
2355
|
-
this.termDI[k] = 0;
|
|
2356
|
-
this.termI[k] = 0;
|
|
2357
2468
|
}
|
|
2469
|
+
this.termDI.fill(0);
|
|
2470
|
+
this.termI.fill(0);
|
|
2358
2471
|
this.qg.fill(0);
|
|
2359
2472
|
this.qgSlope.fill(0);
|
|
2360
2473
|
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt, false));
|
|
@@ -2412,13 +2525,17 @@ export class Solver {
|
|
|
2412
2525
|
flux[i] += res[R[o]];
|
|
2413
2526
|
this._captureRow(b, r, zF, C);
|
|
2414
2527
|
this._replaceRow(b, r);
|
|
2528
|
+
for (const Bk of this.termB) Bk[R[o]] = 0; // (other ports' terms in the row replaced)
|
|
2415
2529
|
this._j(b, r, b, r, 1);
|
|
2416
2530
|
res[R[o]] = -deta;
|
|
2417
2531
|
B[R[o]] += -z[i] / VT;
|
|
2418
2532
|
continue;
|
|
2419
2533
|
}
|
|
2420
|
-
// conductance: s = G V_T (η_out − η)/(z² F); exchange: s = k (η_out − η)
|
|
2421
|
-
|
|
2534
|
+
// conductance: s = G V_T (η_out − η)/(z² F); exchange: s = k (η_out − η). Gated, G times
|
|
2535
|
+
// Π x^p, the gates at this node (each taken in [0, 1]).
|
|
2536
|
+
const kk0 = link.type === 'conductance' ? (link.G * VT) / (z[i] * z[i] * F) : link.k;
|
|
2537
|
+
const gated = link.gates ?? [], q0 = 1 + n + port.surface.length;
|
|
2538
|
+
const open = gated.length ? this._open(gated, (q) => u[b * M + q0 + q]) : 1, kk = kk0 * open;
|
|
2422
2539
|
res[R[o]] -= v * kk * deta;
|
|
2423
2540
|
this._j(b, r, b, r, v * kk);
|
|
2424
2541
|
flux[i] += v * kk * deta;
|
|
@@ -2427,6 +2544,12 @@ export class Solver {
|
|
|
2427
2544
|
C[R[o]] += -zF * v * kk;
|
|
2428
2545
|
this.termDI[k] += (zF * v * kk * z[i]) / VT;
|
|
2429
2546
|
}
|
|
2547
|
+
for (const [q, p] of gated) {
|
|
2548
|
+
const d = this._dOpen(gated, q, p, (qq) => u[b * M + q0 + qq]);
|
|
2549
|
+
if (d === 0) continue;
|
|
2550
|
+
this._j(b, r, b, q0 + q, -v * kk0 * deta * d);
|
|
2551
|
+
if (z[i] !== 0) C[R[b * M + q0 + q]] += zF * v * kk0 * deta * d;
|
|
2552
|
+
}
|
|
2430
2553
|
}
|
|
2431
2554
|
}
|
|
2432
2555
|
if (holds) {
|
|
@@ -2435,6 +2558,7 @@ export class Solver {
|
|
|
2435
2558
|
}
|
|
2436
2559
|
port.reactions.forEach((rx, x) => this._portReaction(port, rx, this.portArea[k - 2], this.portRates[k - 2][x], flux, k));
|
|
2437
2560
|
if (port.capacitance) this._portCapacitance(port, k, dt);
|
|
2561
|
+
if (port.gates.length > 0) this._portGates(port, k, dt);
|
|
2438
2562
|
if (port.surface.length > 0 && Number.isFinite(dt)) this._surfaceStorage(port, dt);
|
|
2439
2563
|
}
|
|
2440
2564
|
|
|
@@ -2503,6 +2627,38 @@ export class Solver {
|
|
|
2503
2627
|
// on the port's side, so each node's charge balance (its φ row) gains vol·a·σ, and the port
|
|
2504
2628
|
// passes the charging current d(Σ vol·a·σ)/dt (none in a steady state). An end node whose φ its
|
|
2505
2629
|
// contact sets is left to the contact.
|
|
2630
|
+
// A membrane port's gates, at each node of its window: (x − x_old)/dt = α(1 − x) − βx in the
|
|
2631
|
+
// voltage across its capacitance there, φ − (V − zeroCharge).
|
|
2632
|
+
_portGates(port, k, dt) {
|
|
2633
|
+
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;
|
|
2634
|
+
port.nodes.forEach((g, w) => {
|
|
2635
|
+
const b = this.blockOfNode[g], V = this._portVoltage(k - 2, g);
|
|
2636
|
+
port.gates.forEach((gate, q) => {
|
|
2637
|
+
const o = b * this.M + q0 + q, x = this.u[o], j = this.portGateIndex[k - 2] + w * port.gates.length + q;
|
|
2638
|
+
const [a, da] = gateRate(gate.alpha, V), [bb, db] = gateRate(gate.beta, V);
|
|
2639
|
+
res[R[o]] = store * (x - this.gateOld[j]) - (a * (1 - x) - bb * x);
|
|
2640
|
+
this._j(b, q0 + q, b, q0 + q, store + a + bb);
|
|
2641
|
+
const s = -(da * (1 - x) - db * x); // ∂/∂V_m, with V_m = V_T φ̂ − V + zeroCharge
|
|
2642
|
+
this._j(b, q0 + q, b, 0, s * VT);
|
|
2643
|
+
B[R[o]] += -s;
|
|
2644
|
+
});
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// Π x^p over a link's gates (each x taken in [0, 1]), and its derivative in gate q.
|
|
2649
|
+
_open(gated, x) {
|
|
2650
|
+
let open = 1;
|
|
2651
|
+
for (const [q, p] of gated) open *= powi(Math.min(1, Math.max(0, x(q))), p);
|
|
2652
|
+
return open;
|
|
2653
|
+
}
|
|
2654
|
+
_dOpen(gated, q, p, x) {
|
|
2655
|
+
const xq = x(q);
|
|
2656
|
+
if (!(xq > 0 && xq < 1)) return 0;
|
|
2657
|
+
let others = 1;
|
|
2658
|
+
for (const [q2, p2] of gated) if (q2 !== q) others *= powi(Math.min(1, Math.max(0, x(q2))), p2);
|
|
2659
|
+
return p * powi(xq, p - 1) * others;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2506
2662
|
_portCapacitance(port, k, dt) {
|
|
2507
2663
|
const { M, u, uLo, res, VT } = this, R = this.rix, vol = this.model.grid.vol, area = this.portArea[k - 2];
|
|
2508
2664
|
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
@@ -2597,7 +2753,7 @@ export class Solver {
|
|
|
2597
2753
|
}
|
|
2598
2754
|
|
|
2599
2755
|
// Conductance links: J (toward the device) = G (V_out − V_i), V_out = V_t + offset; exchange
|
|
2600
|
-
// links (neutral species): N_in = k (μ_out − μ)/RT.
|
|
2756
|
+
// links (neutral species): N_in = k (μ_out − μ)/RT; velocity links, v (c_eq − c).
|
|
2601
2757
|
for (let i = 0; i < n; i++) {
|
|
2602
2758
|
const link = ct.species[i];
|
|
2603
2759
|
const o = b * M + 1 + i;
|
|
@@ -2609,6 +2765,16 @@ export class Solver {
|
|
|
2609
2765
|
res[R[o]] -= (Ac * link.G * (Vt + link.offset - Vi)) / (z[i] * F);
|
|
2610
2766
|
this._j(b, 1 + i, b, 1 + i, (Ac * link.G * VT) / (z[i] * z[i] * F));
|
|
2611
2767
|
B[R[o]] += (-Ac * link.G) / (z[i] * F);
|
|
2768
|
+
} else if (link.type === 'velocity') {
|
|
2769
|
+
// N_in = v c (e^(η_out − η) − 1): v (c_eq − c), c_eq in equilibrium with the outside.
|
|
2770
|
+
const ci = c[g * n + i], E = Math.exp(Math.min(this.contactEta(side, i) - (u[o] + uLo[o]), 700));
|
|
2771
|
+
const N = Ac * link.v * ci * (E - 1), d = this.dA;
|
|
2772
|
+
res[R[o]] -= N;
|
|
2773
|
+
d.fill(0);
|
|
2774
|
+
this._dlnc(g, i, 1, d);
|
|
2775
|
+
for (let s = 0; s <= n; s++) if (d[s] !== 0) this._j(b, 1 + i, b, s, -N * d[s]);
|
|
2776
|
+
this._j(b, 1 + i, b, 1 + i, Ac * link.v * ci * E);
|
|
2777
|
+
if (z[i] !== 0) B[R[o]] += (-Ac * link.v * ci * E * z[i]) / VT;
|
|
2612
2778
|
}
|
|
2613
2779
|
}
|
|
2614
2780
|
|
|
@@ -2738,7 +2904,7 @@ export class Solver {
|
|
|
2738
2904
|
|
|
2739
2905
|
// Scale every row by its largest Jacobian entry (in place, residual too).
|
|
2740
2906
|
_equilibrate() {
|
|
2741
|
-
const { nB, sys, res, termB } = this;
|
|
2907
|
+
const { nB, sys, res, termB } = this, live = this._liveTerms();
|
|
2742
2908
|
const { A, B, C, sizes, offA, offB, offC, offX } = sys;
|
|
2743
2909
|
for (let b = 0; b < nB; b++) {
|
|
2744
2910
|
const m = sizes[b], mp = b > 0 ? sizes[b - 1] : 0, mn = b < nB - 1 ? sizes[b + 1] : 0;
|
|
@@ -2758,7 +2924,7 @@ export class Solver {
|
|
|
2758
2924
|
for (let k = 0; k < m; k++) B[ob + k] *= s;
|
|
2759
2925
|
for (let k = 0; k < mn; k++) C[oc + k] *= s;
|
|
2760
2926
|
res[offX[b] + r] *= s;
|
|
2761
|
-
for (
|
|
2927
|
+
for (const k of live) termB[k][offX[b] + r] *= s; // (each terminal's ∂/∂V)
|
|
2762
2928
|
}
|
|
2763
2929
|
}
|
|
2764
2930
|
}
|
|
@@ -2883,6 +3049,7 @@ export class Solver {
|
|
|
2883
3049
|
const k = this.surfPort[g]; // an electrode surface's coverages, as η
|
|
2884
3050
|
if (k >= 0) for (let q = 0; q < this.model.ports[k].surface.length; q++) mx = Math.max(mx, Math.abs(delta[R[b * M + 1 + n + q]]));
|
|
2885
3051
|
}
|
|
3052
|
+
for (const gt of this.gateList) mx = Math.max(mx, Math.abs(delta[R[gt.o]])); // a gate's fraction open
|
|
2886
3053
|
return mx;
|
|
2887
3054
|
}
|
|
2888
3055
|
|
|
@@ -3104,6 +3271,7 @@ export class Solver {
|
|
|
3104
3271
|
this.termVPrev.set(this.termV);
|
|
3105
3272
|
this.computeConcentrations();
|
|
3106
3273
|
const cN = Float64Array.from(this.c), thN = Float64Array.from(this.th), th0N = Float64Array.from(this.th0);
|
|
3274
|
+
const gN = Float64Array.from(this.gateList, (gt) => this.u[gt.o] + this.uLo[gt.o]);
|
|
3107
3275
|
// Contact displacement before the step: as it was at the end of the previous step (under
|
|
3108
3276
|
// the parameters then), so a gate-voltage change shows up as displacement current.
|
|
3109
3277
|
if (!this.contactDEnd || !this.portQEnd) {
|
|
@@ -3124,6 +3292,7 @@ export class Solver {
|
|
|
3124
3292
|
for (let k = 0; k < cOld.length; k++) cOld[k] = b1 * cN[k] - b2 * prev.c[k];
|
|
3125
3293
|
for (let k = 0; k < thN.length; k++) this.thOld[k] = b1 * thN[k] - b2 * prev.th[k];
|
|
3126
3294
|
for (let k = 0; k < th0N.length; k++) this.th0Old[k] = b1 * th0N[k] - b2 * prev.th0[k];
|
|
3295
|
+
for (let k = 0; k < gN.length; k++) this.gateOld[k] = b1 * gN[k] - b2 * prev.g[k];
|
|
3127
3296
|
this.contactDStart = { left: b1 * DN.left - b2 * prev.D.left, right: b1 * DN.right - b2 * prev.D.right };
|
|
3128
3297
|
this.portQStart = QN.map((q, k) => b1 * q - b2 * prev.Q[k]);
|
|
3129
3298
|
dtEff = dt / a0;
|
|
@@ -3131,6 +3300,7 @@ export class Solver {
|
|
|
3131
3300
|
this.cOld.set(cN);
|
|
3132
3301
|
this.thOld.set(thN);
|
|
3133
3302
|
this.th0Old.set(th0N);
|
|
3303
|
+
this.gateOld.set(gN);
|
|
3134
3304
|
this.contactDStart = DN;
|
|
3135
3305
|
this.portQStart = QN;
|
|
3136
3306
|
}
|
|
@@ -3162,7 +3332,7 @@ export class Solver {
|
|
|
3162
3332
|
}
|
|
3163
3333
|
result.bdf = bdf;
|
|
3164
3334
|
if (result.converged) {
|
|
3165
|
-
this.history.unshift({ t: this.time, dt, u: Float64Array.from(this.uPrev), c: cN, th: thN, th0: th0N, D: DN, Q: QN });
|
|
3335
|
+
this.history.unshift({ t: this.time, dt, u: Float64Array.from(this.uPrev), c: cN, th: thN, th0: th0N, g: gN, D: DN, Q: QN });
|
|
3166
3336
|
if (this.history.length > 3) this.history.length = 3;
|
|
3167
3337
|
this.time = tEnd;
|
|
3168
3338
|
this.lastDt = dtEff;
|
|
@@ -3255,7 +3425,7 @@ export class Solver {
|
|
|
3255
3425
|
}
|
|
3256
3426
|
} else {
|
|
3257
3427
|
// (Order 2 once three consistent states can check it.)
|
|
3258
|
-
const r = this.step(h, { method: this.history.length >= 2 ? method : 'be', guess: this._extrapolate(guess,
|
|
3428
|
+
const r = this.step(h, { method: this.history.length >= 2 ? method : 'be', guess: this._extrapolate(guess, h) });
|
|
3259
3429
|
iterations += r.iterations;
|
|
3260
3430
|
if (!r.converged) {
|
|
3261
3431
|
rejected++;
|
|
@@ -3301,24 +3471,28 @@ export class Solver {
|
|
|
3301
3471
|
}
|
|
3302
3472
|
this.dtNext = dt;
|
|
3303
3473
|
this.landing = undefined;
|
|
3304
|
-
|
|
3474
|
+
// Stopped short, other than on the caller's wall-time budget: say where, and why.
|
|
3475
|
+
let stopped;
|
|
3476
|
+
const at = `advance stopped at t = ${this.time.toPrecision(6)} s of ${tEnd.toPrecision(6)}`;
|
|
3477
|
+
if (failed) stopped = `${at}: a step didn't converge even when shortened to ${dt.toExponential(1)} s. A drive's jump too big for one step (ramp it), a driven current the device can't keep passing (a diode's reverse recovery past its storage time: stop there), or a device the steady solves also find hard.`;
|
|
3478
|
+
else if (this.time < tEnd && steps + rejected >= maxSteps) stopped = `${at}: maxSteps (${maxSteps}) used up (${rejected} of them rejected). Call again to go on, or loosen tol.`;
|
|
3479
|
+
return { converged: !failed, done: this.time >= tEnd, steps, rejected, iterations, trace, ...(stopped ? { stopped } : {}) };
|
|
3305
3480
|
}
|
|
3306
3481
|
|
|
3307
|
-
// Quadratic (or linear) extrapolation of the state
|
|
3308
|
-
// two most recent history entries: a starting guess for Newton.
|
|
3309
|
-
|
|
3482
|
+
// Quadratic (or linear) extrapolation of the state a time h ahead, from the current state and
|
|
3483
|
+
// the two most recent history entries: a starting guess for Newton. (In offsets built from the
|
|
3484
|
+
// steps' own lengths, never differences of absolute times: a second into a run, after a jump
|
|
3485
|
+
// that a fine grid resolves in steps of 1e-13 s, those differences keep only 3 digits.)
|
|
3486
|
+
_extrapolate(out, h) {
|
|
3310
3487
|
const [e0, e1] = this.history;
|
|
3311
|
-
const
|
|
3488
|
+
const u = this.u;
|
|
3312
3489
|
if (!e0) return undefined;
|
|
3313
3490
|
if (!e1) {
|
|
3314
|
-
const l =
|
|
3491
|
+
const l = h / e0.dt;
|
|
3315
3492
|
for (let k = 0; k < out.length; k++) out[k] = u[k] + l * (u[k] - e0.u[k]);
|
|
3316
3493
|
return out;
|
|
3317
3494
|
}
|
|
3318
|
-
const
|
|
3319
|
-
const l0 = ((t - t1) * (t - t2)) / ((t0 - t1) * (t0 - t2));
|
|
3320
|
-
const l1 = ((t - t0) * (t - t2)) / ((t1 - t0) * (t1 - t2));
|
|
3321
|
-
const l2 = ((t - t0) * (t - t1)) / ((t2 - t0) * (t2 - t1));
|
|
3495
|
+
const [l0, l1, l2] = lagrange3(h, 0, -e0.dt, -(e0.dt + e1.dt));
|
|
3322
3496
|
for (let k = 0; k < out.length; k++) out[k] = l0 * u[k] + l1 * e0.u[k] + l2 * e1.u[k];
|
|
3323
3497
|
return out;
|
|
3324
3498
|
}
|
|
@@ -3327,7 +3501,7 @@ export class Solver {
|
|
|
3327
3501
|
// linear after backward Euler), and the factor turning |u − pred| into the local error.
|
|
3328
3502
|
_predict(pred, bdf) {
|
|
3329
3503
|
const [e0, e1, e2] = this.history;
|
|
3330
|
-
const
|
|
3504
|
+
const h = e0.dt;
|
|
3331
3505
|
if (!e1) {
|
|
3332
3506
|
pred.set(e0.u);
|
|
3333
3507
|
return { scale: 1, order: 1 };
|
|
@@ -3335,18 +3509,15 @@ export class Solver {
|
|
|
3335
3509
|
const hp = e1.dt;
|
|
3336
3510
|
if (bdf && e2) {
|
|
3337
3511
|
const hpp = e2.dt;
|
|
3338
|
-
// Lagrange through
|
|
3339
|
-
const
|
|
3340
|
-
const l0 = ((t - t1) * (t - t2)) / ((t0 - t1) * (t0 - t2));
|
|
3341
|
-
const l1 = ((t - t0) * (t - t2)) / ((t1 - t0) * (t1 - t2));
|
|
3342
|
-
const l2 = ((t - t0) * (t - t1)) / ((t2 - t0) * (t2 - t1));
|
|
3512
|
+
// Lagrange through the three states before at the new time (offsets as in _extrapolate).
|
|
3513
|
+
const [l0, l1, l2] = lagrange3(0, -h, -(h + hp), -(h + hp + hpp));
|
|
3343
3514
|
for (let k = 0; k < pred.length; k++) pred[k] = l0 * e0.u[k] + l1 * e1.u[k] + l2 * e2.u[k];
|
|
3344
3515
|
const w = h / hp;
|
|
3345
3516
|
const Cc = (h * h * h * (1 + w) * (1 + w)) / (w * (1 + 2 * w));
|
|
3346
3517
|
const Cp = h * (h + hp) * (h + hp + hpp);
|
|
3347
3518
|
return { scale: Cc / (Cc + Cp), order: 2 };
|
|
3348
3519
|
}
|
|
3349
|
-
const l0 = (
|
|
3520
|
+
const l0 = (h + hp) / hp, l1 = 1 - l0;
|
|
3350
3521
|
for (let k = 0; k < pred.length; k++) pred[k] = l0 * e0.u[k] + l1 * e1.u[k];
|
|
3351
3522
|
// Backward Euler's error against a linear predictor; after a BDF2 step without enough
|
|
3352
3523
|
// history this overestimates, which is safe.
|
|
@@ -3363,9 +3534,22 @@ export class Solver {
|
|
|
3363
3534
|
const fail = (m) => {
|
|
3364
3535
|
throw new DeviceError(m);
|
|
3365
3536
|
};
|
|
3366
|
-
if (!Array.isArray(probes)) fail('advance: probes must be an array of { x, species, quantity, region }');
|
|
3537
|
+
if (!Array.isArray(probes)) fail('advance: probes must be an array of { x, species, quantity, region }, or { interface, species } / { interface, gate }');
|
|
3367
3538
|
const plan = probes.map((p, k) => {
|
|
3368
3539
|
const path = `advance: probes[${k}]`;
|
|
3540
|
+
if (p?.interface !== undefined) {
|
|
3541
|
+
// At a face: a species' flux through it (mol/(m²·s), toward +x), or a gate's fraction open.
|
|
3542
|
+
const f = p.interface, itf = this.model.interfaces[f];
|
|
3543
|
+
if (!(Number.isInteger(f) && itf)) fail(`${path}.interface: no interface ${JSON.stringify(f)} (they're numbered from 0, left to right)`);
|
|
3544
|
+
if (p.gate !== undefined) {
|
|
3545
|
+
const q = itf.gates.findIndex((gt) => gt.name === p.gate);
|
|
3546
|
+
if (q < 0) fail(`${path}.gate: interfaces[${f}] has no gate ${JSON.stringify(p.gate)}${itf.gates.length ? ` (it has ${itf.gates.map((gt) => gt.name).join(', ')})` : ''}`);
|
|
3547
|
+
return { o: this._gateSlot(f, q) };
|
|
3548
|
+
}
|
|
3549
|
+
const i = species.findIndex((sp) => sp.name === p.species);
|
|
3550
|
+
if (i < 0) fail(`${path}.species: no species ${JSON.stringify(p.species)}`);
|
|
3551
|
+
return { o: this.blockOfFace[f] * this.M + 1 + i };
|
|
3552
|
+
}
|
|
3369
3553
|
const quantity = p?.quantity ?? 'c';
|
|
3370
3554
|
if (quantity !== 'c' && quantity !== 'V' && quantity !== 'phi') fail(`${path}.quantity must be 'c' (mol/m³), 'V' (the species voltage) or 'phi' (φ, V)`);
|
|
3371
3555
|
const i = quantity === 'phi' ? -1 : species.findIndex((sp) => sp.name === p?.species);
|
|
@@ -3397,7 +3581,7 @@ export class Solver {
|
|
|
3397
3581
|
};
|
|
3398
3582
|
return {
|
|
3399
3583
|
out,
|
|
3400
|
-
read: () => plan.map((q) => (1 - q.w) * at(q, q.g) + q.w * at(q, q.g + 1)),
|
|
3584
|
+
read: () => plan.map((q) => (q.o !== undefined ? (this.loc[q.o] < 0 ? 0 : this.u[q.o]) : (1 - q.w) * at(q, q.g) + q.w * at(q, q.g + 1))),
|
|
3401
3585
|
push: (values) => values.forEach((v, k) => out[k].push(v)),
|
|
3402
3586
|
};
|
|
3403
3587
|
}
|
|
@@ -3415,6 +3599,7 @@ export class Solver {
|
|
|
3415
3599
|
const k = this.surfPort[g]; // an electrode surface's coverages, as η
|
|
3416
3600
|
if (k >= 0) for (let q = 0; q < this.model.ports[k].surface.length; q++) mx = Math.max(mx, Math.abs(u[b * M + 1 + n + q] - ref[b * M + 1 + n + q]));
|
|
3417
3601
|
}
|
|
3602
|
+
for (const { o } of this.gateList) mx = Math.max(mx, Math.abs(u[o] - ref[o])); // (a fraction: 1e-3 of m³ is 0.3% of P)
|
|
3418
3603
|
return mx;
|
|
3419
3604
|
}
|
|
3420
3605
|
|
|
@@ -3458,6 +3643,7 @@ export class Solver {
|
|
|
3458
3643
|
cOld: Float64Array.from(this.cOld),
|
|
3459
3644
|
thOld: Float64Array.from(this.thOld),
|
|
3460
3645
|
th0Old: Float64Array.from(this.th0Old),
|
|
3646
|
+
gateOld: Float64Array.from(this.gateOld),
|
|
3461
3647
|
time: this.time,
|
|
3462
3648
|
lastDt: this.lastDt,
|
|
3463
3649
|
atSteady: this.atSteady,
|
|
@@ -3480,6 +3666,7 @@ export class Solver {
|
|
|
3480
3666
|
this.cOld.set(s.cOld);
|
|
3481
3667
|
this.thOld.set(s.thOld);
|
|
3482
3668
|
this.th0Old.set(s.th0Old);
|
|
3669
|
+
this.gateOld.set(s.gateOld);
|
|
3483
3670
|
this.time = s.time;
|
|
3484
3671
|
this.lastDt = s.lastDt;
|
|
3485
3672
|
this.atSteady = s.atSteady;
|
|
@@ -3515,7 +3702,17 @@ export class Solver {
|
|
|
3515
3702
|
* Z = δV/δI with I into the device (positive real part for a passive device).
|
|
3516
3703
|
* @param {ArrayLike<number>} frequencies Hz
|
|
3517
3704
|
*/
|
|
3518
|
-
impedance(frequencies,
|
|
3705
|
+
impedance(frequencies, opts) {
|
|
3706
|
+
const was = this.allTerminals;
|
|
3707
|
+
this.allTerminals = true; // (it reads its terminal's ∂res/∂V and ∂I/∂x)
|
|
3708
|
+
try {
|
|
3709
|
+
return this._impedance(frequencies, opts);
|
|
3710
|
+
} finally {
|
|
3711
|
+
this.allTerminals = was;
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
_impedance(frequencies, { profiles = false, terminal = 'right' } = {}) {
|
|
3519
3716
|
const { M, nB, sys, VT } = this;
|
|
3520
3717
|
const kT = this.terms.findIndex((t) => t.name === terminal);
|
|
3521
3718
|
if (kT < 0) throw new SolverError(`impedance: no terminal named '${terminal}' (${this.terms.map((t) => t.name).join(', ')})`);
|
|
@@ -3527,6 +3724,7 @@ export class Solver {
|
|
|
3527
3724
|
this.cOld.set(this.c);
|
|
3528
3725
|
this.thOld.set(this.th);
|
|
3529
3726
|
this.th0Old.set(this.th0);
|
|
3727
|
+
this.gateList.forEach((gt, j) => (this.gateOld[j] = this.u[gt.o]));
|
|
3530
3728
|
this.assemble(Infinity);
|
|
3531
3729
|
this.contactDStart = { ...this.contactD };
|
|
3532
3730
|
this.portQStart = Float64Array.from(this.portQ);
|
|
@@ -3540,7 +3738,15 @@ export class Solver {
|
|
|
3540
3738
|
for (const X of ['A', 'B', 'C']) S[X] = sys[X].map((v, k) => (v - J[X][k]) * dts);
|
|
3541
3739
|
const Cs = this.termC.map((v, k) => v.map((x, j) => (x - Ct[k][j]) * dts));
|
|
3542
3740
|
const DIs = this.termDI.map((x, k) => (x - DI[k]) * dts);
|
|
3543
|
-
|
|
3741
|
+
// Where the measured contact's current may be read across a segment: anywhere without
|
|
3742
|
+
// ports; with them, only between it and the nearest window (a port's exchange takes current
|
|
3743
|
+
// in or out along it). Never for a port's own terminal.
|
|
3744
|
+
let cuts = kT < 2 ? this._currentCuts() : null;
|
|
3745
|
+
if (cuts && this.model.ports.length > 0) {
|
|
3746
|
+
const windows = this.model.ports.flatMap((p) => Array.from(p.nodes));
|
|
3747
|
+
const lo = Math.min(...windows), hi = Math.max(...windows);
|
|
3748
|
+
cuts = cuts.filter((cut) => (kT === 0 ? cut.s + 1 <= lo : cut.s >= hi));
|
|
3749
|
+
}
|
|
3544
3750
|
// J·v for GMRES, exact: the assembled J holds a flux's dependence on η_L and η_R as two
|
|
3545
3751
|
// entries, and where both are huge (an inversion layer) and v nearly uniform, J·v loses the
|
|
3546
3752
|
// flux to round-off. So the dilute kernels' terms are kept in difference form (see
|
|
@@ -3709,7 +3915,7 @@ export class Solver {
|
|
|
3709
3915
|
Ii += im;
|
|
3710
3916
|
for (let j = 0; j < Nc; j++) err += (Math.abs(c[j]) + w * Math.abs(cs[j])) * (eps * Math.hypot(p.r[j], p.i[j]) + p.floor);
|
|
3711
3917
|
}
|
|
3712
|
-
if (!cuts
|
|
3918
|
+
if (!cuts) return { Ir, Ii, err };
|
|
3713
3919
|
const sgn = kT === 0 ? 1 : -1;
|
|
3714
3920
|
for (const cut of cuts) {
|
|
3715
3921
|
let re = 0, im = 0, e = 0;
|
|
@@ -3749,7 +3955,8 @@ export class Solver {
|
|
|
3749
3955
|
// taken at its edges; and what's left, small, solved for to GMRES's floor, its error now
|
|
3750
3956
|
// relative to itself. An electrolyte at its open circuit needs it: the response to its
|
|
3751
3957
|
// terminal is nearly such a shift, and its current the slope of levels uniform to 1e-14,
|
|
3752
|
-
// more than a double can hold. (
|
|
3958
|
+
// more than a double can hold. (A port's exchange sees the shift: its window's rows are
|
|
3959
|
+
// kept with the edges'.) So does a MOS capacitor's back contact, its channel held by a port.
|
|
3753
3960
|
if (K === 0 && cuts && err > 1e-2 * Math.hypot(Ir, Ii)) {
|
|
3754
3961
|
const [er, ei] = this._regionShift(yr, yi);
|
|
3755
3962
|
const tr = new Float64Array(Nc), ti = new Float64Array(Nc);
|
|
@@ -3839,8 +4046,9 @@ export class Solver {
|
|
|
3839
4046
|
return [er, ei];
|
|
3840
4047
|
}
|
|
3841
4048
|
|
|
3842
|
-
// Compact rows a region's uniform shift can touch: faces, the nodes beside them,
|
|
3843
|
-
//
|
|
4049
|
+
// Compact rows a region's uniform shift can touch: faces, the nodes beside them, the two ends
|
|
4050
|
+
// and the ports' windows (whatever holds a level from outside). Elsewhere inside a region,
|
|
4051
|
+
// every row is unchanged by it.
|
|
3844
4052
|
_regionEdgeRows() {
|
|
3845
4053
|
const { M, loc, nB } = this, R = this.rix;
|
|
3846
4054
|
const edge = new Uint8Array(this.sys.size);
|
|
@@ -3856,16 +4064,17 @@ export class Solver {
|
|
|
3856
4064
|
mark(b);
|
|
3857
4065
|
mark(b + 1);
|
|
3858
4066
|
}
|
|
4067
|
+
for (const port of this.model.ports) for (const g of port.nodes) mark(this.blockOfNode[g]);
|
|
3859
4068
|
return edge;
|
|
3860
4069
|
}
|
|
3861
4070
|
|
|
3862
|
-
// The cuts where the impedance may read a small-signal current
|
|
3863
|
-
//
|
|
3864
|
-
//
|
|
3865
|
-
//
|
|
4071
|
+
// The cuts where the impedance may read a small-signal current. Each is the linearised total
|
|
4072
|
+
// current toward +x through one segment s of an ideal region, Σ (g + iω d)·δu over the
|
|
4073
|
+
// unknowns idx: the Scharfetter–Gummel fluxes' derivatives (as in _segmentsDilute) and the
|
|
4074
|
+
// displacement's. Without ports it's the same through every cut; with them, only between a
|
|
4075
|
+
// contact and the nearest port's window (see _impedance).
|
|
3866
4076
|
_currentCuts() {
|
|
3867
4077
|
const { model, n, M, u, uLo, c, z } = this, F = FARADAY;
|
|
3868
|
-
if (model.ports.length > 0) return null;
|
|
3869
4078
|
const { grid, materials, regions } = model;
|
|
3870
4079
|
const cuts = [];
|
|
3871
4080
|
for (let r = 0; r < regions.length; r++) {
|
|
@@ -3905,7 +4114,7 @@ export class Solver {
|
|
|
3905
4114
|
}
|
|
3906
4115
|
if (terms.size === 0) continue;
|
|
3907
4116
|
const idx = [...terms.keys()];
|
|
3908
|
-
cuts.push({ idx, g: idx.map((x) => terms.get(x)[0]), d: idx.map((x) => terms.get(x)[1]) });
|
|
4117
|
+
cuts.push({ s, idx, g: idx.map((x) => terms.get(x)[0]), d: idx.map((x) => terms.get(x)[1]) });
|
|
3909
4118
|
}
|
|
3910
4119
|
}
|
|
3911
4120
|
return cuts;
|
|
@@ -3955,6 +4164,7 @@ export class Solver {
|
|
|
3955
4164
|
// Jacobian are left partial (the next assemble() starts afresh). Concentrations must be current.
|
|
3956
4165
|
_assembleBookkeeping(dt) {
|
|
3957
4166
|
const { model } = this;
|
|
4167
|
+
this.dtNow = dt;
|
|
3958
4168
|
const { grid, materials, regions } = model;
|
|
3959
4169
|
this.res.fill(0);
|
|
3960
4170
|
for (let r = 0; r < regions.length; r++) {
|
|
@@ -4083,7 +4293,7 @@ export class Solver {
|
|
|
4083
4293
|
// pseudo-transient ramp: one direct attempt first.
|
|
4084
4294
|
const quick = (canContinue || this.hasGeneration) && direct && opts.continuation !== false;
|
|
4085
4295
|
let r = this._solveSteady(quick ? { ...opts, maxSteps: 1 } : opts);
|
|
4086
|
-
if (r.converged) this.solvedV = target;
|
|
4296
|
+
if (r.converged) this.solvedV = [level, target];
|
|
4087
4297
|
if (r.converged) return r;
|
|
4088
4298
|
// Generation (e.g. light) holding the device far from equilibrium: ramp it up from nearly
|
|
4089
4299
|
// nothing, each solve warm from the last.
|
|
@@ -4097,7 +4307,7 @@ export class Solver {
|
|
|
4097
4307
|
const g = this._generationContinuation(opts, r);
|
|
4098
4308
|
dimmer = this.dimmer;
|
|
4099
4309
|
if (g.converged) {
|
|
4100
|
-
this.solvedV = target;
|
|
4310
|
+
this.solvedV = [level, target];
|
|
4101
4311
|
return g;
|
|
4102
4312
|
}
|
|
4103
4313
|
r = g;
|
|
@@ -4138,17 +4348,23 @@ export class Solver {
|
|
|
4138
4348
|
this.computeConcentrations();
|
|
4139
4349
|
};
|
|
4140
4350
|
restart();
|
|
4141
|
-
// Ramp from the last converged
|
|
4142
|
-
//
|
|
4143
|
-
|
|
4144
|
-
const
|
|
4351
|
+
// Ramp from the contacts' voltages at the last converged solve when the state is that
|
|
4352
|
+
// solution (a warm start): the left one first where it moved, the right held where it was,
|
|
4353
|
+
// then the right. Otherwise from level terminals.
|
|
4354
|
+
const solved = this.solvedV;
|
|
4355
|
+
let c;
|
|
4356
|
+
if (solved) {
|
|
4357
|
+
c = solved[0] === level ? { ...r, converged: true, ramped: false } : this._continuation(opts, 0, solved[0], level, r, true, solved[1]);
|
|
4358
|
+
// (Ramping the right from level terminals, solve there first: a sweep's start.)
|
|
4359
|
+
if (c.converged) c = this._continuation(opts, 1, solved[1], target, c, c.ramped || solved[1] !== level);
|
|
4360
|
+
} else c = this._continuation(opts, 1, level, target, r, false);
|
|
4145
4361
|
if (c.converged) {
|
|
4146
|
-
this.solvedV = target;
|
|
4362
|
+
this.solvedV = [level, target];
|
|
4147
4363
|
return c;
|
|
4148
4364
|
}
|
|
4149
4365
|
restart();
|
|
4150
4366
|
r = this._solveSteady(opts); // the full pseudo-transient ramp
|
|
4151
|
-
if (r.converged) this.solvedV = target;
|
|
4367
|
+
if (r.converged) this.solvedV = [level, target];
|
|
4152
4368
|
return { ...r, steps: r.steps + c.steps, iterations: r.iterations + c.iterations };
|
|
4153
4369
|
}
|
|
4154
4370
|
|
|
@@ -4229,13 +4445,15 @@ export class Solver {
|
|
|
4229
4445
|
// Floated, the system can lose what held it doesn't (a closed redox cell driven near its
|
|
4230
4446
|
// limit: its bordered row 33 digits short), but a current-driven steady state is the held
|
|
4231
4447
|
// one at the voltage that passes the target: found by held solves, by regula falsi
|
|
4232
|
-
// (Illinois) within the bracket, to 1e-10 of the target
|
|
4448
|
+
// (Illinois) within the bracket, to 1e-10 of the target, or at open circuit of the largest
|
|
4449
|
+
// current the march saw (a target of 0 is never met exactly).
|
|
4450
|
+
const scale = Math.max(Math.abs(target), ...seen.map(([, I]) => Math.abs(I)));
|
|
4233
4451
|
try {
|
|
4234
4452
|
let side = 0;
|
|
4235
4453
|
for (let it = 0; it < 60; it++) {
|
|
4236
4454
|
const Vm = Va - (fa * (Vb - Va)) / (fb - fa), fm = hold(Vm);
|
|
4237
4455
|
if (!Number.isFinite(fm)) return fail();
|
|
4238
|
-
if (Math.abs(fm) <= 1e-10 *
|
|
4456
|
+
if (Math.abs(fm) <= 1e-10 * scale) return { converged: true, steps, iterations, history, residual: Math.abs(fm) };
|
|
4239
4457
|
if (Math.sign(fm) === Math.sign(fb)) {
|
|
4240
4458
|
[Vb, fb] = [Vm, fm];
|
|
4241
4459
|
if (side === -1) fa /= 2;
|
|
@@ -4293,12 +4511,18 @@ export class Solver {
|
|
|
4293
4511
|
}
|
|
4294
4512
|
}
|
|
4295
4513
|
|
|
4296
|
-
|
|
4514
|
+
// Terminal k (a contact) ramped from `level` to `target` through steady solves, each warm from
|
|
4515
|
+
// the last (`hold`: the other contact's voltage meanwhile, if not its own). A warm ramp that has
|
|
4516
|
+
// nowhere to go converges only if what it started from did (`ramped`): the state it carries
|
|
4517
|
+
// must be some ramp's solution, never just the one a failed solve left.
|
|
4518
|
+
_continuation(opts, k, level, target, r, warm, hold) {
|
|
4297
4519
|
const sub = this._directSteady() ? { ...opts, maxSteps: 1 } : opts;
|
|
4298
4520
|
let V = level, dV = (target - level) / 8, steps = r.steps, iterations = r.iterations;
|
|
4299
4521
|
const history = r.history.slice();
|
|
4522
|
+
if (warm && V === target) return { ...r, converged: r.ramped === true, steps, iterations, history, ramped: r.ramped };
|
|
4300
4523
|
try {
|
|
4301
|
-
this.sourceOverride.set(
|
|
4524
|
+
this.sourceOverride.set(k, V);
|
|
4525
|
+
if (hold !== undefined) this.sourceOverride.set(1 - k, hold);
|
|
4302
4526
|
let q;
|
|
4303
4527
|
if (!warm) {
|
|
4304
4528
|
q = this._solveSteady(opts);
|
|
@@ -4320,7 +4544,7 @@ export class Solver {
|
|
|
4320
4544
|
while (V !== target) {
|
|
4321
4545
|
const next = Math.abs(target - V) <= Math.abs(dV) ? target : V + dV;
|
|
4322
4546
|
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
4323
|
-
this.sourceOverride.set(
|
|
4547
|
+
this.sourceOverride.set(k, next);
|
|
4324
4548
|
q = this._solveSteady(sub);
|
|
4325
4549
|
steps += q.steps;
|
|
4326
4550
|
iterations += q.iterations;
|
|
@@ -4337,9 +4561,10 @@ export class Solver {
|
|
|
4337
4561
|
if (Math.abs(dV) < 1e-6 * Math.abs(target - level)) return { converged: false, steps, iterations, history };
|
|
4338
4562
|
}
|
|
4339
4563
|
}
|
|
4340
|
-
return { converged: true, steps, iterations, history };
|
|
4564
|
+
return { converged: true, steps, iterations, history, ramped: true };
|
|
4341
4565
|
} finally {
|
|
4342
|
-
this.sourceOverride.delete(
|
|
4566
|
+
this.sourceOverride.delete(k);
|
|
4567
|
+
if (hold !== undefined) this.sourceOverride.delete(1 - k);
|
|
4343
4568
|
this._refreshSources();
|
|
4344
4569
|
}
|
|
4345
4570
|
}
|