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 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.3.3/src/index.js` (and `src/kit.js`, `src/plot.js`).
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, growing without limit as the cell shrinks (an electrode's DC
191
- resistance in ferri/ferrocyanide fell from 56.6 to 47.5 Ω·cm² between 300 nm and 1 nm). A
192
- warning says past 1%; a `zeroCharge` near the face's rest potential, so it holds little charge,
193
- avoids it on any grid. A sub-grid Gouy–Chapman law is on the [roadmap](../ROADMAP.md).
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 and gates' fractions, or at an electrode port's nodes
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
@@ -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. With one species it's Langmuir filling of sites. The dilute reference
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, corrosion under a drop (an
21
- electrode spread through a film), a voltage clamp, an action potential, one travelling along an
22
- axon.
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.3.3/src/index.js';
34
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.3/src/kit.js';
35
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.3/src/plot.js';
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.3.3/src/index.js';
223
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.3/src/kit.js';
224
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.3/src/plot.js';
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): refining the grid there makes the kinetics worse, not better, since the edge
672
- cell holds the double layer's charge. Read `sol.warnings`; a `zeroCharge` near the rest
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.3",
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": {