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/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 (step > 1e4 || (it > 6 && step > 10 * history[0])) {
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 = step > maxStep ? maxStep / step : 1;
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 trace = { t: [], current: [], voltage: [] };
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: { t: [], current: [], voltage: [] } };
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(): terminal
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
  /**
@@ -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' }} [opts]
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(): terminal
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 = {