driftlet 1.3.0 → 1.3.3
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/device.md +34 -9
- package/docs/kit.md +7 -3
- package/docs/numerics.md +21 -4
- package/docs/reliability.md +18 -13
- package/docs/statistics.md +18 -1
- package/llms.txt +13 -9
- package/package.json +1 -1
- package/src/checks.js +9 -1
- package/src/describe.js +2 -0
- package/src/device.js +16 -0
- package/src/impedance.js +146 -44
- package/src/index.js +1 -0
- package/src/kit.js +6 -2
- package/src/newton.js +23 -5
- package/src/solution.js +12 -1
- package/src/solver.js +12 -3
- package/src/start.js +129 -68
- package/src/steady.js +92 -9
- package/src/timeSteps.js +13 -1
- package/types/impedance.d.ts +2 -2
- package/types/solver.d.ts +2 -0
- package/types/start.d.ts +8 -0
- package/types/steady.d.ts +4 -1
package/src/start.js
CHANGED
|
@@ -27,18 +27,24 @@
|
|
|
27
27
|
// plus each face's dipole. In a region where a level-fixed charged species responds to φ̂,
|
|
28
28
|
// φ̂ is chosen for neutrality (a bisection; node by node where a profile varies);
|
|
29
29
|
// otherwise it's the running φ̂, or where a conductor on the left or an electrode spread
|
|
30
|
-
// through the region has a reaction that can balance, the φ̂ that balances it
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
30
|
+
// through the region has a reaction that can balance, the φ̂ that balances it (by bisection;
|
|
31
|
+
// a trial a non-ideal model refuses counts its ideal charge, which has the sign there). Where
|
|
32
|
+
// neutrality is out of reach (c0's amounts leave a charge no held level balances): in a
|
|
33
|
+
// strictly neutral region the running φ̂, the region noted in s.unbalanced (for a transient
|
|
34
|
+
// that fails from it to say why); otherwise the φ̂ nearest the running one, toward less
|
|
35
|
+
// charge, where the charge is at most ε V_T/(F L²) (L the region's length: what it holds with
|
|
36
|
+
// a thermal voltage across it), or where it stops falling (a fixed charge). A strictly
|
|
34
37
|
// neutral region (ε = 0) with every charged species' amount given must have a neutral c0:
|
|
35
38
|
// an error otherwise. A non-ideal material's composition comes from its statistics
|
|
36
39
|
// (materialAt).
|
|
37
|
-
// 2.
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
40
|
+
// 2. Marks the state cold (s.coldStart). equilibrateFaces(s), which the first time step from it
|
|
41
|
+
// calls (src/timeSteps.js) and a steady solve doesn't: a face between two strictly neutral
|
|
42
|
+
// ideal regions, its φ type neutral, whose links hold some charged species in equilibrium,
|
|
43
|
+
// its two nodes set to that equilibrium, its t = 0⁺ (each passing species' μ̄ equal across
|
|
44
|
+
// it, the left node neutral, each passing species' amount over the two nodes kept; the jump
|
|
45
|
+
// in φ̂ between them the one unknown), everything else at them unchanged in concentration;
|
|
46
|
+
// s.coldStart false. A solve that converges, a warm start and a carried state set it as
|
|
47
|
+
// their state is.
|
|
42
48
|
// 3. An insertion host (a φ-free material, not a conductor): its φ̂ and every η shifted by z·Δ so that its first
|
|
43
49
|
// charged species shared with a neighbour across a face starts at that neighbour's level.
|
|
44
50
|
// 4. A floating contact facing a held one: V = the held V + the difference between the two
|
|
@@ -55,17 +61,18 @@
|
|
|
55
61
|
// pass the current it's driven at, or behind a resistance (V_src − V)/R, by bisection.
|
|
56
62
|
//
|
|
57
63
|
// Invariants (each says what a reviewer should be able to check here):
|
|
58
|
-
// 1. A species given by its amount starts at c0 (or its profile) at every node
|
|
59
|
-
//
|
|
64
|
+
// 1. A species given by its amount starts at c0 (or its profile) at every node; at a face
|
|
65
|
+
// equilibrated by step 2 (a transient's first step), its amount over the two nodes is kept.
|
|
60
66
|
// 2. A species given by its level starts at the level of what feeds it, as step 1 orders them.
|
|
61
67
|
// 3. Where a level-fixed charged species responds, each node starts neutral (Σ z c + the fixed
|
|
62
|
-
// charge = 0) to the bisection's tolerance, unless neutrality is out of reach (step 1
|
|
68
|
+
// charge = 0) to the bisection's tolerance, unless neutrality is out of reach (step 1: then
|
|
69
|
+
// its charge at most ε V_T/(F L²), or the least φ̂ gives, to the bisection's tolerance); a
|
|
63
70
|
// strictly neutral region given by amounts is neutral by its c0, checked.
|
|
64
71
|
// 4. Steps 2 and 3 change no concentration but as they say: step 3 is a gauge (φ̂ and each η by
|
|
65
72
|
// z·Δ), step 2 moves the passing species between a face's two nodes only.
|
|
66
73
|
// 5. Each floating terminal's V is set by the last of steps 4, 6, 7, 9 that applies to it;
|
|
67
74
|
// held terminals' V are untouched.
|
|
68
|
-
// 6. Only u, uLo, termV, s.unbalanced and what computeConcentrations derives are written; the
|
|
75
|
+
// 6. Only u, uLo, termV, s.unbalanced, s.coldStart and what computeConcentrations derives are written; the
|
|
69
76
|
// result depends on the model, every terminal's voltage and s.time, deterministically.
|
|
70
77
|
// Known, and not an invariant (a cold review; see ROADMAP): the gauge (φ̂ and each η by z·Δ)
|
|
71
78
|
// runs from the left, so behind a closed or floating left end the start sits away from a right
|
|
@@ -77,7 +84,7 @@
|
|
|
77
84
|
// Tests: every test (each device starts here); test/transient.test.js on what a start must be.
|
|
78
85
|
|
|
79
86
|
import { gateRate } from './boundaries.js';
|
|
80
|
-
import { FARADAY } from './constants.js';
|
|
87
|
+
import { EPS0, FARADAY } from './constants.js';
|
|
81
88
|
import { sourceAt } from './device.js';
|
|
82
89
|
import { profileAt, SolverError } from './solver.js';
|
|
83
90
|
|
|
@@ -163,10 +170,22 @@ export function initFromComposition(s) {
|
|
|
163
170
|
const zeta = new Float64Array(n), cc = new Float64Array(n);
|
|
164
171
|
const charge = (ph) => {
|
|
165
172
|
if (!mat.ideal) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
// A model can refuse a trial φ̂ far out, where one ion is many orders denser than the rest
|
|
174
|
+
// (Debye–Hückel's limiting law threw out of the start, for KCl at 10 mM, its neutral φ̂
|
|
175
|
+
// 57 units from where the search began): there the ideal charge has the sign, and the
|
|
176
|
+
// search goes on by it.
|
|
177
|
+
let refused = false;
|
|
178
|
+
try {
|
|
179
|
+
materialAt(s, mat, reg.background, eta, mode, cFix, ph, zeta, cc);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (!(err instanceof SolverError)) throw err;
|
|
182
|
+
refused = true;
|
|
183
|
+
}
|
|
184
|
+
if (!refused) {
|
|
185
|
+
let q = reg.fixedCharge / FARADAY;
|
|
186
|
+
for (let i = 0; i < n; i++) if (mat.present[i]) q += z[i] * cc[i];
|
|
187
|
+
return q;
|
|
188
|
+
}
|
|
170
189
|
}
|
|
171
190
|
let q = reg.fixedCharge / FARADAY;
|
|
172
191
|
for (let i = 0; i < n; i++) {
|
|
@@ -244,13 +263,39 @@ export function initFromComposition(s) {
|
|
|
244
263
|
while (charge(lo) < 0 && lo > -1e4) lo -= 2 * (hi - lo);
|
|
245
264
|
while (charge(hi) > 0 && hi < 1e4) hi += 2 * (hi - lo);
|
|
246
265
|
// Out of reach (every responding carrier has one sign and nothing balances it, as in an
|
|
247
|
-
// undoped layer that holds only holes):
|
|
266
|
+
// undoped layer that holds only holes): not run to ±∞, but where the charge is no more than
|
|
267
|
+
// the region holds with a thermal voltage across it, ε V_T/(F L²), or as little as it gets
|
|
268
|
+
// (a fixed charge left). Kept at the running φ̂ (a contact's level), a hole-only layer 100
|
|
269
|
+
// nm thick started full of the anode's 1e27 m⁻³ holes, 7 kV of space charge: with traps in
|
|
270
|
+
// it, every step failed from there.
|
|
248
271
|
if (charge(lo) < 0 || charge(hi) > 0) {
|
|
249
272
|
// In a strictly neutral region that's a start that can't be: the amounts c0 gives leave
|
|
250
273
|
// a charge no level-held species can balance. Noted, for a transient that fails from it
|
|
251
274
|
// to say so (a steady solve, its levels from the contacts, can still find the state).
|
|
252
|
-
if (mat.epsr === 0)
|
|
253
|
-
|
|
275
|
+
if (mat.epsr === 0) {
|
|
276
|
+
s.unbalanced.push(r);
|
|
277
|
+
return ph;
|
|
278
|
+
}
|
|
279
|
+
const L = grid.x[grid.regionEnd[r]] - grid.x[grid.regionStart[r]];
|
|
280
|
+
const most = (mat.epsr * EPS0 * model.RT) / (FARADAY * FARADAY * L * L);
|
|
281
|
+
const q0 = charge(ph), dir = q0 > 0 ? 1 : -1; // (the charge falls as φ̂ rises)
|
|
282
|
+
if (!(Math.abs(q0) > most)) return ph;
|
|
283
|
+
let a = ph, qa = Math.abs(q0);
|
|
284
|
+
for (let step = 1; step < 1e4; step *= 2) {
|
|
285
|
+
let b = a + dir * step;
|
|
286
|
+
const qb = Math.abs(charge(b));
|
|
287
|
+
if (qb <= most) {
|
|
288
|
+
for (let it = 0; it < 100 && Math.abs(b - a) > 1e-9; it++) {
|
|
289
|
+
const m = 0.5 * (a + b);
|
|
290
|
+
if (Math.abs(charge(m)) > most) a = m;
|
|
291
|
+
else b = m;
|
|
292
|
+
}
|
|
293
|
+
return b;
|
|
294
|
+
}
|
|
295
|
+
if (!(qb < qa * (1 - 1e-9))) return a; // (a fixed charge: as little as it gets)
|
|
296
|
+
[a, qa] = [b, qb];
|
|
297
|
+
}
|
|
298
|
+
return a;
|
|
254
299
|
}
|
|
255
300
|
for (let it = 0; it < 200 && hi - lo > 1e-12; it++) {
|
|
256
301
|
const m = 0.5 * (lo + hi);
|
|
@@ -279,53 +324,6 @@ export function initFromComposition(s) {
|
|
|
279
324
|
}
|
|
280
325
|
}
|
|
281
326
|
}
|
|
282
|
-
// Two strictly neutral regions meeting at a face whose links hold ions in equilibrium: where
|
|
283
|
-
// c0 leaves the two sides out of it (a different fixed charge or immobile ion on each, so
|
|
284
|
-
// c₊c₋ differs across it), the face reaches it at t = 0⁺, before anything diffuses, and a
|
|
285
|
-
// first step made to take that jump met an exactly singular pivot. Start there: the ions the
|
|
286
|
-
// face passes shared between its two nodes in equilibrium (μ̄ equal across it), each one's
|
|
287
|
-
// amount over the two nodes kept, everything else as c0 has it. The one unknown is the jump
|
|
288
|
-
// in φ̂ across it (a Donnan potential), set by neutrality on one side; the other's follows,
|
|
289
|
-
// the two nodes' charge summing to what it was, zero.
|
|
290
|
-
for (let f = 0; f < interfaces.length; f++) {
|
|
291
|
-
const itf = interfaces[f], mL = materials[regions[f].material], mR = materials[regions[f + 1].material];
|
|
292
|
-
const strict = (m) => m.epsr === 0 && !m.conductor && !m.phiFree && m.ideal;
|
|
293
|
-
if (itf.phi.type !== 'neutral' || !strict(mL) || !strict(mR)) continue;
|
|
294
|
-
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1], bL = s.blockOfNode[gL] * M, bR = s.blockOfNode[gR] * M;
|
|
295
|
-
const pass = [];
|
|
296
|
-
for (let i = 0; i < n; i++) if (itf.links[i].type === 'equilibrium' && mL.present[i] && mR.present[i]) pass.push(i);
|
|
297
|
-
if (!pass.some((i) => z[i] !== 0)) continue;
|
|
298
|
-
const RT = model.RT, vL = grid.vol[gL], vR = grid.vol[gR];
|
|
299
|
-
const conc = (m, b, i) => m.cRef[i] * Math.exp(u[b + 1 + i] - m.mu0[i] / RT - z[i] * u[b]);
|
|
300
|
-
const amount = pass.map((i) => vL * conc(mL, bL, i) + vR * conc(mR, bR, i));
|
|
301
|
-
let qOther = regions[f].fixedCharge / FARADAY; // (the left node's charge from all it doesn't share)
|
|
302
|
-
for (let i = 0; i < n; i++) if (mL.present[i] && !pass.includes(i) && z[i] !== 0) qOther += z[i] * conc(mL, bL, i);
|
|
303
|
-
// At a jump D = φ̂_R − φ̂_L: c_L/c_R = (cRef_L/cRef_R)·exp((μ°_R − μ°_L)/RT + z D), the amount fixing both.
|
|
304
|
-
const split = (D) =>
|
|
305
|
-
pass.map((i, j) => {
|
|
306
|
-
const K = (mL.cRef[i] / mR.cRef[i]) * Math.exp((mR.mu0[i] - mL.mu0[i]) / RT + z[i] * D), cR = amount[j] / (vL * K + vR);
|
|
307
|
-
return [K * cR, cR];
|
|
308
|
-
});
|
|
309
|
-
const qL = (D) => split(D).reduce((q, [cL], j) => q + z[pass[j]] * cL, qOther); // (increasing in D)
|
|
310
|
-
const D0 = u[bR] - u[bL];
|
|
311
|
-
let lo = D0 - 1, hi = D0 + 1;
|
|
312
|
-
while (qL(lo) > 0 && lo > D0 - 1e3) lo -= 2 * (hi - lo);
|
|
313
|
-
while (qL(hi) < 0 && hi < D0 + 1e3) hi += 2 * (hi - lo);
|
|
314
|
-
if (!(qL(lo) <= 0 && qL(hi) >= 0)) continue; // (nothing the face shares can balance it: leave c0)
|
|
315
|
-
for (let it = 0; it < 200 && hi - lo > 1e-13 * Math.max(1, Math.abs(D0)); it++) {
|
|
316
|
-
const m = 0.5 * (lo + hi);
|
|
317
|
-
if (qL(m) < 0) lo = m;
|
|
318
|
-
else hi = m;
|
|
319
|
-
}
|
|
320
|
-
const D = 0.5 * (lo + hi), cs = split(D);
|
|
321
|
-
const keep = Float64Array.from({ length: n }, (_, i) => (mR.present[i] ? conc(mR, bR, i) : 0)); // (the right node's, before φ̂ moves)
|
|
322
|
-
u[bR] = u[bL] + D;
|
|
323
|
-
for (let i = 0; i < n; i++) if (mR.present[i] && !pass.includes(i)) u[bR + 1 + i] = Math.log(keep[i] / mR.cRef[i]) + mR.mu0[i] / RT + z[i] * u[bR];
|
|
324
|
-
pass.forEach((i, j) => {
|
|
325
|
-
u[bL + 1 + i] = Math.log(cs[j][0] / mL.cRef[i]) + mL.mu0[i] / RT + z[i] * u[bL];
|
|
326
|
-
u[bR + 1 + i] = Math.log(cs[j][1] / mR.cRef[i]) + mR.mu0[i] / RT + z[i] * u[bR];
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
327
|
// An insertion host's φ is a gauge: c0 sets only its neutral combination's level, not how that
|
|
330
328
|
// splits between its ion and its electrons. Start its ion level with the same ion's beside it
|
|
331
329
|
// across a face (the equilibrium a link or a transfer reaction heads for), so its electrons,
|
|
@@ -447,6 +445,69 @@ export function initFromComposition(s) {
|
|
|
447
445
|
}
|
|
448
446
|
s.termV[k] = 0.5 * (lo + hi);
|
|
449
447
|
}
|
|
448
|
+
s.coldStart = true; // (until a solve or a step moves it: see equilibrateFaces)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* The t = 0⁺ state at the faces between strictly neutral regions, taken by the first time step
|
|
453
|
+
* from a cold start (src/timeSteps.js) and not by a steady solve, which needs no t = 0⁺: there it
|
|
454
|
+
* was the worse start, its face nodes moved while the rest of a region stayed (a stack whose
|
|
455
|
+
* middle region started 2.2 V from its neighbour's level failed every steady solve, where the
|
|
456
|
+
* cold start converged in one).
|
|
457
|
+
*/
|
|
458
|
+
export function equilibrateFaces(s) {
|
|
459
|
+
const { model, n, M, u, z } = s;
|
|
460
|
+
const { regions, interfaces, materials } = model;
|
|
461
|
+
const grid = model.grid;
|
|
462
|
+
s.coldStart = false;
|
|
463
|
+
// Two strictly neutral regions meeting at a face whose links hold ions in equilibrium: where
|
|
464
|
+
// c0 leaves the two sides out of it (a different fixed charge or immobile ion on each, so
|
|
465
|
+
// c₊c₋ differs across it), the face reaches it at t = 0⁺, before anything diffuses, and a
|
|
466
|
+
// first step made to take that jump met an exactly singular pivot. Start there: the ions the
|
|
467
|
+
// face passes shared between its two nodes in equilibrium (μ̄ equal across it), each one's
|
|
468
|
+
// amount over the two nodes kept, everything else as c0 has it. The one unknown is the jump
|
|
469
|
+
// in φ̂ across it (a Donnan potential), set by neutrality on one side; the other's follows,
|
|
470
|
+
// the two nodes' charge summing to what it was, zero.
|
|
471
|
+
for (let f = 0; f < interfaces.length; f++) {
|
|
472
|
+
const itf = interfaces[f], mL = materials[regions[f].material], mR = materials[regions[f + 1].material];
|
|
473
|
+
const strict = (m) => m.epsr === 0 && !m.conductor && !m.phiFree && m.ideal;
|
|
474
|
+
if (itf.phi.type !== 'neutral' || !strict(mL) || !strict(mR)) continue;
|
|
475
|
+
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1], bL = s.blockOfNode[gL] * M, bR = s.blockOfNode[gR] * M;
|
|
476
|
+
const pass = [];
|
|
477
|
+
for (let i = 0; i < n; i++) if (itf.links[i].type === 'equilibrium' && mL.present[i] && mR.present[i]) pass.push(i);
|
|
478
|
+
if (!pass.some((i) => z[i] !== 0)) continue;
|
|
479
|
+
const RT = model.RT, vL = grid.vol[gL], vR = grid.vol[gR];
|
|
480
|
+
const conc = (m, b, i) => m.cRef[i] * Math.exp(u[b + 1 + i] - m.mu0[i] / RT - z[i] * u[b]);
|
|
481
|
+
const amount = pass.map((i) => vL * conc(mL, bL, i) + vR * conc(mR, bR, i));
|
|
482
|
+
let qOther = regions[f].fixedCharge / FARADAY; // (the left node's charge from all it doesn't share)
|
|
483
|
+
for (let i = 0; i < n; i++) if (mL.present[i] && !pass.includes(i) && z[i] !== 0) qOther += z[i] * conc(mL, bL, i);
|
|
484
|
+
// At a jump D = φ̂_R − φ̂_L: c_L/c_R = (cRef_L/cRef_R)·exp((μ°_R − μ°_L)/RT + z D), the amount fixing both.
|
|
485
|
+
const split = (D) =>
|
|
486
|
+
pass.map((i, j) => {
|
|
487
|
+
const K = (mL.cRef[i] / mR.cRef[i]) * Math.exp((mR.mu0[i] - mL.mu0[i]) / RT + z[i] * D), cR = amount[j] / (vL * K + vR);
|
|
488
|
+
return [K * cR, cR];
|
|
489
|
+
});
|
|
490
|
+
const qL = (D) => split(D).reduce((q, [cL], j) => q + z[pass[j]] * cL, qOther); // (increasing in D)
|
|
491
|
+
const D0 = u[bR] - u[bL];
|
|
492
|
+
let lo = D0 - 1, hi = D0 + 1;
|
|
493
|
+
while (qL(lo) > 0 && lo > D0 - 1e3) lo -= 2 * (hi - lo);
|
|
494
|
+
while (qL(hi) < 0 && hi < D0 + 1e3) hi += 2 * (hi - lo);
|
|
495
|
+
if (!(qL(lo) <= 0 && qL(hi) >= 0)) continue; // (nothing the face shares can balance it: leave c0)
|
|
496
|
+
for (let it = 0; it < 200 && hi - lo > 1e-13 * Math.max(1, Math.abs(D0)); it++) {
|
|
497
|
+
const m = 0.5 * (lo + hi);
|
|
498
|
+
if (qL(m) < 0) lo = m;
|
|
499
|
+
else hi = m;
|
|
500
|
+
}
|
|
501
|
+
const D = 0.5 * (lo + hi), cs = split(D);
|
|
502
|
+
const keep = Float64Array.from({ length: n }, (_, i) => (mR.present[i] ? conc(mR, bR, i) : 0)); // (the right node's, before φ̂ moves)
|
|
503
|
+
u[bR] = u[bL] + D;
|
|
504
|
+
for (let i = 0; i < n; i++) if (mR.present[i] && !pass.includes(i)) u[bR + 1 + i] = Math.log(keep[i] / mR.cRef[i]) + mR.mu0[i] / RT + z[i] * u[bR];
|
|
505
|
+
pass.forEach((i, j) => {
|
|
506
|
+
u[bL + 1 + i] = Math.log(cs[j][0] / mL.cRef[i]) + mL.mu0[i] / RT + z[i] * u[bL];
|
|
507
|
+
u[bR + 1 + i] = Math.log(cs[j][1] / mR.cRef[i]) + mR.mu0[i] / RT + z[i] * u[bR];
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
s.computeConcentrations();
|
|
450
511
|
}
|
|
451
512
|
|
|
452
513
|
// A non-ideal material's composition at φ̂ = ph: level-fixed species (mode 1) at their η,
|
package/src/steady.js
CHANGED
|
@@ -49,8 +49,14 @@
|
|
|
49
49
|
// (this.stepped), on a direct device with continuation not off, the same again from a cold
|
|
50
50
|
// start (initFromComposition), whose u and uLo become the stages' start (termV stays the
|
|
51
51
|
// start's: a floating terminal keeps the voltage the transient left it at).
|
|
52
|
-
// 2. Continuation: generation ramped up (_generationContinuation);
|
|
53
|
-
// current
|
|
52
|
+
// 2. Continuation: generation ramped up (_generationContinuation); terminals behind a
|
|
53
|
+
// resistance (_resistanceContinuation): one whose current Kirchhoff doesn't give, held at
|
|
54
|
+
// voltages marched to its load line's crossing (_floatingContinuation with a line); else, or
|
|
55
|
+
// where that fails, each held (at its source voltage, or where every other terminal is
|
|
56
|
+
// driven by a current at V_src − I R, Kirchhoff's I), then the resistances put back and the
|
|
57
|
+
// steady system solved from there (and where that fails for one terminal held by
|
|
58
|
+
// Kirchhoff, its load line); a terminal driven by a current, not bound (_bound), held at
|
|
59
|
+
// voltages instead (_floatingContinuation); with
|
|
54
60
|
// canContinue, the right contact's voltage ramped to its target (_continuation): from the
|
|
55
61
|
// voltages of the last converged solve (this.solvedV), the left contact's first where it
|
|
56
62
|
// moved, else from level contacts, a cold start.
|
|
@@ -190,6 +196,7 @@ export class SteadySolves {
|
|
|
190
196
|
this.steady = true; // (the steady system's rows; sources at the present time)
|
|
191
197
|
this.steadyAt = this.time; // (which a pseudo-transient's steps don't move on)
|
|
192
198
|
this.drifted = null; // (an amount no row keeps, lost by a giant step: see _steadySteps)
|
|
199
|
+
this.modelError = null; // (a statistics model's refusal, for the failure: see newton)
|
|
193
200
|
// A solve that fails leaves where the state was solved, and whether it was stepped, as they
|
|
194
201
|
// were (Device.solve restores the state): a floating continuation's held solves had set them,
|
|
195
202
|
// and the next held solve ramped warm from a voltage the state was never at, and failed.
|
|
@@ -252,6 +259,7 @@ export class SteadySolves {
|
|
|
252
259
|
if (q.converged) {
|
|
253
260
|
this.solvedV = [this.termV[0], this.termV[1]]; // (a contact's own: driven by a current, not the current)
|
|
254
261
|
this.stepped = false;
|
|
262
|
+
this.coldStart = false; // (solved: no t = 0⁺ left for a transient to start from)
|
|
255
263
|
}
|
|
256
264
|
return q;
|
|
257
265
|
};
|
|
@@ -289,6 +297,15 @@ export class SteadySolves {
|
|
|
289
297
|
if (g.converged) return solved(g);
|
|
290
298
|
r = g;
|
|
291
299
|
}
|
|
300
|
+
// A terminal behind a resistance, cold: held at its source voltage first (with that its own
|
|
301
|
+
// continuations), then the resistance put back and solved from there (a diode behind 7 Ω·m²
|
|
302
|
+
// failed cold, and converged so; round 14's fuzzer, 4 of 9 such re-solves).
|
|
303
|
+
if (allowed && this.floating.some((k) => this.terms[k].drive.kind === 'V' && this.terms[k].drive.R > 0)) {
|
|
304
|
+
restart();
|
|
305
|
+
const q = this._resistanceContinuation(opts, r);
|
|
306
|
+
if (q.converged) return solved(q);
|
|
307
|
+
r = q;
|
|
308
|
+
}
|
|
292
309
|
// A terminal driven by a current (open circuit, say): hold it at a voltage instead, march the
|
|
293
310
|
// voltage until the current crosses its target, and float it from there.
|
|
294
311
|
if (allowed && this.floating.some((k) => this.terms[k].drive.kind === 'I' && !this._bound(k))) {
|
|
@@ -342,9 +359,10 @@ export class SteadySolves {
|
|
|
342
359
|
// voltage instead, whose steady solves have their own continuation, the voltage marched from
|
|
343
360
|
// the start's until the current crosses its target, the crossing narrowed by bisection, and
|
|
344
361
|
// the terminal floated again from beside it.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
362
|
+
// (With `line`, terminal k behind a resistance: the current its load line passes at V,
|
|
363
|
+
// (V_src − V)/R, the target, the march from the first held terminal's level.)
|
|
364
|
+
_floatingContinuation(opts, r, k = this.floating.find((j) => this.terms[j].drive.kind === 'I' && !this._bound(j)), line = null) {
|
|
365
|
+
const drive = this.terms[k].drive, target = line ? NaN : sourceAt(drive.src, this.time), want = (V) => (line ? line(V) : target);
|
|
348
366
|
// Its drive put back: redrive() finds the laws again for it, with the local constraints'
|
|
349
367
|
// references at zero, so they're taken from the state again (an immobile species held at 0
|
|
350
368
|
// at every node: the floated solve failed, 80 steps, and the regula falsi rescued it at 6×).
|
|
@@ -363,11 +381,12 @@ export class SteadySolves {
|
|
|
363
381
|
iterations += q.iterations ?? 0;
|
|
364
382
|
history.push({ held: V, converged: q.converged });
|
|
365
383
|
if (q.converged) seen.push([V, this.termI[k]]);
|
|
366
|
-
return q.converged ? this.termI[k] -
|
|
384
|
+
return q.converged ? this.termI[k] - want(V) : NaN;
|
|
367
385
|
};
|
|
368
386
|
const fail = () => ({ converged: false, steps, iterations, history });
|
|
369
387
|
const seen = []; // [V, I] of each held solve that converged
|
|
370
|
-
|
|
388
|
+
const heldK = this.terms.findIndex((t) => t.drive.kind === 'V' && !(t.drive.R > 0));
|
|
389
|
+
let Va = line ? (heldK >= 0 ? this.termV[heldK] : 0) : this.termV[k], fa, Vb, fb;
|
|
371
390
|
try {
|
|
372
391
|
fa = hold(Va);
|
|
373
392
|
if (!Number.isFinite(fa)) {
|
|
@@ -396,7 +415,7 @@ export class SteadySolves {
|
|
|
396
415
|
}
|
|
397
416
|
if (!Number.isFinite(fb) || (Math.sign(fb) === Math.sign(fa) && fa !== 0)) {
|
|
398
417
|
// What the held voltages passed, at the two extremes reached.
|
|
399
|
-
if (seen.length > 1) {
|
|
418
|
+
if (seen.length > 1 && !line) {
|
|
400
419
|
seen.sort((a, b) => a[0] - b[0]);
|
|
401
420
|
this.unreached = { terminal: this.terms[k].name, target, low: seen[0], high: seen.at(-1) };
|
|
402
421
|
}
|
|
@@ -423,7 +442,7 @@ export class SteadySolves {
|
|
|
423
442
|
// one at the voltage that passes the target: found by held solves, by regula falsi
|
|
424
443
|
// (Illinois) within the bracket, to 1e-10 of the target, or at open circuit of the largest
|
|
425
444
|
// current the march saw (a target of 0 is never met exactly).
|
|
426
|
-
const scale = Math.max(Math.abs(target), ...seen.map(([, I]) => Math.abs(I)));
|
|
445
|
+
const scale = Math.max(line ? 0 : Math.abs(target), ...seen.map(([, I]) => Math.abs(I)));
|
|
427
446
|
try {
|
|
428
447
|
let side = 0;
|
|
429
448
|
for (let it = 0; it < 60; it++) {
|
|
@@ -446,6 +465,70 @@ export class SteadySolves {
|
|
|
446
465
|
}
|
|
447
466
|
}
|
|
448
467
|
|
|
468
|
+
// One terminal behind a resistance: its load line's crossing by held solves. Otherwise (or where
|
|
469
|
+
// that fails), each terminal behind a resistance held at its source voltage (R taken off; at
|
|
470
|
+
// V_src − I R where Kirchhoff gives I), solved, then the resistances put back and the steady
|
|
471
|
+
// system solved from that state.
|
|
472
|
+
_resistanceContinuation(opts, r) {
|
|
473
|
+
const ks = this.floating.filter((k) => this.terms[k].drive.kind === 'V' && this.terms[k].drive.R > 0);
|
|
474
|
+
const drives = ks.map((k) => this.terms[k].drive);
|
|
475
|
+
let steps = r.steps ?? 0, iterations = r.iterations ?? 0;
|
|
476
|
+
const history = (r.history ?? []).slice();
|
|
477
|
+
// Where every other terminal is driven by a current, Kirchhoff gives the one behind R its
|
|
478
|
+
// current, and so the voltage it ends at, V_src − I R: held there, putting R back moves
|
|
479
|
+
// nothing (held at V_src, it had to slide the whole device by IR, and failed past volts).
|
|
480
|
+
const others = this.terms.every((t, j) => ks.includes(j) || t.drive.kind === 'I');
|
|
481
|
+
const at = (k) => {
|
|
482
|
+
const d = this.terms[k].drive, V = sourceAt(d.src, this.time);
|
|
483
|
+
if (!(others && ks.length === 1)) return V;
|
|
484
|
+
const I = -this.terms.reduce((t, tm, j) => (j === k ? t : t + sourceAt(tm.drive.src, this.time)), 0);
|
|
485
|
+
return V - I * d.R;
|
|
486
|
+
};
|
|
487
|
+
const start = [Float64Array.from(this.u), Float64Array.from(this.uLo), Float64Array.from(this.termV)];
|
|
488
|
+
const restore = () => {
|
|
489
|
+
this.u.set(start[0]);
|
|
490
|
+
this.uLo.set(start[1]);
|
|
491
|
+
this.termV.set(start[2]);
|
|
492
|
+
this.computeConcentrations();
|
|
493
|
+
};
|
|
494
|
+
// One terminal behind R, the rest not all driven by currents (so its current isn't known):
|
|
495
|
+
// its crossing with the load line, (V_src − V)/R, by held solves, as an open circuit's is
|
|
496
|
+
// with its target, first. Held at its source instead, the state is I R from the answer and
|
|
497
|
+
// the pseudo-transient relaxes it over R C at Newton's pace through that swing (a lit Schottky
|
|
498
|
+
// diode passing 0.9 A/m²: 0.05 s at 1 Ω·m², 10 s at 1e4, 105 s at 1e5, steps ∝ R).
|
|
499
|
+
const d = drives[0], Vs = sourceAt(d.src, this.time), line = (V) => (Vs - V) / d.R;
|
|
500
|
+
const byLine = ks.length === 1 && !others;
|
|
501
|
+
if (byLine) {
|
|
502
|
+
const q = this._floatingContinuation(opts, r, ks[0], line);
|
|
503
|
+
if (q.converged) return q;
|
|
504
|
+
restore(); // (from where this stage began, not where the failed one left it)
|
|
505
|
+
steps = q.steps ?? steps;
|
|
506
|
+
iterations = q.iterations ?? iterations;
|
|
507
|
+
history.splice(0, history.length, ...(q.history ?? []));
|
|
508
|
+
}
|
|
509
|
+
let heldOk = false;
|
|
510
|
+
try {
|
|
511
|
+
const held = ks.map(at);
|
|
512
|
+
ks.forEach((k, j) => (this.terms[k].drive = { ...this.terms[k].drive, src: { value: held[j] }, R: 0 }));
|
|
513
|
+
this.redrive();
|
|
514
|
+
const q = this._steadyFromHere(opts);
|
|
515
|
+
steps += q.steps ?? 0;
|
|
516
|
+
iterations += q.iterations ?? 0;
|
|
517
|
+
history.push({ resistancesOff: true, converged: q.converged });
|
|
518
|
+
heldOk = q.converged;
|
|
519
|
+
} finally {
|
|
520
|
+
ks.forEach((k, j) => (this.terms[k].drive = drives[j]));
|
|
521
|
+
this.redrive();
|
|
522
|
+
captureLocal(this);
|
|
523
|
+
}
|
|
524
|
+
const q = heldOk ? this._steadySteps(opts) : { converged: false, steps: 0, iterations: 0, history: [] };
|
|
525
|
+
const done = { ...q, steps: steps + q.steps, iterations: iterations + q.iterations, history: [...history, ...q.history] };
|
|
526
|
+
if (q.converged || ks.length !== 1 || byLine) return done;
|
|
527
|
+
// Where Kirchhoff's hold failed too, the load line.
|
|
528
|
+
restore();
|
|
529
|
+
return this._floatingContinuation(opts, done, ks[0], line);
|
|
530
|
+
}
|
|
531
|
+
|
|
449
532
|
// Steady solves with the generation reactions' rates scaled from 1e-30 up to 1, ×100 a step
|
|
450
533
|
// while they converge, smaller steps where they don't.
|
|
451
534
|
_generationContinuation(opts, r) {
|
package/src/timeSteps.js
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
// (ports' charges) }: what BDF2 and the predictor need.
|
|
22
22
|
//
|
|
23
23
|
// step(s, dt, opts) → newton's result (+ bdf)
|
|
24
|
+
// From a cold start (s.coldStart, outside a steady solve), the faces between strictly neutral
|
|
25
|
+
// regions first set to their t = 0⁺ equilibrium (equilibrateFaces, src/start.js).
|
|
24
26
|
// One step from s.time to tEnd = s.time + dt (or s.landing exactly, where it's within 1e-9·dt
|
|
25
27
|
// of that). Backward Euler; BDF2 when opts.method is 'bdf2', a previous step exists and the
|
|
26
28
|
// ratio ω = dt/dt_prev ≤ 2: then c* = b1·c_n − b2·c_{n−1} (b1 = (1+ω)/a0, b2 = ω²/((1+ω)a0),
|
|
@@ -85,6 +87,7 @@
|
|
|
85
87
|
|
|
86
88
|
import { nextBreakpoint } from './device.js';
|
|
87
89
|
import { SolverError } from './solver.js';
|
|
90
|
+
import { equilibrateFaces } from './start.js';
|
|
88
91
|
|
|
89
92
|
// Lagrange weights at x through the points s0, s1, s2.
|
|
90
93
|
function lagrange3(x, s0, s1, s2) {
|
|
@@ -120,6 +123,8 @@ export function advance(s, dt, opts, depth = 0) {
|
|
|
120
123
|
* the start-of-step values, and dt/a0 replaces dt.
|
|
121
124
|
*/
|
|
122
125
|
export function step(s, dt, opts = {}) {
|
|
126
|
+
// A transient's first step from a cold start begins at its t = 0⁺ (see equilibrateFaces).
|
|
127
|
+
if (s.coldStart && !s.steady) equilibrateFaces(s);
|
|
123
128
|
// What the step overwrites before Newton runs, for a failed one to put back: the Solution read
|
|
124
129
|
// after it takes its contacts' fluxes against the start-of-step values (a host emptied by its
|
|
125
130
|
// current read its contact's Li⁺ flux 8× off at the same state).
|
|
@@ -262,6 +267,7 @@ export function integrate(s, tEnd, opts = {}) {
|
|
|
262
267
|
const sample = s._probes(opts.probes);
|
|
263
268
|
const trace = { t: [], current: [], voltage: [], ...(sample ? { probes: sample.out } : {}) };
|
|
264
269
|
let steps = 0, rejected = 0, iterations = 0, failed = false;
|
|
270
|
+
s.modelError = null; // (a statistics model's refusal, for the failure: see newton)
|
|
265
271
|
// Already there (an animation frame with no time to add): nothing to do, and the step size
|
|
266
272
|
// carried to the next call stays as it was. A time already passed, or reached again outside
|
|
267
273
|
// an animation, is most likely advance(dt) read as a duration: say so.
|
|
@@ -269,7 +275,7 @@ export function integrate(s, tEnd, opts = {}) {
|
|
|
269
275
|
const relative = tEnd < s.time || (s.time > 0 && opts.budgetMs === undefined);
|
|
270
276
|
return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace, ...(relative ? { stopped: `advance(${tEnd}) did nothing: ${absolute}. To run for a further dt, advance(sol.time + dt)` } : {}) };
|
|
271
277
|
}
|
|
272
|
-
const t0 = s.time;
|
|
278
|
+
const t0 = s.time, fromLayout = s.history.length === 0 && !s.stepped && !s.atSteady; // (c0's layout, not a solution)
|
|
273
279
|
// A cold start whose floating double layers start uncharged far from the equilibrium of an ion
|
|
274
280
|
// link beside them (a capacitive contact or port passing K⁺, its outside level set for the
|
|
275
281
|
// charged steady state): the link closes the gap in the first instant, which can be far below
|
|
@@ -395,6 +401,12 @@ export function integrate(s, tEnd, opts = {}) {
|
|
|
395
401
|
stopped = `${at}: the start can't be neutral in ${unbalanced.join(', ')}, strictly neutral (ε = 0): the amounts its c0 gives leave a net charge that no species held at a contact's level can balance, and a step can't move a stored amount at once. Give c0 that balances (with any fixed charge), or leave out of c0 the species a contact feeds, so that they start at its level.`;
|
|
396
402
|
else if (failed && violent.length > 0)
|
|
397
403
|
stopped = `${at}: a step did not converge even when shortened to ${dt.toExponential(1)} s. The start is violent: ${violent.join('; ')}. A double layer starts uncharged, and what passes beside it closes such a gap in the first instant, faster than any step can follow. For a start in equilibrium, solve() first (the steady state at the starting drives), then change what the experiment changes, then advance(). Where solve() fails from there too, hold that terminal at a voltage, solve(), then let it float (set its I: 0): it keeps the state the held one gave it.`;
|
|
404
|
+
else if (failed && s.modelError)
|
|
405
|
+
stopped = `${at}: a step did not converge even when shortened to ${dt.toExponential(1)} s, the state it reached refused by a statistics model: ${s.modelError}. The model doesn't hold there: a smaller concentration or drive, or a model for it (lattice for crowding).`;
|
|
406
|
+
else if (failed && fromLayout && steps === 0)
|
|
407
|
+
// (round 15's fuzzer: the pn template advanced from its c0, a step of 3e-24 s failing, 23 of 62
|
|
408
|
+
// plain diodes; nothing said to solve first)
|
|
409
|
+
stopped = `${at}: the first step did not converge even when shortened to ${dt.toExponential(1)} s, from the starting state, which is c0's layout rather than a solution (a junction's depletion not yet formed, say). For a transient from equilibrium, solve() first at the starting drives, then change what the experiment changes, then advance().`;
|
|
398
410
|
else if (failed) stopped = `${at}: a step did not converge even when shortened to ${dt.toExponential(1)} s. Look for: a host or surface driven to its limit (emptied or filled at its surface by a current it can no longer pass); an electrode at open circuit held only by a slow reaction, with no double-layer capacitance to keep its level (give its face one: phi capacitive); a drive's jump too big for one step (ramp it); a driven current the device cannot keep passing (a diode's reverse recovery past its storage time: stop there); or a device the steady solves also find hard.`;
|
|
399
411
|
else if (s.time < tEnd && steps + rejected >= maxSteps) stopped = `${at}: maxSteps (${maxSteps}) used up (${rejected} of them rejected). Call again to go on, or loosen tol.`;
|
|
400
412
|
return { converged: !failed, done: s.time >= tEnd, steps, rejected, iterations, trace, ...(stopped ? { stopped } : {}) };
|
package/types/impedance.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export class SmallSignal {
|
|
|
4
4
|
* gives (J + iωM)·δx + Σ B_k δV_k = rhs for the grid's rows, and (C_k + iωC′_k)·δx +
|
|
5
5
|
* (∂I_k/∂V_k)·δV_k = δI_set,k for each floating terminal's circuit row, with J the steady
|
|
6
6
|
* Jacobian and M the storage part (from J(dt) = J + M/dt), B_k = ∂res/∂V_k and C_k = ∂I_k/∂x.
|
|
7
|
-
* At the measured terminal,
|
|
8
|
-
*
|
|
7
|
+
* At the measured terminal, its voltage is perturbed (rhs = −B δV) and its current read, held
|
|
8
|
+
* or driven by a current alike (one input impedance; see the contract). The other terminals
|
|
9
9
|
* keep their drives: held ones at AC ground, driven ones at AC open circuit.
|
|
10
10
|
* Z = δV/δI with I into the device (positive real part for a passive device).
|
|
11
11
|
* @param {ArrayLike<number>} frequencies Hz
|
package/types/solver.d.ts
CHANGED
|
@@ -273,6 +273,7 @@ export class Solver {
|
|
|
273
273
|
};
|
|
274
274
|
_warmFrom(src: any): void;
|
|
275
275
|
solvedV: any;
|
|
276
|
+
coldStart: any;
|
|
276
277
|
_snapshot(): {
|
|
277
278
|
u: Float64Array<ArrayBuffer>;
|
|
278
279
|
uLo: Float64Array<ArrayBuffer>;
|
|
@@ -299,6 +300,7 @@ export class Solver {
|
|
|
299
300
|
history: any[];
|
|
300
301
|
termV: Float64Array<ArrayBuffer>;
|
|
301
302
|
sourceTime: number;
|
|
303
|
+
coldStart: any;
|
|
302
304
|
};
|
|
303
305
|
_restore(s: any): void;
|
|
304
306
|
contactDEnd: any;
|
package/types/start.d.ts
CHANGED
|
@@ -4,3 +4,11 @@
|
|
|
4
4
|
* profile varies), or continued across the interface dipole if nothing there responds.
|
|
5
5
|
*/
|
|
6
6
|
export function initFromComposition(s: any): void;
|
|
7
|
+
/**
|
|
8
|
+
* The t = 0⁺ state at the faces between strictly neutral regions, taken by the first time step
|
|
9
|
+
* from a cold start (src/timeSteps.js) and not by a steady solve, which needs no t = 0⁺: there it
|
|
10
|
+
* was the worse start, its face nodes moved while the rest of a region stayed (a stack whose
|
|
11
|
+
* middle region started 2.2 V from its neighbour's level failed every steady solve, where the
|
|
12
|
+
* cold start converged in one).
|
|
13
|
+
*/
|
|
14
|
+
export function equilibrateFaces(s: any): void;
|
package/types/steady.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export class SteadySolves {
|
|
|
29
29
|
steady: boolean;
|
|
30
30
|
steadyAt: any;
|
|
31
31
|
drifted: any;
|
|
32
|
+
modelError: any;
|
|
32
33
|
_solveSteadyAll(opts: any): any;
|
|
33
34
|
atSteady: any;
|
|
34
35
|
_steadyFromHere(opts: any): any;
|
|
@@ -36,9 +37,11 @@ export class SteadySolves {
|
|
|
36
37
|
sourceTime: any;
|
|
37
38
|
solvedV: any[];
|
|
38
39
|
stepped: boolean;
|
|
40
|
+
coldStart: boolean;
|
|
39
41
|
dimmer: boolean;
|
|
40
42
|
_bound(k: any): any;
|
|
41
|
-
_floatingContinuation(opts: any, r: any): any;
|
|
43
|
+
_floatingContinuation(opts: any, r: any, k?: any, line?: any): any;
|
|
44
|
+
_resistanceContinuation(opts: any, r: any): any;
|
|
42
45
|
_generationContinuation(opts: any, r: any): {
|
|
43
46
|
converged: boolean;
|
|
44
47
|
steps: any;
|