driftlet 0.16.2 → 1.0.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 +14 -3
- package/docs/device.md +29 -11
- package/docs/kit.md +10 -1
- package/docs/numerics.md +68 -12
- package/docs/statistics.md +2 -1
- package/docs/templates.md +260 -0
- package/llms.txt +107 -299
- package/package.json +15 -3
- package/src/blockTridiagonal.js +73 -1
- package/src/channels.js +6 -4
- package/src/checks.js +78 -3
- package/src/data.js +5 -1
- package/src/device.js +1 -1
- package/src/devices.js +2 -0
- package/src/equation.js +2 -1
- package/src/errors.js +33 -0
- package/src/grid.js +12 -0
- package/src/index.js +21 -10
- package/src/junction.js +3 -1
- package/src/kit.js +1 -1
- package/src/live.js +2 -0
- package/src/particles.js +2 -0
- package/src/plot.js +4 -0
- package/src/polarization.js +2 -1
- package/src/recorder.js +2 -1
- package/src/solution.js +39 -5
- package/src/solver.js +248 -30
- package/src/sources.js +8 -1
- package/src/stack.js +14 -1
- package/src/statistics.js +1 -1
- package/src/traces.js +3 -0
- package/src/types.js +3 -1
- package/types/blockTridiagonal.d.ts +1 -0
- package/types/channels.d.ts +3 -3
- package/types/checks.d.ts +9 -3
- package/types/data.d.ts +3 -3
- package/types/devices.d.ts +1 -1
- package/types/equation.d.ts +1 -1
- package/types/errors.d.ts +8 -0
- package/types/index.d.ts +4 -7
- package/types/junction.d.ts +2 -2
- package/types/kit.d.ts +1 -1
- package/types/live.d.ts +1 -1
- package/types/particles.d.ts +1 -1
- package/types/plot.d.ts +3 -3
- package/types/polarization.d.ts +1 -1
- package/types/recorder.d.ts +1 -1
- package/types/solver.d.ts +8 -1
- package/types/sources.d.ts +7 -7
- package/types/traces.d.ts +1 -1
- package/types/types.d.ts +9 -1
package/src/blockTridiagonal.js
CHANGED
|
@@ -124,7 +124,15 @@ export class BlockTridiagonal {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
|
|
127
|
+
let ratio = luFactor(lu, o, piv, offX[i], m, this);
|
|
128
|
+
// A pivot far below the entries it's formed from may be round-off alone (the level of a
|
|
129
|
+
// region that only the blocks after it hold): its value, 7e-32 or 3e-16 or 0 as the
|
|
130
|
+
// unknowns happen to be listed, then scales all that follows (by 1e31, and the next block
|
|
131
|
+
// lost its own pivot outright). Redone with each entry's magnitudes carried (see
|
|
132
|
+
// cancellation), a pivot 1e-18 of them, all its digits lost, is perturbed like one that
|
|
133
|
+
// cancelled to exactly nothing. (At 1e-15 it can still hold a digit Newton needs: a port's
|
|
134
|
+
// steady solve wandered instead of converging.)
|
|
135
|
+
if (this.staticPivots && ratio < 1e-12) ratio = this._noisyPivots(i);
|
|
128
136
|
if (!(ratio > 0)) {
|
|
129
137
|
this.factored = false;
|
|
130
138
|
this.singularBlock = i;
|
|
@@ -146,6 +154,70 @@ export class BlockTridiagonal {
|
|
|
146
154
|
this.factored = true;
|
|
147
155
|
}
|
|
148
156
|
|
|
157
|
+
// Block i again, its W and LU with the magnitudes each entry is formed from (as cancellation()
|
|
158
|
+
// does), perturbing pivots 1e-18 of theirs. Returns the block's pivot ratio.
|
|
159
|
+
_noisyPivots(i) {
|
|
160
|
+
const { A, B, sizes, offA, offB, offC, offX } = this, cp = this._cp, lu = this._lu, piv = this._piv;
|
|
161
|
+
const m = sizes[i], mp = i > 0 ? sizes[i - 1] : 0, oa = offA[i], o = offB[i], oc = i > 0 ? offC[i - 1] : 0;
|
|
162
|
+
const S = (this._mag ??= new Float64Array(this._tmp.length * this._tmp.length));
|
|
163
|
+
for (let r = 0; r < m; r++) {
|
|
164
|
+
for (let c = 0; c < m; c++) {
|
|
165
|
+
let v = B[o + r * m + c], sum = Math.abs(v);
|
|
166
|
+
for (let k = 0; k < mp; k++) {
|
|
167
|
+
const a = A[oa + r * mp + k];
|
|
168
|
+
if (a === 0) continue;
|
|
169
|
+
const t = a * cp[oc + k * m + c];
|
|
170
|
+
v -= t;
|
|
171
|
+
sum += Math.abs(t);
|
|
172
|
+
}
|
|
173
|
+
lu[o + r * m + c] = v;
|
|
174
|
+
S[r * m + c] = sum;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
let maxEntry = 0;
|
|
178
|
+
for (let k = 0; k < m * m; k++) maxEntry = Math.max(maxEntry, Math.abs(lu[o + k]));
|
|
179
|
+
if (maxEntry === 0) return 0;
|
|
180
|
+
let minPivot = Infinity;
|
|
181
|
+
for (let k = 0; k < m; k++) {
|
|
182
|
+
let best = k, bestVal = Math.abs(lu[o + k * m + k]);
|
|
183
|
+
for (let r = k + 1; r < m; r++) {
|
|
184
|
+
const v = Math.abs(lu[o + r * m + k]);
|
|
185
|
+
if (v > bestVal) { best = r; bestVal = v; }
|
|
186
|
+
}
|
|
187
|
+
piv[offX[i] + k] = best;
|
|
188
|
+
if (best !== k) {
|
|
189
|
+
for (let c = 0; c < m; c++) {
|
|
190
|
+
let t = lu[o + k * m + c];
|
|
191
|
+
lu[o + k * m + c] = lu[o + best * m + c];
|
|
192
|
+
lu[o + best * m + c] = t;
|
|
193
|
+
t = S[k * m + c];
|
|
194
|
+
S[k * m + c] = S[best * m + c];
|
|
195
|
+
S[best * m + c] = t;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const sum = S[k * m + k];
|
|
199
|
+
if (bestVal <= 1e-18 * sum || bestVal === 0) {
|
|
200
|
+
const size = sum > 0 ? 1e-15 * sum : 1e-15 * maxEntry;
|
|
201
|
+
bestVal = size;
|
|
202
|
+
lu[o + k * m + k] = lu[o + k * m + k] < 0 ? -size : size;
|
|
203
|
+
this.perturbedPivots++;
|
|
204
|
+
}
|
|
205
|
+
if (bestVal < minPivot) minPivot = bestVal;
|
|
206
|
+
const inv = 1 / lu[o + k * m + k];
|
|
207
|
+
for (let r = k + 1; r < m; r++) {
|
|
208
|
+
const f = (lu[o + r * m + k] *= inv);
|
|
209
|
+
if (f !== 0) {
|
|
210
|
+
const af = Math.abs(f);
|
|
211
|
+
for (let c = k + 1; c < m; c++) {
|
|
212
|
+
lu[o + r * m + c] -= f * lu[o + k * m + c];
|
|
213
|
+
S[r * m + c] += af * S[k * m + c];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return minPivot / maxEntry;
|
|
219
|
+
}
|
|
220
|
+
|
|
149
221
|
/**
|
|
150
222
|
* Solve with the current factorisation. `rhs` and `out` are Float64Arrays of length `size`
|
|
151
223
|
* (node-major); they may be the same array.
|
package/src/channels.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// driftlet/kit: voltage-gated channels as plain definition data. Hodgkin and Huxley's squid axon:
|
|
2
2
|
// its gates, and permeabilities for its Na⁺, K⁺ and leak channels, for a membrane face.
|
|
3
3
|
|
|
4
|
-
import { DeviceError } from './errors.js';
|
|
4
|
+
import { DeviceError, checkOptions } from './errors.js';
|
|
5
5
|
import { FARADAY, GAS_CONSTANT } from './constants.js';
|
|
6
6
|
|
|
7
7
|
const fail = (message) => {
|
|
@@ -49,13 +49,15 @@ export function ghkCurrent(z, inside, outside, V, T) {
|
|
|
49
49
|
* port along a region: links that are their linear conductances, G = g × area per volume, gated the
|
|
50
50
|
* same, each ion's reversal potential following from the port's bath and the concentrations inside
|
|
51
51
|
* (so `inside`, `outside` and `at` aren't needed).
|
|
52
|
-
* @param {{ inside: object, outside: object, T
|
|
52
|
+
* @param {{ inside: object, outside: object, T: number, g?: { Na?: number, K?: number, leak?: number },
|
|
53
53
|
* leak?: string, leakZ?: number, at?: number, names?: { Na?: string, K?: string }, linear?: boolean, area?: number }} opts concentrations in
|
|
54
54
|
* mol/m³ by species name, K, S/m², V
|
|
55
55
|
* @returns {{ gates: object, species: object }}
|
|
56
56
|
*/
|
|
57
|
-
export function hodgkinHuxley({ inside, outside, T
|
|
58
|
-
|
|
57
|
+
export function hodgkinHuxley({ inside, outside, T, g = {}, leak = 'Cl-', leakZ = -1, at = -65 * mV, names = {}, linear = false, area } = {}) {
|
|
58
|
+
checkOptions('hodgkinHuxley', arguments[0], ['inside', 'outside', 'T', 'g', 'leak', 'leakZ', 'at', 'names', 'linear', 'area']);
|
|
59
|
+
// T sets the gates' rates, so it must be the device's: never defaulted (their 6.3 °C, under a body-temperature device, doubled a spike's peak, silently).
|
|
60
|
+
need(num(T) && T > 0, `hodgkinHuxley: T must be the device's temperature in K (their own experiments were at 279.45 K, 6.3 °C), got ${T}`);
|
|
59
61
|
need(num(at), `hodgkinHuxley: at must be a voltage (V), got ${at}`);
|
|
60
62
|
const G = { Na: 1200, K: 360, leak: 3, ...g };
|
|
61
63
|
const Na = names.Na ?? 'Na+', K = names.K ?? 'K+';
|
package/src/checks.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Device } from './index.js';
|
|
6
6
|
import { FARADAY } from './constants.js';
|
|
7
7
|
import { unitWarnings } from './describe.js';
|
|
8
|
+
import { checkOptions } from './errors.js';
|
|
8
9
|
|
|
9
10
|
const sig = (v, p = 3) => (v === 0 || !Number.isFinite(v) ? String(v) : Math.abs(v) >= 1e4 || Math.abs(v) < 1e-2 ? v.toExponential(p - 1) : String(+v.toPrecision(p)));
|
|
10
11
|
|
|
@@ -29,6 +30,11 @@ const sig = (v, p = 3) => (v === 0 || !Number.isFinite(v) ? String(v) : Math.abs
|
|
|
29
30
|
* zero; under light it's J = F(G − R), and it says where every carrier went.
|
|
30
31
|
* - **conservation** (a transient): every closed, unreacting stretch keeps what it held plus
|
|
31
32
|
* what came in.
|
|
33
|
+
* - **order** (a steady state, with `refine` and `order`, the defaults): the device solved from
|
|
34
|
+
* its definition twice, its species listed as given and in reverse, which must give the same
|
|
35
|
+
* steady state (each terminal's voltage and current, each species' amount). A solve that
|
|
36
|
+
* depends on the listing has converged somewhere that isn't steady without noticing (round 11's
|
|
37
|
+
* fuzzer found it in ~1% of random reacting devices, by exactly this test).
|
|
32
38
|
* - **grid** (with `refine`, the default; in a transient, `ok: null` and a reminder, since the
|
|
33
39
|
* answer depends on the whole run): in a steady state, the device solved again on a grid
|
|
34
40
|
* twice as fine everywhere, from this solution interpolated onto it. The current, each floating terminal's voltage and each
|
|
@@ -37,12 +43,13 @@ const sig = (v, p = 3) => (v === 0 || !Number.isFinite(v) ? String(v) : Math.abs
|
|
|
37
43
|
* (about 3/4 of it).
|
|
38
44
|
* @param {import('./index.js').Device} device
|
|
39
45
|
* @param {object} [sol] the solution to check (default: the device's present state)
|
|
40
|
-
* @param {{ refine?: boolean, tol?: number }} [opts]
|
|
46
|
+
* @param {{ refine?: boolean, tol?: number, order?: boolean }} [opts]
|
|
41
47
|
* Every check passed when `ok`; an item with `ok: null` (a warning, or a check that couldn't be
|
|
42
48
|
* made) asks you to look, and doesn't fail it. `text` is a line per check, marked ok, FAIL or ?.
|
|
43
49
|
* @returns {{ ok: boolean, items: CheckItem[], text: string }}
|
|
44
50
|
*/
|
|
45
|
-
export function check(device, sol, { refine = true, tol = 1e-2 } = {}) {
|
|
51
|
+
export function check(device, sol, { refine = true, tol = 1e-2, order = true } = {}) {
|
|
52
|
+
checkOptions('check', arguments[2], ['refine', 'tol', 'order']);
|
|
46
53
|
sol ??= device.solution();
|
|
47
54
|
const { model, solver, def } = device;
|
|
48
55
|
const steady = sol.steady ?? solver.atSteady;
|
|
@@ -66,8 +73,17 @@ export function check(device, sol, { refine = true, tol = 1e-2 } = {}) {
|
|
|
66
73
|
// The rest checks a solution, which an unconverged state isn't.
|
|
67
74
|
if (sol.converged) {
|
|
68
75
|
const ledger = steady ? balance(device, sol) : null;
|
|
76
|
+
// A reaction the steady state needs at equilibrium, left far from it (too slow to see: the
|
|
77
|
+
// solution warns of it), isn't steady, however quietly its ledgers sum: "equilibrium" was
|
|
78
|
+
// printed beside that warning.
|
|
79
|
+
const stuck = steady ? solver.stuckReactions() : [];
|
|
80
|
+
if (ledger && stuck.length > 0) {
|
|
81
|
+
ledger.ok = false;
|
|
82
|
+
ledger.summary = `not a steady state: ${stuck.map((s) => `${s.where} is ${sig(s.affinity * solver.VT * 1e3)} mV of affinity from equilibrium`).join(', ')}, too slow for the solve to see (see the warnings); its ledgers say only that nothing moves fast enough to count`;
|
|
83
|
+
}
|
|
69
84
|
items.push(ledger ?? conservation(sol, Math.max(...device.model.grid.area)));
|
|
70
85
|
if (steady && refine) items.push(gridCheck(device, sol, tol, ledger.details.gross));
|
|
86
|
+
if (steady && refine && order && model.species.length > 1) items.push(orderCheck(device, sol, ledger.details.gross));
|
|
71
87
|
// A transient's answer depends on its whole history (and on drives set along the way), so
|
|
72
88
|
// only a run of its own on a finer grid can check it.
|
|
73
89
|
else if (refine) items.push({ name: 'grid', ok: null, summary: 'not checked in a transient: run it again on a grid twice as fine (hmin and hmax halved) and compare' });
|
|
@@ -156,7 +172,10 @@ function balance(device, sol) {
|
|
|
156
172
|
const size = (l) => Math.max(0, ...l.terms.map((t) => Math.abs(t.rate)));
|
|
157
173
|
const natural = naturalScales(device, sol), inSolution = naturalScales(device, sol, false); // (partners' scale: not a metal's conductance)
|
|
158
174
|
const scale = (l) => Math.max(natural[l.i], ...l.terms.flatMap((t) => (partners.get(t.what) ?? []).map((j) => inSolution[j])));
|
|
159
|
-
|
|
175
|
+
// Small flows count too where they balance, as real ones do to round-off (a diode at 1 µV,
|
|
176
|
+
// 1e-19 of its scale, to 1e-15), and round-off doesn't. (Above 1e-8 of the scale, any.)
|
|
177
|
+
const balanced = (l) => Math.abs(l.terms.reduce((a, t) => a + t.rate, 0)) <= 1e-9 * size(l);
|
|
178
|
+
const moving = ledgers.filter((l) => size(l) > 1e-8 * scale(l) || (size(l) > 1e-20 * scale(l) && balanced(l)));
|
|
160
179
|
let worst = 0;
|
|
161
180
|
const lines = [];
|
|
162
181
|
for (const l of moving) {
|
|
@@ -314,6 +333,62 @@ function gridCheck(device, sol, tol, gross) {
|
|
|
314
333
|
};
|
|
315
334
|
}
|
|
316
335
|
|
|
336
|
+
// The device solved from its definition with its species listed as given and in reverse: the
|
|
337
|
+
// same steady state either way, or one of them isn't steady. (Both from the definition, not this
|
|
338
|
+
// state, whose closed amounts a transient or set() may have moved from c0.)
|
|
339
|
+
function orderCheck(device, sol, gross) {
|
|
340
|
+
const { def, model } = device;
|
|
341
|
+
const solveAt = (d) => {
|
|
342
|
+
const dev = new Device(d);
|
|
343
|
+
if (sol.time > 0) dev.solver.time = sol.time; // waveforms at the same time
|
|
344
|
+
return dev.solve();
|
|
345
|
+
};
|
|
346
|
+
let a, b;
|
|
347
|
+
try {
|
|
348
|
+
a = solveAt(def);
|
|
349
|
+
b = solveAt({ ...def, species: [...def.species].reverse() });
|
|
350
|
+
} catch (e) {
|
|
351
|
+
return { name: 'order', ok: null, summary: `could not solve the device with its species reversed (${e.message})` };
|
|
352
|
+
}
|
|
353
|
+
if (!a.converged || !b.converged) {
|
|
354
|
+
const which = a.converged ? 'listed in reverse' : b.converged ? 'listed as given' : 'listed either way';
|
|
355
|
+
return { name: 'order', ok: null, summary: `the device solved from its definition did not converge with its species ${which}: compare with a long advance() by hand` };
|
|
356
|
+
}
|
|
357
|
+
const VT = model.RT / FARADAY, diffs = [];
|
|
358
|
+
// (Currents only where the ledgers see something move: in equilibrium they're round-off of
|
|
359
|
+
// whatever exchanges fastest, an electrode's couple, and a wrong state shows in the voltages
|
|
360
|
+
// and amounts instead.)
|
|
361
|
+
// (and above 1e-16 of what the species could carry, as the grid check's floor: a drift at
|
|
362
|
+
// 1e-16 A/m², still moving at 1e12 s, isn't worth reporting)
|
|
363
|
+
const natural = naturalScales(device, sol), floorI = 1e-16 * FARADAY * Math.max(0, ...model.species.map((sp, i) => Math.abs(sp.z) * natural[i]).filter(Number.isFinite));
|
|
364
|
+
for (const [name, t] of Object.entries(a.terminals)) {
|
|
365
|
+
const u = b.terminals[name];
|
|
366
|
+
diffs.push({ what: `${name} voltage`, a: t.V, b: u.V, rel: Math.abs(u.V - t.V) / (1e-6 * Math.max(VT, Math.abs(t.V))), unit: 'V' });
|
|
367
|
+
if (gross > 0 && Math.max(Math.abs(t.current), Math.abs(u.current), gross) > floorI) diffs.push({ what: `${name} current`, a: t.current, b: u.current, rel: Math.abs(u.current - t.current) / (1e-3 * Math.max(Math.abs(t.current), Math.abs(u.current), gross)), unit: unitsOf(model).current });
|
|
368
|
+
}
|
|
369
|
+
const vol = model.grid.vol, amount = (s, name) => s.x.reduce((m, _, g) => m + (Number.isFinite(s.c[name][g]) ? vol[g] * s.c[name][g] : 0), 0);
|
|
370
|
+
const amounts = model.species.map((sp) => [sp.name, amount(a, sp.name), amount(b, sp.name)]);
|
|
371
|
+
const most = Math.max(0, ...amounts.map(([, x, y]) => Math.max(Math.abs(x), Math.abs(y))));
|
|
372
|
+
for (const [name, x, y] of amounts) diffs.push({ what: `${name} amount`, a: x, b: y, rel: Math.abs(y - x) / (1e-6 * Math.max(Math.abs(x), Math.abs(y)) + 1e-12 * most), unit: 'mol/m²' });
|
|
373
|
+
// Agreeing to near round-off (a µV, a millionth of an amount): ok. Within a thousand times that
|
|
374
|
+
// (a mV, a thousandth), enough for any plot, but a weakly held part of one solve not quite
|
|
375
|
+
// steady: look. Beyond: one of them isn't a steady state.
|
|
376
|
+
const worst = diffs.reduce((w, d) => (d.rel > (w?.rel ?? -1) ? d : w), null);
|
|
377
|
+
const ok = !(worst.rel > 1) ? true : worst.rel > 1e3 ? false : null;
|
|
378
|
+
const what = `${worst.what} ${sig(worst.a, 6)} → ${sig(worst.b, 6)} ${worst.unit}`;
|
|
379
|
+
return {
|
|
380
|
+
name: 'order',
|
|
381
|
+
ok,
|
|
382
|
+
summary:
|
|
383
|
+
ok === true
|
|
384
|
+
? 'the same steady state with the species listed in reverse'
|
|
385
|
+
: ok === null
|
|
386
|
+
? `listed in reverse, the species give nearly the same steady state (${what}): not to round-off, so some weakly held part is not quite steady in one of them`
|
|
387
|
+
: `listed in reverse, the species give a different steady state: ${what}. One of them is not steady, though its solve converged, or the device has more than one steady state (one of them perhaps unstable): compare with a long advance() from the start, kick each and watch it with dtMax (see numerics.md, Steady state), and please report the device.`,
|
|
388
|
+
details: { diffs },
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
317
392
|
// Charge density at a node of region r, C/m³: the species plus the region's fixed charge.
|
|
318
393
|
function rho(s, g, model, r) {
|
|
319
394
|
let q = model.regions[r].fixedCharge ?? 0;
|
package/src/data.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// only. How levels line up at an interface, and a surface's work function, belong to that
|
|
4
4
|
// interface or surface and are never in here.
|
|
5
5
|
|
|
6
|
-
import { DeviceError } from './errors.js';
|
|
6
|
+
import { DeviceError, checkOptions } from './errors.js';
|
|
7
7
|
import { FARADAY, GAS_CONSTANT, AVOGADRO } from './constants.js';
|
|
8
8
|
|
|
9
9
|
const fail = (message) => {
|
|
@@ -72,6 +72,7 @@ export const METALS = freeze(
|
|
|
72
72
|
* @returns {{ species: object[], materials: Record<string, object> }}
|
|
73
73
|
*/
|
|
74
74
|
export function aqueous(names, { epsr = WATER_EPSR, material = 'water' } = {}) {
|
|
75
|
+
checkOptions('aqueous', arguments[1], ['epsr', 'material']);
|
|
75
76
|
if (!Array.isArray(names) || names.length === 0) fail('aqueous: give a list of ions, e.g. aqueous([\'Na+\', \'Cl-\'])');
|
|
76
77
|
const species = [], entries = {};
|
|
77
78
|
for (const name of names) {
|
|
@@ -93,6 +94,8 @@ export function aqueous(names, { epsr = WATER_EPSR, material = 'water' } = {}) {
|
|
|
93
94
|
* @returns {{ species: object[], materials: Record<string, object> }}
|
|
94
95
|
*/
|
|
95
96
|
export function semiconductor(name, { material = name } = {}) {
|
|
97
|
+
if (arguments[1]?.T !== undefined) fail('semiconductor: the data are for 300 K and take no T: give the device T: 300, or write your own material for another temperature (SEMICONDUCTORS has the numbers)');
|
|
98
|
+
checkOptions('semiconductor', arguments[1], ['material']);
|
|
96
99
|
const sc = SEMICONDUCTORS[name];
|
|
97
100
|
if (!sc) fail(`semiconductor: no data for '${name}' (the library has ${Object.keys(SEMICONDUCTORS).join(', ')})`);
|
|
98
101
|
const VT = (GAS_CONSTANT * 300) / FARADAY;
|
|
@@ -121,6 +124,7 @@ export function semiconductor(name, { material = name } = {}) {
|
|
|
121
124
|
* @returns {{ species: object[], materials: Record<string, object> }}
|
|
122
125
|
*/
|
|
123
126
|
export function metal(name, { material = name } = {}) {
|
|
127
|
+
checkOptions('metal', arguments[1], ['material']);
|
|
124
128
|
const m = METALS[name];
|
|
125
129
|
if (!m) fail(`metal: no data for '${name}' (the library has ${Object.keys(METALS).join(', ')})`);
|
|
126
130
|
return { species: [{ name: 'e-', z: -1 }], materials: { [material]: { conductor: { species: 'e-', conductivity: m.conductivity } } } };
|
package/src/device.js
CHANGED
|
@@ -1316,7 +1316,7 @@ function normalizeContact(cdef, side, region, materials, species, speciesIndex,
|
|
|
1316
1316
|
fixed !== 0
|
|
1317
1317
|
? `${path}.bath: the bath is the end material '${mat.name}' at this composition, and with its fixed charge (${fixed} mol/m³ of charge) it is not neutral (net ${charge}). ` +
|
|
1318
1318
|
`For a solution outside a charged region, give the solution's material (bath.material), whose composition is then neutral on its own`
|
|
1319
|
-
: `${path}.bath: composition is not neutral (net ${charge} mol/m³ of charge)`,
|
|
1319
|
+
: `${path}.bath: composition is not neutral (net ${charge} mol/m³ of charge): add the counter-ions' share`,
|
|
1320
1320
|
);
|
|
1321
1321
|
// The reference species pins the bath's φ; every other species follows from composition.
|
|
1322
1322
|
const zb = bathZeta(bm, cb, own ? 0 : region.background);
|
package/src/devices.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { AVOGADRO, FARADAY, GAS_CONSTANT, units } from './constants.js';
|
|
6
6
|
import { photogeneration } from './sources.js';
|
|
7
7
|
import { drive } from './stack.js';
|
|
8
|
+
import { checkOptions } from './errors.js';
|
|
8
9
|
|
|
9
10
|
const m3 = (x) => x / AVOGADRO, eV = units.eV;
|
|
10
11
|
|
|
@@ -33,6 +34,7 @@ const P = {
|
|
|
33
34
|
* @returns {import('./types.js').DeviceDefinition}
|
|
34
35
|
*/
|
|
35
36
|
export function perovskiteCell({ V = 1.2, light = 1, bulkSRH = true } = {}) {
|
|
37
|
+
checkOptions('perovskiteCell', arguments[0], ['V', 'light', 'bulkSRH']);
|
|
36
38
|
const { T } = P, VT = (GAS_CONSTANT * T) / FARADAY;
|
|
37
39
|
const ni = Math.sqrt(P.gc * P.gv) * Math.exp(-(P.Ec - P.Ev) / (2 * VT));
|
|
38
40
|
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
|
package/src/equation.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// definition's reactions can be written this way; they compile to signed stoichiometry (ν < 0
|
|
3
3
|
// consumed by the forward reaction, left to right as written).
|
|
4
4
|
|
|
5
|
-
import { DeviceError } from './errors.js';
|
|
5
|
+
import { DeviceError, checkOptions } from './errors.js';
|
|
6
6
|
import { FARADAY } from './constants.js';
|
|
7
7
|
|
|
8
8
|
const fail = (message) => {
|
|
@@ -163,6 +163,7 @@ export const SHE = Object.freeze(half('2 H+ + 2 e- = H2', { H2: 0 }));
|
|
|
163
163
|
* @returns {Float64Array}
|
|
164
164
|
*/
|
|
165
165
|
export function level(sol, halfReaction, { standard = false } = {}) {
|
|
166
|
+
checkOptions('level', arguments[2], ['standard']);
|
|
166
167
|
if (!isObject(halfReaction)) fail('level: give a half-reaction from half()');
|
|
167
168
|
const { equation, fixed = {} } = halfReaction;
|
|
168
169
|
const terms = parseEquation(equation, 'level');
|
package/src/errors.js
CHANGED
|
@@ -6,3 +6,36 @@ export class DeviceError extends Error {
|
|
|
6
6
|
this.name = 'DeviceError';
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
// The nearest of `names` to a mistyped one: the same but for case, '_' or '-', or one or two
|
|
11
|
+
// letters off (dtmax for dtMax, engery for energy).
|
|
12
|
+
function nearest(name, names) {
|
|
13
|
+
const plain = (s) => s.toLowerCase().replace(/[_-]/g, '');
|
|
14
|
+
const edits = (a, b) => {
|
|
15
|
+
let row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
16
|
+
for (let i = 1; i <= a.length; i++) {
|
|
17
|
+
const next = [i];
|
|
18
|
+
for (let j = 1; j <= b.length; j++) next[j] = Math.min(row[j] + 1, next[j - 1] + 1, row[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
19
|
+
row = next;
|
|
20
|
+
}
|
|
21
|
+
return row[b.length];
|
|
22
|
+
};
|
|
23
|
+
return names.find((n) => plain(n) === plain(name)) ?? names.find((n) => n.length > 3 && edits(plain(n), plain(name)) <= 2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A function's options: a name it doesn't take (a typo) is an error, not silently ignored.
|
|
28
|
+
* @param {string} where the function, for the message
|
|
29
|
+
* @param {unknown} opts
|
|
30
|
+
* @param {string[]} allowed
|
|
31
|
+
* @param {new (message: string) => Error} [Kind]
|
|
32
|
+
*/
|
|
33
|
+
export function checkOptions(where, opts, allowed, Kind = DeviceError) {
|
|
34
|
+
if (opts === undefined || opts === null) return;
|
|
35
|
+
if (typeof opts !== 'object' || Array.isArray(opts)) throw new Kind(`${where}: options must be an object (${allowed.join(', ')})`);
|
|
36
|
+
for (const k of Object.keys(opts)) {
|
|
37
|
+
if (opts[k] === undefined || allowed.includes(k)) continue;
|
|
38
|
+
const near = nearest(k, allowed);
|
|
39
|
+
throw new Kind(`${where}: no option '${k}'${near ? ` (did you mean '${near}'?)` : ''}; the options are ${allowed.join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/grid.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// cell widths outright (`cells`), as a metal region does: one cell, exact for Ohm's law.
|
|
11
11
|
|
|
12
12
|
const DEFAULTS = Object.freeze({ ratio: 1.2, minCells: 8 });
|
|
13
|
+
const MAX_CELLS = 1e6; // a live demo's grid has hundreds to thousands
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Cell widths for one region: fine at both ends, growing toward the middle.
|
|
@@ -52,6 +53,17 @@ export function gradedCells(length, { hmin, hmax, ratio = DEFAULTS.ratio, minCel
|
|
|
52
53
|
*/
|
|
53
54
|
export function buildGrid(regions, opts = {}) {
|
|
54
55
|
if (!Array.isArray(regions) || regions.length === 0) throw new Error('grid needs at least one region');
|
|
56
|
+
// A grid far past any demo's (most often a length given in µm as if in m) would only exhaust
|
|
57
|
+
// memory: refuse it before building it. Each region needs at least length/hmax cells.
|
|
58
|
+
const least = regions.map((reg) => {
|
|
59
|
+
const g = { ...DEFAULTS, ...opts, ...(reg.grid ?? {}) };
|
|
60
|
+
return reg.cells ? reg.cells.length : reg.length / Math.min(g.hmax ?? Infinity, reg.length / g.minCells);
|
|
61
|
+
});
|
|
62
|
+
const total = least.reduce((s, n) => s + n, 0);
|
|
63
|
+
if (!(total <= MAX_CELLS)) {
|
|
64
|
+
const r = least.indexOf(Math.max(...least)), g = { ...opts, ...(regions[r].grid ?? {}) };
|
|
65
|
+
throw new RangeError(`at least ${total.toExponential(1)} cells, past the ${MAX_CELLS.toExponential(0)} a device may have: ${regions[r].name ?? `region ${r}`} is ${regions[r].length} m long at hmax ${g.hmax ?? '(none)'} m. Lengths are in metres (units.um(1) is 1e-6): check them, or raise hmax`);
|
|
66
|
+
}
|
|
55
67
|
|
|
56
68
|
const perRegion = regions.map((reg) => reg.cells ?? gradedCells(reg.length, { ...opts, ...(reg.grid ?? {}) }));
|
|
57
69
|
const nNodes = perRegion.reduce((s, cells) => s + cells.length + 1, 0);
|
package/src/index.js
CHANGED
|
@@ -4,13 +4,11 @@ import { normalizeDevice, DeviceError } from './device.js';
|
|
|
4
4
|
import { Solver, SolverError } from './solver.js';
|
|
5
5
|
import { makeSolution } from './solution.js';
|
|
6
6
|
import { merge } from './merge.js';
|
|
7
|
+
import { checkOptions } from './errors.js';
|
|
7
8
|
|
|
8
|
-
export { DeviceError
|
|
9
|
+
export { DeviceError } from './device.js';
|
|
9
10
|
import { normalizeDrives } from './device.js';
|
|
10
11
|
export { SolverError } from './solver.js';
|
|
11
|
-
export { buildGrid, gradedCells } from './grid.js';
|
|
12
|
-
export { BlockTridiagonal } from './blockTridiagonal.js';
|
|
13
|
-
export { bernoulli, bernoulliDerivative } from './bernoulli.js';
|
|
14
12
|
export * from './constants.js';
|
|
15
13
|
|
|
16
14
|
// Whether two definitions differ only in their terminals' drives (V, I, R on the contacts and
|
|
@@ -32,7 +30,10 @@ function sameExceptDrives(a, b) {
|
|
|
32
30
|
for (const k of keys) {
|
|
33
31
|
if (k === 'contacts') {
|
|
34
32
|
const ca = a.contacts ?? {}, cb = b.contacts ?? {};
|
|
35
|
-
for (const side of new Set([...Object.keys(ca), ...Object.keys(cb)]))
|
|
33
|
+
for (const side of new Set([...Object.keys(ca), ...Object.keys(cb)])) {
|
|
34
|
+
if (side !== 'left' && side !== 'right') return false; // a misspelt side: rebuild, which says so
|
|
35
|
+
if (!eq(ca[side] ?? {}, cb[side] ?? {}, true)) return false;
|
|
36
|
+
}
|
|
36
37
|
} else if (k === 'ports') {
|
|
37
38
|
const pa = a.ports ?? [], pb = b.ports ?? [];
|
|
38
39
|
if (pa.length !== pb.length || pa.some((p, i) => !eq(p, pb[i], true))) return false;
|
|
@@ -41,6 +42,9 @@ function sameExceptDrives(a, b) {
|
|
|
41
42
|
return true;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
// A method's options: a name it doesn't take (a typo, dtmax for dtMax) is an error, not ignored.
|
|
46
|
+
const options = (opts, method, allowed) => checkOptions(method, opts, allowed, SolverError);
|
|
47
|
+
|
|
44
48
|
/** @typedef {import('./types.js').DeviceDefinition} DeviceDefinition */
|
|
45
49
|
/** @typedef {import('./types.js').Solution} Solution */
|
|
46
50
|
|
|
@@ -63,8 +67,8 @@ export class Device {
|
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/**
|
|
66
|
-
* Change part of the definition (deep-merged). A contact given V or I drops the other
|
|
67
|
-
* `{ contacts: { right: { I: 0 } } }` switches it to open circuit. Ports are patched by name,
|
|
70
|
+
* Change part of the definition (deep-merged). A contact given V or I drops the other (and
|
|
71
|
+
* given I, any series R), so `{ contacts: { right: { I: 0 } } }` switches it to open circuit. Ports are patched by name,
|
|
68
72
|
* `{ ports: { gate: { V: 0.2 } } }` (an array replaces them all). A change to the terminals'
|
|
69
73
|
* drives alone (V, I, R) is applied in place, cheaply. Otherwise the device is rebuilt, keeping the current state as
|
|
70
74
|
* the warm start when the grid and species are unchanged (else restarting from the regions' c0).
|
|
@@ -73,7 +77,8 @@ export class Device {
|
|
|
73
77
|
*/
|
|
74
78
|
set(patch) {
|
|
75
79
|
// A terminal given a new kind of drive drops the old one: { I: 0 } replaces a held V.
|
|
76
|
-
|
|
80
|
+
// (A current drives a terminal alone: given I, a contact drops V and any series R too.)
|
|
81
|
+
const redrive = (c) => (c && typeof c === 'object' && !Array.isArray(c) ? ('I' in c && !('V' in c) ? { ...c, V: undefined, ...('R' in c ? {} : { R: undefined }) } : 'V' in c && !('I' in c) ? { ...c, I: undefined } : c) : c);
|
|
77
82
|
for (const side of ['left', 'right']) {
|
|
78
83
|
const c = patch?.contacts?.[side];
|
|
79
84
|
if (c && typeof c === 'object') patch = { ...patch, contacts: { ...patch.contacts, [side]: redrive(c) } };
|
|
@@ -178,6 +183,7 @@ export class Device {
|
|
|
178
183
|
* @returns {Solution}
|
|
179
184
|
*/
|
|
180
185
|
solve(opts) {
|
|
186
|
+
options(opts, 'solve', ['maxSteps', 'tol', 'continuation']);
|
|
181
187
|
// A solve that fails reports the state it failed in, and leaves the device as it was: a
|
|
182
188
|
// device with no steady state (a host filling at a current) can still advance() from there.
|
|
183
189
|
const snap = this.solver._snapshot();
|
|
@@ -195,6 +201,7 @@ export class Device {
|
|
|
195
201
|
* @returns {Solution}
|
|
196
202
|
*/
|
|
197
203
|
step(dt, opts) {
|
|
204
|
+
options(opts, 'step', ['method']);
|
|
198
205
|
return makeSolution(this.solver, this.solver.advance(dt, opts));
|
|
199
206
|
}
|
|
200
207
|
|
|
@@ -210,22 +217,26 @@ export class Device {
|
|
|
210
217
|
* @returns {Solution}
|
|
211
218
|
*/
|
|
212
219
|
advance(tEnd, opts) {
|
|
220
|
+
options(opts, 'advance', ['tol', 'dt0', 'dtMax', 'budgetMs', 'maxSteps', 'method', 'probes']);
|
|
213
221
|
return makeSolution(this.solver, this.solver.integrate(tEnd, opts));
|
|
214
222
|
}
|
|
215
223
|
|
|
216
224
|
/**
|
|
217
225
|
* Small-signal impedance Z(f) about the steady state (solved first), at one terminal: its
|
|
218
226
|
* held voltage is perturbed (or its driven current), the others keep their drives.
|
|
219
|
-
* Z = δV/δI with I into the device (Ω·m²)
|
|
227
|
+
* Z = δV/δI with I into the device (Ω·m²), with the steady state's `warnings`.
|
|
220
228
|
* @param {ArrayLike<number>} frequencies Hz
|
|
221
229
|
* @param {{ terminal?: string, profiles?: boolean }} [opts] the terminal (default 'right'), and
|
|
222
230
|
* whether to return complex profiles per frequency
|
|
223
231
|
* @returns {import('./types.js').ImpedanceResult}
|
|
224
232
|
*/
|
|
225
233
|
impedance(frequencies, opts) {
|
|
234
|
+
options(opts, 'impedance', ['terminal', 'profiles']);
|
|
226
235
|
const steady = this.solver.solveSteady();
|
|
227
236
|
if (!steady.converged) throw new SolverError('impedance: the steady state did not converge');
|
|
228
|
-
|
|
237
|
+
// The steady state's warnings (an unresolved double layer, say) hold for the impedance too.
|
|
238
|
+
const { warnings } = makeSolution(this.solver, steady);
|
|
239
|
+
return { ...this.solver.impedance(frequencies, opts), warnings };
|
|
229
240
|
}
|
|
230
241
|
|
|
231
242
|
/**
|
package/src/junction.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// junction (steady between two baths, it is Planck's; stepping from a sharp boundary, a free
|
|
4
4
|
// diffusion junction); these are for comparison, and for quick estimates.
|
|
5
5
|
|
|
6
|
-
import { DeviceError } from './errors.js';
|
|
6
|
+
import { DeviceError, checkOptions } from './errors.js';
|
|
7
7
|
import { FARADAY, GAS_CONSTANT } from './constants.js';
|
|
8
8
|
|
|
9
9
|
const fail = (message) => {
|
|
@@ -40,6 +40,7 @@ function neutral(list, side, what) {
|
|
|
40
40
|
* @param {{ T?: number }} [opts] K (default 298.15)
|
|
41
41
|
*/
|
|
42
42
|
export function henderson(left, right, ions, { T = 298.15 } = {}) {
|
|
43
|
+
checkOptions('henderson', arguments[3], ['T']);
|
|
43
44
|
const list = prepare(left, right, ions, 'henderson');
|
|
44
45
|
neutral(list, 'cL', 'henderson');
|
|
45
46
|
neutral(list, 'cR', 'henderson');
|
|
@@ -66,6 +67,7 @@ export function henderson(left, right, ions, { T = 298.15 } = {}) {
|
|
|
66
67
|
* @param {{ T?: number, steps?: number }} [opts] K (default 298.15); RK4 steps across (default 4000)
|
|
67
68
|
*/
|
|
68
69
|
export function planck(left, right, ions, { T = 298.15, steps = 4000 } = {}) {
|
|
70
|
+
checkOptions('planck', arguments[3], ['T', 'steps']);
|
|
69
71
|
const list = prepare(left, right, ions, 'planck');
|
|
70
72
|
neutral(list, 'cL', 'planck');
|
|
71
73
|
neutral(list, 'cR', 'planck');
|
package/src/kit.js
CHANGED
|
@@ -6,7 +6,7 @@ import { DeviceError } from './errors.js';
|
|
|
6
6
|
import { FARADAY } from './constants.js';
|
|
7
7
|
|
|
8
8
|
export { build, combine, layer, ohmic, bath } from './stack.js';
|
|
9
|
-
export {
|
|
9
|
+
export { half, SHE, level } from './equation.js';
|
|
10
10
|
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';
|
package/src/live.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { Device } from './index.js';
|
|
8
8
|
import { merge } from './merge.js';
|
|
9
|
+
import { checkOptions } from './errors.js';
|
|
9
10
|
|
|
10
11
|
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && !ArrayBuffer.isView(v);
|
|
11
12
|
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
@@ -118,6 +119,7 @@ export class Session {
|
|
|
118
119
|
* @returns {Live}
|
|
119
120
|
*/
|
|
120
121
|
export function live(def, { worker = false, onsolution, maxRamp = 32 } = {}) {
|
|
122
|
+
checkOptions('live', arguments[1], ['worker', 'onsolution', 'maxRamp']);
|
|
121
123
|
const engine = worker ? workerEngine(def, worker, maxRamp) : localEngine(def, maxRamp);
|
|
122
124
|
let pending = [], busy = false;
|
|
123
125
|
const api = {
|
package/src/particles.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
// and its minority carriers as dots, each one injected, drifting, diffusing and recombining.
|
|
24
24
|
|
|
25
25
|
import { GAS_CONSTANT } from './constants.js';
|
|
26
|
+
import { checkOptions } from './errors.js';
|
|
26
27
|
|
|
27
28
|
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
28
29
|
|
|
@@ -53,6 +54,7 @@ const NONE = 0, DOT = 1, OUTSIDE = 2; // nothing (closed); a cell of dots; a sea
|
|
|
53
54
|
* @param {() => number} [opts.random] a uniform random source in [0, 1) (default Math.random)
|
|
54
55
|
*/
|
|
55
56
|
export function particles(sol, { species, dots = 200, weight, cap = Infinity, cells = 120, random = Math.random } = {}) {
|
|
57
|
+
checkOptions('particles', arguments[1], ['species', 'dots', 'weight', 'cap', 'cells', 'random']);
|
|
56
58
|
const names = species ?? sol.species.map((s) => s.name).filter((nm) => sol.c[nm].some((v) => v > 0));
|
|
57
59
|
for (const nm of names) if (!sol.c[nm]) throw new RangeError(`particles: no species ${JSON.stringify(nm)} in the solution`);
|
|
58
60
|
if (!(dots > 0 && Number.isFinite(dots))) throw new RangeError(`particles: dots must be a positive number, got ${dots}`);
|
package/src/plot.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { traces } from './traces.js';
|
|
16
16
|
import { powi } from './pow.js';
|
|
17
|
+
import { checkOptions } from './errors.js';
|
|
17
18
|
|
|
18
19
|
// Colours by role, light and dark (validated together for colour-vision deficiency; line weight,
|
|
19
20
|
// dash and the legend carry identity too). A role with more members than colours cycles.
|
|
@@ -31,6 +32,7 @@ const ROLES = ['electron', 'cation', 'anion', 'redox'];
|
|
|
31
32
|
* @param {{ dark?: boolean }} [opts]
|
|
32
33
|
*/
|
|
33
34
|
export function themeColor(role, slot, { dark = false } = {}) {
|
|
35
|
+
checkOptions('themeColor', arguments[2], ['dark']);
|
|
34
36
|
const list = THEME[dark ? 'dark' : 'light'][role];
|
|
35
37
|
return list[slot % list.length];
|
|
36
38
|
}
|
|
@@ -140,6 +142,7 @@ const instance = Math.random().toString(36).slice(2, 8);
|
|
|
140
142
|
* @returns {string}
|
|
141
143
|
*/
|
|
142
144
|
export function levelChart(tr, { width = 640, height = 360, ylabel = 'voltage (V)', range, xlim, title, ytick = fmt } = {}) {
|
|
145
|
+
checkOptions('levelChart', arguments[1], ['width', 'height', 'ylabel', 'range', 'xlim', 'title', 'ytick']);
|
|
143
146
|
const id = `driftlet-plot-${instance}-${++counter}`;
|
|
144
147
|
const { x, series, regions } = tr;
|
|
145
148
|
const xs = xlim ? xlim[0] : x[0], xe = xlim ? xlim[1] : x[x.length - 1];
|
|
@@ -264,6 +267,7 @@ export function levelChart(tr, { width = 640, height = 360, ylabel = 'voltage (V
|
|
|
264
267
|
* @returns {string}
|
|
265
268
|
*/
|
|
266
269
|
export function bandDiagram(sol, opts = {}) {
|
|
270
|
+
checkOptions('bandDiagram', arguments[1], ['width', 'height', 'ylabel', 'range', 'xlim', 'title', 'ytick', 'species', 'standard', 'phi', 'energy', 'levels', 'labels', 'shifts']);
|
|
267
271
|
const { width, height, ylabel = opts.energy ? 'electron energy (eV)' : undefined, range, xlim, title, ytick, ...pick } = opts;
|
|
268
272
|
return levelChart(traces(sol, pick), { width, height, ylabel, range, xlim, title, ytick });
|
|
269
273
|
}
|
package/src/polarization.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// beside the electrode held as it is and the metal's level moved, which is what an Evans diagram
|
|
3
3
|
// plots. Where the net current crosses zero is the spot's open-circuit (mixed) potential.
|
|
4
4
|
|
|
5
|
-
import { DeviceError } from './errors.js';
|
|
5
|
+
import { DeviceError, checkOptions } from './errors.js';
|
|
6
6
|
import { FARADAY } from './constants.js';
|
|
7
7
|
import { powi, powr } from './pow.js';
|
|
8
8
|
|
|
@@ -30,6 +30,7 @@ const fail = (message) => {
|
|
|
30
30
|
* positive: into the solution); and their sum
|
|
31
31
|
*/
|
|
32
32
|
export function polarization(device, sol, where, V, opts = {}) {
|
|
33
|
+
checkOptions('polarization', arguments[4], ['surface']);
|
|
33
34
|
const model = device.model, { species, grid, RT } = model, solver = device.solver;
|
|
34
35
|
const Vs = Float64Array.from(typeof V === 'number' ? [V] : V);
|
|
35
36
|
let reactions, nodeOf, carrier, names, surface = null; // (an electrode port's coverages at the spot)
|
package/src/recorder.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// each animation frame), with the trace joined up across calls and whole solutions kept as
|
|
3
3
|
// frames, for scrubbing back through a run or plotting snapshots side by side.
|
|
4
4
|
|
|
5
|
-
import { DeviceError } from './errors.js';
|
|
5
|
+
import { DeviceError, checkOptions } from './errors.js';
|
|
6
6
|
|
|
7
7
|
const fail = (message) => {
|
|
8
8
|
throw new DeviceError(message);
|
|
@@ -18,6 +18,7 @@ const now = () => (typeof performance !== 'undefined' ? performance.now() : Date
|
|
|
18
18
|
* @param {{ every?: number, times?: number[], probes?: ({ x: number, species?: string, quantity?: 'c' | 'V' | 'phi', region?: string | number } | { interface: number, species?: string, gate?: string })[] }} [opts]
|
|
19
19
|
*/
|
|
20
20
|
export function recorder(device, opts = {}) {
|
|
21
|
+
checkOptions('recorder', arguments[1], ['every', 'times', 'probes']);
|
|
21
22
|
const { every, times, probes } = opts;
|
|
22
23
|
if (every !== undefined && times !== undefined) fail('recorder: give every or times, not both');
|
|
23
24
|
if (every !== undefined && !(every > 0 && Number.isFinite(every))) fail('recorder: every must be a time > 0 (s)');
|