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/impedance.js
CHANGED
|
@@ -22,33 +22,44 @@
|
|
|
22
22
|
// → { f, Z: { re, im }, relError, profiles? }, Z in Ω·m² (Ω for a non-planar device) at each
|
|
23
23
|
// frequency.
|
|
24
24
|
// Throws a SolverError where the terminal doesn't exist, has a series resistance, or every
|
|
25
|
-
// other terminal is driven by a current (no return path: Z infinite),
|
|
26
|
-
// isn't positive and finite.
|
|
25
|
+
// other terminal is driven by a current (no return path: Z infinite), where a frequency
|
|
26
|
+
// isn't positive and finite, or where the linearised system is singular at one.
|
|
27
27
|
// Linearised about the present state: (J + iωS)·δx + Σ_k B_k δV_k = 0 for the grid's rows and,
|
|
28
28
|
// for each floating terminal k (driven by a current), (C_k + iωC′_k)·δx + (DI_k + iωDI′_k) δV_k
|
|
29
29
|
// + Σ_k2 (DX + iωDX′)_k,k2 δV_k2 = δI_k, with J the steady Jacobian (dt = ∞) and S, C′, DI′,
|
|
30
|
-
// DX′ the storage parts, (X(dt) − X(∞))·dt at dt = 1e-30.
|
|
30
|
+
// DX′ the storage parts, (X(dt) − X(∞))·dt at dt = 1e-30. The measured terminal is always
|
|
31
|
+
// excited by its voltage: driven by a current, it's measured as if held (δV = 1, Z = 1/δI, the
|
|
32
|
+
// same input impedance), its circuit row out of the small system, its current read as a held
|
|
33
|
+
// one's (with its circuit law's change where it has one); its profiles then scaled by Z, per
|
|
34
|
+
// unit current. A floating terminal that alone feeds
|
|
31
35
|
// a conserved combination (a circuit law: a constraint or time law with its circuit row) has
|
|
32
36
|
// its current read by that combination's change at every frequency, iω F δS/λ plus what a
|
|
33
|
-
// resistance passes: at dt = 1e-30 every circuit law
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// (AC open).
|
|
37
|
+
// resistance passes: at dt = 1e-30 every circuit law with a current to count (λ set) reads by
|
|
38
|
+
// the change (this.lawForm), and
|
|
39
|
+
// its steady parts are C = 0, DI = 1/R (0 driven by a current), DX = 0. δI is the measured
|
|
40
|
+
// terminal's current into the device. Every other terminal keeps its drive: held, δV = 0 (AC
|
|
41
|
+
// ground); driven, δI = 0 (AC open).
|
|
38
42
|
// Each frequency: J + iωS with each row divided by its largest entry, factorised, preconditions
|
|
39
43
|
// GMRES (m 12, 8 restarts, tol 1e-8; its best iterate kept) on the exact J·v (difference form).
|
|
40
|
-
//
|
|
44
|
+
// A floating contact otherwise reads its current, in its circuit row, at the reading of least
|
|
45
|
+
// error by the solved responses (its own, or any cut between it and a port's window: the same
|
|
46
|
+
// current); and where every other terminal floats, a reading with no direct V dependence (a
|
|
47
|
+
// cut's, a law's change) reads y as Σ X_b, every terminal's uniform shift passing nothing.
|
|
48
|
+
// Solved: y, the response with the floating terminals held (to the measured one's δV = 1),
|
|
41
49
|
// and X_k, to each floating terminal's unit δV_k; then the floating terminals' rows, a small
|
|
42
50
|
// complex system, for their δV; δx = y − Σ X_k δV_k.
|
|
43
51
|
// The current read (held terminal): DI + iωDI′ + Σ (DX + iωDX′) δV + (C + iωC′)·δx, its error
|
|
44
|
-
// estimated from its terms' round-off
|
|
52
|
+
// estimated from its terms' round-off, the solves' residuals, and each unknown's round-off in
|
|
53
|
+
// its own row, ε Σ_k |A_jk x_k| / |A_jj| (a metal's current unknown, G Δη to a huge G: a
|
|
54
|
+
// bound, cautious where the coupled system pins it better); for a contact, also across
|
|
45
55
|
// each cut (_currentCuts: a segment's linearised total current, conduction and displacement,
|
|
46
56
|
// between that contact and the nearest port's window where there are ports), the reading with
|
|
47
57
|
// the least error taken; and where every other terminal floats, by Kirchhoff, Σ δV_k/R_k over
|
|
48
58
|
// those behind a resistance, its error (ε + the solves' residual)·|δV_k|/R_k. With no floating
|
|
49
59
|
// terminal, cuts, and that error over 1% of the current:
|
|
50
60
|
// the response split into each region's uniform shift (_regionShift) and a remainder solved to
|
|
51
|
-
// GMRES's floor, the shift's effect taken only at the rows it can touch (_regionEdgeRows
|
|
61
|
+
// GMRES's floor, the shift's effect taken only at the rows it can touch (_regionEdgeRows; a
|
|
62
|
+
// metal's carriers shifted, its current unknown not), and
|
|
52
63
|
// that reading kept where its error is less.
|
|
53
64
|
// relError: per frequency, the chosen reading's error estimate over |δI| (held); driven, the
|
|
54
65
|
// same estimate of its unit current read back from δx. Cautious: it can exceed the true error
|
|
@@ -75,8 +86,7 @@
|
|
|
75
86
|
// Also written, left as the last assembly at dt = ∞ left them: this.sourceTime (the present
|
|
76
87
|
// time), cOld, thOld, th0Old, gateOld (the present state's), contactDStart, portQStart, the
|
|
77
88
|
// residual, the Jacobian and the terminal terms, sysRest and the concentrations; this.lawForm
|
|
78
|
-
// (every circuit law by the change, until a step sets it afresh).
|
|
79
|
-
// node's slot 0 as φ̂, a metal's flux unknown too: harmless while a metal region is one cell.
|
|
89
|
+
// (every circuit law by the change, until a step sets it afresh).
|
|
80
90
|
// Tests: test/impedance.test.js (Macdonald's blocking electrodes, a junction's depletion
|
|
81
91
|
// capacitance, a MOS capacitor at both terminals, ports, profiles, Fermi–Dirac against
|
|
82
92
|
// Boltzmann); the demos' EIS.
|
|
@@ -218,8 +228,8 @@ export class SmallSignal {
|
|
|
218
228
|
* gives (J + iωM)·δx + Σ B_k δV_k = rhs for the grid's rows, and (C_k + iωC′_k)·δx +
|
|
219
229
|
* (∂I_k/∂V_k)·δV_k = δI_set,k for each floating terminal's circuit row, with J the steady
|
|
220
230
|
* Jacobian and M the storage part (from J(dt) = J + M/dt), B_k = ∂res/∂V_k and C_k = ∂I_k/∂x.
|
|
221
|
-
* At the measured terminal,
|
|
222
|
-
*
|
|
231
|
+
* At the measured terminal, its voltage is perturbed (rhs = −B δV) and its current read, held
|
|
232
|
+
* or driven by a current alike (one input impedance; see the contract). The other terminals
|
|
223
233
|
* keep their drives: held ones at AC ground, driven ones at AC open circuit.
|
|
224
234
|
* Z = δV/δI with I into the device (positive real part for a passive device).
|
|
225
235
|
* @param {ArrayLike<number>} frequencies Hz
|
|
@@ -249,8 +259,14 @@ export class SmallSignal {
|
|
|
249
259
|
`its impedance is infinite. Measure at one of those instead (impedance(f, { terminal: '${others[0].name}' }), its voltage against '${terminal}'), or hold one of them at a voltage`,
|
|
250
260
|
);
|
|
251
261
|
}
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
// Z is the device's input impedance at kT, the same whether its voltage is perturbed and its
|
|
263
|
+
// current read or the other way round. A terminal driven by a current is measured as if held
|
|
264
|
+
// (δV = 1, Z = 1/δI) to read that current as a held one's is (cuts, Kirchhoff, a circuit law's
|
|
265
|
+
// change): read through its own circuit row instead, at its contact alone, it was a metal's or
|
|
266
|
+
// a barrier's round-off (a reverse-biased diode's contact 4.3% off dV/dI; a Schottky
|
|
267
|
+
// contact's Re Z −32 for 1282), with an error estimate that never saw it.
|
|
268
|
+
const N = nB * M, fl = this.floating.filter((k) => k !== kT), K = fl.length;
|
|
269
|
+
const kirchhoff = others.every((t) => t.drive.kind === 'I' || t.drive.R > 0);
|
|
254
270
|
this.sourceTime = this.time;
|
|
255
271
|
this.computeConcentrations();
|
|
256
272
|
this.cOld.set(this.c);
|
|
@@ -271,11 +287,14 @@ export class SmallSignal {
|
|
|
271
287
|
// flux at dt = ∞ and by the change at dt = 1e-30, a floating gate's current was counted
|
|
272
288
|
// twice (its capacitance 2× dQ/dV); read by the flux throughout, through a metal it is the
|
|
273
289
|
// round-off of G Δη (a gate behind 1 kΩ·m², measured at the back contact: Re Z = −2.7).
|
|
274
|
-
|
|
290
|
+
// (A law that counts no current, λ unset, as for a terminal at no current that feeds
|
|
291
|
+
// nothing, can't be read by its change: forced to, a closed Fe³⁺/Fe²⁺ cell's Z was NaN.)
|
|
292
|
+
const byChange = (cs) => cs.circuit >= 0 && !(cs.contacts?.length > 0) && Number.isFinite(cs.lambda) && cs.lambda !== 0;
|
|
293
|
+
this.lawForm = new Map([...this.constraints, ...this.timeLaws].map((cs) => [cs, byChange(cs)]));
|
|
275
294
|
this.assemble(dts);
|
|
276
295
|
for (const cs of [...this.constraints, ...this.timeLaws]) {
|
|
277
296
|
const k = cs.circuit;
|
|
278
|
-
if (
|
|
297
|
+
if (!byChange(cs) || !(fl.includes(k) || (k === kT && dT.kind === 'I'))) continue;
|
|
279
298
|
Ct[k].fill(0);
|
|
280
299
|
DI[k] = this.terms[k].drive.R > 0 ? 1 / this.terms[k].drive.R : 0;
|
|
281
300
|
DX[k].fill(0);
|
|
@@ -288,11 +307,31 @@ export class SmallSignal {
|
|
|
288
307
|
// Where the measured contact's current may be read across a segment: anywhere without
|
|
289
308
|
// ports; with them, only between it and the nearest window (a port's exchange takes current
|
|
290
309
|
// in or out along it). Never for a port's own terminal.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
310
|
+
const allCuts = this._currentCuts(), windows = this.model.ports.flatMap((p) => Array.from(p.nodes));
|
|
311
|
+
const lo = windows.length ? Math.min(...windows) : Infinity, hi = windows.length ? Math.max(...windows) : -Infinity;
|
|
312
|
+
const cutsOf = (k) => allCuts.filter((cut) => (k === 0 ? cut.s + 1 <= lo : cut.s >= hi));
|
|
313
|
+
const cuts = kT < 2 ? cutsOf(kT) : null;
|
|
314
|
+
// A floating contact on a metal: its circuit row's current through the metal is the current
|
|
315
|
+
// unknown G Δη, round-off to a huge G (a Pt electrode behind 3000 Ω·m², read at the bath: Z −
|
|
316
|
+
// R came out 70.5 for 3070.5). Any cut between it and a port's window carries the same current
|
|
317
|
+
// (a metal stores none; the total current is continuous through its face), so each frequency
|
|
318
|
+
// reads it where its error, by the solved responses, is least (see readings, below).
|
|
319
|
+
const lawRead = new Set([...this.constraints, ...this.timeLaws].filter(byChange).map((cs) => cs.circuit));
|
|
320
|
+
const choices = new Map(); // k → [{ c, cs, di, dis, dx, dxs }]: its own reading, then each cut's
|
|
321
|
+
for (const k of fl) {
|
|
322
|
+
if (k > 1 || lawRead.has(k)) continue;
|
|
323
|
+
const own = { c: Ct[k].slice(), cs: Cs[k].slice(), di: DI[k], dis: DIs[k], dx: DX[k].slice(), dxs: DXs[k].slice() }, list = [own];
|
|
324
|
+
const sgn = k === 0 ? 1 : -1, R = this.terms[k].drive.R;
|
|
325
|
+
for (const cut of cutsOf(k)) {
|
|
326
|
+
const c = new Float64Array(own.c.length), cs = new Float64Array(own.c.length);
|
|
327
|
+
cut.idx.forEach((x, a) => {
|
|
328
|
+
if (this.rix[x] >= this.sys.size) return;
|
|
329
|
+
c[this.rix[x]] += sgn * cut.g[a];
|
|
330
|
+
cs[this.rix[x]] += sgn * cut.d[a];
|
|
331
|
+
});
|
|
332
|
+
list.push({ c, cs, di: R > 0 ? 1 / R : 0, dis: 0, dx: new Float64Array(own.dx.length), dxs: new Float64Array(own.dx.length) });
|
|
333
|
+
}
|
|
334
|
+
if (list.length > 1) choices.set(k, list);
|
|
296
335
|
}
|
|
297
336
|
// J·v for GMRES, exact: the assembled J holds a flux's dependence on η_L and η_R as two
|
|
298
337
|
// entries, and where both are huge (an inversion layer) and v nearly uniform, J·v loses the
|
|
@@ -348,7 +387,12 @@ export class SmallSignal {
|
|
|
348
387
|
}
|
|
349
388
|
}
|
|
350
389
|
}
|
|
351
|
-
|
|
390
|
+
try {
|
|
391
|
+
csys.factor();
|
|
392
|
+
} catch (err) {
|
|
393
|
+
// (it threw a bare Error, its block number all it said: a cell 2.8 V off its pzc)
|
|
394
|
+
throw new SolverError(`impedance: the linearised equations are singular at ${f} Hz (${err.message}): some part of the device is held by nothing at that frequency, as a double layer far from where it balances can leave it. See the steady state's warnings (solve()).`);
|
|
395
|
+
}
|
|
352
396
|
const sx = (X, vr, out) => {
|
|
353
397
|
for (let blk = 0; blk < nB; blk++) {
|
|
354
398
|
const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
|
|
@@ -404,24 +448,77 @@ export class SmallSignal {
|
|
|
404
448
|
};
|
|
405
449
|
let accuracy = 1e-14; // (relative, of the solves: the readouts' error is weighed with it)
|
|
406
450
|
// y: the response with the floating terminals held; X_k: to a unit δV_k.
|
|
407
|
-
const heldT = dT.kind === 'V';
|
|
408
451
|
for (let j = 0; j < Nc; j++) {
|
|
409
|
-
rr[j] =
|
|
452
|
+
rr[j] = -Bt[kT][j] * scale[j];
|
|
410
453
|
ri[j] = 0;
|
|
411
454
|
}
|
|
412
|
-
solve(rr, ri, yr, yi,
|
|
455
|
+
solve(rr, ri, yr, yi, kT, 1);
|
|
413
456
|
fl.forEach((k, a) => {
|
|
414
457
|
for (let j = 0; j < Nc; j++) rr[j] = Bt[k][j] * scale[j];
|
|
415
458
|
ri.fill(0);
|
|
416
459
|
solve(rr, ri, Xr[a], Xi[a], k, -1);
|
|
417
460
|
});
|
|
461
|
+
// Each unknown's round-off in its own row: |x_j| to ε Σ_k |A_jk x_k| / |A_jj|, A = J + iωS. A
|
|
462
|
+
// metal's current unknown, G Δη to a huge G, is known only to ε G |δη| this way, which the
|
|
463
|
+
// terms' round-off alone never saw (a platinum electrode's current read 20% off at 1 nHz,
|
|
464
|
+
// its estimate 1e-8).
|
|
465
|
+
const rowFloor = (xr0, xi0) => {
|
|
466
|
+
const out = new Float64Array(Nc);
|
|
467
|
+
for (let blk = 0; blk < nB; blk++) {
|
|
468
|
+
const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
|
|
469
|
+
for (let r = 0; r < m; r++) {
|
|
470
|
+
let acc = 0;
|
|
471
|
+
const row = (X, o, len, x0) => {
|
|
472
|
+
for (let cc = 0; cc < len; cc++) acc += (Math.abs(J[X][o + cc]) + w * Math.abs(S[X][o + cc])) * Math.hypot(xr0[x0 + cc], xi0[x0 + cc]);
|
|
473
|
+
};
|
|
474
|
+
if (mp) row('A', offA[blk] + r * mp, mp, offX[blk - 1]);
|
|
475
|
+
row('B', offB[blk] + r * m, m, offX[blk]);
|
|
476
|
+
if (mn) row('C', offC[blk] + r * mn, mn, offX[blk + 1]);
|
|
477
|
+
const dIdx = offB[blk] + r * m + r, diag = Math.hypot(J.B[dIdx], w * S.B[dIdx]);
|
|
478
|
+
out[offX[blk] + r] = diag > 0 ? (1.1e-16 * acc) / diag : 0;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
};
|
|
483
|
+
// Each floating metal contact's reading, of those it can take, with the least error by the
|
|
484
|
+
// solved responses (their terms' round-off and each unknown's in its own row).
|
|
485
|
+
const gaugeRead = new Set(lawRead); // (readings with no direct dependence on any V: see y below)
|
|
486
|
+
if (choices.size) {
|
|
487
|
+
const eps = 1.1e-16, parts = [[yr, yi], ...fl.map((_, b) => [Xr[b], Xi[b]])];
|
|
488
|
+
const mag = new Float64Array(Nc), floor = new Float64Array(Nc);
|
|
489
|
+
for (const [pr, pi] of parts) {
|
|
490
|
+
const rf = rowFloor(pr, pi);
|
|
491
|
+
for (let j = 0; j < Nc; j++) {
|
|
492
|
+
mag[j] += Math.hypot(pr[j], pi[j]);
|
|
493
|
+
floor[j] += rf[j];
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const [k, list] of choices) {
|
|
497
|
+
let best = null, least = Infinity;
|
|
498
|
+
for (const r of list) {
|
|
499
|
+
let e = eps * (Math.abs(r.di) + w * Math.abs(r.dis));
|
|
500
|
+
for (let j = 0; j < Nc; j++) e += (Math.abs(r.c[j]) + w * Math.abs(r.cs[j])) * (eps * mag[j] + floor[j]);
|
|
501
|
+
if (e < least) [best, least] = [r, e];
|
|
502
|
+
}
|
|
503
|
+
if (best !== list[0]) gaugeRead.add(k);
|
|
504
|
+
Ct[k].set(best.c);
|
|
505
|
+
Cs[k].set(best.cs);
|
|
506
|
+
DI[k] = best.di;
|
|
507
|
+
DIs[k] = best.dis;
|
|
508
|
+
DX[k].set(best.dx);
|
|
509
|
+
DXs[k].set(best.dxs);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
418
512
|
// The floating terminals' circuit rows, a small complex system for their δV.
|
|
419
513
|
const Ar = Array.from({ length: K }, () => new Float64Array(K + 1)), Ai = Array.from({ length: K }, () => new Float64Array(K + 1));
|
|
420
514
|
fl.forEach((k, a) => {
|
|
515
|
+
let sre = 0, sim = 0;
|
|
421
516
|
for (let b = 0; b < K; b++) {
|
|
422
517
|
const [re, im] = dot(k, w, Xr[b], Xi[b]);
|
|
423
518
|
Ar[a][b] = -re;
|
|
424
519
|
Ai[a][b] = -im;
|
|
520
|
+
sre += re;
|
|
521
|
+
sim += im;
|
|
425
522
|
}
|
|
426
523
|
Ar[a][a] += DI[k];
|
|
427
524
|
Ai[a][a] += w * DIs[k];
|
|
@@ -432,9 +529,14 @@ export class SmallSignal {
|
|
|
432
529
|
Ar[a][b] += DX[k][k2];
|
|
433
530
|
Ai[a][b] += w * DXs[k][k2];
|
|
434
531
|
});
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
532
|
+
// Every terminal shifted alike moves nothing and passes no current, and y + Σ (−X_b) is
|
|
533
|
+
// that shift where kT and the floating terminals are all there are: so a reading with
|
|
534
|
+
// no direct dependence on a V reads y as Σ X_b, exactly, from responses it resolves, where
|
|
535
|
+
// y itself is nearly that shift and its current a difference of them (a Pt electrode
|
|
536
|
+
// behind 3000 Ω·m², at 1 nHz: its cuts read y 10% apart).
|
|
537
|
+
const [yre, yim] = kirchhoff && gaugeRead.has(k) ? [sre, sim] : dot(k, w, yr, yi);
|
|
538
|
+
Ar[a][K] = -yre - DX[k][kT];
|
|
539
|
+
Ai[a][K] = -yim - w * DXs[k][kT];
|
|
438
540
|
});
|
|
439
541
|
const dV = complexSolve(Ar, Ai, K);
|
|
440
542
|
// δx = y − Σ X_k δV_k
|
|
@@ -475,7 +577,8 @@ export class SmallSignal {
|
|
|
475
577
|
const [re, im] = dot(kT, w, p.r, p.i);
|
|
476
578
|
Ir += re;
|
|
477
579
|
Ii += im;
|
|
478
|
-
|
|
580
|
+
p.rf ??= rowFloor(p.r, p.i);
|
|
581
|
+
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 + p.rf[j]);
|
|
479
582
|
}
|
|
480
583
|
// Kirchhoff, where every other terminal floats: what they pass comes back through kT,
|
|
481
584
|
// δI = Σ δV_k/R_k over those behind a resistance (one driven by a current passes no small
|
|
@@ -503,7 +606,7 @@ export class SmallSignal {
|
|
|
503
606
|
// (g + iωd)(xr + i xi)
|
|
504
607
|
re += gk * p.fr[x] - dk * p.fi[x];
|
|
505
608
|
im += gk * p.fi[x] + dk * p.fr[x];
|
|
506
|
-
e += (Math.abs(gk) + Math.abs(dk)) * (eps * Math.hypot(p.fr[x], p.fi[x]) + p.floor);
|
|
609
|
+
e += (Math.abs(gk) + Math.abs(dk)) * (eps * Math.hypot(p.fr[x], p.fi[x]) + p.floor + (this.rix[x] < Nc ? p.rf[this.rix[x]] : 0));
|
|
507
610
|
}
|
|
508
611
|
}
|
|
509
612
|
if (e < err) [err, Ir, Ii] = [e, sgn * re, sgn * im];
|
|
@@ -524,7 +627,7 @@ export class SmallSignal {
|
|
|
524
627
|
return [fr, fi];
|
|
525
628
|
};
|
|
526
629
|
let Zr, Zi;
|
|
527
|
-
|
|
630
|
+
{
|
|
528
631
|
// δI per volt: (C + iωC′)·δx + (∂I/∂V)(1 + iω′) at the terminal…
|
|
529
632
|
let { Ir, Ii, err } = read([{ r: yr, i: yi, fr: xr, fi: xi, floor: accuracy * largest(yr, yi) }]);
|
|
530
633
|
// …and where the solves leave that unresolved (the estimate past 1% of the current), the
|
|
@@ -565,14 +668,10 @@ export class SmallSignal {
|
|
|
565
668
|
const d2 = Ir * Ir + Ii * Ii; // Z = 1/δI
|
|
566
669
|
Zr = Ir / d2;
|
|
567
670
|
Zi = -Ii / d2;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
Zr = dV.re[a];
|
|
573
|
-
Zi = dV.im[a];
|
|
574
|
-
const back = read([{ r: yr, i: yi, fr: xr, fi: xi, floor: accuracy * largest(yr, yi) }], [Zr, Zi]);
|
|
575
|
-
out.relError[q] = back.err / Math.hypot(back.Ir, back.Ii);
|
|
671
|
+
}
|
|
672
|
+
// (profiles per unit excitation: for a terminal driven by a current, per A/m², δx·Z)
|
|
673
|
+
if (profiles && dT.kind === 'I') {
|
|
674
|
+
for (let j = 0; j < N; j++) [xr[j], xi[j]] = [xr[j] * Zr - xi[j] * Zi, xr[j] * Zi + xi[j] * Zr];
|
|
576
675
|
}
|
|
577
676
|
out.Z.re[q] = Zr;
|
|
578
677
|
out.Z.im[q] = Zi;
|
|
@@ -590,9 +689,12 @@ export class SmallSignal {
|
|
|
590
689
|
for (let r = 0; r < model.regions.length; r++) {
|
|
591
690
|
let sr = 0, si = 0, k = 0;
|
|
592
691
|
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
|
|
692
|
+
// (a metal's slot 0 is its segment's current, not φ̂: shifted, a platinum region of many
|
|
693
|
+
// cells read its electrode's current 8% off, at an error estimate of 1e-8)
|
|
694
|
+
const phiAt = (g) => this.nodeConductor[g] < 0 && loc[this.blockOfNode[g] * M] >= 0;
|
|
593
695
|
for (let g = g0; g <= g1; g++) {
|
|
594
696
|
const o = this.blockOfNode[g] * M;
|
|
595
|
-
if (
|
|
697
|
+
if (phiAt(g)) {
|
|
596
698
|
sr += yr[R[o]];
|
|
597
699
|
si += yi[R[o]];
|
|
598
700
|
k++;
|
|
@@ -614,7 +716,7 @@ export class SmallSignal {
|
|
|
614
716
|
si /= k;
|
|
615
717
|
for (let g = g0; g <= g1; g++) {
|
|
616
718
|
const o = this.blockOfNode[g] * M;
|
|
617
|
-
if (
|
|
719
|
+
if (phiAt(g)) {
|
|
618
720
|
er[R[o]] = sr;
|
|
619
721
|
ei[R[o]] = si;
|
|
620
722
|
}
|
package/src/index.js
CHANGED
|
@@ -132,6 +132,7 @@ export class Device {
|
|
|
132
132
|
solver.sourceTime = old.sourceTime;
|
|
133
133
|
solver._refreshSources();
|
|
134
134
|
solver.dtNext = old.dtNext;
|
|
135
|
+
solver.coldStart = old.coldStart; // (a carried state is as cold as it was: see equilibrateFaces)
|
|
135
136
|
solver.contactDEnd = old.contactDEnd;
|
|
136
137
|
if (old.portQEnd?.length === solver.portQ.length) solver.portQEnd = old.portQEnd;
|
|
137
138
|
solver.solvedV = old.solvedV; // where the carried-over state was solved (for continuation)
|
package/src/kit.js
CHANGED
|
@@ -50,10 +50,14 @@ export function vacuumLevel(def, side, path = 'vacuum') {
|
|
|
50
50
|
if (!Number.isFinite(side.offset)) fail(`${path}.offset must be a finite number (V)`);
|
|
51
51
|
if (side.anchor === 'phi') return -side.offset;
|
|
52
52
|
const sp = (def.species ?? []).find((s) => s.name === side.anchor);
|
|
53
|
-
if (!sp)
|
|
53
|
+
if (!sp)
|
|
54
|
+
fail(
|
|
55
|
+
`${path}.anchor must be 'phi' or a charged species of the device, got ${JSON.stringify(side.anchor)}` +
|
|
56
|
+
(typeof side.anchor === 'string' ? ` (to anchor on it, list it in the library: aqueous([..., '${side.anchor}']), say)` : ''),
|
|
57
|
+
);
|
|
54
58
|
if (!(sp.z !== 0)) fail(`${path}.anchor: '${side.anchor}' is neutral, so it has no level in volts`);
|
|
55
59
|
const entry = mat.species?.[side.anchor];
|
|
56
|
-
if (!isObject(entry)) fail(`${path}.anchor: '${side.anchor}' is absent from '${side.material}'`);
|
|
60
|
+
if (!isObject(entry)) fail(`${path}.anchor: '${side.anchor}' is absent from '${side.material}' (to anchor on it, give the material that species)`);
|
|
57
61
|
if (!Number.isFinite(entry.mu0)) fail(`${path}.anchor: '${side.anchor}' in '${side.material}' has no mu0`);
|
|
58
62
|
return entry.mu0 / (sp.z * FARADAY) - side.offset;
|
|
59
63
|
}
|
package/src/newton.js
CHANGED
|
@@ -75,7 +75,9 @@
|
|
|
75
75
|
// Also written: s.islandsOn (as asked, or on by 8); s.lostMode, s.accepted (7); s.delta, s.deltaV,
|
|
76
76
|
// s.rowDiv; what an assembly writes, at the returned state where a probe or 6's check ran;
|
|
77
77
|
// s.conditioning (a steady solve's failure: the worst cancellation a factorisation showed, for
|
|
78
|
-
// its message).
|
|
78
|
+
// its message); s.modelError, the message of a SolverError a statistics model threw at a state
|
|
79
|
+
// Newton reached (in the assembly, or computing the concentrations where it stops), which fails
|
|
80
|
+
// the call rather than leaving it (cleared by solveSteady and integrate, for their failures).
|
|
79
81
|
// Tests: every solve and step goes through here; test/newton.test.js (the islands round, the
|
|
80
82
|
// residual check's failure) and the MOS gate step in test/ports.test.js (a lost mode, to 1e-6);
|
|
81
83
|
// test/jacobian.test.js checks the system it solves. The devices that needed each branch are in
|
|
@@ -401,6 +403,19 @@ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands
|
|
|
401
403
|
};
|
|
402
404
|
// A steady solve that fails: record how nearly singular the system was, and where; or, failing
|
|
403
405
|
// with the islands' rows in, go back to the solution without them.
|
|
406
|
+
// The concentrations at the state Newton stops at, where a statistics model can refuse it (a
|
|
407
|
+
// Debye–Hückel solution past its range, at the converged state of a transient: the error was
|
|
408
|
+
// thrown from here, uncaught, out of solve() and advance()). Its message, for the failure.
|
|
409
|
+
const settle = () => {
|
|
410
|
+
try {
|
|
411
|
+
s.computeConcentrations();
|
|
412
|
+
return null;
|
|
413
|
+
} catch (err) {
|
|
414
|
+
if (!(err instanceof SolverError)) throw err;
|
|
415
|
+
s.modelError = err.message;
|
|
416
|
+
return err.message;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
404
419
|
const fail = (r) => {
|
|
405
420
|
if (kept) {
|
|
406
421
|
s.u.set(kept.u);
|
|
@@ -430,6 +445,7 @@ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands
|
|
|
430
445
|
} catch (err) {
|
|
431
446
|
// A statistics model can fail far from the solution (e.g. Debye–Hückel beyond its range).
|
|
432
447
|
if (!(err instanceof SolverError)) throw err;
|
|
448
|
+
s.modelError = err.message;
|
|
433
449
|
return fail({ converged: false, iterations: it, history, error: err.message });
|
|
434
450
|
}
|
|
435
451
|
equilibrate(s);
|
|
@@ -500,7 +516,8 @@ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands
|
|
|
500
516
|
const done = { converged: true, iterations: it, history, residual: rmax };
|
|
501
517
|
if (probed()) continue;
|
|
502
518
|
if (islandsOff(it, done)) continue;
|
|
503
|
-
|
|
519
|
+
const refused = settle();
|
|
520
|
+
if (refused) return fail({ converged: false, iterations: it, history, error: refused });
|
|
504
521
|
return done;
|
|
505
522
|
}
|
|
506
523
|
// Converged as far as round-off allows: the updates are already tiny (below 1e-6 thermal
|
|
@@ -523,12 +540,13 @@ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands
|
|
|
523
540
|
const done = { converged: true, iterations: it, history, residual: rmax, roundoff: true };
|
|
524
541
|
if (probed()) continue;
|
|
525
542
|
if (islandsOff(it, done)) continue;
|
|
526
|
-
|
|
543
|
+
const refused = settle();
|
|
544
|
+
if (refused) return fail({ converged: false, iterations: it, history, error: refused });
|
|
527
545
|
return done;
|
|
528
546
|
}
|
|
529
547
|
}
|
|
530
|
-
|
|
531
|
-
return fail({ converged: false, iterations: maxIter, history });
|
|
548
|
+
const refused = settle();
|
|
549
|
+
return fail({ converged: false, iterations: maxIter, history, ...(refused ? { error: refused } : {}) });
|
|
532
550
|
}
|
|
533
551
|
|
|
534
552
|
// How far the terminal currents (as last assembled) are from adding up to zero, relative to the
|
package/src/solution.js
CHANGED
|
@@ -272,10 +272,16 @@ export function makeSolution(solver, result = {}) {
|
|
|
272
272
|
'advance() in time instead, or give the still species a small D where it is still.',
|
|
273
273
|
);
|
|
274
274
|
} else if (slow.length > 0) {
|
|
275
|
+
// Too slow to wait out only where the solve waited: where no time step was taken at all, the
|
|
276
|
+
// start was the trouble (a hole-only layer full of the anode's holes, with traps: "raise its
|
|
277
|
+
// rate constant" changed nothing, ×1e4).
|
|
278
|
+
const waited = (result.history ?? []).some((h) => h.converged && h.dt > 0);
|
|
275
279
|
for (const { where, affinity } of slow) {
|
|
276
280
|
sol.warnings.push(
|
|
277
281
|
`the solve did not converge, likely because ${where} is out of equilibrium by ${(affinity * solver.VT * 1e3).toPrecision(3)} mV of affinity, yet nothing else makes or uses what it changes, so a steady state needs it at equilibrium: ` +
|
|
278
|
-
|
|
282
|
+
(waited
|
|
283
|
+
? 'it is too slow for the steady solve to wait out. Raise its rate constant (the steady state does not depend on it), or advance() in time.'
|
|
284
|
+
: 'the solve failed from its first steps, so start it nearer that equilibrium: c0 near it, or solve without the reaction and set() it back.'),
|
|
279
285
|
);
|
|
280
286
|
}
|
|
281
287
|
} else if (result.converged === false && cond && cond.digits > 12) {
|
|
@@ -311,6 +317,11 @@ export function makeSolution(solver, result = {}) {
|
|
|
311
317
|
`the solve did not converge from a start far from equilibrium: ${solver.violent.join('; ')}, its double layer uncharged. ` +
|
|
312
318
|
'Hold that terminal at a voltage first, solve(), then let it float (set its I: 0): it keeps the state the held one gave it.',
|
|
313
319
|
);
|
|
320
|
+
} else if (result.converged === false && !result.stopped && !un && !ch && !lost && slow.length === 0 && !(cond && cond.digits > 12) && solver.modelError) {
|
|
321
|
+
sol.warnings.push(
|
|
322
|
+
`the solve did not converge: the states it reached were refused by a statistics model: ${solver.modelError}. ` +
|
|
323
|
+
'The model doesn\'t hold there: a smaller concentration or drive, or a model for it (lattice for crowding).',
|
|
324
|
+
);
|
|
314
325
|
} else if (result.converged === false && !result.stopped && !un && !ch && !lost && slow.length === 0 && !(cond && cond.digits > 12)) {
|
|
315
326
|
sol.warnings.push(
|
|
316
327
|
'the solve did not converge, and nothing specific showed why. ' +
|
package/src/solver.js
CHANGED
|
@@ -291,9 +291,15 @@ export class Solver {
|
|
|
291
291
|
this.termDX = this.terms.map(() => new Float64Array(this.terms.length));
|
|
292
292
|
this.termRes = new Float64Array(this.terms.length);
|
|
293
293
|
this._refreshSources();
|
|
294
|
-
// A floating terminal starts level with the first held one (no current)
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
// A floating terminal starts level with the first held one (no current); where none is held,
|
|
295
|
+
// with the source behind the first resistance (it was NaN there, and a metal contact's
|
|
296
|
+
// carrier, its level from that V, asked for a c0 a conductor can't be given), else 0.
|
|
297
|
+
const heldK = this.terms.findIndex((_, k) => !this.floating.includes(k)), behindR = this.terms.find((t) => t.drive.R > 0);
|
|
298
|
+
const level = heldK >= 0 ? this.termV[heldK] : behindR ? sourceAt(behindR.drive.src, this.time) : 0;
|
|
299
|
+
// (One behind a resistance too: at its own source, a silver cell held at 0 V and 16 V behind R
|
|
300
|
+
// began with 16 V across it; left at 0 V, one behind a −30 V source beside a current began
|
|
301
|
+
// with 30 V across it. Level, the circuit rows move them from a device at rest.)
|
|
302
|
+
for (const k of this.floating) this.termV[k] = level;
|
|
297
303
|
// Accepted steps, most recent first: start time, size, start state (for BDF2 and the
|
|
298
304
|
// error estimate of adaptive stepping).
|
|
299
305
|
this.history = [];
|
|
@@ -830,6 +836,7 @@ export class Solver {
|
|
|
830
836
|
}
|
|
831
837
|
this.termV.set(src.termV);
|
|
832
838
|
this.solvedV = src.solvedV;
|
|
839
|
+
this.coldStart = src.coldStart;
|
|
833
840
|
this.time = src.time;
|
|
834
841
|
this.computeConcentrations();
|
|
835
842
|
}
|
|
@@ -855,6 +862,7 @@ export class Solver {
|
|
|
855
862
|
history: this.history.slice(),
|
|
856
863
|
termV: Float64Array.from(this.termV),
|
|
857
864
|
sourceTime: this.sourceTime,
|
|
865
|
+
coldStart: this.coldStart,
|
|
858
866
|
};
|
|
859
867
|
}
|
|
860
868
|
|
|
@@ -878,6 +886,7 @@ export class Solver {
|
|
|
878
886
|
this.history = s.history.slice();
|
|
879
887
|
this.termV.set(s.termV);
|
|
880
888
|
this.sourceTime = s.sourceTime;
|
|
889
|
+
this.coldStart = s.coldStart;
|
|
881
890
|
this.computeConcentrations();
|
|
882
891
|
}
|
|
883
892
|
|