driftlet 0.13.0 → 0.16.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/llms.txt CHANGED
@@ -1,115 +1,128 @@
1
1
  # driftlet: a guide for agents
2
2
 
3
3
  driftlet is a dependency-free JavaScript library that solves 1D drift–diffusion–reaction
4
- (Poisson–Nernst–Planck) problems fast enough for live web demos: semiconductor junctions and
5
- solar cells, electrochemical cells and batteries, double layers, membranes, nerves, mixed
6
- conductors. These are one problem with different casts of species: electrons, holes, ions and
7
- neutral molecules, each moving down its own electrochemical potential μ̄, coupled by Poisson's
8
- equation, reactions and interfaces. So a device in any field is built the same way, from the
9
- same pieces. It runs in a browser, a Web Worker or Node, from npm or a CDN.
10
-
11
- This guide is for writing a demo or calculation with it. Every code block below runs in the
12
- test suite. The references: docs/device.md (the definition), docs/kit.md (the helpers),
13
- docs/conventions.md (signs and levels), docs/visualization.md (reading level diagrams),
14
- docs/alignment.md (band offsets and work functions), docs/statistics.md (non-ideal materials),
15
- docs/data.md (the data library) and docs/reliability.md (what's validated, and the limits).
4
+ (Poisson–Nernst–Planck) problems fast enough for live web demos. Semiconductor junctions, solar
5
+ cells, electrochemical cells, batteries, membranes and nerves are one problem with different
6
+ casts of species (electrons, holes, ions, neutral molecules), each moving down its own
7
+ electrochemical potential μ̄, coupled by Poisson's equation, reactions and interfaces. So a
8
+ device in any field is built the same way, from the same pieces. It runs in a browser, a Web
9
+ Worker or Node, from npm or a CDN.
10
+
11
+ Every code block below runs in the test suite and prints its closed-form check. Skim the next
12
+ two sections, then go to the template nearest your device:
13
+
14
+ - semiconductors: a pn junction (with an I–V sweep), a live page with a slider, a solar cell, a
15
+ MOS capacitor, a Schottky diode, a thin-film transistor, a pulse drifting along a bar (under
16
+ Transients);
17
+ - electrochemistry: a cell, an electrode in a stirred solution (a potential step, and a cyclic
18
+ voltammogram), corrosion;
19
+ - physiology: a cell membrane (a resting potential), a voltage clamp, an action potential, one
20
+ travelling along an axon.
21
+
22
+ The references are in docs/: device.md (the definition), kit.md (the helpers), conventions.md
23
+ (signs and levels), visualization.md, alignment.md (band offsets), statistics.md (non-ideal
24
+ materials, insertion hosts), data.md and reliability.md (what's validated, and the limits).
16
25
 
17
26
  ## Imports
18
27
 
19
28
  From a CDN, pin the version you tested against:
20
29
 
21
30
  ```js nocheck
22
- import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.13.0/src/index.js';
23
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.13.0/src/kit.js';
24
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.13.0/src/plot.js';
31
+ import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/index.js';
32
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/kit.js';
33
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/plot.js';
25
34
  ```
26
35
 
27
36
  From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`. Every export is listed at the end.
28
37
 
29
38
  ## What to know first
30
39
 
31
- - **Units are SI**: m, mol/m³ (1 M = 1000 mol/m³), J/mol, m²/s, V, A/m², F/m², S/m², C/m³.
32
- `units` converts in (`units.um(1)`, `units.perCm3(1e17)` for carriers per cm³ or cm⁻³·s⁻¹,
33
- `units.eV(1.12)` to J/mol, `units.cm2PerS(36)`, `units.molar(0.1)`) and back (`units.toPerCm3`,
34
- `toEV`, `toMolar`, `toUm`, …). For physiology: 1 mS/cm² = 10 S/m², 1 µF/cm² = 0.01 F/m²,
35
- 1 µA/cm² = 0.01 A/m², mM = mol/m³.
36
- - **Everything you set or read is an electrochemical potential μ̄** (or a voltage made from one).
37
- A terminal's `V` is the voltage of its terminal species: for an electron contact, V = −μ̄_e/F,
38
- what a voltmeter reads. φ is bookkeeping, per material, and never a control.
39
- - **Solutions** are plain objects of `Float64Array`s: `x`, `phi`, and per species `c`
40
- (concentration), `mu` (μ̄), `V` (the species voltage μ̄/(zF), a quasi-Fermi level for e⁻ and h⁺)
41
- and `Vstd` (the standard level, a band edge for e⁻ and h⁺; in volts the conduction band sits
42
- below the valence band). `NaN` marks where a value doesn't exist: a species absent from a
43
- material, `V` and `Vstd` of a neutral species, `c` and `Vstd` inside a metal.
40
+ - **Units are SI**: m, mol/m³ (1 M = 1000 mol/m³; 1e16 cm⁻³ is only 0.0166 mol/m³), J/mol, m²/s,
41
+ V, A/m², F/m², S/m². `units` converts in (`units.um(1)`, `units.perCm3(1e17)` for per cm³ or
42
+ per cm³·s, `units.eV(1.12)` to J/mol, `units.cm2PerS(36)`, `units.molar(0.1)`) and back
43
+ (`units.toPerCm3`, `toEV`, `toMolar`, `toUm`, …). Physiology: 1 mS/cm² = 10 S/m², 1 µF/cm² =
44
+ 0.01 F/m², 1 µA/cm² = 0.01 A/m², mM = mol/m³.
45
+ - **Everything you set or read is an electrochemical potential μ̄** (or a voltage made from one). A
46
+ terminal's `V` is the level of its terminal species (`terminal`, which the kit's contacts set):
47
+ for an electron contact V = −μ̄_e/F, what a voltmeter reads. φ is bookkeeping, per material, and
48
+ never a control.
49
+ - **Solutions** are plain objects of `Float64Array`s: `x`, `phi`, and per species `c`, `mu` (μ̄),
50
+ `V` (the species voltage μ̄/(zF): a quasi-Fermi level for e⁻ and h⁺) and `Vstd` (the standard
51
+ level: a band edge; in volts, E = −V, so E_c is the lower line). `NaN` marks a value
52
+ that doesn't exist (a species absent from a material, `V` of a neutral one, `c` in a metal).
44
53
  - **Currents.** `sol.terminals[name]` gives each terminal's `{ V, current }`, its current into
45
- the device. `sol.current` is the right contact's current toward +x (with ports, the two
46
- contacts' differ: read each from `sol.terminals`). Currents, fluxes and amounts are per m² of
47
- a planar device; with a `geometry` (spherical, cylindrical or any cross-section A(x)) they are
48
- totals through it.
54
+ the device; a current drive `{ I }` is the same way round (electrons fed in are I < 0).
55
+ `sol.current` is the right contact's toward +x, so a forward diode with its p side on the right
56
+ carries a negative one (with ports, read each contact from `sol.terminals`). Everything is per
57
+ m² of a planar device; with a `geometry` (shells, or any cross-section A(x)) it's a total.
49
58
  - **What isn't defaulted**: an alignment between different materials, a φ law where a metal
50
- meets a dielectric, and the amount of anything a contact doesn't feed (`c0`). An error names
51
- the path and says what to give. (Kinetic details are defaulted, as a symmetric α = 0.5 or a
52
- midgap trap, and listed in docs/device.md.)
53
- - **Check before trusting.** `sol.converged` must be true, and read `sol.warnings` (a solve
54
- that fails always says something there). `describe(def)` (kit) summarises a definition and
55
- flags likely unit slips. Once a device works, show or log `check(dev, sol).text` (kit): each
56
- species' ledger (what every terminal and reaction supplies, summing to zero), conservation in
57
- transients, and the same device on a grid twice as fine (a FAIL means the answer depends on
58
- the grid: refine it). It costs a solve: run it once, not per frame.
59
+ meets a dielectric, and the amount of anything a contact doesn't feed (`c0`). The error names
60
+ the path and says what to give. (Kinetic details are defaulted: listed in docs/device.md.)
61
+ - **Check before trusting.** `sol.converged` must be true; read `sol.warnings`, where a failed
62
+ solve always says why. `describe(def)` (kit) summarises a definition and flags likely unit
63
+ slips. Once a device works, log `check(dev, sol).text` (kit) once: each species' ledger,
64
+ conservation in transients, and a steady state on a grid twice as fine (a FAIL means refine;
65
+ a transient you rerun on a finer grid yourself).
59
66
 
60
67
  ## How a device is written
61
68
 
62
69
  - **`build({ T, library, stack, bulkReactions, ports, grid, geometry })`** (kit) reads a stack
63
70
  left to right: its first and last items are the contacts, `layer(material, length, opts)`s
64
- are regions, and any other object between two layers is the face between them. Give a layer a
65
- `name` to refer to it (a port's `region`, a probe); unnamed, it's only an index. `ohmic()`,
66
- `bath()` and the like write contacts, but a plain contact object works as well.
67
- - **The library** is pieces with `species` and `materials`: `semiconductor('Si')` (Si, Ge, GaAs,
68
- at 300 K), `aqueous(names, { epsr })` (H⁺ Li⁺ Na⁺ K⁺ Ag⁺ Cu²⁺ Zn²⁺ Fe²⁺ Fe³⁺ OH⁻ Cl⁻ NO₃⁻ SO₄²⁻ in
69
- water, at 25 °C), `metal('Ag')` (Ag Cu Au Al Zn Li Pt). `aqueous(names, { epsr: 0 })` makes the
70
- water strictly neutral (a macroscopic electrolyte, no double layers). An ion it lacks is pushed
71
- into both lists: `lib.species.push({ name: 'A-', z: -1, cRef: 1000 })` and
72
- `lib.materials.water.species['A-'] = { D, mu0 }`. Only μ° differences within a reaction
73
- matter, so for a redox couple, μ°_red − μ°_ox = −F·E° (E° against SHE, with the library's
74
- μ°_H⁺ = 0). An insulator is a material with no mobile species:
75
- `{ species: [], materials: { SiO2: { epsr: 3.9, species: {} } } }`.
76
- - **Faces.** Between two different non-metal materials, give an alignment, `{ dipole }` or
77
- `{ step: { species: 'e-', value } }` (J/mol), or `phi: 'neutral'` for a macroscopic model.
78
- A face next to a metal takes no alignment, but if the other side has ε > 0 it needs a φ law:
79
- `{ phi: { type: 'capacitive', C }, zeroCharge }` or `phi: 'neutral'`. Reactions at a face
80
- label their sides: `'Li+(left) = Li+(right)'`.
81
- - **Ports** are terminals attached sideways to a window of nodes inside a region (`from`, `to`,
82
- m from its left end): a source or drain, an injector, a reference electrode (at `I: 0`), an
83
- electrode spread through a film (`reactions`, `area`), a gate along a channel or a membrane along
84
- an axon (`capacitance`, `gates`).
71
+ are regions (give one a `name` to refer to it), and any other object between two layers is
72
+ the face between them. `ohmic()`, `bath()` and the like write contacts; a plain object works.
73
+ - **The library** is pieces with `species` and `materials`: `semiconductor('Si')` (Si, Ge, GaAs, at
74
+ 300 K), `aqueous(names)` (H⁺ Li⁺ Na⁺ K⁺ Ag⁺ Cu²⁺ Zn²⁺ Fe²⁺ Fe³⁺ OH⁻ Cl⁻ NO₃⁻ SO₄²⁻ in water, at 25
75
+ °C; `{ epsr: 0 }` makes it strictly neutral, with no double layers), `metal('Ag')` (Ag Cu Au Al Zn
76
+ Li Pt), all from `driftlet/kit`. A semiconductor's textbook numbers are in `SEMICONDUCTORS.Si`
77
+ (`Eg` and `chi` in eV, `Nc` and `Nv` in cm⁻³, `mun` and `mup` in cm²/(V·s), `epsr`); in the
78
+ material, e⁻'s and h⁺'s `cRef` are N_c and N_v in mol/m³ and h⁺'s `mu0` is the gap (e⁻'s is 0),
79
+ so n_i = √(N_c N_v) e^(−E_g/2RT), 6.7e9 cm⁻³ for Si. A species it lacks is pushed into both lists
80
+ (`z: 0` for a neutral one): `lib.species.push({ name: 'A-', z: -1, cRef: 1000 })`,
81
+ `lib.materials.water.species['A-'] = { D, mu0 }`. Only μ° differences within a reaction matter:
82
+ for a redox couple μ°_red − μ°_ox = −F·E° (against SHE, the library's μ°_H⁺ being 0). An
83
+ insulator is a material with no species: `{ species: [], materials: { SiO2: { epsr: 3.9, species:
84
+ {} } } }`.
85
+ - **Faces.** Between two different non-metals, give an alignment, `{ dipole }` or `{ step: {
86
+ species: 'e-', value } }` (J/mol), or `phi: 'neutral'`. `vacuumDipole(def, { material, anchor:
87
+ 'e-', offset: χ }, …)` estimates a dipole from electron affinities (Anderson's rule); it reads
88
+ only the materials, so build with `{ dipole: 0 }`, then put its value in (docs/alignment.md). Next
89
+ to a metal, a face takes no alignment, but needs a φ law if the other side has ε > 0: `{ phi: {
90
+ type: 'capacitive', C }, zeroCharge }` or `phi: 'neutral'`, where `zeroCharge` is the metal's V −
91
+ φ at its edge when the face holds no charge (a work-function difference, a potential of zero
92
+ charge, a Schottky barrier). A face's reactions label sides: `'Li+(left) = Li+(right)'`.
93
+ - **Ports** are terminals attached sideways to a window inside a region (`from`, `to`, m from its
94
+ left end): a source or drain, an injector, a reference electrode (`I: 0`), an electrode spread
95
+ through a film (`reactions`, `area`), a gate along a channel or a membrane along an axon
96
+ (`capacitance`, `gates`). A BJT's base contact is a port on a narrow window of the base,
97
+ `{ region: 'base', from, to, V, terminal: 'h+', species: { 'h+': 'equilibrium' } }`.
85
98
  - **Drives.** A terminal is held at a voltage `V`, driven by a current `I` (open circuit is
86
- `I: 0`), or behind a resistance `{ V, R }`. A voltage or current can be a waveform: a table
87
- `{ t: [...], values: [...] }`, piecewise linear (a step is two points at one time), or the kit's
88
- `pulse({ start, width, amplitude, rise, base })`, `square({ high, low, period, duty })`,
89
- `ramp({ from, to, duration, start })` and `triangle({ from, to, period })` (from `from` to `to`
90
- and back each period from t = 0, repeating: a cyclic voltammogram, in terminal volts). A drive's
91
- current is the conventional current into the device: electrons fed in are I < 0. `dev.set({ contacts: { right: { V } } })`
92
- changes a drive and keeps the state as a warm start; ports are patched by name,
93
- `{ ports: { gate: { V } } }`.
94
- - **Reactions** are equations: `{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }`. A coefficient is a
95
- number and a space (`2 e-`); a participant that isn't a species (solid silver) is fixed by its
96
- μ: `fixed: { 'Ag(s)': 0 }`. A bulk reaction's forward rate is `kf · Π c_reactants` in
97
- mol/(m³·s), with c in mol/m³ (1e16 cm⁻³ is only 0.0166 mol/m³); the backward rate follows from
98
- the μ°, so the net rate is the forward rate times (1 − e^(−A/RT)), A the affinity. Band-to-band
99
- recombination `e- + h+ = 0` gives a minority lifetime τ = 1/(kf·N), N the majority density in
100
- mol/m³: `kf: 1e8` is τ ≈ 0.6 µs at 1e16 cm⁻³. Trap-assisted (SRH) recombination is
101
- `{ equation: 'e- + h+ = 0', srh: { Si: { tauN, tauP } } }` (s), and at a face
102
- `{ equation: 'e-(left) + h+(right) = 0', srh: { vn, vp } }` (m/s). Band-to-band and the kit's
103
- `recombination()` are bimolecular, so their lifetime shortens at high injection; for a constant
104
- minority lifetime τ_p at any injection, use SRH with τ_N ≪ τ_p. Generation is a reaction from
105
- a reservoir (`photon = e- + h+`), and light absorbed as it goes in is
106
- `photogeneration({ material, flux, alpha, mu: units.eV(3), from, to })`. A face reaction's
107
- `k0` is in mol/(m²·s), the rate with every participant at its reference concentration (cRef):
108
- i₀ = nF·k0 there, and k0 = k°·cRef for a textbook k° in m/s (1 cm/s at cRef = 1000 mol/m³:
109
- `k0: 10`). A couple the library lacks is two species pushed into it (`z: 0` for a neutral one),
110
- with μ°_red − μ°_ox = −F·E°.
111
-
112
- `build()` writes a plain definition for `new Device()`, which can be written by hand too:
99
+ `I: 0`), or behind a resistance `{ V, R }`. Either can be a waveform: a table `{ t, values }`
100
+ (piecewise linear; a step is two points at one time), or the kit's `pulse({ start, width,
101
+ amplitude, rise, base })`, `square({ high, low, period, duty })`, `ramp({ from, to, duration,
102
+ start })` or `triangle({ from, to, period })` (out and back each period from t = 0: a cyclic
103
+ voltammogram, in terminal volts). `dev.set({ contacts: { right: { V } } })` changes a drive
104
+ and keeps the state as a warm start; ports are patched by name, `{ ports: { gate: { V } } }`.
105
+ - **Reactions** are equations: `{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }`, a coefficient
106
+ written with its space (`2 e-`). A participant that isn't a species (solid silver) is fixed by
107
+ its μ: `fixed: { 'Ag(s)': 0 }`. A bulk reaction's forward rate is `kf · Π c_reactants`
108
+ (mol/(m³·s)); the backward one follows from the μ°.
109
+ - **Recombination and light.** Band-to-band `e- + h+ = 0` gives a minority lifetime τ =
110
+ 1/(kf·N), N the majority density in mol/m³ (`kf: 1e8` is 0.6 µs at 1e16 cm⁻³); being
111
+ bimolecular, as is the kit's `recombination()`, it shortens at high injection. SRH is `srh: { Si:
112
+ { tauN, tauP } }` in place of `kf` (s), or at a face `{ equation: 'e-(left) + h+(right) = 0',
113
+ srh: { vn, vp } }` (m/s); with τ_N ≪ τ_p it gives n-type silicon's holes a lifetime τ_p at any
114
+ injection. Light is a reaction from a reservoir (`photon = e- + h+`, uniform), or absorbed as
115
+ it goes in, `photogeneration({ material, flux, alpha, mu: units.eV(3), from, to })`. To switch
116
+ it during a run, `set()` the device's `bulkReactions` (the state and the step size carry over).
117
+ - **Electrode kinetics.** A face reaction's `k0` (mol/(m²·s)) is its rate with every participant
118
+ at cRef: i₀ = nF·k0 there, and k0 = k°·cRef for a textbook k° in m/s (1 cm/s at 1 M is
119
+ `k0: 10`).
120
+ - **Grids** grade geometrically from `hmin` at each region's ends up to `hmax`, neighbouring cells
121
+ differing by at most `ratio` (1.2): put `hmin` below the shortest length that matters (a Debye
122
+ length, a depletion edge). `check()` says whether it's fine enough.
123
+
124
+ `build()` is the usual way in. It writes a plain definition for `new Device()`, which some
125
+ templates below write by hand (where a port spans a region, or no stack fits):
113
126
 
114
127
  ```js
115
128
  import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
@@ -133,6 +146,14 @@ const tK = D['K+'] / (D['K+'] + D['Cl-']), VT = (GAS_CONSTANT * 298.15) / FARADA
133
146
  console.log(`EMF ${(sol.terminals.right.V * 1e3).toFixed(3)} mV, 2t₊V_T ln 10 = ${(2 * tK * VT * Math.log(10) * 1e3).toFixed(3)} mV`); // 58.048, 58.047
134
147
  ```
135
148
 
149
+ An ion-exchange membrane is a layer whose material has its ions slower, with a `fixedCharge` (−X·F
150
+ for X mol/m³ of fixed anions), between layers of water. A bath against the membrane itself names
151
+ its own material, `bath(c, 'Cl-', 0, { material: 'water' })`, and meets it through a Donnan step.
152
+ An exchange or partition constant between the membrane and the water is a difference of μ°:
153
+ give the membrane's ions μ° shifted from the water's by s_i, and K = e^(−(s_Na − s_H)/RT) for
154
+ Na⁺ against H⁺ (a large K draws in salt too, unless the co-ion's s is raised). The membrane
155
+ demo's page script is a worked example.
156
+
136
157
  ## Template: a semiconductor junction
137
158
 
138
159
  ```js
@@ -164,15 +185,14 @@ for (const V of [0, 0.2, 0.4, 0.6]) {
164
185
 
165
186
  ## Template: a live page with a slider
166
187
 
167
- `live()` merges slider moves made during a solve, warm-starts each solve and keeps the last
168
- good solution; on a failure `info.failed` is set (with `info.error`) and the solution is the last
169
- good one. `bandDiagram()` returns an SVG string; give it a fixed `range: [lo, hi]` so the axis
170
- doesn't jump as the slider moves. Pass `worker: true` to `live()` to solve in a Web Worker (it
171
- works with the library loaded from a CDN). `onsolution` may call `set()`, e.g. to iterate toward
172
- a value the page finds itself: each call queues another solve, so make sure it converges rather
173
- than oscillates. A device of a few hundred nodes solves in milliseconds; thousands, or
174
- transients, want a worker and `advance(t, { budgetMs })` per frame. (A headless browser taking a
175
- screenshot of a page that loads the library from a CDN needs a long virtual-time budget, 30 s.)
188
+ `live()` warm-starts each solve, merges slider moves made during one, and keeps the last good
189
+ solution (on a failure, `info.failed` and `info.error` are set). It returns `{ set, solution, ready,
190
+ close }`; `set()` and `ready` (the first solve) are promises of `{ solution, info }`.
191
+ `bandDiagram()` returns an SVG string; a fixed `range` keeps its axis still. `worker: true` solves
192
+ in a Web Worker, from a CDN too. A few hundred nodes solve in milliseconds; thousands, or
193
+ transients, want a worker and `advance(t, { budgetMs })` per frame. `onsolution` may call `set()`,
194
+ if that converges rather than oscillates. (A headless browser screenshotting a page that loads from
195
+ a CDN needs a long virtual-time budget, 30 s.)
176
196
 
177
197
  ```html
178
198
  <!doctype html>
@@ -182,9 +202,9 @@ screenshot of a page that loads the library from a CDN needs a long virtual-time
182
202
  <span id="readout"></span>
183
203
  <div id="figure"></div>
184
204
  <script type="module">
185
- import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.13.0/src/index.js';
186
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.13.0/src/kit.js';
187
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.13.0/src/plot.js';
205
+ import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/index.js';
206
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/kit.js';
207
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/plot.js';
188
208
 
189
209
  const def = build({
190
210
  T: 300,
@@ -207,11 +227,23 @@ slider.addEventListener('input', () => dev.set({ contacts: { right: { V: +slider
207
227
  To draw in your own style, `traces(sol, opts)` (kit, the same options) gives the lines as data;
208
228
  see "Reading level diagrams" below.
209
229
 
230
+ For an explainer, `particles(sol, { weight, cap })` (kit) draws a solution as dots, each `weight`
231
+ mol/m² of a species, hopping at rates taken from the solution: on average their density is its
232
+ concentration and their net crossing its flux. Say on the page that it samples the continuum
233
+ solution, and how much a dot is. Where a dot would be lost among millions (majority carriers, a
234
+ bath), a cell over `cap` dots is a sea: shade it by `expected(name)`. Each frame,
235
+ `swarm.step(dt)` in the device's time (about h²/D per hop, h a cell), then draw `swarm.dots`
236
+ (`{ species, x }`) and `swarm.events` (`'unmade'` is a recombination, `'cross'` an ion through a
237
+ membrane between two seas); on a new steady solution, `swarm.update(sol, { resample: true })`.
238
+ An ion beside a cell membrane hops ~1e6 times per crossing, so a membrane page shows seas and
239
+ crossing events, not walking ions. The demos' page scripts do this for a diode's carriers (pn),
240
+ ions crossing a nerve membrane (axon, `cap: 0`) and Donnan exclusion (membrane).
241
+
210
242
  ## Template: a solar cell
211
243
 
212
- Light is a reservoir: `photon = e- + h+` with the photons' μ well above the gap generates at
213
- the rate `kf` (uniform in the material). Open circuit is a terminal driven at `I: 0`, whose
214
- voltage is then V_oc. The photocurrent flows toward +x here (holes to the p side on the right).
244
+ `photon = e- + h+`, with the photons' μ well above the gap, generates uniformly at the rate `kf`.
245
+ Open circuit is `I: 0`, and the terminal's V is then V_oc. The photocurrent flows toward +x
246
+ (holes to the p side, on the right).
215
247
 
216
248
  ```js
217
249
  import { Device, units } from 'driftlet';
@@ -238,17 +270,25 @@ const oc = new Device(cell({ I: 0 })).solve(); // open circuit
238
270
  console.log(`J_sc = ${sc.current.toFixed(1)} A/m², V_oc = ${oc.terminals.right.V.toFixed(3)} V`);
239
271
  ```
240
272
 
273
+ In an organic cell, light makes excitons: a neutral species `X` in the donor, made by
274
+ `photogeneration({ ..., makes: 'X' })` and decaying by `{ equation: 'X = 0', kf: { donor: 1 / tau } }`.
275
+ They split at the donor/acceptor face by a reaction there, `X = h+(left) + e-(right)` with e⁻ and
276
+ h⁺ blocked at the face (the reaction moves them), and its reverse is charge-transfer
277
+ recombination; the face's `step` sets E_CT. With `alpha: 0` the splitting is a velocity
278
+ k0/cRef, a Robin condition on the exciton profile; with α > 0 it's k0/cRef · e^(α(E_X − E_CT)/RT).
279
+ The organic demo's page script is a worked example (J_sc against exciton diffusion theory, V_oc
280
+ against detailed balance).
281
+
241
282
  For a perovskite cell, prefer the curated `perovskiteCell({ V, light, bulkSRH })`: IonMonger's
242
283
  default cell (mobile iodide vacancies), with its J–V scans at seven rates in `PEROVSKITE_SCANS`
243
284
  and `hysteresis(trace, turn)` to read the same figures from yours.
244
285
 
245
286
  ## Template: a MOS capacitor
246
287
 
247
- The gate is a metal layer behind its contact, its face to the oxide capacitive (a metal screens
248
- within Å, so C is large) with the gate's alignment as `zeroCharge`. The oxide holds no species.
249
- In 1D nothing supplies the inversion layer's electrons (a MOSFET's source and drain do), so a port
250
- holds their level at the surface: the low-frequency C–V. The gate's charge is the displacement
251
- into the oxide, `sol.interfaces[0].D`.
288
+ The gate is a metal layer with a capacitive face to the oxide (a metal screens within Å, so C is
289
+ large) and the gate's alignment as `zeroCharge`. In 1D nothing supplies the inversion layer's
290
+ electrons (a MOSFET's source and drain do), so a port holds their level at the surface: the
291
+ low-frequency C–V. The gate's charge is the displacement into the oxide, `sol.interfaces[0].D`.
252
292
 
253
293
  ```js
254
294
  import { Device, units, EPS0, FARADAY, GAS_CONSTANT } from 'driftlet';
@@ -284,20 +324,21 @@ for (const Vg of [-2, -0.95, 0, 0.4, 2]) {
284
324
  console.log(`V_G ${Vg} V: C/C_ox ${(C / Cox).toFixed(3)}`); // 0.949, 0.701 (flat band), 0.267, 0.375, 0.973
285
325
  }
286
326
  // At flat band the silicon is a capacitor of ε/L_D, its Debye length's.
287
- const LD = Math.sqrt((11.7 * EPS0 * GAS_CONSTANT * 300) / (FARADAY * FARADAY * NA));
288
- console.log(`flat band, theory: ${(1 / (1 + (Cox * LD) / (11.7 * EPS0))).toFixed(3)}`); // 0.699
327
+ const LD = Math.sqrt((11.9 * EPS0 * GAS_CONSTANT * 300) / (FARADAY * FARADAY * NA)); // (the kit's Si: ε 11.9)
328
+ console.log(`flat band, theory: ${(1 / (1 + (Cox * LD) / (11.9 * EPS0))).toFixed(3)}`); // 0.701
289
329
  ```
290
330
 
291
- `dev.impedance([f], { terminal: 'left' })` gives the same C at low frequency. Draw the band
292
- diagram with `bandDiagram(sol, { energy: true, phi: ['SiO2'] })`: φ is the line to draw across
293
- an insulator, and its slope is the field.
331
+ `dev.impedance([f], { terminal: 'left' })` (C = Im(1/Z)/ω, from `{ f, Z: { re, im } }`) gives
332
+ the same C at any frequency with the port; without it, only below the minority supply's knee
333
+ (generation, or diffusion from the back contact: 0.01 Hz and below), and the high-frequency C–V
334
+ above. Draw it with
335
+ `bandDiagram(sol, { energy: true, phi: ['SiO2'] })`: across an insulator φ is the line to draw.
294
336
 
295
337
  ## Template: a Schottky diode
296
338
 
297
- A Schottky contact is a barrier pinned at the metal (`phi: 'pinned'`, `zeroCharge` the barrier,
298
- for Si whose μ°_e⁻ is 0) with its carriers crossing at their emission velocities: a `velocity`
299
- link, N_in = v (c_eq − c). With `'equilibrium'` links instead (an infinite velocity), the barrier
300
- passes whatever the silicon brings it: diffusion theory, some 20 times more current here.
339
+ A barrier pinned at the metal (`phi: 'pinned'`, `zeroCharge` the barrier height for Si, whose
340
+ μ°_e⁻ is 0), crossed at the emission velocity: a `velocity` link, N_in = v (c_eq − c). With
341
+ `'equilibrium'` links (infinite velocity) it's diffusion theory instead, 20 times the current here.
301
342
 
302
343
  ```js
303
344
  import { Device, units, FARADAY, GAS_CONSTANT } from 'driftlet';
@@ -321,9 +362,44 @@ for (const V of [0.1, 0.2, 0.3]) {
321
362
  }
322
363
  ```
323
364
 
324
- The built-in potential is `sol.phi.at(-1) − sol.phi[0]` at zero bias, and ∫(N_D − n)dx/N_D
325
- gives the depletion width. The same `velocity` link at an ohmic contact is its surface
326
- recombination velocity.
365
+ The same `velocity` link at an ohmic contact is its surface recombination velocity.
366
+
367
+ ## Template: a thin-film transistor (a capacitance along a channel)
368
+
369
+ A **port with a `capacitance`** holds charge σ = C (V − zeroCharge − φ) per area of electrode
370
+ along its window, balanced by the region's: a gate along a channel, a porous electrode's double
371
+ layer. Here x runs from source to drain with the gate spread along it: the gradual-channel
372
+ approximation, exact for a thin film until it pinches off.
373
+
374
+ ```js
375
+ import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
376
+
377
+ const T = 300, VT = (GAS_CONSTANT * T) / FARADAY;
378
+ const t = 20e-9, L = 10e-6, W = 100e-6, C = 1e-3, mu = 1e-3; // film, length, width (m); F/m² of gate; m²/(V·s)
379
+ const tft = (Vg, Vd) =>
380
+ new Device({
381
+ T,
382
+ species: [{ name: 'e-', z: -1, cRef: 46 }], // an undoped film's electrons (N_c 2.8e19 cm⁻³)
383
+ materials: { film: { epsr: 0, species: { 'e-': { D: mu * VT, mu0: 0 } } } }, // ε = 0: F·n·t balances σ
384
+ regions: [{ name: 'channel', material: 'film', length: L }],
385
+ contacts: {
386
+ left: { V: 0, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'neutral' }, // source
387
+ right: { V: Vd, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'neutral' }, // drain
388
+ },
389
+ ports: [{ name: 'gate', region: 'channel', V: Vg, area: 1 / t, capacitance: { C, zeroCharge: 0.3 } }],
390
+ grid: { hmin: L / 2000, hmax: L / 100, ratio: 1.05 }, // fine at the drain, where it pinches off
391
+ });
392
+ // The output characteristic at V_G = 2 V: linear, then saturating past V_D ≈ V_G − 0.3 V.
393
+ for (const Vd of [0.1, 0.5, 1, 1.5, 2, 3]) {
394
+ const sol = tft(2, Vd).solve();
395
+ const I = -sol.current * t * W; // sol.current is per m² of the film's section
396
+ console.log(`V_D ${Vd.toFixed(1)} V: I_D ${(I * 1e6).toFixed(2)} µA, σ at the drain ${(sol.ports[0].sigma.at(-1) * 1e3).toFixed(3)} mC/m²`);
397
+ }
398
+ ```
399
+
400
+ To sweep the gate on one device instead, patch the port: `dev.set({ ports: { gate: { V } } })`.
401
+ An organic electrochemical transistor is the same with a volumetric capacitance C*:
402
+ `area: 1 / t, capacitance: { C: Cstar * t }`.
327
403
 
328
404
  ## Template: an electrochemical cell
329
405
 
@@ -360,15 +436,14 @@ console.log(level(sol, silver)[mid] - level(sol, SHE, { standard: true })[mid]);
360
436
 
361
437
  ## Template: an electrode in a stirred solution, and a potential step
362
438
 
363
- One working electrode, a stagnant layer of solution, and the well-stirred bulk as a bath. The
364
- bath's terminal is its reference species (Cl⁻ here, like a Ag/AgCl reference), so terminal
365
- voltages are against that. An electrode's potential against SHE is its electrons' V minus φ in
366
- the solution (with the library's μ°, μ°_H⁺ = 0), read in the bulk. A terminal's current is into
367
- the device, so at the working electrode it's positive when oxidising (anodic). The bath sits
368
- across the stagnant layer, so its resistance is in series (uncompensated IR drop: keep the
369
- layer thin or the supporting salt strong where the surface potential matters). An excess of
370
- supporting electrolyte suppresses migration but doesn't remove it: at 100:1 the limiting current
371
- is still about 1% off pure diffusion.
439
+ A working electrode, a stagnant layer, and the stirred bulk as a bath whose terminal is its
440
+ reference species (Cl⁻: an Ag/AgCl reference). An electrode's potential against SHE is its
441
+ electrons' V minus φ in the bulk (with the library's μ°), and a Cl⁻ terminal sits ~1.36 V above
442
+ that, so an electrode held at 0 V beside it is strongly anodic: `describe(def)` says where SHE
443
+ sits, and an offset of that much (`bath(c, 'Cl-', 0, { offset })`) makes the terminal read SHE.
444
+ The working electrode's current is positive when oxidising. The layer's resistance is in series
445
+ (uncompensated IR: keep it thin, or the supporting salt strong); a 100:1 excess of salt still
446
+ leaves the limiting current about 1% off pure diffusion.
372
447
 
373
448
  ```js
374
449
  import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
@@ -409,15 +484,18 @@ for (const t of [3e-3, 1e-2, 3e-2]) {
409
484
  }
410
485
  ```
411
486
 
487
+ A cyclic voltammogram is this electrode driven by `triangle({ from, to, period })`, the period
488
+ being 2|to − from| over the scan rate: `solve()` it at `from` first, since the scan starts there
489
+ at t = 0, then `advance()`. Its potential against the bath's terminal is −`trace.voltage` (that
490
+ is V_right − V_left), and `trace.current` is anodic positive.
491
+
412
492
  ## Template: corrosion under a drop (an electrode spread through a film)
413
493
 
414
- A **port with reactions** is an electrode spread through a window of a region: a metal under a
415
- thin film of water, a crevice's walls, a porous electrode. Its `terminal` is the metal's carrier,
416
- `area` the electrode's area per volume of the window (1/h under a film h thick), and each
417
- reaction is Butler–Volmer per area, as at a face. The region must be strictly neutral (ε = 0); a
418
- `capacitance` adds the double layer. A **`geometry`** gives the device a cross-section A(x): here
419
- x is the radius of a drop seen from its centre, A = 2πr·h(r), and every current and amount becomes
420
- a total through it (A, mol), not per m².
494
+ A **port with reactions** is an electrode spread through a strictly neutral region: a metal under
495
+ a film of water, a crevice's walls, a porous electrode. Its `terminal` is the metal's carrier,
496
+ `area` its area per volume (1/h under a film h thick), each reaction Butler–Volmer per area, and
497
+ a `capacitance` the double layer. A **`geometry`** gives the device a cross-section A(x): here x
498
+ is the radius of a drop, A = 2πr·h(r), and currents and amounts become totals (A, mol).
421
499
 
422
500
  ```js
423
501
  import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
@@ -468,58 +546,17 @@ const evans = polarization(dev, sol, { port: 'iron', x: 0 }, levels);
468
546
  for (const rx of evans.reactions) console.log(`${rx.equation}: ${Array.from(rx.current, (i) => i.toFixed(2)).join(', ')} A/m²`);
469
547
  ```
470
548
 
471
- On an electrode's surface, species can take its sites (`surface: { name: { mu0, capacity } }`,
472
- Langmuir coverages), and a reaction with `bare: true` runs only on bare metal: a passive film
473
- (docs/device.md, "An electrode's surface"). For a battery's insertion electrode, a material can
474
- be described by its OCV curve (docs/statistics.md).
475
-
476
- ## Template: a thin-film transistor (a capacitance along a channel)
477
-
478
- A **port with a `capacitance`** holds charge σ = C (V − zeroCharge − φ) per area of electrode
479
- along its window, balanced by the region (Gauss's law, or neutrality at ε = 0): a gate along a
480
- channel, a porous electrode's double layer. In 1D a field-effect transistor is its channel, x from
481
- source to drain, with the gate spread along it: the gradual-channel approximation, exact for a thin
482
- film until it pinches off. (Its gate stack, across the channel, is the MOS capacitor above.)
483
-
484
- ```js
485
- import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
486
-
487
- const T = 300, VT = (GAS_CONSTANT * T) / FARADAY;
488
- const t = 20e-9, L = 10e-6, W = 100e-6, C = 1e-3, mu = 1e-3; // film, length, width (m); F/m² of gate; m²/(V·s)
489
- const tft = (Vg, Vd) =>
490
- new Device({
491
- T,
492
- species: [{ name: 'e-', z: -1, cRef: 46 }], // an undoped film's electrons (N_c 2.8e19 cm⁻³)
493
- materials: { film: { epsr: 0, species: { 'e-': { D: mu * VT, mu0: 0 } } } }, // ε = 0: F·n·t balances σ
494
- regions: [{ name: 'channel', material: 'film', length: L }],
495
- contacts: {
496
- left: { V: 0, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'neutral' }, // source
497
- right: { V: Vd, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'neutral' }, // drain
498
- },
499
- ports: [{ name: 'gate', region: 'channel', V: Vg, area: 1 / t, capacitance: { C, zeroCharge: 0.3 } }],
500
- grid: { hmin: L / 2000, hmax: L / 100, ratio: 1.05 }, // fine at the drain, where it pinches off
501
- });
502
- // The output characteristic at V_G = 2 V: linear, then saturating past V_D ≈ V_G − 0.3 V.
503
- for (const Vd of [0.1, 0.5, 1, 1.5, 2, 3]) {
504
- const sol = tft(2, Vd).solve();
505
- const I = -sol.current * t * W; // sol.current is per m² of the film's section
506
- console.log(`V_D ${Vd.toFixed(1)} V: I_D ${(I * 1e6).toFixed(2)} µA, σ at the drain ${(sol.ports[0].sigma.at(-1) * 1e3).toFixed(3)} mC/m²`);
507
- }
508
- ```
509
-
510
- To sweep the gate on one device instead, patch the port: `dev.set({ ports: { gate: { V } } })`.
511
- An organic electrochemical transistor is the same with a volumetric capacitance C*:
512
- `area: 1 / t, capacitance: { C: Cstar * t }`.
549
+ Species can take an electrode's surface sites (`surface`, Langmuir coverages), and a reaction
550
+ with `bare: true` runs only on bare metal: a passive film (docs/device.md). A battery's insertion
551
+ electrode is a material described by its OCV curve (docs/statistics.md).
513
552
 
514
553
  ## Template: a cell membrane
515
554
 
516
- A lipid bilayer is a face: a capacitor (about 1 µF/cm², 0.01 F/m²) that ions cross through
517
- channels, each with a permeability (`{ type: 'permeability', P }`, m/s: electrodiffusion in a
518
- constant field, so Goldman–Hodgkin–Katz comes out of it), and that pumps cross as reactions with
519
- saturating kinetics (`vmax`, `K`). The solutions on either side can be strictly neutral.
520
- Physiology's membrane potential is φ_in − φ_out: read it from `sol.phi`, with the inside on the
521
- right. A bath with a reference species reads that ion's level instead (a Ag/AgCl electrode's),
522
- which differs from Δφ by its Nernst term; `bath(c, drive)` without one reads its φ.
555
+ A lipid bilayer is a face: a capacitor (1 µF/cm², 0.01 F/m²) crossed by channels with a
556
+ permeability (`{ type: 'permeability', P }`, m/s: Goldman–Hodgkin–Katz comes out of it) and by
557
+ pumps, reactions with saturating kinetics (`vmax`, `K`). The membrane potential is φ_in − φ_out,
558
+ read from `sol.phi` as below. (A bath's terminal V is its φ only without a reference species,
559
+ `bath(c, drive)`; `bath(c, 'Cl-')`'s is Cl⁻'s level, which differs by Cl⁻'s Nernst term.)
523
560
 
524
561
  ```js
525
562
  import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
@@ -564,12 +601,10 @@ current), V_m is GHK. Without the pump, a closed cell runs down to Donnan equili
564
601
 
565
602
  ## Template: a voltage clamp
566
603
 
567
- Voltage-gated channels: a face's `gates` (each a fraction open with Hodgkin–Huxley kinetics,
568
- dx/dt = α(V)(1 − x) − β(V)x, in V = φ_right − φ_left) scale its links (`gates: { m: 3, h: 1 }`).
569
- `hodgkinHuxley({ T, linear: true })` (kit) writes the squid axon's as Hodgkin and Huxley had
570
- them: gated conductances, each current g(V − E) with E its ion's Nernst level. Between two baths
571
- whose terminal voltage is their φ (`bath(c, drive)`, no reference species), V_right − V_left is
572
- V_m, so a held step is their voltage clamp, and a solve finds the rest the clamp holds.
604
+ A face's `gates` (each a fraction open, dx/dt = α(V)(1 − x) − β(V)x with V = φ_right − φ_left)
605
+ scale its links (`gates: { m: 3, h: 1 }`). `hodgkinHuxley({ T, linear: true })` (kit) writes the
606
+ squid axon's as Hodgkin and Huxley had them, conductances g(V − E). Between two baths without a
607
+ reference species, V_right − V_left is V_m, so a held step is their voltage clamp.
573
608
 
574
609
  ```js
575
610
  import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
@@ -604,14 +639,11 @@ console.log(`I_Na peaks ${((run.trace.t[peak] - 1e-3) * 1e3).toFixed(2)} ms into
604
639
  console.log(`g_K after 10 ms: ${((k.at(-1) / (step - EK)) * 0.1).toFixed(2)} mS/cm²`); // 21.12: HH's 36 n∞⁴ is 21.5, which n nearly reaches by 10 ms
605
640
  ```
606
641
 
607
- Without `linear: true` (given `inside` and `outside`) the channels are GHK permeabilities matched
608
- to HH's conductances at −65 mV, which rectify: a g_K read as above then comes out 1.7 times HH's
609
- at −10 mV.
610
-
611
642
  ## Template: an action potential
612
643
 
613
- A pump-less cell has no steady rest (`solve()` finds Donnan), so start the transient from the
614
- concentrations given: the membrane settles within milliseconds.
644
+ Without `linear: true` (given `inside` and `outside`), `hodgkinHuxley` writes GHK permeabilities
645
+ matched to HH's conductances at −65 mV, which rectify as real channels do. A pump-less cell has no
646
+ steady rest (`solve()` finds Donnan), so start the transient from the concentrations given.
615
647
 
616
648
  ```js
617
649
  import { Device } from 'driftlet';
@@ -649,14 +681,14 @@ console.log(`peak ${(peak * 1e3).toFixed(1)} mV`); // +12.6: GHK's Na⁺ current
649
681
 
650
682
  ## Template: an action potential travelling along an axon
651
683
 
652
- Here x runs along the axon. The axoplasm is one region, and the membrane is a port spanning all
653
- of it: a capacitance plus gated channels to an outside given as a `bath` (the port's V is then
654
- the outside's φ). `hodgkinHuxley({ T, area })` writes Hodgkin and Huxley's linear channels for
655
- it, with `area` the membrane per volume, 2/a for radius a. Cable theory isn't put in; it comes
656
- out of the ions' drift along the axoplasm. Each solution's `ports[k].Vm[j]` and
657
- `ports[k].gates[name][j]` give the membrane potential and gates at each node of the window. A
658
- myelinated axon is regions (nodes and internodes), each with a port of its own (an internode's
659
- is just a small capacitance: myelin).
684
+ Here x runs along the axon: the axoplasm is one region, and the membrane a port spanning it, a
685
+ capacitance and gated channels to an outside given as a `bath` (the port's V is then the
686
+ outside's φ). `hodgkinHuxley({ T, area })` writes HH's channels for it, `area` being the membrane
687
+ per volume, 2/a. Cable theory isn't put in; it comes out of the ions' drift. `ports[k].Vm[j]` and
688
+ `ports[k].gates[name][j]` give the membrane at each node. A myelinated axon is regions, each with
689
+ its own port (an internode's just a small capacitance). Nothing warns of a grid too coarse for
690
+ the spike's front: the speed comes out 3% low on 2 mm cells, 0.2% on 0.5 mm, for a = 238 µm
691
+ (scale h with √a). The stimulus is a current per volume, so a thinner axon needs it as 1/a.
660
692
 
661
693
  ```js
662
694
  import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
@@ -691,20 +723,27 @@ console.log(`${(0.02 / (at(probes[1]) - at(probes[0]))).toFixed(1)} m/s`); // 18
691
723
 
692
724
  ## Transients
693
725
 
694
- `advance(t, opts)` steps from the device's present state (its `c0`, or a steady state if you
695
- called `solve()` first) to time t, adaptively, and returns the solution there with `done` and a
696
- `trace` of this call's accepted steps (`t`, `current`: the right contact's toward +x, `voltage`:
697
- V_right − V_left). It stops short when it runs out of `budgetMs` (an animation frame: call again)
698
- or, with a warning saying why, of `maxSteps` (100000) or of steps that converge. `probes` read
699
- the device at points inside it, at accepted steps, so give `dtMax` to resolve a signal in time:
700
- `{ x, species }` (c, or `quantity: 'V'`), `{ x, quantity: 'phi' }`, and at a face
701
- `{ interface: f, species }` (its flux) or `{ interface: f, gate }`. To stop at an event (a
702
- voltage cut-off, the end of a diode's storage time), advance in short calls and test after each;
703
- a driven current held past what the device can pass fails to converge. A device that only stores
704
- what comes in (a battery host filling at a current) has no steady state: don't `solve()` it,
705
- `advance()` from its `c0` (docs/statistics.md, insertion hosts). `dev.step(dt)` is one
706
- backward-Euler step, and `dev.impedance(frequencies, { terminal })` the small-signal Z(f) about
707
- the steady state (Ω·m²).
726
+ `advance(t, opts)` steps adaptively from the device's present state (its `c0`, or the steady
727
+ state if you called `solve()`) to time t. It returns the solution there, with `done` and a `trace`
728
+ of this call's accepted steps (`t`, `current`, `voltage`: V_right − V_left). It stops short at
729
+ `budgetMs` (an animation frame: call again) or, with a warning saying why, at `maxSteps` (100000)
730
+ or a step that won't converge. `probes`, read at accepted steps (give `dtMax` to resolve them):
731
+ `{ x, species }` (c, or `quantity: 'V'`), `{ x, quantity: 'phi' }`, and at a face `{ interface: f,
732
+ species }` (its flux) or `{ interface: f, gate }`.
733
+
734
+ - **Events.** To stop at one (a cut-off voltage, a diode's storage time), advance in short calls
735
+ and test after each: a driven current held past what the device can pass fails to converge.
736
+ - **The first instants.** The first step is sized by error control (from 1e-4 of the interval),
737
+ so a fast start (an initial plateau, a charging spike) can fall inside it: give `dt0` (and
738
+ `dtMax`) to see it. A voltage step's first point carries the displacement that charges the bulk
739
+ at once (a spike of ~1e4 A/m² for ~1e-10 s across a cell's Stern layers): leave it out of
740
+ integrals of I²R.
741
+ - **Storage.** A device that only stores what comes in (a battery host filling at a current, a
742
+ gate charging) has no steady state: `advance()` it from its `c0`, don't `solve()` it
743
+ (docs/statistics.md). At no current (`I: 0`), `solve()` keeps the charge it holds: a closed
744
+ battery sits at the OCV of its state of charge, a floating gate keeps its charge.
745
+ - **Other steps.** `dev.step(dt)` is one backward-Euler step, and `dev.impedance(frequencies,
746
+ { terminal })` the small-signal Z(f) about the steady state, in Ω·m².
708
747
 
709
748
  ```js
710
749
  import { Device, units, FARADAY } from 'driftlet';
@@ -730,132 +769,83 @@ const tArrive = run.trace.t[k] - width / 2; // from the pulse's middle
730
769
  console.log(`peak at ${(run.trace.t[k] * 1e6).toFixed(2)} µs, Δp ${units.toPerCm3(seen[k] - rest.c['h+'][0]).toExponential(1)} cm⁻³: μ_p ≈ ${(1e-3 / (E * tArrive) * 1e4).toFixed(0)} cm²/(V·s)`); // ≈ 1950 for the 1900 put in: a spreading, decaying pulse peaks a little early
731
770
  ```
732
771
 
733
- To start from a state instead of simulating how it got there (a packet already injected, a
734
- salt gradient laid down), give a region's `c0` a profile against the device's x,
735
- `c0: { 'h+': { x: [...], values: [...] } }`, and `advance()` from the new `Device` without
736
- `solve()` (which would go straight to the steady state). A given `c0` is the starting state, even
737
- for a species a contact feeds; species without one start at their contact's level, and φ starts
738
- neutral at each node. When two regions start with different solutions, give each its own `c0`.
739
-
740
- Transients cost more where a carrier's density spans many decades (minority holes in silicon
741
- ahead of the pulse) and at strong injection (Δp near n₀); budget for that, or coarsen the grid.
772
+ To start from a state rather than simulate how it got there (a packet already injected, a salt
773
+ gradient laid down), give a region's `c0` a profile, `{ 'h+': { x: [...], values: [...] } }` against
774
+ the device's x, and `advance()` the new `Device` without `solve()`. A given `c0` is the starting
775
+ state even for a species a contact feeds; the rest start at their contact's level. The grid isn't
776
+ fitted to the profile: grade it fine where the profile is steep (a warning says where it isn't).
777
+ `advance()`'s `tol` (1e-3) is each step's error, in thermal units: tighten it, or cap `dtMax`, for
778
+ numbers to compare with theory.
742
779
 
743
- `recorder(dev, { every, times, probes })` (kit) runs a transient in pieces: its `advance(tEnd,
744
- opts)` takes the same options, `rec.trace` is the whole run's trace from the start, and
745
- `rec.frames` are whole solutions (at the start, then at `times`, every `every` s, or the end of
746
- each call), with `rec.frame(t)` the one at or before t, for a time slider.
780
+ Transients cost more where a carrier's density spans many decades (minority holes in silicon)
781
+ and at strong injection; budget for that, or coarsen the grid. `recorder(dev, { every, times,
782
+ probes })` (kit) runs one in pieces: `rec.trace` is the whole run's, and `rec.frame(t)` the
783
+ whole solution at or before t, for a time slider.
747
784
 
748
785
  ## Reading level diagrams
749
786
 
750
- What `bandDiagram()` and `traces()` draw, and how to explain it to a reader (docs/visualization.md
751
- has the long form):
752
-
753
- - **Which picture.** For a device of electrons and holes only, draw the familiar band diagram,
754
- energy up: `bandDiagram(sol, { energy: true })` (E_c, E_v, E_Fn, E_Fp in eV; electron energies
755
- are E = −V). The species voltages below are the same lines in volts, turned over; use them where
756
- ions share the diagram, or for equilibrium between species, and don't present them in place of
757
- the band diagram.
758
- - **Fermi level** V_e⁻ = −μ̄_e⁻/F: the circuit voltage, what a voltmeter reads. A cell's voltage is
759
- the difference between its terminals' V_e⁻.
760
- - **Redox level** of Ox + z e⁻ ⇌ Red, `level(sol, half)` or `levels: [{ half }]` in the plotting
761
- options: V_e⁻(Ox/Red) = (μ̄_Ox − μ̄_Red)/(zF), where an electrode exchanging electrons by that
762
- couple would sit in equilibrium with the local composition. Drawn thick and dashed. At an
763
- electrode face, the step from the metal's Fermi level to it is the surface overpotential;
764
- through the solution it slopes with ohmic drop and concentration polarisation. Each couple has
765
- its own; they agree only in redox equilibrium.
766
- - **Standard redox level** (`standard: true`), thin and dashed: every participant at reference
767
- concentration. The redox level sits RT/(zF) ln(a_Ox/a_Red) from it (Nernst, locally).
768
- - **Electrode potentials** are gaps to the SHE's standard level V°_e⁻(SHE): E = V_e⁻(electrode) −
769
- V°_e⁻(SHE), E° = V°_e⁻(Ox/Red) − V°_e⁻(SHE), η = E − E_eq. With the data library's μ°,
770
- V°_e⁻(SHE) in water is φ, so `phi: true` draws it.
771
- - **Species voltage** V_i = μ̄_i/(z_iF) (thick, solid) for every charged species: the Fermi level
772
- for e⁻, a quasi-Fermi level for e⁻ and h⁺ in a semiconductor, what a reversible electrode reads
773
- for an ion. It's a display of μ̄_i (the physics, which also covers neutral species): each
774
- species moves down its own μ̄_i, drift and diffusion together, so its current flows down slopes
775
- in V_i (cations and holes move downhill, electrons and anions uphill), and it's in equilibrium
776
- where V_i is flat. A step at a face means resistance there.
787
+ What `bandDiagram()` and `traces()` draw, and how to explain it (docs/visualization.md has more):
788
+
789
+ - **Which picture.** For electrons and holes only, draw the familiar band diagram, energy up:
790
+ `bandDiagram(sol, { energy: true })` (E_c, E_v, E_Fn, E_Fp in eV, E = −V). The species voltages
791
+ below are the same lines in volts, turned over: use them where ions share the diagram, or for
792
+ equilibrium between species, never in place of the band diagram.
793
+ - **Species voltage** V_i = μ̄_i/(z_iF) (thick, solid): the Fermi level for e⁻ (what a voltmeter
794
+ reads), a quasi-Fermi level in a semiconductor, what a reversible electrode reads for an ion.
795
+ Each species moves down its own μ̄_i, drift and diffusion together, so its current flows down
796
+ slopes in V_i (cations and holes downhill, electrons and anions uphill); flat is equilibrium,
797
+ and a step at a face is resistance there. Call it the species voltage and say what it is, since
798
+ it's new to most readers; credit Jamnik and Maier (Phys. Chem. Chem. Phys. 3, 1668 (2001))
799
+ where V_i rails explain impedance or linear response.
777
800
  - **Standard level** V°_i = φ + μ°_i/(z_iF) (thin, solid): a band edge for e⁻ and h⁺; V_i − V°_i =
778
- (RT/z_iF) ln(c/c_ref) for ideal species. Steps in V°_i at faces (Donnan, junctions, dipoles) are
779
- bookkeeping.
780
- - **What's left out.** φ isn't drawn: it's a per-material convention (meaningless in a metal), and
781
- −eφ isn't any electron's energy; the V°_i ladder shows its variation within a material and means
782
- something across faces too. The exceptions: across an insulator with no mobile species, a
783
- plain sloped φ (`phi: ['SiO2']`) is the line to draw (its slope is the field), and in vacuum −eφ
784
- really is an electron's energy.
785
- - **No fixed reference.** Shifting every line together changes nothing, and every gap is
786
- physical. "vs SHE" and "from vacuum" each need a chosen point, because both levels vary in x.
787
- - **Gaps between species.** V_i − V_j is free of φ but carries a constant from the elements'
788
- conventional zeros, so it's read through a reaction. `shifts` (marked ⌇) offsets one species'
789
- lines to fit a shared axis without changing any gap within that species.
790
- - **Plotting in your own style.** `traces(sol, opts)` gives `{ x, series: [{ id, label, kind,
791
- role, slot, y }], regions, faces, range }`: `kind` says how a line reads (`'level'` thick,
792
- `'standard'` thin, `'redox'` and `'redox-standard'` dashed, `'phi'`), `role` and `slot` keep a
793
- species' colour stable (`themeColor(role, slot)` in `driftlet/plot` gives ours), labels mark
794
- subscripts TeX-style (`labelParts(label)` splits them), `NaN` breaks a line, and doubled
795
- interface nodes share an x (draw them as vertical steps). Or read the solution directly:
796
- `sol.x`, `sol.V[name]`, `sol.Vstd[name]`, `sol.c[name]`, `sol.phi`.
797
- - Call V_i the species voltage and say what it is; it's new to most readers. Credit Jamnik and
798
- Maier (Phys. Chem. Chem. Phys. 3, 1668 (2001)) where V_i rails explain impedance or linear
799
- response.
801
+ (RT/z_iF) ln(c/c_ref) for ideal species. Its steps at faces are bookkeeping.
802
+ - **Redox level** of Ox + z e⁻ ⇌ Red (`level(sol, half)`, or `levels: [{ half }]` in the plot
803
+ options; thick, dashed): (μ̄_Ox − μ̄_Red)/(zF), where an electrode by that couple would sit in
804
+ equilibrium with the local solution. At an electrode its step from the metal's Fermi level is
805
+ the surface overpotential. `standard: true` gives its standard level (thin, dashed).
806
+ - **Electrode potentials** are gaps to the SHE's standard level, which with the library's μ° is φ
807
+ in water (`phi: true` draws it): E = V_e⁻(electrode) − V°_e⁻(SHE).
808
+ - **What's left out.** φ is a per-material convention, and −eφ isn't any electron's energy, so it
809
+ isn't drawn, except across an insulator with no mobile species (`phi: ['SiO2']`: its slope is
810
+ the field). No level is a fixed reference: shifting every line together changes nothing. A gap
811
+ between two species' levels carries the elements' conventional zeros, so read it through a
812
+ reaction; `shifts` (marked ⌇) moves one species' lines to fit a shared axis.
813
+ - **Your own style.** `traces(sol, opts)` gives `{ x, series: [{ id, label, kind, role, slot, y }],
814
+ regions, faces, range }`: `kind` says how a line reads (`'level'`, `'standard'`, `'redox'`,
815
+ `'redox-standard'`, `'phi'`), `themeColor(role, slot)` (plot) gives our colours,
816
+ `labelParts(label)` splits TeX-style subscripts, `NaN` breaks a line, and a face's doubled nodes
817
+ share an x (a vertical step).
800
818
 
801
819
  ## Good habits
802
820
 
803
- - **Draw the closed-form answer alongside the simulation**, wherever a textbook limit exists
804
- (Shockley's law, a Nernst potential, a Gaussian pulse, a Warburg arc), as the templates do. It
805
- checks the setup: a solver's output looks authoritative even when an input is wrong, and a
806
- mismatch is the cheapest way to catch it. Where the two should differ (high injection, a
807
- finite pulse, the kinetics or IR drop of an electrode), say why.
808
- - **Stay honestly inside 1D.** driftlet models devices along one coordinate: planar, or with a
809
- cross-section A(x) (`geometry`: spherical or cylindrical shells, or a profile). Where the real
810
- device isn't one, say so and pick the slice that still tells the true story: a MOSFET as its
811
- gate stack (the MOS capacitor) or its channel (the thin-film transistor), never both at once,
812
- since pinch-off is 2D; a BJT along its emitter–base–collector axis; one particle of a porous
813
- battery electrode, or a thin dense film, rather than the whole porous electrode with diffusion
814
- inside its particles (that's a P2D model). A metal that reacts all along a film of electrolyte
815
- is a port with `reactions`, not a face at each end.
821
+ - **Draw the closed-form answer alongside the simulation** wherever a textbook limit exists, as
822
+ the templates do. A solver's output looks authoritative even when an input is wrong, and a
823
+ mismatch is the cheapest way to catch it. Where the two should differ, say why.
824
+ - **Stay honestly inside 1D.** Where the real device isn't one-dimensional, say so and pick the
825
+ slice that tells the true story: a MOSFET as its gate stack or its channel, never both (pinch-off
826
+ is 2D); a BJT along its emitter–base–collector axis; one particle of a porous battery electrode
827
+ rather than the whole electrode (that's a P2D model).
816
828
 
817
829
  ## Common mistakes
818
830
 
819
- - **No alignment at a heterointerface.** `interfaces[0] (between … [Si] and … [GaAs]): an
820
- interface between different materials needs an alignment`. Give the face `{ dipole }` or
821
- `{ step: { species: 'e-', value } }` (the conduction-band offset, J/mol), or estimate one from
822
- electron affinities with `vacuumDipole` (docs/alignment.md). `interfaces[f]` is the f-th face
823
- of the stack, counting from the left.
824
- - **A metal face with no φ law.** `… .phi: a face between a conductor and a material with
825
- ε > 0 needs an explicit φ law`. Give `{ phi: { type: 'capacitive', C }, zeroCharge }`, or
826
- `phi: 'neutral'`, or make the electrolyte strictly neutral (`aqueous(names, { epsr: 0 })`)
827
- when double layers don't matter.
828
- - **A gate as a contact on an oxide.** The carrier is blocked there (`… 'e-' is blocked at this
829
- contact`). Make the gate a metal layer with a capacitive face, as in the MOS template.
830
- - **`ohmic()` on a metal.** Its default holds both e⁻ and h⁺, for semiconductors. On a metal
831
- layer, write `ohmic(V, ['e-'])`.
832
- - **Ions on both sides of a reacting face.** Label them: `'Li+(left) = Li+(right)'`, and give
833
- their link (`species: { 'Li+': 'blocked' }` if they cross only by the reaction).
834
- - **Unit slips.** D in cm²/s, μ° in eV, capacitance in µF/cm², conductance in mS/cm², T in °C.
835
- `describe(def)` flags numbers that look like these.
836
- - **A closed system with no amount.** A species that doesn't reach a contact needs `c0`.
837
- - **An unbalanced `c0` in a closed neutral region.** With `epsr: 0` and no contact feeding a
838
- charged species, Σ z·c0 must be zero (traces included): `… carries a net charge of … mol/m³`.
839
- Add the counter-ion's share. (Where a contact feeds one, it makes up the difference.)
840
- - **A sharp junction between very different solutions** (3 M KCl against 1 µM traces) can't
841
- start: grade it over a few thin layers mixing one solution into the other, as at a frit.
842
- - **A steady solve of a closed cell where every ion can cross.** Its steady state is complete
843
- mixing. To watch a junction or a salt bridge, step in time from the unmixed `c0` (`advance`).
844
- - **Everything floating.** At least one terminal must be held at a voltage `V`.
831
+ - **Errors that say what to give.** A missing alignment at a heterointerface (`vacuumDipole`
832
+ estimates one from electron affinities, docs/alignment.md), a metal face with no φ law,
833
+ everything floating (hold one terminal at a `V`), a species that reaches no contact without a
834
+ `c0`, an unbalanced `c0` in a closed neutral region (add the counter-ion's share), `'2e-'` for
835
+ `'2 e-'`, a current drive of the wrong sign. `interfaces[f]` is the stack's f-th face.
836
+ - **A gate as a contact on an oxide.** It fails as `'e-' is absent from the end material 'SiO2'`: make the
837
+ gate a metal layer with a capacitive face, as in the MOS template.
838
+ - **`ohmic()` on a metal.** Its default holds e⁻ and h⁺; on a metal write `ohmic(V, ['e-'])`.
839
+ - **Ions on both sides of a reacting face.** Label them, `'Li+(left) = Li+(right)'`, and block
840
+ them (`species: { 'Li+': 'blocked' }`) if they cross only by the reaction.
841
+ - **Unit slips.** D in cm²/s, μ° in eV, µF/cm², mS/cm², T in °C. `describe(def)` flags them.
845
842
  - **The library's data are for 25 °C** (its semiconductors for 300 K: give `T: 300`). At body
846
- temperature, `T: 310.15` with the same μ° and D is a fair start; D rises about 2% per kelvin
847
- in water.
848
- - **Sign of the current.** `sol.current` is toward +x: a forward-biased diode with its p side on
849
- the right carries negative current. A terminal's current, and a current drive `{ I }`, are into
850
- the device: that same diode is driven forward by a positive `I` at its right contact. A drive of
851
- the wrong sign fails with `no steady state passes the driven current`, and says so.
852
- - **A membrane potential read from Cl⁻-referenced terminals.** They differ from Δφ by Cl⁻'s
853
- Nernst term: read V_m from `sol.phi` (or a `{ x, quantity: 'phi' }` probe), or use baths
854
- without a reference.
855
- - **A coefficient without its space.** `'2e- + …'` reads `2e-` as a species name; the error says
856
- to write `2 e-`.
857
- - **Changing c0 with `set()`.** A carried state keeps what it holds; `c0` is only the starting
858
- composition. Make a new `Device` to start over from it.
843
+ temperature the same μ° and D are a fair start; D rises about 2% per kelvin in water.
844
+ - **A steady solve of a closed cell where every ion can cross**: its steady state is complete
845
+ mixing. To watch a junction or a salt bridge, `advance()` from the unmixed `c0`.
846
+ - **A sharp junction between very different solutions** (3 M KCl against 1 µM) can't start:
847
+ grade it over a few thin layers, as at a frit.
848
+ - **Changing `c0` with `set()`.** A carried state keeps what it holds: make a new `Device`.
859
849
  - **An unresolved double layer.** If `sol.warnings` says so, refine the grid (`hmin` below the
860
850
  Debye length) or use a neutral model (`epsr: 0`, `phi: 'neutral'`).
861
851
 
@@ -874,6 +864,6 @@ has the long form):
874
864
  - checking: `describe`, `check`, `polarization` (an electrode's reactions at hypothetical
875
865
  metal levels, with the solution held: an Evans diagram), `henderson` and `planck`
876
866
  (liquid-junction potentials);
877
- - pages: `live`, `traces`, `recorder`;
867
+ - pages: `live`, `traces`, `recorder`, `particles`;
878
868
  - curated devices: `perovskiteCell`, `PEROVSKITE_SCANS`, `hysteresis`.
879
869
  - `driftlet/plot` (a quick look): `bandDiagram`, `levelChart`, `labelParts`, `themeColor`.