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 +15 -6
- package/docs/device.md +138 -10
- package/docs/kit.md +58 -4
- package/docs/numerics.md +16 -0
- package/llms.txt +78 -15
- package/package.json +1 -1
- package/src/checks.js +59 -24
- package/src/describe.js +21 -3
- package/src/device.js +126 -17
- package/src/grid.js +73 -0
- package/src/index.js +1 -1
- package/src/junction.js +164 -0
- package/src/kit.js +2 -0
- package/src/polarization.js +84 -0
- package/src/recorder.js +1 -1
- package/src/solution.js +12 -4
- package/src/solver.js +240 -47
- package/src/types.js +15 -4
- package/types/grid.d.ts +29 -0
- package/types/index.d.ts +3 -3
- package/types/junction.d.ts +32 -0
- package/types/kit.d.ts +2 -0
- package/types/polarization.d.ts +31 -0
- package/types/recorder.d.ts +3 -3
- package/types/solver.d.ts +6 -2
- package/types/types.d.ts +41 -4
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)
|
|
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 |
|
|
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.
|
|
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
|
| [](https://markblundeberg.github.io/driftlet/demos/impedance.html) | [](https://markblundeberg.github.io/driftlet/demos/membrane.html) | [](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
|
| [](https://markblundeberg.github.io/driftlet/demos/insertion.html) | [](https://markblundeberg.github.io/driftlet/demos/organic.html) | [](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
|
+
| [](https://markblundeberg.github.io/driftlet/demos/channel.html) | [](https://markblundeberg.github.io/driftlet/demos/cell.html) | [](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.**
|
|
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
|
|
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)
|
|
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
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
18
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
19
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
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
|
|
237
|
-
|
|
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.
|
|
269
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
270
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
391
|
-
|
|
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
|