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/traces.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Plot-ready traces of a solution, for driftlet/kit: the level diagram's lines with labels,
|
|
2
|
+
// line kinds and a colour slot that follows the species, the regions as bands, and a suggested
|
|
3
|
+
// range. Plain data, for any plotting toolkit (driftlet/plot draws it as SVG).
|
|
4
|
+
|
|
5
|
+
import { level } from './equation.js';
|
|
6
|
+
|
|
7
|
+
const finite = (a) => {
|
|
8
|
+
for (let k = 0; k < a.length; k++) if (Number.isFinite(a[k])) return true;
|
|
9
|
+
return false;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One line of a level diagram.
|
|
14
|
+
* @typedef {object} Trace
|
|
15
|
+
* @property {string} id e.g. 'V:e-', 'Vstd:e-', 'phi', 'level:0'
|
|
16
|
+
* @property {string} label e.g. 'V_{e⁻}', 'V°_{SO₄²⁻}' (subscripts marked TeX-style, as `labelParts()` in driftlet/plot reads them)
|
|
17
|
+
* @property {'level' | 'standard' | 'phi' | 'redox' | 'redox-standard'} kind a species voltage, its
|
|
18
|
+
* standard level, φ, or a half-reaction's level or standard level
|
|
19
|
+
* @property {string} [species]
|
|
20
|
+
* @property {'electron' | 'cation' | 'anion' | 'redox' | 'phi'} role what it is, for its colour:
|
|
21
|
+
* electrons, cations (holes too), anions, redox levels, φ
|
|
22
|
+
* @property {number} slot its colour within the role: a species' place among the solution's
|
|
23
|
+
* species of that role, so a colour follows its species whichever lines are shown; a redox
|
|
24
|
+
* level's place among the distinct half-reactions asked for (a couple's level and standard
|
|
25
|
+
* level share one)
|
|
26
|
+
* @property {number} [shift] a display offset added to all of a species' lines, V
|
|
27
|
+
* @property {Float64Array} y V, NaN where undefined (a break in the line)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A charged species' colour role and slot in level diagrams: electrons (`e-`), cations (holes
|
|
32
|
+
* too), or anions, and its place among the solution's species of that role.
|
|
33
|
+
* @param {import('./types.js').Solution} sol
|
|
34
|
+
* @param {string} name
|
|
35
|
+
* @returns {{ role: 'electron' | 'cation' | 'anion', slot: number }}
|
|
36
|
+
*/
|
|
37
|
+
export function speciesRole(sol, name) {
|
|
38
|
+
const z = Object.fromEntries(sol.species.map((sp) => [sp.name, sp.z]));
|
|
39
|
+
const role = (n) => (n === 'e-' ? 'electron' : z[n] > 0 ? 'cation' : 'anion');
|
|
40
|
+
const charged = sol.species.filter((sp) => sp.z !== 0).map((sp) => sp.name);
|
|
41
|
+
return { role: role(name), slot: charged.filter((n) => role(n) === role(name)).indexOf(name) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SUB = '₀₁₂₃₄₅₆₇₈₉', SUP = '⁰¹²³⁴⁵⁶⁷⁸⁹';
|
|
45
|
+
/**
|
|
46
|
+
* A species name typeset for labels, given its charge: the charge as a superscript and the counts
|
|
47
|
+
* in its formula as subscripts ('SO42-' with z = −2 is 'SO₄²⁻', 'Fe3+' with z = 3 is 'Fe³⁺').
|
|
48
|
+
* A name that doesn't end in its charge is returned as it is.
|
|
49
|
+
* @param {string} name
|
|
50
|
+
* @param {number} z
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
export function typeset(name, z) {
|
|
54
|
+
const charge = z === 0 ? '' : `${Math.abs(z) > 1 ? Math.abs(z) : ''}${z > 0 ? '+' : '-'}`;
|
|
55
|
+
if (!name.endsWith(charge)) return name;
|
|
56
|
+
const formula = name.slice(0, name.length - charge.length).replace(/([A-Za-z)\]])(\d+)/g, (_, a, d) => a + [...d].map((c) => SUB[+c]).join(''));
|
|
57
|
+
return formula + [...charge].map((c) => (c === '+' ? '⁺' : c === '-' ? '⁻' : SUP[+c])).join('');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The level diagram of a solution as traces: per charged species, its voltage V_i (`level`) and
|
|
62
|
+
* its standard level V°_i (`standard`, the band edge for e⁻ and h⁺), optionally φ and
|
|
63
|
+
* half-reactions' levels. Doubled interface nodes share an x, so steps draw as vertical lines.
|
|
64
|
+
* @param {import('./types.js').Solution} sol
|
|
65
|
+
* @param {{ species?: string[], standard?: boolean, phi?: boolean | string[],
|
|
66
|
+
* levels?: { half: { equation: string, fixed?: Record<string, number> }, label?: string, standard?: boolean }[],
|
|
67
|
+
* labels?: Record<string, string>, shifts?: Record<string, number> }} [opts]
|
|
68
|
+
* species to show (default: every charged one), whether to show standard levels (default true)
|
|
69
|
+
* and φ (default false; or a list of regions, by name or material, to draw it in only, say an
|
|
70
|
+
* insulator's), half-reaction levels, labels to use by trace id
|
|
71
|
+
* ({ 'V:e-': 'Fermi level' }), and display offsets per species (V), which move all of a
|
|
72
|
+
* species' lines together so that widely separated species can share one readable plot
|
|
73
|
+
* @returns {{ x: Float64Array, series: Trace[], regions: { name: string, material: string, x0: number, x1: number }[],
|
|
74
|
+
* faces: number[], range: [number, number] }}
|
|
75
|
+
*/
|
|
76
|
+
export function traces(sol, { species, standard = true, phi = false, levels = [], labels = {}, shifts = {} } = {}) {
|
|
77
|
+
const names = Object.keys(sol.V);
|
|
78
|
+
const role = (name) => speciesRole(sol, name).role;
|
|
79
|
+
const slotOf = (name) => speciesRole(sol, name).slot;
|
|
80
|
+
const shown = species ?? names.filter((name) => finite(sol.V[name]));
|
|
81
|
+
const zOf = Object.fromEntries(sol.species.map((sp) => [sp.name, sp.z]));
|
|
82
|
+
const pretty = (name) => (name in zOf ? typeset(name, zOf[name]) : name);
|
|
83
|
+
const series = [];
|
|
84
|
+
for (const name of shown) {
|
|
85
|
+
if (!names.includes(name)) throw new Error(`traces: no charged species '${name}' in the solution`);
|
|
86
|
+
const shift = shifts[name] ?? 0;
|
|
87
|
+
const at = (y) => (shift ? y.map((v) => v + shift) : y);
|
|
88
|
+
const common = { species: name, role: role(name), slot: slotOf(name), ...(shift ? { shift } : {}) };
|
|
89
|
+
if (finite(sol.V[name])) series.push({ id: `V:${name}`, label: `V_{${pretty(name)}}`, kind: 'level', ...common, y: at(sol.V[name]) });
|
|
90
|
+
if (standard && finite(sol.Vstd[name])) series.push({ id: `Vstd:${name}`, label: `V°_{${pretty(name)}}`, kind: 'standard', ...common, y: at(sol.Vstd[name]) });
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(phi)) {
|
|
93
|
+
const known = sol.regions.flatMap((r) => [r.name, r.material]);
|
|
94
|
+
for (const name of phi) if (!known.includes(name)) throw new Error(`traces: no region or material '${name}' to draw φ in (${[...new Set(known)].join(', ')})`);
|
|
95
|
+
const inside = sol.regions.map((r) => phi.includes(r.name) || phi.includes(r.material));
|
|
96
|
+
series.push({ id: 'phi', label: 'φ', kind: 'phi', role: 'phi', slot: 0, y: Array.from(sol.phi, (v, g) => (inside[sol.region[g]] ? v : NaN)) });
|
|
97
|
+
} else if (phi) series.push({ id: 'phi', label: 'φ', kind: 'phi', role: 'phi', slot: 0, y: sol.phi });
|
|
98
|
+
const couples = [...new Set(levels.map((lv) => lv.half.equation))];
|
|
99
|
+
levels.forEach((lv, k) => {
|
|
100
|
+
const y = level(sol, lv.half, { standard: lv.standard });
|
|
101
|
+
series.push({
|
|
102
|
+
id: `level:${k}`,
|
|
103
|
+
label: lv.label ?? `${lv.standard ? 'V° ' : ''}${lv.half.equation.split(' ').map(pretty).join(' ')}`,
|
|
104
|
+
kind: lv.standard ? 'redox-standard' : 'redox',
|
|
105
|
+
role: 'redox',
|
|
106
|
+
slot: couples.indexOf(lv.half.equation),
|
|
107
|
+
y,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
for (const s of series) {
|
|
111
|
+
if (labels[s.id] !== undefined) s.label = labels[s.id];
|
|
112
|
+
if (s.shift) s.label += ` ⌇${s.shift > 0 ? '+' : '−'}${Math.abs(s.shift)} V`;
|
|
113
|
+
}
|
|
114
|
+
let lo = Infinity, hi = -Infinity;
|
|
115
|
+
for (const s of series) {
|
|
116
|
+
for (const v of s.y) {
|
|
117
|
+
if (v < lo) lo = v;
|
|
118
|
+
if (v > hi) hi = v;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!(hi > lo)) [lo, hi] = Number.isFinite(lo) ? [lo - 0.5, lo + 0.5] : [-1, 1];
|
|
122
|
+
const pad = 0.06 * (hi - lo);
|
|
123
|
+
const regions = sol.regions.map((r) => ({ ...r }));
|
|
124
|
+
return { x: sol.x, series, regions, faces: regions.slice(1).map((r) => r.x0), range: [lo - pad, hi + pad] };
|
|
125
|
+
}
|
package/src/types.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// JSDoc types for device definitions and solutions (for editors; no runtime content).
|
|
2
|
+
// Reference: docs/device.md. Units are SI throughout.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object} SpeciesDef
|
|
6
|
+
* @property {string} name
|
|
7
|
+
* @property {number} z charge number (integer; 0 for neutral species)
|
|
8
|
+
* @property {number} [cRef] default reference concentration, mol/m³
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} MaterialSpeciesDef
|
|
13
|
+
* @property {number} D coefficient in N = −(D c/RT)∇μ̄, m²/s
|
|
14
|
+
* @property {number} mu0 standard chemical potential, J/mol
|
|
15
|
+
* @property {number} [cRef] concentration that mu0 refers to, mol/m³
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Statistics model: how the listed species' concentrations follow from their reduced
|
|
20
|
+
* potentials ζ = (μ̄ − zFφ − μ°)/RT. Species not in any model are ideal, c = c_ref e^ζ.
|
|
21
|
+
* @typedef {(
|
|
22
|
+
* { type: 'fermi-dirac', species: string[], order?: 0.5 | 0 } |
|
|
23
|
+
* { type: 'lattice', species: string[], cMax: number } |
|
|
24
|
+
* { type: 'redlich-kister', species: [string], cMax: number, A?: number[] } |
|
|
25
|
+
* { type: 'debye-huckel', species: string[], epsr?: number, a?: number } |
|
|
26
|
+
* { type: 'insertion', species: [string, string], cMax: number, A?: number[],
|
|
27
|
+
* ocv?: { x: number[], E: number[], muRef: number } } |
|
|
28
|
+
* { type: 'custom', species: string[],
|
|
29
|
+
* evaluate: (zeta: number[]) => { c: number[], dcdzeta: number[][] } }
|
|
30
|
+
* )} StatisticsDef
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A material: either ordinary (epsr, species, optional statistics) or a conductor.
|
|
35
|
+
* @typedef {object} MaterialDef
|
|
36
|
+
* @property {number} [epsr] relative permittivity; 0 makes the material strictly neutral
|
|
37
|
+
* @property {Record<string, MaterialSpeciesDef>} [species] species present in this material
|
|
38
|
+
* @property {StatisticsDef[]} [statistics] non-ideal statistics (default: all ideal)
|
|
39
|
+
* @property {{ species: string, conductivity: number }} [conductor] a metal or fast ion
|
|
40
|
+
* conductor: its one carrier and conductivity (S/m); its only level is the carrier's (a metal's
|
|
41
|
+
* Fermi level), and φ is undefined inside
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} GridOptions
|
|
46
|
+
* @property {number} [hmin] cell size at region ends, m
|
|
47
|
+
* @property {number} [hmax] largest cell size, m
|
|
48
|
+
* @property {number} [ratio] geometric growth ratio (default 1.2)
|
|
49
|
+
* @property {number} [minCells] minimum cells per region (default 8)
|
|
50
|
+
* @property {number} [hLeft] left-end cell size, overriding hmin
|
|
51
|
+
* @property {number} [hRight] right-end cell size, overriding hmin
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} RegionDef
|
|
56
|
+
* @property {string} material material name
|
|
57
|
+
* @property {number} length m
|
|
58
|
+
* @property {string} [name]
|
|
59
|
+
* @property {number} [fixedCharge] immobile charge density, C/m³
|
|
60
|
+
* @property {Record<string, number>} [c0] initial concentrations, mol/m³ (required for species
|
|
61
|
+
* not connected to a contact: it fixes the amount they conserve)
|
|
62
|
+
* @property {number} [velocity] imposed flow toward +x, m/s, carrying every mobile species
|
|
63
|
+
* @property {number} [mixing] eddy (turbulent) mixing diffusivity, m²/s; current-free
|
|
64
|
+
* @property {GridOptions} [grid]
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/** @typedef {'pinned' | 'neutral' | { type: 'capacitive', C: number }} InterfacePhiLaw */
|
|
68
|
+
/** @typedef {'equilibrium' | 'blocked' | { type: 'conductance', G: number }} InterfaceSpeciesLink */
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A reaction at a face: participants on each side with signed stoichiometric coefficients
|
|
72
|
+
* (ν < 0 consumed by the forward reaction), Butler–Volmer kinetics.
|
|
73
|
+
* @typedef {object} FaceReactionDef
|
|
74
|
+
* @property {string} [equation] the reaction as an equation, e.g. 'Ag+ + e- = Ag(s)', each
|
|
75
|
+
* participant on the side that holds it ('Li+(left)' where both do); or give left and right
|
|
76
|
+
* @property {Record<string, number>} [left] participants on the left side (species present there, or fixed neutrals)
|
|
77
|
+
* @property {Record<string, number>} [right] participants on the right side
|
|
78
|
+
* @property {Record<string, number>} [fixed] μ (J/mol) of fixed-activity participants that aren't species
|
|
79
|
+
* @property {number} k0 standard rate constant, mol/(m²·s)
|
|
80
|
+
* @property {number} [alpha] transfer coefficient, 0…1 (default 0.5)
|
|
81
|
+
*/
|
|
82
|
+
/**
|
|
83
|
+
* One face between consecutive regions. For different materials under a 'pinned' or
|
|
84
|
+
* 'capacitive' law, exactly one alignment (step or dipole) is required.
|
|
85
|
+
* @typedef {object} InterfaceDef
|
|
86
|
+
* @property {{ species: string, value: number }} [step] standard-level step right − left, J/mol
|
|
87
|
+
* @property {number} [dipole] φ jump right − left, V
|
|
88
|
+
* @property {number} [zeroCharge] beside a conductor region: V_F − φ_edge at zero charge, V
|
|
89
|
+
* @property {InterfacePhiLaw} [phi] electrostatic law (default 'pinned'; 'neutral' between ε = 0 materials)
|
|
90
|
+
* @property {number} [sheetCharge] C/m²
|
|
91
|
+
* @property {Record<string, InterfaceSpeciesLink>} [species] per-species laws (default equilibrium
|
|
92
|
+
* where present on both sides and not reacting at the face)
|
|
93
|
+
* @property {FaceReactionDef[]} [reactions] reactions at the face
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @typedef {object} BulkReactionDef
|
|
98
|
+
* @property {Record<string, number>} [nu] signed stoichiometric coefficients (ν < 0 consumed)
|
|
99
|
+
* @property {string} [equation] the reaction as an equation, e.g. 'e- + h+ = 0' (instead of nu)
|
|
100
|
+
* @property {Record<string, number>} [fixed] μ (J/mol) of fixed-activity participants that aren't species
|
|
101
|
+
* @property {Record<string, number>} kf forward rate constant per material name
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Contacts use the same laws as internal faces; the outside is a phase with known levels
|
|
106
|
+
* V_i = V + offset_i (a rigid ladder that the circuit slides by V).
|
|
107
|
+
* @typedef {'blocked' | 'equilibrium'
|
|
108
|
+
* | { type: 'equilibrium', offset?: number, mu?: number }
|
|
109
|
+
* | { type: 'conductance', G: number, offset?: number }
|
|
110
|
+
* | { type: 'exchange', k: number, mu: number }} ContactSpeciesLink
|
|
111
|
+
*/
|
|
112
|
+
/**
|
|
113
|
+
* 'bulk': the end node is plain bulk (locally neutral, no double layer); 'neutral': no charge at
|
|
114
|
+
* the face (D = 0); capacitive: Stern layer or gate; 'pinned': φ_edge = V − zeroCharge. The last
|
|
115
|
+
* two take the contact's zeroCharge.
|
|
116
|
+
* @typedef {'bulk' | 'neutral' | 'pinned' | { type: 'capacitive', C: number }} ContactPhiLink
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @typedef {object} ContactDef
|
|
121
|
+
* @property {Source} [V] held terminal voltage, V (default 0, if no I)
|
|
122
|
+
* @property {Source} [I] or: the current driven into the device, A/m² (the voltage floats)
|
|
123
|
+
* @property {number} [R] with V: a series resistance, Ω·m² (a source behind a resistor)
|
|
124
|
+
* @property {string} [terminal] species whose voltage V is
|
|
125
|
+
* @property {Record<string, ContactSpeciesLink>} [species] per-species links (default blocked)
|
|
126
|
+
* @property {{ c: Record<string, number>, reference: string, offset?: number }} [bath] a neutral
|
|
127
|
+
* composition held at the contact, anchored through a charged reference species
|
|
128
|
+
* @property {ContactPhiLink} [phi] required when any species connects (default 'neutral')
|
|
129
|
+
* @property {number} [zeroCharge] for a capacitive or pinned law: V − φ_edge at zero charge, V
|
|
130
|
+
*/
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A terminal's source: a constant, or a piecewise-linear waveform through (t, value), constant
|
|
134
|
+
* beyond the points, or periodic with period t_last − t_0 if repeat is true.
|
|
135
|
+
* @typedef {number | { t: number[], values: number[], repeat?: boolean }} Source
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* An internal port: an outside phase exchanging with a window of one region's nodes. On a
|
|
140
|
+
* conductor region it attaches to the whole conductor (no window), and a conductance G is per area
|
|
141
|
+
* (S/m²) instead of per volume (S/m³).
|
|
142
|
+
* @typedef {object} PortDef
|
|
143
|
+
* @property {string} [name]
|
|
144
|
+
* @property {string | number} region region name or index
|
|
145
|
+
* @property {number} [from] window start, m from the region's left end (default 0)
|
|
146
|
+
* @property {number} [to] window end, m (default: the region's length)
|
|
147
|
+
* @property {Source} [V] held terminal voltage, V (default 0, if no I)
|
|
148
|
+
* @property {Source} [I] or: the current driven into the device, A/m² (e.g. 0 for a reference electrode)
|
|
149
|
+
* @property {number} [R] with V: a series resistance, Ω·m²
|
|
150
|
+
* @property {string} [terminal] species whose offset defaults to 0
|
|
151
|
+
* @property {Record<string, 'blocked' | 'equilibrium' | { type: 'equilibrium', offset?: number, mu?: number }
|
|
152
|
+
* | { type: 'conductance', G: number, offset?: number } | { type: 'exchange', k: number, mu: number }>} species
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A device definition: plain, serialisable data.
|
|
157
|
+
* @typedef {object} DeviceDefinition
|
|
158
|
+
* @property {number} [T] temperature, K (default 298.15)
|
|
159
|
+
* @property {SpeciesDef[]} species
|
|
160
|
+
* @property {Record<string, MaterialDef>} materials
|
|
161
|
+
* @property {RegionDef[]} regions left to right
|
|
162
|
+
* @property {(InterfaceDef | null | undefined)[]} [interfaces] one per face, interfaces[f] between regions f and f+1
|
|
163
|
+
* @property {BulkReactionDef[]} [bulkReactions]
|
|
164
|
+
* @property {{ left?: ContactDef, right?: ContactDef }} [contacts]
|
|
165
|
+
* @property {PortDef[]} [ports] internal ports
|
|
166
|
+
* @property {GridOptions} [grid]
|
|
167
|
+
*/
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @typedef {object} ContactResult
|
|
171
|
+
* @property {number} V terminal voltage, V
|
|
172
|
+
* @property {Record<string, number>} flux particle flux toward +x through this contact, mol/(m²·s)
|
|
173
|
+
* @property {number} D displacement toward +x at this contact, C/m²
|
|
174
|
+
* @property {number} current total current toward +x, A/m²
|
|
175
|
+
*/
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @typedef {object} Solution
|
|
179
|
+
* @property {Float64Array} x node positions, m (interface positions appear twice)
|
|
180
|
+
* @property {Int32Array} region region index per node
|
|
181
|
+
* @property {{ name: string, z: number }[]} species the species, in order, with their charge numbers
|
|
182
|
+
* @property {{ name: string, material: string, x0: number, x1: number }[]} regions each region's name, material and extent, m
|
|
183
|
+
* @property {Float64Array} phi bookkeeping φ, V (NaN where undefined)
|
|
184
|
+
* @property {Record<string, Float64Array>} c concentrations, mol/m³ (NaN where absent)
|
|
185
|
+
* @property {Record<string, Float64Array>} mu electrochemical potentials μ̄, J/mol
|
|
186
|
+
* @property {Record<string, Float64Array>} muStd standard levels μ° + zFφ, J/mol
|
|
187
|
+
* @property {Record<string, Float64Array>} V species voltages μ̄/(zF), V (charged species)
|
|
188
|
+
* @property {Record<string, Float64Array>} Vstd standard levels as voltages, V
|
|
189
|
+
* @property {number} current current toward +x through the device, A/m²
|
|
190
|
+
* @property {number} terminalVoltage V_right − V_left, V
|
|
191
|
+
* @property {Record<string, { V: number, current: number }>} terminals each terminal (left, right,
|
|
192
|
+
* then the ports by name): its voltage, and its current into the device (A/m²)
|
|
193
|
+
* @property {{ left: ContactResult, right: ContactResult }} contacts
|
|
194
|
+
* @property {{ left?: { V: number, D: number, charge: number }, right?: { V: number, D: number, charge: number } }} gates
|
|
195
|
+
* @property {{ name: string, V: number, flux: Record<string, number>, current: number }[]} ports what each
|
|
196
|
+
* internal port brings into the device
|
|
197
|
+
* @property {{ dipole: number, sheetCharge: number, D: number, N: Record<string, number>, rates: number[] }[]} interfaces
|
|
198
|
+
* @property {number} charge total charge in the device, C/m²
|
|
199
|
+
* @property {{ species: string, regions: number[], spectator: boolean, connected: boolean, reactive: boolean,
|
|
200
|
+
* amount: number, reference: number, intake: number, drift: number }[]} conservation
|
|
201
|
+
* @property {string[]} warnings
|
|
202
|
+
* @property {boolean} converged
|
|
203
|
+
* @property {number} iterations
|
|
204
|
+
* @property {number} time s
|
|
205
|
+
* @property {boolean} [done] advance(): whether tEnd was reached (false if the frame budget ran out)
|
|
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
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @typedef {object} ImpedanceResult
|
|
213
|
+
* @property {Float64Array} f frequencies, Hz
|
|
214
|
+
* @property {{ re: Float64Array, im: Float64Array }} Z impedance −δV/δI, Ω·m²
|
|
215
|
+
* @property {{ phi: ComplexProfile, mu: Record<string, ComplexProfile>, c: Record<string, ComplexProfile> }[]} [profiles]
|
|
216
|
+
* per frequency, per unit excitation (volt, or A/m² in current mode)
|
|
217
|
+
*/
|
|
218
|
+
|
|
219
|
+
/** @typedef {{ re: Float64Array, im: Float64Array }} ComplexProfile */
|
|
220
|
+
|
|
221
|
+
export {};
|
package/src/worker.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The Web Worker behind live(def, { worker: true }): it holds the device and solves each change
|
|
2
|
+
// it's sent, replying in order with { solution, info }.
|
|
3
|
+
|
|
4
|
+
import { Session } from './live.js';
|
|
5
|
+
|
|
6
|
+
let session = null, error = null;
|
|
7
|
+
globalThis.addEventListener('message', (e) => {
|
|
8
|
+
const m = e.data;
|
|
9
|
+
if (m.type === 'init') {
|
|
10
|
+
try {
|
|
11
|
+
session = new Session(m.def, { maxRamp: m.maxRamp });
|
|
12
|
+
} catch (err) {
|
|
13
|
+
error = err.message;
|
|
14
|
+
}
|
|
15
|
+
} else if (m.type === 'update') {
|
|
16
|
+
let reply;
|
|
17
|
+
try {
|
|
18
|
+
reply = session ? session.update(m.patches) : { solution: null, errors: [], info: { ms: 0, ramp: 0, failed: true, error } };
|
|
19
|
+
} catch (err) {
|
|
20
|
+
reply = { solution: null, errors: [], info: { ms: 0, ramp: 0, failed: true, error: err.message } };
|
|
21
|
+
}
|
|
22
|
+
globalThis.postMessage(reply);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export class BlockTridiagonal {
|
|
2
|
+
/**
|
|
3
|
+
* @param {number} n number of block rows (nodes)
|
|
4
|
+
* @param {number | ArrayLike<number>} m block size (unknowns per node), or one size per node
|
|
5
|
+
*/
|
|
6
|
+
constructor(n: number, m: number | ArrayLike<number>);
|
|
7
|
+
n: number;
|
|
8
|
+
/** The block size if every block has the same size, else null. */
|
|
9
|
+
m: number;
|
|
10
|
+
/** Block size of each node. */
|
|
11
|
+
sizes: Int32Array<any>;
|
|
12
|
+
offA: Int32Array<any>;
|
|
13
|
+
offB: Int32Array<any>;
|
|
14
|
+
offC: Int32Array<any>;
|
|
15
|
+
offX: Int32Array<any>;
|
|
16
|
+
/** Total number of unknowns. */
|
|
17
|
+
size: number;
|
|
18
|
+
/** Sub-diagonal blocks A_i (coupling to node i−1). */
|
|
19
|
+
A: Float64Array<ArrayBuffer>;
|
|
20
|
+
/** Diagonal blocks B_i. */
|
|
21
|
+
B: Float64Array<ArrayBuffer>;
|
|
22
|
+
/** Super-diagonal blocks C_i (coupling to node i+1). */
|
|
23
|
+
C: Float64Array<ArrayBuffer>;
|
|
24
|
+
_lu: Float64Array<ArrayBuffer>;
|
|
25
|
+
_piv: Int32Array<ArrayBuffer>;
|
|
26
|
+
_cp: Float64Array<ArrayBuffer>;
|
|
27
|
+
_y: Float64Array<ArrayBuffer>;
|
|
28
|
+
_tmp: Float64Array<ArrayBuffer>;
|
|
29
|
+
factored: boolean;
|
|
30
|
+
/** Smallest |pivot| / (largest |entry| of its block) seen in the last factor(). */
|
|
31
|
+
minPivotRatio: number;
|
|
32
|
+
/** The block found exactly singular by the last factor(), or −1. */
|
|
33
|
+
singularBlock: number;
|
|
34
|
+
/** Zero all of A, B, C (ready for fresh assembly). */
|
|
35
|
+
clear(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Factorise the current A, B, C. A, B, C are left untouched.
|
|
38
|
+
* Throws if a diagonal block is exactly singular.
|
|
39
|
+
*/
|
|
40
|
+
factor(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Solve with the current factorisation. `rhs` and `out` are Float64Arrays of length `size`
|
|
43
|
+
* (node-major); they may be the same array.
|
|
44
|
+
*/
|
|
45
|
+
solve(rhs: any, out?: Float64Array<ArrayBuffer>): Float64Array<ArrayBuffer>;
|
|
46
|
+
/**
|
|
47
|
+
* After factor(): the digits lost to cancellation in the factorisation, at worst, and the block
|
|
48
|
+
* where that happens. A running error bound: alongside each entry of W_i = B_i − A_i C′_{i−1}
|
|
49
|
+
* and of its LU (with the same partial pivoting), the sum of the magnitudes it was formed from
|
|
50
|
+
* is carried, and each pivot is compared with its sum: log₁₀(Σ|terms| / |pivot|). Many digits
|
|
51
|
+
* lost means a nearly singular system, such as a stiff chain held only weakly at its ends,
|
|
52
|
+
* whose level then rests on the lost digits. Costs about as much as a factorisation. After a
|
|
53
|
+
* factorisation that failed on a singular block, it covers the blocks up to that one.
|
|
54
|
+
* @returns {{ digits: number, block: number }}
|
|
55
|
+
*/
|
|
56
|
+
cancellation(): {
|
|
57
|
+
digits: number;
|
|
58
|
+
block: number;
|
|
59
|
+
};
|
|
60
|
+
/** out = M·x using the stored (unfactored) blocks. For residual checks and tests. */
|
|
61
|
+
multiply(x: any, out?: Float64Array<ArrayBuffer>): Float64Array<ArrayBuffer>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Complex block-tridiagonal solver, for small-signal (frequency-domain) problems. Same layout
|
|
65
|
+
* and algorithm as BlockTridiagonal (including per-node block sizes), with real and imaginary
|
|
66
|
+
* parts in separate arrays (Ar/Ai, Br/Bi, Cr/Ci; right-hand sides and solutions as re/im
|
|
67
|
+
* pairs).
|
|
68
|
+
*/
|
|
69
|
+
export class ComplexBlockTridiagonal {
|
|
70
|
+
/**
|
|
71
|
+
* @param {number} n number of block rows (nodes)
|
|
72
|
+
* @param {number | ArrayLike<number>} m block size, or one size per node
|
|
73
|
+
*/
|
|
74
|
+
constructor(n: number, m: number | ArrayLike<number>);
|
|
75
|
+
n: number;
|
|
76
|
+
m: number;
|
|
77
|
+
sizes: Int32Array<any>;
|
|
78
|
+
offA: Int32Array<any>;
|
|
79
|
+
offB: Int32Array<any>;
|
|
80
|
+
offC: Int32Array<any>;
|
|
81
|
+
offX: Int32Array<any>;
|
|
82
|
+
size: number;
|
|
83
|
+
_piv: Int32Array<ArrayBuffer>;
|
|
84
|
+
_yr: Float64Array<ArrayBuffer>;
|
|
85
|
+
_yi: Float64Array<ArrayBuffer>;
|
|
86
|
+
_tr: Float64Array<ArrayBuffer>;
|
|
87
|
+
_ti: Float64Array<ArrayBuffer>;
|
|
88
|
+
clear(): void;
|
|
89
|
+
factor(): void;
|
|
90
|
+
/** Solve for (re, im) right-hand sides; results written to outRe/outIm. */
|
|
91
|
+
solve(bRe: any, bIm: any, outRe: any, outIm: any): void;
|
|
92
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Elementary charge, C. */
|
|
2
|
+
export const ELEMENTARY_CHARGE: 1.602176634e-19;
|
|
3
|
+
/** Avogadro constant, 1/mol. */
|
|
4
|
+
export const AVOGADRO: 6.02214076e+23;
|
|
5
|
+
/** Boltzmann constant, J/K. */
|
|
6
|
+
export const BOLTZMANN: 1.380649e-23;
|
|
7
|
+
/** Faraday constant, C/mol. */
|
|
8
|
+
export const FARADAY: number;
|
|
9
|
+
/** Molar gas constant, J/(mol·K). */
|
|
10
|
+
export const GAS_CONSTANT: number;
|
|
11
|
+
/** Vacuum permittivity, F/m (CODATA 2022). */
|
|
12
|
+
export const EPS0: 8.8541878188e-12;
|
|
13
|
+
/** Unit conversions into driftlet's SI units. */
|
|
14
|
+
export const units: Readonly<{
|
|
15
|
+
/** mol/L → mol/m³ */
|
|
16
|
+
molar: (x: any) => number;
|
|
17
|
+
/** mmol/L → mol/m³ */
|
|
18
|
+
millimolar: (x: any) => any;
|
|
19
|
+
/** particles per cm³ → mol/m³ */
|
|
20
|
+
perCm3: (x: any) => number;
|
|
21
|
+
/** particles per m³ → mol/m³ */
|
|
22
|
+
perM3: (x: any) => number;
|
|
23
|
+
/** eV per particle → J/mol */
|
|
24
|
+
eV: (x: any) => number;
|
|
25
|
+
/** J/mol → eV per particle */
|
|
26
|
+
toEV: (x: any) => number;
|
|
27
|
+
/** nm → m */
|
|
28
|
+
nm: (x: any) => number;
|
|
29
|
+
/** µm → m */
|
|
30
|
+
um: (x: any) => number;
|
|
31
|
+
/** cm²/s → m²/s */
|
|
32
|
+
cm2PerS: (x: any) => number;
|
|
33
|
+
}>;
|
package/types/data.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Water with the named ions, as a library piece for `build()`: each ion as a species
|
|
3
|
+
* (c_ref = 1 M) and the material `water` holding them, with D and μ° from `IONS`.
|
|
4
|
+
* @param {string[]} names ions from `IONS`
|
|
5
|
+
* @param {{ epsr?: number, material?: string }} [opts] permittivity (default that of water;
|
|
6
|
+
* 0 makes the solution strictly neutral, without double layers), and the material's name
|
|
7
|
+
* @returns {{ species: object[], materials: Record<string, object> }}
|
|
8
|
+
*/
|
|
9
|
+
export function aqueous(names: string[], { epsr, material }?: {
|
|
10
|
+
epsr?: number;
|
|
11
|
+
material?: string;
|
|
12
|
+
}): {
|
|
13
|
+
species: object[];
|
|
14
|
+
materials: Record<string, object>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A semiconductor from `SEMICONDUCTORS`, as a library piece: electrons `e-` and holes `h+`, with
|
|
18
|
+
* c_ref = Nc and Nv, D from the mobilities by the Einstein relation at 300 K, and μ°_e⁻ = 0 (φ
|
|
19
|
+
* anchored at the conduction band) and μ°_h⁺ = Eg·F. The data are for 300 K, so use `T: 300`.
|
|
20
|
+
* Statistics are ideal (Boltzmann); add Fermi–Dirac to the material for degenerate doping.
|
|
21
|
+
* @param {string} name 'Si', 'Ge' or 'GaAs'
|
|
22
|
+
* @param {{ material?: string }} [opts]
|
|
23
|
+
* @returns {{ species: object[], materials: Record<string, object> }}
|
|
24
|
+
*/
|
|
25
|
+
export function semiconductor(name: string, { material }?: {
|
|
26
|
+
material?: string;
|
|
27
|
+
}): {
|
|
28
|
+
species: object[];
|
|
29
|
+
materials: Record<string, object>;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* A metal from `METALS` as a conductor material, its carrier `e-`.
|
|
33
|
+
* @param {string} name
|
|
34
|
+
* @param {{ material?: string }} [opts]
|
|
35
|
+
* @returns {{ species: object[], materials: Record<string, object> }}
|
|
36
|
+
*/
|
|
37
|
+
export function metal(name: string, { material }?: {
|
|
38
|
+
material?: string;
|
|
39
|
+
}): {
|
|
40
|
+
species: object[];
|
|
41
|
+
materials: Record<string, object>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Aqueous ions at 25 °C. D: diffusivity at infinite dilution, m²/s (CRC Handbook of Chemistry
|
|
45
|
+
* and Physics, "Ionic conductivity and diffusion at infinite dilution"). mu0: standard Gibbs
|
|
46
|
+
* energy of formation, J/mol, the usual table convention with ΔfG°(H⁺) = 0 (Wagman et al., "The
|
|
47
|
+
* NBS tables of chemical thermodynamic properties", J. Phys. Chem. Ref. Data 11, Suppl. 2, 1982),
|
|
48
|
+
* on the 1 mol/kg standard state, used here with c_ref = 1000 mol/m³ (1 M).
|
|
49
|
+
* Names are the formula followed by the charge.
|
|
50
|
+
*/
|
|
51
|
+
export const IONS: Readonly<{
|
|
52
|
+
[k: string]: any;
|
|
53
|
+
}>;
|
|
54
|
+
/** μ° of liquid water at 25 °C, J/mol (Wagman et al. 1982), for reactions such as H₂O ⇌ H⁺ + OH⁻. */
|
|
55
|
+
export const H2O: -237.129e3;
|
|
56
|
+
/** Relative permittivity of water at 25 °C (Malmberg and Maryott, J. Res. NBS 56, 1, 1956). */
|
|
57
|
+
export const WATER_EPSR: 78.3;
|
|
58
|
+
/**
|
|
59
|
+
* Semiconductors at 300 K (Sze, Physics of Semiconductor Devices, 2nd ed., 1981, appendix):
|
|
60
|
+
* relative permittivity, band gap Eg (eV), electron affinity chi (V), effective densities of
|
|
61
|
+
* states Nc and Nv (cm⁻³), and drift mobilities of lightly doped material mun and mup
|
|
62
|
+
* (cm²/(V·s)).
|
|
63
|
+
*/
|
|
64
|
+
export const SEMICONDUCTORS: Readonly<{
|
|
65
|
+
[k: string]: any;
|
|
66
|
+
}>;
|
|
67
|
+
/**
|
|
68
|
+
* Metals at 293 K: resistivity rho (10⁻⁸ Ω·m, CRC Handbook, "Electrical resistivity of pure
|
|
69
|
+
* metals") and the conductivity 1/rho (S/m).
|
|
70
|
+
*/
|
|
71
|
+
export const METALS: Readonly<{
|
|
72
|
+
[k: string]: any;
|
|
73
|
+
}>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warnings for numbers that look like unit slips: SI is m, mol/m³, J/mol, m²/s, F/m², C/m³.
|
|
3
|
+
* @param {object} def device definition
|
|
4
|
+
* @returns {string[]}
|
|
5
|
+
*/
|
|
6
|
+
export function unitWarnings(def: object): string[];
|
|
7
|
+
/**
|
|
8
|
+
* A readable summary of a device: its regions (with doping, Debye lengths and the grid at their
|
|
9
|
+
* ends), faces, contacts and reactions, its characteristic times, and warnings (unit slips, and
|
|
10
|
+
* double layers coarser than the grid). It validates the definition first, so an invalid one
|
|
11
|
+
* throws its DeviceError.
|
|
12
|
+
* @param {object} def device definition
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function describe(def: object): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate and normalise a device definition.
|
|
3
|
+
* @param {object} def
|
|
4
|
+
* @returns {object} internal model
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeDevice(def: object): object;
|
|
7
|
+
/**
|
|
8
|
+
* The terminals' drives alone (in terminal order: left, right, then the ports), from a definition
|
|
9
|
+
* whose structure is unchanged: for a fast update of sources.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeDrives(def: any, model: any): any[];
|
|
12
|
+
/**
|
|
13
|
+
* A source's value at time t (s). At a step (two points at one time) it's the value after the
|
|
14
|
+
* step; with `before`, the limit from earlier times, as a time step ending at t sees it.
|
|
15
|
+
*/
|
|
16
|
+
export function sourceAt(src: any, t: any, before?: boolean): any;
|
|
17
|
+
/** The first breakpoint of a source strictly after time t (Infinity if none). */
|
|
18
|
+
export function nextBreakpoint(src: any, t: any): any;
|
|
19
|
+
export { DeviceError };
|
|
20
|
+
import { DeviceError } from './errors.js';
|