driftlet 0.16.4 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +3 -3
  2. package/docs/device.md +28 -14
  3. package/docs/kit.md +3 -1
  4. package/docs/numerics.md +12 -0
  5. package/docs/statistics.md +2 -1
  6. package/docs/templates.md +260 -0
  7. package/llms.txt +106 -300
  8. package/package.json +15 -3
  9. package/src/channels.js +6 -4
  10. package/src/checks.js +7 -2
  11. package/src/data.js +5 -1
  12. package/src/device.js +10 -4
  13. package/src/devices.js +2 -0
  14. package/src/equation.js +2 -1
  15. package/src/errors.js +33 -0
  16. package/src/grid.js +12 -0
  17. package/src/index.js +17 -7
  18. package/src/junction.js +3 -1
  19. package/src/kit.js +1 -1
  20. package/src/live.js +2 -0
  21. package/src/particles.js +2 -0
  22. package/src/plot.js +4 -0
  23. package/src/polarization.js +4 -3
  24. package/src/recorder.js +2 -1
  25. package/src/solution.js +20 -5
  26. package/src/solver.js +70 -16
  27. package/src/sources.js +8 -1
  28. package/src/stack.js +14 -1
  29. package/src/statistics.js +1 -1
  30. package/src/traces.js +3 -0
  31. package/src/types.js +5 -2
  32. package/types/channels.d.ts +3 -3
  33. package/types/checks.d.ts +1 -1
  34. package/types/data.d.ts +3 -3
  35. package/types/devices.d.ts +1 -1
  36. package/types/equation.d.ts +1 -1
  37. package/types/errors.d.ts +8 -0
  38. package/types/index.d.ts +2 -5
  39. package/types/junction.d.ts +2 -2
  40. package/types/kit.d.ts +1 -1
  41. package/types/live.d.ts +1 -1
  42. package/types/particles.d.ts +1 -1
  43. package/types/plot.d.ts +3 -3
  44. package/types/polarization.d.ts +1 -1
  45. package/types/recorder.d.ts +1 -1
  46. package/types/solver.d.ts +2 -0
  47. package/types/sources.d.ts +7 -7
  48. package/types/traces.d.ts +1 -1
  49. package/types/types.d.ts +11 -2
package/src/solver.js CHANGED
@@ -373,7 +373,8 @@ export class Solver {
373
373
  // φ is undefined in a conductor or an insertion host, and in a charge-free region (no
374
374
  // charged species) unless its electrostatic cluster, the regions joined to it by faces that
375
375
  // aren't neutral, reaches something that fixes φ: a region with charged species, a gate or
376
- // pinned contact, or a capacitive face to a conductor. (Else φ there is a free constant.)
376
+ // pinned contact, or a face that isn't neutral to a conductor or to a strictly neutral region
377
+ // with ions. (Else φ there is a free constant.)
377
378
  this.phiUndefined = new Uint8Array(nNodes);
378
379
  const charged = (mat) => species.some((sp, i) => mat.present[i] && sp.z !== 0);
379
380
  const pins = (ct) => ct.phi.type === 'capacitive' || ct.phi.type === 'pinned';
@@ -385,8 +386,11 @@ export class Solver {
385
386
  while (r1 + 1 < regions.length && model.interfaces[r1].phi.type !== 'neutral' && !free(r1 + 1)) r1++;
386
387
  let anchor = (r0 === 0 && pins(contacts.left)) || (r1 === regions.length - 1 && pins(contacts.right));
387
388
  for (let r = r0; r <= r1; r++) if (charged(materials[regions[r].material])) anchor = true;
388
- if (r0 > 0 && model.interfaces[r0 - 1].phi.type !== 'neutral' && materials[regions[r0 - 1].material].conductor) anchor = true;
389
- if (r1 < regions.length - 1 && model.interfaces[r1].phi.type !== 'neutral' && materials[regions[r1 + 1].material].conductor) anchor = true;
389
+ // A neighbour across a face that isn't neutral: a conductor (a gate), or a strictly neutral
390
+ // region whose ions fix its φ.
391
+ const fixes = (r) => materials[regions[r].material].conductor || (!materials[regions[r].material].phiFree && charged(materials[regions[r].material]));
392
+ if (r0 > 0 && model.interfaces[r0 - 1].phi.type !== 'neutral' && fixes(r0 - 1)) anchor = true;
393
+ if (r1 < regions.length - 1 && model.interfaces[r1].phi.type !== 'neutral' && fixes(r1 + 1)) anchor = true;
390
394
  for (let r = r0; r <= r1; r++) anchored[r] = anchor ? 1 : 2;
391
395
  r0 = r1;
392
396
  }
@@ -585,6 +589,16 @@ export class Solver {
585
589
  const mat = materials[regions[grid.nodeRegion[g]].material], law = contacts[k === 0 ? 'left' : 'right'].phi.type;
586
590
  if (!mat.conductor && mat.epsr === 0 && (law === 'bulk' || law === 'neutral') && !this.chargeNode[g] && !this.phiUndefined[g] && this.nodeIdeal[g]) this.lateStorage[g] = 1;
587
591
  });
592
+ // An end node's species held by a port (its contact blocks it, so the level is the port's):
593
+ // its row is the port's, storage and all, and the contact passes none of it.
594
+ this.portHeldEnd = new Uint8Array(2 * n);
595
+ model.ports.forEach((port) => {
596
+ [0, nNodes - 1].forEach((g, k) => {
597
+ if (!port.nodes.includes(g)) return;
598
+ const ct = contacts[k === 0 ? 'left' : 'right'];
599
+ for (let i = 0; i < n; i++) if (port.species[i].type === 'equilibrium' && ct.species[i].type === 'blocked') this.portHeldEnd[k * n + i] = 1;
600
+ });
601
+ });
588
602
  this.combining = false;
589
603
  this.bookkeeping = regions.map((_, r) => {
590
604
  const g0 = grid.regionStart[r], g1 = grid.regionEnd[r], nodes = [], segs = [];
@@ -1895,6 +1909,14 @@ export class Solver {
1895
1909
  // toward their mixed potential.
1896
1910
  for (const k of this.floating) {
1897
1911
  const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
1912
+ const gate = t.kind === 'contact' ? model.contacts[t.side].phi : null;
1913
+ if (gate?.type === 'capacitive') {
1914
+ // A gate contact likewise: at its zero-charge level from the φ beside it (a pinned
1915
+ // one's charge is the field's, already zero against a neutral start).
1916
+ const g = t.side === 'left' ? 0 : this.nNodes - 1;
1917
+ if (!this.phiUndefined[g]) this.termV[k] = gate.zeroCharge + this.VT * u[this.blockOfNode[g] * M];
1918
+ continue;
1919
+ }
1898
1920
  if (!port?.capacitance) continue;
1899
1921
  let w = 0, sum = 0;
1900
1922
  port.nodes.forEach((g, j) => {
@@ -1905,6 +1927,22 @@ export class Solver {
1905
1927
  });
1906
1928
  if (w > 0) this.termV[k] = port.capacitance.zeroCharge + sum / w;
1907
1929
  }
1930
+ // A floating port linked to its carrier (no reactions, no double layer: a wire into an
1931
+ // insertion host) starts at that carrier's level across its window, not at 0 V.
1932
+ for (const k of this.floating) {
1933
+ const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
1934
+ const i = port?.terminal, link = i === null || i === undefined ? null : port.species[i];
1935
+ if (!port || port.reactions.length > 0 || port.capacitance || !link || link.type === 'blocked' || z[i] === 0) continue;
1936
+ let w = 0, sum = 0;
1937
+ port.nodes.forEach((g, j) => {
1938
+ const a = this.model.grid.vol[g] * (this.portArea[t.index]?.[j] ?? 1); // (a window with no area: by volume)
1939
+ w += a;
1940
+ sum += (a * this.VT * u[this.blockOfNode[g] * M + 1 + i]) / z[i];
1941
+ });
1942
+ // (Levels at the start mean something only against a held terminal's, as for a contact.)
1943
+ const held = [0, 1].find((h) => !this.floating.includes(h) && Number.isFinite(level(this.terms[h].side)));
1944
+ if (w > 0 && held !== undefined) this.termV[k] = this.termV[held] + sum / w - (link.offset ?? 0) - level(this.terms[held].side);
1945
+ }
1908
1946
  // Gates start open as far as the starting voltage across their face holds them, α/(α + β).
1909
1947
  for (const gt of this.gateList) {
1910
1948
  const { gate, o } = gt, V = this._gateVoltage(gt), a = gateRate(gate.alpha, V)[0], b = gateRate(gate.beta, V)[0];
@@ -1952,9 +1990,9 @@ export class Solver {
1952
1990
  }
1953
1991
  const o = b * M + (p.side === 2 ? 1 + n + p.s : 1 + p.i), e = p.nu < 0 ? -p.nu * (1 - rx.alpha) : p.nu * rx.alpha;
1954
1992
  a -= p.nu * (u[o] + uLo[o]);
1955
- pref *= p.side === 2 ? Math.exp(e * (u[o] + uLo[o] - port.surface[p.s].mu0 / RT)) : powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
1993
+ pref *= p.side === 2 ? powr(th[g * nSurf + p.s], e) : powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
1956
1994
  }
1957
- if (rx.bare) pref *= this.th0[g];
1995
+ if (rx.sites) pref *= powr(this.th0[g], rx.sites);
1958
1996
  return pref * bvFactor(a, rx.alpha).g;
1959
1997
  }
1960
1998
 
@@ -2177,6 +2215,11 @@ export class Solver {
2177
2215
  if (f >= 0) this._j(b, r, this.blockOfFace[f], 0, (this.sheetSign[g] * this.model.grid.area[g]) / (z[im] * FARADAY * dt));
2178
2216
  }
2179
2217
 
2218
+ // Whether a port holds species i at node g, a device end node (see portHeldEnd).
2219
+ _portHeldAt(g, i) {
2220
+ return g === 0 ? this.portHeldEnd[i] === 1 : g === this.nNodes - 1 && this.portHeldEnd[this.n + i] === 1;
2221
+ }
2222
+
2180
2223
  // Dilute nodes g0…g1 (ideal statistics, c = c_ref e^ζ): storage of each species and the space
2181
2224
  // charge in the Gauss row, written straight into the diagonal blocks by local index. A
2182
2225
  // dielectric is the case with no species.
@@ -2196,7 +2239,7 @@ export class Solver {
2196
2239
  for (let i = 0; i < n; i++) {
2197
2240
  const k = g * n + i;
2198
2241
  if (!present[k]) continue;
2199
- const ck = c[k], l = loc[lb + 1 + i], stores = !(late && z[i] !== 0);
2242
+ const ck = c[k], l = loc[lb + 1 + i], stores = !(late && z[i] !== 0 && !this._portHeldAt(g, i));
2200
2243
  if (stores) res[R[lb + 1 + i]] += (v * (ck - cOld[k])) / dt;
2201
2244
  q += F * z[i] * ck;
2202
2245
  dq += F * z[i] * z[i] * ck;
@@ -3077,18 +3120,19 @@ export class Solver {
3077
3120
  aLo -= p.nu * uLo[o];
3078
3121
  const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
3079
3122
  if (p.side === 2) {
3080
- // a surface species at activity θ/θ₀ = e^ζ
3081
- pref *= Math.exp(e * (u[o] + uLo[o] - surf[p.s].mu0 / RT));
3123
+ // a surface species at its coverage θ = θ₀e^ζ, with ∂ln θ_s/∂η_x = δ_sx − θ_x
3124
+ pref *= powr(th[g * nSurf + p.s], e);
3082
3125
  d[1 + n + p.s] += e;
3126
+ for (let x = 0; x < surf.length; x++) d[1 + n + x] -= e * th[g * nSurf + x];
3083
3127
  } else {
3084
3128
  pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
3085
3129
  this._dlnc(g, p.i, e, d);
3086
3130
  }
3087
3131
  }
3088
- if (rx.bare) {
3089
- // on bare metal only: the free fraction θ₀ = 1 − Σθ, with ∂ln θ₀/∂η_s = −θ_s
3090
- for (let x = 0; x < surf.length; x++) d[1 + n + x] -= th[g * nSurf + x];
3091
- pref *= this.th0[g];
3132
+ if (rx.sites) {
3133
+ // the bare sites its transition state holds: θ₀ = 1 − Σθ, with ∂ln θ₀/∂η_s = −θ_s
3134
+ for (let x = 0; x < surf.length; x++) d[1 + n + x] -= rx.sites * th[g * nSurf + x];
3135
+ pref *= powr(this.th0[g], rx.sites);
3092
3136
  }
3093
3137
  const { g: bv, gp } = bvFactor(aHi + aLo, al);
3094
3138
  const rate = pref * bv;
@@ -3220,8 +3264,9 @@ export class Solver {
3220
3264
  // passes, so their flux is zero.)
3221
3265
  const closedIons = this.combining && dyn && this.chargeNode[g] === 1;
3222
3266
  let I = 0;
3267
+ const held = this.portHeldEnd.subarray(k * n, (k + 1) * n);
3223
3268
  for (let i = 0; i < n; i++) {
3224
- const active = this.loc[b * M + 1 + i] >= 0 && !(closedIons && z[i] !== 0);
3269
+ const active = this.loc[b * M + 1 + i] >= 0 && !(closedIons && z[i] !== 0) && !held[i];
3225
3270
  const nIn = active ? res[R[b * M + 1 + i]] : 0;
3226
3271
  flux[i] = sgn * nIn; // (toward +x)
3227
3272
  if (active && z[i] !== 0) {
@@ -3234,7 +3279,7 @@ export class Solver {
3234
3279
  if (this.combining && dyn && this.lateStorage[g] === 1) {
3235
3280
  const v = model.grid.vol[g], { cOld } = this;
3236
3281
  for (let i = 0; i < n; i++) {
3237
- if (z[i] === 0 || this.loc[b * M + 1 + i] < 0) continue;
3282
+ if (z[i] === 0 || this.loc[b * M + 1 + i] < 0 || held[i]) continue;
3238
3283
  const ci = c[g * n + i], st = (v * (ci - cOld[g * n + i])) / dt;
3239
3284
  res[R[b * M + 1 + i]] += st;
3240
3285
  flux[i] += sgn * st;
@@ -3967,8 +4012,12 @@ export class Solver {
3967
4012
  const trace = { t: [], current: [], voltage: [], ...(sample ? { probes: sample.out } : {}) };
3968
4013
  let steps = 0, rejected = 0, iterations = 0, failed = false;
3969
4014
  // Already there (an animation frame with no time to add): nothing to do, and the step size
3970
- // carried to the next call stays as it was.
3971
- if (!(tEnd > this.time)) return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace };
4015
+ // carried to the next call stays as it was. A time already passed, or reached again outside
4016
+ // an animation, is most likely advance(dt) read as a duration: say so.
4017
+ if (!(tEnd > this.time)) {
4018
+ const relative = tEnd < this.time || (this.time > 0 && opts.budgetMs === undefined);
4019
+ return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace, ...(relative ? { stopped: `advance(${tEnd}) did nothing: ${absolute}. To run for a further dt, advance(sol.time + dt)` } : {}) };
4020
+ }
3972
4021
  const t0 = this.time;
3973
4022
  let dt = this.dtNext ?? opts.dt0 ?? (tEnd - this.time) * 1e-4;
3974
4023
  if (!(dt > 0)) dt = (tEnd - this.time) * 1e-4;
@@ -4846,6 +4895,11 @@ export class Solver {
4846
4895
  }
4847
4896
  mu[name].re[g] = RT * xr[b * M + 1 + i];
4848
4897
  mu[name].im[g] = RT * xi[b * M + 1 + i];
4898
+ if (this.nodeConductor[g] >= 0) {
4899
+ // a metal has a Fermi level but no concentration (as a solution reports it)
4900
+ c[name].re[g] = c[name].im[g] = NaN;
4901
+ continue;
4902
+ }
4849
4903
  // δc_i = Σ_j K_ij (δη_j − z_j δφ̂)
4850
4904
  let sr = 0, si = 0;
4851
4905
  for (let j = 0; j < n; j++) {
package/src/sources.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // driftlet/kit: sources as plain definition data. Waveforms for a terminal's V or I, an injector
2
2
  // port, and band-to-band recombination from a lifetime. Each returns an ordinary spec value.
3
3
 
4
- import { DeviceError } from './errors.js';
4
+ import { DeviceError, checkOptions } from './errors.js';
5
5
 
6
6
  const fail = (message) => {
7
7
  throw new DeviceError(message);
@@ -19,6 +19,7 @@ const num = (v) => typeof v === 'number' && Number.isFinite(v);
19
19
  * @returns {{ t: number[], values: number[] }}
20
20
  */
21
21
  export function pulse({ start = 0, width, amplitude, rise = 0, base = 0 } = {}) {
22
+ checkOptions('pulse', arguments[0], ['start', 'width', 'amplitude', 'rise', 'base']);
22
23
  need(num(width) && width > 0, `pulse: width must be > 0 (s), got ${width}`);
23
24
  need(num(amplitude), `pulse: amplitude must be a finite number (V or A/m²), got ${amplitude}`);
24
25
  need(num(start) && start >= 0, `pulse: start must be ≥ 0 (s), got ${start}`);
@@ -35,6 +36,7 @@ export function pulse({ start = 0, width, amplitude, rise = 0, base = 0 } = {})
35
36
  * @returns {{ t: number[], values: number[], repeat: true }}
36
37
  */
37
38
  export function square({ high, low, period, duty = 0.5 } = {}) {
39
+ checkOptions('square', arguments[0], ['high', 'low', 'period', 'duty']);
38
40
  need(num(high) && num(low), `square: high and low must be finite numbers, got ${high} and ${low}`);
39
41
  need(num(period) && period > 0, `square: period must be > 0 (s), got ${period}`);
40
42
  need(num(duty) && duty > 0 && duty < 1, `square: duty must be between 0 and 1, got ${duty}`);
@@ -49,6 +51,7 @@ export function square({ high, low, period, duty = 0.5 } = {}) {
49
51
  * @returns {{ t: number[], values: number[], repeat: true }}
50
52
  */
51
53
  export function triangle({ from, to, period } = {}) {
54
+ checkOptions('triangle', arguments[0], ['from', 'to', 'period']);
52
55
  need(num(from) && num(to), `triangle: from and to must be finite numbers, got ${from} and ${to}`);
53
56
  need(num(period) && period > 0, `triangle: period must be > 0 (s), got ${period}`);
54
57
  return { t: [0, period / 2, period], values: [from, to, from], repeat: true };
@@ -60,6 +63,7 @@ export function triangle({ from, to, period } = {}) {
60
63
  * @returns {{ t: number[], values: number[] }}
61
64
  */
62
65
  export function ramp({ from, to, duration, start = 0 } = {}) {
66
+ checkOptions('ramp', arguments[0], ['from', 'to', 'duration', 'start']);
63
67
  need(num(from) && num(to), `ramp: from and to must be finite numbers, got ${from} and ${to}`);
64
68
  need(num(duration) && duration > 0, `ramp: duration must be > 0 (s), got ${duration}`);
65
69
  need(num(start) && start >= 0, `ramp: start must be ≥ 0 (s), got ${start}`);
@@ -78,6 +82,7 @@ export function ramp({ from, to, duration, start = 0 } = {}) {
78
82
  * @returns {object} a port, for the definition's `ports`
79
83
  */
80
84
  export function injector({ name = 'injector', region, from, to, species, I, headroom = 2.5 } = {}) {
85
+ checkOptions('injector', arguments[0], ['name', 'region', 'from', 'to', 'species', 'I', 'headroom']);
81
86
  need(region !== undefined, 'injector: give the region (its name or index)');
82
87
  need(num(from) && num(to) && to > from, `injector: from and to must bound the window, in m from the region's left end (got ${from}, ${to})`);
83
88
  need(typeof species === 'string', 'injector: give the species it injects, e.g. species: \'h+\'');
@@ -96,6 +101,7 @@ export function injector({ name = 'injector', region, from, to, species, I, head
96
101
  * @returns {{ equation: string, kf: Record<string, number> }} a bulk reaction
97
102
  */
98
103
  export function recombination({ material, tau, majority } = {}) {
104
+ checkOptions('recombination', arguments[0], ['material', 'tau', 'majority']);
99
105
  need(typeof material === 'string', 'recombination: give the material, by name');
100
106
  need(num(tau) && tau > 0, `recombination: tau must be > 0 (s), got ${tau}`);
101
107
  need(num(majority) && majority > 0, `recombination: majority must be the majority carrier density > 0 (mol/m³; units.perCm3 converts), got ${majority}`);
@@ -117,6 +123,7 @@ export function recombination({ material, tau, majority } = {}) {
117
123
  * @returns {{ equation: string, fixed: Record<string, number>, kf: Record<string, { x: number[], values: number[] }> }} a bulk reaction
118
124
  */
119
125
  export function photogeneration({ material, flux, alpha, mu, from = 0, to, makes = 'e- + h+' } = {}) {
126
+ checkOptions('photogeneration', arguments[0], ['material', 'flux', 'alpha', 'mu', 'from', 'to', 'makes']);
120
127
  need(typeof material === 'string', 'photogeneration: give the material, by name');
121
128
  need(num(flux) && flux >= 0, `photogeneration: flux must be the photon flux ≥ 0 entering the material (mol/(m²·s)), got ${flux}`);
122
129
  need(num(alpha) && alpha > 0, `photogeneration: alpha must be the absorption coefficient > 0 (1/m; 1/cm × 100), got ${alpha}`);
package/src/stack.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // that you didn't write: it only moves things to where the definition keeps them, and converts
5
5
  // the kit's doping shorthand (donors and acceptors).
6
6
 
7
- import { DeviceError } from './errors.js';
7
+ import { DeviceError, checkOptions } from './errors.js';
8
8
  import { FARADAY } from './constants.js';
9
9
 
10
10
  const fail = (message) => {
@@ -158,8 +158,21 @@ export function build(def) {
158
158
  }
159
159
  if (pendingFace) fail(`${at(pendingFace.k)}: a face after the last layer (the contact is the boundary there)`);
160
160
  if (regions.length === 0) fail('build.stack has no layers');
161
+ // A contact holding a carrier its layer lacks: most often ohmic()'s default on a metal.
162
+ for (const [k, end] of [[0, regions[0]], [stack.length - 1, regions[regions.length - 1]]]) {
163
+ const held = isObject(stack[k].species) ? Object.keys(stack[k].species).filter((s) => stack[k].species[s] !== 'blocked') : [];
164
+ const mat = materials[end.material];
165
+ if (!isObject(mat)) continue;
166
+ const has = (s) => (isObject(mat.conductor) ? mat.conductor.species === s : isObject(mat.species) && mat.species[s] !== undefined);
167
+ const lacks = held.filter((s) => !has(s));
168
+ if (lacks.length === 0) continue;
169
+ const kept = held.filter(has);
170
+ const hint = stack[k].phi === 'bulk' && kept.length > 0 ? `: write ohmic(V, [${kept.map((s) => `'${s}'`).join(', ')}]) (ohmic() holds e- and h+ unless told${isObject(mat.conductor) ? '; a metal has only its carrier' : ''})` : '';
171
+ fail(`${at(k)}: the contact holds ${lacks.map((s) => `'${s}'`).join(' and ')}, which its layer's material '${end.material}' doesn't have${hint}`);
172
+ }
161
173
 
162
174
  function region(item, path) {
175
+ checkOptions(path, item, ['name', 'material', 'length', 'fixedCharge', 'c0', 'velocity', 'mixing', 'grid', 'donors', 'acceptors']);
163
176
  const { donors, acceptors, ...r } = item;
164
177
  if (donors !== undefined || acceptors !== undefined) {
165
178
  if (r.fixedCharge !== undefined) fail(`${path}: give doping as donors and acceptors, or fixedCharge, not both`);
package/src/statistics.js CHANGED
@@ -157,7 +157,7 @@ function tableOccupancy(xs, zs, fail) {
157
157
  const x = k / 4000;
158
158
  const q = r(x);
159
159
  if (x > 0 && x < 1 && !(1 + x * (1 - x) * q.d > 1e-9)) {
160
- fail(`ocv: the interpolated curve isn't monotone near x = ${x.toFixed(3)} (E must fall steadily with x; add points or smooth the data)`);
160
+ fail(`ocv: near x = ${x.toFixed(3)} the curve is flat or rising against the ideal-solution slope, (RT/F) ln(x/(1−x)), as on a two-phase plateau (not supported): more points alone won't help; give it some of that slope, adding −a (RT/F) ln(x/(1−x)) to E (a = 0.1 passes at points 0.01 apart, less with points closer near the ends; docs/statistics.md)`);
161
161
  }
162
162
  rmin = Math.min(rmin, q.v);
163
163
  rmax = Math.max(rmax, q.v);
package/src/traces.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // range. Plain data, for any plotting toolkit (driftlet/plot draws it as SVG).
4
4
 
5
5
  import { level } from './equation.js';
6
+ import { DeviceError, checkOptions } from './errors.js';
6
7
 
7
8
  const finite = (a) => {
8
9
  for (let k = 0; k < a.length; k++) if (Number.isFinite(a[k])) return true;
@@ -78,6 +79,8 @@ export function typeset(name, z) {
78
79
  * faces: number[], range: [number, number] }}
79
80
  */
80
81
  export function traces(sol, { species, standard = true, phi = false, energy = false, levels = [], labels = {}, shifts = {} } = {}) {
82
+ checkOptions('traces', arguments[1], ['species', 'standard', 'phi', 'energy', 'levels', 'labels', 'shifts']);
83
+ if (!Array.isArray(levels)) throw new DeviceError("traces: levels must be a list, [{ half }, …], of the half-reactions whose levels to draw");
81
84
  const names = Object.keys(sol.V);
82
85
  const role = (name) => speciesRole(sol, name).role;
83
86
  const slotOf = (name) => speciesRole(sol, name).slot;
package/src/types.js CHANGED
@@ -163,7 +163,8 @@
163
163
  * species and the terminal species at the port's level
164
164
  * @property {Record<string, { mu0: number, capacity: number, theta0?: number }>} [surface] with reactions: species
165
165
  * on the electrode's sites, Langmuir (μ = μ° + RT ln(θ/θ₀)), capacity in mol of sites per m² of electrode,
166
- * shared; a reaction with `bare: true` runs only on the bare fraction θ₀ = 1 − Σθ
166
+ * shared; reactions run on Langmuir sites (what fills sites needs bare ones), and one with `bare: true`
167
+ * needs a bare site even if it fills none (θ₀ = 1 − Σθ)
167
168
  * @property {{ C: number, zeroCharge?: number }} [capacitance] a capacitance spread through the window (F/m² of
168
169
  * electrode): σ = C (V − zeroCharge − φ) on the port's side, aσ in each node's charge balance; a gate
169
170
  * along a channel, a double layer
@@ -218,7 +219,8 @@
218
219
  * @property {Record<string, { V: number, current: number }>} terminals each terminal (left, right,
219
220
  * then the ports by name): its voltage, and its current into the device (A/m²)
220
221
  * @property {{ left: ContactResult, right: ContactResult }} contacts
221
- * @property {{ left?: { V: number, D: number, charge: number }, right?: { V: number, D: number, charge: number } }} gates
222
+ * @property {{ left?: { V: number, D: number, charge: number }, right?: { V: number, D: number, charge: number } }} plates the charge on a gate or
223
+ * Stern plate at a capacitive contact
222
224
  * @property {{ name: string, V: number, flux: Record<string, number>, current: number, x?: Float64Array,
223
225
  * area?: Float64Array, rates?: Float64Array[], coverage?: Record<string, Float64Array>, bare?: Float64Array,
224
226
  * sigma?: Float64Array, charge?: number }[]} ports what each internal port brings into the device;
@@ -248,6 +250,7 @@
248
250
  * @property {{ re: Float64Array, im: Float64Array }} Z impedance −δV/δI, Ω·m²
249
251
  * @property {{ phi: ComplexProfile, mu: Record<string, ComplexProfile>, c: Record<string, ComplexProfile> }[]} [profiles]
250
252
  * per frequency, per unit excitation (volt, or A/m² in current mode)
253
+ * @property {string[]} warnings the steady state's warnings, which hold for its impedance too
251
254
  */
252
255
 
253
256
  /** @typedef {{ re: Float64Array, im: Float64Array }} ComplexProfile */
@@ -21,7 +21,7 @@ export function ghkCurrent(z: any, inside: any, outside: any, V: any, T: any): n
21
21
  * port along a region: links that are their linear conductances, G = g × area per volume, gated the
22
22
  * same, each ion's reversal potential following from the port's bath and the concentrations inside
23
23
  * (so `inside`, `outside` and `at` aren't needed).
24
- * @param {{ inside: object, outside: object, T?: number, g?: { Na?: number, K?: number, leak?: number },
24
+ * @param {{ inside: object, outside: object, T: number, g?: { Na?: number, K?: number, leak?: number },
25
25
  * leak?: string, leakZ?: number, at?: number, names?: { Na?: string, K?: string }, linear?: boolean, area?: number }} opts concentrations in
26
26
  * mol/m³ by species name, K, S/m², V
27
27
  * @returns {{ gates: object, species: object }}
@@ -29,7 +29,7 @@ export function ghkCurrent(z: any, inside: any, outside: any, V: any, T: any): n
29
29
  export function hodgkinHuxley({ inside, outside, T, g, leak, leakZ, at, names, linear, area }?: {
30
30
  inside: object;
31
31
  outside: object;
32
- T?: number;
32
+ T: number;
33
33
  g?: {
34
34
  Na?: number;
35
35
  K?: number;
@@ -44,7 +44,7 @@ export function hodgkinHuxley({ inside, outside, T, g, leak, leakZ, at, names, l
44
44
  };
45
45
  linear?: boolean;
46
46
  area?: number;
47
- }): {
47
+ }, ...args: any[]): {
48
48
  gates: object;
49
49
  species: object;
50
50
  };
package/types/checks.d.ts CHANGED
@@ -40,7 +40,7 @@ export function check(device: import("./index.js").Device, sol?: object, { refin
40
40
  refine?: boolean;
41
41
  tol?: number;
42
42
  order?: boolean;
43
- }): {
43
+ }, ...args: any[]): {
44
44
  ok: boolean;
45
45
  items: CheckItem[];
46
46
  text: string;
package/types/data.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  export function aqueous(names: string[], { epsr, material }?: {
10
10
  epsr?: number;
11
11
  material?: string;
12
- }): {
12
+ }, ...args: any[]): {
13
13
  species: object[];
14
14
  materials: Record<string, object>;
15
15
  };
@@ -24,7 +24,7 @@ export function aqueous(names: string[], { epsr, material }?: {
24
24
  */
25
25
  export function semiconductor(name: string, { material }?: {
26
26
  material?: string;
27
- }): {
27
+ }, ...args: any[]): {
28
28
  species: object[];
29
29
  materials: Record<string, object>;
30
30
  };
@@ -36,7 +36,7 @@ export function semiconductor(name: string, { material }?: {
36
36
  */
37
37
  export function metal(name: string, { material }?: {
38
38
  material?: string;
39
- }): {
39
+ }, ...args: any[]): {
40
40
  species: object[];
41
41
  materials: Record<string, object>;
42
42
  };
@@ -19,7 +19,7 @@ export function perovskiteCell({ V, light, bulkSRH }?: {
19
19
  };
20
20
  light?: number;
21
21
  bulkSRH?: boolean;
22
- }): import("./types.js").DeviceDefinition;
22
+ }, ...args: any[]): import("./types.js").DeviceDefinition;
23
23
  /**
24
24
  * A J–V loop's figures of merit, from a transient's trace (`{ t, voltage, current }`, as a
25
25
  * recorder or advance() gives it): the loop splits at time `turn` into the sweep before (`rev`)
@@ -78,7 +78,7 @@ export function level(sol: {
78
78
  fixed?: Record<string, number>;
79
79
  }, { standard }?: {
80
80
  standard?: boolean;
81
- }): Float64Array;
81
+ }, ...args: any[]): Float64Array;
82
82
  /**
83
83
  * The standard hydrogen electrode, 2 H⁺ + 2 e⁻ ⇌ H₂, with μ(H₂) = 0 (the element in its standard
84
84
  * state, on the usual table convention). Needs `H+` among the species.
package/types/errors.d.ts CHANGED
@@ -1,3 +1,11 @@
1
+ /**
2
+ * A function's options: a name it doesn't take (a typo) is an error, not silently ignored.
3
+ * @param {string} where the function, for the message
4
+ * @param {unknown} opts
5
+ * @param {string[]} allowed
6
+ * @param {new (message: string) => Error} [Kind]
7
+ */
8
+ export function checkOptions(where: string, opts: unknown, allowed: string[], Kind?: new (message: string) => Error): void;
1
9
  export class DeviceError extends Error {
2
10
  constructor(message: any);
3
11
  }
package/types/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
+ export { DeviceError } from "./device.js";
1
2
  export { SolverError } from "./solver.js";
2
- export { BlockTridiagonal } from "./blockTridiagonal.js";
3
3
  export * from "./constants.js";
4
4
  /** @typedef {import('./types.js').DeviceDefinition} DeviceDefinition */
5
5
  /** @typedef {import('./types.js').Solution} Solution */
@@ -72,7 +72,7 @@ export class Device {
72
72
  /**
73
73
  * Small-signal impedance Z(f) about the steady state (solved first), at one terminal: its
74
74
  * held voltage is perturbed (or its driven current), the others keep their drives.
75
- * Z = δV/δI with I into the device (Ω·m²).
75
+ * Z = δV/δI with I into the device (Ω·m²), with the steady state's `warnings`.
76
76
  * @param {ArrayLike<number>} frequencies Hz
77
77
  * @param {{ terminal?: string, profiles?: boolean }} [opts] the terminal (default 'right'), and
78
78
  * whether to return complex profiles per frequency
@@ -110,6 +110,3 @@ export class Device {
110
110
  }
111
111
  export type DeviceDefinition = import("./types.js").DeviceDefinition;
112
112
  export type Solution = import("./types.js").Solution;
113
- export { DeviceError, normalizeDevice } from "./device.js";
114
- export { buildGrid, gradedCells } from "./grid.js";
115
- export { bernoulli, bernoulliDerivative } from "./bernoulli.js";
@@ -12,7 +12,7 @@ export function henderson(left: Record<string, number>, right: Record<string, nu
12
12
  D: number;
13
13
  }>, { T }?: {
14
14
  T?: number;
15
- }): number;
15
+ }, ...args: any[]): number;
16
16
  /**
17
17
  * Planck's liquid-junction potential, φ_R − φ_L (V): the steady state of constrained diffusion,
18
18
  * a junction zone held between the two solutions, electroneutral, carrying no current. Solved by
@@ -29,4 +29,4 @@ export function planck(left: Record<string, number>, right: Record<string, numbe
29
29
  }>, { T, steps }?: {
30
30
  T?: number;
31
31
  steps?: number;
32
- }): number;
32
+ }, ...args: any[]): number;
package/types/kit.d.ts CHANGED
@@ -59,7 +59,7 @@ export type VacuumSide = {
59
59
  offset: number;
60
60
  };
61
61
  export { build, combine, layer, ohmic, bath } from "./stack.js";
62
- export { parseEquation, stoichiometry, half, SHE, level } from "./equation.js";
62
+ export { half, SHE, level } from "./equation.js";
63
63
  export { traces, speciesRole, typeset } from "./traces.js";
64
64
  export { pulse, square, triangle, ramp, injector, recombination, photogeneration } from "./sources.js";
65
65
  export { hodgkinHuxley, ghkCurrent } from "./channels.js";
package/types/live.d.ts CHANGED
@@ -28,7 +28,7 @@ export function live(def: object, { worker, onsolution, maxRamp }?: {
28
28
  worker?: boolean | (() => any);
29
29
  onsolution?: (solution: object, info: object) => void;
30
30
  maxRamp?: number;
31
- }): Live;
31
+ }, ...args: any[]): Live;
32
32
  /**
33
33
  * What a live device does with each change, on whichever thread solves it: apply, solve warm,
34
34
  * ramp from the last good state if that fails, and fall back to the last good solution.
@@ -20,7 +20,7 @@ export function particles(sol: object, { species, dots, weight, cap, cells, rand
20
20
  cap?: number;
21
21
  cells?: number;
22
22
  random?: () => number;
23
- }): {
23
+ }, ...args: any[]): {
24
24
  cells: any[];
25
25
  species: any;
26
26
  weight: {};
package/types/plot.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  export function themeColor(role: "electron" | "cation" | "anion" | "redox", slot: number, { dark }?: {
9
9
  dark?: boolean;
10
- }): string;
10
+ }, ...args: any[]): string;
11
11
  /**
12
12
  * A label split into plain and subscript runs. Labels mark subscripts TeX-style: `V_{e⁻}`, or
13
13
  * `C_ox` for a run of letters, digits and charge signs. Everything else is literal.
@@ -34,7 +34,7 @@ export function levelChart(tr: ReturnType<typeof traces>, { width, height, ylabe
34
34
  xlim?: [number, number];
35
35
  title?: string;
36
36
  ytick?: (v: number) => string;
37
- }): string;
37
+ }, ...args: any[]): string;
38
38
  /**
39
39
  * A solution's level diagram as an SVG string: each charged species' voltage (thick) and
40
40
  * standard level (thin), regions as bands; or with `energy: true`, the familiar energy-up band
@@ -43,7 +43,7 @@ export function levelChart(tr: ReturnType<typeof traces>, { width, height, ylabe
43
43
  * @param {Parameters<typeof traces>[1] & Parameters<typeof levelChart>[1]} [opts]
44
44
  * @returns {string}
45
45
  */
46
- export function bandDiagram(sol: import("./types.js").Solution, opts?: Parameters<typeof traces>[1] & Parameters<typeof levelChart>[1]): string;
46
+ export function bandDiagram(sol: import("./types.js").Solution, opts?: Parameters<typeof traces>[1] & Parameters<typeof levelChart>[1], ...args: any[]): string;
47
47
  export const THEME: Readonly<{
48
48
  light: {
49
49
  electron: string[];
@@ -24,7 +24,7 @@ export function polarization(device: import("./index.js").Device, sol: object, w
24
24
  x?: number;
25
25
  }, V: number | ArrayLike<number>, opts?: {
26
26
  surface?: "held" | "equilibrium";
27
- }): {
27
+ }, ...args: any[]): {
28
28
  V: Float64Array;
29
29
  current: Float64Array;
30
30
  bare?: Float64Array;
@@ -19,7 +19,7 @@ export function recorder(device: import("./index.js").Device, opts?: {
19
19
  species?: string;
20
20
  gate?: string;
21
21
  })[];
22
- }): {
22
+ }, ...args: any[]): {
23
23
  advance: (tEnd: number, aopts?: object) => import("./types.js").Solution;
24
24
  frame: (t: number) => import("./types.js").Solution;
25
25
  frames: import("./types.js").Solution[];
package/types/solver.d.ts CHANGED
@@ -115,6 +115,7 @@ export class Solver {
115
115
  chargeNode: Uint8Array<any>;
116
116
  chargeNodes: Int32Array<ArrayBuffer>;
117
117
  lateStorage: Uint8Array<any>;
118
+ portHeldEnd: Uint8Array<ArrayBuffer>;
118
119
  combining: boolean;
119
120
  bookkeeping: any;
120
121
  bookkeepingFaces: number[];
@@ -209,6 +210,7 @@ export class Solver {
209
210
  dtNow: any;
210
211
  transformed: any;
211
212
  _nodeConductor(g: any, dt: any): void;
213
+ _portHeldAt(g: any, i: any): boolean;
212
214
  _nodesDilute(g0: any, g1: any, dt: any): void;
213
215
  _nodeConcentrated(g: any, dt: any): void;
214
216
  _storage(g: any, i: any, dt: any, Kz?: number): void;
@@ -13,7 +13,7 @@ export function pulse({ start, width, amplitude, rise, base }?: {
13
13
  amplitude: number;
14
14
  rise?: number;
15
15
  base?: number;
16
- }): {
16
+ }, ...args: any[]): {
17
17
  t: number[];
18
18
  values: number[];
19
19
  };
@@ -27,7 +27,7 @@ export function square({ high, low, period, duty }?: {
27
27
  low: number;
28
28
  period: number;
29
29
  duty?: number;
30
- }): {
30
+ }, ...args: any[]): {
31
31
  t: number[];
32
32
  values: number[];
33
33
  repeat: true;
@@ -42,7 +42,7 @@ export function triangle({ from, to, period }?: {
42
42
  from: number;
43
43
  to: number;
44
44
  period: number;
45
- }): {
45
+ }, ...args: any[]): {
46
46
  t: number[];
47
47
  values: number[];
48
48
  repeat: true;
@@ -57,7 +57,7 @@ export function ramp({ from, to, duration, start }?: {
57
57
  to: number;
58
58
  duration: number;
59
59
  start?: number;
60
- }): {
60
+ }, ...args: any[]): {
61
61
  t: number[];
62
62
  values: number[];
63
63
  };
@@ -84,7 +84,7 @@ export function injector({ name, region, from, to, species, I, headroom }?: {
84
84
  repeat?: boolean;
85
85
  };
86
86
  headroom?: number;
87
- }): object;
87
+ }, ...args: any[]): object;
88
88
  /**
89
89
  * Band-to-band recombination e⁻ + h⁺ ⇌ 0 in a material, from the minority-carrier lifetime it
90
90
  * should give at a majority density: kf = 1/(τ · majority). (The rate is kf (np − n_i²), so τ is
@@ -96,7 +96,7 @@ export function recombination({ material, tau, majority }?: {
96
96
  material: string;
97
97
  tau: number;
98
98
  majority: number;
99
- }): {
99
+ }, ...args: any[]): {
100
100
  equation: string;
101
101
  kf: Record<string, number>;
102
102
  };
@@ -122,7 +122,7 @@ export function photogeneration({ material, flux, alpha, mu, from, to, makes }?:
122
122
  from?: number;
123
123
  to: number;
124
124
  makes?: string;
125
- }): {
125
+ }, ...args: any[]): {
126
126
  equation: string;
127
127
  fixed: Record<string, number>;
128
128
  kf: Record<string, {
package/types/traces.d.ts CHANGED
@@ -70,7 +70,7 @@ export function traces(sol: import("./types.js").Solution, { species, standard,
70
70
  }[];
71
71
  labels?: Record<string, string>;
72
72
  shifts?: Record<string, number>;
73
- }): {
73
+ }, ...args: any[]): {
74
74
  x: Float64Array;
75
75
  series: Trace[];
76
76
  regions: {