driftlet 0.1.0 → 0.2.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 +31 -17
- package/docs/alignment.md +146 -0
- package/docs/conventions.md +194 -0
- package/docs/data.md +120 -0
- package/docs/device.md +529 -0
- package/docs/kit.md +299 -0
- package/docs/logo-dark.svg +7 -0
- package/docs/logo.svg +7 -0
- package/docs/numerics.md +629 -0
- package/docs/statistics.md +226 -0
- package/docs/visualization.md +259 -0
- package/llms.txt +66 -12
- package/package.json +2 -1
- package/src/constants.js +11 -0
- package/src/index.js +5 -2
- package/src/kit.js +1 -0
- package/src/solver.js +66 -7
- package/src/sources.js +103 -0
- package/src/types.js +3 -2
- package/types/constants.d.ts +10 -0
- package/types/index.d.ts +11 -2
- package/types/kit.d.ts +1 -0
- package/types/solver.d.ts +6 -0
- package/types/sources.d.ts +102 -0
- package/types/types.d.ts +4 -2
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Statistics
|
|
2
|
+
|
|
3
|
+
A material's **statistics** say how its concentrations follow from its species' potentials.
|
|
4
|
+
Every species *i* has a reduced chemical potential
|
|
5
|
+
|
|
6
|
+
```math
|
|
7
|
+
\zeta_i = (\bar\mu_i - z_i F \phi - \mu^\circ_i) / RT
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
and a statistics model gives the concentrations $`c(\zeta)`$ of the species it covers. Species
|
|
11
|
+
that aren't in any model are ideal, $`c = c_{\mathrm{ref}} \cdot e^\zeta`$, which is the default and
|
|
12
|
+
the right choice for dilute species. Non-ideal models describe degenerate carriers, crowding,
|
|
13
|
+
filling of a finite set of sites, activity coefficients, and intercalation hosts.
|
|
14
|
+
|
|
15
|
+
Three rules hold for every model:
|
|
16
|
+
|
|
17
|
+
- **The dilute limit is ideal.** Every model tends to $`c_{\mathrm{ref}} \cdot e^\zeta`$ as $`c \to 0`$,
|
|
18
|
+
so `mu0` and `cRef` keep one meaning (the dilute, Henry's-law reference state) whatever the
|
|
19
|
+
statistics. Changing a material's statistics changes only its non-dilute behaviour.
|
|
20
|
+
- **The Jacobian is a chemical capacitance.** $`K = \partial c/\partial\zeta`$ is the Hessian of a
|
|
21
|
+
convex potential (the grand potential density $`P(\zeta)`$, with $`c = \partial P/\partial\zeta`$),
|
|
22
|
+
so it's symmetric and positive definite. $`RT \cdot K`$ is the chemical capacitance matrix per
|
|
23
|
+
volume. Models whose free energy isn't convex (a miscibility gap) are refused, because they
|
|
24
|
+
need phase separation (Cahn–Hilliard, on the [roadmap](../ROADMAP.md)).
|
|
25
|
+
- **D keeps its meaning.** Fluxes are always $`N = -(D c/RT)\nabla\bar\mu`$, with $`D`$ the
|
|
26
|
+
dilute-limit coefficient (constant mobility). The chemical diffusivity you'd measure is $`D`$
|
|
27
|
+
times the thermodynamic factor $`\partial\ln a/\partial\ln c`$, the generalised Einstein
|
|
28
|
+
relation. See [conventions](conventions.md#transport).
|
|
29
|
+
|
|
30
|
+
## Declaring statistics
|
|
31
|
+
|
|
32
|
+
```js nocheck
|
|
33
|
+
materials: {
|
|
34
|
+
Si: {
|
|
35
|
+
epsr: 11.7,
|
|
36
|
+
species: { 'e-': { D: 36e-4, mu0: 0, cRef: Nc }, 'h+': { D: 12e-4, mu0: units.eV(1.12), cRef: Nv } },
|
|
37
|
+
statistics: [{ type: 'fermi-dirac', species: ['e-', 'h+'] }],
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`statistics` is a list of models, each naming the species it covers. A species belongs to at
|
|
43
|
+
most one model.
|
|
44
|
+
|
|
45
|
+
| Model | Species | $`c(\zeta)`$ | Parameters |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| `fermi-dirac` | any, each independently | $`c_{\mathrm{ref}} \mathcal{F}_{1/2}(\zeta)`$ (3D band); $`c_{\mathrm{ref}} \ln(1 + e^\zeta)`$ with `order: 0` (2D) | `order`: 1/2 (default) or 0 |
|
|
48
|
+
| `lattice` | any, sharing sites | $`c_a = c_{\mathrm{max}} w_a / (1 + \sum w)`$, $`w_a = (c_{\mathrm{ref},a}/c_{\mathrm{max}}) e^{\zeta_a}`$ | `cMax`, site density, mol/m³ |
|
|
49
|
+
| `redlich-kister` | one | on its own sites: $`\zeta = \ln(x/(1-x)) + \ln(c_{\mathrm{max}}/c_{\mathrm{ref}}) + (g(x) - g(0))/RT`$, $`x = c/c_{\mathrm{max}}`$ | `cMax`; `A`, J/mol |
|
|
50
|
+
| `debye-huckel` | charged | $`c_{\mathrm{ref}} e^\zeta / \gamma`$, $`\ln\gamma_i = -z_i^2 \ell_B \kappa / (2(1 + \kappa a))`$ | `epsr` (solvent, for the theory); `a`, m |
|
|
51
|
+
| `insertion` | [ion, carrier], $`\varepsilon = 0`$ host | occupancy from the neutral combination (below) | `cMax`; `A` or `ocv` |
|
|
52
|
+
| `custom` | any | your function | `evaluate(zeta) → { c, dcdzeta }` |
|
|
53
|
+
|
|
54
|
+
### Fermi–Dirac
|
|
55
|
+
|
|
56
|
+
For electrons and holes, $`c_{\mathrm{ref}}`$ is the effective density of states and $`\mu^\circ`$ the
|
|
57
|
+
band edge, as for ideal statistics, and $`c = N_c \mathcal{F}_{1/2}(\zeta)`$ with the normalised
|
|
58
|
+
Fermi–Dirac integral ($`\mathcal{F}_j \to e^\zeta`$ for $`\zeta \to -\infty`$). driftlet evaluates
|
|
59
|
+
$`\mathcal{F}_{1/2}`$ and $`\mathcal{F}_{-1/2}`$ (its derivative) to ~1e-14 relative, so equilibrium
|
|
60
|
+
carrier densities are exact. `order: 0` is a two-dimensional density of states, and it's exact
|
|
61
|
+
in closed form.
|
|
62
|
+
|
|
63
|
+
### Lattice gas
|
|
64
|
+
|
|
65
|
+
The listed species share $`c_{\mathrm{max}}`$ sites per volume and each occupies one, so no
|
|
66
|
+
concentration can exceed $`c_{\mathrm{max}}`$. With both ions of an electrolyte on one lattice, this
|
|
67
|
+
is the Bikerman / Kilic–Bazant–Ajdari model of a crowded double layer. At large potentials the
|
|
68
|
+
counter-ion saturates at $`c_{\mathrm{max}}`$, and the diffuse charge grows as $`\sqrt{\psi}`$ instead
|
|
69
|
+
of exponentially. With one species it's Langmuir filling of sites. The dilute reference
|
|
70
|
+
$`c_{\mathrm{ref}}`$ is kept. To use the half-filling convention of site models
|
|
71
|
+
($`e^\zeta = x/(1-x)`$), set `cRef` equal to `cMax`.
|
|
72
|
+
|
|
73
|
+
### Redlich–Kister
|
|
74
|
+
|
|
75
|
+
One species on its own $`c_{\mathrm{max}}`$ sites, with the excess Gibbs energy of mixing per site
|
|
76
|
+
$`x(1-x) \sum A_k (1-2x)^k`$. $`g(x)`$ in the table is its $`x`$-derivative, the excess chemical
|
|
77
|
+
potential. A single `A: [Ω]` is the regular solution,
|
|
78
|
+
$`\mu = \mu^\circ + RT \ln(x/(1-x)) + \Omega(1-2x) + \mathrm{const}`$. It's convex only for
|
|
79
|
+
$`\Omega < 2RT`$, which is checked at construction.
|
|
80
|
+
|
|
81
|
+
The table form is referred to the dilute limit. The half-filling convention of the battery
|
|
82
|
+
literature, $`\mu = \mu^\circ_{1/2} + RT \ln(x/(1-x)) + \Omega(1-2x)`$, corresponds to
|
|
83
|
+
`cRef = cMax` and `mu0` $`= \mu^\circ_{1/2} + A_0`$ (the excess at $`x = 0`$, $`\sum A_k`$ in general).
|
|
84
|
+
|
|
85
|
+
### Debye–Hückel
|
|
86
|
+
|
|
87
|
+
Extended Debye–Hückel activity coefficients for dilute electrolytes:
|
|
88
|
+
$`\ln\gamma_i = -z_i^2 \ell_B \kappa / (2(1 + \kappa a))`$, with the Bjerrum length
|
|
89
|
+
$`\ell_B = e^2/(4\pi\varepsilon kT)`$ and $`\kappa^2 = F^2 \sum z^2 c / (\varepsilon RT)`$ from the
|
|
90
|
+
solvent permittivity `epsr` (which defaults to the material's, if that's non-zero) and a
|
|
91
|
+
distance of closest approach `a` (default 0, the limiting law). It stops being convex at high
|
|
92
|
+
ionic strength, and the solver then reports a failure.
|
|
93
|
+
|
|
94
|
+
Single-ion activity coefficients are a convention: only neutral combinations (the mean
|
|
95
|
+
activity $`\gamma_\pm`$) are measurable. In a strictly neutral ($`\varepsilon = 0`$) material only
|
|
96
|
+
those combinations matter, so that's where this model belongs. With $`\varepsilon > 0`$, the
|
|
97
|
+
resolved double layers would depend on the convention, so driftlet warns.
|
|
98
|
+
|
|
99
|
+
### Insertion hosts: neutral-combination statistics
|
|
100
|
+
|
|
101
|
+
In an intercalation host, an inserted ion enters together with its compensating electronic
|
|
102
|
+
carrier, e.g. $`\mathrm{Li}^+ + \mathrm{e}^-`$. Only their neutral combination has a well-defined
|
|
103
|
+
chemical potential, $`\mu_{\mathrm{Li}} = \bar\mu_{\mathrm{Li}^+} + \bar\mu_{\mathrm{e}^-}`$, and the
|
|
104
|
+
host's composition depends on nothing else. That's what an OCV curve measures, and the mutual
|
|
105
|
+
chemical capacitance of the pair is the slope of that curve.
|
|
106
|
+
|
|
107
|
+
The `insertion` model describes exactly this, in a strictly neutral host (`epsr: 0`) whose
|
|
108
|
+
only charged species are the pair `[ion, carrier]`. For an ion of charge $`z`$ and a carrier of
|
|
109
|
+
charge $`-z/\nu`$:
|
|
110
|
+
|
|
111
|
+
```math
|
|
112
|
+
\begin{aligned}
|
|
113
|
+
\zeta_{\mathrm{comb}} &= \zeta_{\mathrm{ion}} + \nu \zeta_{\mathrm{carrier}} \quad (\phi\text{ cancels}) \\
|
|
114
|
+
c_{\mathrm{ion}} &= s(\zeta_{\mathrm{comb}}), \qquad c_{\mathrm{carrier}} = \nu s + \text{background}
|
|
115
|
+
\end{aligned}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The occupancy $`s = c_{\mathrm{max}} x`$ comes either from a Redlich–Kister isotherm (`A`, as above,
|
|
119
|
+
referred to the ion's dilute reference) or from a table:
|
|
120
|
+
|
|
121
|
+
```js nocheck
|
|
122
|
+
statistics: [{
|
|
123
|
+
type: 'insertion',
|
|
124
|
+
species: ['Li+', 'e-'],
|
|
125
|
+
cMax: 30000, // sites, mol/m³
|
|
126
|
+
ocv: { x: [...], E: [...], muRef: 0 }, // E against a reference where μ_Li = muRef
|
|
127
|
+
}]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The table gives the open-circuit voltage $`E(x)`$ of the host against a reference electrode in
|
|
131
|
+
which the combination has chemical potential `muRef` (J/mol), so that
|
|
132
|
+
$`\mu_{\mathrm{ion}} + \nu \mu_{\mathrm{carrier}} = \mathtt{muRef} - z F E(x)`$. Against Li metal,
|
|
133
|
+
on the usual table convention, `muRef` is 0. $`x`$ must increase and $`E`$ must strictly decrease.
|
|
134
|
+
A flat plateau means phase separation, which isn't supported yet. The table is read as
|
|
135
|
+
$`\zeta = \ln(x/(1-x)) + r(x)`$: the residual $`r`$, the host's non-ideality, is a shape-preserving
|
|
136
|
+
cubic between points and continues linearly beyond them. Ideal and regular-solution curves are
|
|
137
|
+
therefore reproduced exactly, and $`x`$ stays within $`(0, 1)`$ at any potential. The interpolated
|
|
138
|
+
curve is checked for monotonicity at construction.
|
|
139
|
+
|
|
140
|
+
Because every charged species sits in a neutral combination, **$`\phi`$ is undefined** in such a
|
|
141
|
+
host and is reported as `NaN`. Its faces must be `neutral` (the default next to such a host),
|
|
142
|
+
and its contacts `bulk` or `neutral`. The host's `fixedCharge` is balanced by background
|
|
143
|
+
carriers, $`\mathtt{background} = -\mathtt{fixedCharge}/(z_{\mathrm{carrier}} F)`$, which must be
|
|
144
|
+
non-negative. This is a jellium description of the host's own electrons, which keep it
|
|
145
|
+
conducting when it's empty. With no fixed charge, the carriers are only those that came in with
|
|
146
|
+
the ions. The carrier's `cRef` plays no part (only the combination's $`\mu^\circ`$ and the ion's
|
|
147
|
+
`cRef` enter), but it must still be given.
|
|
148
|
+
|
|
149
|
+
An insertion host belongs with [conductor regions](device.md#materials) more than with the other
|
|
150
|
+
statistics here. Both are conductors with no $`\phi`$ of their own:
|
|
151
|
+
- **A metal** is the one-carrier case: its Fermi level is its only unknown.
|
|
152
|
+
- **An insertion host** is the two-carrier case: only the neutral combination's potential sets
|
|
153
|
+
the composition, and the split between ion and carrier is free.
|
|
154
|
+
|
|
155
|
+
A general mixed conductor with more mobile species needs fully populated mutual capacitance and
|
|
156
|
+
conductance matrices, a different model again (see the [roadmap](../ROADMAP.md)).
|
|
157
|
+
|
|
158
|
+
### Custom
|
|
159
|
+
|
|
160
|
+
```js nocheck
|
|
161
|
+
statistics: [{
|
|
162
|
+
type: 'custom',
|
|
163
|
+
species: ['A', 'B'],
|
|
164
|
+
evaluate: (zeta) => ({ c: [cA, cB], dcdzeta: [[dA_dA, dA_dB], [dB_dA, dB_dB]] }),
|
|
165
|
+
}]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`evaluate` gets the species' $`\zeta`$ (in the listed order) and returns their concentrations and
|
|
169
|
+
the Jacobian $`\partial c/\partial\zeta`$. The Jacobian must be symmetric and positive definite,
|
|
170
|
+
and that's checked at $`\zeta = 0`$ when the device is built. A device with a function in it isn't
|
|
171
|
+
plain data, so it can't be posted to a Web Worker. Build it inside the worker instead.
|
|
172
|
+
|
|
173
|
+
## Transport with non-ideal statistics
|
|
174
|
+
|
|
175
|
+
The flux law is $`N = -(D c/RT)\nabla\bar\mu`$ for every model. Writing $`\ln c = \zeta - \mathrm{ex}`$
|
|
176
|
+
with the excess $`\mathrm{ex} = \zeta - \ln(c/c_{\mathrm{ref}})`$ (zero for ideal statistics), the
|
|
177
|
+
excess acts like an extra potential. The Scharfetter–Gummel flux keeps its form with
|
|
178
|
+
$`\Delta = z\Delta\hat\phi + \Delta\mathrm{ex}`$ (see [numerics](numerics.md#fluxes)), so it's still
|
|
179
|
+
exactly zero at equilibrium. It's second-order accurate in the cell size (and exact for a single
|
|
180
|
+
diffusing species on a lattice).
|
|
181
|
+
|
|
182
|
+
Reaction rates keep their concentration-based prefactors (mass action in $`c`$, and
|
|
183
|
+
$`c/c_{\mathrm{ref}}`$ in the Butler–Volmer forms), while the driving force is always the affinity
|
|
184
|
+
from $`\bar\mu`$. So equilibrium is exact for any statistics, and only the kinetics away from it
|
|
185
|
+
depend on that choice of prefactor.
|
|
186
|
+
|
|
187
|
+
Cross-species transport coefficients (an Onsager matrix) aren't included: each species moves
|
|
188
|
+
down its own $`\bar\mu`$. They should arrive together with cross chemical capacitances, as two
|
|
189
|
+
halves of the same network ([roadmap](../ROADMAP.md)). Note that $`K`$ itself can already be
|
|
190
|
+
non-diagonal, e.g. on a shared lattice.
|
|
191
|
+
|
|
192
|
+
## Example: a lithium host against its OCV
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
196
|
+
|
|
197
|
+
const RT = GAS_CONSTANT * 298.15;
|
|
198
|
+
const x = [0.05, 0.2, 0.4, 0.6, 0.8, 0.95];
|
|
199
|
+
const E = x.map((v) => 0.4 - (RT / FARADAY) * Math.log(v / (1 - v))); // an ideal-solution OCV, V vs Li
|
|
200
|
+
|
|
201
|
+
const dev = new Device({
|
|
202
|
+
species: [
|
|
203
|
+
{ name: 'Li+', z: 1 },
|
|
204
|
+
{ name: 'e-', z: -1 },
|
|
205
|
+
],
|
|
206
|
+
materials: {
|
|
207
|
+
host: {
|
|
208
|
+
epsr: 0,
|
|
209
|
+
species: { 'Li+': { D: 1e-14, mu0: 0, cRef: 30000 }, 'e-': { D: 1e-4, mu0: 0, cRef: 30000 } },
|
|
210
|
+
statistics: [{ type: 'insertion', species: ['Li+', 'e-'], cMax: 30000, ocv: { x, E, muRef: 0 } }],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
regions: [{ material: 'host', length: 1e-6 }],
|
|
214
|
+
contacts: {
|
|
215
|
+
// Li⁺ from a lithium reference (via an electrolyte) on the left; electrons from a current
|
|
216
|
+
// collector on the right, whose voltage is the electrode potential vs Li.
|
|
217
|
+
left: { V: 0, terminal: 'Li+', species: { 'Li+': 'equilibrium' }, phi: 'bulk' },
|
|
218
|
+
right: { V: 0.4, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'bulk' },
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
for (const V of [0.5, 0.4, 0.3]) {
|
|
222
|
+
dev.set({ contacts: { right: { V } } });
|
|
223
|
+
const sol = dev.solve();
|
|
224
|
+
console.log(`E = ${V} V: x = ${(sol.c['Li+'][5] / 30000).toFixed(4)}`); // 0.0200, 0.5000, 0.9800
|
|
225
|
+
}
|
|
226
|
+
```
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Reading level diagrams
|
|
2
|
+
|
|
3
|
+
driftlet draws its results as level diagrams: voltages against position, on one axis, for the
|
|
4
|
+
electrons in the metals, for the reactions in the solutions, and for every charged species. This
|
|
5
|
+
page explains what each line means and why it's drawn that way. The first half covers the
|
|
6
|
+
electronic levels, Fermi levels and redox levels, which connect directly to electrode potentials
|
|
7
|
+
and other familiar quantities. The second half covers the species voltages of every charged
|
|
8
|
+
species, ions included. For how to draw them, see [the kit's plotting helpers](kit.md#plotting).
|
|
9
|
+
|
|
10
|
+
## Electron levels
|
|
11
|
+
|
|
12
|
+
### The Fermi level
|
|
13
|
+
|
|
14
|
+
An electron's electrochemical potential $`\bar\mu_{\mathrm{e}^-}`$, written as a voltage, is
|
|
15
|
+
|
|
16
|
+
```math
|
|
17
|
+
V_{\mathrm{e}^-} = -\bar\mu_{\mathrm{e}^-} / F
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
It's the voltage of ordinary circuits. It's what a voltmeter reads between two terminals, it
|
|
21
|
+
equalises between metals wired together, it drops along a resistor by $`IR`$, and a cell's voltage
|
|
22
|
+
is the difference between its terminals' $`V_{\mathrm{e}^-}`$. In a metal it's the Fermi level, in
|
|
23
|
+
volts. A terminal's `V` in driftlet is this voltage, for an electron contact. In a semiconductor
|
|
24
|
+
out of equilibrium, electrons and holes each have their own (the quasi-Fermi levels, below).
|
|
25
|
+
|
|
26
|
+
### Redox levels
|
|
27
|
+
|
|
28
|
+
A half-reaction $`\mathrm{Ox} + z\,\mathrm{e}^- \rightleftharpoons \mathrm{Red}`$ is in equilibrium when
|
|
29
|
+
$`\bar\mu_{\mathrm{Ox}} + z \bar\mu_{\mathrm{e}^-} = \bar\mu_{\mathrm{Red}}`$. That fixes an electron
|
|
30
|
+
level, even where no free electrons exist:
|
|
31
|
+
|
|
32
|
+
```math
|
|
33
|
+
V_{\mathrm{e}^-}(\mathrm{Ox}/\mathrm{Red}) = (\bar\mu_{\mathrm{Ox}} - \bar\mu_{\mathrm{Red}}) / (zF)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This is the couple's **redox level** (sometimes called its redox Fermi level). At each point
|
|
37
|
+
where Ox and Red are present, it's where an electrode exchanging electrons by that couple would
|
|
38
|
+
sit if it were in equilibrium with the local composition. `level(sol, half)` evaluates it at
|
|
39
|
+
every node.
|
|
40
|
+
|
|
41
|
+
It belongs on the same axis as the metals' Fermi levels, because then the things that matter
|
|
42
|
+
are gaps you can point at:
|
|
43
|
+
|
|
44
|
+
- **At an electrode**, the step from the metal's $`V_{\mathrm{e}^-}`$ to the couple's level at the
|
|
45
|
+
face is the surface overpotential. It's zero at equilibrium, and its size and sign drive the
|
|
46
|
+
reaction's kinetics. Where the metal sits higher (in volts) than the couple's level, electrons
|
|
47
|
+
leave the solution for the metal: oxidation, an anodic current.
|
|
48
|
+
- **Through the solution**, the couple's level slopes wherever current flows. Part of the slope
|
|
49
|
+
is ohmic drop and part is concentration polarisation: the couple's composition changing near
|
|
50
|
+
the electrode.
|
|
51
|
+
- **Several couples** in one solution each have their own level. The levels agree only if the
|
|
52
|
+
couples trade electrons fast enough to reach equilibrium with each other, which often they
|
|
53
|
+
don't. An electrode that exchanges with several couples at once settles between their levels:
|
|
54
|
+
a mixed potential.
|
|
55
|
+
|
|
56
|
+
### Standard redox levels
|
|
57
|
+
|
|
58
|
+
With every participant at its reference concentration, the same formula gives the couple's
|
|
59
|
+
**standard redox level** $`V^\circ_{\mathrm{e}^-}(\mathrm{Ox}/\mathrm{Red})`$
|
|
60
|
+
(`level(sol, half, { standard: true })`). The redox level sits above or below it by the activity
|
|
61
|
+
term, which makes the Nernst equation local:
|
|
62
|
+
|
|
63
|
+
```math
|
|
64
|
+
V_{\mathrm{e}^-}(\mathrm{Ox}/\mathrm{Red}) = V^\circ_{\mathrm{e}^-}(\mathrm{Ox}/\mathrm{Red}) + \frac{RT}{zF} \ln\frac{a_{\mathrm{Ox}}}{a_{\mathrm{Red}}}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The standard level moves with the solution's $`\phi`$, so it slopes with the ohmic drop. The gap
|
|
68
|
+
between the two lines is purely composition.
|
|
69
|
+
|
|
70
|
+
### Electrode potentials
|
|
71
|
+
|
|
72
|
+
The familiar potentials are gaps between these levels, measured from one more standard level:
|
|
73
|
+
the hydrogen couple's, $`V^\circ_{\mathrm{e}^-}(\mathrm{SHE})`$, for 2 H⁺ + 2 e⁻ ⇌ H₂ at unit
|
|
74
|
+
activities.
|
|
75
|
+
|
|
76
|
+
```math
|
|
77
|
+
\begin{aligned}
|
|
78
|
+
E &= V_{\mathrm{e}^-}(\text{electrode}) - V^\circ_{\mathrm{e}^-}(\mathrm{SHE}) && \quad\text{the electrode potential, "vs SHE"} \\
|
|
79
|
+
E_{\mathrm{eq}} &= V_{\mathrm{e}^-}(\mathrm{Ox}/\mathrm{Red}) - V^\circ_{\mathrm{e}^-}(\mathrm{SHE}) && \quad\text{the couple's equilibrium potential} \\
|
|
80
|
+
E^\circ &= V^\circ_{\mathrm{e}^-}(\mathrm{Ox}/\mathrm{Red}) - V^\circ_{\mathrm{e}^-}(\mathrm{SHE}) && \quad\text{its standard potential} \\
|
|
81
|
+
\eta &= E - E_{\mathrm{eq}} && \quad\text{the overpotential}
|
|
82
|
+
\end{aligned}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The Nernst equation, $`E_{\mathrm{eq}} = E^\circ + (RT/zF) \ln(a_{\mathrm{Ox}}/a_{\mathrm{Red}})`$, is the
|
|
86
|
+
floating form above with the SHE's level subtracted from both sides.
|
|
87
|
+
|
|
88
|
+
With the data library's standard potentials, which follow the usual tables
|
|
89
|
+
($`\mu^\circ_{\mathrm{H}^+} = 0`$ and $`\mu_{\mathrm{H_2}} = 0`$), the SHE's standard level in an
|
|
90
|
+
aqueous solution is $`\phi`$ itself. So drawing $`\phi`$ (`traces(sol, { phi: true })`) draws the
|
|
91
|
+
SHE's level, and the demos label it that way.
|
|
92
|
+
|
|
93
|
+
One caution: $`V^\circ_{\mathrm{e}^-}(\mathrm{SHE})`$ is a level in the solution, and it slopes
|
|
94
|
+
wherever current flows. So "vs SHE" needs a place. A reference electrode samples the level where
|
|
95
|
+
its tip sits, which is why practitioners correct for iR drop, use a Luggin capillary, or add
|
|
96
|
+
supporting electrolyte to flatten the bulk. In a cell polarised from wall to wall there's no flat
|
|
97
|
+
bulk and no single $`E`$, but the levels are all still there to read.
|
|
98
|
+
|
|
99
|
+
For example, platinum at open circuit in a ferric/ferrous solution:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
103
|
+
import { build, layer, ohmic, bath, half, level, aqueous, metal } from 'driftlet/kit';
|
|
104
|
+
|
|
105
|
+
// Platinum at open circuit in 2 mM Fe³⁺ and 8 mM Fe²⁺, in 0.5 M KCl.
|
|
106
|
+
const iron = half('Fe3+ + e- = Fe2+');
|
|
107
|
+
const def = build({
|
|
108
|
+
library: [aqueous(['K+', 'Cl-', 'Fe3+', 'Fe2+'], { epsr: 0 }), metal('Pt')],
|
|
109
|
+
stack: [
|
|
110
|
+
ohmic({ I: 0 }, ['e-']),
|
|
111
|
+
layer('Pt', 1e-6),
|
|
112
|
+
{ reactions: [{ ...iron, k0: 1e-3, alpha: 0.5 }] },
|
|
113
|
+
layer('water', 10e-6),
|
|
114
|
+
bath({ 'K+': 500, 'Cl-': 522, 'Fe3+': 2, 'Fe2+': 8 }, 'Cl-', 0),
|
|
115
|
+
],
|
|
116
|
+
grid: { hmin: 10e-9, hmax: 0.5e-6 },
|
|
117
|
+
});
|
|
118
|
+
const sol = new Device(def).solve();
|
|
119
|
+
const g = sol.x.length - 1; // in the bulk
|
|
120
|
+
const fermi = sol.terminals.left.V; // the platinum's V_e⁻
|
|
121
|
+
const she = sol.phi[g]; // V°_e⁻(SHE), with the library's data
|
|
122
|
+
const E = fermi - she, Eeq = level(sol, iron)[g] - she, E0 = level(sol, iron, { standard: true })[g] - she;
|
|
123
|
+
const VT = (GAS_CONSTANT * 298.15) / FARADAY;
|
|
124
|
+
console.log(`E = ${E.toFixed(4)} V vs SHE, E_eq = ${Eeq.toFixed(4)} V, Nernst: ${(E0 + VT * Math.log(2 / 8)).toFixed(4)} V`);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A cell's voltage splits the same way along the electrons' path. It's the difference between
|
|
128
|
+
the two terminals' $`V_{\mathrm{e}^-}`$: at open circuit, the gap between the two couples' levels;
|
|
129
|
+
under load, less each electrode's overpotential and the solution's share. The
|
|
130
|
+
[Daniell demo](https://markblundeberg.github.io/driftlet/demos/daniell.html) draws this and adds it up.
|
|
131
|
+
|
|
132
|
+
### How they're drawn
|
|
133
|
+
|
|
134
|
+
| line | what it is |
|
|
135
|
+
|---|---|
|
|
136
|
+
| thick, solid | a species voltage $`V_i`$: for electrons, the Fermi level |
|
|
137
|
+
| thick, dashed | a redox level $`V_{\mathrm{e}^-}(\mathrm{Ox}/\mathrm{Red})`$ |
|
|
138
|
+
| thin, dashed | a standard redox level $`V^\circ_{\mathrm{e}^-}(\mathrm{Ox}/\mathrm{Red})`$ |
|
|
139
|
+
| thin, solid | a standard level $`V^\circ_i`$ (below) |
|
|
140
|
+
| thin, dotted, grey | $`\phi`$, on request: across an insulator, or as the SHE's level (below) |
|
|
141
|
+
|
|
142
|
+
Electrons are steel blue, cations (and holes) warm colours, anions cool colours, and redox levels
|
|
143
|
+
blue-violet.
|
|
144
|
+
|
|
145
|
+
## Species voltages for every charged species
|
|
146
|
+
|
|
147
|
+
The Fermi level's definition works for any charged species i:
|
|
148
|
+
|
|
149
|
+
```math
|
|
150
|
+
\begin{aligned}
|
|
151
|
+
V_i &= \bar\mu_i / (z_i F) && \quad\text{the species voltage} \\
|
|
152
|
+
V^\circ_i &= \phi + \mu^\circ_i / (z_i F) && \quad\text{its standard level}
|
|
153
|
+
\end{aligned}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
$`V_i`$ is a way of drawing $`\bar\mu_i`$, not new physics: the equations, the docs and the API work
|
|
157
|
+
in $`\bar\mu_i`$, which also covers neutral species (where $`V_i`$ is undefined). For electrons, $`V_i`$
|
|
158
|
+
is the Fermi level. For an ion, it's the voltage an electrode reversible to that ion would read: a
|
|
159
|
+
silver wire in equilibrium with the solution's Ag⁺ reads $`V_{\mathrm{Ag}^+}`$. The standard level
|
|
160
|
+
is where $`V_i`$ would sit at the reference concentration, so for ideal species
|
|
161
|
+
|
|
162
|
+
```math
|
|
163
|
+
V_i - V^\circ_i = \frac{RT}{z_i F} \ln\frac{c_i}{c_{\mathrm{ref},i}}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
and for non-ideal ones the activity replaces $`c_i/c_{\mathrm{ref},i}`$.
|
|
167
|
+
|
|
168
|
+
Drawing every species this way, ions alongside electrons, shows things that are otherwise hard
|
|
169
|
+
to see:
|
|
170
|
+
|
|
171
|
+
- **Each species' driving force is the slope of its own line.** A species moves down its own
|
|
172
|
+
$`\bar\mu_i`$, drift and diffusion together, so on the voltage plot the current it carries flows
|
|
173
|
+
down slopes in its $`V_i`$ (cations and holes move downhill, electrons and anions uphill), and in
|
|
174
|
+
equilibrium its $`V_i`$ is flat. There's no need to split that one slope into an electric field
|
|
175
|
+
and a concentration gradient (a split that depends on how $`\phi`$ was chosen, and so means little
|
|
176
|
+
on its own).
|
|
177
|
+
- **Interfaces read at a glance.** Where a species crosses a face freely, its $`V_i`$ is
|
|
178
|
+
continuous. A step in $`V_i`$ means something resists its transfer and dissipates energy there.
|
|
179
|
+
The standard levels step at faces along with $`\phi`$ (Donnan potentials, liquid junctions,
|
|
180
|
+
dipoles), but those steps are bookkeeping: they cancel from anything measured.
|
|
181
|
+
- **Reactions are levels lining up.** A redox level is a weighted combination of species
|
|
182
|
+
voltages. For Fe³⁺ + e⁻ ⇌ Fe²⁺,
|
|
183
|
+
$`V_{\mathrm{e}^-}(\mathrm{Fe}^{3+}/\mathrm{Fe}^{2+}) = 3V_{\mathrm{Fe}^{3+}} - 2V_{\mathrm{Fe}^{2+}}`$.
|
|
184
|
+
For a plating couple with $`\mu_{\mathrm{M}} = 0`$, the couple's level coincides with the ion's
|
|
185
|
+
own line: $`V_{\mathrm{e}^-}(\mathrm{Ag}^+/\mathrm{Ag}) = V_{\mathrm{Ag}^+}`$.
|
|
186
|
+
- **Semiconductor band diagrams are already this picture.** $`V_{\mathrm{e}^-}`$ and
|
|
187
|
+
$`V_{\mathrm{h}^+}`$ are the quasi-Fermi levels, and their standard levels are the band edges.
|
|
188
|
+
Since $`V = -E/e`$ for electrons, the conduction band sits *below* the valence band in volts.
|
|
189
|
+
- **Small-signal response has the same rails.** Jamnik and Maier's exact equivalent circuit for
|
|
190
|
+
transport has a rail for each species' $`\bar\mu_i/(z_i F)`$, with resistors along it for
|
|
191
|
+
transport and chemical capacitances from it to a $`\phi`$ rail for storage (J. Jamnik and J. Maier,
|
|
192
|
+
[Phys. Chem. Chem. Phys. 3, 1668 (2001)](https://doi.org/10.1039/b100180i)). The
|
|
193
|
+
[impedance demo](https://markblundeberg.github.io/driftlet/demos/impedance.html) draws the voltages on those rails.
|
|
194
|
+
|
|
195
|
+
Different species' levels sit volts apart, because their $`\mu^\circ`$ differ by hundreds of
|
|
196
|
+
kJ/mol. The gap between two species' lines carries a constant fixed by convention (below), so
|
|
197
|
+
moving one species' lines up or down loses nothing. `traces()` does this with `shifts`, and marks
|
|
198
|
+
the shifted lines ⌇.
|
|
199
|
+
|
|
200
|
+
## What's left out, and why
|
|
201
|
+
|
|
202
|
+
### φ
|
|
203
|
+
|
|
204
|
+
The electrostatic potential $`\phi`$ isn't drawn by default, though it's in the same volts. Inside a
|
|
205
|
+
material, $`\phi`$ is a bookkeeping convention: each material's standard potentials fix where its
|
|
206
|
+
$`\phi`$ sits, and any consistent choice gives the same physics (see [conventions](conventions.md)).
|
|
207
|
+
Its steps between materials can't be measured, and inside a metal it has no measurable meaning at
|
|
208
|
+
all. And while the electron diagram is in volts, every line on it should be an electronically
|
|
209
|
+
meaningful level; $`-e\phi`$ isn't the energy of any electron.
|
|
210
|
+
|
|
211
|
+
What $`\phi`$ would show is drawn anyway, more usefully. The standard levels
|
|
212
|
+
$`V^\circ_i = \phi + \mu^\circ_i/(z_i F)`$ of a material move together with its $`\phi`$, so their
|
|
213
|
+
bending ladder carries the same variation in space within each material. Unlike $`\phi`$, the ladder
|
|
214
|
+
means something across materials too: its offsets at a face are band offsets and the like, which
|
|
215
|
+
belong to the interface and can be measured.
|
|
216
|
+
|
|
217
|
+
There are two places where $`\phi`$ is the right line to draw:
|
|
218
|
+
|
|
219
|
+
- **In an insulator** with no mobile charged species, there are no species levels to draw. Band
|
|
220
|
+
edges can stand in, unpopulated and with no Fermi level, but when the point is that nothing
|
|
221
|
+
mobile lives there, the plain sloped $`\phi`$ line says it best: its slope is the field. It's also
|
|
222
|
+
harmless to the reader, since there's no interfacial step in $`\phi`$ in view to mistake for
|
|
223
|
+
something measurable, just a lone line connected to nothing else. The
|
|
224
|
+
[MOS demo](https://markblundeberg.github.io/driftlet/demos/mos.html) draws $`\phi`$ across its oxide this way. It draws $`\phi`$ in its
|
|
225
|
+
silicon too, deliberately anchored at an arbitrary offset from the band edges and stepping at
|
|
226
|
+
the oxide's face, to show the band edges riding on it while its position is a convention.
|
|
227
|
+
- **In vacuum**, $`-e\phi`$ is exactly what it claims to be: the energy of an electron at rest there.
|
|
228
|
+
|
|
229
|
+
`traces(sol, { phi: true })` draws $`\phi`$ everywhere it's defined, and `phi: ['SiO2']` only in the
|
|
230
|
+
regions named. Besides the MOS demo, the demos draw it only as the SHE's standard level in water,
|
|
231
|
+
which is what it equals with the library's data.
|
|
232
|
+
|
|
233
|
+
### A reference
|
|
234
|
+
|
|
235
|
+
Nor is anything drawn against a reference: not $`E(x)`$ vs SHE, not energies from the vacuum. The
|
|
236
|
+
diagrams are reference-free. The simulation grounds one terminal at 0 V because it has to put the
|
|
237
|
+
zero somewhere, but nothing depends on where: shift the whole diagram up or down and the physics
|
|
238
|
+
is unchanged. So pick your own reference. Every vertical gap on the electron diagram is
|
|
239
|
+
physical, so choose a level at a point (the SHE's standard level in the stirred bulk, a reference
|
|
240
|
+
electrode's Fermi level, a band edge deep in a contact) and read everything else against it.
|
|
241
|
+
|
|
242
|
+
That's also why "vs SHE" and "from the vacuum" need a place. The SHE's standard level and the
|
|
243
|
+
vacuum level both vary in space wherever there's current or charge: the SHE's slopes with the
|
|
244
|
+
ohmic drop in a solution, and the vacuum level follows the electrostatics. Plotting $`E(x)`$
|
|
245
|
+
against the local SHE level would subtract a sloping line from every other line, mixing changes
|
|
246
|
+
in the reference into changes in the thing measured.
|
|
247
|
+
|
|
248
|
+
On the species diagram there's one technicality. A gap $`V_i - V_j`$ between two species at a point
|
|
249
|
+
is free of $`\phi`$ (it cancels), so its changes in space and time are physical. Its absolute value
|
|
250
|
+
carries one constant for that pair, though, fixed once by the chemical convention for the
|
|
251
|
+
elements' zeros ($`\mu = 0`$ for each element in its standard state, as in every table). For
|
|
252
|
+
instance, $`V_{\mathrm{Ag}^+} - V_{\mathrm{e}^-} = \mu_{\mathrm{Ag}}/F`$ at equilibrium, which is
|
|
253
|
+
zero only because the convention says so. Electron levels carry no such constant, which is why
|
|
254
|
+
the redox levels are drawn as electron levels: the conventions cancel out of
|
|
255
|
+
$`V_{\mathrm{e}^-}(\mathrm{Ag}^+/\mathrm{Ag}) = V_{\mathrm{Ag}^+} - \mu_{\mathrm{Ag}}/F`$.
|
|
256
|
+
|
|
257
|
+
These diagrams are developed at length, from what a voltmeter measures through semiconductors,
|
|
258
|
+
electrolytes and electrochemical cells, in
|
|
259
|
+
[Electrochemical species band diagrams](https://marklundeberg.com/esbd/).
|
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.2.0/src/index.js';
|
|
18
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.2.0/src/kit.js';
|
|
19
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.2.0/src/plot.js';
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
@@ -44,7 +44,8 @@ From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
|
44
44
|
- **Exports.** `driftlet`: `Device`, `units`, and the constants `FARADAY`, `GAS_CONSTANT`,
|
|
45
45
|
`EPS0`, `BOLTZMANN`, `ELEMENTARY_CHARGE`, `AVOGADRO` (there's no thermal-voltage constant:
|
|
46
46
|
V_T = GAS_CONSTANT·T/FARADAY). `driftlet/kit`: `build`, `layer`, `ohmic`, `bath`, `half`,
|
|
47
|
-
`level`, `SHE`, `describe`, `live`, `traces`, the
|
|
47
|
+
`level`, `SHE`, `describe`, `live`, `traces`, the sources (`pulse`, `square`, `triangle`, `ramp`,
|
|
48
|
+
`injector`, `recombination`), the data library (`aqueous`, `semiconductor`,
|
|
48
49
|
`metal`, `IONS`, …) and the vacuum-level helpers. `driftlet/plot`: `bandDiagram`, `levelChart`, `labelParts` (labels mark subscripts as `V_{e⁻}`).
|
|
49
50
|
- **Reactions** are written as equations, `{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }`, in
|
|
50
51
|
`new Device`, `build()` and `set()` alike. A coefficient is a number and a space (`2 e-`).
|
|
@@ -185,6 +186,42 @@ const dev = new Device(electrode(rest.terminals.left.V + 0.2));
|
|
|
185
186
|
console.log(`anodic current ${dev.solve().terminals.left.current.toFixed(2)} A/m²`);
|
|
186
187
|
```
|
|
187
188
|
|
|
189
|
+
## Template: a transient, with a pulse injected sideways
|
|
190
|
+
|
|
191
|
+
A **port** feeds a window of nodes inside a region from the side (a point contact, an injector,
|
|
192
|
+
a MOS channel's source and drain), held at a voltage or driven by a current; `injector()` writes
|
|
193
|
+
one that spreads a driven current evenly. Transients run with `advance(t)`, adaptively, from the
|
|
194
|
+
steady state that `solve()` finds at time 0, and `probes` read the device at points inside it.
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
import { Device, units, FARADAY } from 'driftlet';
|
|
198
|
+
import { build, layer, ohmic, semiconductor, pulse, injector, recombination } from 'driftlet/kit';
|
|
199
|
+
|
|
200
|
+
// A pulse of holes injected into an n-type germanium bar under a field (Haynes–Shockley), and
|
|
201
|
+
// the hole density read 1 mm downstream as it drifts past.
|
|
202
|
+
const L = 4e-3, ND = units.perCm3(1e15), E = 1000; // m, mol/m³, V/m (10 V/cm)
|
|
203
|
+
const width = 0.5e-6, I = (0.05 * ND * FARADAY * 50e-6) / width; // s; A/m²: 5% of n₀ over 50 µm
|
|
204
|
+
const def = build({
|
|
205
|
+
T: 300,
|
|
206
|
+
library: [semiconductor('Ge')],
|
|
207
|
+
stack: [ohmic(E * L), layer('Ge', L, { name: 'bar', donors: ND }), ohmic(0)], // holes drift toward +x
|
|
208
|
+
bulkReactions: [recombination({ material: 'Ge', tau: 20e-6, majority: ND })], // the minority lifetime
|
|
209
|
+
ports: [injector({ region: 'bar', from: 0.5e-3, to: 0.55e-3, species: 'h+', I: pulse({ width, amplitude: I, rise: 1e-9 }) })],
|
|
210
|
+
grid: { hmin: units.um(10), hmax: units.um(10) },
|
|
211
|
+
});
|
|
212
|
+
const dev = new Device(def);
|
|
213
|
+
const rest = dev.solve(); // the steady state at t = 0 (solve() doesn't advance time)
|
|
214
|
+
// In a page, call advance(tNext, { budgetMs: 12, probes }) once per animation frame, carrying on
|
|
215
|
+
// next frame while sol.done is false. Probes read at accepted steps; dtMax keeps them close.
|
|
216
|
+
const run = dev.advance(10e-6, { dtMax: 20e-9, probes: [{ x: 1.525e-3, species: 'h+' }] });
|
|
217
|
+
const seen = run.trace.probes[0], k = seen.indexOf(Math.max(...seen));
|
|
218
|
+
const tArrive = run.trace.t[k] - width / 2; // from the pulse's middle
|
|
219
|
+
console.log(`peak at ${(run.trace.t[k] * 1e6).toFixed(2)} µs, Δp ${units.toPerCm3(seen[k] - rest.c['h+'][0]).toExponential(1)} cm⁻³: μ_p ≈ ${(1e-3 / (E * tArrive) * 1e4).toFixed(0)} cm²/(V·s)`); // ≈ 1950 for the 1900 put in: a spreading, decaying pulse peaks a little early
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Transients cost more where a carrier's density spans many decades (minority holes in silicon
|
|
223
|
+
ahead of the pulse) and at strong injection (Δp near n₀); budget for that, or coarsen the grid.
|
|
224
|
+
|
|
188
225
|
## Template: a live page with a slider
|
|
189
226
|
|
|
190
227
|
`live()` merges slider moves made during a solve, warm-starts each solve and keeps the last
|
|
@@ -199,9 +236,9 @@ a Web Worker.
|
|
|
199
236
|
<span id="readout"></span>
|
|
200
237
|
<div id="figure"></div>
|
|
201
238
|
<script type="module">
|
|
202
|
-
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
203
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
204
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
239
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.2.0/src/index.js';
|
|
240
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.2.0/src/kit.js';
|
|
241
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.2.0/src/plot.js';
|
|
205
242
|
|
|
206
243
|
const def = build({
|
|
207
244
|
T: 300,
|
|
@@ -236,13 +273,15 @@ has the long form):
|
|
|
236
273
|
- **Standard redox level** (`{ standard: true }`), thin and dashed: every participant at reference
|
|
237
274
|
concentration. The redox level sits RT/(zF) ln(a_Ox/a_Red) from it (Nernst, locally).
|
|
238
275
|
- **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 μ°,
|
|
240
|
-
in water is φ, so `phi: true` draws it. It slopes under current: "vs SHE" needs a
|
|
241
|
-
the stirred bulk).
|
|
276
|
+
V°_e⁻(SHE), E° = V°_e⁻(Ox/Red) − V°_e⁻(SHE), η = E − E_eq. With the data library's μ°,
|
|
277
|
+
V°_e⁻(SHE) in water is φ, so `phi: true` draws it. It slopes under current: "vs SHE" needs a
|
|
278
|
+
place (say, the stirred bulk).
|
|
242
279
|
- **Species voltage** V_i = μ̄_i/(z_iF) (thick, solid) for every charged species: the Fermi level
|
|
243
280
|
for e⁻, a quasi-Fermi level for e⁻ and h⁺ in a semiconductor, what a reversible electrode reads
|
|
244
|
-
for an ion.
|
|
245
|
-
|
|
281
|
+
for an ion. It's a display of μ̄_i (the physics, which also covers neutral species): each
|
|
282
|
+
species moves down its own μ̄_i, drift and diffusion together, so its current flows down slopes
|
|
283
|
+
in V_i (cations and holes move downhill, electrons and anions uphill), and it's in equilibrium
|
|
284
|
+
where V_i is flat. A step at a face means resistance there.
|
|
246
285
|
- **Standard level** V°_i = φ + μ°_i/(z_iF) (thin, solid): a band edge for e⁻ and h⁺; V_i − V°_i =
|
|
247
286
|
(RT/z_iF) ln(c/c_ref) for ideal species. Steps in V°_i at faces (Donnan, junctions, dipoles) are
|
|
248
287
|
bookkeeping. In volts the conduction band sits below the valence band.
|
|
@@ -261,6 +300,21 @@ has the long form):
|
|
|
261
300
|
Maier (Phys. Chem. Chem. Phys. 3, 1668 (2001)) where V_i rails explain impedance or linear
|
|
262
301
|
response.
|
|
263
302
|
|
|
303
|
+
## Good habits
|
|
304
|
+
|
|
305
|
+
- **Draw the closed-form answer alongside the simulation**, wherever a textbook limit exists
|
|
306
|
+
(Shockley's law, a Nernst potential, a Gaussian pulse, a Warburg arc), as the demos and tests
|
|
307
|
+
do. It checks the setup: a solver's output looks authoritative even when an input is wrong, and
|
|
308
|
+
a mismatch is the cheapest way to catch it. Where the two should differ (high injection, a
|
|
309
|
+
finite pulse), say why.
|
|
310
|
+
- **Stay honestly inside 1D.** driftlet models planar devices. Where the real device isn't one,
|
|
311
|
+
say so and pick the slice that still tells the true story: a MOSFET's gate stack, read as the
|
|
312
|
+
MOS capacitor its channel sits on (pinch-off along the channel is 2D); a BJT along its
|
|
313
|
+
emitter–base–collector axis; one particle of a porous battery electrode, or a thin dense film,
|
|
314
|
+
rather than the whole porous electrode (that's a P2D model).
|
|
315
|
+
- **Check `sol.converged` and `sol.warnings`** on every solve, and pin the CDN version you
|
|
316
|
+
tested against.
|
|
317
|
+
|
|
264
318
|
## Common mistakes
|
|
265
319
|
|
|
266
320
|
- **No alignment at a heterointerface.** `interfaces[0] (between … [Si] and … [GaAs]): an
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driftlet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Small, dependency-free 1D drift–diffusion–reaction (Poisson–Nernst–Planck) solver for interactive web demos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"types": "./types/index.d.ts",
|
|
22
22
|
"files": [
|
|
23
23
|
"src",
|
|
24
|
+
"docs",
|
|
24
25
|
"types",
|
|
25
26
|
"llms.txt"
|
|
26
27
|
],
|
package/src/constants.js
CHANGED
|
@@ -33,4 +33,15 @@ export const units = Object.freeze({
|
|
|
33
33
|
um: (x) => x * 1e-6,
|
|
34
34
|
/** cm²/s → m²/s */
|
|
35
35
|
cm2PerS: (x) => x * 1e-4,
|
|
36
|
+
// Back out of SI, for display.
|
|
37
|
+
/** mol/m³ → mol/L */
|
|
38
|
+
toMolar: (x) => x * 1e-3,
|
|
39
|
+
/** mol/m³ → particles per cm³ */
|
|
40
|
+
toPerCm3: (x) => (x * AVOGADRO) / 1e6,
|
|
41
|
+
/** m → nm */
|
|
42
|
+
toNm: (x) => x * 1e9,
|
|
43
|
+
/** m → µm */
|
|
44
|
+
toUm: (x) => x * 1e6,
|
|
45
|
+
/** m²/s → cm²/s */
|
|
46
|
+
toCm2PerS: (x) => x * 1e4,
|
|
36
47
|
});
|
package/src/index.js
CHANGED
|
@@ -146,9 +146,12 @@ export class Device {
|
|
|
146
146
|
/**
|
|
147
147
|
* Adaptive transient to time tEnd (s): variable-step BDF2 with local error control. With
|
|
148
148
|
* `budgetMs`, it returns after that much wall time even if tEnd isn't reached (`done` says
|
|
149
|
-
* which), so an animation can call it once per frame; the step size carries over.
|
|
149
|
+
* which), so an animation can call it once per frame; the step size carries over. `probes`
|
|
150
|
+
* read a species at points inside the device after every accepted step, into `trace.probes`
|
|
151
|
+
* (one array per probe, alongside `trace.t`): what a detector at x sees.
|
|
150
152
|
* @param {number} tEnd s
|
|
151
|
-
* @param {{ tol?: number, dt0?: number, dtMax?: number, budgetMs?: number, maxSteps?: number, method?: 'bdf2' | 'be'
|
|
153
|
+
* @param {{ tol?: number, dt0?: number, dtMax?: number, budgetMs?: number, maxSteps?: number, method?: 'bdf2' | 'be',
|
|
154
|
+
* probes?: { x: number, species: string, quantity?: 'c' | 'V', region?: string | number }[] }} [opts]
|
|
152
155
|
* @returns {Solution}
|
|
153
156
|
*/
|
|
154
157
|
advance(tEnd, opts) {
|