driftlet 0.13.0 → 0.16.2
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/alignment.md +6 -3
- package/docs/data.md +3 -1
- package/docs/device.md +97 -33
- package/docs/kit.md +95 -8
- package/docs/logo-dark.svg +3 -2
- package/docs/logo.svg +3 -2
- package/docs/numerics.md +193 -114
- package/docs/reliability.md +16 -10
- package/docs/statistics.md +28 -4
- package/llms.txt +325 -335
- package/package.json +1 -1
- package/src/blockTridiagonal.js +12 -6
- package/src/checks.js +16 -5
- package/src/describe.js +46 -5
- package/src/device.js +88 -10
- package/src/index.js +19 -7
- package/src/kit.js +1 -0
- package/src/particles.js +407 -0
- package/src/solution.js +171 -10
- package/src/solver.js +797 -262
- package/src/sources.js +10 -2
- package/src/stack.js +16 -6
- package/src/statistics.js +7 -3
- package/types/checks.d.ts +2 -1
- package/types/kit.d.ts +1 -0
- package/types/particles.d.ts +39 -0
- package/types/solution.d.ts +1 -0
- package/types/solver.d.ts +47 -29
- package/types/stack.d.ts +6 -4
package/llms.txt
CHANGED
|
@@ -1,115 +1,128 @@
|
|
|
1
1
|
# driftlet: a guide for agents
|
|
2
2
|
|
|
3
3
|
driftlet is a dependency-free JavaScript library that solves 1D drift–diffusion–reaction
|
|
4
|
-
(Poisson–Nernst–Planck) problems fast enough for live web demos
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
(Poisson–Nernst–Planck) problems fast enough for live web demos. Semiconductor junctions, solar
|
|
5
|
+
cells, electrochemical cells, batteries, membranes and nerves are one problem with different
|
|
6
|
+
casts of species (electrons, holes, ions, neutral molecules), each moving down its own
|
|
7
|
+
electrochemical potential μ̄, coupled by Poisson's equation, reactions and interfaces. So a
|
|
8
|
+
device in any field is built the same way, from the same pieces. It runs in a browser, a Web
|
|
9
|
+
Worker or Node, from npm or a CDN.
|
|
10
|
+
|
|
11
|
+
Every code block below runs in the test suite and prints its closed-form check. Skim the next
|
|
12
|
+
two sections, then go to the template nearest your device:
|
|
13
|
+
|
|
14
|
+
- semiconductors: a pn junction (with an I–V sweep), a live page with a slider, a solar cell, a
|
|
15
|
+
MOS capacitor, a Schottky diode, a thin-film transistor, a pulse drifting along a bar (under
|
|
16
|
+
Transients);
|
|
17
|
+
- electrochemistry: a cell, an electrode in a stirred solution (a potential step, and a cyclic
|
|
18
|
+
voltammogram), corrosion;
|
|
19
|
+
- physiology: a cell membrane (a resting potential), a voltage clamp, an action potential, one
|
|
20
|
+
travelling along an axon.
|
|
21
|
+
|
|
22
|
+
The references are in docs/: device.md (the definition), kit.md (the helpers), conventions.md
|
|
23
|
+
(signs and levels), visualization.md, alignment.md (band offsets), statistics.md (non-ideal
|
|
24
|
+
materials, insertion hosts), data.md and reliability.md (what's validated, and the limits).
|
|
16
25
|
|
|
17
26
|
## Imports
|
|
18
27
|
|
|
19
28
|
From a CDN, pin the version you tested against:
|
|
20
29
|
|
|
21
30
|
```js nocheck
|
|
22
|
-
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
23
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
24
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
31
|
+
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/index.js';
|
|
32
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/kit.js';
|
|
33
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/plot.js';
|
|
25
34
|
```
|
|
26
35
|
|
|
27
36
|
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`. Every export is listed at the end.
|
|
28
37
|
|
|
29
38
|
## What to know first
|
|
30
39
|
|
|
31
|
-
- **Units are SI**: m, mol/m³ (1 M = 1000 mol/m³), J/mol, m²/s,
|
|
32
|
-
`units` converts in (`units.um(1)`, `units.perCm3(1e17)` for
|
|
33
|
-
`units.eV(1.12)` to J/mol, `units.cm2PerS(36)`, `units.molar(0.1)`) and back
|
|
34
|
-
`toEV`, `toMolar`, `toUm`, …).
|
|
35
|
-
1 µA/cm² = 0.01 A/m², mM = mol/m³.
|
|
36
|
-
- **Everything you set or read is an electrochemical potential μ̄** (or a voltage made from one).
|
|
37
|
-
|
|
38
|
-
what a voltmeter reads. φ is bookkeeping, per material, and
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
material, `V`
|
|
40
|
+
- **Units are SI**: m, mol/m³ (1 M = 1000 mol/m³; 1e16 cm⁻³ is only 0.0166 mol/m³), J/mol, m²/s,
|
|
41
|
+
V, A/m², F/m², S/m². `units` converts in (`units.um(1)`, `units.perCm3(1e17)` for per cm³ or
|
|
42
|
+
per cm³·s, `units.eV(1.12)` to J/mol, `units.cm2PerS(36)`, `units.molar(0.1)`) and back
|
|
43
|
+
(`units.toPerCm3`, `toEV`, `toMolar`, `toUm`, …). Physiology: 1 mS/cm² = 10 S/m², 1 µF/cm² =
|
|
44
|
+
0.01 F/m², 1 µA/cm² = 0.01 A/m², mM = mol/m³.
|
|
45
|
+
- **Everything you set or read is an electrochemical potential μ̄** (or a voltage made from one). A
|
|
46
|
+
terminal's `V` is the level of its terminal species (`terminal`, which the kit's contacts set):
|
|
47
|
+
for an electron contact V = −μ̄_e/F, what a voltmeter reads. φ is bookkeeping, per material, and
|
|
48
|
+
never a control.
|
|
49
|
+
- **Solutions** are plain objects of `Float64Array`s: `x`, `phi`, and per species `c`, `mu` (μ̄),
|
|
50
|
+
`V` (the species voltage μ̄/(zF): a quasi-Fermi level for e⁻ and h⁺) and `Vstd` (the standard
|
|
51
|
+
level: a band edge; in volts, E = −V, so E_c is the lower line). `NaN` marks a value
|
|
52
|
+
that doesn't exist (a species absent from a material, `V` of a neutral one, `c` in a metal).
|
|
44
53
|
- **Currents.** `sol.terminals[name]` gives each terminal's `{ V, current }`, its current into
|
|
45
|
-
the device
|
|
46
|
-
|
|
47
|
-
a
|
|
48
|
-
|
|
54
|
+
the device; a current drive `{ I }` is the same way round (electrons fed in are I < 0).
|
|
55
|
+
`sol.current` is the right contact's toward +x, so a forward diode with its p side on the right
|
|
56
|
+
carries a negative one (with ports, read each contact from `sol.terminals`). Everything is per
|
|
57
|
+
m² of a planar device; with a `geometry` (shells, or any cross-section A(x)) it's a total.
|
|
49
58
|
- **What isn't defaulted**: an alignment between different materials, a φ law where a metal
|
|
50
|
-
meets a dielectric, and the amount of anything a contact doesn't feed (`c0`).
|
|
51
|
-
the path and says what to give. (Kinetic details are defaulted
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
transients, and the same device on a grid twice as fine (a FAIL means the answer depends on
|
|
58
|
-
the grid: refine it). It costs a solve: run it once, not per frame.
|
|
59
|
+
meets a dielectric, and the amount of anything a contact doesn't feed (`c0`). The error names
|
|
60
|
+
the path and says what to give. (Kinetic details are defaulted: listed in docs/device.md.)
|
|
61
|
+
- **Check before trusting.** `sol.converged` must be true; read `sol.warnings`, where a failed
|
|
62
|
+
solve always says why. `describe(def)` (kit) summarises a definition and flags likely unit
|
|
63
|
+
slips. Once a device works, log `check(dev, sol).text` (kit) once: each species' ledger,
|
|
64
|
+
conservation in transients, and a steady state on a grid twice as fine (a FAIL means refine;
|
|
65
|
+
a transient you rerun on a finer grid yourself).
|
|
59
66
|
|
|
60
67
|
## How a device is written
|
|
61
68
|
|
|
62
69
|
- **`build({ T, library, stack, bulkReactions, ports, grid, geometry })`** (kit) reads a stack
|
|
63
70
|
left to right: its first and last items are the contacts, `layer(material, length, opts)`s
|
|
64
|
-
are regions, and any other object between two layers is
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
are regions (give one a `name` to refer to it), and any other object between two layers is
|
|
72
|
+
the face between them. `ohmic()`, `bath()` and the like write contacts; a plain object works.
|
|
73
|
+
- **The library** is pieces with `species` and `materials`: `semiconductor('Si')` (Si, Ge, GaAs, at
|
|
74
|
+
300 K), `aqueous(names)` (H⁺ Li⁺ Na⁺ K⁺ Ag⁺ Cu²⁺ Zn²⁺ Fe²⁺ Fe³⁺ OH⁻ Cl⁻ NO₃⁻ SO₄²⁻ in water, at 25
|
|
75
|
+
°C; `{ epsr: 0 }` makes it strictly neutral, with no double layers), `metal('Ag')` (Ag Cu Au Al Zn
|
|
76
|
+
Li Pt), all from `driftlet/kit`. A semiconductor's textbook numbers are in `SEMICONDUCTORS.Si`
|
|
77
|
+
(`Eg` and `chi` in eV, `Nc` and `Nv` in cm⁻³, `mun` and `mup` in cm²/(V·s), `epsr`); in the
|
|
78
|
+
material, e⁻'s and h⁺'s `cRef` are N_c and N_v in mol/m³ and h⁺'s `mu0` is the gap (e⁻'s is 0),
|
|
79
|
+
so n_i = √(N_c N_v) e^(−E_g/2RT), 6.7e9 cm⁻³ for Si. A species it lacks is pushed into both lists
|
|
80
|
+
(`z: 0` for a neutral one): `lib.species.push({ name: 'A-', z: -1, cRef: 1000 })`,
|
|
81
|
+
`lib.materials.water.species['A-'] = { D, mu0 }`. Only μ° differences within a reaction matter:
|
|
82
|
+
for a redox couple μ°_red − μ°_ox = −F·E° (against SHE, the library's μ°_H⁺ being 0). An
|
|
83
|
+
insulator is a material with no species: `{ species: [], materials: { SiO2: { epsr: 3.9, species:
|
|
84
|
+
{} } } }`.
|
|
85
|
+
- **Faces.** Between two different non-metals, give an alignment, `{ dipole }` or `{ step: {
|
|
86
|
+
species: 'e-', value } }` (J/mol), or `phi: 'neutral'`. `vacuumDipole(def, { material, anchor:
|
|
87
|
+
'e-', offset: χ }, …)` estimates a dipole from electron affinities (Anderson's rule); it reads
|
|
88
|
+
only the materials, so build with `{ dipole: 0 }`, then put its value in (docs/alignment.md). Next
|
|
89
|
+
to a metal, a face takes no alignment, but needs a φ law if the other side has ε > 0: `{ phi: {
|
|
90
|
+
type: 'capacitive', C }, zeroCharge }` or `phi: 'neutral'`, where `zeroCharge` is the metal's V −
|
|
91
|
+
φ at its edge when the face holds no charge (a work-function difference, a potential of zero
|
|
92
|
+
charge, a Schottky barrier). A face's reactions label sides: `'Li+(left) = Li+(right)'`.
|
|
93
|
+
- **Ports** are terminals attached sideways to a window inside a region (`from`, `to`, m from its
|
|
94
|
+
left end): a source or drain, an injector, a reference electrode (`I: 0`), an electrode spread
|
|
95
|
+
through a film (`reactions`, `area`), a gate along a channel or a membrane along an axon
|
|
96
|
+
(`capacitance`, `gates`). A BJT's base contact is a port on a narrow window of the base,
|
|
97
|
+
`{ region: 'base', from, to, V, terminal: 'h+', species: { 'h+': 'equilibrium' } }`.
|
|
85
98
|
- **Drives.** A terminal is held at a voltage `V`, driven by a current `I` (open circuit is
|
|
86
|
-
`I: 0`), or behind a resistance `{ V, R }`.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
99
|
+
`I: 0`), or behind a resistance `{ V, R }`. Either can be a waveform: a table `{ t, values }`
|
|
100
|
+
(piecewise linear; a step is two points at one time), or the kit's `pulse({ start, width,
|
|
101
|
+
amplitude, rise, base })`, `square({ high, low, period, duty })`, `ramp({ from, to, duration,
|
|
102
|
+
start })` or `triangle({ from, to, period })` (out and back each period from t = 0: a cyclic
|
|
103
|
+
voltammogram, in terminal volts). `dev.set({ contacts: { right: { V } } })` changes a drive
|
|
104
|
+
and keeps the state as a warm start; ports are patched by name, `{ ports: { gate: { V } } }`.
|
|
105
|
+
- **Reactions** are equations: `{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }`, a coefficient
|
|
106
|
+
written with its space (`2 e-`). A participant that isn't a species (solid silver) is fixed by
|
|
107
|
+
its μ: `fixed: { 'Ag(s)': 0 }`. A bulk reaction's forward rate is `kf · Π c_reactants`
|
|
108
|
+
(mol/(m³·s)); the backward one follows from the μ°.
|
|
109
|
+
- **Recombination and light.** Band-to-band `e- + h+ = 0` gives a minority lifetime τ =
|
|
110
|
+
1/(kf·N), N the majority density in mol/m³ (`kf: 1e8` is 0.6 µs at 1e16 cm⁻³); being
|
|
111
|
+
bimolecular, as is the kit's `recombination()`, it shortens at high injection. SRH is `srh: { Si:
|
|
112
|
+
{ tauN, tauP } }` in place of `kf` (s), or at a face `{ equation: 'e-(left) + h+(right) = 0',
|
|
113
|
+
srh: { vn, vp } }` (m/s); with τ_N ≪ τ_p it gives n-type silicon's holes a lifetime τ_p at any
|
|
114
|
+
injection. Light is a reaction from a reservoir (`photon = e- + h+`, uniform), or absorbed as
|
|
115
|
+
it goes in, `photogeneration({ material, flux, alpha, mu: units.eV(3), from, to })`. To switch
|
|
116
|
+
it during a run, `set()` the device's `bulkReactions` (the state and the step size carry over).
|
|
117
|
+
- **Electrode kinetics.** A face reaction's `k0` (mol/(m²·s)) is its rate with every participant
|
|
118
|
+
at cRef: i₀ = nF·k0 there, and k0 = k°·cRef for a textbook k° in m/s (1 cm/s at 1 M is
|
|
119
|
+
`k0: 10`).
|
|
120
|
+
- **Grids** grade geometrically from `hmin` at each region's ends up to `hmax`, neighbouring cells
|
|
121
|
+
differing by at most `ratio` (1.2): put `hmin` below the shortest length that matters (a Debye
|
|
122
|
+
length, a depletion edge). `check()` says whether it's fine enough.
|
|
123
|
+
|
|
124
|
+
`build()` is the usual way in. It writes a plain definition for `new Device()`, which some
|
|
125
|
+
templates below write by hand (where a port spans a region, or no stack fits):
|
|
113
126
|
|
|
114
127
|
```js
|
|
115
128
|
import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
@@ -133,6 +146,14 @@ const tK = D['K+'] / (D['K+'] + D['Cl-']), VT = (GAS_CONSTANT * 298.15) / FARADA
|
|
|
133
146
|
console.log(`EMF ${(sol.terminals.right.V * 1e3).toFixed(3)} mV, 2t₊V_T ln 10 = ${(2 * tK * VT * Math.log(10) * 1e3).toFixed(3)} mV`); // 58.048, 58.047
|
|
134
147
|
```
|
|
135
148
|
|
|
149
|
+
An ion-exchange membrane is a layer whose material has its ions slower, with a `fixedCharge` (−X·F
|
|
150
|
+
for X mol/m³ of fixed anions), between layers of water. A bath against the membrane itself names
|
|
151
|
+
its own material, `bath(c, 'Cl-', 0, { material: 'water' })`, and meets it through a Donnan step.
|
|
152
|
+
An exchange or partition constant between the membrane and the water is a difference of μ°:
|
|
153
|
+
give the membrane's ions μ° shifted from the water's by s_i, and K = e^(−(s_Na − s_H)/RT) for
|
|
154
|
+
Na⁺ against H⁺ (a large K draws in salt too, unless the co-ion's s is raised). The membrane
|
|
155
|
+
demo's page script is a worked example.
|
|
156
|
+
|
|
136
157
|
## Template: a semiconductor junction
|
|
137
158
|
|
|
138
159
|
```js
|
|
@@ -164,15 +185,14 @@ for (const V of [0, 0.2, 0.4, 0.6]) {
|
|
|
164
185
|
|
|
165
186
|
## Template: a live page with a slider
|
|
166
187
|
|
|
167
|
-
`live()` merges slider moves made during
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
than oscillates. A
|
|
174
|
-
|
|
175
|
-
screenshot of a page that loads the library from a CDN needs a long virtual-time budget, 30 s.)
|
|
188
|
+
`live()` warm-starts each solve, merges slider moves made during one, and keeps the last good
|
|
189
|
+
solution (on a failure, `info.failed` and `info.error` are set). It returns `{ set, solution, ready,
|
|
190
|
+
close }`; `set()` and `ready` (the first solve) are promises of `{ solution, info }`.
|
|
191
|
+
`bandDiagram()` returns an SVG string; a fixed `range` keeps its axis still. `worker: true` solves
|
|
192
|
+
in a Web Worker, from a CDN too. A few hundred nodes solve in milliseconds; thousands, or
|
|
193
|
+
transients, want a worker and `advance(t, { budgetMs })` per frame. `onsolution` may call `set()`,
|
|
194
|
+
if that converges rather than oscillates. (A headless browser screenshotting a page that loads from
|
|
195
|
+
a CDN needs a long virtual-time budget, 30 s.)
|
|
176
196
|
|
|
177
197
|
```html
|
|
178
198
|
<!doctype html>
|
|
@@ -182,9 +202,9 @@ screenshot of a page that loads the library from a CDN needs a long virtual-time
|
|
|
182
202
|
<span id="readout"></span>
|
|
183
203
|
<div id="figure"></div>
|
|
184
204
|
<script type="module">
|
|
185
|
-
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
186
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
187
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
205
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/index.js';
|
|
206
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/kit.js';
|
|
207
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/plot.js';
|
|
188
208
|
|
|
189
209
|
const def = build({
|
|
190
210
|
T: 300,
|
|
@@ -207,11 +227,23 @@ slider.addEventListener('input', () => dev.set({ contacts: { right: { V: +slider
|
|
|
207
227
|
To draw in your own style, `traces(sol, opts)` (kit, the same options) gives the lines as data;
|
|
208
228
|
see "Reading level diagrams" below.
|
|
209
229
|
|
|
230
|
+
For an explainer, `particles(sol, { weight, cap })` (kit) draws a solution as dots, each `weight`
|
|
231
|
+
mol/m² of a species, hopping at rates taken from the solution: on average their density is its
|
|
232
|
+
concentration and their net crossing its flux. Say on the page that it samples the continuum
|
|
233
|
+
solution, and how much a dot is. Where a dot would be lost among millions (majority carriers, a
|
|
234
|
+
bath), a cell over `cap` dots is a sea: shade it by `expected(name)`. Each frame,
|
|
235
|
+
`swarm.step(dt)` in the device's time (about h²/D per hop, h a cell), then draw `swarm.dots`
|
|
236
|
+
(`{ species, x }`) and `swarm.events` (`'unmade'` is a recombination, `'cross'` an ion through a
|
|
237
|
+
membrane between two seas); on a new steady solution, `swarm.update(sol, { resample: true })`.
|
|
238
|
+
An ion beside a cell membrane hops ~1e6 times per crossing, so a membrane page shows seas and
|
|
239
|
+
crossing events, not walking ions. The demos' page scripts do this for a diode's carriers (pn),
|
|
240
|
+
ions crossing a nerve membrane (axon, `cap: 0`) and Donnan exclusion (membrane).
|
|
241
|
+
|
|
210
242
|
## Template: a solar cell
|
|
211
243
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
244
|
+
`photon = e- + h+`, with the photons' μ well above the gap, generates uniformly at the rate `kf`.
|
|
245
|
+
Open circuit is `I: 0`, and the terminal's V is then V_oc. The photocurrent flows toward +x
|
|
246
|
+
(holes to the p side, on the right).
|
|
215
247
|
|
|
216
248
|
```js
|
|
217
249
|
import { Device, units } from 'driftlet';
|
|
@@ -238,17 +270,25 @@ const oc = new Device(cell({ I: 0 })).solve(); // open circuit
|
|
|
238
270
|
console.log(`J_sc = ${sc.current.toFixed(1)} A/m², V_oc = ${oc.terminals.right.V.toFixed(3)} V`);
|
|
239
271
|
```
|
|
240
272
|
|
|
273
|
+
In an organic cell, light makes excitons: a neutral species `X` in the donor, made by
|
|
274
|
+
`photogeneration({ ..., makes: 'X' })` and decaying by `{ equation: 'X = 0', kf: { donor: 1 / tau } }`.
|
|
275
|
+
They split at the donor/acceptor face by a reaction there, `X = h+(left) + e-(right)` with e⁻ and
|
|
276
|
+
h⁺ blocked at the face (the reaction moves them), and its reverse is charge-transfer
|
|
277
|
+
recombination; the face's `step` sets E_CT. With `alpha: 0` the splitting is a velocity
|
|
278
|
+
k0/cRef, a Robin condition on the exciton profile; with α > 0 it's k0/cRef · e^(α(E_X − E_CT)/RT).
|
|
279
|
+
The organic demo's page script is a worked example (J_sc against exciton diffusion theory, V_oc
|
|
280
|
+
against detailed balance).
|
|
281
|
+
|
|
241
282
|
For a perovskite cell, prefer the curated `perovskiteCell({ V, light, bulkSRH })`: IonMonger's
|
|
242
283
|
default cell (mobile iodide vacancies), with its J–V scans at seven rates in `PEROVSKITE_SCANS`
|
|
243
284
|
and `hysteresis(trace, turn)` to read the same figures from yours.
|
|
244
285
|
|
|
245
286
|
## Template: a MOS capacitor
|
|
246
287
|
|
|
247
|
-
The gate is a metal layer
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
into the oxide, `sol.interfaces[0].D`.
|
|
288
|
+
The gate is a metal layer with a capacitive face to the oxide (a metal screens within Å, so C is
|
|
289
|
+
large) and the gate's alignment as `zeroCharge`. In 1D nothing supplies the inversion layer's
|
|
290
|
+
electrons (a MOSFET's source and drain do), so a port holds their level at the surface: the
|
|
291
|
+
low-frequency C–V. The gate's charge is the displacement into the oxide, `sol.interfaces[0].D`.
|
|
252
292
|
|
|
253
293
|
```js
|
|
254
294
|
import { Device, units, EPS0, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
@@ -284,20 +324,21 @@ for (const Vg of [-2, -0.95, 0, 0.4, 2]) {
|
|
|
284
324
|
console.log(`V_G ${Vg} V: C/C_ox ${(C / Cox).toFixed(3)}`); // 0.949, 0.701 (flat band), 0.267, 0.375, 0.973
|
|
285
325
|
}
|
|
286
326
|
// At flat band the silicon is a capacitor of ε/L_D, its Debye length's.
|
|
287
|
-
const LD = Math.sqrt((11.
|
|
288
|
-
console.log(`flat band, theory: ${(1 / (1 + (Cox * LD) / (11.
|
|
327
|
+
const LD = Math.sqrt((11.9 * EPS0 * GAS_CONSTANT * 300) / (FARADAY * FARADAY * NA)); // (the kit's Si: ε 11.9)
|
|
328
|
+
console.log(`flat band, theory: ${(1 / (1 + (Cox * LD) / (11.9 * EPS0))).toFixed(3)}`); // 0.701
|
|
289
329
|
```
|
|
290
330
|
|
|
291
|
-
`dev.impedance([f], { terminal: 'left' })`
|
|
292
|
-
|
|
293
|
-
|
|
331
|
+
`dev.impedance([f], { terminal: 'left' })` (C = Im(1/Z)/ω, from `{ f, Z: { re, im } }`) gives
|
|
332
|
+
the same C at any frequency with the port; without it, only below the minority supply's knee
|
|
333
|
+
(generation, or diffusion from the back contact: 0.01 Hz and below), and the high-frequency C–V
|
|
334
|
+
above. Draw it with
|
|
335
|
+
`bandDiagram(sol, { energy: true, phi: ['SiO2'] })`: across an insulator φ is the line to draw.
|
|
294
336
|
|
|
295
337
|
## Template: a Schottky diode
|
|
296
338
|
|
|
297
|
-
A
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
passes whatever the silicon brings it: diffusion theory, some 20 times more current here.
|
|
339
|
+
A barrier pinned at the metal (`phi: 'pinned'`, `zeroCharge` the barrier height for Si, whose
|
|
340
|
+
μ°_e⁻ is 0), crossed at the emission velocity: a `velocity` link, N_in = v (c_eq − c). With
|
|
341
|
+
`'equilibrium'` links (infinite velocity) it's diffusion theory instead, 20 times the current here.
|
|
301
342
|
|
|
302
343
|
```js
|
|
303
344
|
import { Device, units, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
@@ -321,9 +362,44 @@ for (const V of [0.1, 0.2, 0.3]) {
|
|
|
321
362
|
}
|
|
322
363
|
```
|
|
323
364
|
|
|
324
|
-
The
|
|
325
|
-
|
|
326
|
-
|
|
365
|
+
The same `velocity` link at an ohmic contact is its surface recombination velocity.
|
|
366
|
+
|
|
367
|
+
## Template: a thin-film transistor (a capacitance along a channel)
|
|
368
|
+
|
|
369
|
+
A **port with a `capacitance`** holds charge σ = C (V − zeroCharge − φ) per area of electrode
|
|
370
|
+
along its window, balanced by the region's: a gate along a channel, a porous electrode's double
|
|
371
|
+
layer. Here x runs from source to drain with the gate spread along it: the gradual-channel
|
|
372
|
+
approximation, exact for a thin film until it pinches off.
|
|
373
|
+
|
|
374
|
+
```js
|
|
375
|
+
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
376
|
+
|
|
377
|
+
const T = 300, VT = (GAS_CONSTANT * T) / FARADAY;
|
|
378
|
+
const t = 20e-9, L = 10e-6, W = 100e-6, C = 1e-3, mu = 1e-3; // film, length, width (m); F/m² of gate; m²/(V·s)
|
|
379
|
+
const tft = (Vg, Vd) =>
|
|
380
|
+
new Device({
|
|
381
|
+
T,
|
|
382
|
+
species: [{ name: 'e-', z: -1, cRef: 46 }], // an undoped film's electrons (N_c 2.8e19 cm⁻³)
|
|
383
|
+
materials: { film: { epsr: 0, species: { 'e-': { D: mu * VT, mu0: 0 } } } }, // ε = 0: F·n·t balances σ
|
|
384
|
+
regions: [{ name: 'channel', material: 'film', length: L }],
|
|
385
|
+
contacts: {
|
|
386
|
+
left: { V: 0, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'neutral' }, // source
|
|
387
|
+
right: { V: Vd, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'neutral' }, // drain
|
|
388
|
+
},
|
|
389
|
+
ports: [{ name: 'gate', region: 'channel', V: Vg, area: 1 / t, capacitance: { C, zeroCharge: 0.3 } }],
|
|
390
|
+
grid: { hmin: L / 2000, hmax: L / 100, ratio: 1.05 }, // fine at the drain, where it pinches off
|
|
391
|
+
});
|
|
392
|
+
// The output characteristic at V_G = 2 V: linear, then saturating past V_D ≈ V_G − 0.3 V.
|
|
393
|
+
for (const Vd of [0.1, 0.5, 1, 1.5, 2, 3]) {
|
|
394
|
+
const sol = tft(2, Vd).solve();
|
|
395
|
+
const I = -sol.current * t * W; // sol.current is per m² of the film's section
|
|
396
|
+
console.log(`V_D ${Vd.toFixed(1)} V: I_D ${(I * 1e6).toFixed(2)} µA, σ at the drain ${(sol.ports[0].sigma.at(-1) * 1e3).toFixed(3)} mC/m²`);
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
To sweep the gate on one device instead, patch the port: `dev.set({ ports: { gate: { V } } })`.
|
|
401
|
+
An organic electrochemical transistor is the same with a volumetric capacitance C*:
|
|
402
|
+
`area: 1 / t, capacitance: { C: Cstar * t }`.
|
|
327
403
|
|
|
328
404
|
## Template: an electrochemical cell
|
|
329
405
|
|
|
@@ -360,15 +436,14 @@ console.log(level(sol, silver)[mid] - level(sol, SHE, { standard: true })[mid]);
|
|
|
360
436
|
|
|
361
437
|
## Template: an electrode in a stirred solution, and a potential step
|
|
362
438
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
is still about 1% off pure diffusion.
|
|
439
|
+
A working electrode, a stagnant layer, and the stirred bulk as a bath whose terminal is its
|
|
440
|
+
reference species (Cl⁻: an Ag/AgCl reference). An electrode's potential against SHE is its
|
|
441
|
+
electrons' V minus φ in the bulk (with the library's μ°), and a Cl⁻ terminal sits ~1.36 V above
|
|
442
|
+
that, so an electrode held at 0 V beside it is strongly anodic: `describe(def)` says where SHE
|
|
443
|
+
sits, and an offset of that much (`bath(c, 'Cl-', 0, { offset })`) makes the terminal read SHE.
|
|
444
|
+
The working electrode's current is positive when oxidising. The layer's resistance is in series
|
|
445
|
+
(uncompensated IR: keep it thin, or the supporting salt strong); a 100:1 excess of salt still
|
|
446
|
+
leaves the limiting current about 1% off pure diffusion.
|
|
372
447
|
|
|
373
448
|
```js
|
|
374
449
|
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
@@ -409,15 +484,18 @@ for (const t of [3e-3, 1e-2, 3e-2]) {
|
|
|
409
484
|
}
|
|
410
485
|
```
|
|
411
486
|
|
|
487
|
+
A cyclic voltammogram is this electrode driven by `triangle({ from, to, period })`, the period
|
|
488
|
+
being 2|to − from| over the scan rate: `solve()` it at `from` first, since the scan starts there
|
|
489
|
+
at t = 0, then `advance()`. Its potential against the bath's terminal is −`trace.voltage` (that
|
|
490
|
+
is V_right − V_left), and `trace.current` is anodic positive.
|
|
491
|
+
|
|
412
492
|
## Template: corrosion under a drop (an electrode spread through a film)
|
|
413
493
|
|
|
414
|
-
A **port with reactions** is an electrode spread through a
|
|
415
|
-
|
|
416
|
-
`area`
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
x is the radius of a drop seen from its centre, A = 2πr·h(r), and every current and amount becomes
|
|
420
|
-
a total through it (A, mol), not per m².
|
|
494
|
+
A **port with reactions** is an electrode spread through a strictly neutral region: a metal under
|
|
495
|
+
a film of water, a crevice's walls, a porous electrode. Its `terminal` is the metal's carrier,
|
|
496
|
+
`area` its area per volume (1/h under a film h thick), each reaction Butler–Volmer per area, and
|
|
497
|
+
a `capacitance` the double layer. A **`geometry`** gives the device a cross-section A(x): here x
|
|
498
|
+
is the radius of a drop, A = 2πr·h(r), and currents and amounts become totals (A, mol).
|
|
421
499
|
|
|
422
500
|
```js
|
|
423
501
|
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
@@ -468,58 +546,17 @@ const evans = polarization(dev, sol, { port: 'iron', x: 0 }, levels);
|
|
|
468
546
|
for (const rx of evans.reactions) console.log(`${rx.equation}: ${Array.from(rx.current, (i) => i.toFixed(2)).join(', ')} A/m²`);
|
|
469
547
|
```
|
|
470
548
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
be described by its OCV curve (docs/statistics.md).
|
|
475
|
-
|
|
476
|
-
## Template: a thin-film transistor (a capacitance along a channel)
|
|
477
|
-
|
|
478
|
-
A **port with a `capacitance`** holds charge σ = C (V − zeroCharge − φ) per area of electrode
|
|
479
|
-
along its window, balanced by the region (Gauss's law, or neutrality at ε = 0): a gate along a
|
|
480
|
-
channel, a porous electrode's double layer. In 1D a field-effect transistor is its channel, x from
|
|
481
|
-
source to drain, with the gate spread along it: the gradual-channel approximation, exact for a thin
|
|
482
|
-
film until it pinches off. (Its gate stack, across the channel, is the MOS capacitor above.)
|
|
483
|
-
|
|
484
|
-
```js
|
|
485
|
-
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
486
|
-
|
|
487
|
-
const T = 300, VT = (GAS_CONSTANT * T) / FARADAY;
|
|
488
|
-
const t = 20e-9, L = 10e-6, W = 100e-6, C = 1e-3, mu = 1e-3; // film, length, width (m); F/m² of gate; m²/(V·s)
|
|
489
|
-
const tft = (Vg, Vd) =>
|
|
490
|
-
new Device({
|
|
491
|
-
T,
|
|
492
|
-
species: [{ name: 'e-', z: -1, cRef: 46 }], // an undoped film's electrons (N_c 2.8e19 cm⁻³)
|
|
493
|
-
materials: { film: { epsr: 0, species: { 'e-': { D: mu * VT, mu0: 0 } } } }, // ε = 0: F·n·t balances σ
|
|
494
|
-
regions: [{ name: 'channel', material: 'film', length: L }],
|
|
495
|
-
contacts: {
|
|
496
|
-
left: { V: 0, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'neutral' }, // source
|
|
497
|
-
right: { V: Vd, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'neutral' }, // drain
|
|
498
|
-
},
|
|
499
|
-
ports: [{ name: 'gate', region: 'channel', V: Vg, area: 1 / t, capacitance: { C, zeroCharge: 0.3 } }],
|
|
500
|
-
grid: { hmin: L / 2000, hmax: L / 100, ratio: 1.05 }, // fine at the drain, where it pinches off
|
|
501
|
-
});
|
|
502
|
-
// The output characteristic at V_G = 2 V: linear, then saturating past V_D ≈ V_G − 0.3 V.
|
|
503
|
-
for (const Vd of [0.1, 0.5, 1, 1.5, 2, 3]) {
|
|
504
|
-
const sol = tft(2, Vd).solve();
|
|
505
|
-
const I = -sol.current * t * W; // sol.current is per m² of the film's section
|
|
506
|
-
console.log(`V_D ${Vd.toFixed(1)} V: I_D ${(I * 1e6).toFixed(2)} µA, σ at the drain ${(sol.ports[0].sigma.at(-1) * 1e3).toFixed(3)} mC/m²`);
|
|
507
|
-
}
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
To sweep the gate on one device instead, patch the port: `dev.set({ ports: { gate: { V } } })`.
|
|
511
|
-
An organic electrochemical transistor is the same with a volumetric capacitance C*:
|
|
512
|
-
`area: 1 / t, capacitance: { C: Cstar * t }`.
|
|
549
|
+
Species can take an electrode's surface sites (`surface`, Langmuir coverages), and a reaction
|
|
550
|
+
with `bare: true` runs only on bare metal: a passive film (docs/device.md). A battery's insertion
|
|
551
|
+
electrode is a material described by its OCV curve (docs/statistics.md).
|
|
513
552
|
|
|
514
553
|
## Template: a cell membrane
|
|
515
554
|
|
|
516
|
-
A lipid bilayer is a face: a capacitor (
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
right. A bath with a reference species reads that ion's level instead (a Ag/AgCl electrode's),
|
|
522
|
-
which differs from Δφ by its Nernst term; `bath(c, drive)` without one reads its φ.
|
|
555
|
+
A lipid bilayer is a face: a capacitor (1 µF/cm², 0.01 F/m²) crossed by channels with a
|
|
556
|
+
permeability (`{ type: 'permeability', P }`, m/s: Goldman–Hodgkin–Katz comes out of it) and by
|
|
557
|
+
pumps, reactions with saturating kinetics (`vmax`, `K`). The membrane potential is φ_in − φ_out,
|
|
558
|
+
read from `sol.phi` as below. (A bath's terminal V is its φ only without a reference species,
|
|
559
|
+
`bath(c, drive)`; `bath(c, 'Cl-')`'s is Cl⁻'s level, which differs by Cl⁻'s Nernst term.)
|
|
523
560
|
|
|
524
561
|
```js
|
|
525
562
|
import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
@@ -564,12 +601,10 @@ current), V_m is GHK. Without the pump, a closed cell runs down to Donnan equili
|
|
|
564
601
|
|
|
565
602
|
## Template: a voltage clamp
|
|
566
603
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
whose terminal voltage is their φ (`bath(c, drive)`, no reference species), V_right − V_left is
|
|
572
|
-
V_m, so a held step is their voltage clamp, and a solve finds the rest the clamp holds.
|
|
604
|
+
A face's `gates` (each a fraction open, dx/dt = α(V)(1 − x) − β(V)x with V = φ_right − φ_left)
|
|
605
|
+
scale its links (`gates: { m: 3, h: 1 }`). `hodgkinHuxley({ T, linear: true })` (kit) writes the
|
|
606
|
+
squid axon's as Hodgkin and Huxley had them, conductances g(V − E). Between two baths without a
|
|
607
|
+
reference species, V_right − V_left is V_m, so a held step is their voltage clamp.
|
|
573
608
|
|
|
574
609
|
```js
|
|
575
610
|
import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
@@ -604,14 +639,11 @@ console.log(`I_Na peaks ${((run.trace.t[peak] - 1e-3) * 1e3).toFixed(2)} ms into
|
|
|
604
639
|
console.log(`g_K after 10 ms: ${((k.at(-1) / (step - EK)) * 0.1).toFixed(2)} mS/cm²`); // 21.12: HH's 36 n∞⁴ is 21.5, which n nearly reaches by 10 ms
|
|
605
640
|
```
|
|
606
641
|
|
|
607
|
-
Without `linear: true` (given `inside` and `outside`) the channels are GHK permeabilities matched
|
|
608
|
-
to HH's conductances at −65 mV, which rectify: a g_K read as above then comes out 1.7 times HH's
|
|
609
|
-
at −10 mV.
|
|
610
|
-
|
|
611
642
|
## Template: an action potential
|
|
612
643
|
|
|
613
|
-
|
|
614
|
-
|
|
644
|
+
Without `linear: true` (given `inside` and `outside`), `hodgkinHuxley` writes GHK permeabilities
|
|
645
|
+
matched to HH's conductances at −65 mV, which rectify as real channels do. A pump-less cell has no
|
|
646
|
+
steady rest (`solve()` finds Donnan), so start the transient from the concentrations given.
|
|
615
647
|
|
|
616
648
|
```js
|
|
617
649
|
import { Device } from 'driftlet';
|
|
@@ -649,14 +681,14 @@ console.log(`peak ${(peak * 1e3).toFixed(1)} mV`); // +12.6: GHK's Na⁺ current
|
|
|
649
681
|
|
|
650
682
|
## Template: an action potential travelling along an axon
|
|
651
683
|
|
|
652
|
-
Here x runs along the axon
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
is
|
|
684
|
+
Here x runs along the axon: the axoplasm is one region, and the membrane a port spanning it, a
|
|
685
|
+
capacitance and gated channels to an outside given as a `bath` (the port's V is then the
|
|
686
|
+
outside's φ). `hodgkinHuxley({ T, area })` writes HH's channels for it, `area` being the membrane
|
|
687
|
+
per volume, 2/a. Cable theory isn't put in; it comes out of the ions' drift. `ports[k].Vm[j]` and
|
|
688
|
+
`ports[k].gates[name][j]` give the membrane at each node. A myelinated axon is regions, each with
|
|
689
|
+
its own port (an internode's just a small capacitance). Nothing warns of a grid too coarse for
|
|
690
|
+
the spike's front: the speed comes out 3% low on 2 mm cells, 0.2% on 0.5 mm, for a = 238 µm
|
|
691
|
+
(scale h with √a). The stimulus is a current per volume, so a thinner axon needs it as 1/a.
|
|
660
692
|
|
|
661
693
|
```js
|
|
662
694
|
import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
@@ -691,20 +723,27 @@ console.log(`${(0.02 / (at(probes[1]) - at(probes[0]))).toFixed(1)} m/s`); // 18
|
|
|
691
723
|
|
|
692
724
|
## Transients
|
|
693
725
|
|
|
694
|
-
`advance(t, opts)` steps from the device's present state (its `c0`, or
|
|
695
|
-
called `solve()`
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
or
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
a driven current held past what the device can pass fails to converge.
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
726
|
+
`advance(t, opts)` steps adaptively from the device's present state (its `c0`, or the steady
|
|
727
|
+
state if you called `solve()`) to time t. It returns the solution there, with `done` and a `trace`
|
|
728
|
+
of this call's accepted steps (`t`, `current`, `voltage`: V_right − V_left). It stops short at
|
|
729
|
+
`budgetMs` (an animation frame: call again) or, with a warning saying why, at `maxSteps` (100000)
|
|
730
|
+
or a step that won't converge. `probes`, read at accepted steps (give `dtMax` to resolve them):
|
|
731
|
+
`{ x, species }` (c, or `quantity: 'V'`), `{ x, quantity: 'phi' }`, and at a face `{ interface: f,
|
|
732
|
+
species }` (its flux) or `{ interface: f, gate }`.
|
|
733
|
+
|
|
734
|
+
- **Events.** To stop at one (a cut-off voltage, a diode's storage time), advance in short calls
|
|
735
|
+
and test after each: a driven current held past what the device can pass fails to converge.
|
|
736
|
+
- **The first instants.** The first step is sized by error control (from 1e-4 of the interval),
|
|
737
|
+
so a fast start (an initial plateau, a charging spike) can fall inside it: give `dt0` (and
|
|
738
|
+
`dtMax`) to see it. A voltage step's first point carries the displacement that charges the bulk
|
|
739
|
+
at once (a spike of ~1e4 A/m² for ~1e-10 s across a cell's Stern layers): leave it out of
|
|
740
|
+
integrals of I²R.
|
|
741
|
+
- **Storage.** A device that only stores what comes in (a battery host filling at a current, a
|
|
742
|
+
gate charging) has no steady state: `advance()` it from its `c0`, don't `solve()` it
|
|
743
|
+
(docs/statistics.md). At no current (`I: 0`), `solve()` keeps the charge it holds: a closed
|
|
744
|
+
battery sits at the OCV of its state of charge, a floating gate keeps its charge.
|
|
745
|
+
- **Other steps.** `dev.step(dt)` is one backward-Euler step, and `dev.impedance(frequencies,
|
|
746
|
+
{ terminal })` the small-signal Z(f) about the steady state, in Ω·m².
|
|
708
747
|
|
|
709
748
|
```js
|
|
710
749
|
import { Device, units, FARADAY } from 'driftlet';
|
|
@@ -730,132 +769,83 @@ const tArrive = run.trace.t[k] - width / 2; // from the pulse's middle
|
|
|
730
769
|
console.log(`peak at ${(run.trace.t[k] * 1e6).toFixed(2)} µs, Δp ${units.toPerCm3(seen[k] - rest.c['h+'][0]).toExponential(1)} cm⁻³: μ_p ≈ ${(1e-3 / (E * tArrive) * 1e4).toFixed(0)} cm²/(V·s)`); // ≈ 1950 for the 1900 put in: a spreading, decaying pulse peaks a little early
|
|
731
770
|
```
|
|
732
771
|
|
|
733
|
-
To start from a state
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
Transients cost more where a carrier's density spans many decades (minority holes in silicon
|
|
741
|
-
ahead of the pulse) and at strong injection (Δp near n₀); budget for that, or coarsen the grid.
|
|
772
|
+
To start from a state rather than simulate how it got there (a packet already injected, a salt
|
|
773
|
+
gradient laid down), give a region's `c0` a profile, `{ 'h+': { x: [...], values: [...] } }` against
|
|
774
|
+
the device's x, and `advance()` the new `Device` without `solve()`. A given `c0` is the starting
|
|
775
|
+
state even for a species a contact feeds; the rest start at their contact's level. The grid isn't
|
|
776
|
+
fitted to the profile: grade it fine where the profile is steep (a warning says where it isn't).
|
|
777
|
+
`advance()`'s `tol` (1e-3) is each step's error, in thermal units: tighten it, or cap `dtMax`, for
|
|
778
|
+
numbers to compare with theory.
|
|
742
779
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
`
|
|
746
|
-
|
|
780
|
+
Transients cost more where a carrier's density spans many decades (minority holes in silicon)
|
|
781
|
+
and at strong injection; budget for that, or coarsen the grid. `recorder(dev, { every, times,
|
|
782
|
+
probes })` (kit) runs one in pieces: `rec.trace` is the whole run's, and `rec.frame(t)` the
|
|
783
|
+
whole solution at or before t, for a time slider.
|
|
747
784
|
|
|
748
785
|
## Reading level diagrams
|
|
749
786
|
|
|
750
|
-
What `bandDiagram()` and `traces()` draw, and how to explain it
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
electrode face, the step from the metal's Fermi level to it is the surface overpotential;
|
|
764
|
-
through the solution it slopes with ohmic drop and concentration polarisation. Each couple has
|
|
765
|
-
its own; they agree only in redox equilibrium.
|
|
766
|
-
- **Standard redox level** (`standard: true`), thin and dashed: every participant at reference
|
|
767
|
-
concentration. The redox level sits RT/(zF) ln(a_Ox/a_Red) from it (Nernst, locally).
|
|
768
|
-
- **Electrode potentials** are gaps to the SHE's standard level V°_e⁻(SHE): E = V_e⁻(electrode) −
|
|
769
|
-
V°_e⁻(SHE), E° = V°_e⁻(Ox/Red) − V°_e⁻(SHE), η = E − E_eq. With the data library's μ°,
|
|
770
|
-
V°_e⁻(SHE) in water is φ, so `phi: true` draws it.
|
|
771
|
-
- **Species voltage** V_i = μ̄_i/(z_iF) (thick, solid) for every charged species: the Fermi level
|
|
772
|
-
for e⁻, a quasi-Fermi level for e⁻ and h⁺ in a semiconductor, what a reversible electrode reads
|
|
773
|
-
for an ion. It's a display of μ̄_i (the physics, which also covers neutral species): each
|
|
774
|
-
species moves down its own μ̄_i, drift and diffusion together, so its current flows down slopes
|
|
775
|
-
in V_i (cations and holes move downhill, electrons and anions uphill), and it's in equilibrium
|
|
776
|
-
where V_i is flat. A step at a face means resistance there.
|
|
787
|
+
What `bandDiagram()` and `traces()` draw, and how to explain it (docs/visualization.md has more):
|
|
788
|
+
|
|
789
|
+
- **Which picture.** For electrons and holes only, draw the familiar band diagram, energy up:
|
|
790
|
+
`bandDiagram(sol, { energy: true })` (E_c, E_v, E_Fn, E_Fp in eV, E = −V). The species voltages
|
|
791
|
+
below are the same lines in volts, turned over: use them where ions share the diagram, or for
|
|
792
|
+
equilibrium between species, never in place of the band diagram.
|
|
793
|
+
- **Species voltage** V_i = μ̄_i/(z_iF) (thick, solid): the Fermi level for e⁻ (what a voltmeter
|
|
794
|
+
reads), a quasi-Fermi level in a semiconductor, what a reversible electrode reads for an ion.
|
|
795
|
+
Each species moves down its own μ̄_i, drift and diffusion together, so its current flows down
|
|
796
|
+
slopes in V_i (cations and holes downhill, electrons and anions uphill); flat is equilibrium,
|
|
797
|
+
and a step at a face is resistance there. Call it the species voltage and say what it is, since
|
|
798
|
+
it's new to most readers; credit Jamnik and Maier (Phys. Chem. Chem. Phys. 3, 1668 (2001))
|
|
799
|
+
where V_i rails explain impedance or linear response.
|
|
777
800
|
- **Standard level** V°_i = φ + μ°_i/(z_iF) (thin, solid): a band edge for e⁻ and h⁺; V_i − V°_i =
|
|
778
|
-
(RT/z_iF) ln(c/c_ref) for ideal species.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
- **
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
- **
|
|
791
|
-
|
|
792
|
-
`'
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
interface nodes share an x (draw them as vertical steps). Or read the solution directly:
|
|
796
|
-
`sol.x`, `sol.V[name]`, `sol.Vstd[name]`, `sol.c[name]`, `sol.phi`.
|
|
797
|
-
- Call V_i the species voltage and say what it is; it's new to most readers. Credit Jamnik and
|
|
798
|
-
Maier (Phys. Chem. Chem. Phys. 3, 1668 (2001)) where V_i rails explain impedance or linear
|
|
799
|
-
response.
|
|
801
|
+
(RT/z_iF) ln(c/c_ref) for ideal species. Its steps at faces are bookkeeping.
|
|
802
|
+
- **Redox level** of Ox + z e⁻ ⇌ Red (`level(sol, half)`, or `levels: [{ half }]` in the plot
|
|
803
|
+
options; thick, dashed): (μ̄_Ox − μ̄_Red)/(zF), where an electrode by that couple would sit in
|
|
804
|
+
equilibrium with the local solution. At an electrode its step from the metal's Fermi level is
|
|
805
|
+
the surface overpotential. `standard: true` gives its standard level (thin, dashed).
|
|
806
|
+
- **Electrode potentials** are gaps to the SHE's standard level, which with the library's μ° is φ
|
|
807
|
+
in water (`phi: true` draws it): E = V_e⁻(electrode) − V°_e⁻(SHE).
|
|
808
|
+
- **What's left out.** φ is a per-material convention, and −eφ isn't any electron's energy, so it
|
|
809
|
+
isn't drawn, except across an insulator with no mobile species (`phi: ['SiO2']`: its slope is
|
|
810
|
+
the field). No level is a fixed reference: shifting every line together changes nothing. A gap
|
|
811
|
+
between two species' levels carries the elements' conventional zeros, so read it through a
|
|
812
|
+
reaction; `shifts` (marked ⌇) moves one species' lines to fit a shared axis.
|
|
813
|
+
- **Your own style.** `traces(sol, opts)` gives `{ x, series: [{ id, label, kind, role, slot, y }],
|
|
814
|
+
regions, faces, range }`: `kind` says how a line reads (`'level'`, `'standard'`, `'redox'`,
|
|
815
|
+
`'redox-standard'`, `'phi'`), `themeColor(role, slot)` (plot) gives our colours,
|
|
816
|
+
`labelParts(label)` splits TeX-style subscripts, `NaN` breaks a line, and a face's doubled nodes
|
|
817
|
+
share an x (a vertical step).
|
|
800
818
|
|
|
801
819
|
## Good habits
|
|
802
820
|
|
|
803
|
-
- **Draw the closed-form answer alongside the simulation
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
device isn't one, say so and pick the slice that still tells the true story: a MOSFET as its
|
|
811
|
-
gate stack (the MOS capacitor) or its channel (the thin-film transistor), never both at once,
|
|
812
|
-
since pinch-off is 2D; a BJT along its emitter–base–collector axis; one particle of a porous
|
|
813
|
-
battery electrode, or a thin dense film, rather than the whole porous electrode with diffusion
|
|
814
|
-
inside its particles (that's a P2D model). A metal that reacts all along a film of electrolyte
|
|
815
|
-
is a port with `reactions`, not a face at each end.
|
|
821
|
+
- **Draw the closed-form answer alongside the simulation** wherever a textbook limit exists, as
|
|
822
|
+
the templates do. A solver's output looks authoritative even when an input is wrong, and a
|
|
823
|
+
mismatch is the cheapest way to catch it. Where the two should differ, say why.
|
|
824
|
+
- **Stay honestly inside 1D.** Where the real device isn't one-dimensional, say so and pick the
|
|
825
|
+
slice that tells the true story: a MOSFET as its gate stack or its channel, never both (pinch-off
|
|
826
|
+
is 2D); a BJT along its emitter–base–collector axis; one particle of a porous battery electrode
|
|
827
|
+
rather than the whole electrode (that's a P2D model).
|
|
816
828
|
|
|
817
829
|
## Common mistakes
|
|
818
830
|
|
|
819
|
-
- **
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
of the
|
|
824
|
-
- **A
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
- **`ohmic()` on a metal.** Its default holds both e⁻ and h⁺, for semiconductors. On a metal
|
|
831
|
-
layer, write `ohmic(V, ['e-'])`.
|
|
832
|
-
- **Ions on both sides of a reacting face.** Label them: `'Li+(left) = Li+(right)'`, and give
|
|
833
|
-
their link (`species: { 'Li+': 'blocked' }` if they cross only by the reaction).
|
|
834
|
-
- **Unit slips.** D in cm²/s, μ° in eV, capacitance in µF/cm², conductance in mS/cm², T in °C.
|
|
835
|
-
`describe(def)` flags numbers that look like these.
|
|
836
|
-
- **A closed system with no amount.** A species that doesn't reach a contact needs `c0`.
|
|
837
|
-
- **An unbalanced `c0` in a closed neutral region.** With `epsr: 0` and no contact feeding a
|
|
838
|
-
charged species, Σ z·c0 must be zero (traces included): `… carries a net charge of … mol/m³`.
|
|
839
|
-
Add the counter-ion's share. (Where a contact feeds one, it makes up the difference.)
|
|
840
|
-
- **A sharp junction between very different solutions** (3 M KCl against 1 µM traces) can't
|
|
841
|
-
start: grade it over a few thin layers mixing one solution into the other, as at a frit.
|
|
842
|
-
- **A steady solve of a closed cell where every ion can cross.** Its steady state is complete
|
|
843
|
-
mixing. To watch a junction or a salt bridge, step in time from the unmixed `c0` (`advance`).
|
|
844
|
-
- **Everything floating.** At least one terminal must be held at a voltage `V`.
|
|
831
|
+
- **Errors that say what to give.** A missing alignment at a heterointerface (`vacuumDipole`
|
|
832
|
+
estimates one from electron affinities, docs/alignment.md), a metal face with no φ law,
|
|
833
|
+
everything floating (hold one terminal at a `V`), a species that reaches no contact without a
|
|
834
|
+
`c0`, an unbalanced `c0` in a closed neutral region (add the counter-ion's share), `'2e-'` for
|
|
835
|
+
`'2 e-'`, a current drive of the wrong sign. `interfaces[f]` is the stack's f-th face.
|
|
836
|
+
- **A gate as a contact on an oxide.** It fails as `'e-' is absent from the end material 'SiO2'`: make the
|
|
837
|
+
gate a metal layer with a capacitive face, as in the MOS template.
|
|
838
|
+
- **`ohmic()` on a metal.** Its default holds e⁻ and h⁺; on a metal write `ohmic(V, ['e-'])`.
|
|
839
|
+
- **Ions on both sides of a reacting face.** Label them, `'Li+(left) = Li+(right)'`, and block
|
|
840
|
+
them (`species: { 'Li+': 'blocked' }`) if they cross only by the reaction.
|
|
841
|
+
- **Unit slips.** D in cm²/s, μ° in eV, µF/cm², mS/cm², T in °C. `describe(def)` flags them.
|
|
845
842
|
- **The library's data are for 25 °C** (its semiconductors for 300 K: give `T: 300`). At body
|
|
846
|
-
temperature
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
- **A membrane potential read from Cl⁻-referenced terminals.** They differ from Δφ by Cl⁻'s
|
|
853
|
-
Nernst term: read V_m from `sol.phi` (or a `{ x, quantity: 'phi' }` probe), or use baths
|
|
854
|
-
without a reference.
|
|
855
|
-
- **A coefficient without its space.** `'2e- + …'` reads `2e-` as a species name; the error says
|
|
856
|
-
to write `2 e-`.
|
|
857
|
-
- **Changing c0 with `set()`.** A carried state keeps what it holds; `c0` is only the starting
|
|
858
|
-
composition. Make a new `Device` to start over from it.
|
|
843
|
+
temperature the same μ° and D are a fair start; D rises about 2% per kelvin in water.
|
|
844
|
+
- **A steady solve of a closed cell where every ion can cross**: its steady state is complete
|
|
845
|
+
mixing. To watch a junction or a salt bridge, `advance()` from the unmixed `c0`.
|
|
846
|
+
- **A sharp junction between very different solutions** (3 M KCl against 1 µM) can't start:
|
|
847
|
+
grade it over a few thin layers, as at a frit.
|
|
848
|
+
- **Changing `c0` with `set()`.** A carried state keeps what it holds: make a new `Device`.
|
|
859
849
|
- **An unresolved double layer.** If `sol.warnings` says so, refine the grid (`hmin` below the
|
|
860
850
|
Debye length) or use a neutral model (`epsr: 0`, `phi: 'neutral'`).
|
|
861
851
|
|
|
@@ -874,6 +864,6 @@ has the long form):
|
|
|
874
864
|
- checking: `describe`, `check`, `polarization` (an electrode's reactions at hypothetical
|
|
875
865
|
metal levels, with the solution held: an Evans diagram), `henderson` and `planck`
|
|
876
866
|
(liquid-junction potentials);
|
|
877
|
-
- pages: `live`, `traces`, `recorder`;
|
|
867
|
+
- pages: `live`, `traces`, `recorder`, `particles`;
|
|
878
868
|
- curated devices: `perovskiteCell`, `PEROVSKITE_SCANS`, `hysteresis`.
|
|
879
869
|
- `driftlet/plot` (a quick look): `bandDiagram`, `levelChart`, `labelParts`, `themeColor`.
|