driftlet 0.5.0 → 0.7.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 +14 -7
- package/docs/device.md +61 -7
- package/docs/kit.md +147 -0
- package/llms.txt +100 -15
- package/package.json +1 -1
- package/src/checks.js +302 -0
- package/src/describe.js +18 -7
- package/src/device.js +31 -4
- package/src/devices.js +135 -0
- package/src/index.js +1 -1
- package/src/junction.js +164 -0
- package/src/kit.js +4 -0
- package/src/recorder.js +89 -0
- package/src/solution.js +18 -0
- package/src/solver.js +135 -7
- package/src/stack.js +1 -1
- package/src/types.js +4 -1
- package/types/checks.d.ts +59 -0
- package/types/devices.d.ts +77 -0
- package/types/index.d.ts +3 -3
- package/types/junction.d.ts +32 -0
- package/types/kit.d.ts +4 -0
- package/types/recorder.d.ts +27 -0
- package/types/solution.d.ts +1 -0
- package/types/solver.d.ts +8 -1
- package/types/stack.d.ts +1 -0
- package/types/types.d.ts +15 -0
package/README.md
CHANGED
|
@@ -31,13 +31,13 @@ The same equations go by different names in different fields. If your problem is
|
|
|
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
|
-
| 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
|
|
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)
|
|
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), [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) tests | `i_lim·tanh(V/4V_T)`, Butler–Volmer closed form, finite-length Warburg; junction potentials against Henderson, Planck, JPCalc and LJPcalc |
|
|
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.7.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
|
|
|
@@ -128,7 +128,9 @@ Solutions are plain objects of `Float64Array`s, ready to plot (fresh arrays from
|
|
|
128
128
|
and the voltage views `V`, `Vstd`. They also carry the terminals' voltages and currents, fluxes,
|
|
129
129
|
conservation bookkeeping and warnings; see [the device reference](docs/device.md). The
|
|
130
130
|
[kit](docs/kit.md) adds reactions written as equations (`'Ag+ + e- = Ag(s)'`), redox levels,
|
|
131
|
-
level diagrams, slider demos
|
|
131
|
+
level diagrams, slider demos, a transient recorder, `describe()` (which flags likely unit slips),
|
|
132
|
+
`check()` (a solution's ledgers and a grid-convergence check),
|
|
133
|
+
and curated devices with reference results: IonMonger's perovskite cell, with its J–V scans.
|
|
132
134
|
|
|
133
135
|
## Demos
|
|
134
136
|
|
|
@@ -147,7 +149,9 @@ them locally, serve the repository root (e.g. `python3 -m http.server`) and open
|
|
|
147
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) |
|
|
148
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 |
|
|
149
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) |
|
|
150
|
-
| **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 |
|
|
151
155
|
|
|
152
156
|
## How to think about it
|
|
153
157
|
|
|
@@ -236,9 +240,12 @@ Every physics feature is tested against analytic results (`npm test`, node's bui
|
|
|
236
240
|
| Beer–Lambert photogeneration in an n⁺p cell | J_sc = qΦ∫αe^(−αx)η(x)dx with the emitter's, depletion layer's and base's collection, α from 1e3 to 1e7 /m on one coarse grid | 5e-4 (3e-3 in the emitter) |
|
|
237
241
|
| Organic bilayer: excitons | J_sc from excitons diffusing to the donor/acceptor interface (Beer–Lambert, a blocking anode, the splitting reaction's finite velocity); V_oc from charge-transfer detailed balance | 5e-4; 1e-6 V |
|
|
238
242
|
| SRH recombination | bulk: G = R_SRH(n, p) with n = N_D + p, midgap and shallow traps, low to high injection; at a face: zero in the dark, the law between the two edge nodes, every pair collected or recombined | 1e-6; 1e-9 |
|
|
239
|
-
| 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 | 1.5e-3; 0.05 mW/cm²; 2 mV; 0.
|
|
243
|
+
| 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) |
|
|
240
244
|
| Illuminated long pn diode | `J_sc = qG(L_n + L_p + W)` from a cold start; superposition at low injection | 5e-3; 1e-2 |
|
|
241
245
|
| Schottky barrier (metal region \| n-Si) | surface density from the alignment; depletion charge | 5e-3; 2% |
|
|
246
|
+
| 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 |
|
|
247
|
+
| 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 |
|
|
248
|
+
| 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 |
|
|
242
249
|
| Liquid junction, open circuit | cell EMF 2t₊(RT/F) ln(c₁/c₂); Planck diffusion potential | 1e-4 |
|
|
243
250
|
| Salt diffusion mode from a `c0` profile | c̄ + a·sin(πx/2L) decaying at π²D/4L², D = 2D₊D₋/(D₊ + D₋) | 2e-5 |
|
|
244
251
|
| Concentration polarization | `i = i_lim tanh(V/4V_T)`, incl. galvanostatic and load modes | 2e-4 |
|
package/docs/device.md
CHANGED
|
@@ -98,7 +98,8 @@ regions: [
|
|
|
98
98
|
- `fixedCharge`: immobile charge density (doping, ionomer), C/m³. Default 0. In an insertion
|
|
99
99
|
host it's balanced by background electronic carriers.
|
|
100
100
|
- `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
|
|
101
|
+
connected to a contact don't need it (without it, they start from that contact's level: the
|
|
102
|
+
left contact's if the species reaches it, else the right's, else a port's). Any
|
|
102
103
|
other species (blocked everywhere, or only made and consumed by
|
|
103
104
|
reactions) does: its `c0` fixes the amount it conserves. Concentrations are positive ($`\bar\mu`$ is
|
|
104
105
|
logarithmic in them); to have none of a species in a region, leave it out of that region's
|
|
@@ -169,7 +170,8 @@ defaults to no dipole.
|
|
|
169
170
|
|
|
170
171
|
**Species laws** `species` (default: local equilibrium, $`\bar\mu`$ continuous, where the species is
|
|
171
172
|
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)
|
|
173
|
+
`{ type: 'conductance', G }` ($`J = G \cdot (V_L - V_R)`$, G in S/m², charged species), or
|
|
174
|
+
`{ type: 'permeability', P }` (P in m/s; see [membranes](#membranes)). A species that
|
|
173
175
|
takes part in a reaction at the face and exists on both sides has no default: give its link
|
|
174
176
|
(`'blocked'` if it crosses only through the reaction), since free crossing alongside would
|
|
175
177
|
short-circuit the kinetics.
|
|
@@ -226,6 +228,54 @@ lies between the two sides (a band offset, a $`\phi`$ jump). Where one carrier i
|
|
|
226
228
|
rate saturates at the other's capture, $`r \to v_p\,p`$: holes reaching an electron-rich layer
|
|
227
229
|
recombine at $`v_p`$ however many electrons wait there, which plain mass action can't do.
|
|
228
230
|
|
|
231
|
+
**Saturating kinetics** (a transporter, a pump, an enzyme: something that turns over at most so
|
|
232
|
+
fast) replaces `k0` and `alpha` with `vmax` (mol/(m²·s)) and `K`, a half-saturation
|
|
233
|
+
concentration (mol/m³) for every species the forward reaction consumes:
|
|
234
|
+
|
|
235
|
+
```math
|
|
236
|
+
r = v_{\max} \prod_{\nu<0} \left(\frac{c}{c + K}\right)^{|\nu|} \left(1 - e^{-A/RT}\right)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Michaelis–Menten in each substrate, it runs at $`v_{\max}`$ when they're plentiful and the driving
|
|
240
|
+
force is large, and it's still exactly zero at $`A = 0`$, so a pump stalls where the free energy it
|
|
241
|
+
spends is used up: its static head. The backward rate is the one detailed balance implies, and
|
|
242
|
+
isn't itself saturating, so this is for reactions that run forward.
|
|
243
|
+
|
|
244
|
+
### Membranes
|
|
245
|
+
|
|
246
|
+
A membrane thin against everything else (a lipid bilayer, 4–5 nm) is a face. Give it a
|
|
247
|
+
capacitive $`\phi`$ law, its capacitance (about 0.01 F/m², 1 µF/cm²), and a species law
|
|
248
|
+
`{ type: 'permeability', P }` for each ion that crosses it, through channels or carriers. The
|
|
249
|
+
flux is electrodiffusion across the membrane in a constant field (Goldman–Hodgkin–Katz),
|
|
250
|
+
|
|
251
|
+
```math
|
|
252
|
+
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}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
toward +x, with $`c_L`$ and $`c_R`$ at the face's two edge nodes and $`\phi_R - \phi_L`$ the capacitor's
|
|
256
|
+
voltage. (It's Scharfetter–Gummel across the face, with $`D/h \to P`$; where the two sides are
|
|
257
|
+
different materials, the step in standard level between them counts with $`z\Delta\phi`$, so the flux
|
|
258
|
+
is still exactly zero where $`\bar\mu`$ is level.) The solutions on either side can be strictly
|
|
259
|
+
neutral ($`\varepsilon = 0`$), the membrane's charge then held in each edge node's box, or resolve
|
|
260
|
+
their diffuse layers ($`\varepsilon > 0`$); either way the membrane potential comes out the same, to
|
|
261
|
+
a few microvolts. Pumps and other
|
|
262
|
+
transporters go on the same face as reactions with saturating kinetics, for example the
|
|
263
|
+
Na⁺/K⁺-ATPase between the outside (left) and the cytoplasm (right):
|
|
264
|
+
|
|
265
|
+
```js nocheck
|
|
266
|
+
{
|
|
267
|
+
phi: { type: 'capacitive', C: 0.01 },
|
|
268
|
+
species: { 'K+': { type: 'permeability', P: 1e-8 }, 'Na+': { type: 'permeability', P: 4e-10 }, 'Cl-': { type: 'permeability', P: 4.5e-9 }, 'A-': 'blocked' },
|
|
269
|
+
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 } }],
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`fixed` gives ATP's hydrolysis free energy (here 50 kJ/mol, with ADP and phosphate taken
|
|
274
|
+
together). Membrane potentials are $`\phi`$ differences between the two solutions, $`\phi_{\mathrm{in}} - \phi_{\mathrm{out}}`$,
|
|
275
|
+
read from `sol.phi` (or a `'phi'` probe in a transient). A bath contact's terminal voltage is
|
|
276
|
+
its reference species' level instead, what an electrode reversible to that species would read,
|
|
277
|
+
so between two different solutions it differs from $`\Delta\phi`$ by that species' Nernst term.
|
|
278
|
+
|
|
229
279
|
### Faces next to a conductor
|
|
230
280
|
|
|
231
281
|
A conductor has no $`\phi`$, so a face beside it can't take a `dipole` or `step` alignment. Its $`\phi`$
|
|
@@ -500,11 +550,13 @@ const now = dev.solution(); // snapshot of the current state
|
|
|
500
550
|
`dt0` (first step), `dtMax`, `budgetMs` (return after this much wall time, with
|
|
501
551
|
`done: false`), `maxSteps`, `method`. The step size carries over between calls, so an
|
|
502
552
|
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
|
-
|
|
553
|
+
`rejected`, and a `trace` of terminal current and voltage after every accepted step (the
|
|
554
|
+
voltage is the right contact's minus the left's: what a meter between the terminals reads,
|
|
555
|
+
which at a bath is its reference species' level, not $`\phi`$). With
|
|
556
|
+
`probes: [{ x, species, quantity, region }]` the trace also reads inside the device,
|
|
557
|
+
as `trace.probes[k]` beside `trace.t`: a species' concentration (`quantity: 'c'`, the default,
|
|
558
|
+
mol/m³) or species voltage (`'V'`), or $`\phi`$ itself (`'phi'`, no species), linearly between the
|
|
559
|
+
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
560
|
as a transient slows, so give `dtMax` to resolve a signal in time.
|
|
509
561
|
- `impedance(frequencies, { terminal, profiles })` solves the steady state, then linearises about
|
|
510
562
|
it: $`Z(f) = \delta V/\delta I`$ in Ω·m² at one terminal (`'right'` by default), with I into the
|
|
@@ -564,10 +616,12 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
|
|
|
564
616
|
| `gates.left/right` | charge on a gate or Stern plate, where the contact is capacitive |
|
|
565
617
|
| `ports[k]` | `{ name, V, flux: {name}, current }`: what each internal port brings into the device |
|
|
566
618
|
| `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)) |
|
|
619
|
+
| `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 |
|
|
567
620
|
| `charge` | total charge in the device, C/m² |
|
|
568
621
|
| `conservation` | per species stretch: amount, reference, intake through contacts, drift |
|
|
569
622
|
| `warnings` | e.g. unresolved double layers, conventions a statistics model relies on, and for a failed solve, where the system is nearly singular |
|
|
570
623
|
| `converged`, `iterations`, `steps`, `substeps`, `history`, `time` | solver bookkeeping |
|
|
624
|
+
| `steady` | whether this is a converged steady state (from `solve()`), not a transient's: what `check()` in the kit decides its checks by |
|
|
571
625
|
| `done`, `rejected`, `trace` | from `advance()`: whether `tEnd` was reached; rejected steps; `{ t, current, voltage }` per accepted step |
|
|
572
626
|
|
|
573
627
|
A stretch is a run of regions in which a species is present and connected. Its `drift`
|
package/docs/kit.md
CHANGED
|
@@ -169,6 +169,99 @@ const seen = run.trace.probes[0], k = seen.indexOf(Math.max(...seen));
|
|
|
169
169
|
console.log(`the pulse passes at ${(run.trace.t[k] * 1e6).toFixed(2)} µs, Δp ≈ ${units.toPerCm3(seen[k]).toExponential(1)} cm⁻³`);
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
+
## Liquid junctions
|
|
173
|
+
|
|
174
|
+
`henderson(left, right, ions, { T })` and `planck(left, right, ions, { T })` give the junction
|
|
175
|
+
potential $`\phi_R - \phi_L`$ (V) between two neutral solutions (mol/m³ by ion), with `ions` their
|
|
176
|
+
`{ z, D }` (the data library's `IONS` has that shape):
|
|
177
|
+
|
|
178
|
+
- **Henderson's** formula takes the junction to be the two solutions mixed in one proportion at
|
|
179
|
+
every point (so every ion's mixing fraction follows the same curve). It's
|
|
180
|
+
a closed form, exact for one salt, and what JPCalc and most electrophysiology corrections use.
|
|
181
|
+
- **Planck's** is the steady state of diffusion through a zone held between the two solutions,
|
|
182
|
+
electroneutral and carrying no current, solved by shooting on the Nernst–Planck equations. It's
|
|
183
|
+
exact for that junction, and equal to driftlet's own steady state between two baths.
|
|
184
|
+
|
|
185
|
+
A junction growing freely from first contact is neither: simulate it, as a transient from a
|
|
186
|
+
`c0` step. For one salt all three agree; for mixtures they differ by up to a millivolt or so.
|
|
187
|
+
Validated against JPCalc (Henderson), and LJPcalc and JLJP (stationary Nernst–Planck), in
|
|
188
|
+
test/junction.test.js.
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
import { henderson, planck, IONS } from 'driftlet/kit';
|
|
192
|
+
|
|
193
|
+
// 50 mM NaCl against 50 mM KCl, and a K-gluconate pipette (gluconate's D from its relative mobility, 0.33 of K⁺'s).
|
|
194
|
+
const ions = { ...IONS, 'gluconate-': { z: -1, D: 0.33 * IONS['K+'].D } };
|
|
195
|
+
console.log(1000 * henderson({ 'Na+': 50, 'Cl-': 50 }, { 'K+': 50, 'Cl-': 50 }, ions), 'mV');
|
|
196
|
+
const pipette = { 'K+': 140, 'gluconate-': 130, 'Cl-': 10 }, bath = { 'Na+': 145, 'K+': 5, 'Cl-': 150 };
|
|
197
|
+
console.log(`Henderson ${(1000 * henderson(pipette, bath, ions)).toFixed(2)} mV, Planck ${(1000 * planck(pipette, bath, ions)).toFixed(2)} mV`);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Recording a transient
|
|
201
|
+
|
|
202
|
+
`recorder(device, { every, times, probes })` runs a transient in pieces and keeps what a demo
|
|
203
|
+
needs to show it: `trace`, the terminal current and voltage (and the probes' readings) at every
|
|
204
|
+
accepted step from the start, joined up across calls; and `frames`, whole solutions to scrub back
|
|
205
|
+
through or draw side by side. Frames are kept at the start, then at each of `times` or every
|
|
206
|
+
`every` seconds, which the stepping lands on exactly; with neither, at the end of each call.
|
|
207
|
+
Its `advance(tEnd, opts)` takes `Device.advance()`'s options, so an animation calls it once per
|
|
208
|
+
frame with a `budgetMs` (`done` says whether it got there), and a run in budgets is step for
|
|
209
|
+
step the run made in one go. `frame(t)` is the frame at or just before t.
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
import { Device } from 'driftlet';
|
|
213
|
+
import { build, layer, bath, recorder } from 'driftlet/kit';
|
|
214
|
+
|
|
215
|
+
// A salt step relaxing: 100 mM KCl in from a bath, into 10 mM held behind a blocking wall.
|
|
216
|
+
const dev = new Device(
|
|
217
|
+
build({
|
|
218
|
+
species: [
|
|
219
|
+
{ name: 'K+', z: 1, cRef: 1000 },
|
|
220
|
+
{ name: 'Cl-', z: -1, cRef: 1000 },
|
|
221
|
+
],
|
|
222
|
+
materials: { water: { epsr: 0, species: { 'K+': { D: 1.96e-9, mu0: 0 }, 'Cl-': { D: 2.03e-9, mu0: 0 } } } },
|
|
223
|
+
stack: [bath({ 'K+': 100, 'Cl-': 100 }, 'Cl-'), layer('water', 100e-6, { c0: { 'K+': 10, 'Cl-': 10 } }), { species: { 'K+': 'blocked', 'Cl-': 'blocked' }, phi: 'neutral' }],
|
|
224
|
+
grid: { hmin: 2e-6, hmax: 2e-6 },
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
const rec = recorder(dev, { every: 1, probes: [{ x: 100e-6, species: 'K+' }] });
|
|
228
|
+
rec.advance(10, { tol: 1e-4 });
|
|
229
|
+
console.log(`${rec.frames.length} frames, ${rec.trace.t.length} steps; at the wall after 5 s: ${rec.frame(5).c['K+'].at(-1).toFixed(2)} mol/m³`);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Curated devices
|
|
233
|
+
|
|
234
|
+
Whole devices from a published parameter set, with reference results from an independent code
|
|
235
|
+
to check a run against, so a demo starts from numbers that someone has measured or
|
|
236
|
+
benchmarked rather than invented ones. Each returns an ordinary definition (print it to see
|
|
237
|
+
every parameter, or edit it).
|
|
238
|
+
|
|
239
|
+
- **`perovskiteCell({ V, light, bulkSRH })`**: IonMonger's default planar perovskite solar
|
|
240
|
+
cell (Courtier et al., J. Comput. Electron. 18, 1435 (2019)): TiO₂ (100 nm) | MAPbI₃ (400 nm)
|
|
241
|
+
with mobile iodide vacancies over an equal immobile background | spiro-OMeTAD (200 nm), at
|
|
242
|
+
298 K, lit through the TiO₂ (1.4e21 photons/(m²·s) at α = 1.3e7 /m, times `light`). SRH at
|
|
243
|
+
both faces and, unless `bulkSRH: false`, in the perovskite. `V` drives the spiro side's contact,
|
|
244
|
+
so photocurrent is positive. `PEROVSKITE_SCANS` holds IonMonger's own J–V scans of it (steady
|
|
245
|
+
at 1.2 V under light, then 1.2 → 0 → 1.2 V, at seven rates from 1 mV/s to 1 kV/s): per scan
|
|
246
|
+
the current at 0 V, each sweep's maximum power and V_oc, and the hysteresis index, with bulk
|
|
247
|
+
SRH (`full`) and without (`noBulkSRH`). driftlet's test suite reproduces them (and the whole
|
|
248
|
+
loops, to 0.03 mA/cm² where they're gentle and 1 mV where steep). `hysteresis(trace, turn)`
|
|
249
|
+
reads the same figures from a run's trace.
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
import { Device } from 'driftlet';
|
|
253
|
+
import { perovskiteCell, PEROVSKITE_SCANS, hysteresis, recorder } from 'driftlet/kit';
|
|
254
|
+
|
|
255
|
+
const rate = 10, half = 1.2 / rate; // V/s
|
|
256
|
+
const dev = new Device(perovskiteCell({ V: 1.2 }));
|
|
257
|
+
dev.solve();
|
|
258
|
+
dev.set({ contacts: { right: { V: { t: [0, half, 2 * half], values: [1.2, 0, 1.2] } } } });
|
|
259
|
+
const rec = recorder(dev);
|
|
260
|
+
rec.advance(2 * half, { tol: 1e-4, dtMax: (2 * half) / 480 });
|
|
261
|
+
const ours = hysteresis(rec.trace, half), theirs = PEROVSKITE_SCANS.full.find((s) => s.rate === rate);
|
|
262
|
+
console.log(`P_max ${ours.rev.Pmax.toFixed(1)} W/m² going down (IonMonger ${theirs.rev.Pmax}), hysteresis index ${ours.hi.toFixed(4)} (${theirs.hi})`);
|
|
263
|
+
```
|
|
264
|
+
|
|
172
265
|
## Plotting
|
|
173
266
|
|
|
174
267
|
Solutions are plain arrays, ready for any plotting toolkit. Two helpers save the bookkeeping of
|
|
@@ -263,6 +356,60 @@ const def = build({
|
|
|
263
356
|
console.log(describe(def)); // … warnings: materials.Si.species.e-.D: 36 m²/s is beyond any real diffusivity …
|
|
264
357
|
```
|
|
265
358
|
|
|
359
|
+
## Checking a result
|
|
360
|
+
|
|
361
|
+
`check(device, sol, { refine, tol })` checks a solved device, and says what it compared, so a
|
|
362
|
+
page can show that its numbers hold up. Pass the solution `solve()` or `advance()` returned.
|
|
363
|
+
|
|
364
|
+
- **converged**: the solve converged.
|
|
365
|
+
- **warnings**: the solution's warnings (unresolved double layers, steep profiles, a weakly held
|
|
366
|
+
part) and the definition's likely unit slips. They're heuristics, so they're marked `?`, a
|
|
367
|
+
prompt to look, and don't fail the check.
|
|
368
|
+
- **balance**, in a steady state: each species' ledger, what comes in through each terminal and
|
|
369
|
+
what each reaction (bulk, or at a face) makes or uses, in mol/(m²·s) and for a charged species
|
|
370
|
+
as a current. The terms must sum to zero. A species that a face lets through only by a law (a
|
|
371
|
+
membrane's permeability, a conductance) or by a reaction has a ledger on each side, with what
|
|
372
|
+
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
|
|
373
|
+
every carrier went: collected, or recombined in the bulk or at which face. A species at rest
|
|
374
|
+
(in equilibrium, flows only of round-off against what it could carry) isn't listed.
|
|
375
|
+
- **conservation**, in a transient: every closed, unreacting stretch keeps what it held plus
|
|
376
|
+
what came in.
|
|
377
|
+
- **grid**, in a steady state (unless `refine: false`): the device solved again on a grid twice
|
|
378
|
+
as fine everywhere (half of each `hmin` and `hmax`, the square root of each `ratio`), starting
|
|
379
|
+
from this solution interpolated onto it. The current, each floating terminal's voltage (an open-circuit voltage) and each
|
|
380
|
+
region's charge must agree to `tol` (relative; default 1e-2, about what a plot shows; a
|
|
381
|
+
benchmark wants 1e-3 or less). The discretisation is second order, so the change estimates
|
|
382
|
+
this grid's own error (about ¾ of it). It costs one more solve, so run it once, not on every
|
|
383
|
+
slider move.
|
|
384
|
+
|
|
385
|
+
It returns `{ ok, items, text }`: `ok` when nothing failed, an item per check
|
|
386
|
+
(`{ name, ok, summary, details }`, with `ok: null` for a prompt to look), and `text`, a line
|
|
387
|
+
per check marked `ok`, `FAIL` or `?`, ready to log or put on the page.
|
|
388
|
+
|
|
389
|
+
```js
|
|
390
|
+
import { Device, units } from 'driftlet';
|
|
391
|
+
import { build, layer, ohmic, semiconductor, photogeneration, check } from 'driftlet/kit';
|
|
392
|
+
|
|
393
|
+
// An n⁺p silicon cell under blue-green light (α = 1e5 /m), at 0.5 V.
|
|
394
|
+
const W = units.um(60.5);
|
|
395
|
+
const dev = new Device(
|
|
396
|
+
build({
|
|
397
|
+
T: 300,
|
|
398
|
+
library: [semiconductor('Si')],
|
|
399
|
+
stack: [ohmic(0), layer('Si', units.um(0.5), { donors: units.perCm3(1e19) }), layer('Si', units.um(60), { acceptors: units.perCm3(1e16) }), ohmic(0.5)],
|
|
400
|
+
bulkReactions: [{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }, photogeneration({ material: 'Si', flux: 3e-3, alpha: 1e5, mu: units.eV(3), to: W })],
|
|
401
|
+
grid: { hmin: units.nm(1), hmax: units.um(1) },
|
|
402
|
+
}),
|
|
403
|
+
);
|
|
404
|
+
const report = check(dev, dev.solve());
|
|
405
|
+
console.log(report.text);
|
|
406
|
+
// ok balance: 2 ledgers sum to zero …
|
|
407
|
+
// e-: photon = e- + h+: +2.99e-3 (289 A/m²); left contact: -2.23e-3 (-215 A/m²); …
|
|
408
|
+
// FAIL grid: on a grid twice as fine (172 → 339 nodes), the largest change is region 0 (Si)
|
|
409
|
+
// charge, … (0.021, over 0.01: refine the grid …): the current is fine (1e-4), but the
|
|
410
|
+
// emitter's depletion charge is 2 % off on this grid; hmin: units.nm(0.25) fixes it.
|
|
411
|
+
```
|
|
412
|
+
|
|
266
413
|
## Live demos
|
|
267
414
|
|
|
268
415
|
`live(def, opts)` wraps a device for sliders. Call `set(patch)` as often as a control moves
|
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.7.0/src/index.js';
|
|
18
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.7.0/src/kit.js';
|
|
19
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.7.0/src/plot.js';
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
@@ -40,13 +40,18 @@ From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
|
40
40
|
next to a dielectric needs its φ law. Every species that doesn't reach a contact needs its
|
|
41
41
|
initial amount (`c0`). An error names the path and says what to give.
|
|
42
42
|
- **Check before trusting.** `sol.converged` must be true, and read `sol.warnings`.
|
|
43
|
-
`describe(def)` (kit) summarises a definition and flags likely unit slips.
|
|
43
|
+
`describe(def)` (kit) summarises a definition and flags likely unit slips. Once a device
|
|
44
|
+
works, run `check(dev, sol)` (kit) and show or log its `text`: each species' ledger (what
|
|
45
|
+
every terminal and reaction brings in or uses, summing to zero: J = F(G − R) under light),
|
|
46
|
+
conservation in transients, and the same device on a grid twice as fine (a FAIL means the
|
|
47
|
+
answer depends on the grid: refine it). It costs a solve, so run it once, not per frame.
|
|
44
48
|
- **Exports.** `driftlet`: `Device`, `units`, and the constants `FARADAY`, `GAS_CONSTANT`,
|
|
45
49
|
`EPS0`, `BOLTZMANN`, `ELEMENTARY_CHARGE`, `AVOGADRO` (there's no thermal-voltage constant:
|
|
46
50
|
V_T = GAS_CONSTANT·T/FARADAY). `driftlet/kit`: `build`, `layer`, `ohmic`, `bath`, `half`,
|
|
47
51
|
`level`, `SHE`, `describe`, `live`, `traces`, the sources (`pulse`, `square`, `triangle`, `ramp`,
|
|
48
52
|
`injector`, `recombination`, `photogeneration`), the data library (`aqueous`, `semiconductor`,
|
|
49
|
-
`metal`, `IONS`, …)
|
|
53
|
+
`metal`, `IONS`, …), `check` (a solution's checks), `henderson` and `planck` (liquid-junction potentials), `recorder` (a transient's trace and frames across calls), the curated
|
|
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`.
|
|
50
55
|
- **Reactions** are written as equations, `{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }`, in
|
|
51
56
|
`new Device`, `build()` and `set()` alike. A coefficient is a number and a space (`2 e-`).
|
|
52
57
|
- **Rate constants carry units.** A bulk reaction's rate is `kf · Π c_reactants` in mol/(m³·s),
|
|
@@ -155,6 +160,57 @@ const oc = new Device(cell({ I: 0 })).solve(); // open circuit
|
|
|
155
160
|
console.log(`J_sc = ${sc.current.toFixed(1)} A/m², V_oc = ${oc.terminals.right.V.toFixed(3)} V`);
|
|
156
161
|
```
|
|
157
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
|
+
|
|
158
214
|
## Template: an electrode in a stirred solution
|
|
159
215
|
|
|
160
216
|
One working electrode, a stagnant layer of solution, and the well-stirred bulk as a bath. The
|
|
@@ -217,7 +273,8 @@ const def = build({
|
|
|
217
273
|
const dev = new Device(def);
|
|
218
274
|
const rest = dev.solve(); // the steady state at t = 0 (solve() doesn't advance time)
|
|
219
275
|
// In a page, call advance(tNext, { budgetMs: 12, probes }) once per animation frame, carrying on
|
|
220
|
-
// next frame while sol.done is false
|
|
276
|
+
// next frame while sol.done is false (or let recorder() join the traces and keep frames to scrub
|
|
277
|
+
// back through). Probes read at accepted steps; dtMax keeps them close.
|
|
221
278
|
const run = dev.advance(10e-6, { dtMax: 20e-9, probes: [{ x: 1.525e-3, species: 'h+' }] });
|
|
222
279
|
const seen = run.trace.probes[0], k = seen.indexOf(Math.max(...seen));
|
|
223
280
|
const tArrive = run.trace.t[k] - width / 2; // from the pulse's middle
|
|
@@ -227,18 +284,32 @@ console.log(`peak at ${(run.trace.t[k] * 1e6).toFixed(2)} µs, Δp ${units.toPer
|
|
|
227
284
|
To start from a state instead of simulating how it got there (a packet already injected, a
|
|
228
285
|
salt gradient laid down), give a region's `c0` a profile against the device's x:
|
|
229
286
|
`c0: { 'h+': { x: [...], values: [...] } }`, piecewise linear. A given `c0` is the starting state,
|
|
230
|
-
even for a species a contact feeds; species without one start at their contact's level
|
|
231
|
-
|
|
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
|
|
232
290
|
state.
|
|
233
291
|
|
|
234
292
|
Transients cost more where a carrier's density spans many decades (minority holes in silicon
|
|
235
293
|
ahead of the pulse) and at strong injection (Δp near n₀); budget for that, or coarsen the grid.
|
|
236
294
|
|
|
295
|
+
`recorder(dev, { every, times, probes })` (kit) runs a transient in pieces: its `advance(tEnd,
|
|
296
|
+
opts)` takes the same options, `rec.trace` is the whole run's trace from the start, and
|
|
297
|
+
`rec.frames` are whole solutions (at the start, then at `times`, every `every` s, or the end of
|
|
298
|
+
each call), with `rec.frame(t)` the one at or before t, for a time slider.
|
|
299
|
+
|
|
300
|
+
**Prefer a curated device to inventing numbers** where one fits: `perovskiteCell({ V, light,
|
|
301
|
+
bulkSRH })` is IonMonger's default perovskite solar cell (mobile iodide vacancies, SRH in the bulk
|
|
302
|
+
and at both faces), and `PEROVSKITE_SCANS` holds IonMonger's J–V scans of it at seven rates;
|
|
303
|
+
`hysteresis(rec.trace, turn)` reads the same figures from yours, so a demo can show its agreement.
|
|
304
|
+
The definition is plain data: print it, edit what your demo varies.
|
|
305
|
+
|
|
237
306
|
## Template: a live page with a slider
|
|
238
307
|
|
|
239
308
|
`live()` merges slider moves made during a solve, warm-starts each solve and keeps the last
|
|
240
309
|
good solution. `bandDiagram()` returns an SVG string. Pass `worker: true` to `live()` to solve in
|
|
241
|
-
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.
|
|
242
313
|
|
|
243
314
|
```html
|
|
244
315
|
<!doctype html>
|
|
@@ -248,9 +319,9 @@ a Web Worker.
|
|
|
248
319
|
<span id="readout"></span>
|
|
249
320
|
<div id="figure"></div>
|
|
250
321
|
<script type="module">
|
|
251
|
-
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
252
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
253
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
322
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.7.0/src/index.js';
|
|
323
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.7.0/src/kit.js';
|
|
324
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.7.0/src/plot.js';
|
|
254
325
|
|
|
255
326
|
const def = build({
|
|
256
327
|
T: 300,
|
|
@@ -275,6 +346,16 @@ slider.addEventListener('input', () => dev.set({ contacts: { right: { V: +slider
|
|
|
275
346
|
What `bandDiagram()` and `traces()` draw, and how to explain it to a reader (docs/visualization.md
|
|
276
347
|
has the long form):
|
|
277
348
|
|
|
349
|
+
- **Plotting in your own style.** `bandDiagram()` is a quick look. For a page of your own, draw
|
|
350
|
+
with your own charting code from `traces(sol, opts)` (in `driftlet/kit`, the same options):
|
|
351
|
+
`{ x, series: [{ id, label, kind, role, slot, y }], regions, faces, range }`, where `kind` says how
|
|
352
|
+
a line reads (`'level'` thick, `'standard'` thin, `'redox'`, `'phi'`), `role` and `slot` keep a
|
|
353
|
+
species' colour stable (`themeColor(role, slot)` in `driftlet/plot` gives ours), labels mark
|
|
354
|
+
subscripts TeX-style (`labelParts(label)` splits them), `NaN` breaks a line, and doubled
|
|
355
|
+
interface nodes share an x (draw them as vertical steps). Or read the solution directly:
|
|
356
|
+
`sol.x`, `sol.V[name]`, `sol.Vstd[name]`, `sol.c[name]`, `sol.phi`; electron energies are
|
|
357
|
+
E = −V (eV) for both carriers' levels and band edges.
|
|
358
|
+
|
|
278
359
|
- **Which picture.** For a device of electrons and holes only, draw the familiar band diagram,
|
|
279
360
|
energy up: `bandDiagram(sol, { energy: true })` (E_c, E_v, E_Fn, E_Fp in eV). The species
|
|
280
361
|
voltages below are the same lines in volts, turned over; use them where ions share the diagram,
|
|
@@ -357,11 +438,15 @@ has the long form):
|
|
|
357
438
|
- **A steady solve of a closed cell where every ion can cross.** Its steady state is complete
|
|
358
439
|
mixing. To watch a junction or a salt bridge, step in time from the unmixed `c0` (`advance`).
|
|
359
440
|
- **Everything floating.** At least one terminal must be held at a voltage `V`.
|
|
360
|
-
- **The library's
|
|
441
|
+
- **The library's data are for 25 °C** (its semiconductors for 300 K: give `T: 300`). At body
|
|
442
|
+
temperature, `T: 310.15` with the same μ° and D is a fair start; D rises about 2% per kelvin
|
|
443
|
+
in water.
|
|
361
444
|
- **Sign of the current.** `sol.current` is positive toward +x: a forward-biased diode with its
|
|
362
445
|
p side on the right carries negative current. A terminal's current is into the device.
|
|
363
|
-
- **
|
|
364
|
-
|
|
446
|
+
- **A membrane potential read from the terminals.** Physiology's V_m is φ_inside − φ_outside:
|
|
447
|
+
read it from `sol.phi` (or a `{ x, quantity: 'phi' }` probe in a transient), with the inside on
|
|
448
|
+
the right. `sol.terminalVoltage` (and a trace's `voltage`) is V_right − V_left of the contacts,
|
|
449
|
+
which at baths are their reference species' levels: it differs from Δφ by that ion's Nernst term.
|
|
365
450
|
- **A coefficient without its space.** `'2e- + …'` reads `2e-` as a species name; the error says
|
|
366
451
|
to write `2 e-`.
|
|
367
452
|
- **Changing c0 with `set()`.** A carried state keeps what it holds; `c0` is only the starting
|