driftlet 1.3.3 → 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 +73 -5
- package/docs/internals.md +5 -2
- package/docs/statistics.md +2 -1
- package/docs/templates.md +157 -2
- package/llms.txt +17 -13
- package/package.json +1 -1
- package/src/boundaries.js +200 -14
- package/src/checks.js +4 -1
- package/src/describe.js +3 -1
- package/src/device.js +54 -5
- package/src/diffuse.js +240 -0
- package/src/impedance.js +2 -1
- package/src/lawRows.js +26 -1
- package/src/newton.js +31 -5
- package/src/regions.js +6 -3
- package/src/solution.js +39 -5
- package/src/solver.js +87 -9
- package/src/statistics.js +1 -0
- package/src/steady.js +13 -4
- 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
|
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/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
|
|
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,
|
|
@@ -668,9 +673,8 @@ those:
|
|
|
668
673
|
grade it over a few thin layers, as at a frit.
|
|
669
674
|
- **Changing `c0` with `set()`.** A carried state keeps what it holds: make a new `Device`.
|
|
670
675
|
- **A charged electrode face beside a strictly neutral solution** (`epsr: 0`, a capacitive face
|
|
671
|
-
with reactions):
|
|
672
|
-
cell holds the double layer's charge
|
|
673
|
-
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).
|
|
674
678
|
- **An unresolved double layer.** If `sol.warnings` says so, refine the grid (`hmin` below the
|
|
675
679
|
Debye length) or use a neutral model (`epsr: 0`, `phi: 'neutral'`).
|
|
676
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": {
|