driftlet 0.1.0 → 0.2.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 +31 -17
- package/docs/alignment.md +146 -0
- package/docs/conventions.md +194 -0
- package/docs/data.md +120 -0
- package/docs/device.md +529 -0
- package/docs/kit.md +299 -0
- package/docs/logo-dark.svg +7 -0
- package/docs/logo.svg +7 -0
- package/docs/numerics.md +629 -0
- package/docs/statistics.md +226 -0
- package/docs/visualization.md +259 -0
- package/llms.txt +66 -12
- package/package.json +2 -1
- package/src/constants.js +11 -0
- package/src/index.js +5 -2
- package/src/kit.js +1 -0
- package/src/solver.js +66 -7
- package/src/sources.js +103 -0
- package/src/types.js +3 -2
- package/types/constants.d.ts +10 -0
- package/types/index.d.ts +11 -2
- package/types/kit.d.ts +1 -0
- package/types/solver.d.ts +6 -0
- package/types/sources.d.ts +102 -0
- package/types/types.d.ts +4 -2
package/src/kit.js
CHANGED
|
@@ -9,6 +9,7 @@ export { build, combine, layer, ohmic, bath } from './stack.js';
|
|
|
9
9
|
export { parseEquation, stoichiometry, half, SHE, level } from './equation.js';
|
|
10
10
|
export { traces, speciesRole, typeset } from './traces.js';
|
|
11
11
|
export { live } from './live.js';
|
|
12
|
+
export { pulse, square, triangle, ramp, injector, recombination } from './sources.js';
|
|
12
13
|
export { describe, unitWarnings } from './describe.js';
|
|
13
14
|
export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal } from './data.js';
|
|
14
15
|
|
package/src/solver.js
CHANGED
|
@@ -145,6 +145,7 @@ import { BlockTridiagonal, ComplexBlockTridiagonal } from './blockTridiagonal.js
|
|
|
145
145
|
import { bernoulli, bernoulliDerivative } from './bernoulli.js';
|
|
146
146
|
import { EPS0, FARADAY } from './constants.js';
|
|
147
147
|
import { nextBreakpoint, sourceAt } from './device.js';
|
|
148
|
+
import { DeviceError } from './errors.js';
|
|
148
149
|
|
|
149
150
|
export class SolverError extends Error {
|
|
150
151
|
constructor(message, details) {
|
|
@@ -1846,7 +1847,7 @@ export class Solver {
|
|
|
1846
1847
|
}
|
|
1847
1848
|
|
|
1848
1849
|
// Largest update among the potential-like unknowns (φ̂ and η at grid nodes).
|
|
1849
|
-
_maxPotentialStep(delta, deltaV = this.deltaV) {
|
|
1850
|
+
_maxPotentialStep(delta, deltaV = this.deltaV) { // (deltaV null: the device's potentials only)
|
|
1850
1851
|
const { n, M } = this;
|
|
1851
1852
|
const R = this.rix; // (the sink entry of delta is 0)
|
|
1852
1853
|
let mx = 0;
|
|
@@ -1868,7 +1869,7 @@ export class Solver {
|
|
|
1868
1869
|
newton(dt, { maxIter = 60, tol = 1e-10, maxStep = 10 } = {}) {
|
|
1869
1870
|
const { delta, res } = this;
|
|
1870
1871
|
const deltaV = this.deltaV ?? (this.deltaV = new Float64Array(this.floating.length));
|
|
1871
|
-
const history = [];
|
|
1872
|
+
const history = [], ownHistory = [];
|
|
1872
1873
|
// A steady solve that fails: record how nearly singular the system was, and where.
|
|
1873
1874
|
const fail = (r) => {
|
|
1874
1875
|
if (dt === Infinity) this._noteConditioning();
|
|
@@ -1899,15 +1900,22 @@ export class Solver {
|
|
|
1899
1900
|
if (!(err instanceof SolverError)) throw err;
|
|
1900
1901
|
return fail({ converged: false, iterations: it, history, error: err.message });
|
|
1901
1902
|
}
|
|
1903
|
+
// Convergence counts every unknown, floating terminal voltages too. Damping and the
|
|
1904
|
+
// divergence check count only the device's own potentials: a terminal voltage enters
|
|
1905
|
+
// linearly (conductance links, held levels, a capacitive face), so a large swing in one is
|
|
1906
|
+
// safe, and damping it would throttle the whole update (a current-driven port switching
|
|
1907
|
+
// off, its voltage collapsing by hundreds of volts, crawled 10 thermal units at a time).
|
|
1902
1908
|
const step = this._maxPotentialStep(delta, deltaV);
|
|
1909
|
+
const own = this._maxPotentialStep(delta, null);
|
|
1903
1910
|
history.push(step);
|
|
1911
|
+
ownHistory.push(own);
|
|
1904
1912
|
if (!Number.isFinite(step)) return fail({ converged: false, iterations: it, history, error: 'non-finite update' });
|
|
1905
1913
|
// Give up early on clear divergence; the caller will take a smaller step instead.
|
|
1906
|
-
if (
|
|
1914
|
+
if (own > 1e4 || (it > 6 && own > 10 * ownHistory[0])) {
|
|
1907
1915
|
this.computeConcentrations();
|
|
1908
1916
|
return fail({ converged: false, iterations: it, history, error: 'diverging' });
|
|
1909
1917
|
}
|
|
1910
|
-
const alpha =
|
|
1918
|
+
const alpha = own > maxStep ? maxStep / own : 1;
|
|
1911
1919
|
this._addToState(delta, -alpha);
|
|
1912
1920
|
this.floating.forEach((k, a) => (this.termV[k] -= alpha * deltaV[a]));
|
|
1913
1921
|
if (alpha === 1 && step < tol) {
|
|
@@ -2071,11 +2079,12 @@ export class Solver {
|
|
|
2071
2079
|
const { tol = 1e-3, dtMax = Infinity, budgetMs = Infinity, maxSteps = 100000, method = 'bdf2' } = opts;
|
|
2072
2080
|
const clock = () => (globalThis.performance ? globalThis.performance.now() : Date.now());
|
|
2073
2081
|
const start = clock();
|
|
2074
|
-
const
|
|
2082
|
+
const sample = this._probes(opts.probes);
|
|
2083
|
+
const trace = { t: [], current: [], voltage: [], ...(sample ? { probes: sample.out } : {}) };
|
|
2075
2084
|
let steps = 0, rejected = 0, iterations = 0, failed = false;
|
|
2076
2085
|
// Already there (an animation frame with no time to add): nothing to do, and the step size
|
|
2077
2086
|
// carried to the next call stays as it was.
|
|
2078
|
-
if (!(tEnd > this.time)) return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace
|
|
2087
|
+
if (!(tEnd > this.time)) return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace };
|
|
2079
2088
|
let dt = this.dtNext ?? opts.dt0 ?? (tEnd - this.time) * 1e-4;
|
|
2080
2089
|
if (!(dt > 0)) dt = (tEnd - this.time) * 1e-4;
|
|
2081
2090
|
let grow = 0; // longer first steps tried after a Newton failure (see below)
|
|
@@ -2113,7 +2122,7 @@ export class Solver {
|
|
|
2113
2122
|
this._restore(snap);
|
|
2114
2123
|
const a = this.step(h / 2);
|
|
2115
2124
|
// The first half step is kept too, so it goes in the trace (a jump's charge is in it).
|
|
2116
|
-
half = a.converged ? { t: this.time, current: this._terminalCurrent(), voltage: this.termV[1] - this.termV[0] } : null;
|
|
2125
|
+
half = a.converged ? { t: this.time, current: this._terminalCurrent(), voltage: this.termV[1] - this.termV[0], probes: sample?.read() } : null;
|
|
2117
2126
|
const b = a.converged ? this.step(h / 2) : a;
|
|
2118
2127
|
iterations += a.iterations + (b === a ? 0 : b.iterations);
|
|
2119
2128
|
if (b.converged) {
|
|
@@ -2165,10 +2174,12 @@ export class Solver {
|
|
|
2165
2174
|
trace.t.push(half.t);
|
|
2166
2175
|
trace.current.push(half.current);
|
|
2167
2176
|
trace.voltage.push(half.voltage);
|
|
2177
|
+
if (sample) sample.push(half.probes);
|
|
2168
2178
|
}
|
|
2169
2179
|
trace.t.push(this.time);
|
|
2170
2180
|
trace.current.push(this._terminalCurrent());
|
|
2171
2181
|
trace.voltage.push(this.termV[1] - this.termV[0]);
|
|
2182
|
+
if (sample) sample.push(sample.read());
|
|
2172
2183
|
}
|
|
2173
2184
|
this.dtNext = dt;
|
|
2174
2185
|
this.landing = undefined;
|
|
@@ -2224,6 +2235,54 @@ export class Solver {
|
|
|
2224
2235
|
return { scale: bdf ? 1 : h / (2 * h + hp), order: 1 };
|
|
2225
2236
|
}
|
|
2226
2237
|
|
|
2238
|
+
// Probes for a transient's trace: { x, species, quantity: 'c' | 'V', region } read after every
|
|
2239
|
+
// accepted step, linearly between the two nodes of x's region around it. At an interface x
|
|
2240
|
+
// belongs to both regions, so `region` (a name or index) picks the side; it defaults to the
|
|
2241
|
+
// first region that holds x.
|
|
2242
|
+
_probes(probes) {
|
|
2243
|
+
if (probes === undefined) return null;
|
|
2244
|
+
const { grid, regions, species } = this.model;
|
|
2245
|
+
const fail = (m) => {
|
|
2246
|
+
throw new DeviceError(m);
|
|
2247
|
+
};
|
|
2248
|
+
if (!Array.isArray(probes)) fail('advance: probes must be an array of { x, species, quantity, region }');
|
|
2249
|
+
const plan = probes.map((p, k) => {
|
|
2250
|
+
const path = `advance: probes[${k}]`;
|
|
2251
|
+
const i = species.findIndex((sp) => sp.name === p?.species);
|
|
2252
|
+
if (i < 0) fail(`${path}.species: no species ${JSON.stringify(p?.species)}`);
|
|
2253
|
+
const quantity = p.quantity ?? 'c';
|
|
2254
|
+
if (quantity !== 'c' && quantity !== 'V') fail(`${path}.quantity must be 'c' (mol/m³) or 'V' (the species voltage)`);
|
|
2255
|
+
if (quantity === 'V' && species[i].z === 0) fail(`${path}: '${p.species}' is neutral, so it has no voltage; read 'c'`);
|
|
2256
|
+
const x = grid.x, inside = (r) => p.x >= x[grid.regionStart[r]] && p.x <= x[grid.regionEnd[r]];
|
|
2257
|
+
let r;
|
|
2258
|
+
if (p.region !== undefined) {
|
|
2259
|
+
r = typeof p.region === 'number' ? p.region : regions.findIndex((reg) => reg.name === p.region);
|
|
2260
|
+
if (!(r >= 0 && r < regions.length)) fail(`${path}.region: no region ${JSON.stringify(p.region)}`);
|
|
2261
|
+
if (!inside(r)) fail(`${path}.x (${p.x} m) is outside region ${JSON.stringify(p.region)}`);
|
|
2262
|
+
} else {
|
|
2263
|
+
r = regions.findIndex((_, rr) => inside(rr));
|
|
2264
|
+
if (!(Number.isFinite(p.x) && r >= 0)) fail(`${path}.x must be a position in the device, in m (got ${p.x})`);
|
|
2265
|
+
}
|
|
2266
|
+
let g = grid.regionStart[r];
|
|
2267
|
+
while (g + 1 < grid.regionEnd[r] && x[g + 1] < p.x) g++;
|
|
2268
|
+
const w = x[g + 1] > x[g] ? (p.x - x[g]) / (x[g + 1] - x[g]) : 0;
|
|
2269
|
+
return { i, g, w, quantity };
|
|
2270
|
+
});
|
|
2271
|
+
const out = plan.map(() => []);
|
|
2272
|
+
const { n, M } = this;
|
|
2273
|
+
const at = (q, g) => {
|
|
2274
|
+
if (!this.present[g * n + q.i]) return NaN;
|
|
2275
|
+
if (q.quantity === 'c') return this.c[g * n + q.i];
|
|
2276
|
+
const o = this.blockOfNode[g] * M + 1 + q.i;
|
|
2277
|
+
return (this.VT * (this.u[o] + this.uLo[o])) / this.z[q.i];
|
|
2278
|
+
};
|
|
2279
|
+
return {
|
|
2280
|
+
out,
|
|
2281
|
+
read: () => plan.map((q) => (1 - q.w) * at(q, q.g) + q.w * at(q, q.g + 1)),
|
|
2282
|
+
push: (values) => values.forEach((v, k) => out[k].push(v)),
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2227
2286
|
// Largest difference from ref among the potentials that carry state, in thermal units.
|
|
2228
2287
|
_errorNorm(ref) {
|
|
2229
2288
|
const { n, M, u } = this;
|
package/src/sources.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// driftlet/kit: sources as plain definition data. Waveforms for a terminal's V or I, an injector
|
|
2
|
+
// port, and band-to-band recombination from a lifetime. Each returns an ordinary spec value.
|
|
3
|
+
|
|
4
|
+
import { DeviceError } from './errors.js';
|
|
5
|
+
|
|
6
|
+
const fail = (message) => {
|
|
7
|
+
throw new DeviceError(message);
|
|
8
|
+
};
|
|
9
|
+
const need = (ok, message) => ok || fail(message);
|
|
10
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A rectangular pulse, as a waveform for a terminal's `V` or `I`: `base` until `start`, then
|
|
14
|
+
* `base + amplitude` for `width`, then `base` again. With a `rise` > 0 its edges are ramps of
|
|
15
|
+
* that length, placed so the area stays `amplitude · width`; without one they're jumps, which
|
|
16
|
+
* the time stepping handles exactly (so a pulse with no rise must start after t = 0, the time a
|
|
17
|
+
* device starts at and solve() reads: it's off then).
|
|
18
|
+
* @param {{ start?: number, width: number, amplitude: number, rise?: number, base?: number }} opts s, s, V or A/m², s, V or A/m²
|
|
19
|
+
* @returns {{ t: number[], values: number[] }}
|
|
20
|
+
*/
|
|
21
|
+
export function pulse({ start = 0, width, amplitude, rise = 0, base = 0 } = {}) {
|
|
22
|
+
need(num(width) && width > 0, `pulse: width must be > 0 (s), got ${width}`);
|
|
23
|
+
need(num(amplitude), `pulse: amplitude must be a finite number (V or A/m²), got ${amplitude}`);
|
|
24
|
+
need(num(start) && start >= 0, `pulse: start must be ≥ 0 (s), got ${start}`);
|
|
25
|
+
need(num(rise) && rise >= 0 && rise <= width, `pulse: rise must be between 0 and the width (s), got ${rise}`);
|
|
26
|
+
need(num(base), `pulse: base must be a finite number, got ${base}`);
|
|
27
|
+
need(rise > 0 || start > 0, 'pulse: with no rise, start after t = 0, so the device starts (and solve() reads it) before the pulse');
|
|
28
|
+
const top = base + amplitude;
|
|
29
|
+
return { t: [start, start + rise, start + width, start + width + rise], values: [base, top, top, base] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A square wave, repeating: `high` for `duty · period`, then `low`, from t = 0.
|
|
34
|
+
* @param {{ high: number, low: number, period: number, duty?: number }} opts
|
|
35
|
+
* @returns {{ t: number[], values: number[], repeat: true }}
|
|
36
|
+
*/
|
|
37
|
+
export function square({ high, low, period, duty = 0.5 } = {}) {
|
|
38
|
+
need(num(high) && num(low), `square: high and low must be finite numbers, got ${high} and ${low}`);
|
|
39
|
+
need(num(period) && period > 0, `square: period must be > 0 (s), got ${period}`);
|
|
40
|
+
need(num(duty) && duty > 0 && duty < 1, `square: duty must be between 0 and 1, got ${duty}`);
|
|
41
|
+
const on = duty * period;
|
|
42
|
+
return { t: [0, on, on, period], values: [high, high, low, low], repeat: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A triangle wave, repeating: from `from` to `to` and back in each `period`, from t = 0 (a
|
|
47
|
+
* cyclic voltammogram's sweep, from its starting potential).
|
|
48
|
+
* @param {{ from: number, to: number, period: number }} opts
|
|
49
|
+
* @returns {{ t: number[], values: number[], repeat: true }}
|
|
50
|
+
*/
|
|
51
|
+
export function triangle({ from, to, period } = {}) {
|
|
52
|
+
need(num(from) && num(to), `triangle: from and to must be finite numbers, got ${from} and ${to}`);
|
|
53
|
+
need(num(period) && period > 0, `triangle: period must be > 0 (s), got ${period}`);
|
|
54
|
+
return { t: [0, period / 2, period], values: [from, to, from], repeat: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A ramp from `from` to `to` over `duration`, starting at `start`, constant on either side.
|
|
59
|
+
* @param {{ from: number, to: number, duration: number, start?: number }} opts
|
|
60
|
+
* @returns {{ t: number[], values: number[] }}
|
|
61
|
+
*/
|
|
62
|
+
export function ramp({ from, to, duration, start = 0 } = {}) {
|
|
63
|
+
need(num(from) && num(to), `ramp: from and to must be finite numbers, got ${from} and ${to}`);
|
|
64
|
+
need(num(duration) && duration > 0, `ramp: duration must be > 0 (s), got ${duration}`);
|
|
65
|
+
need(num(start) && start >= 0, `ramp: start must be ≥ 0 (s), got ${start}`);
|
|
66
|
+
return { t: [start, start + duration], values: [from, to] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A port that injects a species into a window of a region at a driven current `I` (A/m², a
|
|
71
|
+
* number or a waveform), spread evenly over the window. It's an ordinary port: driven by `I`,
|
|
72
|
+
* its terminal the species, linked by a conductance small enough that at the peak current the
|
|
73
|
+
* port sits `headroom` volts above the window's level of that species, so every node of the
|
|
74
|
+
* window takes the same share (like a current source). At I = 0 it still links the window,
|
|
75
|
+
* weakly.
|
|
76
|
+
* @param {{ name?: string, region: string | number, from: number, to: number, species: string,
|
|
77
|
+
* I: number | { t: number[], values: number[], repeat?: boolean }, headroom?: number }} opts
|
|
78
|
+
* @returns {object} a port, for the definition's `ports`
|
|
79
|
+
*/
|
|
80
|
+
export function injector({ name = 'injector', region, from, to, species, I, headroom = 2.5 } = {}) {
|
|
81
|
+
need(region !== undefined, 'injector: give the region (its name or index)');
|
|
82
|
+
need(num(from) && num(to) && to > from, `injector: from and to must bound the window, in m from the region's left end (got ${from}, ${to})`);
|
|
83
|
+
need(typeof species === 'string', 'injector: give the species it injects, e.g. species: \'h+\'');
|
|
84
|
+
need(num(headroom) && headroom > 0, `injector: headroom must be > 0 (V), got ${headroom}`);
|
|
85
|
+
const peak = num(I) ? Math.abs(I) : Array.isArray(I?.values) && I.values.every(num) ? Math.max(...I.values.map(Math.abs)) : NaN;
|
|
86
|
+
need(Number.isFinite(peak), 'injector: I must be a current (A/m²) or a waveform { t, values }');
|
|
87
|
+
const G = peak > 0 ? peak / ((to - from) * headroom) : 1; // S/m³
|
|
88
|
+
return { name, region, from, to, terminal: species, species: { [species]: { type: 'conductance', G } }, I };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Band-to-band recombination e⁻ + h⁺ ⇌ 0 in a material, from the minority-carrier lifetime it
|
|
93
|
+
* should give at a majority density: kf = 1/(τ · majority). (The rate is kf (np − n_i²), so τ is
|
|
94
|
+
* the low-injection lifetime of whichever carrier is the minority.)
|
|
95
|
+
* @param {{ material: string, tau: number, majority: number }} opts material name, s, mol/m³
|
|
96
|
+
* @returns {{ equation: string, kf: Record<string, number> }} a bulk reaction
|
|
97
|
+
*/
|
|
98
|
+
export function recombination({ material, tau, majority } = {}) {
|
|
99
|
+
need(typeof material === 'string', 'recombination: give the material, by name');
|
|
100
|
+
need(num(tau) && tau > 0, `recombination: tau must be > 0 (s), got ${tau}`);
|
|
101
|
+
need(num(majority) && majority > 0, `recombination: majority must be the majority carrier density > 0 (mol/m³; units.perCm3 converts), got ${majority}`);
|
|
102
|
+
return { equation: 'e- + h+ = 0', kf: { [material]: 1 / (tau * majority) } };
|
|
103
|
+
}
|
package/src/types.js
CHANGED
|
@@ -204,8 +204,9 @@
|
|
|
204
204
|
* @property {number} time s
|
|
205
205
|
* @property {boolean} [done] advance(): whether tEnd was reached (false if the frame budget ran out)
|
|
206
206
|
* @property {number} [rejected] advance(): steps rejected by error control or Newton failure
|
|
207
|
-
* @property {{ t: number[], current: number[], voltage: number[] }} [trace] advance():
|
|
208
|
-
* current (A/m², toward +x) and voltage after each accepted step
|
|
207
|
+
* @property {{ t: number[], current: number[], voltage: number[], probes?: number[][] }} [trace] advance():
|
|
208
|
+
* terminal current (A/m², toward +x) and voltage after each accepted step, and each probe's
|
|
209
|
+
* reading (mol/m³, or V for quantity 'V'), interpolated between the nodes around its x
|
|
209
210
|
*/
|
|
210
211
|
|
|
211
212
|
/**
|
package/types/constants.d.ts
CHANGED
|
@@ -30,4 +30,14 @@ export const units: Readonly<{
|
|
|
30
30
|
um: (x: any) => number;
|
|
31
31
|
/** cm²/s → m²/s */
|
|
32
32
|
cm2PerS: (x: any) => number;
|
|
33
|
+
/** mol/m³ → mol/L */
|
|
34
|
+
toMolar: (x: any) => number;
|
|
35
|
+
/** mol/m³ → particles per cm³ */
|
|
36
|
+
toPerCm3: (x: any) => number;
|
|
37
|
+
/** m → nm */
|
|
38
|
+
toNm: (x: any) => number;
|
|
39
|
+
/** m → µm */
|
|
40
|
+
toUm: (x: any) => number;
|
|
41
|
+
/** m²/s → cm²/s */
|
|
42
|
+
toCm2PerS: (x: any) => number;
|
|
33
43
|
}>;
|
package/types/index.d.ts
CHANGED
|
@@ -45,9 +45,12 @@ export class Device {
|
|
|
45
45
|
/**
|
|
46
46
|
* Adaptive transient to time tEnd (s): variable-step BDF2 with local error control. With
|
|
47
47
|
* `budgetMs`, it returns after that much wall time even if tEnd isn't reached (`done` says
|
|
48
|
-
* which), so an animation can call it once per frame; the step size carries over.
|
|
48
|
+
* which), so an animation can call it once per frame; the step size carries over. `probes`
|
|
49
|
+
* read a species at points inside the device after every accepted step, into `trace.probes`
|
|
50
|
+
* (one array per probe, alongside `trace.t`): what a detector at x sees.
|
|
49
51
|
* @param {number} tEnd s
|
|
50
|
-
* @param {{ tol?: number, dt0?: number, dtMax?: number, budgetMs?: number, maxSteps?: number, method?: 'bdf2' | 'be'
|
|
52
|
+
* @param {{ tol?: number, dt0?: number, dtMax?: number, budgetMs?: number, maxSteps?: number, method?: 'bdf2' | 'be',
|
|
53
|
+
* probes?: { x: number, species: string, quantity?: 'c' | 'V', region?: string | number }[] }} [opts]
|
|
51
54
|
* @returns {Solution}
|
|
52
55
|
*/
|
|
53
56
|
advance(tEnd: number, opts?: {
|
|
@@ -57,6 +60,12 @@ export class Device {
|
|
|
57
60
|
budgetMs?: number;
|
|
58
61
|
maxSteps?: number;
|
|
59
62
|
method?: "bdf2" | "be";
|
|
63
|
+
probes?: {
|
|
64
|
+
x: number;
|
|
65
|
+
species: string;
|
|
66
|
+
quantity?: "c" | "V";
|
|
67
|
+
region?: string | number;
|
|
68
|
+
}[];
|
|
60
69
|
}): Solution;
|
|
61
70
|
/**
|
|
62
71
|
* Small-signal impedance Z(f) about the steady state (solved first), at one terminal: its
|
package/types/kit.d.ts
CHANGED
|
@@ -57,5 +57,6 @@ export type VacuumSide = {
|
|
|
57
57
|
export { build, combine, layer, ohmic, bath } from "./stack.js";
|
|
58
58
|
export { parseEquation, stoichiometry, half, SHE, level } from "./equation.js";
|
|
59
59
|
export { traces, speciesRole, typeset } from "./traces.js";
|
|
60
|
+
export { pulse, square, triangle, ramp, injector, recombination } from "./sources.js";
|
|
60
61
|
export { describe, unitWarnings } from "./describe.js";
|
|
61
62
|
export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal } from "./data.js";
|
package/types/solver.d.ts
CHANGED
|
@@ -216,6 +216,7 @@ export class Solver {
|
|
|
216
216
|
rejected: number;
|
|
217
217
|
iterations: number;
|
|
218
218
|
trace: {
|
|
219
|
+
probes?: any;
|
|
219
220
|
t: any[];
|
|
220
221
|
current: any[];
|
|
221
222
|
voltage: any[];
|
|
@@ -227,6 +228,11 @@ export class Solver {
|
|
|
227
228
|
scale: number;
|
|
228
229
|
order: number;
|
|
229
230
|
};
|
|
231
|
+
_probes(probes: any): {
|
|
232
|
+
out: any;
|
|
233
|
+
read: () => any;
|
|
234
|
+
push: (values: any) => any;
|
|
235
|
+
};
|
|
230
236
|
_errorNorm(ref: any): number;
|
|
231
237
|
_snapshot(): {
|
|
232
238
|
u: Float64Array<ArrayBuffer>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rectangular pulse, as a waveform for a terminal's `V` or `I`: `base` until `start`, then
|
|
3
|
+
* `base + amplitude` for `width`, then `base` again. With a `rise` > 0 its edges are ramps of
|
|
4
|
+
* that length, placed so the area stays `amplitude · width`; without one they're jumps, which
|
|
5
|
+
* the time stepping handles exactly (so a pulse with no rise must start after t = 0, the time a
|
|
6
|
+
* device starts at and solve() reads: it's off then).
|
|
7
|
+
* @param {{ start?: number, width: number, amplitude: number, rise?: number, base?: number }} opts s, s, V or A/m², s, V or A/m²
|
|
8
|
+
* @returns {{ t: number[], values: number[] }}
|
|
9
|
+
*/
|
|
10
|
+
export function pulse({ start, width, amplitude, rise, base }?: {
|
|
11
|
+
start?: number;
|
|
12
|
+
width: number;
|
|
13
|
+
amplitude: number;
|
|
14
|
+
rise?: number;
|
|
15
|
+
base?: number;
|
|
16
|
+
}): {
|
|
17
|
+
t: number[];
|
|
18
|
+
values: number[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A square wave, repeating: `high` for `duty · period`, then `low`, from t = 0.
|
|
22
|
+
* @param {{ high: number, low: number, period: number, duty?: number }} opts
|
|
23
|
+
* @returns {{ t: number[], values: number[], repeat: true }}
|
|
24
|
+
*/
|
|
25
|
+
export function square({ high, low, period, duty }?: {
|
|
26
|
+
high: number;
|
|
27
|
+
low: number;
|
|
28
|
+
period: number;
|
|
29
|
+
duty?: number;
|
|
30
|
+
}): {
|
|
31
|
+
t: number[];
|
|
32
|
+
values: number[];
|
|
33
|
+
repeat: true;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A triangle wave, repeating: from `from` to `to` and back in each `period`, from t = 0 (a
|
|
37
|
+
* cyclic voltammogram's sweep, from its starting potential).
|
|
38
|
+
* @param {{ from: number, to: number, period: number }} opts
|
|
39
|
+
* @returns {{ t: number[], values: number[], repeat: true }}
|
|
40
|
+
*/
|
|
41
|
+
export function triangle({ from, to, period }?: {
|
|
42
|
+
from: number;
|
|
43
|
+
to: number;
|
|
44
|
+
period: number;
|
|
45
|
+
}): {
|
|
46
|
+
t: number[];
|
|
47
|
+
values: number[];
|
|
48
|
+
repeat: true;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* A ramp from `from` to `to` over `duration`, starting at `start`, constant on either side.
|
|
52
|
+
* @param {{ from: number, to: number, duration: number, start?: number }} opts
|
|
53
|
+
* @returns {{ t: number[], values: number[] }}
|
|
54
|
+
*/
|
|
55
|
+
export function ramp({ from, to, duration, start }?: {
|
|
56
|
+
from: number;
|
|
57
|
+
to: number;
|
|
58
|
+
duration: number;
|
|
59
|
+
start?: number;
|
|
60
|
+
}): {
|
|
61
|
+
t: number[];
|
|
62
|
+
values: number[];
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* A port that injects a species into a window of a region at a driven current `I` (A/m², a
|
|
66
|
+
* number or a waveform), spread evenly over the window. It's an ordinary port: driven by `I`,
|
|
67
|
+
* its terminal the species, linked by a conductance small enough that at the peak current the
|
|
68
|
+
* port sits `headroom` volts above the window's level of that species, so every node of the
|
|
69
|
+
* window takes the same share (like a current source). At I = 0 it still links the window,
|
|
70
|
+
* weakly.
|
|
71
|
+
* @param {{ name?: string, region: string | number, from: number, to: number, species: string,
|
|
72
|
+
* I: number | { t: number[], values: number[], repeat?: boolean }, headroom?: number }} opts
|
|
73
|
+
* @returns {object} a port, for the definition's `ports`
|
|
74
|
+
*/
|
|
75
|
+
export function injector({ name, region, from, to, species, I, headroom }?: {
|
|
76
|
+
name?: string;
|
|
77
|
+
region: string | number;
|
|
78
|
+
from: number;
|
|
79
|
+
to: number;
|
|
80
|
+
species: string;
|
|
81
|
+
I: number | {
|
|
82
|
+
t: number[];
|
|
83
|
+
values: number[];
|
|
84
|
+
repeat?: boolean;
|
|
85
|
+
};
|
|
86
|
+
headroom?: number;
|
|
87
|
+
}): object;
|
|
88
|
+
/**
|
|
89
|
+
* Band-to-band recombination e⁻ + h⁺ ⇌ 0 in a material, from the minority-carrier lifetime it
|
|
90
|
+
* should give at a majority density: kf = 1/(τ · majority). (The rate is kf (np − n_i²), so τ is
|
|
91
|
+
* the low-injection lifetime of whichever carrier is the minority.)
|
|
92
|
+
* @param {{ material: string, tau: number, majority: number }} opts material name, s, mol/m³
|
|
93
|
+
* @returns {{ equation: string, kf: Record<string, number> }} a bulk reaction
|
|
94
|
+
*/
|
|
95
|
+
export function recombination({ material, tau, majority }?: {
|
|
96
|
+
material: string;
|
|
97
|
+
tau: number;
|
|
98
|
+
majority: number;
|
|
99
|
+
}): {
|
|
100
|
+
equation: string;
|
|
101
|
+
kf: Record<string, number>;
|
|
102
|
+
};
|
package/types/types.d.ts
CHANGED
|
@@ -538,13 +538,15 @@ export type Solution = {
|
|
|
538
538
|
*/
|
|
539
539
|
rejected?: number;
|
|
540
540
|
/**
|
|
541
|
-
* advance():
|
|
542
|
-
* current (A/m², toward +x) and voltage after each accepted step
|
|
541
|
+
* advance():
|
|
542
|
+
* terminal current (A/m², toward +x) and voltage after each accepted step, and each probe's
|
|
543
|
+
* reading (mol/m³, or V for quantity 'V'), interpolated between the nodes around its x
|
|
543
544
|
*/
|
|
544
545
|
trace?: {
|
|
545
546
|
t: number[];
|
|
546
547
|
current: number[];
|
|
547
548
|
voltage: number[];
|
|
549
|
+
probes?: number[][];
|
|
548
550
|
};
|
|
549
551
|
};
|
|
550
552
|
export type ImpedanceResult = {
|