driftlet 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,18 +26,18 @@ minor versions.
26
26
  ## Is it for your problem?
27
27
 
28
28
  The same equations go by different names in different fields. If your problem is one-dimensional
29
- (planar), it's probably here:
29
+ (planar, or radial: spheres and cylinders, or any cross-section varying along x), it's probably here:
30
30
 
31
31
  | If you work on | you may call it | start from | checked against |
32
32
  |---|---|---|---|
33
33
  | Semiconductor devices | drift–diffusion, van Roosbroeck, quasi-Fermi levels, Scharfetter–Gummel; pn, Schottky, MOS, heterojunctions | [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
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 | Nernst–Planck, concentration polarization, limiting current, Butler–Volmer, Warburg, cyclic voltammetry, salt bridges and liquid junctions | [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) and [impedance](https://markblundeberg.github.io/driftlet/demos/impedance.html) demos, [`circuit`](test/circuit.test.js), [`kinetics`](test/kinetics.test.js), [`impedance`](test/impedance.test.js) tests | `i_lim·tanh(V/4V_T)`, Butler–Volmer closed form, finite-length Warburg |
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
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
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
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
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 | Poisson–Nernst–Planck, ion channels, membrane potentials | the electrochemistry setups, with fixed charge in the channel region | no dedicated check yet |
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
41
 
42
42
  The tests are worked setups, each with its analytic check, so they double as recipes. The
43
43
  [validation table](#validation) lists them all.
@@ -55,7 +55,7 @@ import { bandDiagram } from 'driftlet/plot'; // a level diagram as an SVG str
55
55
  ```
56
56
 
57
57
  Or straight from a CDN in a page, pinned to a version:
58
- `https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
58
+ `https://cdn.jsdelivr.net/npm/driftlet@0.8.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
59
59
  TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
60
60
  [`llms.txt`](llms.txt).
61
61
 
@@ -149,7 +149,9 @@ them locally, serve the repository root (e.g. `python3 -m http.server`) and open
149
149
  | [![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
150
  | **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
151
  | [![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) |
152
- | **Intercalation host**: cycling between cutoffs against the OCV | | |
152
+ | **Intercalation host**: cycling between cutoffs against the OCV | **Organic solar cell**: excitons diffusing to a donor/acceptor interface, against theory | **Perovskite hysteresis**: mobile ions and scan rate, against IonMonger |
153
+ | [![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
+ | **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 |
153
155
 
154
156
  ## How to think about it
155
157
 
@@ -202,7 +204,9 @@ current. Supported physics:
202
204
 
203
205
  ## What it doesn't do
204
206
 
205
- - **More than one dimension.** Planar 1D only, so no porous-electrode (P2D) models. For 2D/3D,
207
+ - **More than one dimension.** 1D only: planar, or a cross-section varying along x (spheres,
208
+ cylinders, a thinning film), so no porous electrodes with diffusion inside their particles
209
+ (P2D models). For 2D/3D,
206
210
  see [ChargeTransport.jl](https://github.com/WIAS-PDELib/ChargeTransport.jl), TCAD tools or
207
211
  COMSOL.
208
212
  - **Cross-diffusion.** Each species moves down its own μ̄ with its own D (the statistics can be
@@ -241,6 +245,9 @@ Every physics feature is tested against analytic results (`npm test`, node's bui
241
245
  | Perovskite J–V hysteresis, against IonMonger | an independent code (finite elements, Octave): mobile iodide vacancies, interface SRH, scans from 1 mV/s to 1 kV/s — hysteresis index, P_max, V_oc, the whole loop (as current where it's gentle, as a voltage offset near V_oc, where it falls at up to 1100 mA/cm² per volt) | 1.5e-3; 0.05 mW/cm²; 2 mV; 0.03 mA/cm² and 1 mV (0.010 and 0.31 mV found) |
242
246
  | Illuminated long pn diode | `J_sc = qG(L_n + L_p + W)` from a cold start; superposition at low injection | 5e-3; 1e-2 |
243
247
  | Schottky barrier (metal region \| n-Si) | surface density from the alignment; depletion charge | 5e-3; 2% |
248
+ | Charged pore or nanochannel (resolved Donnan layers at each mouth) | conductance against salt, down to its plateau, as Teorell–Meyer–Sievers in a long pore (short ones short by end layers ∝ 1/L); the zero-current voltage across a salt gradient; a symmetric pore's I–V odd, rectification with charge on one side or bipolar | 1%; 5e-3 to 1e-4; 1e-9 |
249
+ | Cell membrane (a capacitive face with permeabilities) | GHK resting potential (neutral or resolved solutions) and current–voltage curve; the same as a resolved 5 nm lipid layer; charging at I/C; with the Na⁺/K⁺ pump in a closed cell, Mullins–Noda, the static head 3Δμ̄_Na − 2Δμ̄_K = ΔG_ATP, and Donnan with the pump off | 1e-5 V; 3e-3; 2e-5 V; 1%; 1e-9 |
250
+ | Liquid junctions in mixtures | Henderson's formula against JPCalc's published values; driftlet's steady junction against Planck by shooting and the stationary Nernst–Planck codes LJPcalc and JLJP, a patch pipette's K-gluconate among them; a free-diffusion junction constant in time, between the two | 0.015 mV; 2e-4 and 0.02 mV; 5e-3 mV |
244
251
  | Liquid junction, open circuit | cell EMF 2t₊(RT/F) ln(c₁/c₂); Planck diffusion potential | 1e-4 |
245
252
  | Salt diffusion mode from a `c0` profile | c̄ + a·sin(πx/2L) decaying at π²D/4L², D = 2D₊D₋/(D₊ + D₋) | 2e-5 |
246
253
  | Concentration polarization | `i = i_lim tanh(V/4V_T)`, incl. galvanostatic and load modes | 2e-4 |
@@ -254,6 +261,8 @@ Every physics feature is tested against analytic results (`npm test`, node's bui
254
261
  | Advection and eddy mixing | exact convection–diffusion profile; `D + D_mix`; junction EMF unchanged by mixing | 1e-12; 1e-12; 1e-5 |
255
262
  | 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 |
256
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 |
264
+ | 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
+ | 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 |
257
266
  | 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 |
258
267
  | Conservation | per step, and against time-integrated contact fluxes | 1e-11 relative |
259
268
  | Strictly neutral limit (ε = 0) | Planck EMF; polarization with no overlimiting; Donnan at neutral faces | 1e-5; 1e-4; 1e-9 |
package/docs/device.md CHANGED
@@ -24,6 +24,7 @@ too (`FARADAY`, `GAS_CONSTANT`, `EPS0`, …).
24
24
  | `contacts` | `{ left, right }` |
25
25
  | `ports` | internal ports: outside phases exchanging with a window of nodes |
26
26
  | `grid` | default spacing for every region |
27
+ | `geometry` | the cross-section $`A(x)`$: planar (default), spherical or cylindrical shells, or a profile (see [geometry](#geometry)) |
27
28
 
28
29
  ## Species
29
30
 
@@ -98,7 +99,8 @@ regions: [
98
99
  - `fixedCharge`: immobile charge density (doping, ionomer), C/m³. Default 0. In an insertion
99
100
  host it's balanced by background electronic carriers.
100
101
  - `c0`: initial concentrations, mol/m³: the starting state of each species given. Species
101
- connected to a contact don't need it (without it, they start from that contact's level). Any
102
+ connected to a contact don't need it (without it, they start from that contact's level: the
103
+ left contact's if the species reaches it, else the right's, else a port's). Any
102
104
  other species (blocked everywhere, or only made and consumed by
103
105
  reactions) does: its `c0` fixes the amount it conserves. Concentrations are positive ($`\bar\mu`$ is
104
106
  logarithmic in them); to have none of a species in a region, leave it out of that region's
@@ -169,7 +171,8 @@ defaults to no dipole.
169
171
 
170
172
  **Species laws** `species` (default: local equilibrium, $`\bar\mu`$ continuous, where the species is
171
173
  present on both sides; blocked otherwise): `'equilibrium'`, `'blocked'`, or
172
- `{ type: 'conductance', G }` ($`J = G \cdot (V_L - V_R)`$, G in S/m², charged species). A species that
174
+ `{ type: 'conductance', G }` ($`J = G \cdot (V_L - V_R)`$, G in S/m², charged species), or
175
+ `{ type: 'permeability', P }` (P in m/s; see [membranes](#membranes)). A species that
173
176
  takes part in a reaction at the face and exists on both sides has no default: give its link
174
177
  (`'blocked'` if it crosses only through the reaction), since free crossing alongside would
175
178
  short-circuit the kinetics.
@@ -226,6 +229,54 @@ lies between the two sides (a band offset, a $`\phi`$ jump). Where one carrier i
226
229
  rate saturates at the other's capture, $`r \to v_p\,p`$: holes reaching an electron-rich layer
227
230
  recombine at $`v_p`$ however many electrons wait there, which plain mass action can't do.
228
231
 
232
+ **Saturating kinetics** (a transporter, a pump, an enzyme: something that turns over at most so
233
+ fast) replaces `k0` and `alpha` with `vmax` (mol/(m²·s)) and `K`, a half-saturation
234
+ concentration (mol/m³) for every species the forward reaction consumes:
235
+
236
+ ```math
237
+ r = v_{\max} \prod_{\nu<0} \left(\frac{c}{c + K}\right)^{|\nu|} \left(1 - e^{-A/RT}\right)
238
+ ```
239
+
240
+ Michaelis–Menten in each substrate, it runs at $`v_{\max}`$ when they're plentiful and the driving
241
+ force is large, and it's still exactly zero at $`A = 0`$, so a pump stalls where the free energy it
242
+ spends is used up: its static head. The backward rate is the one detailed balance implies, and
243
+ isn't itself saturating, so this is for reactions that run forward.
244
+
245
+ ### Membranes
246
+
247
+ A membrane thin against everything else (a lipid bilayer, 4–5 nm) is a face. Give it a
248
+ capacitive $`\phi`$ law, its capacitance (about 0.01 F/m², 1 µF/cm²), and a species law
249
+ `{ type: 'permeability', P }` for each ion that crosses it, through channels or carriers. The
250
+ flux is electrodiffusion across the membrane in a constant field (Goldman–Hodgkin–Katz),
251
+
252
+ ```math
253
+ N = P\,\frac{z u\,(c_L - c_R\,e^{z u})}{e^{z u} - 1}, \qquad u = \frac{\phi_R - \phi_L}{V_T}
254
+ ```
255
+
256
+ toward +x, with $`c_L`$ and $`c_R`$ at the face's two edge nodes and $`\phi_R - \phi_L`$ the capacitor's
257
+ voltage. (It's Scharfetter–Gummel across the face, with $`D/h \to P`$; where the two sides are
258
+ different materials, the step in standard level between them counts with $`z\Delta\phi`$, so the flux
259
+ is still exactly zero where $`\bar\mu`$ is level.) The solutions on either side can be strictly
260
+ neutral ($`\varepsilon = 0`$), the membrane's charge then held in each edge node's box, or resolve
261
+ their diffuse layers ($`\varepsilon > 0`$); either way the membrane potential comes out the same, to
262
+ a few microvolts. Pumps and other
263
+ transporters go on the same face as reactions with saturating kinetics, for example the
264
+ Na⁺/K⁺-ATPase between the outside (left) and the cytoplasm (right):
265
+
266
+ ```js nocheck
267
+ {
268
+ phi: { type: 'capacitive', C: 0.01 },
269
+ species: { 'K+': { type: 'permeability', P: 1e-8 }, 'Na+': { type: 'permeability', P: 4e-10 }, 'Cl-': { type: 'permeability', P: 4.5e-9 }, 'A-': 'blocked' },
270
+ reactions: [{ equation: '3 Na+(right) + 2 K+(left) + ATP = 3 Na+(left) + 2 K+(right) + ADP', fixed: { ATP: 50e3, ADP: 0 }, vmax: 5e-7, K: { 'Na+': 10, 'K+': 1.5 } }],
271
+ }
272
+ ```
273
+
274
+ `fixed` gives ATP's hydrolysis free energy (here 50 kJ/mol, with ADP and phosphate taken
275
+ together). Membrane potentials are $`\phi`$ differences between the two solutions, $`\phi_{\mathrm{in}} - \phi_{\mathrm{out}}`$,
276
+ read from `sol.phi` (or a `'phi'` probe in a transient). A bath contact's terminal voltage is
277
+ its reference species' level instead, what an electrode reversible to that species would read,
278
+ so between two different solutions it differs from $`\Delta\phi`$ by that species' Nernst term.
279
+
229
280
  ### Faces next to a conductor
230
281
 
231
282
  A conductor has no $`\phi`$, so a face beside it can't take a `dipole` or `step` alignment. Its $`\phi`$
@@ -395,7 +446,16 @@ contact: held at a voltage, behind a resistance, or driven by a current (a refer
395
446
  is a port at `I: 0`). Each solution reports `ports[k]`, with `{ name, V, flux, current }`: what
396
447
  the port brings into the device.
397
448
 
398
- A held (`'equilibrium'`) level leaves the device's two end nodes to their contacts.
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`$).
399
459
 
400
460
  A port driven by a current (`I`, or a waveform) floats to whatever voltage delivers it: with a
401
461
  conductance `G` per volume over a window of width $`w`$, it sits about $`I/(G w)`$ above the
@@ -417,6 +477,44 @@ steady state can be computed there. A port holding the electrons beside the oxid
417
477
  channel's potential anchors it, as source and drain would, and the device then shows the
418
478
  low-frequency C–V.
419
479
 
480
+ ### Electrodes spread through a window
481
+
482
+ A port can also hold an electrode: a metal whose carrier sits at the port's level, reacting all
483
+ through the window rather than at one face. That's the floor under a thin film of electrolyte, the
484
+ walls of a crevice or a pit, or the matrix of a porous electrode, wherever the metal conducts well
485
+ enough to be one level. Give the port its `reactions` (Butler–Volmer, as at a face), the carrier as
486
+ its `terminal` (the species need be in no material), and the electrode's `area` per volume of the
487
+ window:
488
+
489
+ ```js nocheck
490
+ ports: [{
491
+ name: 'iron', region: 'film', terminal: 'e-', V: 0,
492
+ area: 1 / 100e-6, // m²/m³: a film 100 µm thick on the metal; or a profile against x
493
+ reactions: [
494
+ { equation: 'Fe2+ + 2 e- = Fe(s)', fixed: { 'Fe(s)': 0 }, k0: 5e-6, alpha: 0.5 },
495
+ { equation: 'O2 + 2 H2O + 4 e- = 4 OH-', fixed: { H2O: -237.13e3 }, k0: 3e-9, alpha: 0.125 },
496
+ ],
497
+ }]
498
+ ```
499
+
500
+ Each node's rate per area is the face's law, with the region's species at that node and the
501
+ 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` and `ports[k].rates`
508
+ (mol/(m²·s) of electrode, forward), with `ports[k].area`.
509
+
510
+ Validated against the transmission line (linear kinetics along a bar, test/ports.test.js) and the
511
+ Wagner–Traud mixed potential of two Butler–Volmer couples. In 1D, a film on a metal is a slice
512
+ along the metal: the film's thickness enters through `area`, and through a bulk reaction for
513
+ anything that reaches the film from above (O₂ from the air, see the exchange link above). Where
514
+ the film's thickness varies, give the device its [cross-section](#geometry) too, so that ions
515
+ travelling along the film squeeze through where it thins: for a drop on a metal, seen from its
516
+ centre, $`A = 2\pi r\,h(r)`$ with `area` $`1/h(r)`$.
517
+
420
518
  ## Terminals
421
519
 
422
520
  The two contacts and every port are the device's terminals, named `left`, `right` and by each
@@ -471,6 +569,34 @@ cells exceeds the limiting current by 1.4% at 0.5 V, while `{ hmin: 10e-9, hmax:
471
569
  within 0.1% with as many nodes. Where a species carrying the current is steep across a region's
472
570
  end cells in this way, the solution's `warnings` say so, with a rough estimate of the excess.
473
571
 
572
+ ## Geometry
573
+
574
+ A device is planar unless it says otherwise: a slab whose cross-section $`A`$ is 1 m², so every
575
+ current, flux, amount and charge reads per m². With `geometry`, the cross-section varies along x,
576
+ and the conservation laws hold for totals through it: a box holds $`\int A\,dx`$, and what crosses
577
+ a face is the flux there times $`A`$ there.
578
+
579
+ ```js nocheck
580
+ geometry: { type: 'spherical', r0: 10e-6 }, // shells, r = r0 + x: a microelectrode of radius r0
581
+ geometry: { type: 'cylindrical', r0: 0 }, // a wire or fibre from its axis (per metre of length)
582
+ geometry: { area: { x: [0, 1e-3], values: [3e-6, 1e-7] } }, // A (m²) against x: a film thinning, a pit
583
+ ```
584
+
585
+ This is how radial problems become 1D, with the right $`1/r^2`$ (or $`1/r`$) in every divergence and in
586
+ Gauss's law, without writing them out. The finite volumes integrate over shells, so conservation
587
+ is exact as in a slab. Each segment's transport is weighted by its length over $`\int dx/A`$, which
588
+ makes steady diffusion between two nodes exact for any $`A(x)`$ (the $`1/r`$ and $`\ln r`$ profiles
589
+ on any grid). Where $`A`$ vanishes at an end (a sphere's centre, `r0: 0`), the segment takes $`A`$ at
590
+ its middle, and nothing passes the end. Only an end may have $`A = 0`$. Flow (`velocity`) is for
591
+ planar devices.
592
+
593
+ With a cross-section, the solution's currents, contact and port fluxes, terminal currents and
594
+ `charge` are totals (A, mol/s; per metre for cylinders), and so are `I` drives. Face laws,
595
+ rates, `interfaces[f].N` and `D`, and concentrations stay per area or per volume. Validated against
596
+ steady diffusion to a sphere and a cylinder (exact), Cottrell's transient with the spherical term,
597
+ uptake by a sphere filling from its surface, and Debye–Hückel screening around a charged sphere
598
+ (test/geometry.test.js).
599
+
474
600
  ## Using a device
475
601
 
476
602
  ```js nocheck
@@ -500,11 +626,13 @@ const now = dev.solution(); // snapshot of the current state
500
626
  `dt0` (first step), `dtMax`, `budgetMs` (return after this much wall time, with
501
627
  `done: false`), `maxSteps`, `method`. The step size carries over between calls, so an
502
628
  animation can call `advance(tNext, { budgetMs })` once per frame. The solution adds `done`,
503
- `rejected`, and a `trace` of terminal current and voltage after every accepted step. With
504
- `probes: [{ x, species, quantity, region }]` the trace also reads a species inside the device,
505
- as `trace.probes[k]` beside `trace.t`: its concentration (`quantity: 'c'`, the default, mol/m³)
506
- or species voltage (`'V'`), linearly between the nodes around `x` (at an interface, `region`
507
- picks the side). That's what a detector at `x` sees. Probes read at accepted steps, which grow
629
+ `rejected`, and a `trace` of terminal current and voltage after every accepted step (the
630
+ voltage is the right contact's minus the left's: what a meter between the terminals reads,
631
+ which at a bath is its reference species' level, not $`\phi`$). With
632
+ `probes: [{ x, species, quantity, region }]` the trace also reads inside the device,
633
+ as `trace.probes[k]` beside `trace.t`: a species' concentration (`quantity: 'c'`, the default,
634
+ mol/m³) or species voltage (`'V'`), or $`\phi`$ itself (`'phi'`, no species), linearly between the
635
+ nodes around `x` (at an interface, `region` picks the side). That's what a detector at `x` sees. Probes read at accepted steps, which grow
508
636
  as a transient slows, so give `dtMax` to resolve a signal in time.
509
637
  - `impedance(frequencies, { terminal, profiles })` solves the steady state, then linearises about
510
638
  it: $`Z(f) = \delta V/\delta I`$ in Ω·m² at one terminal (`'right'` by default), with I into the
@@ -558,14 +686,14 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
558
686
  | `phi` | bookkeeping $`\phi`$, V (`NaN` where undefined) |
559
687
  | `c[name]`, `mu[name]`, `muStd[name]` | concentration, $`\bar\mu`$, standard level $`\mu^\circ + zF\phi`$ (`NaN` where absent) |
560
688
  | `V[name]`, `Vstd[name]` | species voltage $`\bar\mu/(zF)`$ and standard level as a voltage (charged species) |
561
- | `current`, `terminalVoltage` | current toward +x through the device (A/m²) and $`V_{\mathrm{right}} - V_{\mathrm{left}}`$ |
689
+ | `current`, `terminalVoltage` | current toward +x through the device (A/m², or A through a [cross-section](#geometry)) and $`V_{\mathrm{right}} - V_{\mathrm{left}}`$ |
562
690
  | `terminals[name]` | `{ V, current }` for each terminal (contacts and ports), current into the device |
563
691
  | `contacts.left/right` | `{ V, flux: {name}, D, current }` at each contact |
564
692
  | `gates.left/right` | charge on a gate or Stern plate, where the contact is capacitive |
565
693
  | `ports[k]` | `{ name, V, flux: {name}, current }`: what each internal port brings into the device |
566
694
  | `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)) |
567
695
  | `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 |
568
- | `charge` | total charge in the device, C/m² |
696
+ | `charge` | total charge in the device, C/m² (C through a cross-section) |
569
697
  | `conservation` | per species stretch: amount, reference, intake through contacts, drift |
570
698
  | `warnings` | e.g. unresolved double layers, conventions a statistics model relies on, and for a failed solve, where the system is nearly singular |
571
699
  | `converged`, `iterations`, `steps`, `substeps`, `history`, `time` | solver bookkeeping |
package/docs/kit.md CHANGED
@@ -82,7 +82,7 @@ $`\mu_{\mathrm{H_2}} = 0`$, so its standard level is $`\phi + \mu^\circ_{\mathrm
82
82
 
83
83
  ```js
84
84
  import { Device } from 'driftlet';
85
- import { build, layer, ohmic, half, level } from 'driftlet/kit';
85
+ import { build, layer, ohmic, half, level, polarization } from 'driftlet/kit';
86
86
 
87
87
  const silver = half('Ag+ + e- = Ag(s)', { 'Ag(s)': 0 });
88
88
  const stern = { phi: { type: 'capacitive', C: 0.2 }, zeroCharge: 0.1 }; // an assumed pzc
@@ -108,11 +108,35 @@ const def = build({
108
108
  grid: { hmin: 0.1e-9, hmax: 100e-9, ratio: 1.15 },
109
109
  });
110
110
  console.log(JSON.stringify(def.interfaces[0].reactions[0])); // { left: { e-: -1, Ag(s): 1 }, right: { Ag+: -1 }, … }
111
- const sol = new Device(def).solve();
111
+ const dev = new Device(def), sol = dev.solve();
112
112
  const redox = level(sol, silver); // V_Ag⁺(x) in the solution
113
113
  console.log(`redox level at mid-cell: ${redox[sol.x.length >> 1].toFixed(4)} V`);
114
+ // The left electrode's polarization curve, 0.2 V either side of its own level (see below).
115
+ const metalSide = sol.V['e-'][0];
116
+ const curve = polarization(dev, sol, { face: 0 }, Array.from({ length: 81 }, (_, j) => metalSide - 0.2 + j * 0.005));
117
+ console.log(curve.reactions[0].level, curve.current[40]); // the couple's level here, and the current at the metal's own
114
118
  ```
115
119
 
120
+ ### Polarization curves: an Evans diagram
121
+
122
+ `polarization(device, sol, where, V)` evaluates every electrode reaction at one spot with the
123
+ solution beside it held as `sol` has it and the metal's level set to each `V` (an electron
124
+ voltage, the terminals' convention). It works at a face (`{ face: f }`, the face's metal side) or
125
+ at a spot of an [electrode port](device.md#electrodes-spread-through-a-window)
126
+ (`{ port: name, x }`). Each reaction comes back with its rate (mol/(m²·s), forward), its partial
127
+ current (A/m², anodic positive: into the solution) and its `level`, where the rate vanishes (the
128
+ couple's redox level there, as `level()` gives it). The net current is their sum. Plotting
129
+ log|current| against V gives the Evans diagram. The net crosses zero at the spot's open-circuit
130
+ potential, between the couples' levels, and the metal's actual level is where the solve put it.
131
+ The law is the solver's own, so at that level the rates are the solution's. Against SHE, subtract
132
+ the SHE level (`sol.phi` with the usual tables' ions).
133
+
134
+ The example above ends with the left electrode's curve.
135
+
136
+ Concentrations are held, so these are the curves of that instant (for a mixed potential, the
137
+ kinetic picture). Away from the actual level the composition beside the electrode would change,
138
+ which is the transport the full solve accounts for.
139
+
116
140
  ## Sources
117
141
 
118
142
  Helpers that write sources as plain data:
@@ -169,6 +193,34 @@ const seen = run.trace.probes[0], k = seen.indexOf(Math.max(...seen));
169
193
  console.log(`the pulse passes at ${(run.trace.t[k] * 1e6).toFixed(2)} µs, Δp ≈ ${units.toPerCm3(seen[k]).toExponential(1)} cm⁻³`);
170
194
  ```
171
195
 
196
+ ## Liquid junctions
197
+
198
+ `henderson(left, right, ions, { T })` and `planck(left, right, ions, { T })` give the junction
199
+ potential $`\phi_R - \phi_L`$ (V) between two neutral solutions (mol/m³ by ion), with `ions` their
200
+ `{ z, D }` (the data library's `IONS` has that shape):
201
+
202
+ - **Henderson's** formula takes the junction to be the two solutions mixed in one proportion at
203
+ every point (so every ion's mixing fraction follows the same curve). It's
204
+ a closed form, exact for one salt, and what JPCalc and most electrophysiology corrections use.
205
+ - **Planck's** is the steady state of diffusion through a zone held between the two solutions,
206
+ electroneutral and carrying no current, solved by shooting on the Nernst–Planck equations. It's
207
+ exact for that junction, and equal to driftlet's own steady state between two baths.
208
+
209
+ A junction growing freely from first contact is neither: simulate it, as a transient from a
210
+ `c0` step. For one salt all three agree; for mixtures they differ by up to a millivolt or so.
211
+ Validated against JPCalc (Henderson), and LJPcalc and JLJP (stationary Nernst–Planck), in
212
+ test/junction.test.js.
213
+
214
+ ```js
215
+ import { henderson, planck, IONS } from 'driftlet/kit';
216
+
217
+ // 50 mM NaCl against 50 mM KCl, and a K-gluconate pipette (gluconate's D from its relative mobility, 0.33 of K⁺'s).
218
+ const ions = { ...IONS, 'gluconate-': { z: -1, D: 0.33 * IONS['K+'].D } };
219
+ console.log(1000 * henderson({ 'Na+': 50, 'Cl-': 50 }, { 'K+': 50, 'Cl-': 50 }, ions), 'mV');
220
+ const pipette = { 'K+': 140, 'gluconate-': 130, 'Cl-': 10 }, bath = { 'Na+': 145, 'K+': 5, 'Cl-': 150 };
221
+ console.log(`Henderson ${(1000 * henderson(pipette, bath, ions)).toFixed(2)} mV, Planck ${(1000 * planck(pipette, bath, ions)).toFixed(2)} mV`);
222
+ ```
223
+
172
224
  ## Recording a transient
173
225
 
174
226
  `recorder(device, { every, times, probes })` runs a transient in pieces and keeps what a demo
@@ -339,7 +391,9 @@ page can show that its numbers hold up. Pass the solution `solve()` or `advance(
339
391
  prompt to look, and don't fail the check.
340
392
  - **balance**, in a steady state: each species' ledger, what comes in through each terminal and
341
393
  what each reaction (bulk, or at a face) makes or uses, in mol/(m²·s) and for a charged species
342
- as a current. The terms must sum to zero. Under light it's J = F(G − R), and it says where
394
+ as a current. The terms must sum to zero. A species that a face lets through only by a law (a
395
+ membrane's permeability, a conductance) or by a reaction has a ledger on each side, with what
396
+ crosses as a term: in a cell, Na⁺ leaks into the cytoplasm as fast as the pump puts it out. Under light it's J = F(G − R), and it says where
343
397
  every carrier went: collected, or recombined in the bulk or at which face. A species at rest
344
398
  (in equilibrium, flows only of round-off against what it could carry) isn't listed.
345
399
  - **conservation**, in a transient: every closed, unreacting stretch keeps what it held plus
@@ -373,7 +427,7 @@ const dev = new Device(
373
427
  );
374
428
  const report = check(dev, dev.solve());
375
429
  console.log(report.text);
376
- // ok balance: 2 species' sources and sinks sum to zero …
430
+ // ok balance: 2 ledgers sum to zero …
377
431
  // e-: photon = e- + h+: +2.99e-3 (289 A/m²); left contact: -2.23e-3 (-215 A/m²); …
378
432
  // FAIL grid: on a grid twice as fine (172 → 339 nodes), the largest change is region 0 (Si)
379
433
  // charge, … (0.021, over 0.01: refine the grid …): the current is fine (1e-4), but the
package/docs/numerics.md CHANGED
@@ -117,6 +117,22 @@ Grids grade geometrically from `hmin` at each region end up to `hmax`. They're n
117
117
  automatically, to avoid performance surprises. Instead, solutions warn when a double layer the
118
118
  model is meant to resolve is coarser than the local Debye length.
119
119
 
120
+ ### A cross-section
121
+
122
+ With a [`geometry`](device.md#geometry), the device has a cross-section $`A(x)`$ (spherical
123
+ shells $`4\pi r^2`$, cylindrical $`2\pi r`$ with $`r = r_0 + x`$, or a profile), and every balance
124
+ is for totals through it:
125
+ $`\partial_t (A c_i) + \partial_x (A N_i) = A \sum_r \nu_{ir} r_r`$, and Gauss's law
126
+ $`-\partial_x (A\varepsilon\,\partial_x\phi) = A\rho`$. The box method carries this without writing out any
127
+ $`1/r`$ terms: a box's volume becomes $`\int A\,dx`$ over it (exactly, for each shape), and what a
128
+ segment, a face or a contact passes is multiplied by the area it passes through. A segment's
129
+ $`1/h`$ becomes $`1/\int dx/A`$ over it, its resistance per unit conductivity, which is the exact
130
+ weighting for steady diffusion (a constant total flux $`AN`$, so $`\Delta c = AN\int dx/A\,/D`$).
131
+ Scharfetter–Gummel keeps its form with that weighting, and so does the displacement. Where $`A`$
132
+ vanishes at an end (a sphere's centre), $`\int dx/A`$ diverges, so that segment takes $`A`$ at its
133
+ middle. A flux node's unknowns stay per area, entering each side's balance times the face's
134
+ area. A planar device has $`A = 1`$ and every factor is exactly 1, so it's unchanged to the last bit.
135
+
120
136
  ### What lives where
121
137
 
122
138
  | Place | Holds |
package/llms.txt CHANGED
@@ -14,9 +14,9 @@ docs/conventions.md (signs and levels) and docs/data.md (the data library).
14
14
  From a CDN, pin the version:
15
15
 
16
16
  ```js nocheck
17
- import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/index.js';
18
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/kit.js';
19
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/plot.js';
17
+ import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.8.0/src/index.js';
18
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.8.0/src/kit.js';
19
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.8.0/src/plot.js';
20
20
  ```
21
21
 
22
22
  From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
@@ -50,7 +50,7 @@ From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
50
50
  V_T = GAS_CONSTANT·T/FARADAY). `driftlet/kit`: `build`, `layer`, `ohmic`, `bath`, `half`,
51
51
  `level`, `SHE`, `describe`, `live`, `traces`, the sources (`pulse`, `square`, `triangle`, `ramp`,
52
52
  `injector`, `recombination`, `photogeneration`), the data library (`aqueous`, `semiconductor`,
53
- `metal`, `IONS`, …), `check` (a solution's checks), `recorder` (a transient's trace and frames across calls), the curated
53
+ `metal`, `IONS`, …), `check` (a solution's checks), `polarization` (an electrode's reactions at hypothetical metal levels, with the solution held: an Evans diagram), `henderson` and `planck` (liquid-junction potentials), `recorder` (a transient's trace and frames across calls), the curated
54
54
  `perovskiteCell` with IonMonger's `PEROVSKITE_SCANS` and `hysteresis`, and the vacuum-level helpers. `driftlet/plot` (a quick look; `traces()` is the data for your own charts): `bandDiagram`, `levelChart`, `labelParts` (labels mark subscripts as `V_{e⁻}`), `themeColor`.
55
55
  - **Reactions** are written as equations, `{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }`, in
56
56
  `new Device`, `build()` and `set()` alike. A coefficient is a number and a space (`2 e-`).
@@ -160,6 +160,57 @@ const oc = new Device(cell({ I: 0 })).solve(); // open circuit
160
160
  console.log(`J_sc = ${sc.current.toFixed(1)} A/m², V_oc = ${oc.terminals.right.V.toFixed(3)} V`);
161
161
  ```
162
162
 
163
+ ## Template: a cell membrane
164
+
165
+ A lipid bilayer is a face: a capacitor (about 1 µF/cm², 0.01 F/m²) that ions cross through
166
+ channels, each with a permeability (`{ type: 'permeability', P }`, m/s: electrodiffusion in a
167
+ constant field, so Goldman–Hodgkin–Katz comes out of it), and that pumps cross as reactions with
168
+ saturating kinetics (`vmax`, `K`). The solutions on either side can be strictly neutral. The
169
+ membrane potential is φ_in − φ_out from `sol.phi` (a bath's terminal voltage reads its reference
170
+ species' level instead, which differs from Δφ by that ion's Nernst term).
171
+
172
+ ```js
173
+ import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
174
+ import { build, layer, bath, aqueous, check } from 'driftlet/kit';
175
+
176
+ const T = 310.15, VT = (GAS_CONSTANT * T) / FARADAY;
177
+ const out = { 'Na+': 145, 'K+': 5, 'Cl-': 110, 'A-': 40 }; // mM = mol/m³; A⁻: anions that don't cross
178
+ const inside = { 'Na+': 12, 'K+': 140, 'Cl-': 10, 'A-': 142 };
179
+ const water = aqueous(['Na+', 'K+', 'Cl-'], { epsr: 0 }); // strictly neutral solutions
180
+ water.species.push({ name: 'A-', z: -1, cRef: 1000 });
181
+ water.materials.water.species['A-'] = { D: 0.5e-9, mu0: 0 };
182
+ const P = { 'K+': 1e-8, 'Na+': 0.04e-8, 'Cl-': 0.45e-8 }; // m/s, Hodgkin and Katz's ratios
183
+ const membrane = {
184
+ phi: { type: 'capacitive', C: 0.01 },
185
+ species: { 'K+': { type: 'permeability', P: P['K+'] }, 'Na+': { type: 'permeability', P: P['Na+'] }, 'Cl-': { type: 'permeability', P: P['Cl-'] }, 'A-': 'blocked' },
186
+ // The Na⁺/K⁺ pump (outside on the left): 3 Na⁺ out, 2 K⁺ in, per ATP (50 kJ/mol); vmax ≈ 3000
187
+ // pumps per µm² at 100 cycles/s.
188
+ reactions: [{ equation: '3 Na+(right) + 2 K+(left) + ATP = 3 Na+(left) + 2 K+(right) + ADP', fixed: { ATP: 50e3, ADP: 0 }, vmax: 5e-7, K: { 'Na+': 10, 'K+': 1.5 } }],
189
+ };
190
+ const cell = build({
191
+ T,
192
+ library: [water],
193
+ stack: [
194
+ bath(out, 'Cl-'), // the extracellular fluid, held
195
+ layer('water', 1e-6, { name: 'outside', c0: out }),
196
+ membrane,
197
+ layer('water', 5e-6, { name: 'cytoplasm', c0: inside }), // a closed cell: its contents set by leaks and the pump
198
+ { species: { 'Na+': 'blocked', 'K+': 'blocked', 'Cl-': 'blocked', 'A-': 'blocked' }, phi: 'neutral' },
199
+ ],
200
+ grid: { hmin: 50e-9, hmax: 200e-9 },
201
+ });
202
+ const dev = new Device(cell), sol = dev.solve();
203
+ const g = sol.x.length - 1, ci = (s) => sol.c[s][g], Vm = sol.phi[g] - sol.phi[0];
204
+ // Mullins–Noda: in steady state the pump's 3:2 sets the ratio of the Na⁺ and K⁺ leaks.
205
+ const mn = VT * Math.log((1.5 * P['K+'] * out['K+'] + P['Na+'] * out['Na+']) / (1.5 * P['K+'] * ci('K+') + P['Na+'] * ci('Na+')));
206
+ console.log(`V_m = ${(Vm * 1e3).toFixed(2)} mV (Mullins–Noda ${(mn * 1e3).toFixed(2)}), inside Na⁺ ${ci('Na+').toFixed(1)} K⁺ ${ci('K+').toFixed(1)} mM`);
207
+ console.log(check(dev, sol).text); // per compartment: Na⁺ leaks into the cytoplasm as fast as it's pumped out
208
+ ```
209
+
210
+ Between two baths instead (the inside one `bath(inside, 'Cl-', { I: 0 })`: held composition, no
211
+ current), V_m is GHK. Without the pump, a closed cell runs down to Donnan equilibrium. driftlet
212
+ has no voltage-gated channels; a permeability can be changed with `set()` between steps.
213
+
163
214
  ## Template: an electrode in a stirred solution
164
215
 
165
216
  One working electrode, a stagnant layer of solution, and the well-stirred bulk as a bath. The
@@ -233,8 +284,9 @@ console.log(`peak at ${(run.trace.t[k] * 1e6).toFixed(2)} µs, Δp ${units.toPer
233
284
  To start from a state instead of simulating how it got there (a packet already injected, a
234
285
  salt gradient laid down), give a region's `c0` a profile against the device's x:
235
286
  `c0: { 'h+': { x: [...], values: [...] } }`, piecewise linear. A given `c0` is the starting state,
236
- even for a species a contact feeds; species without one start at their contact's level, with φ
237
- made neutral node by node (electrons follow the holes). Then `advance()` without `solve()`, which would go straight to the steady
287
+ even for a species a contact feeds; species without one start at their contact's level (the
288
+ left's if they reach it, else the right's), with φ made neutral node by node (electrons follow
289
+ the holes). Between two different solutions, give every region its `c0`. Then `advance()` without `solve()`, which would go straight to the steady
238
290
  state.
239
291
 
240
292
  Transients cost more where a carrier's density spans many decades (minority holes in silicon
@@ -255,7 +307,9 @@ The definition is plain data: print it, edit what your demo varies.
255
307
 
256
308
  `live()` merges slider moves made during a solve, warm-starts each solve and keeps the last
257
309
  good solution. `bandDiagram()` returns an SVG string. Pass `worker: true` to `live()` to solve in
258
- a Web Worker.
310
+ a Web Worker (it works with the library loaded from a CDN: a same-origin stub imports it). Calling
311
+ `set()` from `onsolution` is fine, for a quantity the page solves for itself (it queues the next
312
+ solve), as long as it settles rather than ping-pongs.
259
313
 
260
314
  ```html
261
315
  <!doctype html>
@@ -265,9 +319,9 @@ a Web Worker.
265
319
  <span id="readout"></span>
266
320
  <div id="figure"></div>
267
321
  <script type="module">
268
- import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/index.js';
269
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/kit.js';
270
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/plot.js';
322
+ import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.8.0/src/index.js';
323
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.8.0/src/kit.js';
324
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.8.0/src/plot.js';
271
325
 
272
326
  const def = build({
273
327
  T: 300,
@@ -351,11 +405,16 @@ has the long form):
351
405
  do. It checks the setup: a solver's output looks authoritative even when an input is wrong, and
352
406
  a mismatch is the cheapest way to catch it. Where the two should differ (high injection, a
353
407
  finite pulse), say why.
354
- - **Stay honestly inside 1D.** driftlet models planar devices. Where the real device isn't one,
408
+ - **Stay honestly inside 1D.** driftlet models devices along one coordinate: planar, or with a
409
+ cross-section A(x) (`geometry`: spherical or cylindrical shells, or a profile), in which case
410
+ currents and fluxes are totals through it. Where the real device isn't one,
355
411
  say so and pick the slice that still tells the true story: a MOSFET's gate stack, read as the
356
412
  MOS capacitor its channel sits on (pinch-off along the channel is 2D); a BJT along its
357
413
  emitter–base–collector axis; one particle of a porous battery electrode, or a thin dense film,
358
- rather than the whole porous electrode (that's a P2D model).
414
+ rather than the whole porous electrode with diffusion inside its particles (that's a P2D model).
415
+ A metal that reacts all along a film of electrolyte (corroding iron under a drop, a crevice's
416
+ walls, a well-conducting porous matrix) is a port with `reactions` and an `area` per volume:
417
+ an electrode spread through a window, not a face at each end.
359
418
  - **Check `sol.converged` and `sol.warnings`** on every solve, and pin the CDN version you
360
419
  tested against.
361
420
 
@@ -384,11 +443,15 @@ has the long form):
384
443
  - **A steady solve of a closed cell where every ion can cross.** Its steady state is complete
385
444
  mixing. To watch a junction or a salt bridge, step in time from the unmixed `c0` (`advance`).
386
445
  - **Everything floating.** At least one terminal must be held at a voltage `V`.
387
- - **The library's semiconductors are at 300 K.** Give `T: 300`.
446
+ - **The library's data are for 25 °C** (its semiconductors for 300 K: give `T: 300`). At body
447
+ temperature, `T: 310.15` with the same μ° and D is a fair start; D rises about 2% per kelvin
448
+ in water.
388
449
  - **Sign of the current.** `sol.current` is positive toward +x: a forward-biased diode with its
389
450
  p side on the right carries negative current. A terminal's current is into the device.
390
- - **Sign of a voltage across a membrane.** `sol.terminalVoltage` is V_right − V_left. Physiology
391
- quotes V_inside − V_outside, so put the inside on the right or flip the sign.
451
+ - **A membrane potential read from the terminals.** Physiology's V_m is φ_inside − φ_outside:
452
+ read it from `sol.phi` (or a `{ x, quantity: 'phi' }` probe in a transient), with the inside on
453
+ the right. `sol.terminalVoltage` (and a trace's `voltage`) is V_right − V_left of the contacts,
454
+ which at baths are their reference species' levels: it differs from Δφ by that ion's Nernst term.
392
455
  - **A coefficient without its space.** `'2e- + …'` reads `2e-` as a species name; the error says
393
456
  to write `2 e-`.
394
457
  - **Changing c0 with `set()`.** A carried state keeps what it holds; `c0` is only the starting
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driftlet",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Small, dependency-free 1D drift–diffusion–reaction (Poisson–Nernst–Planck) solver for interactive web demos",
5
5
  "type": "module",
6
6
  "exports": {