driftlet 1.3.2 → 1.4.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 +1 -1
- package/docs/device.md +79 -6
- package/docs/internals.md +5 -2
- package/docs/kit.md +7 -3
- package/docs/numerics.md +11 -0
- package/docs/statistics.md +7 -2
- package/docs/templates.md +157 -2
- package/llms.txt +19 -14
- package/package.json +1 -1
- package/src/boundaries.js +200 -14
- package/src/checks.js +13 -2
- package/src/describe.js +3 -1
- package/src/device.js +70 -5
- package/src/diffuse.js +240 -0
- package/src/impedance.js +2 -1
- package/src/kit.js +6 -2
- package/src/lawRows.js +26 -1
- package/src/newton.js +31 -5
- package/src/regions.js +6 -3
- package/src/solution.js +46 -6
- package/src/solver.js +87 -9
- package/src/start.js +37 -8
- package/src/statistics.js +1 -0
- package/src/steady.js +51 -22
- package/src/timeSteps.js +10 -6
- package/src/types.js +16 -2
- package/types/boundaries.d.ts +8 -0
- package/types/diffuse.d.ts +25 -0
- package/types/newton.d.ts +1 -1
- package/types/solver.d.ts +8 -2
- package/types/types.d.ts +36 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ import { bandDiagram } from 'driftlet/plot'; // a level diagram as an SVG str
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Or straight from a CDN in a page, pinned to a version:
|
|
59
|
-
`https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
59
|
+
`https://cdn.jsdelivr.net/npm/driftlet@1.4.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
|
|
60
60
|
TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
|
|
61
61
|
[`llms.txt`](llms.txt).
|
|
62
62
|
|
package/docs/device.md
CHANGED
|
@@ -156,7 +156,8 @@ interfaces: [
|
|
|
156
156
|
{
|
|
157
157
|
step: { species: 'e-', value: units.eV(0.25) }, // alignment: or { dipole: volts }
|
|
158
158
|
phi: 'pinned', // 'pinned' | 'neutral' | { type: 'capacitive', C }
|
|
159
|
-
sheetCharge: 0, // C/m
|
|
159
|
+
sheetCharge: 0, // C/m², on the face's right plate
|
|
160
|
+
diffuse: { epsr: 78.4 }, // a Gouy–Chapman layer below the grid, beside a strictly neutral side
|
|
160
161
|
species: { 'Cl-': 'blocked', 'Li+': { type: 'conductance', G: 50 }, 'Na+': 'blocked' },
|
|
161
162
|
reactions: [{ left: { 'Na+': -1 }, right: { 'Na+': 1 }, k0: 1e-3, alpha: 0.5 }], // Na⁺ crosses only by this
|
|
162
163
|
},
|
|
@@ -187,10 +188,77 @@ defaults to no dipole.
|
|
|
187
188
|
in series with C. So keep that cell coarse rather than fine (a warning says when it costs C more
|
|
188
189
|
than 2%, and how coarse is enough). A face reaction there sees that cell's composition, which
|
|
189
190
|
holding the charge shifts by about $`z\sigma F/(C_{\mathrm{cell}} RT)`$ for an ion of charge $`z`$: a
|
|
190
|
-
Frumkin effect set by the mesh,
|
|
191
|
-
resistance in ferri/ferrocyanide
|
|
192
|
-
warning says past 1
|
|
193
|
-
|
|
191
|
+
Frumkin effect set by the mesh, which doesn't settle as the cell shrinks (an electrode's DC
|
|
192
|
+
resistance in ferri/ferrocyanide went from 56.6 to 47.4 Ω·cm² between 300 nm and 1 nm, the
|
|
193
|
+
real one being 46.4). A warning says past 1%. A diffuse layer (next) puts that charge where it
|
|
194
|
+
belongs, on any grid; a `zeroCharge` near the face's rest potential, so it holds little charge,
|
|
195
|
+
avoids the question.
|
|
196
|
+
|
|
197
|
+
**Diffuse layer** `diffuse: { epsr }`: on each strictly neutral side of a charged (pinned or
|
|
198
|
+
capacitive) face, a Gouy–Chapman layer below the grid, of relative permittivity `epsr` (the bulk's
|
|
199
|
+
is 0, so it's given here: 78.4 for water at 25 °C). Its ions are in Boltzmann equilibrium with the
|
|
200
|
+
edge node's concentrations $`c_i`$ across a drop $`\psi = \phi_{\mathrm{plane}} - \phi_{\mathrm{edge}}`$
|
|
201
|
+
($`s = F\psi/RT`$), and it holds that side's share of the face's charge,
|
|
202
|
+
|
|
203
|
+
```math
|
|
204
|
+
q_d = -\operatorname{sign}(s)\sqrt{2\varepsilon RT \sum_i c_i \left(e^{-z_i s} - 1 + z_i s\right)}, \qquad
|
|
205
|
+
\Gamma_i = \int (c_i(x) - c_i)\,dx,\quad F \sum_i z_i \Gamma_i = q_d
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
as each ion's surface excess $`\Gamma_i`$ (by quadrature of the same first integral; exact for any
|
|
209
|
+
mixture, a uniform fixed charge included). The face's law then runs to the plane: the Stern drop
|
|
210
|
+
is $`V_F - \mathtt{zeroCharge} - \phi_{\mathrm{plane}}`$, or between two materials
|
|
211
|
+
$`\phi_{R,\mathrm{plane}} - \phi_{L,\mathrm{plane}} - \mathtt{dipole}`$. The edge box holds what the
|
|
212
|
+
layer holds, $`A\,\Gamma_i`$ of each ion, in every amount, conservation law and transient (so its
|
|
213
|
+
bulk stays neutral, and the salt a charging layer takes comes out of the solution), and a face
|
|
214
|
+
reaction's participant on that side reacts at the plane, at $`c_i e^{-z_i s}`$ with its $`\bar\mu`$
|
|
215
|
+
unchanged: Butler–Volmer then depends on the Stern drop alone, the Frumkin correction, and the
|
|
216
|
+
mesh no longer enters (the ferri/ferrocyanide electrode: 46.399 Ω·cm² on cells from 1 nm to 300 nm,
|
|
217
|
+
the limit of the same electrode resolved at ε = 78.4 as its cells shrink to 0.0125 nm). Each
|
|
218
|
+
solution reports it as `interfaces[f].diffuse.left`/`.right`: `psi` (V), `charge` (C/m²), `excess`
|
|
219
|
+
(mol/m²) and `plane` (each ion's concentration there). With ideal ions it's a dilute layer, so a
|
|
220
|
+
warning says where its plane passes 5 M (a Stern C so large, or a zeroCharge so far from where the
|
|
221
|
+
face works, that its counter-ions would have no room there). Where the side's ions are on a
|
|
222
|
+
[lattice](statistics.md) instead, the layer crowds on the same sites (Bikerman): $`c_i e^{-z_i s}`$
|
|
223
|
+
over $`1 + \sum_j \theta_j (e^{-z_j s} - 1)`$, $`\theta_j = c_j/c_{\mathrm{max}}`$, and
|
|
224
|
+
$`\sum_i c_i (e^{-z_i s} - 1 + z_i s)`$ above becomes $`c_{\mathrm{max}}\ln(1 + \sum_j \theta_j (e^{-z_j s} - 1)) + s\sum_j c_j z_j`$, so the plane fills to $`c_{\mathrm{max}}`$
|
|
225
|
+
and no further, and a neutral species on the lattice is pushed out of it. Its ions must otherwise be
|
|
226
|
+
ideal (all of them on the one lattice, or none) and mobile, and SRH kinetics beside a layer isn't
|
|
227
|
+
supported.
|
|
228
|
+
|
|
229
|
+
What a face reaction takes or makes at the plane crosses the layer, against the layer's
|
|
230
|
+
resistance to it, $`K_i = \int (c_i/c_i(x) - 1)\,dx`$ (a length, by the same quadrature): its level
|
|
231
|
+
at the plane is below the edge node's by what its flux $`N_i`$ toward the plane needs,
|
|
232
|
+
$`a_{\mathrm{plane}} = a_{\mathrm{edge}}\,(1 - N_i K_i/(D_i c_i))`$, one more unknown per such species
|
|
233
|
+
(the `plane` it reports includes it). That's exact for a species too dilute to shape the layer,
|
|
234
|
+
which is the case that matters: a reactant the face's charge repels, for which $`K_i`$ grows as
|
|
235
|
+
$`c_i/c_{i,\mathrm{plane}}`$, so the plane can run out of it. Its current then rises and falls as the
|
|
236
|
+
electrode is driven harder, Frumkin's dip (ferricyanide reduced at a negatively charged electrode
|
|
237
|
+
in 0.1 M KCl, as peroxodisulfate is): within 1.5e-6 of the same electrode resolved, where leaving
|
|
238
|
+
the resistance out had it 68% high; and a plane 99.7% full of ferrocyanide, which its other ions
|
|
239
|
+
cross only through it, puts the electrode's R_dc within 1e-8 of the resolved one's (6% off
|
|
240
|
+
without it). Where the layer gathers a species, $`K_i < 0`$, bounded by about the layer's width: a
|
|
241
|
+
correction of order $`\lambda_D/L`$. What the layer doesn't cover:
|
|
242
|
+
- a salt run out at the electrode, toward and past an unsupported electrolyte's limiting current:
|
|
243
|
+
the layer spreads into the solution there (an extended space charge), and where the resolved
|
|
244
|
+
electrode's current climbs past the limit, the law's stays at it (silver deposited from 10 mM
|
|
245
|
+
AgNO₃ across 100 µm: 8e-4 high at 96% of the limiting current, 1% low at −1.5 V, well past it);
|
|
246
|
+
- a solution beside it only a few Debye lengths wide (a nanopore, overlapping layers): the layer is
|
|
247
|
+
taken whole, as if the solution went on;
|
|
248
|
+
- a permeability link's ions crossing it: at a membrane they're at the planes in equilibrium with
|
|
249
|
+
the edges (the membrane, many orders less permeable than the layer, sets their flux);
|
|
250
|
+
- ions of different sizes, and the field's lowering of ε near the plane: one site for every ion
|
|
251
|
+
(Bikerman), and one permittivity.
|
|
252
|
+
|
|
253
|
+
At a membrane between two solutions, a layer on each side: a
|
|
254
|
+
permeability link runs between the planes (Goldman–Hodgkin–Katz on the planes' concentrations,
|
|
255
|
+
across the drop between them), and gates follow the voltage between the planes, the one the
|
|
256
|
+
membrane itself has across it (`interfaces[f].V`), so a surface charge (`sheetCharge`) and the
|
|
257
|
+
ions that screen it shift a channel's gating and permeation, as divalent cations do. A face's
|
|
258
|
+
`sheetCharge` sits on its right plate: beside a layer on the right, at that layer's plane (ions
|
|
259
|
+
adsorbed at the outer Helmholtz plane, the Stern layer between them and the left side). For a
|
|
260
|
+
charge on the left plate (an oxide's surface groups, its Stern layer on the solution's side),
|
|
261
|
+
build the stack the other way round.
|
|
194
262
|
|
|
195
263
|
**Species laws** `species` (default: local equilibrium, $`\bar\mu`$ continuous, where the species is
|
|
196
264
|
present on both sides and can move on one at least; blocked otherwise, so trap states with
|
|
@@ -541,7 +609,12 @@ levels are that material's, and the end node takes them through a Donnan step.
|
|
|
541
609
|
- `'pinned'` with `zeroCharge`: the $`C \to \infty`$ limit,
|
|
542
610
|
$`\phi_{\mathrm{edge}} = V - \mathtt{zeroCharge}`$. This is what a "fixed $`\phi`$" boundary honestly
|
|
543
611
|
means. For example, a Schottky barrier $`\phi_B`$ on n-type material with
|
|
544
|
-
$`\mu^\circ_{\mathrm{e}^-} = 0`$ is `zeroCharge: φ_B`.
|
|
612
|
+
$`\mu^\circ_{\mathrm{e}^-} = 0`$ is `zeroCharge: φ_B`. With a carrier linked in equilibrium, a
|
|
613
|
+
pinned contact sets that carrier's density at the edge,
|
|
614
|
+
$`c_{\mathrm{edge}} = c_{\mathrm{ref}} \exp\big((z F\,\mathtt{zeroCharge} - \mu^\circ)/RT\big)`$, so a contact that injects holes at
|
|
615
|
+
$`N_v`$ (their `cRef`, $`\mu^\circ = 0`$) is `zeroCharge: 0`, and a 0.5 eV barrier for them is
|
|
616
|
+
`zeroCharge: -0.5`. This is the contact for a layer of one carrier with nothing to neutralise it
|
|
617
|
+
(a hole-only organic diode): `ohmic()`'s `'bulk'` law, local neutrality, can't hold there.
|
|
545
618
|
|
|
546
619
|
An ideally blocking electrode (inert: no reactions, nothing crossing) is a contact with no
|
|
547
620
|
terminal species and no links, `{ V, phi: 'pinned', zeroCharge }`: the solution's diffuse layer
|
package/docs/internals.md
CHANGED
|
@@ -20,7 +20,8 @@ them is wrong: fix both, with a test, in the same commit.
|
|
|
20
20
|
| `src/newton.js` | yes: Newton's iteration, its scaled rows, GMRES-refined solves and the islands' round |
|
|
21
21
|
| `src/assemble.js` | yes: the equations assembled, the conventions of every row, and the order of the terms |
|
|
22
22
|
| `src/regions.js` | yes: statistics, storage, fluxes along segments, bulk reactions, the neutral nodes' charge rows |
|
|
23
|
-
| `src/boundaries.js` | yes: faces (φ laws, links, reactions, gates), ports, contacts, circuit rows |
|
|
23
|
+
| `src/boundaries.js` | yes: faces (φ laws, links, reactions, gates, diffuse layers), ports, contacts, circuit rows |
|
|
24
|
+
| `src/diffuse.js` | yes: a Gouy–Chapman layer's charge, surface excesses, resistances and their derivatives |
|
|
24
25
|
| `src/steady.js` | yes: steady solves, their stages and continuations, the pseudo-transient ramp |
|
|
25
26
|
| `src/impedance.js` | yes: the small-signal impedance, its readouts and profiles |
|
|
26
27
|
| `src/difference.js` | header: the dilute kernels in difference form |
|
|
@@ -47,7 +48,9 @@ them is wrong: fix both, with a test, in the same commit.
|
|
|
47
48
|
`blockOfNode[g]`, face f's flux block is `blockOfFace[f]`, in order along x.
|
|
48
49
|
- **Slots.** Each block has `M = 1 + n + extra` slots: slot 0 is φ̂ (at a face its
|
|
49
50
|
displacement D, at a metal's node its carriers' segment flux J), slots `1 … n` the species'
|
|
50
|
-
η = μ̄/RT, then a face's reaction rates
|
|
51
|
+
η = μ̄/RT, then a face's reaction rates, gates' fractions, diffuse layers' drops (`diffList`,
|
|
52
|
+
each layer's excesses in `gam`) and the levels at their planes of the species its reactions
|
|
53
|
+
take or make there (each layer's `drawSlot`, as η), or at an electrode port's nodes
|
|
51
54
|
its surface's coverages (as η) and a membrane port's gates. Row and unknown `o = b·M + slot`.
|
|
52
55
|
- **Compact indices.** Only active slots enter the linear system: `loc[o]` is a slot's place in
|
|
53
56
|
its block (−1: inactive), `rix[o]` its index in compact vectors, with `rix = N` (the sink) for
|
package/docs/kit.md
CHANGED
|
@@ -59,8 +59,9 @@ console.log(`I = ${sol.current.toFixed(1)} A/m²`);
|
|
|
59
59
|
infinite-recombination contact; `['e-']` alone is selective, and is the current collector on
|
|
60
60
|
a metal region). `bath(c, reference, drive, { offset, material })` is a bath whose terminal voltage is
|
|
61
61
|
the level of its `reference` species, as an electrode reversible to it would read (`offset`
|
|
62
|
-
places that species relative to the terminal voltage, e.g. to read a
|
|
63
|
-
against SHE).
|
|
62
|
+
places that species relative to the terminal voltage, its level at V + `offset`, e.g. to read a
|
|
63
|
+
solution's potentials against SHE). Every species a bath lists is held at its level there, so
|
|
64
|
+
one absent from the bulk (a product) is given as a trace, not 0. `bath(c, drive)`, without a reference, is a bath whose terminal voltage is its
|
|
64
65
|
φ: an ideal salt bridge, the convention of membrane potentials, so that between two such baths
|
|
65
66
|
V_right − V_left is the φ difference a voltage clamp sets. A drive is a voltage, a waveform, or
|
|
66
67
|
`{ V }`, `{ I }`, `{ V, R }`. A bath against a charged layer (an ion exchanger) gives its own
|
|
@@ -448,7 +449,10 @@ page can show that its numbers hold up. Pass the solution `solve()` or `advance(
|
|
|
448
449
|
term named by its equation; two with the same equation (radiative and SRH recombination, both
|
|
449
450
|
`e- + h+ = 0`) are told apart by their place, `e- + h+ = 0 (bulkReactions[1])`, and
|
|
450
451
|
`sol.bulkReactions[k]` has each one's rates. A species at rest
|
|
451
|
-
(in equilibrium, flows only of round-off against what it could carry) isn't listed.
|
|
452
|
+
(in equilibrium, flows only of round-off against what it could carry) isn't listed. Where none
|
|
453
|
+
is left, it says "equilibrium" only if no flow comes within ε of what it could carry; otherwise
|
|
454
|
+
it names the largest, which may be round-off or a real flow too small to check this way (a
|
|
455
|
+
reverse diode's 1 µA/m², 4e-15 of what its holes could carry).
|
|
452
456
|
- **conservation**, in a transient: every closed, unreacting stretch keeps what it held plus
|
|
453
457
|
what came in.
|
|
454
458
|
- **order**, in a steady state (unless `refine: false` or `order: false`): the device solved from
|
package/docs/numerics.md
CHANGED
|
@@ -951,6 +951,17 @@ A solve that fails falls back stage by stage:
|
|
|
951
951
|
the floated solve fails, held solves find that voltage instead, by regula falsi within the
|
|
952
952
|
bracket, to 1e-10 of the current. (Not a terminal at no current that keeps an amount or a
|
|
953
953
|
charge: every voltage passes nothing there.)
|
|
954
|
+
- **A resistance.** A terminal behind a resistance R, its source at $`V_{\mathrm{src}}`$, is
|
|
955
|
+
found the same way, held at voltages marched from the held terminal's level until the
|
|
956
|
+
device's current crosses the load line's, $`(V_{\mathrm{src}} - V)/R`$, and floated from
|
|
957
|
+
beside it. Held at its source instead and the resistance put back, the state would sit
|
|
958
|
+
$`IR`$ from the answer, and the pseudo-transient relaxes that over $`RC`$ at the pace Newton
|
|
959
|
+
takes through the swing: a lit Schottky diode passing 0.9 A/m² took 0.05 s behind
|
|
960
|
+
1 Ω·m² and 105 s behind 10⁵ Ω·m² that way, and takes 33 ms behind any R by the load line
|
|
961
|
+
(80 fuzzed diodes from 0.01 to 10⁷ Ω·m²: 1835 s → 34 s). That route remains for terminals the load line doesn't
|
|
962
|
+
fit: several behind resistances, or one beside terminals all driven by currents, where
|
|
963
|
+
Kirchhoff gives its current and it's held at $`V_{\mathrm{src}} - IR`$ (from there, putting
|
|
964
|
+
$`R`$ back moves nothing).
|
|
954
965
|
- **The voltages.** The right terminal's voltage ramped to its target: from the contacts'
|
|
955
966
|
voltages at the last converged solve when the state is that solution (the left one first,
|
|
956
967
|
where it moved), and otherwise from level terminals, where a cold start is consistent. (A
|
package/docs/statistics.md
CHANGED
|
@@ -69,7 +69,8 @@ The listed species share $`c_{\mathrm{max}}`$ sites per volume and each occupies
|
|
|
69
69
|
concentration can exceed $`c_{\mathrm{max}}`$. With both ions of an electrolyte on one lattice, this
|
|
70
70
|
is the Bikerman / Kilic–Bazant–Ajdari model of a crowded double layer. At large potentials the
|
|
71
71
|
counter-ion saturates at $`c_{\mathrm{max}}`$, and the diffuse charge grows as $`\sqrt{\psi}`$ instead
|
|
72
|
-
of exponentially
|
|
72
|
+
of exponentially: resolved by the grid (ε > 0), or below it, a face's `diffuse` layer beside a
|
|
73
|
+
strictly neutral solution on a lattice crowds on the same sites ([faces](device.md)). With one species it's Langmuir filling of sites. The dilute reference
|
|
73
74
|
$`c_{\mathrm{ref}}`$ is kept. To use the half-filling convention of site models
|
|
74
75
|
($`e^\zeta = x/(1-x)`$), set `cRef` equal to `cMax`.
|
|
75
76
|
|
|
@@ -105,7 +106,11 @@ $`\ln\gamma_i = -z_i^2 \ell_B \kappa / (2(1 + \kappa a))`$, with the Bjerrum len
|
|
|
105
106
|
$`\ell_B = e^2/(4\pi\varepsilon kT)`$ and $`\kappa^2 = F^2 \sum z^2 c / (\varepsilon RT)`$ from the
|
|
106
107
|
solvent permittivity `epsr` (which defaults to the material's, if that's non-zero) and a
|
|
107
108
|
distance of closest approach `a` (default 0, the limiting law). It stops being convex at high
|
|
108
|
-
ionic strength, and the solver then reports a failure.
|
|
109
|
+
ionic strength, and the solver then reports a failure. It goes on a material's `statistics`, as
|
|
110
|
+
any model does; with `aqueous()`, on the piece it returns: `lib.materials.water.statistics =
|
|
111
|
+
[{ type: 'debye-huckel', species: ['H+', 'Cl-'], a: 4.5e-10 }]`. `a` is an ion size (a few
|
|
112
|
+
ångström: tabulated per ion, as Kielland's sizes, or fitted to measured γ±), and it matters:
|
|
113
|
+
in 10 mM HCl, 4.5 Å against the limiting law moves a concentration cell's voltage by 0.6 mV.
|
|
109
114
|
|
|
110
115
|
Single-ion activity coefficients are a convention: only neutral combinations (the mean
|
|
111
116
|
activity $`\gamma_\pm`$) are measurable. In a strictly neutral ($`\varepsilon = 0`$) material only
|
package/docs/templates.md
CHANGED
|
@@ -4,9 +4,13 @@ Templates for devices outside [llms.txt](../llms.txt)'s core set, written the sa
|
|
|
4
4
|
block runs in the test suite and prints its closed-form check. Read llms.txt first; these build
|
|
5
5
|
on it.
|
|
6
6
|
|
|
7
|
-
- semiconductors: [a Schottky diode](#a-schottky-diode), [a thin-film transistor](#a-thin-film-transistor-a-capacitance-along-a-channel)
|
|
7
|
+
- semiconductors: [a Schottky diode](#a-schottky-diode), [a thin-film transistor](#a-thin-film-transistor-a-capacitance-along-a-channel),
|
|
8
|
+
[traps in a layer of holes alone](#traps-in-a-layer-of-holes-alone-a-space-charge-limited-diode);
|
|
8
9
|
- electrochemistry: [corrosion under a drop](#corrosion-under-a-drop-an-electrode-spread-through-a-film)
|
|
9
|
-
(an electrode spread through a film, and a cross-section that varies)
|
|
10
|
+
(an electrode spread through a film, and a cross-section that varies),
|
|
11
|
+
[an electrode's double-layer capacitance](#an-electrodes-double-layer-capacitance-gouychapmanstern),
|
|
12
|
+
[a semiconductor electrode's Mott–Schottky plot](#a-semiconductor-electrodes-mottschottky-plot),
|
|
13
|
+
[a concentration cell with transference](#a-concentration-cell-with-transference);
|
|
10
14
|
- physiology: [a voltage clamp](#a-voltage-clamp), [an action potential](#an-action-potential),
|
|
11
15
|
[one travelling along an axon](#an-action-potential-travelling-along-an-axon).
|
|
12
16
|
|
|
@@ -77,6 +81,45 @@ To sweep the gate on one device instead, patch the port: `dev.set({ ports: { gat
|
|
|
77
81
|
An organic electrochemical transistor is the same with a volumetric capacitance C*:
|
|
78
82
|
`area: 1 / t, capacitance: { C: Cstar * t }`.
|
|
79
83
|
|
|
84
|
+
## traps in a layer of holes alone (a space-charge-limited diode)
|
|
85
|
+
|
|
86
|
+
An organic layer with holes and no dopant can't be neutral anywhere, so `ohmic()`'s `'bulk'` law
|
|
87
|
+
can't hold at its contacts: **pinned contacts** set the hole density at each edge instead
|
|
88
|
+
(`zeroCharge` = −barrier for holes of $`\mu^\circ = 0`$, density $`N_v e^{-\mathrm{barrier}/kT}`$). A **trap**
|
|
89
|
+
is two immobile species, empty and filled, and a reaction between them; its depth is the filled
|
|
90
|
+
one's $`\mu^\circ`$. In a steady state immobile traps sit at their reaction's equilibrium at every
|
|
91
|
+
node, under current too, so their filling is exactly $`p/(p + p_1)`$; past the trap-filled limit
|
|
92
|
+
the current climbs toward the trap-free space-charge-limited one.
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
import { Device, units, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
96
|
+
import { build, layer } from 'driftlet/kit';
|
|
97
|
+
|
|
98
|
+
const T = 300, VT = (GAS_CONSTANT * T) / FARADAY, Nv = units.perCm3(1e21), Nt = units.perCm3(1e17), Et = 0.45;
|
|
99
|
+
// Holes alone (an organic semiconductor's), and a trap level Et above the valence band: empty T0,
|
|
100
|
+
// filled T+, h⁺ + T0 = T+. The filled one's μ° below the empty's sets the depth.
|
|
101
|
+
const lib = {
|
|
102
|
+
species: [{ name: 'h+', z: 1, cRef: Nv }, { name: 'T0', z: 0, cRef: 1 }, { name: 'T+', z: 1, cRef: 1 }],
|
|
103
|
+
materials: { org: { epsr: 3, T, species: { 'h+': { D: 1e-8 * VT, mu0: 0 }, T0: { D: 0, mu0: 0 }, 'T+': { D: 0, mu0: -units.eV(Et) } } } },
|
|
104
|
+
};
|
|
105
|
+
// Pinned contacts set the hole density at each edge: Nv at the anode, Nv·e^(−0.5 eV/kT) at the cathode.
|
|
106
|
+
const contact = (V, barrier) => ({ V, terminal: 'h+', species: { 'h+': 'equilibrium' }, phi: 'pinned', zeroCharge: -barrier });
|
|
107
|
+
const diode = (V, traps) => build({
|
|
108
|
+
T, library: [lib],
|
|
109
|
+
stack: [contact(V, 0), layer('org', 100e-9, { c0: { T0: Nt / 2, 'T+': Nt / 2 } }), contact(0, 0.5)],
|
|
110
|
+
bulkReactions: traps ? [{ equation: 'h+ + T0 = T+', kf: { org: 1e6 } }] : [],
|
|
111
|
+
grid: { hmin: 0.01e-9, hmax: 1e-9, ratio: 1.15 },
|
|
112
|
+
});
|
|
113
|
+
const p1 = Nv * Math.exp(-Et / VT); // the hole density at which a trap is half full
|
|
114
|
+
for (const V of [1, 3, 10]) {
|
|
115
|
+
const s = new Device(diode(V, true)).solve(), free = new Device(diode(V, false)).solve();
|
|
116
|
+
// Immobile traps in a steady state sit at their reaction's equilibrium at every node: T+/Nt = p/(p + p1).
|
|
117
|
+
const off = Math.max(...Array.from(s.c['h+'], (p, g) => Math.abs(s.c['T+'][g] / Nt - p / (p + p1))));
|
|
118
|
+
const mid = s.x.length >> 1;
|
|
119
|
+
console.log(`${V} V: J ${s.current.toExponential(3)} A/m², ${(s.current / free.current).toFixed(3)} of the trap-free; traps ${(100 * s.c['T+'][mid] / Nt).toFixed(1)}% full mid-layer; occupancy off p/(p + p1) by ${off.toExponential(1)}`);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
80
123
|
## corrosion under a drop (an electrode spread through a film)
|
|
81
124
|
|
|
82
125
|
A **port with reactions** is an electrode spread through a strictly neutral region: a metal under
|
|
@@ -137,6 +180,118 @@ for (const rx of evans.reactions) console.log(`${rx.equation}: ${Array.from(rx.c
|
|
|
137
180
|
Species can take an electrode's surface sites (`surface`, Langmuir coverages), and a reaction
|
|
138
181
|
with `bare: true` runs only on bare metal: a passive film (docs/device.md).
|
|
139
182
|
|
|
183
|
+
## an electrode's double-layer capacitance (Gouy–Chapman–Stern)
|
|
184
|
+
|
|
185
|
+
A blocking metal in dilute KCl, the solution strictly neutral (`epsr: 0`) and the face's double
|
|
186
|
+
layer below the grid: a Stern layer (`phi: { type: 'capacitive', C }`) and its diffuse layer
|
|
187
|
+
(`diffuse: { epsr }`). The capacitance from `impedance()` at a low frequency has the classic
|
|
188
|
+
minimum at the potential of zero charge, where the diffuse layer is thickest, and follows Stern and
|
|
189
|
+
Gouy–Chapman in series, $`1/C = 1/C_H + 1/(\varepsilon\kappa\cosh(F\psi/2RT))`$, with the drop ψ
|
|
190
|
+
the solution reports. (The salt the layer takes in or gives up comes from the bath by diffusion:
|
|
191
|
+
at 0.1 Hz across this 100 µm it shows, 2e-5 at ±0.2 V.)
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
import { Device, EPS0, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
195
|
+
import { build, layer, ohmic, bath, aqueous, metal } from 'driftlet/kit';
|
|
196
|
+
|
|
197
|
+
const T = 298.15, VT = (GAS_CONSTANT * T) / FARADAY, CH = 0.2, epsr = 78.4; // a Stern layer of 20 µF/cm²
|
|
198
|
+
const salt = { 'K+': 10, 'Cl-': 10 }; // 10 mM KCl
|
|
199
|
+
const cell = (V) => build({
|
|
200
|
+
T, library: [aqueous(['K+', 'Cl-'], { epsr: 0 }), metal('Pt')],
|
|
201
|
+
stack: [ohmic(V, ['e-']), layer('Pt', 1e-6), { phi: { type: 'capacitive', C: CH }, zeroCharge: 0, diffuse: { epsr } }, layer('water', 100e-6, { c0: salt }), bath(salt, 'Cl-')],
|
|
202
|
+
grid: { hmin: 1e-6, hmax: 10e-6 },
|
|
203
|
+
});
|
|
204
|
+
// The potential of zero charge: the solution's φ at the face (zeroCharge 0: V_F − φ_plane there).
|
|
205
|
+
const first = new Device(cell(0)).solve(), pzc = first.phi[first.region.indexOf(1)];
|
|
206
|
+
const kappa = Math.sqrt((2 * FARADAY ** 2 * 10) / (epsr * EPS0 * GAS_CONSTANT * T)); // 1/Debye length
|
|
207
|
+
for (const E of [-0.2, -0.1, 0, 0.1, 0.2]) {
|
|
208
|
+
const dev = new Device(cell(pzc + E));
|
|
209
|
+
const psi = dev.solve().interfaces[0].diffuse.right.psi; // the diffuse layer's drop
|
|
210
|
+
const f = 1e-3, { Z } = dev.impedance([f]), C = -1 / (2 * Math.PI * f * Z.im[0]);
|
|
211
|
+
const gcs = 1 / (1 / CH + 1 / (epsr * EPS0 * kappa * Math.cosh(psi / (2 * VT))));
|
|
212
|
+
console.log(`${E.toFixed(1)} V from the pzc: C ${(100 * C).toFixed(3)} µF/cm², Gouy–Chapman–Stern ${(100 * gcs).toFixed(3)}`);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## a semiconductor electrode's Mott–Schottky plot
|
|
217
|
+
|
|
218
|
+
A semiconductor dipped in an electrolyte, the face between them **blocking** (no reactions), with
|
|
219
|
+
its **alignment** given (a `dipole`: what puts silicon's levels against the solution's is a
|
|
220
|
+
choice, here Si's electron affinity against an absolute SHE, not a datum). Its capacitance from
|
|
221
|
+
`impedance()` at a frequency where the space charge is what's measured, $`1/C^2`$ against the
|
|
222
|
+
voltage across the depletion range, gives back the doping from the slope and the flat band from
|
|
223
|
+
the intercept less kT/q. Past about $`2\phi_F`$ of depletion, holes gather at the face (inversion):
|
|
224
|
+
keep the fit below it.
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
import { Device, units, EPS0, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
228
|
+
import { build, layer, ohmic, bath, semiconductor, aqueous } from 'driftlet/kit';
|
|
229
|
+
|
|
230
|
+
const T = 298.15, VT = (GAS_CONSTANT * T) / FARADAY, ND = units.perCm3(1e16);
|
|
231
|
+
const water = { 'K+': 100, 'Cl-': 100 }; // 0.1 M KCl
|
|
232
|
+
const lib = [semiconductor('Si', { T }), aqueous(['K+', 'Cl-'])];
|
|
233
|
+
// n-Si | a blocking face | 40 nm of the solution, resolved | the bath, read against an electrode reversible to Cl⁻.
|
|
234
|
+
// The face's dipole aligns the two (here Si's electron affinity against the solution: an assumption, not data).
|
|
235
|
+
const cell = (V) => build({
|
|
236
|
+
T, library: lib,
|
|
237
|
+
stack: [ohmic(V), layer('Si', units.um(2), { donors: ND }), { dipole: 0.39 }, layer('water', 40e-9, { c0: water }), bath(water, 'Cl-')],
|
|
238
|
+
grid: { hmin: 0.1e-9, hmax: 5e-9, ratio: 1.1 },
|
|
239
|
+
});
|
|
240
|
+
const pts = [];
|
|
241
|
+
for (const V of [-1.3, -1.2, -1.1, -1.0]) { // the silicon positive: its electrons pushed back, a depletion layer
|
|
242
|
+
const dev = new Device(cell(V));
|
|
243
|
+
dev.solve();
|
|
244
|
+
const { f, Z } = dev.impedance([1e5]); // 100 kHz: the space charge, the double layer in series negligible
|
|
245
|
+
const w = 2 * Math.PI * f[0], C = -Z.im[0] / (w * (Z.re[0] ** 2 + Z.im[0] ** 2));
|
|
246
|
+
pts.push([V, 1 / C ** 2]);
|
|
247
|
+
}
|
|
248
|
+
// 1/C² = 2/(F ε N_D) (V − V_fb − kT/q) in depletion (N_D in mol/m³, as driftlet's are)
|
|
249
|
+
const slope = (pts.at(-1)[1] - pts[0][1]) / (pts.at(-1)[0] - pts[0][0]), V0 = pts[0][0] - pts[0][1] / slope;
|
|
250
|
+
console.log(`N_D from the slope: ${units.toPerCm3(2 / (FARADAY * 11.9 * EPS0 * slope)).toPrecision(5)} cm⁻³ (1e16 put in)`);
|
|
251
|
+
// The model's own flat band: where the silicon holds no charge at the face (interfaces[0].D = 0).
|
|
252
|
+
let lo = -2, hi = -1.3;
|
|
253
|
+
for (let k = 0; k < 40; k++) {
|
|
254
|
+
const m = (lo + hi) / 2, D = new Device(cell(m)).solve().interfaces[0].D;
|
|
255
|
+
if (D < 0) lo = m;
|
|
256
|
+
else hi = m;
|
|
257
|
+
}
|
|
258
|
+
console.log(`flat band: ${(V0 - VT).toFixed(4)} V from the intercept less kT/q, ${hi.toFixed(4)} V where the face holds no charge`);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## a concentration cell with transference
|
|
262
|
+
|
|
263
|
+
Two electrodes reversible to Cl⁻ (`bath(c, 'Cl-')`), one held and one at open circuit, with
|
|
264
|
+
two strengths of HCl between them meeting at a **liquid junction**. For one salt the voltage
|
|
265
|
+
doesn't depend on the junction's profile, so baths at both ends holding it stationary give
|
|
266
|
+
the free-diffusion junction's value: $`E = -2t_+ (RT/F)\ln(a_{\pm,2}/a_{\pm,1})`$, the junction's
|
|
267
|
+
share $`(2t_+ - 1)/(2t_+)`$. Activity coefficients go on the water's `statistics`.
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
import { Device, FARADAY, GAS_CONSTANT, EPS0, AVOGADRO } from 'driftlet';
|
|
271
|
+
import { build, layer, bath, aqueous, IONS } from 'driftlet/kit';
|
|
272
|
+
|
|
273
|
+
const T = 298.15, VT = (GAS_CONSTANT * T) / FARADAY;
|
|
274
|
+
const dilute = { 'H+': 1, 'Cl-': 1 }, strong = { 'H+': 10, 'Cl-': 10 }; // mol/m³: 1 and 10 mM HCl
|
|
275
|
+
const tPlus = IONS['H+'].D / (IONS['H+'].D + IONS['Cl-'].D);
|
|
276
|
+
// Ag|AgCl at each end: a bath read against an electrode reversible to Cl⁻. The left one held, the
|
|
277
|
+
// right at open circuit; between them a junction held stationary by the two baths (strictly
|
|
278
|
+
// neutral water, ε = 0: the junction is 2 mm wide, its double layers nothing to it).
|
|
279
|
+
for (const activity of [null, { type: 'debye-huckel', species: ['H+', 'Cl-'], epsr: 78.4, a: 0 }]) {
|
|
280
|
+
const lib = aqueous(['H+', 'Cl-'], { epsr: 0 });
|
|
281
|
+
if (activity) lib.materials.water.statistics = [activity];
|
|
282
|
+
const s = new Device(build({
|
|
283
|
+
T, library: [lib],
|
|
284
|
+
stack: [bath(dilute, 'Cl-'), layer('water', 2e-3, { c0: dilute }), { phi: 'neutral' }, layer('water', 2e-3, { c0: strong }), bath(strong, 'Cl-', { I: 0 })],
|
|
285
|
+
grid: { hmin: 1e-7, hmax: 2e-5 },
|
|
286
|
+
})).solve();
|
|
287
|
+
// E = −2 t₊ (RT/F) ln(a±₂/a±₁), the limiting law's ln γ± = −ℓ_B κ/2 (Bjerrum length, Debye κ)
|
|
288
|
+
const eps = EPS0 * (activity?.epsr ?? 1), lB = FARADAY ** 2 / (4 * Math.PI * eps * GAS_CONSTANT * T * AVOGADRO);
|
|
289
|
+
const lnGamma = (c) => (activity ? (-lB * Math.sqrt((2 * FARADAY ** 2 * c) / (eps * GAS_CONSTANT * T))) / 2 : 0);
|
|
290
|
+
const E = s.terminals.right.V - s.terminals.left.V, theory = -2 * tPlus * VT * (Math.log(10) + lnGamma(10) - lnGamma(1));
|
|
291
|
+
console.log(`${activity ? 'Debye–Hückel' : 'ideal'}: ${(E * 1e3).toFixed(3)} mV, theory ${(theory * 1e3).toFixed(3)}; the junction's share ${(2 * tPlus - 1) / (2 * tPlus)}`);
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
140
295
|
## a voltage clamp
|
|
141
296
|
|
|
142
297
|
A face's `gates` (each a fraction open, $`dx/dt = \alpha(V)(1 - x) - \beta(V)x`$ with $`V = \phi_{\mathrm{right}} - \phi_{\mathrm{left}}`$)
|
package/llms.txt
CHANGED
|
@@ -17,9 +17,11 @@ two sections, then go to the template nearest your device:
|
|
|
17
17
|
voltammogram), a battery particle discharging;
|
|
18
18
|
- physiology: a cell membrane (a resting potential).
|
|
19
19
|
|
|
20
|
-
More in docs/templates.md: a Schottky diode, a thin-film transistor,
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
More in docs/templates.md: a Schottky diode, a thin-film transistor, traps in a layer of holes
|
|
21
|
+
alone (pinned contacts that set a carrier's density), corrosion under a drop (an electrode spread
|
|
22
|
+
through a film), an electrode's double-layer capacitance (Gouy–Chapman–Stern, the diffuse layer
|
|
23
|
+
below the grid), a semiconductor electrode's Mott–Schottky plot, a concentration cell with
|
|
24
|
+
transference, a voltage clamp, an action potential, one travelling along an axon.
|
|
23
25
|
|
|
24
26
|
The references are in docs/: device.md (the definition), kit.md (the helpers), conventions.md
|
|
25
27
|
(signs and levels), visualization.md, alignment.md (band offsets), statistics.md (non-ideal
|
|
@@ -30,9 +32,9 @@ materials, insertion hosts), data.md and reliability.md (what's validated, and t
|
|
|
30
32
|
From a CDN, pin the version you tested against:
|
|
31
33
|
|
|
32
34
|
```js nocheck
|
|
33
|
-
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
34
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
35
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
35
|
+
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.4.0/src/index.js';
|
|
36
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.4.0/src/kit.js';
|
|
37
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.4.0/src/plot.js';
|
|
36
38
|
```
|
|
37
39
|
|
|
38
40
|
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`. Every export is listed at the end.
|
|
@@ -95,7 +97,10 @@ From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`. Every export is listed at
|
|
|
95
97
|
to a metal, a face takes no alignment, but needs a φ law if the other side has ε > 0: `{ phi: {
|
|
96
98
|
type: 'capacitive', C }, zeroCharge }` or `phi: 'neutral'`, where `zeroCharge` is the metal's V −
|
|
97
99
|
φ at its edge when the face holds no charge (a work-function difference, a potential of zero
|
|
98
|
-
charge, a Schottky barrier). A face's reactions label sides: `'Li+(left) = Li+(right)'`.
|
|
100
|
+
charge, a Schottky barrier). A face's reactions label sides: `'Li+(left) = Li+(right)'`. Beside a
|
|
101
|
+
strictly neutral solution, `diffuse: { epsr }` adds its Gouy–Chapman layer below the grid (the
|
|
102
|
+
Frumkin effect on any grid, a reactant's way across the layer included;
|
|
103
|
+
`sol.interfaces[f].diffuse`).
|
|
99
104
|
- **Ports** are terminals attached sideways to a window inside a region (`from`, `to`, m from its
|
|
100
105
|
left end): a source or drain, an injector, a reference electrode (`I: 0`), an electrode spread
|
|
101
106
|
through a film (`reactions`, `area`), a gate along a channel or a membrane along an axon
|
|
@@ -219,9 +224,9 @@ a CDN needs a long virtual-time budget, 30 s.)
|
|
|
219
224
|
<span id="readout"></span>
|
|
220
225
|
<div id="figure"></div>
|
|
221
226
|
<script type="module">
|
|
222
|
-
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
223
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
224
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
227
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.4.0/src/index.js';
|
|
228
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.4.0/src/kit.js';
|
|
229
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.4.0/src/plot.js';
|
|
225
230
|
|
|
226
231
|
const def = build({
|
|
227
232
|
T: 300,
|
|
@@ -558,7 +563,8 @@ species }` (its flux) or `{ interface: f, gate }`.
|
|
|
558
563
|
(docs/statistics.md). At no current (`I: 0`), `solve()` keeps the charge it holds: a closed
|
|
559
564
|
battery sits at the OCV of its state of charge, a floating gate keeps its charge.
|
|
560
565
|
- **Other steps.** `dev.step(dt)` is one backward-Euler step, and `dev.impedance(frequencies,
|
|
561
|
-
{ terminal })` the small-signal Z(f) about the steady state, in Ω·m
|
|
566
|
+
{ terminal })` the small-signal Z(f) about the steady state, in Ω·m²: `{ f, Z: { re, im } }`,
|
|
567
|
+
each an array over the frequencies (`Z.re[k]`, not `Z[k].re`).
|
|
562
568
|
|
|
563
569
|
```js
|
|
564
570
|
import { Device, units, FARADAY } from 'driftlet';
|
|
@@ -667,9 +673,8 @@ those:
|
|
|
667
673
|
grade it over a few thin layers, as at a frit.
|
|
668
674
|
- **Changing `c0` with `set()`.** A carried state keeps what it holds: make a new `Device`.
|
|
669
675
|
- **A charged electrode face beside a strictly neutral solution** (`epsr: 0`, a capacitive face
|
|
670
|
-
with reactions):
|
|
671
|
-
cell holds the double layer's charge
|
|
672
|
-
potential avoids it.
|
|
676
|
+
with reactions): give the face its diffuse layer, `diffuse: { epsr: 78.4 }`. Without it the edge
|
|
677
|
+
cell holds the double layer's charge, and the kinetics depend on the grid (a warning says so).
|
|
673
678
|
- **An unresolved double layer.** If `sol.warnings` says so, refine the grid (`hmin` below the
|
|
674
679
|
Debye length) or use a neutral model (`epsr: 0`, `phi: 'neutral'`).
|
|
675
680
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driftlet",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Dependency-free 1D drift–diffusion–reaction (Poisson–Nernst–Planck) solver for semiconductors, electrochemistry and nerves, fast enough for live web demos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|