driftlet 0.1.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/LICENSE +14 -0
- package/README.md +285 -0
- package/llms.txt +299 -0
- package/package.json +54 -0
- package/src/bernoulli.js +29 -0
- package/src/blockTridiagonal.js +492 -0
- package/src/constants.js +36 -0
- package/src/data.js +127 -0
- package/src/describe.js +190 -0
- package/src/device.js +954 -0
- package/src/equation.js +188 -0
- package/src/errors.js +8 -0
- package/src/fermi.js +106 -0
- package/src/grid.js +111 -0
- package/src/index.js +207 -0
- package/src/kit.js +76 -0
- package/src/live.js +217 -0
- package/src/merge.js +11 -0
- package/src/plot.js +267 -0
- package/src/solution.js +246 -0
- package/src/solver.js +2884 -0
- package/src/stack.js +172 -0
- package/src/statistics.js +593 -0
- package/src/traces.js +125 -0
- package/src/types.js +221 -0
- package/src/worker.js +24 -0
- package/types/bernoulli.d.ts +4 -0
- package/types/blockTridiagonal.d.ts +92 -0
- package/types/constants.d.ts +33 -0
- package/types/data.d.ts +73 -0
- package/types/describe.d.ts +15 -0
- package/types/device.d.ts +20 -0
- package/types/equation.d.ts +102 -0
- package/types/errors.d.ts +3 -0
- package/types/fermi.d.ts +3 -0
- package/types/grid.d.ts +28 -0
- package/types/index.d.ts +103 -0
- package/types/kit.d.ts +61 -0
- package/types/live.d.ts +101 -0
- package/types/merge.d.ts +1 -0
- package/types/plot.d.ts +60 -0
- package/types/solution.d.ts +18 -0
- package/types/solver.d.ts +342 -0
- package/types/stack.d.ts +59 -0
- package/types/statistics.d.ts +10 -0
- package/types/traces.d.ts +118 -0
- package/types/types.d.ts +574 -0
package/src/describe.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// A readable summary of a device definition, for driftlet/kit, with warnings for numbers that
|
|
2
|
+
// look like unit slips (a D in cm²/s, a μ° in eV, a capacitance in µF/cm²) and for double layers
|
|
3
|
+
// the grid won't resolve. Heuristics only: a warning is a prompt to check, never an error.
|
|
4
|
+
|
|
5
|
+
import { normalizeDevice } from './device.js';
|
|
6
|
+
import { EPS0, FARADAY, GAS_CONSTANT, AVOGADRO } from './constants.js';
|
|
7
|
+
|
|
8
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
9
|
+
|
|
10
|
+
// A length, time or other quantity with an SI prefix: 1.5e-8 → '15 n'.
|
|
11
|
+
function si(v, unit) {
|
|
12
|
+
if (v === 0 || !Number.isFinite(v)) return `${v} ${unit}`;
|
|
13
|
+
const prefixes = [[1e9, 'G'], [1e6, 'M'], [1e3, 'k'], [1, ''], [1e-3, 'm'], [1e-6, 'µ'], [1e-9, 'n'], [1e-12, 'p'], [1e-15, 'f']];
|
|
14
|
+
const a = Math.abs(v);
|
|
15
|
+
const [f, p] = prefixes.find(([f]) => a >= f * 0.9995) ?? prefixes[prefixes.length - 1];
|
|
16
|
+
return `${+(v / f).toPrecision(3)} ${p}${unit}`;
|
|
17
|
+
}
|
|
18
|
+
const num = (v) => (Math.abs(v) >= 1e4 || (Math.abs(v) < 1e-2 && v !== 0) ? v.toExponential(2) : String(+v.toPrecision(3)));
|
|
19
|
+
const perCm3 = (c) => `${(c * AVOGADRO * 1e-6).toExponential(2)} cm⁻³`;
|
|
20
|
+
|
|
21
|
+
// An equation from signed stoichiometries, for display.
|
|
22
|
+
function equation(...maps) {
|
|
23
|
+
const lhs = [], rhs = [];
|
|
24
|
+
for (const m of maps) {
|
|
25
|
+
for (const [name, nu] of Object.entries(m ?? {})) (nu < 0 ? lhs : rhs).push(`${Math.abs(nu) === 1 ? '' : `${Math.abs(nu)} `}${name}`);
|
|
26
|
+
}
|
|
27
|
+
return `${lhs.join(' + ') || '0'} = ${rhs.join(' + ') || '0'}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function drive(c) {
|
|
31
|
+
const v = (x) => (isObject(x) ? 'a waveform' : `${num(x)}`);
|
|
32
|
+
if (c?.I !== undefined) return `driven at I = ${v(c.I)} A/m²`;
|
|
33
|
+
if (c?.R !== undefined) return `a source of ${v(c.V ?? 0)} V behind R = ${num(c.R)} Ω·m²`;
|
|
34
|
+
return `held at ${v(c?.V ?? 0)} V`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Warnings for numbers that look like unit slips: SI is m, mol/m³, J/mol, m²/s, F/m², C/m³.
|
|
39
|
+
* @param {object} def device definition
|
|
40
|
+
* @returns {string[]}
|
|
41
|
+
*/
|
|
42
|
+
export function unitWarnings(def) {
|
|
43
|
+
const out = [];
|
|
44
|
+
const warn = (path, msg) => out.push(`${path}: ${msg}`);
|
|
45
|
+
if (def.T !== undefined && def.T < 60) warn('T', `${def.T} K is very cold; T is in kelvin (if that's °C, add 273.15)`);
|
|
46
|
+
if (def.T > 5000) warn('T', `${def.T} K is hotter than any device; T is in kelvin`);
|
|
47
|
+
const conc = (path, c) => {
|
|
48
|
+
if (c > 1e6) warn(path, `${num(c)} mol/m³ is over 1000 M; concentrations are mol/m³ (units.molar, and units.perCm3 for carriers)`);
|
|
49
|
+
};
|
|
50
|
+
(def.species ?? []).forEach((sp, i) => sp?.cRef !== undefined && conc(`species[${i}].cRef`, sp.cRef));
|
|
51
|
+
for (const [mname, mat] of Object.entries(def.materials ?? {})) {
|
|
52
|
+
if (!isObject(mat)) continue;
|
|
53
|
+
const path = `materials.${mname}`;
|
|
54
|
+
if (mat.epsr > 0 && mat.epsr < 1) warn(`${path}.epsr`, `${mat.epsr} is below 1; epsr is relative to vacuum`);
|
|
55
|
+
if (mat.conductor?.conductivity > 1e9) warn(`${path}.conductor.conductivity`, `${num(mat.conductor.conductivity)} S/m is beyond any metal (silver is 6.3e7 S/m)`);
|
|
56
|
+
for (const [sname, sp] of Object.entries(mat.species ?? {})) {
|
|
57
|
+
if (!isObject(sp)) continue;
|
|
58
|
+
const sp_ = `${path}.species.${sname}`;
|
|
59
|
+
const z = (def.species ?? []).find((x) => x?.name === sname)?.z;
|
|
60
|
+
const ion = z !== undefined && z !== 0 && sname !== 'e-' && sname !== 'h+';
|
|
61
|
+
if (sp.D > 1) warn(`${sp_}.D`, `${num(sp.D)} m²/s is beyond any real diffusivity; D is m²/s (units.cm2PerS)`);
|
|
62
|
+
else if (ion && sp.D > 1e-7) warn(`${sp_}.D`, `${num(sp.D)} m²/s is fast for an ion (H⁺ in water is 9.3e-9 m²/s); D is m²/s, so cm²/s needs units.cm2PerS`);
|
|
63
|
+
if (sp.mu0 !== 0 && Math.abs(sp.mu0) < 2000) warn(`${sp_}.mu0`, `${sp.mu0} J/mol is small for a standard potential; mu0 is J/mol (kJ/mol × 1000, units.eV for per-particle energies, × F for volts)`);
|
|
64
|
+
if (Math.abs(sp.mu0) > 1e7) warn(`${sp_}.mu0`, `${num(sp.mu0)} J/mol is over 100 eV per particle; mu0 is J/mol`);
|
|
65
|
+
if (sp.cRef !== undefined) conc(`${sp_}.cRef`, sp.cRef);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
(def.regions ?? []).forEach((reg, r) => {
|
|
69
|
+
if (!isObject(reg)) return;
|
|
70
|
+
const path = `regions[${r}]`;
|
|
71
|
+
if (reg.length > 1) warn(`${path}.length`, `${num(reg.length)} m is over a metre; lengths are m (units.um, units.nm)`);
|
|
72
|
+
if (reg.length > 0 && reg.length < 1e-10) warn(`${path}.length`, `${num(reg.length)} m is less than an atom; lengths are m`);
|
|
73
|
+
if (Math.abs(reg.fixedCharge) > 1e10) warn(`${path}.fixedCharge`, `${num(reg.fixedCharge)} C/m³ is over 6e22 cm⁻³ of charge; fixedCharge is C/m³ (units.perCm3(N) * FARADAY)`);
|
|
74
|
+
for (const [sname, c] of Object.entries(reg.c0 ?? {})) conc(`${path}.c0.${sname}`, c);
|
|
75
|
+
});
|
|
76
|
+
const capacitance = (path, phi) => {
|
|
77
|
+
if (isObject(phi) && phi.C > 10) warn(`${path}.phi.C`, `${num(phi.C)} F/m² is over 1000 µF/cm²; C is F/m² (1 µF/cm² = 0.01 F/m²)`);
|
|
78
|
+
};
|
|
79
|
+
const kinetics = (path, reactions) => {
|
|
80
|
+
(reactions ?? []).forEach((rx, k) => {
|
|
81
|
+
if (rx?.k0 > 10) warn(`${path}.reactions[${k}].k0`, `${num(rx.k0)} mol/(m²·s) is an exchange current of ${num(rx.k0 * FARADAY)} A/m² per electron; k0 is mol/(m²·s)`);
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
(def.interfaces ?? []).forEach((f, k) => {
|
|
85
|
+
if (!isObject(f)) return;
|
|
86
|
+
capacitance(`interfaces[${k}]`, f.phi);
|
|
87
|
+
kinetics(`interfaces[${k}]`, f.reactions);
|
|
88
|
+
});
|
|
89
|
+
for (const side of ['left', 'right']) {
|
|
90
|
+
const c = def.contacts?.[side];
|
|
91
|
+
if (!isObject(c)) continue;
|
|
92
|
+
capacitance(`contacts.${side}`, c.phi);
|
|
93
|
+
for (const [sname, v] of Object.entries(c.bath?.c ?? {})) conc(`contacts.${side}.bath.c.${sname}`, v);
|
|
94
|
+
if (typeof c.V === 'number' && Math.abs(c.V) > 50) warn(`contacts.${side}.V`, `${c.V} V is a very large bias; voltages are V`);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A readable summary of a device: its regions (with doping, Debye lengths and the grid at their
|
|
101
|
+
* ends), faces, contacts and reactions, its characteristic times, and warnings (unit slips, and
|
|
102
|
+
* double layers coarser than the grid). It validates the definition first, so an invalid one
|
|
103
|
+
* throws its DeviceError.
|
|
104
|
+
* @param {object} def device definition
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
export function describe(def) {
|
|
108
|
+
const model = normalizeDevice(def);
|
|
109
|
+
const { species, materials, regions, grid } = model;
|
|
110
|
+
const RT = GAS_CONSTANT * model.T;
|
|
111
|
+
const lines = [];
|
|
112
|
+
const warnings = unitWarnings(def);
|
|
113
|
+
const total = grid.x[grid.x.length - 1] - grid.x[0];
|
|
114
|
+
lines.push(`${regions.length} region${regions.length > 1 ? 's' : ''}, ${si(total, 'm')}, ${grid.x.length} nodes, T = ${model.T} K`);
|
|
115
|
+
lines.push(`species: ${species.map((sp) => `${sp.name} (z = ${sp.z > 0 ? '+' : ''}${sp.z})`).join(', ')}`);
|
|
116
|
+
|
|
117
|
+
lines.push('regions:');
|
|
118
|
+
const times = [];
|
|
119
|
+
regions.forEach((reg, r) => {
|
|
120
|
+
const mat = materials[reg.material];
|
|
121
|
+
const a = grid.regionStart[r], b = grid.regionEnd[r];
|
|
122
|
+
const head = ` ${reg.name} [${mat.name}] ${si(reg.length, 'm')}`;
|
|
123
|
+
if (mat.conductor) {
|
|
124
|
+
lines.push(`${head}: conductor, ${species[mat.conductor.i].name} at ${num(mat.conductor.sigma)} S/m`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const parts = [mat.epsr > 0 ? `εr ${mat.epsr}` : 'strictly neutral (ε = 0)'];
|
|
128
|
+
if (reg.fixedCharge !== 0) parts.push(`fixed charge ${reg.fixedCharge > 0 ? 'donor-like' : 'acceptor-like'} ${perCm3(Math.abs(reg.fixedCharge) / FARADAY)}`);
|
|
129
|
+
// The screening concentration: Σ z²c from the initial composition, else the fixed charge's.
|
|
130
|
+
let zzc = 0;
|
|
131
|
+
species.forEach((sp, i) => {
|
|
132
|
+
if (mat.present[i] && Number.isFinite(reg.c0[i])) zzc += sp.z * sp.z * reg.c0[i];
|
|
133
|
+
});
|
|
134
|
+
if (zzc === 0) zzc = Math.abs(reg.fixedCharge) / FARADAY;
|
|
135
|
+
const Dmax = Math.max(0, ...species.map((sp, i) => (mat.present[i] && sp.z !== 0 ? mat.D[i] : 0)));
|
|
136
|
+
const Dmin = Math.min(...species.map((sp, i) => (mat.present[i] && mat.D[i] > 0 ? mat.D[i] : Infinity)));
|
|
137
|
+
if (mat.epsr > 0 && zzc > 0) {
|
|
138
|
+
const lambda = Math.sqrt((mat.epsr * EPS0 * RT) / (FARADAY * FARADAY * zzc));
|
|
139
|
+
// Double layers form at faces that aren't neutral, and at gate or pinned contacts.
|
|
140
|
+
const pins = (side) => ['capacitive', 'pinned'].includes(model.contacts[side].phi.type);
|
|
141
|
+
const atLeft = r === 0 ? pins('left') : model.interfaces[r - 1].phi.type !== 'neutral';
|
|
142
|
+
const atRight = r === regions.length - 1 ? pins('right') : model.interfaces[r].phi.type !== 'neutral';
|
|
143
|
+
const hEnd = Math.max(atLeft ? grid.segLength[a] : 0, atRight ? grid.segLength[b - 1] : 0);
|
|
144
|
+
parts.push(`Debye length ${si(lambda, 'm')}${hEnd > 0 ? ` (end cells ${si(hEnd, 'm')})` : ''}`);
|
|
145
|
+
if (lambda < 2e-10) {
|
|
146
|
+
warnings.push(`${reg.name}: a Debye length of ${si(lambda, 'm')} is below atomic size, where a continuum double layer means little; consider epsr: 0 (strict neutrality)`);
|
|
147
|
+
} else if (hEnd > lambda) {
|
|
148
|
+
warnings.push(`${reg.name}: end cells of ${si(hEnd, 'm')} are coarser than the Debye length, ${si(lambda, 'm')}; a double layer there won't be resolved (grid.hmin)`);
|
|
149
|
+
}
|
|
150
|
+
if (Dmax > 0) times.push(` dielectric relaxation in ${reg.name}: ~${si((lambda * lambda) / Dmax, 's')}`);
|
|
151
|
+
}
|
|
152
|
+
if (Number.isFinite(Dmin)) times.push(` diffusion across ${reg.name}: L²/D up to ${si((reg.length * reg.length) / Dmin, 's')}`);
|
|
153
|
+
lines.push(`${head}: ${parts.join(', ')}; ${b - a} cells`);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const idefs = def.interfaces ?? [];
|
|
157
|
+
if (regions.length > 1) {
|
|
158
|
+
lines.push('faces:');
|
|
159
|
+
model.interfaces.forEach((itf, f) => {
|
|
160
|
+
const L = regions[f], R = regions[f + 1];
|
|
161
|
+
const idef = isObject(idefs[f]) ? idefs[f] : {};
|
|
162
|
+
if (L.material === R.material && itf.phi.type === 'pinned' && itf.dipole === 0 && !(idef.reactions ?? []).length && idef.species === undefined) {
|
|
163
|
+
lines.push(` ${L.name} | ${R.name}: same material`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const law = itf.phi.type === 'capacitive' ? `capacitive, C = ${num(itf.phi.C)} F/m²` : itf.phi.type;
|
|
167
|
+
const align = itf.conductor ? (itf.phi.type === 'capacitive' ? `, zeroCharge ${num(itf.zeroCharge)} V` : '') : itf.phi.type === 'neutral' ? '' : `, dipole ${num(itf.dipole)} V`;
|
|
168
|
+
const blocked = species.filter((sp, i) => itf.links[i].type === 'blocked' && materials[L.material].present[i] && materials[R.material].present[i]).map((sp) => sp.name);
|
|
169
|
+
const rx = (idef.reactions ?? []).map((r) => r.equation ?? equation(r.left, r.right));
|
|
170
|
+
lines.push(` ${L.name} | ${R.name}: ${law}${align}${blocked.length ? `; blocked: ${blocked.join(', ')}` : ''}${rx.length ? `; reactions: ${rx.join('; ')}` : ''}`);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
lines.push('contacts:');
|
|
175
|
+
for (const side of ['left', 'right']) {
|
|
176
|
+
const cdef = def.contacts?.[side];
|
|
177
|
+
const ct = model.contacts[side];
|
|
178
|
+
const linked = species.filter((sp, i) => ct.species[i].type !== 'blocked').map((sp) => sp.name);
|
|
179
|
+
const term = ct.terminal === null ? '' : ` (terminal ${species[ct.terminal].name})`;
|
|
180
|
+
lines.push(` ${side}: ${drive(cdef)}${term}; ${linked.length ? `exchanges ${linked.join(', ')}` : 'exchanges nothing'}; φ ${ct.phi.type}`);
|
|
181
|
+
}
|
|
182
|
+
(def.ports ?? []).forEach((p, k) => lines.push(` port ${model.ports[k].name}: ${drive(p)}, in ${regions[model.ports[k].region].name}`));
|
|
183
|
+
if ((def.bulkReactions ?? []).length) {
|
|
184
|
+
lines.push('bulk reactions:');
|
|
185
|
+
for (const rx of def.bulkReactions) lines.push(` ${rx.equation ?? equation(rx.nu)} in ${Object.keys(rx.kf ?? {}).join(', ')}`);
|
|
186
|
+
}
|
|
187
|
+
if (times.length) lines.push('time scales:', ...times);
|
|
188
|
+
if (warnings.length) lines.push('warnings:', ...warnings.map((w) => ` ${w}`));
|
|
189
|
+
return lines.join('\n');
|
|
190
|
+
}
|