driftlet 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/docs/device.md +33 -3
- package/docs/kit.md +123 -0
- package/docs/numerics.md +6 -2
- package/docs/visualization.md +9 -0
- package/llms.txt +46 -11
- package/package.json +1 -1
- package/src/checks.js +268 -0
- package/src/describe.js +11 -6
- package/src/device.js +50 -3
- package/src/devices.js +135 -0
- package/src/index.js +11 -2
- package/src/kit.js +3 -0
- package/src/plot.js +3 -2
- package/src/recorder.js +89 -0
- package/src/solution.js +19 -1
- package/src/solver.js +209 -12
- package/src/stack.js +1 -1
- package/src/traces.js +23 -10
- package/src/types.js +4 -1
- package/types/checks.d.ts +59 -0
- package/types/devices.d.ts +77 -0
- package/types/index.d.ts +3 -2
- package/types/kit.d.ts +3 -0
- package/types/plot.d.ts +2 -1
- package/types/recorder.d.ts +27 -0
- package/types/solution.d.ts +1 -0
- package/types/solver.d.ts +18 -1
- package/types/stack.d.ts +1 -0
- package/types/traces.d.ts +7 -2
- package/types/types.d.ts +15 -0
package/src/recorder.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// A transient recorder, for driftlet/kit: advance() in pieces (a run to completion, or a little
|
|
2
|
+
// each animation frame), with the trace joined up across calls and whole solutions kept as
|
|
3
|
+
// frames, for scrubbing back through a run or plotting snapshots side by side.
|
|
4
|
+
|
|
5
|
+
import { DeviceError } from './errors.js';
|
|
6
|
+
|
|
7
|
+
const fail = (message) => {
|
|
8
|
+
throw new DeviceError(message);
|
|
9
|
+
};
|
|
10
|
+
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Record a device's transient. Frames are whole solutions: at the start, then at each of
|
|
14
|
+
* `times` (or every `every` seconds from the start), which `advance()` lands on exactly; with
|
|
15
|
+
* neither, at the end of each `advance()` call. The trace (`t`, `current`, `voltage`, and
|
|
16
|
+
* `probes` if given) runs from the start through every accepted step of every call.
|
|
17
|
+
* @param {import('./index.js').Device} device
|
|
18
|
+
* @param {{ every?: number, times?: number[], probes?: { x: number, species: string, quantity?: 'c' | 'V', region?: string | number }[] }} [opts]
|
|
19
|
+
*/
|
|
20
|
+
export function recorder(device, opts = {}) {
|
|
21
|
+
const { every, times, probes } = opts;
|
|
22
|
+
if (every !== undefined && times !== undefined) fail('recorder: give every or times, not both');
|
|
23
|
+
if (every !== undefined && !(every > 0 && Number.isFinite(every))) fail('recorder: every must be a time > 0 (s)');
|
|
24
|
+
if (times !== undefined && !(Array.isArray(times) && times.every(Number.isFinite))) fail('recorder: times must be an array of times (s)');
|
|
25
|
+
const first = device.solution();
|
|
26
|
+
const t0 = first.time;
|
|
27
|
+
const schedule = times ? [...times].filter((t) => t > t0).sort((a, b) => a - b) : null;
|
|
28
|
+
const scheduled = schedule !== null || every !== undefined;
|
|
29
|
+
let next = every !== undefined ? 1 : 0; // the next frame due: schedule[next], or t0 + next·every
|
|
30
|
+
const due = () => (schedule ? schedule[next] : every !== undefined ? t0 + next * every : undefined);
|
|
31
|
+
|
|
32
|
+
const trace = { t: [first.time], current: [first.current], voltage: [first.terminals.right.V - first.terminals.left.V] };
|
|
33
|
+
if (probes) trace.probes = device.solver._probes(probes).read().map((v) => [v]); // the start's readings
|
|
34
|
+
const frames = [first];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Advance to tEnd (s), stopping at each frame time on the way to keep a frame. Takes
|
|
38
|
+
* advance()'s options; with `budgetMs` it returns when that much wall time is spent, `done`
|
|
39
|
+
* saying whether tEnd was reached. Returns the latest solution (its own `trace` is only the
|
|
40
|
+
* last piece: the recorder's `trace` is the whole run).
|
|
41
|
+
* @param {number} tEnd
|
|
42
|
+
* @param {object} [aopts] as for Device.advance()
|
|
43
|
+
*/
|
|
44
|
+
function advance(tEnd, aopts = {}) {
|
|
45
|
+
const start = now(), budget = aopts.budgetMs ?? Infinity;
|
|
46
|
+
let sol;
|
|
47
|
+
for (;;) {
|
|
48
|
+
const stop = due();
|
|
49
|
+
const target = stop !== undefined && stop < tEnd ? stop : tEnd;
|
|
50
|
+
const left = budget - (now() - start);
|
|
51
|
+
sol = device.advance(target, { ...aopts, ...(probes ? { probes } : {}), budgetMs: left > 0 ? left : 0 });
|
|
52
|
+
const tr = sol.trace;
|
|
53
|
+
for (let k = 0; k < tr.t.length; k++) {
|
|
54
|
+
trace.t.push(tr.t[k]);
|
|
55
|
+
trace.current.push(tr.current[k]);
|
|
56
|
+
trace.voltage.push(tr.voltage[k]);
|
|
57
|
+
if (probes) tr.probes.forEach((p, j) => trace.probes[j].push(p[k]));
|
|
58
|
+
}
|
|
59
|
+
if (!sol.converged || !sol.done) break;
|
|
60
|
+
if (target === stop) {
|
|
61
|
+
frames.push(sol);
|
|
62
|
+
next++;
|
|
63
|
+
}
|
|
64
|
+
if (target === tEnd) break;
|
|
65
|
+
if (now() - start > budget) {
|
|
66
|
+
sol.done = false;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!scheduled && sol.converged && sol.time > frames.at(-1).time) frames.push(sol);
|
|
71
|
+
return sol;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The frame at or just before time t (the first frame before the start).
|
|
76
|
+
* @param {number} t
|
|
77
|
+
*/
|
|
78
|
+
function frame(t) {
|
|
79
|
+
let lo = 0, hi = frames.length - 1;
|
|
80
|
+
while (lo < hi) {
|
|
81
|
+
const mid = (lo + hi + 1) >> 1;
|
|
82
|
+
if (frames[mid].time <= t) lo = mid;
|
|
83
|
+
else hi = mid - 1;
|
|
84
|
+
}
|
|
85
|
+
return frames[lo];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { advance, frame, frames, trace };
|
|
89
|
+
}
|
package/src/solution.js
CHANGED
|
@@ -27,6 +27,7 @@ export function makeSolution(solver, result = {}) {
|
|
|
27
27
|
V: {},
|
|
28
28
|
Vstd: {},
|
|
29
29
|
time: solver.time,
|
|
30
|
+
steady: solver.atSteady,
|
|
30
31
|
converged: result.converged ?? true,
|
|
31
32
|
iterations: result.iterations ?? 0,
|
|
32
33
|
steps: result.steps,
|
|
@@ -121,7 +122,24 @@ export function makeSolution(solver, result = {}) {
|
|
|
121
122
|
const N = {};
|
|
122
123
|
for (let i = 0; i < n; i++) N[species[i].name] = u[b * M + 1 + i];
|
|
123
124
|
const rates = itf.reactions.map((_, k) => u[b * M + 1 + n + k]); // mol/(m²·s), forward
|
|
124
|
-
return { dipole: itf.dipole, sheetCharge: itf.sheetCharge, D: u[b * M], N, rates };
|
|
125
|
+
return { left: model.regions[f].name, right: model.regions[f + 1].name, dipole: itf.dipole, sheetCharge: itf.sheetCharge, D: u[b * M], N, rates };
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Bulk reactions: each one's forward rate at every node (mol/(m³·s), NaN where it doesn't run),
|
|
129
|
+
// and integrated over each region and the device (mol/(m²·s)), by the nodes' control volumes,
|
|
130
|
+
// just as the balances count it.
|
|
131
|
+
sol.bulkReactions = solver.rxs.map((rx) => {
|
|
132
|
+
const rate = new Float64Array(nNodes).fill(NaN), regions = model.regions.map(() => 0);
|
|
133
|
+
let total = 0;
|
|
134
|
+
for (let g = 0; g < nNodes; g++) {
|
|
135
|
+
const m = solver.nodeMaterial[g];
|
|
136
|
+
if (!(rx.kf[m] > 0)) continue;
|
|
137
|
+
rate[g] = solver.bulkRate(rx, g);
|
|
138
|
+
const amount = grid.vol[g] * rate[g];
|
|
139
|
+
regions[grid.nodeRegion[g]] += amount;
|
|
140
|
+
total += amount;
|
|
141
|
+
}
|
|
142
|
+
return { rate, regions, total };
|
|
125
143
|
});
|
|
126
144
|
|
|
127
145
|
// Total charge per area in the device (space charge plus sheet charges).
|
package/src/solver.js
CHANGED
|
@@ -321,6 +321,7 @@ export class Solver {
|
|
|
321
321
|
this.jR = new Float64Array(M);
|
|
322
322
|
this.time = 0;
|
|
323
323
|
this.lastDt = Infinity;
|
|
324
|
+
this.atSteady = false; // whether the state is a converged steady solve
|
|
324
325
|
// Contact bookkeeping, filled by assemble(): particle flux toward +x through each contact,
|
|
325
326
|
// and the displacement there (the metal's surface charge for a neutral link).
|
|
326
327
|
this.contactFlux = { left: new Float64Array(n), right: new Float64Array(n) };
|
|
@@ -362,6 +363,7 @@ export class Solver {
|
|
|
362
363
|
const { x: gx, segLength } = model.grid;
|
|
363
364
|
this.rxs = model.reactions.map((rx) => ({
|
|
364
365
|
kf: rx.kf,
|
|
366
|
+
srh: rx.srh, // per material: SRH kinetics in place of mass action, or null
|
|
365
367
|
kfNode: Float64Array.from(gx, (x, g) => {
|
|
366
368
|
const m = this.nodeMaterial[g], p = rx.kfProfile[m];
|
|
367
369
|
return p ? profileMean(p, x - (g > 0 ? segLength[g - 1] : 0) / 2, x + (g < segLength.length ? segLength[g] : 0) / 2) : rx.kf[m];
|
|
@@ -563,9 +565,63 @@ export class Solver {
|
|
|
563
565
|
// The row replaced: the basis vector's own stretch (its weight is 1, and no other vector has one).
|
|
564
566
|
add(parts, parts.find((p) => w[p.stretch] === 1)?.stretch ?? parts[0].stretch);
|
|
565
567
|
}
|
|
568
|
+
// A combination of immobile stretches (trap states: X⁰ + X⁻ under e⁻ + X⁰ = X⁻) is conserved
|
|
569
|
+
// node by node, since nothing carries it anywhere: at each node its weighted sum stays what
|
|
570
|
+
// it was, a row local to that node's block, in place of the balance row of one of them.
|
|
571
|
+
this.localConstraints = [];
|
|
572
|
+
const immobile = (st) => {
|
|
573
|
+
for (let q = st.regions[0]; q <= st.regions[1]; q++) if (materials[regions[q].material].D[st.species] > 0) return false;
|
|
574
|
+
return true;
|
|
575
|
+
};
|
|
576
|
+
for (const w of this.moieties) {
|
|
577
|
+
const parts = [...w.keys()].filter((k) => w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
578
|
+
const sts = parts.map((p) => this.stretches[p.stretch]);
|
|
579
|
+
if (sts.some((st) => st.conserved || !immobile(st) || conductor(st))) continue;
|
|
580
|
+
if (sts.some((st) => st.nodes[0] !== sts[0].nodes[0] || st.nodes[1] !== sts[0].nodes[1])) continue; // (side by side only)
|
|
581
|
+
for (const st of sts) st.conserved = true;
|
|
582
|
+
const row = parts.find((p) => p.w === 1) ?? parts[0];
|
|
583
|
+
this.localConstraints.push({
|
|
584
|
+
parts: parts.map((p) => ({ i: this.stretches[p.stretch].species, w: p.w })),
|
|
585
|
+
row: 1 + this.stretches[row.stretch].species,
|
|
586
|
+
nodes: sts[0].nodes,
|
|
587
|
+
reference: new Float64Array(sts[0].nodes[1] - sts[0].nodes[0] + 1),
|
|
588
|
+
});
|
|
589
|
+
}
|
|
566
590
|
this.constrained = false;
|
|
567
591
|
}
|
|
568
592
|
|
|
593
|
+
// Each local constraint's weighted sum at each node, as the state holds it now: what a steady
|
|
594
|
+
// solve keeps.
|
|
595
|
+
_captureLocal() {
|
|
596
|
+
const { n, c } = this;
|
|
597
|
+
for (const lc of this.localConstraints) {
|
|
598
|
+
for (let g = lc.nodes[0]; g <= lc.nodes[1]; g++) {
|
|
599
|
+
let t = 0;
|
|
600
|
+
for (const { i, w } of lc.parts) t += w * c[g * n + i];
|
|
601
|
+
lc.reference[g - lc.nodes[0]] = t;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// The local constraints' rows (steady solves only): Σ w c_i − reference at each node.
|
|
607
|
+
_applyLocalConstraints() {
|
|
608
|
+
const { n, M, c, res, dA: d } = this;
|
|
609
|
+
for (const lc of this.localConstraints) {
|
|
610
|
+
for (let g = lc.nodes[0]; g <= lc.nodes[1]; g++) {
|
|
611
|
+
const b = this.blockOfNode[g];
|
|
612
|
+
if (this.loc[b * M + lc.row] < 0) continue;
|
|
613
|
+
this._replaceRow(b, lc.row);
|
|
614
|
+
let t = 0;
|
|
615
|
+
for (const { i, w } of lc.parts) {
|
|
616
|
+
t += w * c[g * n + i];
|
|
617
|
+
this._dc(g, i, d);
|
|
618
|
+
for (let s = 0; s < M; s++) if (d[s] !== 0) this._j(b, lc.row, b, s, w * d[s]);
|
|
619
|
+
}
|
|
620
|
+
res[this.rix[b * M + lc.row]] = t - lc.reference[g - lc.nodes[0]];
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
569
625
|
// A basis of the conserved combinations of stretch amounts: the null space of the
|
|
570
626
|
// stoichiometry (a row per reaction, over the stretches it touches) together with a unit row
|
|
571
627
|
// for each stretch fed from outside. Exact integer data, so plain elimination will do.
|
|
@@ -747,6 +803,7 @@ export class Solver {
|
|
|
747
803
|
// After the terminals' drives change (Device.set): which are floating, and the held values. A
|
|
748
804
|
// change of source is a discontinuity, so time stepping restarts its order.
|
|
749
805
|
redrive() {
|
|
806
|
+
this.atSteady = false;
|
|
750
807
|
const before = this.floating;
|
|
751
808
|
this.floating = this.terms.flatMap((t, k) => (t.drive.kind === 'I' || t.drive.R > 0 ? [k] : []));
|
|
752
809
|
if (this.floating.length !== before.length) this.deltaV = null;
|
|
@@ -1115,6 +1172,7 @@ export class Solver {
|
|
|
1115
1172
|
// flux), then contacts, then the circuit rows of the floating ones.
|
|
1116
1173
|
this._terminals(dt);
|
|
1117
1174
|
if (this.constrained && dt === Infinity && this.constraints.length > 0) this._applyConstraints();
|
|
1175
|
+
if (this.constrained && dt === Infinity && this.localConstraints.length > 0) this._applyLocalConstraints();
|
|
1118
1176
|
this.transformed = this.combining && dt !== Infinity;
|
|
1119
1177
|
if (this.transformed) this._chargeRows(dt);
|
|
1120
1178
|
}
|
|
@@ -1304,6 +1362,26 @@ export class Solver {
|
|
|
1304
1362
|
const dr = this.dA;
|
|
1305
1363
|
for (let x = 0; x < rxs.length; x++) {
|
|
1306
1364
|
const rx = rxs[x];
|
|
1365
|
+
const law = rx.srh?.[this.nodeMaterial[g]];
|
|
1366
|
+
if (law) {
|
|
1367
|
+
// SRH: dr is ∂rate/∂slot directly, from its sensitivities to ln n, ln p and a.
|
|
1368
|
+
let aHi = rx.fixedA, aLo = 0;
|
|
1369
|
+
for (let p = 0; p < rx.sp.length; p++) {
|
|
1370
|
+
aHi += rx.nu[p] * u[b * M + 1 + rx.sp[p]];
|
|
1371
|
+
aLo += rx.nu[p] * uLo[b * M + 1 + rx.sp[p]];
|
|
1372
|
+
}
|
|
1373
|
+
const { rate, Cn, Cp, Ca } = this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], aHi + aLo);
|
|
1374
|
+
dr.fill(0);
|
|
1375
|
+
this._dlnc(g, law.n.i, Cn, dr);
|
|
1376
|
+
this._dlnc(g, law.p.i, Cp, dr);
|
|
1377
|
+
for (let p = 0; p < rx.sp.length; p++) dr[1 + rx.sp[p]] += Ca * rx.nu[p];
|
|
1378
|
+
for (let p = 0; p < rx.sp.length; p++) {
|
|
1379
|
+
const row = 1 + rx.sp[p], w = v * rx.nu[p];
|
|
1380
|
+
res[R[b * M + row]] += w * rate;
|
|
1381
|
+
for (let k = 0; k < M; k++) if (dr[k] !== 0) this._j(b, row, b, k, w * dr[k]);
|
|
1382
|
+
}
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1307
1385
|
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g], aHi = rx.fixedA, aLo = 0;
|
|
1308
1386
|
dr.fill(0); // ∂ ln P / ∂slot
|
|
1309
1387
|
// Participants: reactants with ν > 0, then products with ν < 0.
|
|
@@ -1639,6 +1717,53 @@ export class Solver {
|
|
|
1639
1717
|
d[0] = -this._Kz(g, k);
|
|
1640
1718
|
}
|
|
1641
1719
|
|
|
1720
|
+
// A bulk reaction's rate at node g (mol/(m³·s), forward), as assembled: for the solution.
|
|
1721
|
+
bulkRate(rx, g) {
|
|
1722
|
+
const { n, M, u, uLo, c } = this, b = this.blockOfNode[g];
|
|
1723
|
+
let aHi = rx.fixedA, aLo = 0;
|
|
1724
|
+
for (let p = 0; p < rx.sp.length; p++) {
|
|
1725
|
+
aHi += rx.nu[p] * u[b * M + 1 + rx.sp[p]];
|
|
1726
|
+
aLo += rx.nu[p] * uLo[b * M + 1 + rx.sp[p]];
|
|
1727
|
+
}
|
|
1728
|
+
const law = rx.srh?.[this.nodeMaterial[g]];
|
|
1729
|
+
if (law) return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], aHi + aLo).rate;
|
|
1730
|
+
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g];
|
|
1731
|
+
for (let p = 0; p < rx.sp.length; p++) if (rx.nu[p] > 0) P *= rx.nu[p] === 1 ? c[g * n + rx.sp[p]] : powi(c[g * n + rx.sp[p]], rx.nu[p]);
|
|
1732
|
+
return P * -Math.expm1(-(aHi + aLo));
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// The larger of a bulk reaction's two one-way rates at node g (mol/(m³·s)): the scale its net
|
|
1736
|
+
// rate is read against (in equilibrium the net rate is their round-off).
|
|
1737
|
+
bulkOneWay(rx, g) {
|
|
1738
|
+
const { n, M, u, uLo, c } = this, b = this.blockOfNode[g];
|
|
1739
|
+
let a = rx.fixedA;
|
|
1740
|
+
for (let p = 0; p < rx.sp.length; p++) a += rx.nu[p] * (u[b * M + 1 + rx.sp[p]] + uLo[b * M + 1 + rx.sp[p]]);
|
|
1741
|
+
const law = rx.srh?.[this.nodeMaterial[g]];
|
|
1742
|
+
if (law) return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], a).oneWay;
|
|
1743
|
+
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g];
|
|
1744
|
+
for (let p = 0; p < rx.sp.length; p++) if (rx.nu[p] > 0) P *= rx.nu[p] === 1 ? c[g * n + rx.sp[p]] : powi(c[g * n + rx.sp[p]], rx.nu[p]);
|
|
1745
|
+
return P * Math.max(1, Math.exp(-a));
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// SRH kinetics (see srhLaw in device.js): the rate, and its sensitivities to ln n, ln p and
|
|
1749
|
+
// a = A/RT. With no n₁ given, the trap is midgap: n₁ = p₁ = n_i = √(n p e^{−a}).
|
|
1750
|
+
_srhRate(law, cn, cp, a) {
|
|
1751
|
+
const { tn, tp } = law, lnnp = Math.log(cn) + Math.log(cp);
|
|
1752
|
+
let n1, kN = 0, kP = 0, kA = 0; // ln n₁ = kN ln n + kP ln p + kA a + const
|
|
1753
|
+
if (Number.isNaN(law.n1)) {
|
|
1754
|
+
n1 = Math.exp((lnnp - a) / 2);
|
|
1755
|
+
kN = kP = 0.5;
|
|
1756
|
+
kA = -0.5;
|
|
1757
|
+
} else n1 = law.n1;
|
|
1758
|
+
const p1 = Math.exp(lnnp - a - Math.log(n1)), np = cn * cp;
|
|
1759
|
+
const den = tp * (cn + n1) + tn * (cp + p1), f = -Math.expm1(-a);
|
|
1760
|
+
const rate = (np * f) / den;
|
|
1761
|
+
const dDn = tp * cn + tp * n1 * kN + tn * p1 * (1 - kN);
|
|
1762
|
+
const dDp = tp * n1 * kP + tn * cp + tn * p1 * (1 - kP);
|
|
1763
|
+
const dDa = tp * n1 * kA + tn * p1 * (-1 - kA);
|
|
1764
|
+
return { rate, oneWay: (np / den) * Math.max(1, Math.exp(-a)), Cn: rate * (1 - dDn / den), Cp: rate * (1 - dDp / den), Ca: (np / den) * (1 - f) - (rate * dDa) / den };
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1642
1767
|
// A face reaction (see normalizeFaceReactions). Its rate r_k is an unknown of the face block,
|
|
1643
1768
|
// with the row r_k − rate(u_L, u_R) = 0, and each participant's edge node takes ν·r_k (made
|
|
1644
1769
|
// there when ν > 0). The rate couples the two edge nodes only through the face block between
|
|
@@ -1650,24 +1775,34 @@ export class Solver {
|
|
|
1650
1775
|
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1];
|
|
1651
1776
|
const al = rx.alpha;
|
|
1652
1777
|
const dL = this.dA.fill(0), dR = this.dB.fill(0); // ∂ ln(prefactor)/∂slot, per side
|
|
1653
|
-
let pref = rx.k0, aHi = rx.fixedA, aLo = 0;
|
|
1778
|
+
let pref = rx.k0, aHi = rx.fixedA, aLo = 0, rate;
|
|
1654
1779
|
for (const p of rx.part) {
|
|
1655
1780
|
const g = p.side ? gR : gL, b = p.side ? bR : bL, o = b * M + 1 + p.i;
|
|
1656
1781
|
aHi -= p.nu * u[o];
|
|
1657
1782
|
aLo -= p.nu * uLo[o];
|
|
1658
|
-
if (this.nodeConductor[g] === p.i) continue; // a conductor's carrier has activity 1
|
|
1783
|
+
if (rx.srh || this.nodeConductor[g] === p.i) continue; // a conductor's carrier has activity 1
|
|
1659
1784
|
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
1660
1785
|
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
1661
1786
|
this._dlnc(g, p.i, e, p.side ? dR : dL);
|
|
1662
1787
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1788
|
+
if (rx.srh) {
|
|
1789
|
+
// SRH, with n and p each at its own side's edge node.
|
|
1790
|
+
const { n: pn, p: pp } = rx.srh, gn = pn.side ? gR : gL, gq = pp.side ? gR : gL;
|
|
1791
|
+
const s = this._srhRate(rx.srh, c[gn * n + pn.i], c[gq * n + pp.i], aHi + aLo);
|
|
1792
|
+
rate = s.rate;
|
|
1793
|
+
this._dlnc(gn, pn.i, s.Cn, pn.side ? dR : dL);
|
|
1794
|
+
this._dlnc(gq, pp.i, s.Cp, pp.side ? dR : dL);
|
|
1795
|
+
for (const p of rx.part) (p.side ? dR : dL)[1 + p.i] -= s.Ca * p.nu;
|
|
1796
|
+
} else {
|
|
1797
|
+
const { g: bv, gp } = bvFactor(aHi + aLo, al);
|
|
1798
|
+
rate = pref * bv;
|
|
1799
|
+
// ∂rate/∂slot = rate·∂ln(prefactor) + pref·g′·∂a, with ∂a/∂η_i = −ν
|
|
1800
|
+
for (let s = 0; s < M; s++) {
|
|
1801
|
+
dL[s] *= rate;
|
|
1802
|
+
dR[s] *= rate;
|
|
1803
|
+
}
|
|
1804
|
+
for (const p of rx.part) (p.side ? dR : dL)[1 + p.i] -= pref * gp * p.nu;
|
|
1669
1805
|
}
|
|
1670
|
-
for (const p of rx.part) (p.side ? dR : dL)[1 + p.i] -= pref * gp * p.nu;
|
|
1671
1806
|
const slot = 1 + n + k, o = bf * M + slot;
|
|
1672
1807
|
res[R[o]] = u[o] - rate;
|
|
1673
1808
|
this._j(bf, slot, bf, slot, 1);
|
|
@@ -2066,6 +2201,7 @@ export class Solver {
|
|
|
2066
2201
|
* the start-of-step values, and dt/a0 replaces dt.
|
|
2067
2202
|
*/
|
|
2068
2203
|
step(dt, opts = {}) {
|
|
2204
|
+
this.atSteady = false;
|
|
2069
2205
|
const prev = this.history[0];
|
|
2070
2206
|
const w = prev ? dt / prev.dt : 0;
|
|
2071
2207
|
const bdf = opts.method === 'bdf2' && prev !== undefined && w <= 2;
|
|
@@ -2148,6 +2284,9 @@ export class Solver {
|
|
|
2148
2284
|
let dt = this.dtNext ?? opts.dt0 ?? (tEnd - this.time) * 1e-4;
|
|
2149
2285
|
if (!(dt > 0)) dt = (tEnd - this.time) * 1e-4;
|
|
2150
2286
|
let grow = 0; // longer first steps tried after a Newton failure (see below)
|
|
2287
|
+
// The shortest step worth trying: round-off relative to the time, or the device's fastest
|
|
2288
|
+
// time scale, whichever is shorter (a cold start can need steps far below a long run's 1e-14).
|
|
2289
|
+
const floor = Math.min(1e-14 * Math.max(tEnd, 1e-300), this.fastestTime() * 1e-2);
|
|
2151
2290
|
const pred = new Float64Array(this.u.length), guess = new Float64Array(this.u.length);
|
|
2152
2291
|
const factor = (err, p) => (err > 0 ? Math.min(2, Math.max(0.2, 0.9 * Math.exp(Math.log(tol / err) / (p + 1)))) : 2);
|
|
2153
2292
|
while (this.time < tEnd) {
|
|
@@ -2204,7 +2343,7 @@ export class Solver {
|
|
|
2204
2343
|
grow = 3;
|
|
2205
2344
|
dt = h / 4;
|
|
2206
2345
|
}
|
|
2207
|
-
if (dt <
|
|
2346
|
+
if (dt < floor) { failed = true; break; }
|
|
2208
2347
|
continue;
|
|
2209
2348
|
}
|
|
2210
2349
|
} else {
|
|
@@ -2213,7 +2352,7 @@ export class Solver {
|
|
|
2213
2352
|
if (!r.converged) {
|
|
2214
2353
|
rejected++;
|
|
2215
2354
|
dt = h / 4;
|
|
2216
|
-
if (dt <
|
|
2355
|
+
if (dt < floor) { failed = true; break; }
|
|
2217
2356
|
continue;
|
|
2218
2357
|
}
|
|
2219
2358
|
p = this._predict(pred, r.bdf);
|
|
@@ -2357,6 +2496,39 @@ export class Solver {
|
|
|
2357
2496
|
return mx;
|
|
2358
2497
|
}
|
|
2359
2498
|
|
|
2499
|
+
// Start from another solver's state for the same device on another grid (a refined one): each
|
|
2500
|
+
// node's unknowns interpolated linearly in x within its region, each face's copied, and the
|
|
2501
|
+
// terminals' voltages. A warm start for a steady solve; nothing else is carried over.
|
|
2502
|
+
_warmFrom(src) {
|
|
2503
|
+
const { M, u, uLo } = this, grid = this.model.grid, sg = src.model.grid;
|
|
2504
|
+
if (src.M !== M || sg.regionStart.length !== grid.regionStart.length) throw new Error('_warmFrom: a different device');
|
|
2505
|
+
const value = (b, k) => src.u[b * M + k] + src.uLo[b * M + k];
|
|
2506
|
+
for (let r = 0; r < grid.regionStart.length; r++) {
|
|
2507
|
+
let a = sg.regionStart[r];
|
|
2508
|
+
for (let g = grid.regionStart[r]; g <= grid.regionEnd[r]; g++) {
|
|
2509
|
+
const x = grid.x[g];
|
|
2510
|
+
while (a + 1 < sg.regionEnd[r] && sg.x[a + 1] < x) a++;
|
|
2511
|
+
const b = Math.min(a + 1, sg.regionEnd[r]), h = sg.x[b] - sg.x[a];
|
|
2512
|
+
const w = h > 0 ? Math.min(1, Math.max(0, (x - sg.x[a]) / h)) : 0;
|
|
2513
|
+
const ba = src.blockOfNode[a], bb = src.blockOfNode[b], bg = this.blockOfNode[g];
|
|
2514
|
+
for (let k = 0; k < M; k++) {
|
|
2515
|
+
u[bg * M + k] = (1 - w) * value(ba, k) + w * value(bb, k);
|
|
2516
|
+
uLo[bg * M + k] = 0;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
for (let f = 0; f < this.nFaces; f++) {
|
|
2521
|
+
for (let k = 0; k < M; k++) {
|
|
2522
|
+
u[this.blockOfFace[f] * M + k] = value(src.blockOfFace[f], k);
|
|
2523
|
+
uLo[this.blockOfFace[f] * M + k] = 0;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
this.termV.set(src.termV);
|
|
2527
|
+
this.solvedV = src.solvedV;
|
|
2528
|
+
this.time = src.time;
|
|
2529
|
+
this.computeConcentrations();
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2360
2532
|
_snapshot() {
|
|
2361
2533
|
return {
|
|
2362
2534
|
u: Float64Array.from(this.u),
|
|
@@ -2364,6 +2536,7 @@ export class Solver {
|
|
|
2364
2536
|
cOld: Float64Array.from(this.cOld),
|
|
2365
2537
|
time: this.time,
|
|
2366
2538
|
lastDt: this.lastDt,
|
|
2539
|
+
atSteady: this.atSteady,
|
|
2367
2540
|
contactDStart: this.contactDStart,
|
|
2368
2541
|
contactDOld: this.contactDOld,
|
|
2369
2542
|
contactDEnd: this.contactDEnd,
|
|
@@ -2380,6 +2553,7 @@ export class Solver {
|
|
|
2380
2553
|
this.cOld.set(s.cOld);
|
|
2381
2554
|
this.time = s.time;
|
|
2382
2555
|
this.lastDt = s.lastDt;
|
|
2556
|
+
this.atSteady = s.atSteady;
|
|
2383
2557
|
this.contactDStart = s.contactDStart;
|
|
2384
2558
|
this.contactDOld = s.contactDOld;
|
|
2385
2559
|
this.contactDEnd = s.contactDEnd;
|
|
@@ -2734,6 +2908,17 @@ export class Solver {
|
|
|
2734
2908
|
return Number.isFinite(dMin) ? (L * L) / dMin : 1;
|
|
2735
2909
|
}
|
|
2736
2910
|
|
|
2911
|
+
// The fastest diffusion time: across the smallest cell, at the largest D.
|
|
2912
|
+
fastestTime() {
|
|
2913
|
+
let dMax = 0;
|
|
2914
|
+
for (const mat of this.model.materials) {
|
|
2915
|
+
for (let i = 0; i < this.n; i++) if (mat.present[i]) dMax = Math.max(dMax, mat.D[i]);
|
|
2916
|
+
}
|
|
2917
|
+
let h = Infinity;
|
|
2918
|
+
for (const s of this.model.grid.segLength) if (s > 0) h = Math.min(h, s);
|
|
2919
|
+
return dMax > 0 && Number.isFinite(h) ? (h * h) / dMax : this.slowestTime();
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2737
2922
|
/**
|
|
2738
2923
|
* Steady state.
|
|
2739
2924
|
* - If every species stretch is fed by a contact, nothing is conserved on its own, so the
|
|
@@ -2744,8 +2929,18 @@ export class Solver {
|
|
|
2744
2929
|
* If Newton fails, dt ramps up from a small value (pseudo-transient continuation) instead.
|
|
2745
2930
|
* The clock is not advanced, and open-system conservation bookkeeping restarts here.
|
|
2746
2931
|
*/
|
|
2932
|
+
// Steady state from the present one; `atSteady` says whether the state is one (until a step or
|
|
2933
|
+
// a change of drive).
|
|
2747
2934
|
solveSteady(opts = {}) {
|
|
2935
|
+
const r = this._steadyFromHere(opts);
|
|
2936
|
+
this.atSteady = r.converged;
|
|
2937
|
+
return r;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
_steadyFromHere(opts) {
|
|
2748
2941
|
this.conditioning = null;
|
|
2942
|
+
this.computeConcentrations();
|
|
2943
|
+
this._captureLocal(); // immobile combinations keep, node by node, what they hold now
|
|
2749
2944
|
this.sourceTime = this.time;
|
|
2750
2945
|
// Continuation applies where the right terminal is held at a voltage, against a held left one.
|
|
2751
2946
|
const [dl, dr] = [this.terms[0].drive, this.terms[1].drive];
|
|
@@ -2921,8 +3116,10 @@ export class Solver {
|
|
|
2921
3116
|
history.push({ dt, converged: r.converged, iterations: r.iterations, maxStep: 3 });
|
|
2922
3117
|
}
|
|
2923
3118
|
if (!r.converged) {
|
|
3119
|
+
// Pseudo-transient continuation: down from a slow time scale until a step converges,
|
|
3120
|
+
// as far as the fastest (slow ions mustn't stop it short of what a cold start needs).
|
|
2924
3121
|
dt = dt >= giant ? tau * 1e-6 : dt / 4;
|
|
2925
|
-
if (dt < tau * 1e-15) break;
|
|
3122
|
+
if (dt < Math.min(tau * 1e-15, this.fastestTime() * 1e-2)) break;
|
|
2926
3123
|
continue;
|
|
2927
3124
|
}
|
|
2928
3125
|
if (dt === Infinity) {
|
package/src/stack.js
CHANGED
|
@@ -59,7 +59,7 @@ export function layer(material, length, more = {}) {
|
|
|
59
59
|
|
|
60
60
|
// A drive: a number is a held voltage, a waveform { t, values } a voltage waveform, and an
|
|
61
61
|
// object with V, I or R is the drive itself.
|
|
62
|
-
function drive(d, path) {
|
|
62
|
+
export function drive(d, path) {
|
|
63
63
|
if (d === undefined) return { V: 0 };
|
|
64
64
|
if (typeof d === 'number') return { V: d };
|
|
65
65
|
if (isObject(d) && Array.isArray(d.t)) return { V: d };
|
package/src/traces.js
CHANGED
|
@@ -62,7 +62,11 @@ export function typeset(name, z) {
|
|
|
62
62
|
* its standard level V°_i (`standard`, the band edge for e⁻ and h⁺), optionally φ and
|
|
63
63
|
* half-reactions' levels. Doubled interface nodes share an x, so steps draw as vertical lines.
|
|
64
64
|
* @param {import('./types.js').Solution} sol
|
|
65
|
-
*
|
|
65
|
+
* With `energy: true` it's the familiar band diagram instead, energy up: electrons and holes as
|
|
66
|
+
* electron energies in eV (E = −V: E_c and E_v from the standard levels, E_Fn and E_Fp, or E_F
|
|
67
|
+
* where only electrons are present, from the levels), with any redox levels and φ (as −qφ) turned
|
|
68
|
+
* over too. Only electrons and holes have bands; ions are refused (draw them as species voltages).
|
|
69
|
+
* @param {{ species?: string[], standard?: boolean, phi?: boolean | string[], energy?: boolean,
|
|
66
70
|
* levels?: { half: { equation: string, fixed?: Record<string, number> }, label?: string, standard?: boolean }[],
|
|
67
71
|
* labels?: Record<string, string>, shifts?: Record<string, number> }} [opts]
|
|
68
72
|
* species to show (default: every charged one), whether to show standard levels (default true)
|
|
@@ -73,31 +77,40 @@ export function typeset(name, z) {
|
|
|
73
77
|
* @returns {{ x: Float64Array, series: Trace[], regions: { name: string, material: string, x0: number, x1: number }[],
|
|
74
78
|
* faces: number[], range: [number, number] }}
|
|
75
79
|
*/
|
|
76
|
-
export function traces(sol, { species, standard = true, phi = false, levels = [], labels = {}, shifts = {} } = {}) {
|
|
80
|
+
export function traces(sol, { species, standard = true, phi = false, energy = false, levels = [], labels = {}, shifts = {} } = {}) {
|
|
77
81
|
const names = Object.keys(sol.V);
|
|
78
82
|
const role = (name) => speciesRole(sol, name).role;
|
|
79
83
|
const slotOf = (name) => speciesRole(sol, name).slot;
|
|
80
|
-
const
|
|
84
|
+
const carriers = ['e-', 'h+'];
|
|
85
|
+
const shown = species ?? names.filter((name) => finite(sol.V[name]) && (!energy || carriers.includes(name)));
|
|
86
|
+
if (energy) {
|
|
87
|
+
for (const name of shown) {
|
|
88
|
+
if (!carriers.includes(name)) throw new Error(`traces: '${name}' has no band to draw as an energy; draw it as a species voltage (energy: false)`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const sign = energy ? -1 : 1, unit = energy ? 'eV' : 'V';
|
|
92
|
+
const bipolar = names.includes('h+');
|
|
93
|
+
const energyLabels = { 'V:e-': bipolar ? 'E_{Fn}' : 'E_F', 'Vstd:e-': 'E_c', 'V:h+': 'E_{Fp}', 'Vstd:h+': 'E_v' };
|
|
81
94
|
const zOf = Object.fromEntries(sol.species.map((sp) => [sp.name, sp.z]));
|
|
82
95
|
const pretty = (name) => (name in zOf ? typeset(name, zOf[name]) : name);
|
|
83
96
|
const series = [];
|
|
84
97
|
for (const name of shown) {
|
|
85
98
|
if (!names.includes(name)) throw new Error(`traces: no charged species '${name}' in the solution`);
|
|
86
99
|
const shift = shifts[name] ?? 0;
|
|
87
|
-
const at = (y) => (shift ?
|
|
100
|
+
const at = (y) => (shift || energy ? Array.from(y, (v) => sign * v + shift) : y);
|
|
88
101
|
const common = { species: name, role: role(name), slot: slotOf(name), ...(shift ? { shift } : {}) };
|
|
89
|
-
if (finite(sol.V[name])) series.push({ id: `V:${name}`, label: `V_{${pretty(name)}}`, kind: 'level', ...common, y: at(sol.V[name]) });
|
|
90
|
-
if (standard && finite(sol.Vstd[name])) series.push({ id: `Vstd:${name}`, label: `V°_{${pretty(name)}}`, kind: 'standard', ...common, y: at(sol.Vstd[name]) });
|
|
102
|
+
if (finite(sol.V[name])) series.push({ id: `V:${name}`, label: energy ? energyLabels[`V:${name}`] : `V_{${pretty(name)}}`, kind: 'level', ...common, y: at(sol.V[name]) });
|
|
103
|
+
if (standard && finite(sol.Vstd[name])) series.push({ id: `Vstd:${name}`, label: energy ? energyLabels[`Vstd:${name}`] : `V°_{${pretty(name)}}`, kind: 'standard', ...common, y: at(sol.Vstd[name]) });
|
|
91
104
|
}
|
|
92
105
|
if (Array.isArray(phi)) {
|
|
93
106
|
const known = sol.regions.flatMap((r) => [r.name, r.material]);
|
|
94
107
|
for (const name of phi) if (!known.includes(name)) throw new Error(`traces: no region or material '${name}' to draw φ in (${[...new Set(known)].join(', ')})`);
|
|
95
108
|
const inside = sol.regions.map((r) => phi.includes(r.name) || phi.includes(r.material));
|
|
96
|
-
series.push({ id: 'phi', label: 'φ', kind: 'phi', role: 'phi', slot: 0, y: Array.from(sol.phi, (v, g) => (inside[sol.region[g]] ? v : NaN)) });
|
|
97
|
-
} else if (phi) series.push({ id: 'phi', label: 'φ', kind: 'phi', role: 'phi', slot: 0, y: sol.phi });
|
|
109
|
+
series.push({ id: 'phi', label: energy ? '−qφ' : 'φ', kind: 'phi', role: 'phi', slot: 0, y: Array.from(sol.phi, (v, g) => (inside[sol.region[g]] ? sign * v : NaN)) });
|
|
110
|
+
} else if (phi) series.push({ id: 'phi', label: energy ? '−qφ' : 'φ', kind: 'phi', role: 'phi', slot: 0, y: energy ? Array.from(sol.phi, (v) => -v) : sol.phi });
|
|
98
111
|
const couples = [...new Set(levels.map((lv) => lv.half.equation))];
|
|
99
112
|
levels.forEach((lv, k) => {
|
|
100
|
-
const y = level(sol, lv.half, { standard: lv.standard });
|
|
113
|
+
const y = energy ? Array.from(level(sol, lv.half, { standard: lv.standard }), (v) => -v) : level(sol, lv.half, { standard: lv.standard });
|
|
101
114
|
series.push({
|
|
102
115
|
id: `level:${k}`,
|
|
103
116
|
label: lv.label ?? `${lv.standard ? 'V° ' : ''}${lv.half.equation.split(' ').map(pretty).join(' ')}`,
|
|
@@ -109,7 +122,7 @@ export function traces(sol, { species, standard = true, phi = false, levels = []
|
|
|
109
122
|
});
|
|
110
123
|
for (const s of series) {
|
|
111
124
|
if (labels[s.id] !== undefined) s.label = labels[s.id];
|
|
112
|
-
if (s.shift) s.label += ` ⌇${s.shift > 0 ? '+' : '−'}${Math.abs(s.shift)}
|
|
125
|
+
if (s.shift) s.label += ` ⌇${s.shift > 0 ? '+' : '−'}${Math.abs(s.shift)} ${unit}`;
|
|
113
126
|
}
|
|
114
127
|
let lo = Infinity, hi = -Infinity;
|
|
115
128
|
for (const s of series) {
|
package/src/types.js
CHANGED
|
@@ -201,7 +201,9 @@
|
|
|
201
201
|
* @property {{ left?: { V: number, D: number, charge: number }, right?: { V: number, D: number, charge: number } }} gates
|
|
202
202
|
* @property {{ name: string, V: number, flux: Record<string, number>, current: number }[]} ports what each
|
|
203
203
|
* internal port brings into the device
|
|
204
|
-
* @property {{ dipole: number, sheetCharge: number, D: number, N: Record<string, number>, rates: number[] }[]} interfaces
|
|
204
|
+
* @property {{ left: string, right: string, dipole: number, sheetCharge: number, D: number, N: Record<string, number>, rates: number[] }[]} interfaces
|
|
205
|
+
* @property {{ rate: Float64Array, regions: number[], total: number }[]} bulkReactions each bulk reaction's
|
|
206
|
+
* forward rate per node (mol/(m³·s), NaN where it doesn't run), and per region and in all (mol/(m²·s))
|
|
205
207
|
* @property {number} charge total charge in the device, C/m²
|
|
206
208
|
* @property {{ species: string, regions: number[], spectator: boolean, connected: boolean, reactive: boolean,
|
|
207
209
|
* amount: number, reference: number, intake: number, drift: number }[]} conservation
|
|
@@ -209,6 +211,7 @@
|
|
|
209
211
|
* @property {boolean} converged
|
|
210
212
|
* @property {number} iterations
|
|
211
213
|
* @property {number} time s
|
|
214
|
+
* @property {boolean} steady whether this is a converged steady state (solve()), not a transient's
|
|
212
215
|
* @property {boolean} [done] advance(): whether tEnd was reached (false if the frame budget ran out)
|
|
213
216
|
* @property {number} [rejected] advance(): steps rejected by error control or Newton failure
|
|
214
217
|
* @property {{ t: number[], current: number[], voltage: number[], probes?: number[][] }} [trace] advance():
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} CheckItem
|
|
3
|
+
* @property {string} name what was checked: 'converged', 'warnings', 'balance', 'conservation' or 'grid'
|
|
4
|
+
* @property {boolean | null} ok whether it passed (null: look at it, a warning or a check that
|
|
5
|
+
* couldn't be made)
|
|
6
|
+
* @property {string} summary one line
|
|
7
|
+
* @property {object} [details] the numbers behind it
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Check a device's present state (pass the solution solve() or advance() returned, for its
|
|
11
|
+
* convergence and warnings):
|
|
12
|
+
* - **converged**: the last solve converged.
|
|
13
|
+
* - **warnings**: the solution's warnings (unresolved double layers, steep profiles, weakly held
|
|
14
|
+
* parts) and the definition's likely unit slips.
|
|
15
|
+
* - **balance** (a steady state): each species' ledger, what comes in through each terminal and
|
|
16
|
+
* what each reaction (bulk or at a face) makes or uses, in mol/(m²·s) and, for a charged
|
|
17
|
+
* species, as a current F·|z|·rate. The terms must sum to zero; under light it's J = F(G − R),
|
|
18
|
+
* and it says where every carrier went.
|
|
19
|
+
* - **conservation** (a transient): every closed, unreacting stretch keeps what it held plus
|
|
20
|
+
* what came in.
|
|
21
|
+
* - **grid** (a steady state, with `refine`, the default): the device solved again on a grid
|
|
22
|
+
* twice as fine everywhere, from this solution interpolated onto it. The current, each floating terminal's voltage and each
|
|
23
|
+
* region's charge must agree to `tol` (relative, default 1e-2, a plot's accuracy; a benchmark
|
|
24
|
+
* wants less). The discretisation is second order, so the change estimates this grid's error
|
|
25
|
+
* (about 3/4 of it).
|
|
26
|
+
* @param {import('./index.js').Device} device
|
|
27
|
+
* @param {object} [sol] the solution to check (default: the device's present state)
|
|
28
|
+
* @param {{ refine?: boolean, tol?: number }} [opts]
|
|
29
|
+
* Every check passed when `ok`; an item with `ok: null` (a warning, or a check that couldn't be
|
|
30
|
+
* made) asks you to look, and doesn't fail it. `text` is a line per check, marked ok, FAIL or ?.
|
|
31
|
+
* @returns {{ ok: boolean, items: CheckItem[], text: string }}
|
|
32
|
+
*/
|
|
33
|
+
export function check(device: import("./index.js").Device, sol?: object, { refine, tol }?: {
|
|
34
|
+
refine?: boolean;
|
|
35
|
+
tol?: number;
|
|
36
|
+
}): {
|
|
37
|
+
ok: boolean;
|
|
38
|
+
items: CheckItem[];
|
|
39
|
+
text: string;
|
|
40
|
+
};
|
|
41
|
+
export type CheckItem = {
|
|
42
|
+
/**
|
|
43
|
+
* what was checked: 'converged', 'warnings', 'balance', 'conservation' or 'grid'
|
|
44
|
+
*/
|
|
45
|
+
name: string;
|
|
46
|
+
/**
|
|
47
|
+
* whether it passed (null: look at it, a warning or a check that
|
|
48
|
+
* couldn't be made)
|
|
49
|
+
*/
|
|
50
|
+
ok: boolean | null;
|
|
51
|
+
/**
|
|
52
|
+
* one line
|
|
53
|
+
*/
|
|
54
|
+
summary: string;
|
|
55
|
+
/**
|
|
56
|
+
* the numbers behind it
|
|
57
|
+
*/
|
|
58
|
+
details?: object;
|
|
59
|
+
};
|