driftlet 0.9.0 → 0.11.4

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/README.md CHANGED
@@ -13,35 +13,24 @@ to be the engine of live, thermodynamically honest web demos of charge transport
13
13
  semiconductors, electrochemical cells, membranes and solid ionic conductors. It handles any mix
14
14
  of charged and neutral species, Poisson electrostatics (or strict neutrality), bulk and
15
15
  interfacial reactions, heterointerfaces, metals and external circuits, in steady state, in time
16
- and as small-signal impedance. Every physics feature is validated against analytic results, and
17
- it's small enough to run in a web page: no dependencies, millisecond solves.
16
+ and as small-signal impedance. It's small enough to run in a web page: no dependencies,
17
+ millisecond solves.
18
18
 
19
19
  [Live demos](https://markblundeberg.github.io/driftlet/demos/) · [npm](https://www.npmjs.com/package/driftlet) ·
20
20
  [reading level diagrams](docs/visualization.md) · [device reference](docs/device.md) ·
21
21
  [agent guide](llms.txt)
22
22
 
23
+ **Built to be relied on.** A drift–diffusion solver that handles a textbook pn junction is a
24
+ weekend's work; one that keeps working on the devices people actually build is not. driftlet is
25
+ tested two ways: every physics feature against an analytic result or an independent code, and
26
+ thousands of randomly generated devices (junctions, MOS capacitors, solar cells, electrolyte
27
+ cells, electrodes) put through cold solves, sweeps, transients and impedance, every result
28
+ checked. A solve that fails reports it, with what went wrong where it can tell.
29
+ [How far to trust it](docs/reliability.md) has the details, the numbers and the known limits.
30
+
23
31
  **Status: 0.x.** Released on npm and used by the demos, but the API may still change between
24
32
  minor versions.
25
33
 
26
- ## Is it for your problem?
27
-
28
- The same equations go by different names in different fields. If your problem is one-dimensional
29
- (planar, or radial: spheres and cylinders, or any cross-section varying along x), it's probably here:
30
-
31
- | If you work on | you may call it | start from | checked against |
32
- |---|---|---|---|
33
- | Semiconductor devices | drift–diffusion, van Roosbroeck, quasi-Fermi levels, Scharfetter–Gummel; pn, Schottky, MOS, heterojunctions; thin-film transistors (gradual channel) | [first example](#a-semiconductor-junction), [pn](https://markblundeberg.github.io/driftlet/demos/pn.html) and [MOS](https://markblundeberg.github.io/driftlet/demos/mos.html) demos, [`contacts`](test/contacts.test.js), [`metal`](test/metal.test.js) tests | exact built-in potential, Shockley J–V, depletion charge, MOS C–V |
34
- | Solar cells | photogeneration, radiative and SRH recombination | [solar](https://markblundeberg.github.io/driftlet/demos/solar.html) [organic (excitons)](https://markblundeberg.github.io/driftlet/demos/organic.html) and [perovskite (mobile ions)](https://markblundeberg.github.io/driftlet/demos/perovskite.html) demos, [`ionmonger`](test/ionmonger.test.js), [`reactions`](test/reactions.test.js) and [`generation`](test/generation.test.js) tests: generation is a reaction from a photon reservoir (uniform, or Beer–Lambert with `photogeneration()`), SRH as a rate law, in the bulk and at faces | `J_sc = qG(L_n + L_p + W)`; J_sc under Beer–Lambert against collection theory; Shockley J–V in the dark |
35
- | Electrochemistry, corrosion | Nernst–Planck, concentration polarization, limiting current, Butler–Volmer, Warburg, cyclic voltammetry, salt bridges and liquid junctions, mixed potentials, electrodes under a film | [second example](#an-electrochemical-cell), [cyclic voltammetry](https://markblundeberg.github.io/driftlet/demos/redox.html), [Daniell cell](https://markblundeberg.github.io/driftlet/demos/daniell.html), [saturation](https://markblundeberg.github.io/driftlet/demos/saturation.html), [impedance](https://markblundeberg.github.io/driftlet/demos/impedance.html) and [liquid-junction](https://markblundeberg.github.io/driftlet/demos/junction.html) demos, [`circuit`](test/circuit.test.js), [`kinetics`](test/kinetics.test.js), [`impedance`](test/impedance.test.js), [`junction`](test/junction.test.js), [`ports`](test/ports.test.js) tests | `i_lim·tanh(V/4V_T)`, Butler–Volmer closed form, finite-length Warburg; junction potentials against Henderson, Planck, JPCalc and LJPcalc; Wagner–Traud |
36
- | Batteries, intercalation | OCV, insertion hosts, chemical diffusion | [insertion demo](https://markblundeberg.github.io/driftlet/demos/insertion.html), [`statistics`](test/statistics.test.js) test | composition vs OCV, π²D/4L² relaxation |
37
- | Double layers, colloids | Poisson–Boltzmann, Gouy–Chapman–Stern, Debye screening, crowding (Bikerman) | [double-layer demo](https://markblundeberg.github.io/driftlet/demos/double-layer.html), [`equilibrium`](test/equilibrium.test.js) test | Gouy–Chapman charge and profile, Kilic–Bazant–Ajdari |
38
- | Membranes, desalination | Donnan, ion exchange, liquid junctions, water dissociation | [membrane demo](https://markblundeberg.github.io/driftlet/demos/membrane.html), [`equilibrium`](test/equilibrium.test.js), [`neutral`](test/neutral.test.js) tests | Donnan partition, Planck EMF |
39
- | Solid-state ionics | mixed ionic–electronic conduction, defect chemistry, mobile ions | [`statistics`](test/statistics.test.js), [`reactions`](test/reactions.test.js) tests | mass action from standard potentials |
40
- | Biophysics, nanofluidics | charged nanochannels and pores, resting potentials, Goldman–Hodgkin–Katz, Nernst, Donnan, pumps and leaks | [charged-nanochannel](https://markblundeberg.github.io/driftlet/demos/channel.html), [resting-potential](https://markblundeberg.github.io/driftlet/demos/cell.html) and [liquid-junction](https://markblundeberg.github.io/driftlet/demos/junction.html) (a patch pipette's) demos, [`pore`](test/pore.test.js) test; a membrane as a face: a capacitor with ion permeabilities, and the Na⁺/K⁺ pump as a reaction on it ([`membrane`](test/membrane.test.js) test) | GHK potential and current–voltage curve; a resolved lipid layer; Mullins–Noda; the pump's static head; Donnan |
41
-
42
- The tests are worked setups, each with its analytic check, so they double as recipes. The
43
- [validation table](#validation) lists them all.
44
-
45
34
  ## Install
46
35
 
47
36
  ```sh
@@ -55,7 +44,7 @@ import { bandDiagram } from 'driftlet/plot'; // a level diagram as an SVG str
55
44
  ```
56
45
 
57
46
  Or straight from a CDN in a page, pinned to a version:
58
- `https://cdn.jsdelivr.net/npm/driftlet@0.9.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
47
+ `https://cdn.jsdelivr.net/npm/driftlet@0.11.4/src/index.js` (and `src/kit.js`, `src/plot.js`).
59
48
  TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
60
49
  [`llms.txt`](llms.txt).
61
50
 
@@ -143,9 +132,9 @@ them locally, serve the repository root (e.g. `python3 -m http.server`) and open
143
132
  | | | |
144
133
  |---|---|---|
145
134
  | [![pn junction](demos/screenshots/pn.png)](https://markblundeberg.github.io/driftlet/demos/pn.html) | [![solar cell](demos/screenshots/solar.png)](https://markblundeberg.github.io/driftlet/demos/solar.html) | [![MOS capacitor](demos/screenshots/mos.png)](https://markblundeberg.github.io/driftlet/demos/mos.html) |
146
- | **pn junction**: quasi-Fermi levels under bias; I–V against Shockley | **Solar cell**: J<sub>sc</sub> and V<sub>oc</sub> against collection theory | **MOS capacitor**: band bending; C–V against ideal theory |
135
+ | **pn junction**: quasi-Fermi levels under bias; I–V against Shockley | **Solar cell**: J<sub>sc</sub> and V<sub>oc</sub> against collection theory | **MOS capacitor**: band bending; C–V at any frequency, low to high |
147
136
  | [![cyclic voltammetry](demos/screenshots/redox.png)](https://markblundeberg.github.io/driftlet/demos/redox.html) | [![Daniell cell](demos/screenshots/daniell.png)](https://markblundeberg.github.io/driftlet/demos/daniell.html) | [![saturation](demos/screenshots/saturation.png)](https://markblundeberg.github.io/driftlet/demos/saturation.html) |
148
- | **Cyclic voltammetry**: a Fermi level against a redox level | **Daniell cell**: a real salt bridge, leaking, ion by ion | **Saturation**: Ag \| AgNO₃ \| Ag under a square wave, up to its limiting current |
137
+ | **Cyclic voltammetry**: a Fermi level against a redox level | **Daniell cell**: a real salt bridge, leaking, ion by ion | **Saturation**: Ag \| AgNO₃ \| Ag up to its limiting current, and a thin-film transistor pinching off, against the charge-sheet model |
149
138
  | [![impedance](demos/screenshots/impedance.png)](https://markblundeberg.github.io/driftlet/demos/impedance.html) | [![ion-exchange membrane](demos/screenshots/membrane.png)](https://markblundeberg.github.io/driftlet/demos/membrane.html) | [![double layer](demos/screenshots/double-layer.png)](https://markblundeberg.github.io/driftlet/demos/double-layer.html) |
150
139
  | **Impedance**: the Warburg arc, and the cell's small-signal response inside | **Ion-exchange membrane**: Donnan steps, ion by ion, against TMS theory | **Double layer**: dilute vs crowded ions, against closed forms |
151
140
  | [![insertion host](demos/screenshots/insertion.png)](https://markblundeberg.github.io/driftlet/demos/insertion.html) | [![organic solar cell](demos/screenshots/organic.png)](https://markblundeberg.github.io/driftlet/demos/organic.html) | [![perovskite hysteresis](demos/screenshots/perovskite.png)](https://markblundeberg.github.io/driftlet/demos/perovskite.html) |
@@ -153,6 +142,25 @@ them locally, serve the repository root (e.g. `python3 -m http.server`) and open
153
142
  | [![charged nanochannel](demos/screenshots/channel.png)](https://markblundeberg.github.io/driftlet/demos/channel.html) | [![resting potential](demos/screenshots/cell.png)](https://markblundeberg.github.io/driftlet/demos/cell.html) | [![liquid junctions](demos/screenshots/junction.png)](https://markblundeberg.github.io/driftlet/demos/junction.html) |
154
143
  | **Charged nanochannel**: selectivity, overlapping Donnan layers and rectification, against Teorell–Meyer–Sievers | **Resting potential**: leaks and the Na⁺/K⁺ pump, against Mullins–Noda; the run-down to Donnan | **Liquid junctions**: a patch pipette's against Henderson, Planck and LJPcalc; a charged frit |
155
144
 
145
+ ## Is it for your problem?
146
+
147
+ The same equations go by different names in different fields. If your problem is one-dimensional
148
+ (planar, or radial: spheres and cylinders, or any cross-section varying along x), it's probably here:
149
+
150
+ | If you work on | you may call it | start from | checked against |
151
+ |---|---|---|---|
152
+ | Semiconductor devices | drift–diffusion, van Roosbroeck, quasi-Fermi levels, Scharfetter–Gummel; pn, Schottky, MOS, heterojunctions; thin-film transistors (gradual channel, in the [saturation demo](https://markblundeberg.github.io/driftlet/demos/saturation.html#transistor)) | [first example](#a-semiconductor-junction), [pn](https://markblundeberg.github.io/driftlet/demos/pn.html) and [MOS](https://markblundeberg.github.io/driftlet/demos/mos.html) demos, [`contacts`](test/contacts.test.js), [`metal`](test/metal.test.js) tests | exact built-in potential, Shockley J–V, depletion charge, MOS C–V |
153
+ | Solar cells | photogeneration, radiative and SRH recombination | [solar](https://markblundeberg.github.io/driftlet/demos/solar.html) [organic (excitons)](https://markblundeberg.github.io/driftlet/demos/organic.html) and [perovskite (mobile ions)](https://markblundeberg.github.io/driftlet/demos/perovskite.html) demos, [`ionmonger`](test/ionmonger.test.js), [`reactions`](test/reactions.test.js) and [`generation`](test/generation.test.js) tests: generation is a reaction from a photon reservoir (uniform, or Beer–Lambert with `photogeneration()`), SRH as a rate law, in the bulk and at faces | `J_sc = qG(L_n + L_p + W)`; J_sc under Beer–Lambert against collection theory; Shockley J–V in the dark |
154
+ | Electrochemistry, corrosion | Nernst–Planck, concentration polarization, limiting current, Butler–Volmer, Warburg, cyclic voltammetry, salt bridges and liquid junctions, mixed potentials, electrodes under a film | [second example](#an-electrochemical-cell), [cyclic voltammetry](https://markblundeberg.github.io/driftlet/demos/redox.html), [Daniell cell](https://markblundeberg.github.io/driftlet/demos/daniell.html), [saturation](https://markblundeberg.github.io/driftlet/demos/saturation.html), [impedance](https://markblundeberg.github.io/driftlet/demos/impedance.html) and [liquid-junction](https://markblundeberg.github.io/driftlet/demos/junction.html) demos, [`circuit`](test/circuit.test.js), [`kinetics`](test/kinetics.test.js), [`impedance`](test/impedance.test.js), [`junction`](test/junction.test.js), [`ports`](test/ports.test.js) tests | `i_lim·tanh(V/4V_T)`, Butler–Volmer closed form, finite-length Warburg; junction potentials against Henderson, Planck, JPCalc and LJPcalc; Wagner–Traud |
155
+ | Batteries, intercalation | OCV, insertion hosts, chemical diffusion | [insertion demo](https://markblundeberg.github.io/driftlet/demos/insertion.html), [`statistics`](test/statistics.test.js) test | composition vs OCV, π²D/4L² relaxation |
156
+ | Double layers, colloids | Poisson–Boltzmann, Gouy–Chapman–Stern, Debye screening, crowding (Bikerman) | [double-layer demo](https://markblundeberg.github.io/driftlet/demos/double-layer.html), [`equilibrium`](test/equilibrium.test.js) test | Gouy–Chapman charge and profile, Kilic–Bazant–Ajdari |
157
+ | Membranes, desalination | Donnan, ion exchange, liquid junctions, water dissociation | [membrane demo](https://markblundeberg.github.io/driftlet/demos/membrane.html), [`equilibrium`](test/equilibrium.test.js), [`neutral`](test/neutral.test.js) tests | Donnan partition, Planck EMF |
158
+ | Solid-state ionics | mixed ionic–electronic conduction, defect chemistry, mobile ions | [`statistics`](test/statistics.test.js), [`reactions`](test/reactions.test.js) tests | mass action from standard potentials |
159
+ | Biophysics, nanofluidics | charged nanochannels and pores, resting potentials, Goldman–Hodgkin–Katz, Nernst, Donnan, pumps and leaks | [charged-nanochannel](https://markblundeberg.github.io/driftlet/demos/channel.html), [resting-potential](https://markblundeberg.github.io/driftlet/demos/cell.html) and [liquid-junction](https://markblundeberg.github.io/driftlet/demos/junction.html) (a patch pipette's) demos, [`pore`](test/pore.test.js) test; a membrane as a face: a capacitor with ion permeabilities, and the Na⁺/K⁺ pump as a reaction on it ([`membrane`](test/membrane.test.js) test) | GHK potential and current–voltage curve; a resolved lipid layer; Mullins–Noda; the pump's static head; Donnan |
160
+
161
+ The tests are worked setups, each with its analytic check, so they double as recipes. The
162
+ [validation table](#validation) lists them all.
163
+
156
164
  ## How to think about it
157
165
 
158
166
  driftlet insists on thermodynamically honest concepts ([conventions](docs/conventions.md)):
@@ -260,12 +268,12 @@ Every physics feature is tested against analytic results (`npm test`, node's bui
260
268
  | Time integration | BE first order, BDF2 second order; adaptive error control | ratios 2, 4 |
261
269
  | Advection and eddy mixing | exact convection–diffusion profile; `D + D_mix`; junction EMF unchanged by mixing | 1e-12; 1e-12; 1e-5 |
262
270
  | Metal regions | ohmic; Schottky face and MOS gate equal their contact forms; Ag \| AgNO₃ \| Ag with metal electrodes; bipolar electrode at V/2 | 1e-12; 1e-9; 1e-9; 1e-9 |
263
- | Internal ports | transmission line σV tanh(L/λ)/λ (O(h²)); held level; MOS low-frequency C–V with a grounded channel | 1e-4; 1e-9; 2e-3 |
271
+ | Internal ports | transmission line σV tanh(L/λ)/λ (O(h²)); held level; MOS low-frequency C–V with a grounded channel, and without one the same equilibrium, and a gate step filling the inversion layer to it | 1e-4; 1e-9; 2e-3; 1e-6; 1e-3 |
264
272
  | Cross-sections (spherical, cylindrical, any A(x)) | steady diffusion to a sphere and a cylinder (exact on any grid); Cottrell with the spherical term; uptake by a sphere filling from its surface; Debye–Hückel around a charged sphere, potential and charge | 1e-10; 3e-4; 2e-4; 1e-3, 3e-4 |
265
273
  | A capacitance through a port | a thin-film transistor against the charge-sheet model (below threshold to saturation, second order); de Levie impedance; gate impedance against dQ/dV | 5e-4; 1e-3; 1e-6 |
266
274
  | Electrode surfaces (coverage) | Langmuir isotherm against potential; the charge to fill a surface; the active–passive curve, blocking exactly (1 − θ) | 1e-9; 1e-3; 1e-9 |
267
275
  | Electrodes spread through a port | transmission line with the reaction's linear kinetics as the conductance (O(h²)), rates along it; Wagner–Traud mixed potential of two Butler–Volmer couples, floating | 1e-4, 2e-3; 1e-6 V |
268
- | Impedance | Macdonald blocking-electrode spectrum, 100 Hz–1 GHz; finite-length Warburg (Ag \| AgNO₃ \| Ag); DC limit = differential resistance | 3e-4; 3e-5; 1e-6 |
276
+ | Impedance | Macdonald blocking-electrode spectrum, 100 Hz–1 GHz; finite-length Warburg (Ag \| AgNO₃ \| Ag); DC limit = differential resistance; a p⁺n⁺ junction without recombination, Re Y → dI/dV and C = dQ/dV; a redox electrode against a bath at its open circuit, Re Y → dI/dV | 3e-4; 3e-5; 1e-6; 1e-4, 1e-6; 1e-4 |
269
277
  | Conservation | per step, and against time-integrated contact fluxes | 1e-11 relative |
270
278
  | Strictly neutral limit (ε = 0) | Planck EMF; polarization with no overlimiting; Donnan at neutral faces | 1e-5; 1e-4; 1e-9 |
271
279
  | Fermi–Dirac statistics | `𝓕_{±1/2}` vs quadrature; degenerate bulk; accumulation charge via `𝓕_{3/2}` | 1e-13; 1e-12; 1e-3 |
@@ -284,17 +292,17 @@ checked in CI; times are from a desktop Ryzen 7600X in Node 22:
284
292
  | Task | Time | Factorisations |
285
293
  |---|---|---|
286
294
  | Linear solve, 300 nodes × 7 unknowns (factor + solve) | 0.4 ms | 1 |
287
- | pn diode (264 nodes): cold equilibrium | 2 ms | 9 |
288
- | pn diode: I–V sweep 0 → 0.6 V, 31 points | 31 ms | 137 |
289
- | pn diode: warm jump from +0.4 V to −1 V | 7 ms | 51 |
290
- | pn diode: adaptive transient, 0 → 0.5 V, 100 ns | 150 ms | 1026 |
291
- | pn diode: impedance at 20 frequencies | 8 ms | 21 |
292
- | MOS with a metal gate: C–V sweep, 26 points | 11 ms | 126 |
293
- | n-Si \| KCl (4 species, 3 unknowns per node): Mott–Schottky sweep, 31 points | 29 ms | 156 |
294
- | n-Si \| KCl: impedance at 20 frequencies | 10 ms | 21 |
295
+ | pn diode (264 nodes): cold equilibrium | 3 ms | 9 |
296
+ | pn diode: I–V sweep 0 → 0.6 V, 31 points | 29 ms | 137 |
297
+ | pn diode: warm jump from +0.4 V to −1 V | 8 ms | 51 |
298
+ | pn diode: adaptive transient, 0 → 0.5 V, 100 ns | 165 ms | 926 |
299
+ | pn diode: impedance at 20 frequencies | 14 ms | 21 |
300
+ | MOS with a metal gate: C–V sweep, 26 points | 9 ms | 126 |
301
+ | n-Si \| KCl (4 species, 3 unknowns per node): Mott–Schottky sweep, 31 points | 36 ms | 156 |
302
+ | n-Si \| KCl: impedance at 20 frequencies | 16 ms | 21 |
295
303
  | Ag \| AgNO₃ \| Ag with double layers: sweep 0 → 0.1 V, 21 points | 12 ms | 81 |
296
- | Ag \| AgNO₃ \| Ag, neutral: adaptive transient over 1 s | 29 ms | 287 |
297
- | Silver electrodes with a bipolar plate: sweep 0 → 1 V, 11 points | 27 ms | 63 |
304
+ | Ag \| AgNO₃ \| Ag, neutral: adaptive transient over 1 s | 24 ms | 186 |
305
+ | Silver electrodes with a bipolar plate: sweep 0 → 1 V, 11 points | 25 ms | 54 |
298
306
 
299
307
  The library has no dependencies and does no DOM access, so it runs in a Web Worker. (A device
300
308
  definition is plain data and can be posted to a worker. Devices using custom-function
@@ -307,12 +315,14 @@ No dependencies. Tests use node's built-in runner (Node ≥ 22):
307
315
  ```sh
308
316
  npm test # the validation suite
309
317
  npm run bench # typical workloads against bench/baseline.json (--save to update it)
318
+ npm run stress # random devices, every result checked (--n 500 for more; see docs/reliability.md)
319
+ npm run hard # the corpus of once-hard cases against bench/hard.json
310
320
  npm run types # TypeScript declarations from the JSDoc, into types/ (fetches TypeScript via npx)
311
321
  npm run smoke # every demo in headless Chrome, every control to its extremes (CHROME=… to choose)
312
322
  npm run shots # retake the demos' screenshots, each caught at a set state (tools/shots.js)
313
323
  ```
314
324
 
315
- Design notes and plans: [numerics](docs/numerics.md), [conventions](docs/conventions.md),
325
+ Design notes and plans: [reliability](docs/reliability.md), [numerics](docs/numerics.md), [conventions](docs/conventions.md),
316
326
  [statistics](docs/statistics.md), [the kit](docs/kit.md), [reading level diagrams](docs/visualization.md),
317
327
  [data](docs/data.md), [roadmap](ROADMAP.md).
318
328
 
package/docs/device.md CHANGED
@@ -421,65 +421,74 @@ with the contact behind it holding the conductor's electrons (see
421
421
  ## Internal ports
422
422
 
423
423
  A port is an outside phase with known levels, like a contact's, attached to a window of nodes
424
- inside one region instead of at an end. It's the 1D stand-in for whatever feeds or drains a
425
- species sideways: source and drain grounding a MOS channel, salt injected mid-solution, a
426
- reference electrode's reservoir.
424
+ inside one region instead of at an end: the 1D stand-in for whatever feeds, drains or charges the
425
+ region sideways. It's a [terminal](#terminals) like a contact (held at `V`, behind `R`, or driven
426
+ by `I`; a reference electrode is a port at `I: 0`), and in its window it does any of these, in
427
+ any combination:
428
+
429
+ | Field | What it adds | For |
430
+ |---|---|---|
431
+ | `species` | [levels held or exchanged](#levels-held-or-exchanged), per volume | source and drain grounding a channel, salt injected mid-solution, O₂ from the air |
432
+ | `reactions`, `area` | an [electrode spread through the window](#electrodes-spread-through-a-window) | a thin film on a metal, a crevice, a porous electrode |
433
+ | `surface` | [species on that electrode's sites](#an-electrodes-surface-adsorbates-and-passive-films) | adsorbates, a passive film |
434
+ | `capacitance`, `area` | [charge held across a capacitance](#a-capacitance-through-a-window) | a gate along a channel, a double layer |
427
435
 
428
436
  ```js nocheck
429
437
  ports: [{
430
438
  name: 'channel',
431
439
  region: 'Si', // name or index
432
440
  from: 495e-9, to: 500e-9, // window, m from the region's left end (default: the whole region)
433
- V: 0, // the port's terminal voltage (or I, or V and R: see terminals)
434
- terminal: 'e-',
435
- species: {
436
- 'e-': 'equilibrium', // μ̄ held at V_i = V + offset throughout the window
437
- 'Na+': { type: 'conductance', G: 1e9, offset: 0.2 }, // source G (V_out − V_i)/(zF) per volume, G in S/m³
438
- O2: { type: 'exchange', k: 1e-3, mu: -2e3 }, // neutral: source k (μ_out − μ)/RT per volume, k in mol/(m³·s)
439
- },
441
+ V: 0, // or I, or V and R (see terminals)
442
+ terminal: 'e-', // the species whose level V is
443
+ species: { 'e-': 'equilibrium' },
440
444
  }]
441
445
  ```
442
446
 
443
- Links and offsets are those of contacts, per volume instead of per area: 0 by default only for
444
- the terminal species, and an absolute `mu` for neutral species. A port is a terminal like a
445
- contact: held at a voltage, behind a resistance, or driven by a current (a reference electrode
446
- is a port at `I: 0`). Each solution reports `ports[k]`, with `{ name, V, flux, current }`: what
447
- the port brings into the device.
448
-
449
- A held (`'equilibrium'`) level leaves the device's two end nodes to their contacts. A port that
450
- exchanges only neutral species carries no current, so its voltage means nothing: give it no drive.
451
-
452
- An exchange link's source is linear in $`\mu_{out} - \mu`$, so it grows without bound as the
453
- species runs out ($`\ln c`$). A supply limited by diffusion through a film above the window, such as O₂
454
- reaching a thin layer of water from the air, is linear in $`c`$ instead: $`k(c_{sat} - c)`$. For that,
455
- use a [bulk reaction](#bulk-reactions) with the outside phase as a fixed participant, e.g.
456
- `{ equation: 'Air = O2', fixed: { Air: muSat }, kf: { water: profile } }`. Its rate is
457
- $`k_f (1 - c/c_{sat})`$, and a profile gives $`k_f`$ its spatial variation (a film of thickness $`h(x)`$:
458
- $`D c_{sat}/h^2`$).
459
-
460
- A port driven by a current (`I`, or a waveform) floats to whatever voltage delivers it: with a
461
- conductance `G` per volume over a window of width $`w`$, it sits about $`I/(G w)`$ above the
462
- window's level for that species. So `G` decides how the current is shared across the window, not
463
- how much enters. A small `G`, with the port volts above the window, spreads it evenly, like a
464
- current source; a large one pins the window near the port's level, and the current enters
465
- wherever the species is drawn away fastest. At `I: 0` the port still ties that species' level
466
- together across its window (a large `G` shorts the window for it), so keep such windows narrow.
467
-
468
447
  On a [conductor region](#materials), a port is a wire to the whole conductor: it takes no window,
469
448
  only the conductor's carrier, and a conductance link's `G` is per area (S/m², a resistance
470
449
  $`R \cdot A`$ to the port's voltage as $`G = 1/(R \cdot A)`$). That's how a floating electrode is tied
471
450
  to ground through a resistor.
472
451
 
473
- For example, in a 1D MOS capacitor without generation, inversion electrons can only arrive by
474
- minority-carrier diffusion from the back contact, which can take weeks. The inversion layer is
475
- then effectively floating, and its Fermi level is undetermined to within round-off, so no
476
- steady state can be computed there. A port holding the electrons beside the oxide at the
477
- channel's potential anchors it, as source and drain would, and the device then shows the
478
- low-frequency C–V.
452
+ ### Levels held or exchanged
453
+
454
+ ```js nocheck
455
+ species: {
456
+ 'e-': 'equilibrium', // μ̄ held at V_i = V + offset throughout the window
457
+ 'Na+': { type: 'conductance', G: 1e9, offset: 0.2 }, // source G (V_out − V_i)/(zF) per volume, G in S/m³
458
+ O2: { type: 'exchange', k: 1e-3, mu: -2e3 }, // neutral: source k (μ_out − μ)/RT per volume, k in mol/(m³·s)
459
+ }
460
+ ```
461
+
462
+ Links and offsets are those of contacts, per volume instead of per area: 0 by default only for
463
+ the terminal species, and an absolute `mu` for neutral species. A held (`'equilibrium'`) level
464
+ leaves the device's two end nodes to their contacts. A port that exchanges only neutral species
465
+ carries no current, so its voltage means nothing: give it no drive.
466
+
467
+ A port driven by a current floats to whatever voltage delivers it: with a conductance `G` per
468
+ volume over a window of width $`w`$, about $`I/(G w)`$ above the window's level for that species. So
469
+ `G` decides how the current is shared across the window, not how much enters. A small `G` spreads
470
+ it evenly, like a current source; a large one pins the window near the port's level, and the
471
+ current enters wherever the species is drawn away fastest. At `I: 0` the port still ties that
472
+ species' level together across its window (a large `G` shorts the window for it), so keep such
473
+ windows narrow.
474
+
475
+ An exchange link's source is linear in $`\mu_{out} - \mu`$, so it grows without bound as the
476
+ species runs out ($`\ln c`$). A supply limited by diffusion through a film above the window, such
477
+ as O₂ reaching a thin layer of water from the air, is linear in $`c`$ instead: $`k(c_{sat} - c)`$. For
478
+ that, use a [bulk reaction](#bulk-reactions) with the outside phase as a fixed participant,
479
+ `{ equation: 'Air = O2', fixed: { Air: muSat }, kf: { water: profile } }`, whose rate is
480
+ $`k_f (1 - c/c_{sat})`$; a profile gives $`k_f`$ its spatial variation ($`D c_{sat}/h^2`$ under a film
481
+ of thickness $`h(x)`$).
482
+
483
+ A held level can also anchor what nothing else holds. In a 1D MOS capacitor without generation,
484
+ inversion electrons can only arrive by minority-carrier diffusion from the back contact (weeks),
485
+ so the inversion layer's Fermi level is undetermined to within round-off and no steady state can
486
+ be computed. A port holding the electrons beside the oxide, as source and drain would, anchors it,
487
+ and the device shows the low-frequency C–V.
479
488
 
480
489
  ### Electrodes spread through a window
481
490
 
482
- A port can also hold an electrode: a metal whose carrier sits at the port's level, reacting all
491
+ A port can hold an electrode: a metal whose carrier sits at the port's level, reacting all
483
492
  through the window rather than at one face. That's the floor under a thin film of electrolyte, the
484
493
  walls of a crevice or a pit, or the matrix of a porous electrode, wherever the metal conducts well
485
494
  enough to be one level. Give the port its `reactions` (Butler–Volmer, as at a face), the carrier as
@@ -499,28 +508,26 @@ ports: [{
499
508
 
500
509
  Each node's rate per area is the face's law, with the region's species at that node and the
501
510
  carrier at activity 1 and level $`V`$. Times the area per volume, it makes and consumes the
502
- region's species there, and the electrons it takes or gives are the port's current. The port is
503
- a terminal like any other. Held at a voltage, it's a potentiostat. At `I: 0` (or with nothing
504
- else to carry current), the reactions settle at the mixed potential, the corrosion potential where
505
- anodic and cathodic currents cancel over the window, while each node can be a net anode or a net
506
- cathode. A floating one starts there, for the start's composition. Each solution reports the
507
- window's nodes and every reaction's rate at each: `ports[k].x[j]` (m) and `ports[k].area[j]`
508
- (m²/m³) for the window's node j, and `ports[k].rates[q][j]` for reaction q there (mol/(m²·s) of
509
- electrode, forward), so reaction first, then node. They're typed arrays (`Float64Array`), so
510
- `Array.from` them before mapping to anything but numbers. Per volume of the window it's
511
+ region's species there, and the electrons it takes or gives are the port's current. Held at a
512
+ voltage, the port is a potentiostat. At `I: 0` (or with nothing else to carry current), the
513
+ reactions settle at the mixed potential, the corrosion potential where anodic and cathodic
514
+ currents cancel over the window, while each node can be a net anode or a net cathode; a floating
515
+ one starts there, for the start's composition (with a double layer, it starts uncharged and
516
+ charges toward it). An electrode that's the device's only path for current is at `I: 0` whatever
517
+ its drive: hold it at `V: 0`, which also fixes the level the water's φ is read against. Per volume of the window a rate is
511
518
  `rates[q][j] * area[j]`, and per area of a film's floor (thickness h, `area` 1/h) the rate itself.
512
519
 
513
- Validated against the transmission line (linear kinetics along a bar, test/ports.test.js) and the
514
- Wagner–Traud mixed potential of two Butler–Volmer couples. In 1D, a film on a metal is a slice
515
- along the metal: the film's thickness enters through `area`, and through a bulk reaction for
516
- anything that reaches the film from above (O₂ from the air, see the exchange link above). Where
517
- the film's thickness varies, give the device its [cross-section](#geometry) too, so that ions
520
+ In 1D, a film on a metal is a slice along the metal: the film's thickness enters through `area`,
521
+ and through a bulk reaction for anything that reaches the film from above (O₂ from the air, see
522
+ above). Where the thickness varies, give the device its [cross-section](#geometry) too, so that ions
518
523
  travelling along the film squeeze through where it thins: for a drop on a metal, seen from its
519
- centre, $`A = 2\pi r\,h(r)`$ with `area` $`1/h(r)`$.
524
+ centre, $`A = 2\pi r\,h(r)`$ with `area` $`1/h(r)`$. The region must be strictly neutral
525
+ ($`\varepsilon = 0`$), as in porous-electrode theory: each spot's double layer is below the grid, so
526
+ the reactions can't leave the solution charged (give the port a
527
+ [capacitance](#a-capacitance-through-a-window) for the double layer's charge).
520
528
 
521
- The region must be strictly neutral ($`\varepsilon = 0`$), as in porous-electrode theory: each spot's
522
- double layer, the metal's countercharge, is below the grid, so the reactions can't leave the
523
- solution charged.
529
+ Validated against the transmission line (linear kinetics along a bar, test/ports.test.js) and the
530
+ Wagner–Traud mixed potential of two Butler–Volmer couples.
524
531
 
525
532
  ### An electrode's surface: adsorbates and passive films
526
533
 
@@ -538,7 +545,7 @@ ports: [{
538
545
  surface: { 'Fe(OH)2': { mu0: -490e3, capacity: 2e-5, theta0: 1e-6 } }, // theta0: the start
539
546
  reactions: [
540
547
  { equation: 'Fe2+ + 2 e- = Fe(s)', fixed: { 'Fe(s)': 0 }, k0: 5e-6, alpha: 0.5, bare: true }, // dissolves where bare
541
- { equation: 'Fe(s) + 2 OH- = Fe(OH)2 + 2 e-', fixed: { 'Fe(s)': 0 }, k0: 1e-6, alpha: 0.5, bare: true }, // the film forms
548
+ { equation: 'Fe(s) + 2 OH- = Fe(OH)2 + 2 e-', fixed: { 'Fe(s)': 0 }, k0: 1e-4, alpha: 0.5, bare: true }, // the film forms (in minutes)
542
549
  { equation: 'O2 + 2 H2O + 4 e- = 4 OH-', fixed: { H2O: -237.13e3 }, k0: 3e-9, alpha: 0.125 }, // on film or not
543
550
  ],
544
551
  }]
@@ -546,35 +553,39 @@ ports: [{
546
553
 
547
554
  A film forms where the potential and the pH favour it, through its own thermodynamics, and blocks
548
555
  what needs bare metal: the dissolution current climbs with potential, peaks, and falls as the film
549
- covers the metal, the active–passive curve. Each node's surface stores
550
- $`\Gamma\,d\theta/dt`$ per area of electrode, so filling it passes a current, and a solution reports
551
- `ports[k].coverage[name][j]`. It's a monolayer picture: a real passive oxide is nanometres thick,
552
- grows, and breaks down under chloride, none of which a coverage has.
553
-
554
- Two cautions. Keep a film's stability within reason: blocking the bare fraction to 1e-3–1e-6 is
555
- already a passive metal, and far below that ($`\theta_0 \lesssim 10^{-10}`$) the coverage's η barely
556
- affects anything, so Newton struggles with it. And an electrode port has no double-layer
557
- capacitance (its solution is strictly neutral), so its potential follows its kinetics instantly.
558
- With a passivating film, current against potential has the active–passive peak, and when the
559
- last active patch covers over, the potential where no current flows can vanish from the active
560
- branch and must jump to the passive one. A real electrode makes that jump quickly but
561
- continuously, through its double layer; here it's a discontinuity, and a transient stops there.
562
- Give the port a [capacitance](#a-capacitance-through-a-window-a-gate-along-a-channel-a-double-layer) and the jump becomes a quick, continuous swing.
556
+ covers the metal, the active–passive curve. Each node's surface stores $`\Gamma\,d\theta/dt`$ per area
557
+ of electrode, so filling it passes a current, and what passes through a surface is conserved with
558
+ it. It's a monolayer picture: a real passive oxide is nanometres thick, grows, and breaks down
559
+ under chloride, none of which a coverage has.
560
+
561
+ Cautions:
562
+ - **Keep a film's stability within reason.** Blocking the bare fraction to 1e-3–1e-6 is already
563
+ a passive metal; far below that ($`\theta_0 \lesssim 10^{-10}`$) the coverage's η barely affects
564
+ anything and Newton struggles with it (a solution warns).
565
+ - **A film's μ° is a fitted number.** A monolayer given a bulk hydroxide's ΔG°f passivates iron far
566
+ into neutral water, so choose it to put the passivation where the metal's chemistry does.
567
+ - **Without a double layer, passivation can jump.** With no capacitance the potential follows the
568
+ kinetics instantly, and when the last active patch covers over, the potential where no current
569
+ flows can vanish from the active branch: a discontinuity, where a transient stops. Give the port
570
+ a [capacitance](#a-capacitance-through-a-window) and it's a quick, continuous swing, as on a real
571
+ electrode.
563
572
 
564
573
  Validated in test/coverage.test.js: the Langmuir isotherm against the electrode's potential (to
565
- 1e-9), the charge to fill the surface, $`F\,\Gamma\,\Delta\theta`$ per area of electrode, and the
566
- active–passive curve with blocking exactly $`(1-\theta)`$ times Butler–Volmer.
574
+ 1e-9), filling against its exponential and its charge $`F\,\Gamma\,\Delta\theta`$ per area of electrode,
575
+ the active–passive curve with blocking exactly $`(1-\theta)`$ times Butler–Volmer, and a steady state
576
+ through a surface intermediate where the transient ends.
567
577
 
568
- ### A capacitance through a window: a gate along a channel, a double layer
578
+ ### A capacitance through a window
569
579
 
570
580
  A port can hold charge across a capacitance spread through its window: per area of electrode,
571
581
  $`\sigma = C\,(V - \mathtt{zeroCharge} - \phi)`$ on the port's side (C in F/m²), with `area` per volume
572
582
  as for reactions, and the window holds the opposite. Each node's charge balance gains $`a\sigma`$:
573
- Gauss's law averaged across the section,
574
- $`-\partial_x(\varepsilon\,\partial_x\phi) = \rho + a\sigma`$, and at $`\varepsilon = 0`$ neutrality
575
- $`\rho + a\sigma = 0`$, the mobile species rearranging to supply it (counter-ions in, co-ions out).
576
- The port passes the charging current, $`d(\int a\sigma\,dV)/dt`$, none in a steady state. Solutions
577
- report `ports[k].sigma` at each node of the window and the total `ports[k].charge`.
583
+ Gauss's law averaged across the section, $`-\partial_x(\varepsilon\,\partial_x\phi) = \rho + a\sigma`$,
584
+ and at $`\varepsilon = 0`$ neutrality $`\rho + a\sigma = 0`$, the mobile species rearranging to supply
585
+ it (counter-ions in, co-ions out). The port passes the charging current, $`d(\int a\sigma\,dV)/dt`$,
586
+ none in a steady state. Capacitances on overlapping windows add (a channel's top and bottom
587
+ gates). A capacitance alone, driven by a current, only charges: it starts uncharged, `advance()`
588
+ follows it, and `solve()` refuses (there's no steady state, as for a gate contact driven by one).
578
589
 
579
590
  ```js nocheck
580
591
  // a thin-film transistor: the gate along a 10 nm channel through 1 mF/m² (35 nm of SiO₂)
@@ -583,15 +594,23 @@ ports: [{ name: 'gate', region: 'channel', V: 1.5, area: 1 / 10e-9, capacitance:
583
594
  ports: [{ name: 'iron', region: 'film', terminal: 'e-', V: 0, area: 1e4, capacitance: { C: 0.2, zeroCharge: -0.4 }, reactions: [/* … */] }]
584
595
  ```
585
596
 
597
+ A volumetric capacitance $`C^*`$ (F/m³, an organic electrochemical transistor's) goes in per
598
+ area of a nominal electrode: `area: 1 / t, capacitance: { C: Cstar * t }` for a film t thick
599
+ (`area: 1, C: Cstar` is the same physics, but reads to `describe()` like a unit slip). To read the
600
+ gate's voltage as the film's effective one, put the undisturbed film at φ = 0 (its carrier's
601
+ `cRef` at its doping, `mu0: 0`) and leave `zeroCharge` at 0. The gate charges the carriers
602
+ through their own chemical capacitance too, in series, so $`dQ/dV_G`$ comes out below $`C^*`$
603
+ (about 12% for holes at 1e20 cm⁻³ against 40 F/cm³).
604
+
586
605
  That's the gradual-channel approximation of a field-effect transistor, exact for a thin film
587
606
  with no body (a TFT, an organic or oxide transistor) while the channel is long against the gate
588
607
  dielectric, and failing where it pinches off (where the 2D field takes over). It's also the
589
- Bernards model of an organic electrochemical transistor (a volumetric capacitance), cable theory
590
- for an axon's membrane, and a porous electrode's double layer, which lets an electrode port's
591
- potential move continuously: without it, a passivating film that covers the last active patch
592
- forces a jump (see above). The gate's flat level pins the channel's standard level through C, as
593
- a blocked spectator's flat level does in an electrolyte, and the current saturates as the
594
- carriers run out at the drain, in both.
608
+ Bernards model of an organic electrochemical transistor (a volumetric capacitance; the
609
+ Bernards–Malliaras current is the charge-sheet formula below without its $`V_T`$ term, so they
610
+ part near pinch-off, where driftlet keeps the diffusion that formula drops), cable theory
611
+ for an axon's membrane, and a porous electrode's double layer. The gate's flat level pins the
612
+ channel's standard level through C, as a blocked spectator's flat level does in an electrolyte,
613
+ and the current saturates as the carriers run out at the drain, in both.
595
614
 
596
615
  Validated in test/capacitance.test.js: a thin-film transistor's current against the
597
616
  charge-sheet model, $`I/W = (\mu/L)[(\sigma_s^2 - \sigma_d^2)/(2C) + V_T(\sigma_s - \sigma_d)]`$, from
@@ -599,6 +618,23 @@ below threshold through saturation (second order in the grid); a porous electrod
599
618
  impedance $`\sqrt{r/y}\coth(L\sqrt{ry})`$, the double layer in series with the ions' chemical
600
619
  capacitance; and the gate's low-frequency impedance against $`dQ/dV_G`$ from steady states.
601
620
 
621
+ ### What a solution reports
622
+
623
+ Each solution's `ports[k]`, as the port has them:
624
+
625
+ | Field | Contents |
626
+ |---|---|
627
+ | `name`, `V` | the port's name and voltage |
628
+ | `flux[name]`, `current` | what it brings into the device: each species (mol/(m²·s)) and the current (A/m², the charging current included), totals through a [cross-section](#geometry) |
629
+ | `x[j]`, `area[j]` | with reactions or a capacitance: the window's node j (m) and the electrode's area per volume there (m²/m³) |
630
+ | `rates[q][j]` | reaction q's forward rate at node j, mol/(m²·s) of electrode (reaction first, then node) |
631
+ | `coverage[name][j]`, `bare[j]` | with a surface: each species' coverage θ, and the bare fraction θ₀ to full precision |
632
+ | `sigma[j]`, `charge` | with a capacitance: σ per area of electrode (C/m²), and the total the port's side holds |
633
+
634
+ The arrays are typed (`Float64Array`), so `Array.from` them before mapping to anything but
635
+ numbers. The kit's [`polarization()`](kit.md#polarization-curves-an-evans-diagram) gives each reaction's rate at a spot of the
636
+ electrode with its level moved, an Evans diagram.
637
+
602
638
  ## Terminals
603
639
 
604
640
  The two contacts and every port are the device's terminals, named `left`, `right` and by each
@@ -610,7 +646,7 @@ contacts: {
610
646
  right: { I: -2, ... }, // driven by a current into the device, A/m² (I: 0 is open circuit)
611
647
  },
612
648
  ports: [{ name: 'ref', I: 0, ... }, // a reference electrode: no current, its voltage read off
613
- { name: 'wire', V: 0, R: 1e-3, ... }], // a source V behind a series resistance R (Ω·m²)
649
+ { name: 'wire', V: 0, R: 1e-3, ... }], // a source V behind a series resistance R (Ω·m², or Ω with a geometry)
614
650
  ```
615
651
 
616
652
  - **`V`**: the terminal's voltage, the shift of its outside phase's ladder, as the voltage of
@@ -620,7 +656,9 @@ ports: [{ name: 'ref', I: 0, ... }, // a reference electrode: no current, its v
620
656
  the voltage floats and is solved for.
621
657
  - **`V` and `R`**: a source behind a resistance, $`I = (V - V_{\mathrm{terminal}})/R`$.
622
658
 
623
- At least one terminal must be held at a voltage, or the device's overall level floats. In
659
+ At least one terminal must be held at a voltage, or the device's overall level floats, and if
660
+ one is driven by a current, a held one must pass current (a closed end held at a voltage fixes
661
+ nothing). In
624
662
  steady state the terminal currents sum to zero. A contact can be driven by a current only if
625
663
  something passes it (a linked species, or a gate's displacement).
626
664
 
@@ -675,7 +713,8 @@ its middle, and nothing passes the end. Only an end may have $`A = 0`$. Flow (`v
675
713
  planar devices.
676
714
 
677
715
  With a cross-section, the solution's currents, contact and port fluxes, terminal currents and
678
- `charge` are totals (A, mol/s; per metre for cylinders), and so are `I` drives. Face laws,
716
+ `charge` are totals (A, mol/s; per metre for cylinders), and so are `I` drives, a series `R`
717
+ and the impedance (Ω, not Ω·m²). Face laws,
679
718
  rates, `interfaces[f].N` and `D`, and concentrations stay per area or per volume. Validated against
680
719
  steady diffusion to a sphere and a cylinder (exact), Cottrell's transient with the spherical term,
681
720
  uptake by a sphere filling from its surface, and Debye–Hückel screening around a charged sphere
@@ -687,6 +726,7 @@ uptake by a sphere filling from its surface, and Debye–Hückel screening aroun
687
726
  const dev = new Device(def);
688
727
  const sol = dev.solve(); // steady state (or equilibrium)
689
728
  dev.set({ contacts: { right: { V: 0.3 } } }); // deep-merged change; the state is kept as a warm start
729
+ dev.set({ ports: { gate: { V: 0.2 } } }); // ports patched by name (an array replaces them all)
690
730
  const sol2 = dev.solve();
691
731
  const tr = dev.step(1e-6); // one backward-Euler step (auto-subdivided if needed)
692
732
  const tr2 = dev.step(1e-6, { method: 'bdf2' }); // second-order BDF2 once a previous step exists
@@ -701,17 +741,23 @@ const now = dev.solution(); // snapshot of the current state
701
741
  amounts (blocked species, reactive moieties) are kept exactly. If a direct solve fails at a
702
742
  bias (a cold start far from equilibrium), it solves with both terminals level and ramps the
703
743
  right terminal's voltage to its target; with generation reactions (below), it ramps their
704
- rates up from nearly nothing.
744
+ rates up from nearly nothing. A terminal driven by a current (open circuit, say) is held at a
745
+ voltage instead, the voltage marched until the current crosses its target, and floated from
746
+ there. If 20 V either way brings no crossing (a current beyond the limiting one), the solve
747
+ fails with a warning giving the range of currents the held voltages passed.
705
748
  - `step(dt, { method })` advances the transient by dt seconds, halving internally where Newton
706
749
  needs it. `method` is `'be'` (backward Euler, the default) or `'bdf2'`.
707
750
  - `advance(tEnd, opts)` integrates adaptively to `tEnd` with variable-step BDF2, controlling
708
751
  the local error per step to `tol` (default 1e-3) in thermal units of every potential ($`\phi`$ and
709
- each $`\bar\mu/RT`$): roughly 0.1% in concentrations. It lands exactly on `tEnd`. Options: `tol`,
752
+ each $`\bar\mu/RT`$): roughly 0.1% in concentrations per step. That's local: over a long decay
753
+ the errors add up, and a small current at the end of one can be tens of percent off, so tighten
754
+ `tol` (1e-5, say) before fitting a time constant to a tail. It lands exactly on `tEnd`. Options: `tol`,
710
755
  `dt0` (first step), `dtMax`, `budgetMs` (return after this much wall time, with
711
756
  `done: false`), `maxSteps`, `method`. The step size carries over between calls, so an
712
757
  animation can call `advance(tNext, { budgetMs })` once per frame. The solution adds `done`,
713
758
  `rejected`, and a `trace` of terminal current and voltage after every accepted step (the
714
- voltage is the right contact's minus the left's: what a meter between the terminals reads,
759
+ current is the right contact's, toward +x, as `sol.current`; a port's is in `sol.terminals`
760
+ after each call; the voltage is the right contact's minus the left's: what a meter between the terminals reads,
715
761
  which at a bath is its reference species' level, not $`\phi`$). With
716
762
  `probes: [{ x, species, quantity, region }]` the trace also reads inside the device,
717
763
  as `trace.probes[k]` beside `trace.t`: a species' concentration (`quantity: 'c'`, the default,
@@ -774,7 +820,7 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
774
820
  | `terminals[name]` | `{ V, current }` for each terminal (contacts and ports), current into the device |
775
821
  | `contacts.left/right` | `{ V, flux: {name}, D, current }` at each contact |
776
822
  | `gates.left/right` | charge on a gate or Stern plate, where the contact is capacitive |
777
- | `ports[k]` | `{ name, V, flux: {name}, current }`: what each internal port brings into the device |
823
+ | `ports[k]` | `{ name, V, flux: {name}, current }`: what each internal port brings into the device; an electrode's rates, coverages and charge [as well](#what-a-solution-reports) |
778
824
  | `interfaces[f]` | `{ left, right, dipole, sheetCharge, D, N: {name}, rates }`: the names of the regions the face joins, what crosses it by its links, and each face reaction's rate (mol/(m²·s)) |
779
825
  | `bulkReactions[k]` | `{ rate, regions, total }`: each bulk reaction's forward rate at every node (mol/(m³·s), `NaN` where it doesn't run), and integrated over each region and the device (mol/(m²·s)), as the balances count it: a charge-balance check is J = F(generation − recombination), which the kit's `check()` does for every species |
780
826
  | `charge` | total charge in the device, C/m² (C through a cross-section) |
package/docs/kit.md CHANGED
@@ -133,6 +133,11 @@ the SHE level (`sol.phi` with the usual tables' ions).
133
133
 
134
134
  The example above ends with the left electrode's curve.
135
135
 
136
+ An electrode port's surface is held too by default. With `{ surface: 'equilibrium' }` (a fifth
137
+ argument) its coverages are re-equilibrated at each V instead, each species by the reaction that
138
+ makes it from the solution: the steady-state curve, where a passive film's active–passive peak
139
+ shows (`curve.bare` gives the bare fraction along it).
140
+
136
141
  Concentrations are held, so these are the curves of that instant (for a mixed potential, the
137
142
  kinetic picture). Away from the actual level the composition beside the electrode would change,
138
143
  which is the transport the full solve accounts for.