driftlet 0.1.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/LICENSE +14 -0
- package/README.md +285 -0
- package/llms.txt +299 -0
- package/package.json +54 -0
- package/src/bernoulli.js +29 -0
- package/src/blockTridiagonal.js +492 -0
- package/src/constants.js +36 -0
- package/src/data.js +127 -0
- package/src/describe.js +190 -0
- package/src/device.js +954 -0
- package/src/equation.js +188 -0
- package/src/errors.js +8 -0
- package/src/fermi.js +106 -0
- package/src/grid.js +111 -0
- package/src/index.js +207 -0
- package/src/kit.js +76 -0
- package/src/live.js +217 -0
- package/src/merge.js +11 -0
- package/src/plot.js +267 -0
- package/src/solution.js +246 -0
- package/src/solver.js +2884 -0
- package/src/stack.js +172 -0
- package/src/statistics.js +593 -0
- package/src/traces.js +125 -0
- package/src/types.js +221 -0
- package/src/worker.js +24 -0
- package/types/bernoulli.d.ts +4 -0
- package/types/blockTridiagonal.d.ts +92 -0
- package/types/constants.d.ts +33 -0
- package/types/data.d.ts +73 -0
- package/types/describe.d.ts +15 -0
- package/types/device.d.ts +20 -0
- package/types/equation.d.ts +102 -0
- package/types/errors.d.ts +3 -0
- package/types/fermi.d.ts +3 -0
- package/types/grid.d.ts +28 -0
- package/types/index.d.ts +103 -0
- package/types/kit.d.ts +61 -0
- package/types/live.d.ts +101 -0
- package/types/merge.d.ts +1 -0
- package/types/plot.d.ts +60 -0
- package/types/solution.d.ts +18 -0
- package/types/solver.d.ts +342 -0
- package/types/stack.d.ts +59 -0
- package/types/statistics.d.ts +10 -0
- package/types/traces.d.ts +118 -0
- package/types/types.d.ts +574 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
BSD Zero Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Lundeberg
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
9
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
10
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
11
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
12
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
13
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
14
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# driftlet
|
|
2
|
+
|
|
3
|
+
[](https://github.com/markblundeberg/driftlet/actions/workflows/test.yml)
|
|
4
|
+
|
|
5
|
+
**driftlet solves 1D drift–diffusion–reaction problems properly**: any mix of charged and
|
|
6
|
+
neutral species, Poisson electrostatics (or strict neutrality), bulk and interfacial reactions,
|
|
7
|
+
heterointerfaces, metals, and external circuits, in steady state, in time, and as small-signal
|
|
8
|
+
impedance. It's validated against analytic results, and small enough to run live in a web page:
|
|
9
|
+
pure JavaScript, no dependencies, millisecond solves.
|
|
10
|
+
|
|
11
|
+
**Status: pre-release.** Not yet on npm, and the API may still change.
|
|
12
|
+
|
|
13
|
+
## Is it for your problem?
|
|
14
|
+
|
|
15
|
+
The same equations go by different names in different fields. If your problem is one-dimensional
|
|
16
|
+
(planar), it's probably here:
|
|
17
|
+
|
|
18
|
+
| If you work on | you may call it | start from | checked against |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| Semiconductor devices | drift–diffusion, van Roosbroeck, quasi-Fermi levels, Scharfetter–Gummel; pn, Schottky, MOS, heterojunctions | [first example](#a-semiconductor-junction), [pn demo](https://markblundeberg.github.io/driftlet/demos/pn.html), [`contacts`](test/contacts.test.js), [`metal`](test/metal.test.js) tests | exact built-in potential, Shockley J–V, depletion charge, MOS C–V |
|
|
21
|
+
| Solar cells | photogeneration, radiative and SRH recombination | [`reactions`](test/reactions.test.js) test: generation is a reaction from a photon reservoir (uniform per material), SRH runs through explicit trap species | `J_sc = qG(L_n + L_p + W)`; Shockley J–V in the dark |
|
|
22
|
+
| Electrochemistry | Nernst–Planck, concentration polarization, limiting current, Butler–Volmer, Warburg, cyclic voltammetry | [second example](#an-electrochemical-cell), [saturation](https://markblundeberg.github.io/driftlet/demos/saturation.html) and [impedance](https://markblundeberg.github.io/driftlet/demos/impedance.html) demos, [`circuit`](test/circuit.test.js), [`kinetics`](test/kinetics.test.js), [`impedance`](test/impedance.test.js) tests | `i_lim·tanh(V/4V_T)`, Butler–Volmer closed form, finite-length Warburg |
|
|
23
|
+
| 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 |
|
|
24
|
+
| 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 |
|
|
25
|
+
| Membranes, desalination | Donnan, ion exchange, liquid junctions, water dissociation | [`equilibrium`](test/equilibrium.test.js), [`neutral`](test/neutral.test.js) tests | Donnan partition, Planck EMF |
|
|
26
|
+
| 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 |
|
|
27
|
+
| Biophysics | Poisson–Nernst–Planck, ion channels, membrane potentials | the electrochemistry setups, with fixed charge in the channel region | no dedicated check yet |
|
|
28
|
+
|
|
29
|
+
The tests are worked setups, each with its analytic check, so they double as recipes. The
|
|
30
|
+
[validation table](#validation) lists them all.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install driftlet
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```js nocheck
|
|
39
|
+
import { Device, units } from 'driftlet'; // the solver
|
|
40
|
+
import { build, layer, ohmic } from 'driftlet/kit'; // helpers that write definitions, data, live demos
|
|
41
|
+
import { bandDiagram } from 'driftlet/plot'; // a level diagram as an SVG string
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or straight from a CDN in a page, pinned to a version:
|
|
45
|
+
`https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
|
|
46
|
+
TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
|
|
47
|
+
[`llms.txt`](llms.txt).
|
|
48
|
+
|
|
49
|
+
## A semiconductor junction
|
|
50
|
+
|
|
51
|
+
A silicon pn junction with ohmic contacts, at equilibrium and then under forward bias. A device
|
|
52
|
+
is plain data; `build()` from `driftlet/kit` writes it from the stack as it's drawn, with a
|
|
53
|
+
[data library](docs/data.md) for the material:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import { Device, units } from 'driftlet';
|
|
57
|
+
import { build, layer, ohmic, semiconductor } from 'driftlet/kit';
|
|
58
|
+
|
|
59
|
+
const dev = new Device(
|
|
60
|
+
build({
|
|
61
|
+
T: 300,
|
|
62
|
+
library: [semiconductor('Si')], // e⁻ and h⁺ with Sze's band data at 300 K
|
|
63
|
+
stack: [
|
|
64
|
+
ohmic(0), // e⁻ and h⁺ in equilibrium with a metal, the terminal at 0 V
|
|
65
|
+
layer('Si', units.um(2), { name: 'n', donors: units.perCm3(1e17) }),
|
|
66
|
+
layer('Si', units.um(2), { name: 'p', acceptors: units.perCm3(1e16) }),
|
|
67
|
+
ohmic(0),
|
|
68
|
+
],
|
|
69
|
+
grid: { hmin: units.nm(0.5), hmax: units.nm(20) },
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const eq = dev.solve();
|
|
74
|
+
console.log('built-in potential (V):', eq.phi[0] - eq.phi[eq.phi.length - 1]); // ≈ 0.795
|
|
75
|
+
|
|
76
|
+
dev.set({ contacts: { right: { V: 0.4 } } }); // p side at +0.4 V: forward bias
|
|
77
|
+
const on = dev.solve(); // warm-started from the previous solution
|
|
78
|
+
console.log('current (A/m²):', on.current); // negative: it flows toward −x, from p to n
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## An electrochemical cell
|
|
82
|
+
|
|
83
|
+
Silver nitrate between two silver electrodes, as a macroscopic (strictly neutral) electrolyte.
|
|
84
|
+
A reversible electrode is a contact whose terminal species is the ion it exchanges: Ag⁺ in
|
|
85
|
+
equilibrium with the metal, NO₃⁻ blocked. Polarized, the cell's current saturates at the
|
|
86
|
+
limiting current, `i_lim = 4FD₊c₀/L` for this binary salt:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
import { Device, FARADAY } from 'driftlet';
|
|
90
|
+
import { build, layer, aqueous } from 'driftlet/kit';
|
|
91
|
+
|
|
92
|
+
const L = 100e-6, c0 = 10; // m, mol/m³ (10 mM)
|
|
93
|
+
// Ag⁺ + e⁻ ⇌ Ag at the metal: V is the voltage of Ag⁺ there, i.e. the electrode potential.
|
|
94
|
+
const silver = (V) => ({ V, terminal: 'Ag+', species: { 'Ag+': 'equilibrium', 'NO3-': 'blocked' }, phi: 'bulk' });
|
|
95
|
+
const dev = new Device(
|
|
96
|
+
build({
|
|
97
|
+
library: [aqueous(['Ag+', 'NO3-'], { epsr: 0 })], // epsr 0: no double layers resolved
|
|
98
|
+
// Ag⁺ reaches the contacts and takes their level; NO₃⁻ doesn't, so it needs its amount.
|
|
99
|
+
stack: [silver(0), layer('water', L, { c0: { 'NO3-': c0 } }), silver(0)],
|
|
100
|
+
grid: { hmin: 10e-9, hmax: 2e-6 }, // graded: fine where Ag⁺ depletes at the electrode
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
const iLim = (4 * FARADAY * 1.648e-9 * c0) / L;
|
|
104
|
+
for (const V of [0.05, 0.1, 0.2, 0.4]) {
|
|
105
|
+
dev.set({ contacts: { right: { V } } });
|
|
106
|
+
const sol = dev.solve();
|
|
107
|
+
// V > 0 raises Ag⁺'s level on the right: silver dissolves there and plates on the left, so the
|
|
108
|
+
// current flows toward −x.
|
|
109
|
+
console.log(`${V} V: I/i_lim = ${(-sol.current / iLim).toFixed(4)}`); // ≈ tanh(V/4V_T): 0.451, 0.750, 0.961, 0.999
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Solutions are plain objects of `Float64Array`s, ready to plot (fresh arrays from every solve):
|
|
114
|
+
`x`, `phi`, and per species `c`, `mu` (electrochemical potential μ̄), `muStd` (standard level)
|
|
115
|
+
and the voltage views `V`, `Vstd`. They also carry the terminals' voltages and currents, fluxes,
|
|
116
|
+
conservation bookkeeping and warnings; see [the device reference](docs/device.md). The
|
|
117
|
+
[kit](docs/kit.md) adds reactions written as equations (`'Ag+ + e- = Ag(s)'`), redox levels,
|
|
118
|
+
level diagrams, slider demos and `describe()`, which flags likely unit slips.
|
|
119
|
+
|
|
120
|
+
## Demos
|
|
121
|
+
|
|
122
|
+
Live pages in [`demos/`](demos/), each building its device with `driftlet/kit`, solving it in the
|
|
123
|
+
browser as you move the controls, and showing its whole script as a worked example. Most draw the
|
|
124
|
+
closed-form theory alongside, as a check. They're live at
|
|
125
|
+
[markblundeberg.github.io/driftlet/demos](https://markblundeberg.github.io/driftlet/demos/); to run
|
|
126
|
+
them locally, serve the repository root (e.g. `python3 -m http.server`) and open `/demos/`.
|
|
127
|
+
|
|
128
|
+
| | | |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| [](https://markblundeberg.github.io/driftlet/demos/pn.html) | [](https://markblundeberg.github.io/driftlet/demos/solar.html) | [](https://markblundeberg.github.io/driftlet/demos/mos.html) |
|
|
131
|
+
| **pn junction**: quasi-Fermi levels under bias; I–V against Shockley | **Solar cell**: J<sub>sc</sub> and V<sub>oc</sub> against collection theory | **MOS capacitor**: band bending; C–V against ideal theory |
|
|
132
|
+
| [](https://markblundeberg.github.io/driftlet/demos/redox.html) | [](https://markblundeberg.github.io/driftlet/demos/daniell.html) | [](https://markblundeberg.github.io/driftlet/demos/saturation.html) |
|
|
133
|
+
| **Cyclic voltammetry**: a Fermi level against a redox level | **Daniell cell**: a real salt bridge, leaking, ion by ion | **Saturation**: Ag \| AgNO₃ \| Ag under a square wave, up to its limiting current |
|
|
134
|
+
| [](https://markblundeberg.github.io/driftlet/demos/impedance.html) | | |
|
|
135
|
+
| **Impedance**: the Warburg arc, and the cell's small-signal response inside | | |
|
|
136
|
+
| [](https://markblundeberg.github.io/driftlet/demos/membrane.html) | [](https://markblundeberg.github.io/driftlet/demos/double-layer.html) | [](https://markblundeberg.github.io/driftlet/demos/insertion.html) |
|
|
137
|
+
| **Ion-exchange membrane**: Donnan steps, ion by ion, against TMS theory | **Double layer**: dilute vs crowded ions, against closed forms | **Intercalation host**: cycling between cutoffs against the OCV |
|
|
138
|
+
|
|
139
|
+
## How to think about it
|
|
140
|
+
|
|
141
|
+
driftlet insists on thermodynamically honest concepts ([conventions](docs/conventions.md)):
|
|
142
|
+
|
|
143
|
+
- **Everything you set or measure is an electrochemical potential μ̄** (or a difference of
|
|
144
|
+
them). A bias sets Δμ̄ of electrons between terminals; a gate sets its metal's `μ̄_e⁻`. Nothing
|
|
145
|
+
you set is an electrostatic potential.
|
|
146
|
+
- **φ is bookkeeping.** Each material's standard chemical potentials anchor its own φ, and φ
|
|
147
|
+
jumps at every interface between different materials.
|
|
148
|
+
- **Band alignment is a property of each interface** and must be given explicitly: nothing is
|
|
149
|
+
assumed by default. If vacuum-level estimates are all you have, helpers in `driftlet/kit`
|
|
150
|
+
take an anchor and an offset per side (electron affinity, work function, absolute SHE
|
|
151
|
+
potential, …) and return the alignment that lines up the vacuum levels. The
|
|
152
|
+
[alignment guide](docs/alignment.md) covers the recipe and its caveats.
|
|
153
|
+
- The species voltage `V_i = μ̄_i / (z_i F)` and standard level `V°_i` are available as views,
|
|
154
|
+
alongside redox levels: [reading level diagrams](docs/visualization.md) explains them.
|
|
155
|
+
|
|
156
|
+
A device is a line of **regions** (each a **material** plus a length, fixed charge and
|
|
157
|
+
initial composition), joined at **interfaces**, with a **contact** at each end. The contacts
|
|
158
|
+
and any internal **ports** are its **terminals**, each held at a voltage or driven by a
|
|
159
|
+
current. Supported physics:
|
|
160
|
+
|
|
161
|
+
- any mix of charged and neutral species; per-material diffusivities and standard potentials;
|
|
162
|
+
species absent from some materials;
|
|
163
|
+
- ideal statistics by default, or per material: Fermi–Dirac (degenerate carriers), lattice
|
|
164
|
+
gas (crowding, site filling), Redlich–Kister, Debye–Hückel, intercalation hosts described by
|
|
165
|
+
their OCV curve, or your own function ([statistics](docs/statistics.md));
|
|
166
|
+
- Poisson electrostatics, or strictly neutral (ε = 0) materials such as macroscopic
|
|
167
|
+
electrolytes;
|
|
168
|
+
- conductor regions (metals, fast ion conductors) with only their carrier's level and a
|
|
169
|
+
conductivity: Schottky and MOS gates as regions, electrodes with reactions at their faces,
|
|
170
|
+
bipolar electrodes;
|
|
171
|
+
- contacts as outside phases with known levels, joined by the same laws as internal faces:
|
|
172
|
+
ohmic contacts, reversible electrodes, baths, pinned barriers, conductance links, gates and
|
|
173
|
+
Stern layers;
|
|
174
|
+
- interfaces with explicit alignment, blocking, interface resistance, and Butler–Volmer
|
|
175
|
+
reactions with participants on either side (electrode reactions, ion and electron transfer,
|
|
176
|
+
mixed potentials), and a choice of electrostatic law (pinned, neutral, Helmholtz);
|
|
177
|
+
- bulk reactions with thermodynamically consistent mass action (recombination, water
|
|
178
|
+
autoionisation, …);
|
|
179
|
+
- imposed flow (advection) and current-free eddy mixing;
|
|
180
|
+
- internal ports: reservoirs feeding a window of nodes, e.g. grounding a MOS channel;
|
|
181
|
+
- terminals held at a voltage, driven by a current (including open circuit) or behind a
|
|
182
|
+
resistance, with piecewise-linear waveforms (cyclic voltammetry); reference electrodes and
|
|
183
|
+
other internal terminals as ports; impedance at any terminal;
|
|
184
|
+
- steady states; transients by backward Euler, BDF2, or adaptive BDF2 with error control and
|
|
185
|
+
frame budgets for animation; exact conservation bookkeeping;
|
|
186
|
+
- small-signal impedance spectra Z(f), with complex profiles.
|
|
187
|
+
|
|
188
|
+
## What it doesn't do
|
|
189
|
+
|
|
190
|
+
- **More than one dimension.** Planar 1D only, so no porous-electrode (P2D) models. For 2D/3D,
|
|
191
|
+
see [ChargeTransport.jl](https://github.com/WIAS-PDELib/ChargeTransport.jl), TCAD tools or
|
|
192
|
+
COMSOL.
|
|
193
|
+
- **Cross-diffusion.** Each species moves down its own μ̄ with its own D (the statistics can be
|
|
194
|
+
non-ideal); there are no Stefan–Maxwell or Onsager cross terms beyond eddy mixing.
|
|
195
|
+
- **Heat and optics.** Temperature is uniform. Generation is a reaction, uniform within a
|
|
196
|
+
material; there's no optical model or absorption profile. For those, see
|
|
197
|
+
[Driftfusion](https://github.com/barnesgroupICL/Driftfusion),
|
|
198
|
+
[SIMsalabim](https://github.com/kostergroup/SIMsalabim) or
|
|
199
|
+
[PyBaMM](https://github.com/pybamm-team/PyBaMM).
|
|
200
|
+
- **Field-dependent transport.** No high-field mobility, impact ionisation, field-enhanced
|
|
201
|
+
dissociation (second Wien effect) or tunnelling.
|
|
202
|
+
- **Fitting.** It solves forward problems; parameter estimation is up to you.
|
|
203
|
+
|
|
204
|
+
How it works numerically is in [numerics](docs/numerics.md).
|
|
205
|
+
|
|
206
|
+
## Validation
|
|
207
|
+
|
|
208
|
+
Every physics feature is tested against analytic results (`npm test`, node's built-in runner).
|
|
209
|
+
|
|
210
|
+
| What | Checked against | Tolerance |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| Equilibrium of any device | μ̄ of every species flat; zero current | 1e-9 RT or better |
|
|
213
|
+
| Gouy–Chapman double layer | analytic charge and full nonlinear profile | 2e-3 |
|
|
214
|
+
| Debye screening (linear limit) | gate in series with `ε/λ_D` | 1e-3 |
|
|
215
|
+
| Donnan potential | analytic partition, between floating layers and real baths | 1e-8 |
|
|
216
|
+
| Heterointerfaces with unequal dipoles (A \| B \| A) | per-face analytic double-layer split | 3e-3 |
|
|
217
|
+
| Floating island | Gauss's law; conserved amounts through gate sweeps | 1e-12 |
|
|
218
|
+
| pn junction | exact built-in potential; depletion charge; short-diode J–V | 1e-12; 1%; 2e-3 |
|
|
219
|
+
| Long pn diode with recombination | Shockley J–V incl. depletion recombination | 2e-3 |
|
|
220
|
+
| Illuminated long pn diode | `J_sc = qG(L_n + L_p + W)` from a cold start; superposition at low injection | 5e-3; 1e-2 |
|
|
221
|
+
| Schottky barrier (metal region \| n-Si) | surface density from the alignment; depletion charge | 5e-3; 2% |
|
|
222
|
+
| Liquid junction, open circuit | cell EMF 2t₊(RT/F) ln(c₁/c₂); Planck diffusion potential | 1e-4 |
|
|
223
|
+
| Concentration polarization | `i = i_lim tanh(V/4V_T)`, incl. galvanostatic and load modes | 2e-4 |
|
|
224
|
+
| Butler–Volmer electrode | Nernst equilibrium; mixed kinetic/diffusion closed form | 5e-4 |
|
|
225
|
+
| Redox couple between inert electrodes | Nernst level of Fe³⁺/Fe²⁺; the couple's total conserved at equilibrium and under current | 1e-9; 1e-10 |
|
|
226
|
+
| Interface conductance and ion transfer | series 1/G; BV rate law at the interface state | 1e-4; 1e-8 |
|
|
227
|
+
| Mass action | `c(H⁺)c(OH⁻) = K_w` from standard potentials; moiety conservation | 1e-9 |
|
|
228
|
+
| Second law | free energy in through the terminals (Σ N μ̄ over every species) equals the dissipation, every term ≥ 0: a pn diode, an open-circuit junction running on chemical input alone, electrodes with a bipolar plate, a port and a contact behind conductances | 1e-10 |
|
|
229
|
+
| Transients | RC charging of a gated island; water relaxation rate | 1%; 2e-3 |
|
|
230
|
+
| Time integration | BE first order, BDF2 second order; adaptive error control | ratios 2, 4 |
|
|
231
|
+
| Advection and eddy mixing | exact convection–diffusion profile; `D + D_mix`; junction EMF unchanged by mixing | 1e-12; 1e-12; 1e-5 |
|
|
232
|
+
| 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 |
|
|
233
|
+
| 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 |
|
|
234
|
+
| 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 |
|
|
235
|
+
| Conservation | per step, and against time-integrated contact fluxes | 1e-11 relative |
|
|
236
|
+
| Strictly neutral limit (ε = 0) | Planck EMF; polarization with no overlimiting; Donnan at neutral faces | 1e-5; 1e-4; 1e-9 |
|
|
237
|
+
| Fermi–Dirac statistics | `𝓕_{±1/2}` vs quadrature; degenerate bulk; accumulation charge via `𝓕_{3/2}` | 1e-13; 1e-12; 1e-3 |
|
|
238
|
+
| Crowded double layer (lattice gas) | Kilic–Bazant–Ajdari charge, up to `ψ = 40 V_T`; custom function reproduces it | 5e-4; 1e-10 |
|
|
239
|
+
| Non-ideal transport | steady flux −(D/L)ΔP of the grand potential (lattice exact, Redlich–Kister O(h²)) | 1e-12; 1e-4 |
|
|
240
|
+
| Debye–Hückel | junction EMF 2t₊(RT/F) ln(a₁/a₂) with activities | 1e-6 |
|
|
241
|
+
| Data library | ion μ° against the electrochemical series and `K_w`; D against limiting conductivities; band data against `n_i` | 10 mV; 0.5%; 15% |
|
|
242
|
+
| Intercalation host (OCV) | composition vs table and isotherm; chemical diffusion flux and relaxation rate | 1e-12; 1e-5; 1e-4 |
|
|
243
|
+
|
|
244
|
+
## Performance
|
|
245
|
+
|
|
246
|
+
`npm run bench` runs typical interactive workloads and compares them with a committed
|
|
247
|
+
baseline. Matrix factorisations (≈ Newton iterations) and block work are deterministic and
|
|
248
|
+
checked in CI; times are from a desktop Ryzen 7600X in Node 22:
|
|
249
|
+
|
|
250
|
+
| Task | Time | Factorisations |
|
|
251
|
+
|---|---|---|
|
|
252
|
+
| Linear solve, 300 nodes × 7 unknowns (factor + solve) | 0.4 ms | 1 |
|
|
253
|
+
| pn diode (264 nodes): cold equilibrium | 2 ms | 9 |
|
|
254
|
+
| pn diode: I–V sweep 0 → 0.6 V, 31 points | 31 ms | 137 |
|
|
255
|
+
| pn diode: warm jump from +0.4 V to −1 V | 7 ms | 51 |
|
|
256
|
+
| pn diode: adaptive transient, 0 → 0.5 V, 100 ns | 150 ms | 1026 |
|
|
257
|
+
| pn diode: impedance at 20 frequencies | 8 ms | 21 |
|
|
258
|
+
| MOS with a metal gate: C–V sweep, 26 points | 11 ms | 126 |
|
|
259
|
+
| n-Si \| KCl (4 species, 3 unknowns per node): Mott–Schottky sweep, 31 points | 29 ms | 156 |
|
|
260
|
+
| n-Si \| KCl: impedance at 20 frequencies | 10 ms | 21 |
|
|
261
|
+
| Ag \| AgNO₃ \| Ag with double layers: sweep 0 → 0.1 V, 21 points | 12 ms | 81 |
|
|
262
|
+
| Ag \| AgNO₃ \| Ag, neutral: adaptive transient over 1 s | 29 ms | 287 |
|
|
263
|
+
| Silver electrodes with a bipolar plate: sweep 0 → 1 V, 11 points | 27 ms | 63 |
|
|
264
|
+
|
|
265
|
+
The library has no dependencies and does no DOM access, so it runs in a Web Worker. (A device
|
|
266
|
+
definition is plain data and can be posted to a worker. Devices using custom-function
|
|
267
|
+
statistics can't be, and must be built inside the worker.)
|
|
268
|
+
|
|
269
|
+
## Development
|
|
270
|
+
|
|
271
|
+
No dependencies. Tests use node's built-in runner (Node ≥ 20):
|
|
272
|
+
|
|
273
|
+
```sh
|
|
274
|
+
npm test # the validation suite
|
|
275
|
+
npm run bench # typical workloads against bench/baseline.json (--save to update it)
|
|
276
|
+
npm run types # TypeScript declarations from the JSDoc, into types/ (fetches TypeScript via npx)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Design notes and plans: [numerics](docs/numerics.md), [conventions](docs/conventions.md),
|
|
280
|
+
[statistics](docs/statistics.md), [the kit](docs/kit.md), [reading level diagrams](docs/visualization.md),
|
|
281
|
+
[data](docs/data.md), [roadmap](ROADMAP.md).
|
|
282
|
+
|
|
283
|
+
## Licence
|
|
284
|
+
|
|
285
|
+
[0BSD](LICENSE): do anything you like with it.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# driftlet: a guide for agents
|
|
2
|
+
|
|
3
|
+
driftlet is a dependency-free JavaScript library that solves 1D drift–diffusion–reaction
|
|
4
|
+
(Poisson–Nernst–Planck) problems fast enough for live web demos: semiconductor junctions,
|
|
5
|
+
electrochemical cells, double layers, membranes, mixed conductors. It runs in a browser, a Web
|
|
6
|
+
Worker or Node, from npm or a CDN.
|
|
7
|
+
|
|
8
|
+
This guide is for writing a demo or calculation with it. Every code block below runs in the
|
|
9
|
+
test suite. The full references are docs/device.md (the definition), docs/kit.md (the helpers),
|
|
10
|
+
docs/conventions.md (signs and levels) and docs/data.md (the data library).
|
|
11
|
+
|
|
12
|
+
## Imports
|
|
13
|
+
|
|
14
|
+
From a CDN, pin the version:
|
|
15
|
+
|
|
16
|
+
```js nocheck
|
|
17
|
+
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/index.js';
|
|
18
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/kit.js';
|
|
19
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/plot.js';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
23
|
+
|
|
24
|
+
## What to know first
|
|
25
|
+
|
|
26
|
+
- **Units are SI**: m, mol/m³ (1 M = 1000 mol/m³), J/mol, m²/s, V, A/m², F/m², C/m³. Use
|
|
27
|
+
`units` to convert: `units.um(1)`, `units.perCm3(1e17)` (carriers per cm³ to mol/m³),
|
|
28
|
+
`units.eV(1.12)` (to J/mol), `units.cm2PerS(36)`, `units.molar(0.1)`.
|
|
29
|
+
- **Everything you set or read is an electrochemical potential μ̄** (or a voltage made from one).
|
|
30
|
+
A terminal's `V` is the voltage of its terminal species: for an electron contact,
|
|
31
|
+
V = −μ̄_e/F, what a voltmeter reads. φ is bookkeeping and never a control.
|
|
32
|
+
- **Solutions** give, per species, `c` (concentration), `mu` (μ̄), `V` (the species voltage
|
|
33
|
+
μ̄/(zF), a quasi-Fermi level for e⁻ and h⁺) and `Vstd` (the standard level, a band edge for e⁻
|
|
34
|
+
and h⁺). In voltage units the conduction band sits below the valence band. `NaN` marks where a
|
|
35
|
+
species is absent.
|
|
36
|
+
- **Current**: `sol.current` is positive toward +x through the device. `sol.terminals[name]` gives
|
|
37
|
+
each terminal's `{ V, current }`, current into the device.
|
|
38
|
+
- **Nothing physical is defaulted.** Every interface between different materials needs an
|
|
39
|
+
alignment (`dipole` or `step`), or `phi: 'neutral'` for a macroscopic model. Every metal face
|
|
40
|
+
next to a dielectric needs its φ law. Every species that doesn't reach a contact needs its
|
|
41
|
+
initial amount (`c0`). An error names the path and says what to give.
|
|
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.
|
|
44
|
+
- **Exports.** `driftlet`: `Device`, `units`, and the constants `FARADAY`, `GAS_CONSTANT`,
|
|
45
|
+
`EPS0`, `BOLTZMANN`, `ELEMENTARY_CHARGE`, `AVOGADRO` (there's no thermal-voltage constant:
|
|
46
|
+
V_T = GAS_CONSTANT·T/FARADAY). `driftlet/kit`: `build`, `layer`, `ohmic`, `bath`, `half`,
|
|
47
|
+
`level`, `SHE`, `describe`, `live`, `traces`, the data library (`aqueous`, `semiconductor`,
|
|
48
|
+
`metal`, `IONS`, …) and the vacuum-level helpers. `driftlet/plot`: `bandDiagram`, `levelChart`, `labelParts` (labels mark subscripts as `V_{e⁻}`).
|
|
49
|
+
- **Reactions** are written as equations, `{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }`, in
|
|
50
|
+
`new Device`, `build()` and `set()` alike. A coefficient is a number and a space (`2 e-`).
|
|
51
|
+
- **Rate constants carry units.** A bulk reaction's rate is `kf · Π c_reactants` in mol/(m³·s),
|
|
52
|
+
with c in mol/m³ (and 1e16 cm⁻³ is only 0.0166 mol/m³). Band-to-band recombination,
|
|
53
|
+
`e- + h+ = 0`, gives a minority lifetime τ = 1/(kf·N) with N the majority density in mol/m³:
|
|
54
|
+
`kf: 1e8` is τ ≈ 0.6 µs at 1e16 cm⁻³. Generation is a reaction from a reservoir, rate `kf` in
|
|
55
|
+
mol/(m³·s) (`units.perCm3` converts cm⁻³·s⁻¹ too). A face reaction's `k0` is in mol/(m²·s);
|
|
56
|
+
its exchange current density is F·k0 per electron at the reference concentrations.
|
|
57
|
+
|
|
58
|
+
## Template: a semiconductor junction
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import { Device, units } from 'driftlet';
|
|
62
|
+
import { build, layer, ohmic, semiconductor, describe } from 'driftlet/kit';
|
|
63
|
+
|
|
64
|
+
const def = build({
|
|
65
|
+
T: 300, // the library's semiconductor data are for 300 K
|
|
66
|
+
library: [semiconductor('Si')],
|
|
67
|
+
stack: [
|
|
68
|
+
ohmic(0), // e⁻ and h⁺ in equilibrium with a metal at 0 V
|
|
69
|
+
layer('Si', units.um(1), { name: 'n', donors: units.perCm3(1e17) }),
|
|
70
|
+
layer('Si', units.um(1), { name: 'p', acceptors: units.perCm3(1e16) }),
|
|
71
|
+
ohmic(0.5), // the p side at +0.5 V: forward bias
|
|
72
|
+
],
|
|
73
|
+
bulkReactions: [{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }], // recombination: τ = 1/(kf·N) ≈ 0.6 µs at N = 1e16 cm⁻³
|
|
74
|
+
grid: { hmin: units.nm(1), hmax: units.nm(20) },
|
|
75
|
+
});
|
|
76
|
+
console.log(describe(def));
|
|
77
|
+
const dev = new Device(def);
|
|
78
|
+
const sol = dev.solve();
|
|
79
|
+
console.log(sol.converged, `I = ${sol.current.toFixed(1)} A/m²`); // negative: toward −x, p to n
|
|
80
|
+
// An I–V sweep: change the drive, solve again (warm-started).
|
|
81
|
+
for (const V of [0, 0.2, 0.4, 0.6]) {
|
|
82
|
+
dev.set({ contacts: { right: { V } } });
|
|
83
|
+
console.log(V, dev.solve().current);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Template: an electrochemical cell
|
|
88
|
+
|
|
89
|
+
An electrode is a metal layer with the reaction at its face, behind a contact holding the
|
|
90
|
+
metal's electrons (`ohmic(V, ['e-'])`). Reactions are written as equations. A participant that
|
|
91
|
+
isn't a species (solid silver) is fixed by its μ.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import { Device } from 'driftlet';
|
|
95
|
+
import { build, layer, ohmic, half, level, SHE, aqueous, metal } from 'driftlet/kit';
|
|
96
|
+
|
|
97
|
+
const silver = half('Ag+ + e- = Ag(s)', { 'Ag(s)': 0 }); // μ of the element: 0 on table conventions
|
|
98
|
+
// A Stern layer at each metal face: its capacitance and the potential of zero charge are
|
|
99
|
+
// properties of that interface, so they're yours to choose.
|
|
100
|
+
const face = { phi: { type: 'capacitive', C: 0.2 }, zeroCharge: 0.1, reactions: [{ ...silver, k0: 1e-3, alpha: 0.5 }] };
|
|
101
|
+
const def = build({
|
|
102
|
+
library: [aqueous(['H+', 'Ag+', 'NO3-']), metal('Ag')],
|
|
103
|
+
stack: [
|
|
104
|
+
ohmic(0, ['e-']),
|
|
105
|
+
layer('Ag', 1e-6),
|
|
106
|
+
face,
|
|
107
|
+
layer('water', 20e-6, { c0: { 'H+': 1, 'Ag+': 10, 'NO3-': 11 } }),
|
|
108
|
+
face,
|
|
109
|
+
layer('Ag', 1e-6),
|
|
110
|
+
ohmic(0.05, ['e-']),
|
|
111
|
+
],
|
|
112
|
+
grid: { hmin: 0.1e-9, hmax: 200e-9, ratio: 1.15 },
|
|
113
|
+
});
|
|
114
|
+
const sol = new Device(def).solve();
|
|
115
|
+
const mid = sol.x.length >> 1;
|
|
116
|
+
console.log(`I = ${sol.current.toFixed(2)} A/m²`);
|
|
117
|
+
// The silver couple's redox level, and the SHE level, mid-cell (electron voltages).
|
|
118
|
+
console.log(level(sol, silver)[mid] - level(sol, SHE, { standard: true })[mid]); // ≈ 0.80 + (RT/F) ln(c/1 M)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Template: a solar cell
|
|
122
|
+
|
|
123
|
+
Light is a reservoir: `photon = e- + h+` with the photons' μ well above the gap generates at
|
|
124
|
+
the rate `kf` (uniform in the material). Open circuit is a terminal driven at `I: 0`, whose
|
|
125
|
+
voltage is then V_oc. The photocurrent flows toward +x here (holes to the p side on the right).
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
import { Device, units } from 'driftlet';
|
|
129
|
+
import { build, layer, ohmic, semiconductor } from 'driftlet/kit';
|
|
130
|
+
|
|
131
|
+
const cell = (right) =>
|
|
132
|
+
build({
|
|
133
|
+
T: 300,
|
|
134
|
+
library: [semiconductor('Si')],
|
|
135
|
+
stack: [
|
|
136
|
+
ohmic(0),
|
|
137
|
+
layer('Si', units.um(1), { name: 'n', donors: units.perCm3(1e18) }),
|
|
138
|
+
layer('Si', units.um(100), { name: 'p', acceptors: units.perCm3(1e16) }),
|
|
139
|
+
ohmic(right),
|
|
140
|
+
],
|
|
141
|
+
bulkReactions: [
|
|
142
|
+
{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }, // τ ≈ 0.6 µs in the p base: L ≈ 48 µm
|
|
143
|
+
{ equation: 'photon = e- + h+', fixed: { photon: units.eV(3) }, kf: { Si: 50 } }, // G, mol/(m³·s)
|
|
144
|
+
],
|
|
145
|
+
grid: { hmin: units.nm(1), hmax: units.um(1) },
|
|
146
|
+
});
|
|
147
|
+
const sc = new Device(cell(0)).solve(); // short circuit
|
|
148
|
+
const oc = new Device(cell({ I: 0 })).solve(); // open circuit
|
|
149
|
+
console.log(`J_sc = ${sc.current.toFixed(1)} A/m², V_oc = ${oc.terminals.right.V.toFixed(3)} V`);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Template: an electrode in a stirred solution
|
|
153
|
+
|
|
154
|
+
One working electrode, a stagnant layer of solution, and the well-stirred bulk as a bath. The
|
|
155
|
+
bath's terminal is its reference species (Cl⁻ here, like a Ag/AgCl reference), so terminal
|
|
156
|
+
voltages are against that. With table μ° (μ°_H⁺ = 0), V − φ in the solution reads against SHE.
|
|
157
|
+
A terminal's current is into the device, so at the working electrode it's positive when
|
|
158
|
+
oxidising (anodic). An excess of supporting electrolyte (KCl) suppresses migration but doesn't
|
|
159
|
+
remove it: at 100:1 the limiting current is still about 1% off pure diffusion.
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
163
|
+
import { build, layer, ohmic, bath, half, aqueous, metal } from 'driftlet/kit';
|
|
164
|
+
|
|
165
|
+
const iron = half('Fe3+ + e- = Fe2+');
|
|
166
|
+
const electrode = (drive) =>
|
|
167
|
+
build({
|
|
168
|
+
library: [aqueous(['K+', 'Cl-', 'Fe3+', 'Fe2+'], { epsr: 0 }), metal('Pt')],
|
|
169
|
+
stack: [
|
|
170
|
+
ohmic(drive, ['e-']), // the working electrode's terminal: the platinum's electrons
|
|
171
|
+
layer('Pt', 1e-6),
|
|
172
|
+
{ reactions: [{ ...iron, k0: 1e-3 }] },
|
|
173
|
+
layer('water', 20e-6), // the stagnant layer
|
|
174
|
+
bath({ 'K+': 500, 'Cl-': 535, 'Fe3+': 5, 'Fe2+': 10 }, 'Cl-'), // the stirred bulk (neutral)
|
|
175
|
+
],
|
|
176
|
+
grid: { hmin: 10e-9, hmax: 0.5e-6 },
|
|
177
|
+
});
|
|
178
|
+
// At rest (no current), the electrode sits at the couple's Nernst potential.
|
|
179
|
+
const rest = new Device(electrode({ I: 0 })).solve();
|
|
180
|
+
const E = rest.V['e-'][0] - rest.phi[rest.x.length - 1]; // against SHE
|
|
181
|
+
const VT = (GAS_CONSTANT * 298.15) / FARADAY;
|
|
182
|
+
console.log(`rest potential ${E.toFixed(4)} V vs SHE; E° + V_T ln(c₃/c₂) = ${(0.769 + VT * Math.log(5 / 10)).toFixed(4)} V`);
|
|
183
|
+
// Polarised 0.2 V positive of rest: oxidising Fe²⁺, toward its limiting current F·D·c/L.
|
|
184
|
+
const dev = new Device(electrode(rest.terminals.left.V + 0.2));
|
|
185
|
+
console.log(`anodic current ${dev.solve().terminals.left.current.toFixed(2)} A/m²`);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Template: a live page with a slider
|
|
189
|
+
|
|
190
|
+
`live()` merges slider moves made during a solve, warm-starts each solve and keeps the last
|
|
191
|
+
good solution. `bandDiagram()` returns an SVG string. Pass `worker: true` to `live()` to solve in
|
|
192
|
+
a Web Worker.
|
|
193
|
+
|
|
194
|
+
```html
|
|
195
|
+
<!doctype html>
|
|
196
|
+
<meta charset="utf-8">
|
|
197
|
+
<title>pn junction</title>
|
|
198
|
+
<label>bias <input id="V" type="range" min="-0.5" max="0.7" step="0.01" value="0"></label>
|
|
199
|
+
<span id="readout"></span>
|
|
200
|
+
<div id="figure"></div>
|
|
201
|
+
<script type="module">
|
|
202
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/index.js';
|
|
203
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/kit.js';
|
|
204
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/plot.js';
|
|
205
|
+
|
|
206
|
+
const def = build({
|
|
207
|
+
T: 300,
|
|
208
|
+
library: [semiconductor('Si')],
|
|
209
|
+
stack: [ohmic(0), layer('Si', 1e-6, { donors: units.perCm3(1e17) }), layer('Si', 1e-6, { acceptors: units.perCm3(1e16) }), ohmic(0)],
|
|
210
|
+
bulkReactions: [{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }],
|
|
211
|
+
grid: { hmin: 1e-9, hmax: 20e-9 },
|
|
212
|
+
});
|
|
213
|
+
const slider = document.getElementById('V');
|
|
214
|
+
const dev = live(def, {
|
|
215
|
+
onsolution(sol, info) {
|
|
216
|
+
document.getElementById('figure').innerHTML = bandDiagram(sol, { title: `V = ${sol.terminals.right.V.toFixed(2)} V` });
|
|
217
|
+
document.getElementById('readout').textContent = info.failed ? 'no solution here' : `I = ${sol.current.toExponential(2)} A/m²`;
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
slider.addEventListener('input', () => dev.set({ contacts: { right: { V: +slider.value } } }));
|
|
221
|
+
</script>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Reading level diagrams
|
|
225
|
+
|
|
226
|
+
What `bandDiagram()` and `traces()` draw, and how to explain it to a reader (docs/visualization.md
|
|
227
|
+
has the long form):
|
|
228
|
+
|
|
229
|
+
- **Fermi level** V_e⁻ = −μ̄_e⁻/F: the circuit voltage, what a voltmeter reads. A cell's voltage is
|
|
230
|
+
the difference between its terminals' V_e⁻.
|
|
231
|
+
- **Redox level** of Ox + z e⁻ ⇌ Red, `level(sol, half)`: V_e⁻(Ox/Red) = (μ̄_Ox − μ̄_Red)/(zF), where
|
|
232
|
+
an electrode exchanging electrons by that couple would sit in equilibrium with the local
|
|
233
|
+
composition. Drawn thick and dashed. At an electrode face, the step from the metal's Fermi level
|
|
234
|
+
to it is the surface overpotential; through the solution it slopes with ohmic drop and
|
|
235
|
+
concentration polarisation. Each couple has its own; they agree only in redox equilibrium.
|
|
236
|
+
- **Standard redox level** (`{ standard: true }`), thin and dashed: every participant at reference
|
|
237
|
+
concentration. The redox level sits RT/(zF) ln(a_Ox/a_Red) from it (Nernst, locally).
|
|
238
|
+
- **Electrode potentials** are gaps to the SHE's standard level V°_e⁻(SHE): E = V_e⁻(electrode) −
|
|
239
|
+
V°_e⁻(SHE), E° = V°_e⁻(Ox/Red) − V°_e⁻(SHE), η = E − E_eq. With the data library's μ°, V°_e⁻(SHE)
|
|
240
|
+
in water is φ, so `phi: true` draws it. It slopes under current: "vs SHE" needs a place (say,
|
|
241
|
+
the stirred bulk).
|
|
242
|
+
- **Species voltage** V_i = μ̄_i/(z_iF) (thick, solid) for every charged species: the Fermi level
|
|
243
|
+
for e⁻, a quasi-Fermi level for e⁻ and h⁺ in a semiconductor, what a reversible electrode reads
|
|
244
|
+
for an ion. A species moves down slopes in its own V_i (drift and diffusion together) and is in
|
|
245
|
+
equilibrium where it's flat. A step at a face means resistance there.
|
|
246
|
+
- **Standard level** V°_i = φ + μ°_i/(z_iF) (thin, solid): a band edge for e⁻ and h⁺; V_i − V°_i =
|
|
247
|
+
(RT/z_iF) ln(c/c_ref) for ideal species. Steps in V°_i at faces (Donnan, junctions, dipoles) are
|
|
248
|
+
bookkeeping. In volts the conduction band sits below the valence band.
|
|
249
|
+
- **What's left out.** φ isn't drawn: it's a per-material convention (meaningless in a metal), and
|
|
250
|
+
−eφ isn't any electron's energy; the V°_i ladder shows its variation within a material and means
|
|
251
|
+
something across faces too. The exceptions: across an insulator with no mobile species, a
|
|
252
|
+
plain sloped φ (`phi: true`) is the line to draw (its slope is the field), and in vacuum −eφ
|
|
253
|
+
really is an electron's energy. Nor is anything drawn against a reference: the diagrams are
|
|
254
|
+
reference-free (shifting everything changes nothing), every gap on the electron diagram is
|
|
255
|
+
physical, so a reader picks a level at a point as their reference. "vs SHE" and "from the vacuum"
|
|
256
|
+
need a place, since both levels vary in space.
|
|
257
|
+
- A gap V_i − V_j between two species is free of φ, but carries one constant fixed by the elements'
|
|
258
|
+
conventional zeros (μ = 0 per element). So `shifts` (marked ⌇) moves a species' lines to share a
|
|
259
|
+
plot without losing anything.
|
|
260
|
+
- Call V_i the species voltage and say what it is; it's new to most readers. Credit Jamnik and
|
|
261
|
+
Maier (Phys. Chem. Chem. Phys. 3, 1668 (2001)) where V_i rails explain impedance or linear
|
|
262
|
+
response.
|
|
263
|
+
|
|
264
|
+
## Common mistakes
|
|
265
|
+
|
|
266
|
+
- **No alignment at a heterointerface.** `interfaces[0] (between … [Si] and … [GaAs]): an
|
|
267
|
+
interface between different materials needs an alignment`. Give the face `{ dipole }` or
|
|
268
|
+
`{ step: { species: 'e-', value } }` (the conduction-band offset, J/mol), or estimate one from
|
|
269
|
+
electron affinities with `vacuumDipole` (docs/alignment.md). `interfaces[f]` is the f-th face
|
|
270
|
+
of the stack, counting from the left.
|
|
271
|
+
- **A metal face with no φ law.** `… .phi: a face between a conductor and a material with
|
|
272
|
+
ε > 0 needs an explicit φ law`. Give `{ phi: { type: 'capacitive', C }, zeroCharge }`, or
|
|
273
|
+
`phi: 'neutral'`, or make the electrolyte strictly neutral (`aqueous(names, { epsr: 0 })`)
|
|
274
|
+
when double layers don't matter.
|
|
275
|
+
- **`ohmic()` on a metal.** Its default holds both e⁻ and h⁺, for semiconductors. On a metal
|
|
276
|
+
layer, write `ohmic(V, ['e-'])`.
|
|
277
|
+
- **Ions on both sides of a reacting face.** Label them: `'Li+(left) = Li+(right)'`, and give
|
|
278
|
+
their link (`species: { 'Li+': 'blocked' }` if they cross only by the reaction).
|
|
279
|
+
- **Unit slips.** D in cm²/s, μ° in eV, capacitance in µF/cm², T in °C. `describe(def)` flags
|
|
280
|
+
numbers that look like these.
|
|
281
|
+
- **A closed system with no amount.** A species that doesn't reach a contact needs `c0`.
|
|
282
|
+
- **A strictly neutral `c0` that doesn't balance.** With `epsr: 0`, Σ z·c0 must be zero (traces
|
|
283
|
+
included): `… carries a net charge of … mol/m³`. Add the counter-ion's share.
|
|
284
|
+
- **A sharp junction between very different solutions** (3 M KCl against 1 µM traces) can't
|
|
285
|
+
start: grade it over a few thin layers mixing one solution into the other, as at a frit.
|
|
286
|
+
- **A steady solve of a closed cell where every ion can cross.** Its steady state is complete
|
|
287
|
+
mixing. To watch a junction or a salt bridge, step in time from the unmixed `c0` (`advance`).
|
|
288
|
+
- **Everything floating.** At least one terminal must be held at a voltage `V`.
|
|
289
|
+
- **The library's semiconductors are at 300 K.** Give `T: 300`.
|
|
290
|
+
- **Sign of the current.** `sol.current` is positive toward +x: a forward-biased diode with its
|
|
291
|
+
p side on the right carries negative current. A terminal's current is into the device.
|
|
292
|
+
- **Sign of a voltage across a membrane.** `sol.terminalVoltage` is V_right − V_left. Physiology
|
|
293
|
+
quotes V_inside − V_outside, so put the inside on the right or flip the sign.
|
|
294
|
+
- **A coefficient without its space.** `'2e- + …'` reads `2e-` as a species name; the error says
|
|
295
|
+
to write `2 e-`.
|
|
296
|
+
- **Changing c0 with `set()`.** A carried state keeps what it holds; `c0` is only the starting
|
|
297
|
+
composition. Make a new `Device` to start over from it.
|
|
298
|
+
- **An unresolved double layer.** If `sol.warnings` says so, refine the grid (`hmin` below the
|
|
299
|
+
Debye length) or use a neutral model (`epsr: 0`, `phi: 'neutral'`).
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "driftlet",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Small, dependency-free 1D drift–diffusion–reaction (Poisson–Nernst–Planck) solver for interactive web demos",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./types/index.d.ts",
|
|
9
|
+
"default": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./kit": {
|
|
12
|
+
"types": "./types/kit.d.ts",
|
|
13
|
+
"default": "./src/kit.js"
|
|
14
|
+
},
|
|
15
|
+
"./plot": {
|
|
16
|
+
"types": "./types/plot.d.ts",
|
|
17
|
+
"default": "./src/plot.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"main": "./src/index.js",
|
|
21
|
+
"types": "./types/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"types",
|
|
25
|
+
"llms.txt"
|
|
26
|
+
],
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node --test",
|
|
30
|
+
"bench": "node bench/bench.js",
|
|
31
|
+
"prepublishOnly": "npm test && npm run types",
|
|
32
|
+
"types": "rm -rf types && npx -y -p typescript@5 tsc --allowJs --declaration --emitDeclarationOnly --target es2022 --module nodenext --moduleResolution nodenext --outDir types src/index.js src/kit.js src/plot.js"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"drift-diffusion",
|
|
39
|
+
"poisson-nernst-planck",
|
|
40
|
+
"electrochemistry",
|
|
41
|
+
"semiconductor",
|
|
42
|
+
"simulation"
|
|
43
|
+
],
|
|
44
|
+
"homepage": "https://markblundeberg.github.io/driftlet/demos/",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/markblundeberg/driftlet.git"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/markblundeberg/driftlet/issues"
|
|
51
|
+
},
|
|
52
|
+
"author": "Mark Lundeberg",
|
|
53
|
+
"license": "0BSD"
|
|
54
|
+
}
|