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.
Files changed (47) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +285 -0
  3. package/llms.txt +299 -0
  4. package/package.json +54 -0
  5. package/src/bernoulli.js +29 -0
  6. package/src/blockTridiagonal.js +492 -0
  7. package/src/constants.js +36 -0
  8. package/src/data.js +127 -0
  9. package/src/describe.js +190 -0
  10. package/src/device.js +954 -0
  11. package/src/equation.js +188 -0
  12. package/src/errors.js +8 -0
  13. package/src/fermi.js +106 -0
  14. package/src/grid.js +111 -0
  15. package/src/index.js +207 -0
  16. package/src/kit.js +76 -0
  17. package/src/live.js +217 -0
  18. package/src/merge.js +11 -0
  19. package/src/plot.js +267 -0
  20. package/src/solution.js +246 -0
  21. package/src/solver.js +2884 -0
  22. package/src/stack.js +172 -0
  23. package/src/statistics.js +593 -0
  24. package/src/traces.js +125 -0
  25. package/src/types.js +221 -0
  26. package/src/worker.js +24 -0
  27. package/types/bernoulli.d.ts +4 -0
  28. package/types/blockTridiagonal.d.ts +92 -0
  29. package/types/constants.d.ts +33 -0
  30. package/types/data.d.ts +73 -0
  31. package/types/describe.d.ts +15 -0
  32. package/types/device.d.ts +20 -0
  33. package/types/equation.d.ts +102 -0
  34. package/types/errors.d.ts +3 -0
  35. package/types/fermi.d.ts +3 -0
  36. package/types/grid.d.ts +28 -0
  37. package/types/index.d.ts +103 -0
  38. package/types/kit.d.ts +61 -0
  39. package/types/live.d.ts +101 -0
  40. package/types/merge.d.ts +1 -0
  41. package/types/plot.d.ts +60 -0
  42. package/types/solution.d.ts +18 -0
  43. package/types/solver.d.ts +342 -0
  44. package/types/stack.d.ts +59 -0
  45. package/types/statistics.d.ts +10 -0
  46. package/types/traces.d.ts +118 -0
  47. 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,4 @@
1
+ /** B(x) = x / (eˣ − 1), with B(0) = 1. */
2
+ export function bernoulli(x: any): number;
3
+ /** dB/dx, with B'(0) = −1/2. */
4
+ export function bernoulliDerivative(x: any): number;
@@ -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
+ }>;
@@ -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';