driftlet 1.1.1 → 1.2.2

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/src/stack.js CHANGED
@@ -33,6 +33,9 @@ export function combine(...parts) {
33
33
  const species = [], materials = {};
34
34
  parts.forEach((part, k) => {
35
35
  if (!isObject(part)) fail(`combine: part ${k} must be { species, materials }`);
36
+ // (trap()'s whole result, given for its library, dropped the traps without a word)
37
+ if (part.library !== undefined) fail(`combine: part ${k} is { library, … }, as trap() returns: give its library here (trap(…).library), and its bulkReactions and c0 to the device`);
38
+ fields(part, `combine: part ${k}`, ['species', 'materials']);
36
39
  for (const sp of part.species ?? []) {
37
40
  const prev = species.find((s) => s.name === sp.name);
38
41
  if (!prev) species.push({ ...sp });
@@ -100,6 +103,7 @@ export function ohmic(d = 0, carriers = ['e-', 'h+']) {
100
103
  */
101
104
  export function bath(c, reference, d = 0, opts = {}) {
102
105
  if (typeof reference !== 'string') [d, opts] = [reference ?? 0, isObject(d) ? d : {}];
106
+ checkOptions('bath', opts, ['offset', 'material']);
103
107
  const { offset, material } = opts;
104
108
  return {
105
109
  ...drive(d, 'bath'),
@@ -161,7 +165,7 @@ export function build(def) {
161
165
  if (regions.length === 0) fail('build.stack has no layers');
162
166
  // A contact holding a carrier its layer lacks: most often ohmic()'s default on a metal.
163
167
  for (const [k, end] of [[0, regions[0]], [stack.length - 1, regions[regions.length - 1]]]) {
164
- const held = isObject(stack[k].species) ? Object.keys(stack[k].species).filter((s) => stack[k].species[s] !== 'blocked') : [];
168
+ const held = isObject(stack[k].species) ? Object.keys(stack[k].species).filter((s) => stack[k].species[s] !== 'blocked' && stack[k].species[s]?.type !== 'blocked') : [];
165
169
  const mat = materials[end.material];
166
170
  if (!isObject(mat)) continue;
167
171
  const has = (s) => (isObject(mat.conductor) ? mat.conductor.species === s : isObject(mat.species) && mat.species[s] !== undefined);
package/src/statistics.js CHANGED
@@ -286,7 +286,7 @@ function debyeHuckelModel(idx, cRef, z, epsr, a, RT) {
286
286
  idx,
287
287
  evaluate(zeta, c, K) {
288
288
  for (let b = 0; b < k; b++) y[b] = zeta[b] + Math.log(cRef[b]);
289
- let st;
289
+ let st, mx = Infinity;
290
290
  for (let it = 0; it < 100; it++) {
291
291
  for (let b = 0; b < k; b++) cc[b] = Math.exp(y[b]);
292
292
  st = state(cc);
@@ -297,7 +297,7 @@ function debyeHuckelModel(idx, cRef, z, epsr, a, RT) {
297
297
  R[b] = y[b] + lng[b] - zeta[b] - Math.log(cRef[b]);
298
298
  dot += s[b] * cc[b] * R[b];
299
299
  }
300
- let mx = 0;
300
+ mx = 0;
301
301
  for (let b = 0; b < k; b++) {
302
302
  let dy = R[b] + (st.A * s[b] * dot) / st.denom;
303
303
  if (dy > 1) dy = 1;
@@ -307,6 +307,8 @@ function debyeHuckelModel(idx, cRef, z, epsr, a, RT) {
307
307
  }
308
308
  if (mx < 1e-14) break;
309
309
  }
310
+ // (a stall at round-off, near the convexity limit, is converged; anything more isn't)
311
+ if (!(mx < 1e-10)) throw new SolverError(`debye-huckel: the concentrations at these levels did not converge (a step of ${mx.toExponential(2)} in ln c after 100 iterations)`);
310
312
  for (let b = 0; b < k; b++) c[b] = Math.exp(y[b]);
311
313
  st = state(c);
312
314
  const f = st.A / st.denom;
@@ -315,18 +317,37 @@ function debyeHuckelModel(idx, cRef, z, epsr, a, RT) {
315
317
  }
316
318
  },
317
319
  invert(zeta, fixed, target) {
318
- for (let b = 0; b < k; b++) cc[b] = fixed[b] ? target[b] : cRef[b] * Math.exp(zeta[b]);
319
- for (let it = 0; it < 500; it++) {
320
- state(cc);
321
- let change = 0;
320
+ // The free species by Newton in y = ln c, as evaluate() does, the fixed held at their targets:
321
+ // J = I − A s_F (s∘c)_Fᵀ over the free ones alone. (A fixed-point iteration contracted by
322
+ // 1 − denom, and near the convexity limit stopped short without a word.)
323
+ for (let b = 0; b < k; b++) {
324
+ y[b] = fixed[b] ? Math.log(target[b]) : zeta[b] + Math.log(cRef[b]);
325
+ cc[b] = Math.exp(y[b]);
326
+ }
327
+ let mx = 0;
328
+ for (let it = 0; it < 100; it++) {
329
+ const st = state(cc);
330
+ let dot = 0, s2cF = 0;
322
331
  for (let b = 0; b < k; b++) {
323
332
  if (fixed[b]) continue;
324
- const next = cRef[b] * Math.exp(zeta[b] - lng[b]);
325
- if (cc[b] > 0) change = Math.max(change, Math.abs(next / cc[b] - 1));
326
- cc[b] = next;
333
+ lng[b] = y[b] + lng[b] - zeta[b] - Math.log(cRef[b]); // (the residual, in lng's place)
334
+ dot += s[b] * cc[b] * lng[b];
335
+ s2cF += s[b] * s[b] * cc[b];
327
336
  }
328
- if (!(change > 1e-15)) break;
337
+ const dF = 1 - st.A * s2cF; // (≥ denom > 0)
338
+ mx = 0;
339
+ for (let b = 0; b < k; b++) {
340
+ if (fixed[b]) continue;
341
+ let dy = lng[b] + (st.A * s[b] * dot) / dF;
342
+ if (dy > 1) dy = 1;
343
+ else if (dy < -1) dy = -1;
344
+ y[b] -= dy;
345
+ cc[b] = Math.exp(y[b]);
346
+ mx = Math.max(mx, Math.abs(dy));
347
+ }
348
+ if (mx < 1e-14) break;
329
349
  }
350
+ if (!(mx < 1e-10)) throw new SolverError(`debye-huckel: the composition given did not converge to its levels (a step of ${mx.toExponential(2)} in ln c after 100 iterations)`);
330
351
  state(cc);
331
352
  for (let b = 0; b < k; b++) if (fixed[b]) zeta[b] = Math.log(target[b] / cRef[b]) + lng[b];
332
353
  },
package/src/traces.js CHANGED
@@ -37,6 +37,8 @@ const finite = (a) => {
37
37
  */
38
38
  export function speciesRole(sol, name) {
39
39
  const z = Object.fromEntries(sol.species.map((sp) => [sp.name, sp.z]));
40
+ if (!(name in z)) throw new DeviceError(`speciesRole: no species '${name}' in the solution`);
41
+ if (z[name] === 0) throw new DeviceError(`speciesRole: '${name}' is neutral, with no role in level diagrams (draw it muted, or in a colour of your own)`);
40
42
  const role = (n) => (n === 'e-' ? 'electron' : z[n] > 0 ? 'cation' : 'anion');
41
43
  const charged = sol.species.filter((sp) => sp.z !== 0).map((sp) => sp.name);
42
44
  return { role: role(name), slot: charged.filter((n) => role(n) === role(name)).indexOf(name) };
@@ -88,7 +90,7 @@ export function traces(sol, { species, standard = true, phi = false, energy = fa
88
90
  const shown = species ?? names.filter((name) => finite(sol.V[name]) && (!energy || carriers.includes(name)));
89
91
  if (energy) {
90
92
  for (const name of shown) {
91
- if (!carriers.includes(name)) throw new Error(`traces: '${name}' has no band to draw as an energy; draw it as a species voltage (energy: false)`);
93
+ if (!carriers.includes(name)) throw new DeviceError(`traces: '${name}' has no band to draw as an energy; draw it as a species voltage (energy: false)`);
92
94
  }
93
95
  }
94
96
  const sign = energy ? -1 : 1, unit = energy ? 'eV' : 'V';
@@ -98,22 +100,23 @@ export function traces(sol, { species, standard = true, phi = false, energy = fa
98
100
  const pretty = (name) => (name in zOf ? typeset(name, zOf[name]) : name);
99
101
  const series = [];
100
102
  for (const name of shown) {
101
- if (!names.includes(name)) throw new Error(`traces: no charged species '${name}' in the solution`);
103
+ if (!names.includes(name)) throw new DeviceError(`traces: no charged species '${name}' in the solution`);
102
104
  const shift = shifts[name] ?? 0;
103
- const at = (y) => (shift || energy ? Array.from(y, (v) => sign * v + shift) : y);
105
+ const at = (y) => (shift || energy ? Float64Array.from(y, (v) => sign * v + shift) : y);
104
106
  const common = { species: name, role: role(name), slot: slotOf(name), ...(shift ? { shift } : {}) };
105
107
  if (finite(sol.V[name])) series.push({ id: `V:${name}`, label: energy ? energyLabels[`V:${name}`] : `V_{${pretty(name)}}`, kind: 'level', ...common, y: at(sol.V[name]) });
106
108
  if (standard && finite(sol.Vstd[name])) series.push({ id: `Vstd:${name}`, label: energy ? energyLabels[`Vstd:${name}`] : `V°_{${pretty(name)}}`, kind: 'standard', ...common, y: at(sol.Vstd[name]) });
107
109
  }
108
110
  if (Array.isArray(phi)) {
109
111
  const known = sol.regions.flatMap((r) => [r.name, r.material]);
110
- 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(', ')})`);
112
+ for (const name of phi) if (!known.includes(name)) throw new DeviceError(`traces: no region or material '${name}' to draw φ in (${[...new Set(known)].join(', ')})`);
111
113
  const inside = sol.regions.map((r) => phi.includes(r.name) || phi.includes(r.material));
112
- series.push({ id: 'phi', label: energy ? '−qφ' : 'φ', kind: 'phi', role: 'phi', slot: 0, y: Array.from(sol.phi, (v, g) => (inside[sol.region[g]] ? sign * v : NaN)) });
113
- } else if (phi) series.push({ id: 'phi', label: energy ? '−qφ' : 'φ', kind: 'phi', role: 'phi', slot: 0, y: energy ? Array.from(sol.phi, (v) => -v) : sol.phi });
114
+ series.push({ id: 'phi', label: energy ? '−qφ' : 'φ', kind: 'phi', role: 'phi', slot: 0, y: Float64Array.from(sol.phi, (v, g) => (inside[sol.region[g]] ? sign * v : NaN)) });
115
+ } else if (phi) series.push({ id: 'phi', label: energy ? '−qφ' : 'φ', kind: 'phi', role: 'phi', slot: 0, y: energy ? Float64Array.from(sol.phi, (v) => -v) : sol.phi });
116
+ for (const lv of levels) if (typeof lv?.half?.equation !== 'string') throw new DeviceError('traces: each of levels is { half, label, standard }, its half a half() (as { half: half(\'Fe3+ + e- = Fe2+\') })');
114
117
  const couples = [...new Set(levels.map((lv) => lv.half.equation))];
115
118
  levels.forEach((lv, k) => {
116
- const y = energy ? Array.from(level(sol, lv.half, { standard: lv.standard }), (v) => -v) : level(sol, lv.half, { standard: lv.standard });
119
+ const y = energy ? Float64Array.from(level(sol, lv.half, { standard: lv.standard }), (v) => -v) : level(sol, lv.half, { standard: lv.standard });
117
120
  series.push({
118
121
  id: `level:${k}`,
119
122
  label: lv.label ?? `${lv.standard ? 'V° ' : ''}${lv.half.equation.split(' ').map(pretty).join(' ')}`,
package/src/types.js CHANGED
@@ -19,6 +19,7 @@
19
19
  * Statistics model: how the listed species' concentrations follow from their reduced
20
20
  * potentials ζ = (μ̄ − zFφ − μ°)/RT. Species not in any model are ideal, c = c_ref e^ζ.
21
21
  * @typedef {(
22
+ * { type: 'ideal', species: string[] } |
22
23
  * { type: 'fermi-dirac', species: string[], order?: 0.5 | 0 } |
23
24
  * { type: 'lattice', species: string[], cMax: number } |
24
25
  * { type: 'redlich-kister', species: [string], cMax: number, A?: number[] } |
@@ -73,19 +74,45 @@
73
74
  */
74
75
 
75
76
  /** @typedef {'pinned' | 'neutral' | { type: 'capacitive', C: number }} InterfacePhiLaw */
76
- /** @typedef {'equilibrium' | 'blocked' | { type: 'conductance', G: number }} InterfaceSpeciesLink */
77
+ /**
78
+ * A species' law across a face. conductance: J = G (V_L − V_R), G in S/m², charged species;
79
+ * permeability: Goldman–Hodgkin–Katz electrodiffusion, P in m/s, ideal statistics on both sides,
80
+ * not beside a conductor. gates: the face's gates that scale G or P, with their exponents
81
+ * ({ m: 3, h: 1 } for m³h; not beside a conductor).
82
+ * @typedef {'equilibrium' | 'blocked' | { type: 'equilibrium' | 'blocked' }
83
+ * | { type: 'conductance', G: number, gates?: Record<string, number> }
84
+ * | { type: 'permeability', P: number, gates?: Record<string, number> }} InterfaceSpeciesLink
85
+ */
86
+
87
+ /**
88
+ * One of a gate's rates (1/s) in the voltage V it follows, in NeuroML's standard forms (midpoint
89
+ * and scale in V, scale ≠ 0): 'exp', rate·e^((V − midpoint)/scale); 'sigmoid',
90
+ * rate/(1 + e^((midpoint − V)/scale)); 'expLinear', rate·y/(1 − e^(−y)), y = (V − midpoint)/scale.
91
+ * @typedef {{ type: 'exp' | 'sigmoid' | 'expLinear', rate: number, midpoint: number, scale: number }} GateRate
92
+ */
93
+ /**
94
+ * A Hodgkin–Huxley gate: a fraction open x in [0, 1], dx/dt = α(V)(1 − x) − β(V)x.
95
+ * @typedef {{ alpha: GateRate, beta: GateRate }} GateDef
96
+ */
77
97
 
78
98
  /**
79
99
  * A reaction at a face: participants on each side with signed stoichiometric coefficients
80
- * (ν < 0 consumed by the forward reaction), Butler–Volmer kinetics.
100
+ * (ν < 0 consumed by the forward reaction). Butler–Volmer kinetics (k0, alpha), or instead SRH
101
+ * (srh) or saturating (vmax and K), neither with a conductor's carrier.
81
102
  * @typedef {object} FaceReactionDef
82
103
  * @property {string} [equation] the reaction as an equation, e.g. 'Ag+ + e- = Ag(s)', each
83
104
  * participant on the side that holds it ('Li+(left)' where both do); or give left and right
84
105
  * @property {Record<string, number>} [left] participants on the left side (species present there, or fixed neutrals)
85
106
  * @property {Record<string, number>} [right] participants on the right side
86
107
  * @property {Record<string, number>} [fixed] μ (J/mol) of fixed-activity participants that aren't species
87
- * @property {number} k0 standard rate constant, mol/(m²·s)
88
- * @property {number} [alpha] transfer coefficient, 0…1 (default 0.5)
108
+ * @property {number} [k0] Butler–Volmer: standard rate constant, mol/(m²·s) (required unless srh or vmax)
109
+ * @property {number} [alpha] with k0: transfer coefficient, 0…1 (default 0.5)
110
+ * @property {{ vn: number, vp: number, n1?: number }} [srh] instead: trap-assisted recombination, for a
111
+ * reaction consuming one negative and one positive species and making none; capture velocities
112
+ * vn, vp in m/s, n1 in mol/m³ (default n_i, a midgap trap)
113
+ * @property {number} [vmax] instead: saturating kinetics, the largest rate, mol/(m²·s)
114
+ * @property {Record<string, number>} [K] with vmax: a half-saturation concentration (mol/m³) for
115
+ * every species the forward reaction consumes
89
116
  */
90
117
  /**
91
118
  * One face between consecutive regions. For different materials under a 'pinned' or
@@ -94,11 +121,15 @@
94
121
  * @property {{ species: string, value: number }} [step] standard-level step right − left, J/mol
95
122
  * @property {number} [dipole] φ jump right − left, V
96
123
  * @property {number} [zeroCharge] beside a conductor region: V_F − φ_edge at zero charge, V
97
- * @property {InterfacePhiLaw} [phi] electrostatic law (default 'pinned'; 'neutral' between ε = 0 materials)
124
+ * @property {InterfacePhiLaw} [phi] electrostatic law (default 'pinned'; 'neutral' between ε = 0 materials;
125
+ * beside a conductor 'neutral' or capacitive, required next to an ε > 0 material)
98
126
  * @property {number} [sheetCharge] C/m²
99
127
  * @property {Record<string, InterfaceSpeciesLink>} [species] per-species laws (default equilibrium
100
- * where present on both sides and not reacting at the face)
128
+ * where present on both sides and mobile on one, blocked otherwise; a species reacting at the
129
+ * face and present on both sides has no default)
101
130
  * @property {FaceReactionDef[]} [reactions] reactions at the face
131
+ * @property {Record<string, GateDef>} [gates] voltage-gated channels: gates following
132
+ * V = φ_right − φ_left, which its links name (not beside a conductor)
102
133
  */
103
134
 
104
135
  /**
@@ -106,14 +137,18 @@
106
137
  * @property {Record<string, number>} [nu] signed stoichiometric coefficients (ν < 0 consumed)
107
138
  * @property {string} [equation] the reaction as an equation, e.g. 'e- + h+ = 0' (instead of nu)
108
139
  * @property {Record<string, number>} [fixed] μ (J/mol) of fixed-activity participants that aren't species
109
- * @property {Record<string, number | Profile>} kf forward rate constant per material name, or a
110
- * profile of it against the device's x (an absorption profile, for photogeneration)
140
+ * @property {Record<string, number | Profile>} [kf] mass action: forward rate constant per material
141
+ * name, or a profile of it against the device's x (an absorption profile, for photogeneration);
142
+ * give kf or srh
143
+ * @property {Record<string, { tauN: number, tauP: number, n1?: number }>} [srh] instead: SRH
144
+ * (trap-assisted) recombination per material name, for a reaction consuming one negative and
145
+ * one positive species and making none; lifetimes in s, n1 in mol/m³ (default n_i, a midgap trap)
111
146
  */
112
147
 
113
148
  /**
114
149
  * Contacts use the same laws as internal faces; the outside is a phase with known levels
115
150
  * V_i = V + offset_i (a rigid ladder that the circuit slides by V).
116
- * @typedef {'blocked' | 'equilibrium'
151
+ * @typedef {'blocked' | 'equilibrium' | { type: 'blocked' }
117
152
  * | { type: 'equilibrium', offset?: number, mu?: number }
118
153
  * | { type: 'conductance', G: number, offset?: number }
119
154
  * | { type: 'exchange', k: number, mu: number }
@@ -133,9 +168,11 @@
133
168
  * @property {number} [R] with V: a series resistance, Ω·m² (a source behind a resistor)
134
169
  * @property {string} [terminal] species whose voltage V is
135
170
  * @property {Record<string, ContactSpeciesLink>} [species] per-species links (default blocked)
136
- * @property {{ c: Record<string, number>, reference: string, offset?: number }} [bath] a neutral
137
- * composition held at the contact, anchored through a charged reference species
138
- * @property {ContactPhiLink} [phi] required when any species connects (default 'neutral')
171
+ * @property {{ c: Record<string, number>, reference?: string, offset?: number, material?: string }} [bath]
172
+ * instead of species: a neutral composition c (mol/m³) held at the contact, anchored through a
173
+ * charged reference species (offset: its V_i − V, V); with no reference, V is the bath's φ. It's
174
+ * the end material at that composition, or the material it names (a solution beside a charged region)
175
+ * @property {ContactPhiLink} [phi] required when any species connects (default 'neutral'; 'bulk' with a bath)
139
176
  * @property {number} [zeroCharge] for a capacitive or pinned law: V − φ_edge at zero charge, V
140
177
  */
141
178
 
@@ -158,20 +195,27 @@
158
195
  * @property {Source} [I] or: the current driven into the device, A/m² (e.g. 0 for a reference electrode)
159
196
  * @property {number} [R] with V: a series resistance, Ω·m²
160
197
  * @property {string} [terminal] species whose offset defaults to 0; with reactions, the electrode's carrier
161
- * @property {Record<string, 'blocked' | 'equilibrium' | { type: 'equilibrium', offset?: number, mu?: number }
162
- * | { type: 'conductance', G: number, offset?: number } | { type: 'exchange', k: number, mu: number }>} [species]
163
- * @property {{ equation: string, fixed?: Record<string, number>, k0: number, alpha: number, bare?: boolean }[]} [reactions] an
164
- * electrode spread through the window: Butler–Volmer per area (k0 in mol/(m²·s)) between the region's
165
- * species and the terminal species at the port's level
198
+ * @property {Record<string, 'blocked' | 'equilibrium' | { type: 'blocked' } | { type: 'equilibrium', offset?: number, mu?: number }
199
+ * | { type: 'conductance', G: number, offset?: number, gates?: Record<string, number> }
200
+ * | { type: 'exchange', k: number, mu: number }>} [species] per-species links, per volume (default
201
+ * blocked): G in S/m³ (S/m² on a conductor), k in mol/(m³·s); a conductance's gates name the
202
+ * port's gates that scale G, with their exponents
203
+ * @property {{ c: Record<string, number> }} [bath] the outside's composition (mol/m³): V is then its φ,
204
+ * and each linked species' level there follows from it (unless its link gives an offset or mu)
205
+ * @property {{ equation: string, fixed?: Record<string, number>, k0: number, alpha?: number, bare?: boolean }[]} [reactions] an
206
+ * electrode spread through the window: Butler–Volmer per area (k0 in mol/(m²·s), alpha default 0.5)
207
+ * between the region's species and the terminal species at the port's level
166
208
  * @property {Record<string, { mu0: number, capacity: number, theta0?: number }>} [surface] with reactions: species
167
209
  * on the electrode's sites, Langmuir (μ = μ° + RT ln(θ/θ₀)), capacity in mol of sites per m² of electrode,
168
- * shared; reactions run on Langmuir sites (what fills sites needs bare ones), and one with `bare: true`
169
- * needs a bare site even if it fills none (θ₀ = 1 − Σθ)
210
+ * shared, theta0 the starting coverage (default 1e-6); reactions run on Langmuir sites (what fills
211
+ * sites needs bare ones), and one with `bare: true` needs a bare site even if it fills none (θ₀ = 1 − Σθ)
170
212
  * @property {{ C: number, zeroCharge?: number }} [capacitance] a capacitance spread through the window (F/m² of
171
213
  * electrode): σ = C (V − zeroCharge − φ) on the port's side, aσ in each node's charge balance; a gate
172
214
  * along a channel, a double layer
173
215
  * @property {number | { x: number[], values: number[] }} [area] with reactions or a capacitance: electrode area per volume,
174
216
  * m²/m³ (1/h for a film of thickness h), or a profile against the device's x
217
+ * @property {Record<string, GateDef>} [gates] with a capacitance: voltage-gated channels at each node of
218
+ * the window, following the voltage across it, φ − (V − zeroCharge), which its conductance links name
175
219
  */
176
220
 
177
221
  /**
@@ -198,6 +242,7 @@
198
242
  * @property {Record<string, number>} flux particle flux toward +x through this contact, mol/(m²·s)
199
243
  * @property {number} D displacement toward +x at this contact, C/m²
200
244
  * @property {number} current total current toward +x, A/m²
245
+ * @property {Record<string, string>} links how each species that crosses it does (its link type)
201
246
  */
202
247
 
203
248
  /**
@@ -216,6 +261,11 @@
216
261
  * @property {Record<string, Float64Array>} muStd standard levels μ° + zFφ, J/mol
217
262
  * @property {Record<string, Float64Array>} V species voltages μ̄/(zF), V (charged species)
218
263
  * @property {Record<string, Float64Array>} Vstd standard levels as voltages, V
264
+ * @property {Record<string, Float64Array>} flux each species' flux toward +x across each segment, between
265
+ * nodes g and g + 1 (a face's across its pair of nodes), mol/(m²·s): the fluxes the balances add up
266
+ * @property {Record<string, Float64Array>} D diffusivity at each node, m²/s (NaN where absent, or in a conductor)
267
+ * @property {number} T temperature, K
268
+ * @property {'planar' | 'cylindrical' | 'spherical' | 'profile'} geometry the device's (amounts and rates per area only if planar)
219
269
  * @property {number} current current toward +x through the device, A/m²
220
270
  * @property {number} terminalVoltage V_right − V_left, V
221
271
  * @property {Record<string, { V: number, current: number }>} terminals each terminal (left, right,
@@ -225,31 +275,48 @@
225
275
  * Stern plate at a capacitive contact
226
276
  * @property {{ name: string, V: number, flux: Record<string, number>, current: number, x?: Float64Array,
227
277
  * area?: Float64Array, rates?: Float64Array[], coverage?: Record<string, Float64Array>, bare?: Float64Array,
228
- * sigma?: Float64Array, charge?: number }[]} ports what each internal port brings into the device;
229
- * with reactions, also its window's nodes (x, m), area per volume there, and each reaction's rate per area
230
- * at each node (mol/(m²·s), forward)
231
- * @property {{ left: string, right: string, dipole: number, sheetCharge: number, D: number, N: Record<string, number>, rates: number[] }[]} interfaces
232
- * @property {{ rate: Float64Array, regions: number[], total: number }[]} bulkReactions each bulk reaction's
233
- * forward rate per node (mol/(m³·s), NaN where it doesn't run), and per region and in all (mol/(m²·s))
278
+ * sigma?: Float64Array, charge?: number, gates?: Record<string, Float64Array>, Vm?: Float64Array }[]} ports
279
+ * what each internal port brings into the device (current: the charging current included); with
280
+ * reactions or a capacitance, also its window's nodes (x, m) and area per volume there (m²/m³); with
281
+ * reactions, each reaction's net rate per area at each node (mol/(m²·s), forward); with a surface, each
282
+ * species' coverage and the bare fraction; with a capacitance, σ per area (C/m²) and the charge its
283
+ * side holds; with gates, each gate's fraction open at each node and the voltage they follow (V)
284
+ * @property {{ left: string, right: string, dipole: number, sheetCharge: number, D: number, N: Record<string, number>,
285
+ * rates: number[], links: Record<string, string>, oneWay?: Record<string, [number, number]>,
286
+ * gates?: Record<string, number>, V?: number }[]} interfaces each face: the regions it joins, its dipole
287
+ * (V) and sheet charge (C/m²), its displacement D (C/m²), each species' flux N across it and each face
288
+ * reaction's net rate (mol/(m²·s), toward +x and forward), how each species that crosses it does (its
289
+ * link type); for those crossing by permeability, the one-way fluxes [toward +x, toward −x] whose
290
+ * difference is N; with gates, each one's fraction open and the voltage they follow, φ_right − φ_left (V)
291
+ * @property {{ rate: Float64Array, forward: Float64Array, nu: Record<string, number>, regions: number[], total: number }[]} bulkReactions
292
+ * each bulk reaction's net rate per node (mol/(m³·s), forward minus backward, NaN where it doesn't
293
+ * run), its one-way forward rate (the backward is forward − rate), what one forward reaction makes
294
+ * of each species (ν, negative for what it consumes), and the net rate per region and in all (mol/(m²·s))
234
295
  * @property {number} charge total charge in the device, C/m²
235
296
  * @property {{ species: string, regions: number[], spectator: boolean, connected: boolean, reactive: boolean,
236
297
  * amount: number, reference: number, intake: number, drift: number }[]} conservation
237
298
  * @property {string[]} warnings
238
299
  * @property {boolean} converged
239
- * @property {number} iterations
300
+ * @property {number} iterations Newton iterations
301
+ * @property {number} [steps] solve(): the steps it took to the steady state; advance(): accepted steps
302
+ * @property {number} [substeps] step(): how many pieces the interval was split into
303
+ * @property {(number | object)[]} [history] solver bookkeeping: solve(), a record of its steps and
304
+ * stages; step(), Newton's largest update per iteration (thermal units)
240
305
  * @property {number} time s
241
306
  * @property {boolean} steady whether this is a converged steady state (solve()), not a transient's
242
307
  * @property {boolean} [done] advance(): whether tEnd was reached (false if the frame budget ran out)
243
308
  * @property {number} [rejected] advance(): steps rejected by error control or Newton failure
244
309
  * @property {{ t: number[], current: number[], voltage: number[], probes?: number[][] }} [trace] advance():
245
- * terminal current (A/m², toward +x) and voltage after each accepted step, and each probe's
246
- * reading (mol/m³, or V for quantity 'V'), interpolated between the nodes around its x
310
+ * the right contact's current (A/m², toward +x) and V_right − V_left after each accepted step, and
311
+ * each probe's reading: inside, interpolated between the nodes around its x (mol/m³ for quantity
312
+ * 'c', V for 'V' or 'phi'); at a face, a species' flux through it (mol/(m²·s), toward +x) or a
313
+ * gate's fraction open
247
314
  */
248
315
 
249
316
  /**
250
317
  * @typedef {object} ImpedanceResult
251
318
  * @property {Float64Array} f frequencies, Hz
252
- * @property {{ re: Float64Array, im: Float64Array }} Z impedance −δV/δI, Ω·m²
319
+ * @property {{ re: Float64Array, im: Float64Array }} Z impedance δV/δI at the terminal, I into the device, Ω·m²
253
320
  * @property {{ phi: ComplexProfile, mu: Record<string, ComplexProfile>, c: Record<string, ComplexProfile> }[]} [profiles]
254
321
  * per frequency, per unit excitation (volt, or A/m² in current mode)
255
322
  * @property {string[]} warnings the steady state's warnings, which hold for its impedance too
package/types/index.d.ts CHANGED
@@ -48,11 +48,14 @@ export class Device {
48
48
  * Adaptive transient to time tEnd (s): variable-step BDF2 with local error control. With
49
49
  * `budgetMs`, it returns after that much wall time even if tEnd isn't reached (`done` says
50
50
  * which), so an animation can call it once per frame; the step size carries over. `probes`
51
- * read a species at points inside the device after every accepted step, into `trace.probes`
52
- * (one array per probe, alongside `trace.t`): what a detector at x sees.
51
+ * read a species (or φ) at points inside the device after every accepted step, into
52
+ * `trace.probes` (one array per probe, alongside `trace.t`): what a detector at x sees. At a
53
+ * face, `{ interface, species }` reads a species' flux through it (mol/(m²·s), toward +x), and
54
+ * `{ interface, gate }` a gate's fraction open.
53
55
  * @param {number} tEnd s
54
56
  * @param {{ tol?: number, dt0?: number, dtMax?: number, budgetMs?: number, maxSteps?: number, method?: 'bdf2' | 'be',
55
- * probes?: { x: number, species?: string, quantity?: 'c' | 'V' | 'phi', region?: string | number }[] }} [opts]
57
+ * probes?: ({ x: number, species?: string, quantity?: 'c' | 'V' | 'phi', region?: string | number }
58
+ * | { interface: number, species: string } | { interface: number, gate: string })[] }} [opts]
56
59
  * @returns {Solution}
57
60
  */
58
61
  advance(tEnd: number, opts?: {
@@ -62,12 +65,18 @@ export class Device {
62
65
  budgetMs?: number;
63
66
  maxSteps?: number;
64
67
  method?: "bdf2" | "be";
65
- probes?: {
68
+ probes?: ({
66
69
  x: number;
67
70
  species?: string;
68
71
  quantity?: "c" | "V" | "phi";
69
72
  region?: string | number;
70
- }[];
73
+ } | {
74
+ interface: number;
75
+ species: string;
76
+ } | {
77
+ interface: number;
78
+ gate: string;
79
+ })[];
71
80
  }): Solution;
72
81
  /**
73
82
  * Small-signal impedance Z(f) about the steady state (solved first), at one terminal: its
package/types/kit.d.ts CHANGED
@@ -25,7 +25,7 @@ export function vacuumLevel(def: object, side: VacuumSide, path?: string): numbe
25
25
  export function vacuumDipole(def: object, left: VacuumSide, right: VacuumSide): number;
26
26
  /**
27
27
  * The vacuum-level (Schottky–Mott-type) estimate of a `zeroCharge` where a conductor meets a
28
- * material: at a conductor region's face, or a contact's capacitive or dipole law. The conductor's
28
+ * material: at a conductor region's face, or a contact's capacitive or pinned law. The conductor's
29
29
  * vacuum level sits its work function W beyond its Fermi level, so
30
30
  * zeroCharge = V_F − φ_edge at zero charge = W + (V_vac − φ)_inside.
31
31
  * @param {object} def device definition
@@ -0,0 +1,5 @@
1
+ export function applyLaws(s: any, dt: any): void;
2
+ export function captureLaws(s: any): void;
3
+ export function captureLocal(s: any): void;
4
+ export function solveBordered(s: any, rhs: any, delta: any, deltaV: any, pins: any): void;
5
+ export function snapFlat(s: any): void;
@@ -0,0 +1,86 @@
1
+ export function moves({ model }: {
2
+ model: any;
3
+ }, st: any): boolean;
4
+ export function still({ model }: {
5
+ model: any;
6
+ }, st: any): boolean;
7
+ export function carrierZ({ model, z }: {
8
+ model: any;
9
+ z: any;
10
+ }, k: any): any;
11
+ export function contactCharges({ model, nNodes, phiUndefined }: {
12
+ model: any;
13
+ nNodes: any;
14
+ phiUndefined: any;
15
+ }, k: any): boolean;
16
+ export function feeds(k: any, st: any): any;
17
+ export function noCurrent(t: any): boolean;
18
+ export function chargeOnly({ model, terms }: {
19
+ model: any;
20
+ terms: any;
21
+ }, k: any): boolean;
22
+ export function conservedMoieties(s: any, { drives, alsoCurrent, skip }?: {
23
+ drives?: boolean;
24
+ alsoCurrent?: any[];
25
+ skip?: any;
26
+ }): Float64Array<any>[];
27
+ export function findLaws(s: any): {
28
+ constraints: any[];
29
+ affinities: {
30
+ rx: any;
31
+ f: any;
32
+ row: any;
33
+ idx: Int32Array<any>;
34
+ w: Float64Array<any>;
35
+ len: number;
36
+ res: number;
37
+ }[];
38
+ localConstraints: {
39
+ parts: ({
40
+ i: any;
41
+ w: number;
42
+ q?: undefined;
43
+ k?: undefined;
44
+ } | {
45
+ q: any;
46
+ k: any;
47
+ w: number;
48
+ i?: undefined;
49
+ })[];
50
+ row: any;
51
+ nodes: any;
52
+ reference: Float64Array<any>;
53
+ }[];
54
+ islands: {
55
+ stretch: any;
56
+ row: any;
57
+ flux: any[];
58
+ weight: number[];
59
+ outside: any[];
60
+ idx: Int32Array<ArrayBuffer>;
61
+ w: Float64Array<ArrayBuffer>;
62
+ len: number;
63
+ res: number;
64
+ }[];
65
+ timeLaws: {
66
+ parts: {
67
+ stretch: number;
68
+ w: number;
69
+ }[];
70
+ surface: {
71
+ col: number;
72
+ w: number;
73
+ }[];
74
+ carriers: any;
75
+ terminals: any[];
76
+ circuit: any;
77
+ lambda: number;
78
+ idx: Int32Array<ArrayBuffer>;
79
+ w: Float64Array<ArrayBuffer>;
80
+ len: number;
81
+ }[];
82
+ contactLaw: boolean[];
83
+ carriers: any;
84
+ conserved: Set<any>;
85
+ unkept: Float64Array<any>[];
86
+ };
package/types/plot.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * The colour of a trace's role and slot (a CSS colour), for drawing other charts in the same
3
3
  * colours as the level diagrams.
4
- * @param {'electron' | 'cation' | 'anion' | 'redox'} role
4
+ * @param {'electron' | 'cation' | 'anion' | 'redox' | 'phi'} role
5
5
  * @param {number} slot
6
6
  * @param {{ dark?: boolean }} [opts]
7
7
  */
8
- export function themeColor(role: "electron" | "cation" | "anion" | "redox", slot: number, { dark }?: {
8
+ export function themeColor(role: "electron" | "cation" | "anion" | "redox" | "phi", slot: number, { dark }?: {
9
9
  dark?: boolean;
10
10
  }, ...args: any[]): string;
11
11
  /**
@@ -10,6 +10,7 @@ export function makeSolution(solver: any, result?: {}): {
10
10
  V: {};
11
11
  Vstd: {};
12
12
  T: any;
13
+ geometry: any;
13
14
  time: any;
14
15
  steady: any;
15
16
  converged: any;