driftlet 0.16.4 → 1.0.1
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 +3 -3
- package/docs/device.md +28 -14
- package/docs/kit.md +3 -1
- package/docs/numerics.md +12 -0
- package/docs/statistics.md +2 -1
- package/docs/templates.md +260 -0
- package/llms.txt +106 -300
- package/package.json +15 -3
- package/src/channels.js +6 -4
- package/src/checks.js +7 -2
- package/src/data.js +5 -1
- package/src/device.js +10 -4
- 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 +17 -7
- 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 +4 -3
- package/src/recorder.js +2 -1
- package/src/solution.js +20 -5
- package/src/solver.js +70 -16
- 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 +5 -2
- package/types/channels.d.ts +3 -3
- package/types/checks.d.ts +1 -1
- 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 +2 -5
- 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 +2 -0
- package/types/sources.d.ts +7 -7
- package/types/traces.d.ts +1 -1
- package/types/types.d.ts +11 -2
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
|
|
|
@@ -48,6 +49,7 @@ const sig = (v, p = 3) => (v === 0 || !Number.isFinite(v) ? String(v) : Math.abs
|
|
|
48
49
|
* @returns {{ ok: boolean, items: CheckItem[], text: string }}
|
|
49
50
|
*/
|
|
50
51
|
export function check(device, sol, { refine = true, tol = 1e-2, order = true } = {}) {
|
|
52
|
+
checkOptions('check', arguments[2], ['refine', 'tol', 'order']);
|
|
51
53
|
sol ??= device.solution();
|
|
52
54
|
const { model, solver, def } = device;
|
|
53
55
|
const steady = sol.steady ?? solver.atSteady;
|
|
@@ -170,7 +172,10 @@ function balance(device, sol) {
|
|
|
170
172
|
const size = (l) => Math.max(0, ...l.terms.map((t) => Math.abs(t.rate)));
|
|
171
173
|
const natural = naturalScales(device, sol), inSolution = naturalScales(device, sol, false); // (partners' scale: not a metal's conductance)
|
|
172
174
|
const scale = (l) => Math.max(natural[l.i], ...l.terms.flatMap((t) => (partners.get(t.what) ?? []).map((j) => inSolution[j])));
|
|
173
|
-
|
|
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)));
|
|
174
179
|
let worst = 0;
|
|
175
180
|
const lines = [];
|
|
176
181
|
for (const l of moving) {
|
|
@@ -379,7 +384,7 @@ function orderCheck(device, sol, gross) {
|
|
|
379
384
|
? 'the same steady state with the species listed in reverse'
|
|
380
385
|
: ok === null
|
|
381
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`
|
|
382
|
-
: `listed in reverse, the species give a different steady state: ${what}. One of them is not steady, though its solve converged: compare with a long advance(), and please report the device.`,
|
|
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.`,
|
|
383
388
|
details: { diffs },
|
|
384
389
|
};
|
|
385
390
|
}
|
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
|
@@ -355,8 +355,8 @@ export function normalizeDevice(def) {
|
|
|
355
355
|
// A contact that passes nothing can't be driven by a current.
|
|
356
356
|
for (const side of ['left', 'right']) {
|
|
357
357
|
const ct = contacts[side];
|
|
358
|
-
const passes = ct.species.some((l) => l.type !== 'blocked') || ct.phi.type === 'capacitive' || ct.phi.type === 'pinned';
|
|
359
|
-
need(passes || ct.drive.kind === 'V', `contacts.${side}.I: this contact passes no current (no linked species, no gate)`);
|
|
358
|
+
const passes = ct.species.some((l, i) => l.type !== 'blocked' && species[i].z !== 0) || ct.phi.type === 'capacitive' || ct.phi.type === 'pinned';
|
|
359
|
+
need(passes || ct.drive.kind === 'V', `contacts.${side}.I: this contact passes no current (no linked charged species, no gate)`);
|
|
360
360
|
ct.passes = passes;
|
|
361
361
|
}
|
|
362
362
|
checkLevel(terminals.map((t) => t.drive), terminals.map((t) => (t.kind === 'port' ? ports[t.index] : contacts[t.side])));
|
|
@@ -889,7 +889,13 @@ function portReactions(pdef, path, mat, terminal, species, speciesIndex, RT, sur
|
|
|
889
889
|
need(part.some((p) => p.side === 0 || p.side === 2), `${rpath}: no species of the port's region or its surface takes part`);
|
|
890
890
|
need(charge === 0, `${rpath}: charge is not balanced (Σ ν z = ${charge})`);
|
|
891
891
|
need(rdef.bare === undefined || typeof rdef.bare === 'boolean', `${rpath}.bare must be true or false`);
|
|
892
|
-
|
|
892
|
+
const alpha = transferCoefficient(rdef.alpha, `${rpath}.alpha`), bare = rdef.bare === true;
|
|
893
|
+
// Langmuir sites: the reaction fills Δ of them going forward, and its transition state holds
|
|
894
|
+
// m bare ones (those it fills, or one if it runs only on bare metal), so the forward rate
|
|
895
|
+
// goes as θ₀^m and the backward as θ₀^(m−Δ): the prefactor's θ₀^(m(1−α)+(m−Δ)α), with each
|
|
896
|
+
// surface species at its coverage θ.
|
|
897
|
+
const fills = part.reduce((t, p) => t + (p.side === 2 ? p.nu : 0), 0), m = Math.max(fills, 0, bare ? 1 : 0);
|
|
898
|
+
return { part, fixedA, k0: positive(rdef.k0, `${rpath}.k0 (mol/(m²·s))`), alpha, bare, sites: m * (1 - alpha) + (m - fills) * alpha };
|
|
893
899
|
});
|
|
894
900
|
}
|
|
895
901
|
|
|
@@ -1316,7 +1322,7 @@ function normalizeContact(cdef, side, region, materials, species, speciesIndex,
|
|
|
1316
1322
|
fixed !== 0
|
|
1317
1323
|
? `${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
1324
|
`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)`,
|
|
1325
|
+
: `${path}.bath: composition is not neutral (net ${charge} mol/m³ of charge): add the counter-ions' share`,
|
|
1320
1326
|
);
|
|
1321
1327
|
// The reference species pins the bath's φ; every other species follows from composition.
|
|
1322
1328
|
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
|
|
|
@@ -179,6 +183,7 @@ export class Device {
|
|
|
179
183
|
* @returns {Solution}
|
|
180
184
|
*/
|
|
181
185
|
solve(opts) {
|
|
186
|
+
options(opts, 'solve', ['maxSteps', 'tol', 'continuation']);
|
|
182
187
|
// A solve that fails reports the state it failed in, and leaves the device as it was: a
|
|
183
188
|
// device with no steady state (a host filling at a current) can still advance() from there.
|
|
184
189
|
const snap = this.solver._snapshot();
|
|
@@ -196,6 +201,7 @@ export class Device {
|
|
|
196
201
|
* @returns {Solution}
|
|
197
202
|
*/
|
|
198
203
|
step(dt, opts) {
|
|
204
|
+
options(opts, 'step', ['method']);
|
|
199
205
|
return makeSolution(this.solver, this.solver.advance(dt, opts));
|
|
200
206
|
}
|
|
201
207
|
|
|
@@ -211,22 +217,26 @@ export class Device {
|
|
|
211
217
|
* @returns {Solution}
|
|
212
218
|
*/
|
|
213
219
|
advance(tEnd, opts) {
|
|
220
|
+
options(opts, 'advance', ['tol', 'dt0', 'dtMax', 'budgetMs', 'maxSteps', 'method', 'probes']);
|
|
214
221
|
return makeSolution(this.solver, this.solver.integrate(tEnd, opts));
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
/**
|
|
218
225
|
* Small-signal impedance Z(f) about the steady state (solved first), at one terminal: its
|
|
219
226
|
* held voltage is perturbed (or its driven current), the others keep their drives.
|
|
220
|
-
* 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`.
|
|
221
228
|
* @param {ArrayLike<number>} frequencies Hz
|
|
222
229
|
* @param {{ terminal?: string, profiles?: boolean }} [opts] the terminal (default 'right'), and
|
|
223
230
|
* whether to return complex profiles per frequency
|
|
224
231
|
* @returns {import('./types.js').ImpedanceResult}
|
|
225
232
|
*/
|
|
226
233
|
impedance(frequencies, opts) {
|
|
234
|
+
options(opts, 'impedance', ['terminal', 'profiles']);
|
|
227
235
|
const steady = this.solver.solveSteady();
|
|
228
236
|
if (!steady.converged) throw new SolverError('impedance: the steady state did not converge');
|
|
229
|
-
|
|
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 };
|
|
230
240
|
}
|
|
231
241
|
|
|
232
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)
|
|
@@ -114,10 +115,10 @@ export function polarization(device, sol, where, V, opts = {}) {
|
|
|
114
115
|
const out = parts.map(({ rx, k, base, s, q, pref, surf }) => {
|
|
115
116
|
const rate = Float64Array.from(Vs, (v, j) => {
|
|
116
117
|
const { lnA, bare } = at[j];
|
|
117
|
-
let a = base + s * v, p = pref * (rx.
|
|
118
|
+
let a = base + s * v, p = pref * (rx.sites ? powr(bare, rx.sites) : 1);
|
|
118
119
|
for (const u of surf) {
|
|
119
120
|
a -= u.nu * lnA[u.s];
|
|
120
|
-
p *= Math.exp(u.e * lnA[u.s]);
|
|
121
|
+
p *= Math.exp(u.e * lnA[u.s]) * powr(bare, u.e); // its coverage θ = θ₀ a
|
|
121
122
|
}
|
|
122
123
|
return rx.vmax ? p * -Math.expm1(-a) : p * (Math.exp(rx.alpha * a) - Math.exp(-(1 - rx.alpha) * a));
|
|
123
124
|
});
|
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)');
|
package/src/solution.js
CHANGED
|
@@ -87,7 +87,7 @@ export function makeSolution(solver, result = {}) {
|
|
|
87
87
|
}
|
|
88
88
|
solver._assembleBookkeeping(solver.lastDt);
|
|
89
89
|
sol.contacts = {};
|
|
90
|
-
sol.
|
|
90
|
+
sol.plates = {};
|
|
91
91
|
for (const side of ['left', 'right']) {
|
|
92
92
|
const ct = contacts[side];
|
|
93
93
|
const flux = {};
|
|
@@ -107,7 +107,7 @@ export function makeSolution(solver, result = {}) {
|
|
|
107
107
|
if (ct.phi.type === 'capacitive' || ct.phi.type === 'pinned') {
|
|
108
108
|
// Charge on the gate (or metal) plate, per area if the device is planar: +D at the left, −D
|
|
109
109
|
// at the right.
|
|
110
|
-
sol.
|
|
110
|
+
sol.plates[side] = { V, D, charge: side === 'left' ? D : -D };
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
sol.current = sol.contacts.right.current;
|
|
@@ -313,11 +313,24 @@ export function makeSolution(solver, result = {}) {
|
|
|
313
313
|
// refined. Flag it where it lowers that capacitance by over 2%. (Where the box is the smaller
|
|
314
314
|
// part, as an ε = 0 metal's against a semiconductor's depletion, it's a fine stand-in for the
|
|
315
315
|
// metal's own screening.)
|
|
316
|
-
|
|
316
|
+
// And a reaction at the face sees that box's composition, which holding the charge σ shifts
|
|
317
|
+
// by about z·σ/(Cbox·V_T) for an ion of charge z: a Frumkin effect set by the mesh, which grows
|
|
318
|
+
// without limit as the box shrinks. Flag it past 1%.
|
|
319
|
+
const boxed = (g, h, phi, other, where, sigma = 0, reacting = []) => {
|
|
317
320
|
const mat = model.materials[model.regions[grid.nodeRegion[g]].material];
|
|
318
321
|
if (mat.conductor || mat.epsr !== 0 || solver.phiUndefined?.[g]) return;
|
|
319
322
|
const s2 = solver.screening(g), Cbox = (F * F * s2 * h) / (2 * RT);
|
|
320
323
|
if (!(s2 > 0)) return;
|
|
324
|
+
const ion = reacting.filter((i) => species[i].z !== 0).reduce((a, i) => (a < 0 || Math.abs(species[i].z) > Math.abs(species[a].z) ? i : a), -1);
|
|
325
|
+
const shift = ion < 0 ? 0 : (Math.abs(species[ion].z * sigma) / Cbox) * (F / RT);
|
|
326
|
+
if (shift > 0.01) {
|
|
327
|
+
sol.warnings.push(
|
|
328
|
+
`${where}: the reaction here sees the strictly neutral cell beside the face (${h.toExponential(2)} m), which holds the face's charge as a diffuse layer that wide: ` +
|
|
329
|
+
`${species[ion].name} there is shifted by about ${(100 * shift).toPrecision(2)}%, a Frumkin effect set by the mesh that grows as the cell shrinks. ` +
|
|
330
|
+
`Set the face's zeroCharge near its rest potential (so it holds little charge), coarsen that cell to ${((200 * Math.abs(species[ion].z * sigma)) / (F * s2)).toExponential(1)} m or more, ` +
|
|
331
|
+
'or resolve the double layer (epsr > 0, hmin below the Debye length).',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
321
334
|
let C = phi.C;
|
|
322
335
|
if (phi.type === 'pinned') {
|
|
323
336
|
const om = other < 0 ? null : model.materials[model.regions[grid.nodeRegion[other]].material];
|
|
@@ -338,8 +351,10 @@ export function makeSolution(solver, result = {}) {
|
|
|
338
351
|
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1];
|
|
339
352
|
check(gL, grid.segLength[gL - 1], `interfaces[${f}] (left side)`);
|
|
340
353
|
check(gR, grid.segLength[gR], `interfaces[${f}] (right side)`);
|
|
341
|
-
|
|
342
|
-
|
|
354
|
+
const reacting = (side) => itf.reactions.flatMap((rx) => rx.part.filter((p) => p.side === side).map((p) => p.i));
|
|
355
|
+
const sigma = sol.interfaces[f].D;
|
|
356
|
+
boxed(gL, grid.segLength[gL - 1], itf.phi, gR, `interfaces[${f}] (left side)`, sigma, reacting(0));
|
|
357
|
+
boxed(gR, grid.segLength[gR], itf.phi, gL, `interfaces[${f}] (right side)`, sigma, reacting(1));
|
|
343
358
|
});
|
|
344
359
|
const resolves = (ct) => ct.phi.type === 'capacitive' || ct.phi.type === 'pinned';
|
|
345
360
|
if (resolves(contacts.left)) {
|