driftlet 0.5.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 +6 -4
- package/docs/device.md +2 -0
- package/docs/kit.md +117 -0
- package/llms.txt +36 -9
- package/package.json +1 -1
- package/src/checks.js +268 -0
- package/src/describe.js +9 -5
- package/src/devices.js +135 -0
- package/src/kit.js +3 -0
- package/src/recorder.js +89 -0
- package/src/solution.js +18 -0
- package/src/solver.js +75 -1
- package/src/stack.js +1 -1
- package/src/types.js +4 -1
- package/types/checks.d.ts +59 -0
- package/types/devices.d.ts +77 -0
- package/types/kit.d.ts +3 -0
- package/types/recorder.d.ts +27 -0
- package/types/solution.d.ts +1 -0
- package/types/solver.d.ts +8 -1
- package/types/stack.d.ts +1 -0
- package/types/types.d.ts +15 -0
package/src/devices.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Curated devices, for driftlet/kit: whole definitions from a published parameter set (light and
|
|
2
|
+
// contacts included), with reference results from an independent code to check a run against.
|
|
3
|
+
// Each returns ordinary definition data, to print, edit and pass to new Device.
|
|
4
|
+
|
|
5
|
+
import { AVOGADRO, FARADAY, GAS_CONSTANT, units } from './constants.js';
|
|
6
|
+
import { photogeneration } from './sources.js';
|
|
7
|
+
import { drive } from './stack.js';
|
|
8
|
+
|
|
9
|
+
const m3 = (x) => x / AVOGADRO, eV = units.eV;
|
|
10
|
+
|
|
11
|
+
// IonMonger's default parameters (parameters_template.m, IonMonger 2.0): Courtier, Cave, Walker,
|
|
12
|
+
// Richardson & Foster, J. Comput. Electron. 18, 1435 (2019). Levels in eV on the vacuum scale,
|
|
13
|
+
// densities per m³, D in m²/s, lifetimes in s, recombination velocities in m/s.
|
|
14
|
+
const P = {
|
|
15
|
+
T: 298, b: 400e-9, eps: 24.1, alpha: 1.3e7, Ec: -3.7, Ev: -5.4, Dn: 1.7e-4, Dp: 1.7e-4, gc: 8.1e24, gv: 5.8e24,
|
|
16
|
+
N0: 1.6e25, DIinf: 6.5e-8, EAI: 0.58, // vacancy D = DIinf·exp(−EAI/kT), ≈ 1.0e-17 m²/s at 298 K
|
|
17
|
+
dE: 1e24, gcE: 5e25, EcE: -4.0, bE: 100e-9, epsE: 10, DE: 1e-5,
|
|
18
|
+
dH: 1e24, gvH: 5e25, EvH: -5.1, bH: 200e-9, epsH: 3, DH: 1e-6,
|
|
19
|
+
tn: 3e-9, tp: 3e-7, vnE: 1e5, vpE: 10, vnH: 0.1, vpH: 1e5, Fph: 1.4e21,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* IonMonger's default planar perovskite solar cell: TiO₂ (100 nm) | MAPbI₃ (400 nm) with mobile
|
|
24
|
+
* iodide vacancies over an equal immobile background | spiro-OMeTAD (200 nm), at 298 K, lit
|
|
25
|
+
* through the TiO₂. Species e⁻, h⁺ and the vacancies 'V+'. Recombination is SRH at both faces
|
|
26
|
+
* (IonMonger's interface law is driftlet's face SRH, with n₁ from the TiO₂'s electron density)
|
|
27
|
+
* and, with `bulkSRH`, SRH through midgap traps in the perovskite (τₙ = 3 ns, τₚ = 300 ns).
|
|
28
|
+
* Light is 1.4e21 photons/(m²·s) at α = 1.3e7 /m (IonMonger's one sun), times `light`. The left
|
|
29
|
+
* contact is the TiO₂'s, at 0 V; the right is the spiro's, at `V`, so photocurrent is positive.
|
|
30
|
+
*
|
|
31
|
+
* `PEROVSKITE_SCANS` holds IonMonger's own results for this cell, scan by scan.
|
|
32
|
+
* @param {{ V?: import('./types.js').Source | { V?: import('./types.js').Source, I?: import('./types.js').Source, R?: number }, light?: number, bulkSRH?: boolean }} [opts]
|
|
33
|
+
* @returns {import('./types.js').DeviceDefinition}
|
|
34
|
+
*/
|
|
35
|
+
export function perovskiteCell({ V = 1.2, light = 1, bulkSRH = true } = {}) {
|
|
36
|
+
const { T } = P, VT = (GAS_CONSTANT * T) / FARADAY;
|
|
37
|
+
const ni = Math.sqrt(P.gc * P.gv) * Math.exp(-(P.Ec - P.Ev) / (2 * VT));
|
|
38
|
+
const n0 = P.gc * Math.exp((P.EcE + VT * Math.log(P.dE / P.gcE) - P.Ec) / VT); // the perovskite's n at the TiO₂'s Fermi level
|
|
39
|
+
const DI = P.DIinf * Math.exp(-P.EAI / VT);
|
|
40
|
+
return {
|
|
41
|
+
T,
|
|
42
|
+
species: [
|
|
43
|
+
{ name: 'e-', z: -1 },
|
|
44
|
+
{ name: 'h+', z: 1 },
|
|
45
|
+
{ name: 'V+', z: 1 }, // iodide vacancies
|
|
46
|
+
],
|
|
47
|
+
materials: {
|
|
48
|
+
// Levels on the vacuum scale: μ° is the band edge's depth (minus it, for electrons).
|
|
49
|
+
'TiO₂': { epsr: P.epsE, species: { 'e-': { D: P.DE, mu0: eV(P.EcE), cRef: m3(P.gcE) } } },
|
|
50
|
+
'MAPbI₃': {
|
|
51
|
+
epsr: P.eps,
|
|
52
|
+
species: {
|
|
53
|
+
'e-': { D: P.Dn, mu0: eV(P.Ec), cRef: m3(P.gc) },
|
|
54
|
+
'h+': { D: P.Dp, mu0: eV(-P.Ev), cRef: m3(P.gv) },
|
|
55
|
+
'V+': { D: DI, mu0: 0, cRef: m3(P.N0) },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
'spiro-OMeTAD': { epsr: P.epsH, species: { 'h+': { D: P.DH, mu0: eV(-P.EvH), cRef: m3(P.gvH) } } },
|
|
59
|
+
},
|
|
60
|
+
regions: [
|
|
61
|
+
{ material: 'TiO₂', length: P.bE, fixedCharge: FARADAY * m3(P.dE) },
|
|
62
|
+
{ material: 'MAPbI₃', length: P.b, fixedCharge: -FARADAY * m3(P.N0), c0: { 'V+': m3(P.N0) } },
|
|
63
|
+
{ material: 'spiro-OMeTAD', length: P.bH, fixedCharge: -FARADAY * m3(P.dH) },
|
|
64
|
+
],
|
|
65
|
+
interfaces: [
|
|
66
|
+
// Recombination through interface states (SRH), saturating at one carrier's capture: holes
|
|
67
|
+
// reaching the TiO₂ recombine at v_p however many electrons wait there.
|
|
68
|
+
{ dipole: 0, species: { 'e-': 'equilibrium' }, reactions: [{ equation: 'e-(left) + h+ = 0', srh: { vn: P.vnE, vp: P.vpE, n1: m3((P.dE / n0) * ni) } }] },
|
|
69
|
+
{ dipole: 0, species: { 'h+': 'equilibrium' }, reactions: [{ equation: 'e- + h+(right) = 0', srh: { vn: P.vnH, vp: P.vpH, n1: m3(ni) } }] },
|
|
70
|
+
],
|
|
71
|
+
contacts: {
|
|
72
|
+
left: { V: 0, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'bulk' },
|
|
73
|
+
right: { ...drive(V, 'perovskiteCell: V'), terminal: 'h+', species: { 'h+': 'equilibrium' }, phi: 'bulk' },
|
|
74
|
+
},
|
|
75
|
+
bulkReactions: [
|
|
76
|
+
...(bulkSRH ? [{ equation: 'e- + h+ = 0', srh: { 'MAPbI₃': { tauN: P.tn, tauP: P.tp } } }] : []),
|
|
77
|
+
...(light > 0 ? [photogeneration({ material: 'MAPbI₃', flux: m3(P.Fph * light), alpha: P.alpha, mu: eV(3), from: P.bE, to: P.bE + P.b })] : []),
|
|
78
|
+
],
|
|
79
|
+
grid: { hmin: 0.05e-9, hmax: 5e-9, ratio: 1.15 },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* IonMonger's J–V scans of `perovskiteCell()` (IonMonger 2.0, commit 1ae4f9e, run in GNU Octave):
|
|
85
|
+
* steady state at 1.2 V under light, then 1.2 → 0 → 1.2 V at `rate` (V/s). `full` is the cell
|
|
86
|
+
* as given (bulk SRH on); `noBulkSRH` is `perovskiteCell({ bulkSRH: false })`. Per scan: J0, the
|
|
87
|
+
* current at 0 V (A/m²); each sweep's maximum power Pmax (W/m², the largest V·J among IonMonger's
|
|
88
|
+
* 201 output points per loop) and Voc (V, interpolated); and the hysteresis index
|
|
89
|
+
* hi = (Pmax,rev − Pmax,fwd)/Pmax,rev. `hysteresis()` reads the same from a run's trace.
|
|
90
|
+
* driftlet reproduces these to 0.15 % in hi, 0.5 W/m² in Pmax and 2 mV in Voc (its test suite).
|
|
91
|
+
*/
|
|
92
|
+
export const PEROVSKITE_SCANS = Object.freeze({
|
|
93
|
+
source: 'IonMonger 2.0 (github.com/PerovskiteSCModelling/IonMonger, commit 1ae4f9e), default parameters',
|
|
94
|
+
full: [
|
|
95
|
+
{ rate: 0.001, J0: 220.66, rev: { Pmax: 201.95, Voc: 1.09296 }, fwd: { Pmax: 200.42, Voc: 1.08517 }, hi: 0.00756 },
|
|
96
|
+
{ rate: 0.01, J0: 220.89, rev: { Pmax: 210.97, Voc: 1.11157 }, fwd: { Pmax: 195.18, Voc: 1.06566 }, hi: 0.07483 },
|
|
97
|
+
{ rate: 0.1, J0: 222.28, rev: { Pmax: 216.46, Voc: 1.11698 }, fwd: { Pmax: 144.85, Voc: 1.01277 }, hi: 0.33083 },
|
|
98
|
+
{ rate: 1, J0: 223.01, rev: { Pmax: 217.02, Voc: 1.11712 }, fwd: { Pmax: 210.91, Voc: 1.08238 }, hi: 0.02815 },
|
|
99
|
+
{ rate: 10, J0: 223.01, rev: { Pmax: 217.04, Voc: 1.11708 }, fwd: { Pmax: 216.55, Voc: 1.11509 }, hi: 0.00225 },
|
|
100
|
+
{ rate: 100, J0: 223.06, rev: { Pmax: 217.09, Voc: 1.11708 }, fwd: { Pmax: 216.93, Voc: 1.11693 }, hi: 0.0007 },
|
|
101
|
+
{ rate: 1000, J0: 223.54, rev: { Pmax: 217.58, Voc: 1.11714 }, fwd: { Pmax: 216.48, Voc: 1.11699 }, hi: 0.00505 },
|
|
102
|
+
],
|
|
103
|
+
noBulkSRH: [
|
|
104
|
+
{ rate: 0.001, J0: 220.96, rev: { Pmax: 203.06, Voc: 1.0957 }, fwd: { Pmax: 201.54, Voc: 1.08762 }, hi: 0.00751 },
|
|
105
|
+
{ rate: 0.01, J0: 221.17, rev: { Pmax: 214.35, Voc: 1.1168 }, fwd: { Pmax: 196.01, Voc: 1.06737 }, hi: 0.08556 },
|
|
106
|
+
{ rate: 0.1, J0: 222.43, rev: { Pmax: 221.67, Voc: 1.12199 }, fwd: { Pmax: 145.73, Voc: 1.01331 }, hi: 0.3426 },
|
|
107
|
+
{ rate: 1, J0: 223.02, rev: { Pmax: 222.39, Voc: 1.12257 }, fwd: { Pmax: 213.41, Voc: 1.08376 }, hi: 0.04038 },
|
|
108
|
+
{ rate: 10, J0: 223.02, rev: { Pmax: 222.54, Voc: 1.12263 }, fwd: { Pmax: 221.77, Voc: 1.11951 }, hi: 0.00348 },
|
|
109
|
+
{ rate: 100, J0: 223.07, rev: { Pmax: 222.61, Voc: 1.12264 }, fwd: { Pmax: 222.38, Voc: 1.1223 }, hi: 0.00103 },
|
|
110
|
+
{ rate: 1000, J0: 223.55, rev: { Pmax: 223.12, Voc: 1.12271 }, fwd: { Pmax: 221.98, Voc: 1.12252 }, hi: 0.00513 },
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A J–V loop's figures of merit, from a transient's trace (`{ t, voltage, current }`, as a
|
|
116
|
+
* recorder or advance() gives it): the loop splits at time `turn` into the sweep before (`rev`)
|
|
117
|
+
* and after (`fwd`). Each sweep's maximum power V·I (W/m², the largest among the trace's points)
|
|
118
|
+
* and its open-circuit voltage (where I changes sign, interpolated linearly); and the
|
|
119
|
+
* hysteresis index (Pmax,rev − Pmax,fwd)/Pmax,rev. Power out is positive, so a cell whose
|
|
120
|
+
* current runs the other way should have it negated first.
|
|
121
|
+
* @param {{ t: ArrayLike<number>, voltage: ArrayLike<number>, current: ArrayLike<number> }} trace
|
|
122
|
+
* @param {number} turn s
|
|
123
|
+
*/
|
|
124
|
+
export function hysteresis(trace, turn) {
|
|
125
|
+
const out = {};
|
|
126
|
+
for (const [name, on] of [['rev', (t) => t <= turn], ['fwd', (t) => t >= turn]]) {
|
|
127
|
+
const pts = [];
|
|
128
|
+
for (let k = 0; k < trace.t.length; k++) if (on(trace.t[k]) && Number.isFinite(trace.current[k])) pts.push([trace.voltage[k], trace.current[k]]);
|
|
129
|
+
pts.sort((a, b) => a[0] - b[0]);
|
|
130
|
+
const k = pts.findIndex(([, I], j) => j > 0 && pts[j - 1][1] > 0 && I <= 0);
|
|
131
|
+
const Voc = k > 0 ? pts[k - 1][0] + (pts[k - 1][1] * (pts[k][0] - pts[k - 1][0])) / (pts[k - 1][1] - pts[k][1]) : NaN;
|
|
132
|
+
out[name] = { Pmax: Math.max(...pts.map(([V, I]) => V * I)), Voc };
|
|
133
|
+
}
|
|
134
|
+
return { ...out, hi: (out.rev.Pmax - out.fwd.Pmax) / out.rev.Pmax };
|
|
135
|
+
}
|
package/src/kit.js
CHANGED
|
@@ -11,6 +11,9 @@ export { traces, speciesRole, typeset } from './traces.js';
|
|
|
11
11
|
export { live } from './live.js';
|
|
12
12
|
export { pulse, square, triangle, ramp, injector, recombination, photogeneration } from './sources.js';
|
|
13
13
|
export { describe, unitWarnings } from './describe.js';
|
|
14
|
+
export { recorder } from './recorder.js';
|
|
15
|
+
export { check } from './checks.js';
|
|
16
|
+
export { perovskiteCell, PEROVSKITE_SCANS, hysteresis } from './devices.js';
|
|
14
17
|
export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal } from './data.js';
|
|
15
18
|
|
|
16
19
|
const fail = (message) => {
|
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,
|
|
@@ -124,6 +125,23 @@ export function makeSolution(solver, result = {}) {
|
|
|
124
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 };
|
|
125
126
|
});
|
|
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 };
|
|
143
|
+
});
|
|
144
|
+
|
|
127
145
|
// Total charge per area in the device (space charge plus sheet charges).
|
|
128
146
|
let q = 0;
|
|
129
147
|
for (let g = 0; g < nNodes; g++) {
|
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) };
|
|
@@ -802,6 +803,7 @@ export class Solver {
|
|
|
802
803
|
// After the terminals' drives change (Device.set): which are floating, and the held values. A
|
|
803
804
|
// change of source is a discontinuity, so time stepping restarts its order.
|
|
804
805
|
redrive() {
|
|
806
|
+
this.atSteady = false;
|
|
805
807
|
const before = this.floating;
|
|
806
808
|
this.floating = this.terms.flatMap((t, k) => (t.drive.kind === 'I' || t.drive.R > 0 ? [k] : []));
|
|
807
809
|
if (this.floating.length !== before.length) this.deltaV = null;
|
|
@@ -1715,6 +1717,34 @@ export class Solver {
|
|
|
1715
1717
|
d[0] = -this._Kz(g, k);
|
|
1716
1718
|
}
|
|
1717
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
|
+
|
|
1718
1748
|
// SRH kinetics (see srhLaw in device.js): the rate, and its sensitivities to ln n, ln p and
|
|
1719
1749
|
// a = A/RT. With no n₁ given, the trap is midgap: n₁ = p₁ = n_i = √(n p e^{−a}).
|
|
1720
1750
|
_srhRate(law, cn, cp, a) {
|
|
@@ -1731,7 +1761,7 @@ export class Solver {
|
|
|
1731
1761
|
const dDn = tp * cn + tp * n1 * kN + tn * p1 * (1 - kN);
|
|
1732
1762
|
const dDp = tp * n1 * kP + tn * cp + tn * p1 * (1 - kP);
|
|
1733
1763
|
const dDa = tp * n1 * kA + tn * p1 * (-1 - kA);
|
|
1734
|
-
return { rate, Cn: rate * (1 - dDn / den), Cp: rate * (1 - dDp / den), Ca: (np / den) * (1 - f) - (rate * dDa) / den };
|
|
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 };
|
|
1735
1765
|
}
|
|
1736
1766
|
|
|
1737
1767
|
// A face reaction (see normalizeFaceReactions). Its rate r_k is an unknown of the face block,
|
|
@@ -2171,6 +2201,7 @@ export class Solver {
|
|
|
2171
2201
|
* the start-of-step values, and dt/a0 replaces dt.
|
|
2172
2202
|
*/
|
|
2173
2203
|
step(dt, opts = {}) {
|
|
2204
|
+
this.atSteady = false;
|
|
2174
2205
|
const prev = this.history[0];
|
|
2175
2206
|
const w = prev ? dt / prev.dt : 0;
|
|
2176
2207
|
const bdf = opts.method === 'bdf2' && prev !== undefined && w <= 2;
|
|
@@ -2465,6 +2496,39 @@ export class Solver {
|
|
|
2465
2496
|
return mx;
|
|
2466
2497
|
}
|
|
2467
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
|
+
|
|
2468
2532
|
_snapshot() {
|
|
2469
2533
|
return {
|
|
2470
2534
|
u: Float64Array.from(this.u),
|
|
@@ -2472,6 +2536,7 @@ export class Solver {
|
|
|
2472
2536
|
cOld: Float64Array.from(this.cOld),
|
|
2473
2537
|
time: this.time,
|
|
2474
2538
|
lastDt: this.lastDt,
|
|
2539
|
+
atSteady: this.atSteady,
|
|
2475
2540
|
contactDStart: this.contactDStart,
|
|
2476
2541
|
contactDOld: this.contactDOld,
|
|
2477
2542
|
contactDEnd: this.contactDEnd,
|
|
@@ -2488,6 +2553,7 @@ export class Solver {
|
|
|
2488
2553
|
this.cOld.set(s.cOld);
|
|
2489
2554
|
this.time = s.time;
|
|
2490
2555
|
this.lastDt = s.lastDt;
|
|
2556
|
+
this.atSteady = s.atSteady;
|
|
2491
2557
|
this.contactDStart = s.contactDStart;
|
|
2492
2558
|
this.contactDOld = s.contactDOld;
|
|
2493
2559
|
this.contactDEnd = s.contactDEnd;
|
|
@@ -2863,7 +2929,15 @@ export class Solver {
|
|
|
2863
2929
|
* If Newton fails, dt ramps up from a small value (pseudo-transient continuation) instead.
|
|
2864
2930
|
* The clock is not advanced, and open-system conservation bookkeeping restarts here.
|
|
2865
2931
|
*/
|
|
2932
|
+
// Steady state from the present one; `atSteady` says whether the state is one (until a step or
|
|
2933
|
+
// a change of drive).
|
|
2866
2934
|
solveSteady(opts = {}) {
|
|
2935
|
+
const r = this._steadyFromHere(opts);
|
|
2936
|
+
this.atSteady = r.converged;
|
|
2937
|
+
return r;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
_steadyFromHere(opts) {
|
|
2867
2941
|
this.conditioning = null;
|
|
2868
2942
|
this.computeConcentrations();
|
|
2869
2943
|
this._captureLocal(); // immobile combinations keep, node by node, what they hold now
|
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/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
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IonMonger's default planar perovskite solar cell: TiO₂ (100 nm) | MAPbI₃ (400 nm) with mobile
|
|
3
|
+
* iodide vacancies over an equal immobile background | spiro-OMeTAD (200 nm), at 298 K, lit
|
|
4
|
+
* through the TiO₂. Species e⁻, h⁺ and the vacancies 'V+'. Recombination is SRH at both faces
|
|
5
|
+
* (IonMonger's interface law is driftlet's face SRH, with n₁ from the TiO₂'s electron density)
|
|
6
|
+
* and, with `bulkSRH`, SRH through midgap traps in the perovskite (τₙ = 3 ns, τₚ = 300 ns).
|
|
7
|
+
* Light is 1.4e21 photons/(m²·s) at α = 1.3e7 /m (IonMonger's one sun), times `light`. The left
|
|
8
|
+
* contact is the TiO₂'s, at 0 V; the right is the spiro's, at `V`, so photocurrent is positive.
|
|
9
|
+
*
|
|
10
|
+
* `PEROVSKITE_SCANS` holds IonMonger's own results for this cell, scan by scan.
|
|
11
|
+
* @param {{ V?: import('./types.js').Source | { V?: import('./types.js').Source, I?: import('./types.js').Source, R?: number }, light?: number, bulkSRH?: boolean }} [opts]
|
|
12
|
+
* @returns {import('./types.js').DeviceDefinition}
|
|
13
|
+
*/
|
|
14
|
+
export function perovskiteCell({ V, light, bulkSRH }?: {
|
|
15
|
+
V?: import("./types.js").Source | {
|
|
16
|
+
V?: import("./types.js").Source;
|
|
17
|
+
I?: import("./types.js").Source;
|
|
18
|
+
R?: number;
|
|
19
|
+
};
|
|
20
|
+
light?: number;
|
|
21
|
+
bulkSRH?: boolean;
|
|
22
|
+
}): import("./types.js").DeviceDefinition;
|
|
23
|
+
/**
|
|
24
|
+
* A J–V loop's figures of merit, from a transient's trace (`{ t, voltage, current }`, as a
|
|
25
|
+
* recorder or advance() gives it): the loop splits at time `turn` into the sweep before (`rev`)
|
|
26
|
+
* and after (`fwd`). Each sweep's maximum power V·I (W/m², the largest among the trace's points)
|
|
27
|
+
* and its open-circuit voltage (where I changes sign, interpolated linearly); and the
|
|
28
|
+
* hysteresis index (Pmax,rev − Pmax,fwd)/Pmax,rev. Power out is positive, so a cell whose
|
|
29
|
+
* current runs the other way should have it negated first.
|
|
30
|
+
* @param {{ t: ArrayLike<number>, voltage: ArrayLike<number>, current: ArrayLike<number> }} trace
|
|
31
|
+
* @param {number} turn s
|
|
32
|
+
*/
|
|
33
|
+
export function hysteresis(trace: {
|
|
34
|
+
t: ArrayLike<number>;
|
|
35
|
+
voltage: ArrayLike<number>;
|
|
36
|
+
current: ArrayLike<number>;
|
|
37
|
+
}, turn: number): {
|
|
38
|
+
hi: number;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* IonMonger's J–V scans of `perovskiteCell()` (IonMonger 2.0, commit 1ae4f9e, run in GNU Octave):
|
|
42
|
+
* steady state at 1.2 V under light, then 1.2 → 0 → 1.2 V at `rate` (V/s). `full` is the cell
|
|
43
|
+
* as given (bulk SRH on); `noBulkSRH` is `perovskiteCell({ bulkSRH: false })`. Per scan: J0, the
|
|
44
|
+
* current at 0 V (A/m²); each sweep's maximum power Pmax (W/m², the largest V·J among IonMonger's
|
|
45
|
+
* 201 output points per loop) and Voc (V, interpolated); and the hysteresis index
|
|
46
|
+
* hi = (Pmax,rev − Pmax,fwd)/Pmax,rev. `hysteresis()` reads the same from a run's trace.
|
|
47
|
+
* driftlet reproduces these to 0.15 % in hi, 0.5 W/m² in Pmax and 2 mV in Voc (its test suite).
|
|
48
|
+
*/
|
|
49
|
+
export const PEROVSKITE_SCANS: Readonly<{
|
|
50
|
+
source: "IonMonger 2.0 (github.com/PerovskiteSCModelling/IonMonger, commit 1ae4f9e), default parameters";
|
|
51
|
+
full: {
|
|
52
|
+
rate: number;
|
|
53
|
+
J0: number;
|
|
54
|
+
rev: {
|
|
55
|
+
Pmax: number;
|
|
56
|
+
Voc: number;
|
|
57
|
+
};
|
|
58
|
+
fwd: {
|
|
59
|
+
Pmax: number;
|
|
60
|
+
Voc: number;
|
|
61
|
+
};
|
|
62
|
+
hi: number;
|
|
63
|
+
}[];
|
|
64
|
+
noBulkSRH: {
|
|
65
|
+
rate: number;
|
|
66
|
+
J0: number;
|
|
67
|
+
rev: {
|
|
68
|
+
Pmax: number;
|
|
69
|
+
Voc: number;
|
|
70
|
+
};
|
|
71
|
+
fwd: {
|
|
72
|
+
Pmax: number;
|
|
73
|
+
Voc: number;
|
|
74
|
+
};
|
|
75
|
+
hi: number;
|
|
76
|
+
}[];
|
|
77
|
+
}>;
|
package/types/kit.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export function vacuumDipole(def: object, left: VacuumSide, right: VacuumSide):
|
|
|
34
34
|
*/
|
|
35
35
|
export function vacuumZeroCharge(def: object, workFunction: number, inside: VacuumSide): number;
|
|
36
36
|
export { live } from "./live.js";
|
|
37
|
+
export { recorder } from "./recorder.js";
|
|
38
|
+
export { check } from "./checks.js";
|
|
37
39
|
/**
|
|
38
40
|
* One material seen from vacuum: a level inside it (the anchor) and how far above that level,
|
|
39
41
|
* in electron energy, the vacuum level just outside lies (the offset, V). So
|
|
@@ -59,4 +61,5 @@ export { parseEquation, stoichiometry, half, SHE, level } from "./equation.js";
|
|
|
59
61
|
export { traces, speciesRole, typeset } from "./traces.js";
|
|
60
62
|
export { pulse, square, triangle, ramp, injector, recombination, photogeneration } from "./sources.js";
|
|
61
63
|
export { describe, unitWarnings } from "./describe.js";
|
|
64
|
+
export { perovskiteCell, PEROVSKITE_SCANS, hysteresis } from "./devices.js";
|
|
62
65
|
export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal } from "./data.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record a device's transient. Frames are whole solutions: at the start, then at each of
|
|
3
|
+
* `times` (or every `every` seconds from the start), which `advance()` lands on exactly; with
|
|
4
|
+
* neither, at the end of each `advance()` call. The trace (`t`, `current`, `voltage`, and
|
|
5
|
+
* `probes` if given) runs from the start through every accepted step of every call.
|
|
6
|
+
* @param {import('./index.js').Device} device
|
|
7
|
+
* @param {{ every?: number, times?: number[], probes?: { x: number, species: string, quantity?: 'c' | 'V', region?: string | number }[] }} [opts]
|
|
8
|
+
*/
|
|
9
|
+
export function recorder(device: import("./index.js").Device, opts?: {
|
|
10
|
+
every?: number;
|
|
11
|
+
times?: number[];
|
|
12
|
+
probes?: {
|
|
13
|
+
x: number;
|
|
14
|
+
species: string;
|
|
15
|
+
quantity?: "c" | "V";
|
|
16
|
+
region?: string | number;
|
|
17
|
+
}[];
|
|
18
|
+
}): {
|
|
19
|
+
advance: (tEnd: number, aopts?: object) => import("./types.js").Solution;
|
|
20
|
+
frame: (t: number) => import("./types.js").Solution;
|
|
21
|
+
frames: import("./types.js").Solution[];
|
|
22
|
+
trace: {
|
|
23
|
+
t: number[];
|
|
24
|
+
current: number[];
|
|
25
|
+
voltage: number[];
|
|
26
|
+
};
|
|
27
|
+
};
|