driftlet 0.11.4 → 0.13.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 +50 -74
- package/docs/device.md +143 -9
- package/docs/kit.md +25 -2
- package/docs/numerics.md +8 -1
- package/docs/reliability.md +60 -3
- package/docs/statistics.md +10 -0
- package/llms.txt +572 -259
- package/package.json +1 -1
- package/src/channels.js +96 -0
- package/src/describe.js +4 -2
- package/src/device.js +108 -21
- package/src/index.js +6 -1
- package/src/kit.js +1 -0
- package/src/recorder.js +1 -1
- package/src/solution.js +28 -3
- package/src/solver.js +292 -67
- package/src/stack.js +6 -4
- package/src/types.js +2 -1
- package/types/channels.d.ts +50 -0
- package/types/kit.d.ts +1 -0
- package/types/recorder.d.ts +7 -3
- package/types/solver.d.ts +32 -5
- package/types/stack.d.ts +6 -5
- package/types/types.d.ts +5 -0
package/llms.txt
CHANGED
|
@@ -1,72 +1,137 @@
|
|
|
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: semiconductor junctions
|
|
5
|
-
electrochemical cells, double layers, membranes,
|
|
6
|
-
|
|
4
|
+
(Poisson–Nernst–Planck) problems fast enough for live web demos: semiconductor junctions and
|
|
5
|
+
solar cells, electrochemical cells and batteries, double layers, membranes, nerves, mixed
|
|
6
|
+
conductors. These are one problem with different casts of species: electrons, holes, ions and
|
|
7
|
+
neutral molecules, each moving down its own electrochemical potential μ̄, coupled by Poisson's
|
|
8
|
+
equation, reactions and interfaces. So a device in any field is built the same way, from the
|
|
9
|
+
same pieces. It runs in a browser, a Web Worker or Node, from npm or a CDN.
|
|
7
10
|
|
|
8
11
|
This guide is for writing a demo or calculation with it. Every code block below runs in the
|
|
9
|
-
test suite. The
|
|
10
|
-
docs/conventions.md (signs and levels)
|
|
12
|
+
test suite. The references: docs/device.md (the definition), docs/kit.md (the helpers),
|
|
13
|
+
docs/conventions.md (signs and levels), docs/visualization.md (reading level diagrams),
|
|
14
|
+
docs/alignment.md (band offsets and work functions), docs/statistics.md (non-ideal materials),
|
|
15
|
+
docs/data.md (the data library) and docs/reliability.md (what's validated, and the limits).
|
|
11
16
|
|
|
12
17
|
## Imports
|
|
13
18
|
|
|
14
|
-
From a CDN, pin the version:
|
|
19
|
+
From a CDN, pin the version you tested against:
|
|
15
20
|
|
|
16
21
|
```js nocheck
|
|
17
|
-
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
18
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
19
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
22
|
+
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.13.0/src/index.js';
|
|
23
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.13.0/src/kit.js';
|
|
24
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.13.0/src/plot.js';
|
|
20
25
|
```
|
|
21
26
|
|
|
22
|
-
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
27
|
+
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`. Every export is listed at the end.
|
|
23
28
|
|
|
24
29
|
## What to know first
|
|
25
30
|
|
|
26
|
-
- **Units are SI**: m, mol/m³ (1 M = 1000 mol/m³), J/mol, m²/s, V, A/m², F/m², C/m³.
|
|
27
|
-
`units`
|
|
28
|
-
`units.eV(1.12)`
|
|
31
|
+
- **Units are SI**: m, mol/m³ (1 M = 1000 mol/m³), J/mol, m²/s, V, A/m², F/m², S/m², C/m³.
|
|
32
|
+
`units` converts in (`units.um(1)`, `units.perCm3(1e17)` for carriers per cm³ or cm⁻³·s⁻¹,
|
|
33
|
+
`units.eV(1.12)` to J/mol, `units.cm2PerS(36)`, `units.molar(0.1)`) and back (`units.toPerCm3`,
|
|
34
|
+
`toEV`, `toMolar`, `toUm`, …). For physiology: 1 mS/cm² = 10 S/m², 1 µF/cm² = 0.01 F/m²,
|
|
35
|
+
1 µA/cm² = 0.01 A/m², mM = mol/m³.
|
|
29
36
|
- **Everything you set or read is an electrochemical potential μ̄** (or a voltage made from one).
|
|
30
|
-
A terminal's `V` is the voltage of its terminal species: for an electron contact,
|
|
31
|
-
|
|
32
|
-
- **Solutions**
|
|
33
|
-
μ̄/(zF), a quasi-Fermi level for e⁻ and h⁺)
|
|
34
|
-
and
|
|
35
|
-
species
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
are
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
`
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
37
|
+
A terminal's `V` is the voltage of its terminal species: for an electron contact, V = −μ̄_e/F,
|
|
38
|
+
what a voltmeter reads. φ is bookkeeping, per material, and never a control.
|
|
39
|
+
- **Solutions** are plain objects of `Float64Array`s: `x`, `phi`, and per species `c`
|
|
40
|
+
(concentration), `mu` (μ̄), `V` (the species voltage μ̄/(zF), a quasi-Fermi level for e⁻ and h⁺)
|
|
41
|
+
and `Vstd` (the standard level, a band edge for e⁻ and h⁺; in volts the conduction band sits
|
|
42
|
+
below the valence band). `NaN` marks where a value doesn't exist: a species absent from a
|
|
43
|
+
material, `V` and `Vstd` of a neutral species, `c` and `Vstd` inside a metal.
|
|
44
|
+
- **Currents.** `sol.terminals[name]` gives each terminal's `{ V, current }`, its current into
|
|
45
|
+
the device. `sol.current` is the right contact's current toward +x (with ports, the two
|
|
46
|
+
contacts' differ: read each from `sol.terminals`). Currents, fluxes and amounts are per m² of
|
|
47
|
+
a planar device; with a `geometry` (spherical, cylindrical or any cross-section A(x)) they are
|
|
48
|
+
totals through it.
|
|
49
|
+
- **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`). An error names
|
|
51
|
+
the path and says what to give. (Kinetic details are defaulted, as a symmetric α = 0.5 or a
|
|
52
|
+
midgap trap, and listed in docs/device.md.)
|
|
53
|
+
- **Check before trusting.** `sol.converged` must be true, and read `sol.warnings` (a solve
|
|
54
|
+
that fails always says something there). `describe(def)` (kit) summarises a definition and
|
|
55
|
+
flags likely unit slips. Once a device works, show or log `check(dev, sol).text` (kit): each
|
|
56
|
+
species' ledger (what every terminal and reaction supplies, summing to zero), conservation in
|
|
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
|
+
|
|
60
|
+
## How a device is written
|
|
61
|
+
|
|
62
|
+
- **`build({ T, library, stack, bulkReactions, ports, grid, geometry })`** (kit) reads a stack
|
|
63
|
+
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 the face between them. Give a layer a
|
|
65
|
+
`name` to refer to it (a port's `region`, a probe); unnamed, it's only an index. `ohmic()`,
|
|
66
|
+
`bath()` and the like write contacts, but a plain contact object works as well.
|
|
67
|
+
- **The library** is pieces with `species` and `materials`: `semiconductor('Si')` (Si, Ge, GaAs,
|
|
68
|
+
at 300 K), `aqueous(names, { epsr })` (H⁺ Li⁺ Na⁺ K⁺ Ag⁺ Cu²⁺ Zn²⁺ Fe²⁺ Fe³⁺ OH⁻ Cl⁻ NO₃⁻ SO₄²⁻ in
|
|
69
|
+
water, at 25 °C), `metal('Ag')` (Ag Cu Au Al Zn Li Pt). `aqueous(names, { epsr: 0 })` makes the
|
|
70
|
+
water strictly neutral (a macroscopic electrolyte, no double layers). An ion it lacks is pushed
|
|
71
|
+
into both lists: `lib.species.push({ name: 'A-', z: -1, cRef: 1000 })` and
|
|
72
|
+
`lib.materials.water.species['A-'] = { D, mu0 }`. Only μ° differences within a reaction
|
|
73
|
+
matter, so for a redox couple, μ°_red − μ°_ox = −F·E° (E° against SHE, with the library's
|
|
74
|
+
μ°_H⁺ = 0). An insulator is a material with no mobile species:
|
|
75
|
+
`{ species: [], materials: { SiO2: { epsr: 3.9, species: {} } } }`.
|
|
76
|
+
- **Faces.** Between two different non-metal materials, give an alignment, `{ dipole }` or
|
|
77
|
+
`{ step: { species: 'e-', value } }` (J/mol), or `phi: 'neutral'` for a macroscopic model.
|
|
78
|
+
A face next to a metal takes no alignment, but if the other side has ε > 0 it needs a φ law:
|
|
79
|
+
`{ phi: { type: 'capacitive', C }, zeroCharge }` or `phi: 'neutral'`. Reactions at a face
|
|
80
|
+
label their sides: `'Li+(left) = Li+(right)'`.
|
|
81
|
+
- **Ports** are terminals attached sideways to a window of nodes inside a region (`from`, `to`,
|
|
82
|
+
m from its left end): a source or drain, an injector, a reference electrode (at `I: 0`), an
|
|
83
|
+
electrode spread through a film (`reactions`, `area`), a gate along a channel or a membrane along
|
|
84
|
+
an axon (`capacitance`, `gates`).
|
|
85
|
+
- **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 }`. A voltage or current can be a waveform: a table
|
|
87
|
+
`{ t: [...], values: [...] }`, piecewise linear (a step is two points at one time), or the kit's
|
|
88
|
+
`pulse({ start, width, amplitude, rise, base })`, `square({ high, low, period, duty })`,
|
|
89
|
+
`ramp({ from, to, duration, start })` and `triangle({ from, to, period })` (from `from` to `to`
|
|
90
|
+
and back each period from t = 0, repeating: a cyclic voltammogram, in terminal volts). A drive's
|
|
91
|
+
current is the conventional current into the device: electrons fed in are I < 0. `dev.set({ contacts: { right: { V } } })`
|
|
92
|
+
changes a drive and keeps the state as a warm start; ports are patched by name,
|
|
93
|
+
`{ ports: { gate: { V } } }`.
|
|
94
|
+
- **Reactions** are equations: `{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }`. A coefficient is a
|
|
95
|
+
number and a space (`2 e-`); a participant that isn't a species (solid silver) is fixed by its
|
|
96
|
+
μ: `fixed: { 'Ag(s)': 0 }`. A bulk reaction's forward rate is `kf · Π c_reactants` in
|
|
97
|
+
mol/(m³·s), with c in mol/m³ (1e16 cm⁻³ is only 0.0166 mol/m³); the backward rate follows from
|
|
98
|
+
the μ°, so the net rate is the forward rate times (1 − e^(−A/RT)), A the affinity. Band-to-band
|
|
99
|
+
recombination `e- + h+ = 0` gives a minority lifetime τ = 1/(kf·N), N the majority density in
|
|
100
|
+
mol/m³: `kf: 1e8` is τ ≈ 0.6 µs at 1e16 cm⁻³. Trap-assisted (SRH) recombination is
|
|
101
|
+
`{ equation: 'e- + h+ = 0', srh: { Si: { tauN, tauP } } }` (s), and at a face
|
|
102
|
+
`{ equation: 'e-(left) + h+(right) = 0', srh: { vn, vp } }` (m/s). Band-to-band and the kit's
|
|
103
|
+
`recombination()` are bimolecular, so their lifetime shortens at high injection; for a constant
|
|
104
|
+
minority lifetime τ_p at any injection, use SRH with τ_N ≪ τ_p. Generation is a reaction from
|
|
105
|
+
a reservoir (`photon = e- + h+`), and light absorbed as it goes in is
|
|
106
|
+
`photogeneration({ material, flux, alpha, mu: units.eV(3), from, to })`. A face reaction's
|
|
107
|
+
`k0` is in mol/(m²·s), the rate with every participant at its reference concentration (cRef):
|
|
108
|
+
i₀ = nF·k0 there, and k0 = k°·cRef for a textbook k° in m/s (1 cm/s at cRef = 1000 mol/m³:
|
|
109
|
+
`k0: 10`). A couple the library lacks is two species pushed into it (`z: 0` for a neutral one),
|
|
110
|
+
with μ°_red − μ°_ox = −F·E°.
|
|
111
|
+
|
|
112
|
+
`build()` writes a plain definition for `new Device()`, which can be written by hand too:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
116
|
+
|
|
117
|
+
// A concentration cell: 100 mM KCl against 10 mM through a gap, read by Ag/AgCl electrodes.
|
|
118
|
+
const D = { 'K+': 1.957e-9, 'Cl-': 2.032e-9 };
|
|
119
|
+
const dev = new Device({
|
|
120
|
+
T: 298.15,
|
|
121
|
+
species: [{ name: 'K+', z: 1, cRef: 1000 }, { name: 'Cl-', z: -1, cRef: 1000 }], // cRef: the standard state, mol/m³
|
|
122
|
+
materials: { water: { epsr: 0, species: { 'K+': { D: D['K+'], mu0: -283.27e3 }, 'Cl-': { D: D['Cl-'], mu0: -131.23e3 } } } },
|
|
123
|
+
regions: [{ name: 'gap', material: 'water', length: 100e-6, c0: { 'K+': 10, 'Cl-': 10 } }], // left to right
|
|
124
|
+
// (interfaces: [...] would hold the faces between regions, one fewer than the regions)
|
|
125
|
+
contacts: {
|
|
126
|
+
left: { V: 0, bath: { c: { 'K+': 100, 'Cl-': 100 }, reference: 'Cl-' } }, // V is Cl⁻'s level: an Ag/AgCl electrode's
|
|
127
|
+
right: { I: 0, bath: { c: { 'K+': 10, 'Cl-': 10 }, reference: 'Cl-' } }, // open circuit
|
|
128
|
+
},
|
|
129
|
+
grid: { hmin: 0.1e-6, hmax: 5e-6 },
|
|
130
|
+
});
|
|
131
|
+
const sol = dev.solve();
|
|
132
|
+
const tK = D['K+'] / (D['K+'] + D['Cl-']), VT = (GAS_CONSTANT * 298.15) / FARADAY;
|
|
133
|
+
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
|
+
```
|
|
70
135
|
|
|
71
136
|
## Template: a semiconductor junction
|
|
72
137
|
|
|
@@ -97,40 +162,51 @@ for (const V of [0, 0.2, 0.4, 0.6]) {
|
|
|
97
162
|
}
|
|
98
163
|
```
|
|
99
164
|
|
|
100
|
-
## Template:
|
|
165
|
+
## Template: a live page with a slider
|
|
101
166
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
167
|
+
`live()` merges slider moves made during a solve, warm-starts each solve and keeps the last
|
|
168
|
+
good solution; on a failure `info.failed` is set (with `info.error`) and the solution is the last
|
|
169
|
+
good one. `bandDiagram()` returns an SVG string; give it a fixed `range: [lo, hi]` so the axis
|
|
170
|
+
doesn't jump as the slider moves. Pass `worker: true` to `live()` to solve in a Web Worker (it
|
|
171
|
+
works with the library loaded from a CDN). `onsolution` may call `set()`, e.g. to iterate toward
|
|
172
|
+
a value the page finds itself: each call queues another solve, so make sure it converges rather
|
|
173
|
+
than oscillates. A device of a few hundred nodes solves in milliseconds; thousands, or
|
|
174
|
+
transients, want a worker and `advance(t, { budgetMs })` per frame. (A headless browser taking a
|
|
175
|
+
screenshot of a page that loads the library from a CDN needs a long virtual-time budget, 30 s.)
|
|
105
176
|
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
|
|
177
|
+
```html
|
|
178
|
+
<!doctype html>
|
|
179
|
+
<meta charset="utf-8">
|
|
180
|
+
<title>pn junction</title>
|
|
181
|
+
<label>bias <input id="V" type="range" min="-0.5" max="0.7" step="0.01" value="0"></label>
|
|
182
|
+
<span id="readout"></span>
|
|
183
|
+
<div id="figure"></div>
|
|
184
|
+
<script type="module">
|
|
185
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.13.0/src/index.js';
|
|
186
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.13.0/src/kit.js';
|
|
187
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.13.0/src/plot.js';
|
|
109
188
|
|
|
110
|
-
const silver = half('Ag+ + e- = Ag(s)', { 'Ag(s)': 0 }); // μ of the element: 0 on table conventions
|
|
111
|
-
// A Stern layer at each metal face: its capacitance and the potential of zero charge are
|
|
112
|
-
// properties of that interface, so they're yours to choose.
|
|
113
|
-
const face = { phi: { type: 'capacitive', C: 0.2 }, zeroCharge: 0.1, reactions: [{ ...silver, k0: 1e-3, alpha: 0.5 }] };
|
|
114
189
|
const def = build({
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
layer('water', 20e-6, { c0: { 'H+': 1, 'Ag+': 10, 'NO3-': 11 } }),
|
|
121
|
-
face,
|
|
122
|
-
layer('Ag', 1e-6),
|
|
123
|
-
ohmic(0.05, ['e-']),
|
|
124
|
-
],
|
|
125
|
-
grid: { hmin: 0.1e-9, hmax: 200e-9, ratio: 1.15 },
|
|
190
|
+
T: 300,
|
|
191
|
+
library: [semiconductor('Si')],
|
|
192
|
+
stack: [ohmic(0), layer('Si', 1e-6, { donors: units.perCm3(1e17) }), layer('Si', 1e-6, { acceptors: units.perCm3(1e16) }), ohmic(0)],
|
|
193
|
+
bulkReactions: [{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }],
|
|
194
|
+
grid: { hmin: 1e-9, hmax: 20e-9 },
|
|
126
195
|
});
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
196
|
+
const slider = document.getElementById('V');
|
|
197
|
+
const dev = live(def, {
|
|
198
|
+
onsolution(sol, info) {
|
|
199
|
+
document.getElementById('figure').innerHTML = bandDiagram(sol, { energy: true, range: [-1.2, 1.7], title: `V = ${sol.terminals.right.V.toFixed(2)} V` });
|
|
200
|
+
document.getElementById('readout').textContent = info.failed ? 'no solution here' : `I = ${sol.current.toExponential(2)} A/m²`;
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
slider.addEventListener('input', () => dev.set({ contacts: { right: { V: +slider.value } } }));
|
|
204
|
+
</script>
|
|
132
205
|
```
|
|
133
206
|
|
|
207
|
+
To draw in your own style, `traces(sol, opts)` (kit, the same options) gives the lines as data;
|
|
208
|
+
see "Reading level diagrams" below.
|
|
209
|
+
|
|
134
210
|
## Template: a solar cell
|
|
135
211
|
|
|
136
212
|
Light is a reservoir: `photon = e- + h+` with the photons' μ well above the gap generates at
|
|
@@ -162,69 +238,141 @@ const oc = new Device(cell({ I: 0 })).solve(); // open circuit
|
|
|
162
238
|
console.log(`J_sc = ${sc.current.toFixed(1)} A/m², V_oc = ${oc.terminals.right.V.toFixed(3)} V`);
|
|
163
239
|
```
|
|
164
240
|
|
|
165
|
-
|
|
241
|
+
For a perovskite cell, prefer the curated `perovskiteCell({ V, light, bulkSRH })`: IonMonger's
|
|
242
|
+
default cell (mobile iodide vacancies), with its J–V scans at seven rates in `PEROVSKITE_SCANS`
|
|
243
|
+
and `hysteresis(trace, turn)` to read the same figures from yours.
|
|
166
244
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
245
|
+
## Template: a MOS capacitor
|
|
246
|
+
|
|
247
|
+
The gate is a metal layer behind its contact, its face to the oxide capacitive (a metal screens
|
|
248
|
+
within Å, so C is large) with the gate's alignment as `zeroCharge`. The oxide holds no species.
|
|
249
|
+
In 1D nothing supplies the inversion layer's electrons (a MOSFET's source and drain do), so a port
|
|
250
|
+
holds their level at the surface: the low-frequency C–V. The gate's charge is the displacement
|
|
251
|
+
into the oxide, `sol.interfaces[0].D`.
|
|
173
252
|
|
|
174
253
|
```js
|
|
175
|
-
import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
176
|
-
import { build, layer,
|
|
254
|
+
import { Device, units, EPS0, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
255
|
+
import { build, layer, ohmic, semiconductor, metal, vacuumZeroCharge } from 'driftlet/kit';
|
|
177
256
|
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
257
|
+
const oxide = { species: [], materials: { SiO2: { epsr: 3.9, species: {} } } };
|
|
258
|
+
const tox = units.nm(10), Cox = (3.9 * EPS0) / tox, NA = units.perCm3(1e17);
|
|
259
|
+
const mos = (Vg, zeroCharge) =>
|
|
260
|
+
build({
|
|
261
|
+
T: 300,
|
|
262
|
+
library: [semiconductor('Si'), metal('Al'), oxide],
|
|
263
|
+
stack: [
|
|
264
|
+
ohmic(Vg, ['e-']), // the gate's terminal: the aluminium's electrons
|
|
265
|
+
layer('Al', units.nm(20)),
|
|
266
|
+
{ phi: { type: 'capacitive', C: 10 }, zeroCharge }, // the gate's face
|
|
267
|
+
layer('SiO2', tox),
|
|
268
|
+
{ dipole: 0 }, // the oxide holds no species, so this alignment changes nothing
|
|
269
|
+
layer('Si', units.um(1), { name: 'Si', acceptors: NA }),
|
|
270
|
+
ohmic(0),
|
|
271
|
+
],
|
|
272
|
+
ports: [{ name: 'channel', region: 'Si', from: 0, to: units.nm(1), V: 0, terminal: 'e-', species: { 'e-': 'equilibrium' } }],
|
|
273
|
+
grid: { hmin: units.nm(0.25), hmax: units.nm(50), ratio: 1.15 },
|
|
274
|
+
});
|
|
275
|
+
// The gate's alignment from vacuum levels: Al's work function 4.1 eV, Si's electron affinity 4.05 eV.
|
|
276
|
+
const zc = vacuumZeroCharge(mos(0, 0), 4.1, { material: 'Si', anchor: 'e-', offset: 4.05 });
|
|
277
|
+
const dev = new Device(mos(0, zc));
|
|
278
|
+
const Q = (Vg) => {
|
|
279
|
+
dev.set({ contacts: { left: { V: Vg } } });
|
|
280
|
+
return dev.solve().interfaces[0].D;
|
|
191
281
|
};
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
282
|
+
for (const Vg of [-2, -0.95, 0, 0.4, 2]) {
|
|
283
|
+
const C = (Q(Vg + 0.01) - Q(Vg - 0.01)) / 0.02;
|
|
284
|
+
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
|
+
}
|
|
286
|
+
// At flat band the silicon is a capacitor of ε/L_D, its Debye length's.
|
|
287
|
+
const LD = Math.sqrt((11.7 * EPS0 * GAS_CONSTANT * 300) / (FARADAY * FARADAY * NA));
|
|
288
|
+
console.log(`flat band, theory: ${(1 / (1 + (Cox * LD) / (11.7 * EPS0))).toFixed(3)}`); // 0.699
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`dev.impedance([f], { terminal: 'left' })` gives the same C at low frequency. Draw the band
|
|
292
|
+
diagram with `bandDiagram(sol, { energy: true, phi: ['SiO2'] })`: φ is the line to draw across
|
|
293
|
+
an insulator, and its slope is the field.
|
|
294
|
+
|
|
295
|
+
## Template: a Schottky diode
|
|
296
|
+
|
|
297
|
+
A Schottky contact is a barrier pinned at the metal (`phi: 'pinned'`, `zeroCharge` the barrier,
|
|
298
|
+
for Si whose μ°_e⁻ is 0) with its carriers crossing at their emission velocities: a `velocity`
|
|
299
|
+
link, N_in = v (c_eq − c). With `'equilibrium'` links instead (an infinite velocity), the barrier
|
|
300
|
+
passes whatever the silicon brings it: diffusion theory, some 20 times more current here.
|
|
301
|
+
|
|
302
|
+
```js
|
|
303
|
+
import { Device, units, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
304
|
+
import { build, layer, ohmic, semiconductor } from 'driftlet/kit';
|
|
305
|
+
|
|
306
|
+
const T = 300, VT = (GAS_CONSTANT * T) / FARADAY, Si = semiconductor('Si');
|
|
307
|
+
const phiB = 0.85, A = 1.12e6; // the barrier (V); Richardson's constant for n-Si, A/(m²K²)
|
|
308
|
+
const v = (A * T * T) / (FARADAY * Si.materials.Si.species['e-'].cRef); // Bethe's emission velocity, 2.2e4 m/s (cRef is N_c)
|
|
309
|
+
const schottky = (V) => ({
|
|
310
|
+
V,
|
|
311
|
+
terminal: 'e-',
|
|
312
|
+
species: { 'e-': { type: 'velocity', v }, 'h+': { type: 'velocity', v, offset: 0 } },
|
|
313
|
+
phi: 'pinned',
|
|
314
|
+
zeroCharge: phiB, // E_c − E_F at the metal
|
|
315
|
+
});
|
|
316
|
+
const dev = new Device(build({ T, library: [Si], stack: [schottky(0), layer('Si', units.um(2), { donors: units.perCm3(1e16) }), ohmic(0)], grid: { hmin: units.nm(1), hmax: units.nm(20) } }));
|
|
317
|
+
for (const V of [0.1, 0.2, 0.3]) {
|
|
318
|
+
dev.set({ contacts: { left: { V } } }); // the metal positive: forward
|
|
319
|
+
const J = dev.solve().current, te = A * T * T * Math.exp(-phiB / VT) * Math.expm1(V / VT);
|
|
320
|
+
console.log(`${V} V: J ${J.toExponential(3)} A/m², thermionic emission ${te.toExponential(3)}`); // about 4% below: the depletion region in series
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The built-in potential is `sol.phi.at(-1) − sol.phi[0]` at zero bias, and ∫(N_D − n)dx/N_D
|
|
325
|
+
gives the depletion width. The same `velocity` link at an ohmic contact is its surface
|
|
326
|
+
recombination velocity.
|
|
327
|
+
|
|
328
|
+
## Template: an electrochemical cell
|
|
329
|
+
|
|
330
|
+
An electrode is a metal layer with the reaction at its face, behind a contact holding the
|
|
331
|
+
metal's electrons (`ohmic(V, ['e-'])`).
|
|
332
|
+
|
|
333
|
+
```js
|
|
334
|
+
import { Device } from 'driftlet';
|
|
335
|
+
import { build, layer, ohmic, half, level, SHE, aqueous, metal } from 'driftlet/kit';
|
|
336
|
+
|
|
337
|
+
const silver = half('Ag+ + e- = Ag(s)', { 'Ag(s)': 0 }); // μ of the element: 0 on table conventions
|
|
338
|
+
// A Stern layer at each metal face: its capacitance and the potential of zero charge are
|
|
339
|
+
// properties of that interface, so they're yours to choose.
|
|
340
|
+
const face = { phi: { type: 'capacitive', C: 0.2 }, zeroCharge: 0.1, reactions: [{ ...silver, k0: 1e-3, alpha: 0.5 }] };
|
|
341
|
+
const def = build({
|
|
342
|
+
library: [aqueous(['H+', 'Ag+', 'NO3-']), metal('Ag')],
|
|
195
343
|
stack: [
|
|
196
|
-
|
|
197
|
-
layer('
|
|
198
|
-
|
|
199
|
-
layer('water',
|
|
200
|
-
|
|
344
|
+
ohmic(0, ['e-']),
|
|
345
|
+
layer('Ag', 1e-6),
|
|
346
|
+
face,
|
|
347
|
+
layer('water', 20e-6, { c0: { 'H+': 1, 'Ag+': 10, 'NO3-': 11 } }),
|
|
348
|
+
face,
|
|
349
|
+
layer('Ag', 1e-6),
|
|
350
|
+
ohmic(0.05, ['e-']),
|
|
201
351
|
],
|
|
202
|
-
grid: { hmin:
|
|
352
|
+
grid: { hmin: 0.1e-9, hmax: 200e-9, ratio: 1.15 },
|
|
203
353
|
});
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
console.log(
|
|
209
|
-
console.log(check(dev, sol).text); // per compartment: Na⁺ leaks into the cytoplasm as fast as it's pumped out
|
|
354
|
+
const sol = new Device(def).solve();
|
|
355
|
+
const mid = sol.x.length >> 1;
|
|
356
|
+
console.log(`I = ${sol.current.toFixed(2)} A/m²`);
|
|
357
|
+
// The silver couple's redox level, and the SHE level, mid-cell (electron voltages).
|
|
358
|
+
console.log(level(sol, silver)[mid] - level(sol, SHE, { standard: true })[mid]); // ≈ 0.80 + (RT/F) ln(c/1 M)
|
|
210
359
|
```
|
|
211
360
|
|
|
212
|
-
|
|
213
|
-
current), V_m is GHK. Without the pump, a closed cell runs down to Donnan equilibrium. driftlet
|
|
214
|
-
has no voltage-gated channels; a permeability can be changed with `set()` between steps.
|
|
215
|
-
|
|
216
|
-
## Template: an electrode in a stirred solution
|
|
361
|
+
## Template: an electrode in a stirred solution, and a potential step
|
|
217
362
|
|
|
218
363
|
One working electrode, a stagnant layer of solution, and the well-stirred bulk as a bath. The
|
|
219
364
|
bath's terminal is its reference species (Cl⁻ here, like a Ag/AgCl reference), so terminal
|
|
220
|
-
voltages are against that.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
365
|
+
voltages are against that. An electrode's potential against SHE is its electrons' V minus φ in
|
|
366
|
+
the solution (with the library's μ°, μ°_H⁺ = 0), read in the bulk. A terminal's current is into
|
|
367
|
+
the device, so at the working electrode it's positive when oxidising (anodic). The bath sits
|
|
368
|
+
across the stagnant layer, so its resistance is in series (uncompensated IR drop: keep the
|
|
369
|
+
layer thin or the supporting salt strong where the surface potential matters). An excess of
|
|
370
|
+
supporting electrolyte suppresses migration but doesn't remove it: at 100:1 the limiting current
|
|
371
|
+
is still about 1% off pure diffusion.
|
|
224
372
|
|
|
225
373
|
```js
|
|
226
374
|
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
227
|
-
import { build, layer, ohmic, bath, half, aqueous, metal } from 'driftlet/kit';
|
|
375
|
+
import { build, layer, ohmic, bath, half, aqueous, metal, IONS } from 'driftlet/kit';
|
|
228
376
|
|
|
229
377
|
const iron = half('Fe3+ + e- = Fe2+');
|
|
230
378
|
const electrode = (drive) =>
|
|
@@ -233,7 +381,7 @@ const electrode = (drive) =>
|
|
|
233
381
|
stack: [
|
|
234
382
|
ohmic(drive, ['e-']), // the working electrode's terminal: the platinum's electrons
|
|
235
383
|
layer('Pt', 1e-6),
|
|
236
|
-
{ reactions: [{ ...iron, k0: 1e-3 }] },
|
|
384
|
+
{ reactions: [{ ...iron, k0: 1e-3 }] }, // i₀ = F·k0 ≈ 96 A/m² at 1 M each; about 1 A/m² at these mM
|
|
237
385
|
layer('water', 20e-6), // the stagnant layer
|
|
238
386
|
bath({ 'K+': 500, 'Cl-': 535, 'Fe3+': 5, 'Fe2+': 10 }, 'Cl-'), // the stirred bulk (neutral)
|
|
239
387
|
],
|
|
@@ -241,12 +389,24 @@ const electrode = (drive) =>
|
|
|
241
389
|
});
|
|
242
390
|
// At rest (no current), the electrode sits at the couple's Nernst potential.
|
|
243
391
|
const rest = new Device(electrode({ I: 0 })).solve();
|
|
244
|
-
const
|
|
245
|
-
const VT = (GAS_CONSTANT * 298.15) / FARADAY;
|
|
246
|
-
console.log(`rest
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
392
|
+
const vsSHE = (sol) => sol.V['e-'][0] - sol.phi.at(-1);
|
|
393
|
+
const VT = (GAS_CONSTANT * 298.15) / FARADAY, Erest = vsSHE(rest);
|
|
394
|
+
console.log(`rest ${Erest.toFixed(4)} V vs SHE; E° + V_T ln(c₃/c₂) = ${(0.769 + VT * Math.log(5 / 10)).toFixed(4)} V`);
|
|
395
|
+
const drive = (E) => E - Erest + rest.terminals.left.V; // the terminal voltage for E vs SHE
|
|
396
|
+
// Steady, 0.2 V positive of rest: oxidising Fe²⁺. Its limiting current F·D·c/L is 35 A/m²; the
|
|
397
|
+
// kinetics (i₀ ≈ 1 A/m²) still hold it to about half here.
|
|
398
|
+
console.log(`anodic ${new Device(electrode(drive(Erest + 0.2))).solve().terminals.left.current.toFixed(2)} A/m²`); // 16.81
|
|
399
|
+
// A potential step from rest to 0.5 V positive: at first Fe²⁺ is depleted as at a plane,
|
|
400
|
+
// Cottrell's F·c·√(D/πt), until the stagnant layer is crossed (L²/D ≈ 0.5 s).
|
|
401
|
+
const dev = new Device(electrode(drive(Erest)));
|
|
402
|
+
dev.solve();
|
|
403
|
+
dev.set({ contacts: { left: { V: drive(Erest + 0.5) } } });
|
|
404
|
+
const run = dev.advance(0.03, { dtMax: 1e-3 }); // run.trace: this call's accepted steps
|
|
405
|
+
for (const t of [3e-3, 1e-2, 3e-2]) {
|
|
406
|
+
const k = run.trace.t.findIndex((tk) => tk >= t);
|
|
407
|
+
// (trace.current is the right contact's toward +x: positive for an anodic electrode on the left)
|
|
408
|
+
console.log(`${t * 1e3} ms: ${run.trace.current[k].toFixed(2)} A/m², Cottrell ${(FARADAY * 10 * Math.sqrt(IONS['Fe2+'].D / (Math.PI * run.trace.t[k]))).toFixed(2)}`);
|
|
409
|
+
}
|
|
250
410
|
```
|
|
251
411
|
|
|
252
412
|
## Template: corrosion under a drop (an electrode spread through a film)
|
|
@@ -261,7 +421,7 @@ a total through it (A, mol), not per m².
|
|
|
261
421
|
|
|
262
422
|
```js
|
|
263
423
|
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
264
|
-
import { aqueous, polarization } from 'driftlet/kit';
|
|
424
|
+
import { aqueous, polarization, H2O } from 'driftlet/kit';
|
|
265
425
|
|
|
266
426
|
// Salt water on iron: a drop 3 mm across. The iron dissolves and reduces O₂, which comes in from
|
|
267
427
|
// the air through the drop's top, fastest at the thin rim: the centre corrodes, the rim doesn't.
|
|
@@ -279,7 +439,7 @@ const dev = new Device({
|
|
|
279
439
|
regions: [{ name: 'drop', material: 'water', length: R, c0: { 'Na+': 500, 'Cl-': 500 + 2e-6, 'Fe2+': 1e-6, 'OH-': 1e-4, 'H+': 1e-4, O2: cO2 } }],
|
|
280
440
|
contacts: { left: { phi: 'neutral' }, right: { phi: 'neutral' } }, // closed: the centre, and the rim
|
|
281
441
|
bulkReactions: [
|
|
282
|
-
{ equation: 'H+ + OH- = H2O', fixed: { H2O
|
|
442
|
+
{ equation: 'H+ + OH- = H2O', fixed: { H2O }, kf: { water: 1.4e8 } }, // H2O: water's μ, −237.129 kJ/mol
|
|
283
443
|
// O₂ from the air through the drop: a rate k_f (1 − c/c_sat), k_f = D c_sat/h² (not a port's
|
|
284
444
|
// exchange link, whose rate is linear in μ rather than c)
|
|
285
445
|
{ equation: 'Air = O2', fixed: { Air: 16.4e3 + RT * Math.log(cO2 / 1000) }, kf: { water: { x: r, values: h.map((y) => (D * cO2) / (y * y)) } } },
|
|
@@ -291,7 +451,7 @@ const dev = new Device({
|
|
|
291
451
|
capacitance: { C: 0.2, zeroCharge: -0.4 }, // the double layer: the potential moves continuously
|
|
292
452
|
reactions: [
|
|
293
453
|
{ equation: 'Fe2+ + 2 e- = Fe(s)', fixed: { 'Fe(s)': 0 }, k0: 5e-6, alpha: 0.5 },
|
|
294
|
-
{ equation: 'O2 + 2 H2O + 4 e- = 4 OH-', fixed: { H2O
|
|
454
|
+
{ equation: 'O2 + 2 H2O + 4 e- = 4 OH-', fixed: { H2O }, k0: 3e-9, alpha: 0.125 },
|
|
295
455
|
],
|
|
296
456
|
}],
|
|
297
457
|
grid: { hmin: 5e-6, hmax: 60e-6, ratio: 1.2 },
|
|
@@ -310,7 +470,8 @@ for (const rx of evans.reactions) console.log(`${rx.equation}: ${Array.from(rx.c
|
|
|
310
470
|
|
|
311
471
|
On an electrode's surface, species can take its sites (`surface: { name: { mu0, capacity } }`,
|
|
312
472
|
Langmuir coverages), and a reaction with `bare: true` runs only on bare metal: a passive film
|
|
313
|
-
(docs/device.md, "An electrode's surface").
|
|
473
|
+
(docs/device.md, "An electrode's surface"). For a battery's insertion electrode, a material can
|
|
474
|
+
be described by its OCV curve (docs/statistics.md).
|
|
314
475
|
|
|
315
476
|
## Template: a thin-film transistor (a capacitance along a channel)
|
|
316
477
|
|
|
@@ -318,7 +479,7 @@ A **port with a `capacitance`** holds charge σ = C (V − zeroCharge − φ) pe
|
|
|
318
479
|
along its window, balanced by the region (Gauss's law, or neutrality at ε = 0): a gate along a
|
|
319
480
|
channel, a porous electrode's double layer. In 1D a field-effect transistor is its channel, x from
|
|
320
481
|
source to drain, with the gate spread along it: the gradual-channel approximation, exact for a thin
|
|
321
|
-
film until it pinches off.
|
|
482
|
+
film until it pinches off. (Its gate stack, across the channel, is the MOS capacitor above.)
|
|
322
483
|
|
|
323
484
|
```js
|
|
324
485
|
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
@@ -346,17 +507,204 @@ for (const Vd of [0.1, 0.5, 1, 1.5, 2, 3]) {
|
|
|
346
507
|
}
|
|
347
508
|
```
|
|
348
509
|
|
|
349
|
-
To sweep the gate on one device instead, patch the port
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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 }`.
|
|
513
|
+
|
|
514
|
+
## Template: a cell membrane
|
|
515
|
+
|
|
516
|
+
A lipid bilayer is a face: a capacitor (about 1 µF/cm², 0.01 F/m²) that ions cross through
|
|
517
|
+
channels, each with a permeability (`{ type: 'permeability', P }`, m/s: electrodiffusion in a
|
|
518
|
+
constant field, so Goldman–Hodgkin–Katz comes out of it), and that pumps cross as reactions with
|
|
519
|
+
saturating kinetics (`vmax`, `K`). The solutions on either side can be strictly neutral.
|
|
520
|
+
Physiology's membrane potential is φ_in − φ_out: read it from `sol.phi`, with the inside on the
|
|
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 φ.
|
|
523
|
+
|
|
524
|
+
```js
|
|
525
|
+
import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
526
|
+
import { build, layer, bath, aqueous, check } from 'driftlet/kit';
|
|
527
|
+
|
|
528
|
+
const T = 310.15, VT = (GAS_CONSTANT * T) / FARADAY;
|
|
529
|
+
const out = { 'Na+': 145, 'K+': 5, 'Cl-': 110, 'A-': 40 }; // mM = mol/m³; A⁻: anions that don't cross
|
|
530
|
+
const inside = { 'Na+': 12, 'K+': 140, 'Cl-': 10, 'A-': 142 };
|
|
531
|
+
const water = aqueous(['Na+', 'K+', 'Cl-'], { epsr: 0 }); // strictly neutral solutions
|
|
532
|
+
water.species.push({ name: 'A-', z: -1, cRef: 1000 });
|
|
533
|
+
water.materials.water.species['A-'] = { D: 0.5e-9, mu0: 0 };
|
|
534
|
+
const P = { 'K+': 1e-8, 'Na+': 0.04e-8, 'Cl-': 0.45e-8 }; // m/s, Hodgkin and Katz's ratios
|
|
535
|
+
const membrane = {
|
|
536
|
+
phi: { type: 'capacitive', C: 0.01 },
|
|
537
|
+
species: { 'K+': { type: 'permeability', P: P['K+'] }, 'Na+': { type: 'permeability', P: P['Na+'] }, 'Cl-': { type: 'permeability', P: P['Cl-'] }, 'A-': 'blocked' },
|
|
538
|
+
// The Na⁺/K⁺ pump (outside on the left): 3 Na⁺ out, 2 K⁺ in, per ATP (50 kJ/mol); vmax ≈ 3000
|
|
539
|
+
// pumps per µm² at 100 cycles/s.
|
|
540
|
+
reactions: [{ equation: '3 Na+(right) + 2 K+(left) + ATP = 3 Na+(left) + 2 K+(right) + ADP', fixed: { ATP: 50e3, ADP: 0 }, vmax: 5e-7, K: { 'Na+': 10, 'K+': 1.5 } }],
|
|
541
|
+
};
|
|
542
|
+
const cell = build({
|
|
543
|
+
T,
|
|
544
|
+
library: [water],
|
|
545
|
+
stack: [
|
|
546
|
+
bath(out, 'Cl-'), // the extracellular fluid, held
|
|
547
|
+
layer('water', 1e-6, { name: 'outside', c0: out }),
|
|
548
|
+
membrane,
|
|
549
|
+
layer('water', 5e-6, { name: 'cytoplasm', c0: inside }), // a closed cell: its contents set by leaks and the pump
|
|
550
|
+
{ species: { 'Na+': 'blocked', 'K+': 'blocked', 'Cl-': 'blocked', 'A-': 'blocked' }, phi: 'neutral' },
|
|
551
|
+
],
|
|
552
|
+
grid: { hmin: 50e-9, hmax: 200e-9 },
|
|
553
|
+
});
|
|
554
|
+
const dev = new Device(cell), sol = dev.solve();
|
|
555
|
+
const g = sol.x.length - 1, ci = (s) => sol.c[s][g], Vm = sol.phi[g] - sol.phi[0];
|
|
556
|
+
// Mullins–Noda: in steady state the pump's 3:2 sets the ratio of the Na⁺ and K⁺ leaks.
|
|
557
|
+
const mn = VT * Math.log((1.5 * P['K+'] * out['K+'] + P['Na+'] * out['Na+']) / (1.5 * P['K+'] * ci('K+') + P['Na+'] * ci('Na+')));
|
|
558
|
+
console.log(`V_m = ${(Vm * 1e3).toFixed(2)} mV (Mullins–Noda ${(mn * 1e3).toFixed(2)}), inside Na⁺ ${ci('Na+').toFixed(1)} K⁺ ${ci('K+').toFixed(1)} mM`);
|
|
559
|
+
console.log(check(dev, sol).text); // per compartment: Na⁺ leaks into the cytoplasm as fast as it's pumped out
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Between two baths instead (the inside one `bath(inside, 'Cl-', { I: 0 })`: held composition, no
|
|
563
|
+
current), V_m is GHK. Without the pump, a closed cell runs down to Donnan equilibrium.
|
|
564
|
+
|
|
565
|
+
## Template: a voltage clamp
|
|
566
|
+
|
|
567
|
+
Voltage-gated channels: a face's `gates` (each a fraction open with Hodgkin–Huxley kinetics,
|
|
568
|
+
dx/dt = α(V)(1 − x) − β(V)x, in V = φ_right − φ_left) scale its links (`gates: { m: 3, h: 1 }`).
|
|
569
|
+
`hodgkinHuxley({ T, linear: true })` (kit) writes the squid axon's as Hodgkin and Huxley had
|
|
570
|
+
them: gated conductances, each current g(V − E) with E its ion's Nernst level. Between two baths
|
|
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.
|
|
573
|
+
|
|
574
|
+
```js
|
|
575
|
+
import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
576
|
+
import { build, layer, bath, aqueous, hodgkinHuxley } from 'driftlet/kit';
|
|
577
|
+
|
|
578
|
+
const T = 279.45, VT = (GAS_CONSTANT * T) / FARADAY; // HH's 6.3 °C
|
|
579
|
+
const out = { 'Na+': 440, 'K+': 20, 'Cl-': 450, 'A-': 10 }, inside = { 'Na+': 50, 'K+': 400, 'Cl-': 52, 'A-': 398 }; // squid, mM
|
|
580
|
+
const water = aqueous(['Na+', 'K+', 'Cl-'], { epsr: 0 });
|
|
581
|
+
water.species.push({ name: 'A-', z: -1, cRef: 1000 });
|
|
582
|
+
water.materials.water.species['A-'] = { D: 0.5e-9, mu0: 0 };
|
|
583
|
+
const hh = hodgkinHuxley({ T, linear: true });
|
|
584
|
+
const step = -0.01; // from −65 mV to −10 mV at 1 ms
|
|
585
|
+
const dev = new Device(
|
|
586
|
+
build({
|
|
587
|
+
T,
|
|
588
|
+
library: [water],
|
|
589
|
+
stack: [
|
|
590
|
+
bath(out), // the outside, on the left
|
|
591
|
+
layer('water', 1e-6, { c0: out }),
|
|
592
|
+
{ phi: { type: 'capacitive', C: 0.01 }, gates: hh.gates, species: { ...hh.species, 'A-': 'blocked' } },
|
|
593
|
+
layer('water', 1e-6, { c0: inside }),
|
|
594
|
+
bath(inside, { t: [0, 1e-3, 1e-3, 1], values: [-0.065, -0.065, step, step] }),
|
|
595
|
+
],
|
|
596
|
+
grid: { hmin: 20e-9, hmax: 0.2e-6 },
|
|
597
|
+
}),
|
|
598
|
+
);
|
|
599
|
+
dev.solve(); // the rest at −65 mV, the gates at their values there
|
|
600
|
+
const run = dev.advance(11e-3, { dtMax: 0.02e-3, probes: [{ interface: 0, species: 'Na+' }, { interface: 0, species: 'K+' }] });
|
|
601
|
+
const [na, k] = run.trace.probes.map((N) => N.map((x) => -FARADAY * x)); // outward currents, A/m²
|
|
602
|
+
const peak = na.indexOf(Math.min(...na)), EK = VT * Math.log(out['K+'] / inside['K+']);
|
|
603
|
+
console.log(`I_Na peaks ${((run.trace.t[peak] - 1e-3) * 1e3).toFixed(2)} ms into the step, ${(na[peak] * 0.1).toFixed(2)} mA/cm²`); // 0.73 ms, −1.48
|
|
604
|
+
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
|
+
```
|
|
606
|
+
|
|
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
|
+
## Template: an action potential
|
|
612
|
+
|
|
613
|
+
A pump-less cell has no steady rest (`solve()` finds Donnan), so start the transient from the
|
|
614
|
+
concentrations given: the membrane settles within milliseconds.
|
|
615
|
+
|
|
616
|
+
```js
|
|
617
|
+
import { Device } from 'driftlet';
|
|
618
|
+
import { build, layer, bath, aqueous, injector, pulse, hodgkinHuxley } from 'driftlet/kit';
|
|
619
|
+
|
|
620
|
+
const T = 279.45; // HH's 6.3 °C
|
|
621
|
+
const out = { 'Na+': 440, 'K+': 20, 'Cl-': 450, 'A-': 10 }, inside = { 'Na+': 50, 'K+': 400, 'Cl-': 52, 'A-': 398 }; // squid, mM
|
|
622
|
+
const water = aqueous(['Na+', 'K+', 'Cl-'], { epsr: 0 });
|
|
623
|
+
water.species.push({ name: 'A-', z: -1, cRef: 1000 });
|
|
624
|
+
water.materials.water.species['A-'] = { D: 0.5e-9, mu0: 0 };
|
|
625
|
+
const hh = hodgkinHuxley({ inside, outside: out, T }); // GHK channels; outside on the left
|
|
626
|
+
const axon = new Device(
|
|
627
|
+
build({
|
|
628
|
+
T,
|
|
629
|
+
library: [water],
|
|
630
|
+
stack: [
|
|
631
|
+
bath(out, 'Cl-'),
|
|
632
|
+
layer('water', 5e-6, { name: 'outside', c0: out }),
|
|
633
|
+
{ phi: { type: 'capacitive', C: 0.01 }, gates: hh.gates, species: { ...hh.species, 'A-': 'blocked' } },
|
|
634
|
+
layer('water', 20e-6, { name: 'cytoplasm', c0: inside }),
|
|
635
|
+
{ species: { 'Na+': 'blocked', 'K+': 'blocked', 'Cl-': 'blocked', 'A-': 'blocked' }, phi: 'neutral' },
|
|
636
|
+
],
|
|
637
|
+
// the stimulus: 10 µA/cm² of K⁺ for 1 ms at 20 ms, from a pipette in the cytoplasm
|
|
638
|
+
ports: [injector({ region: 'cytoplasm', from: 15e-6, to: 20e-6, species: 'K+', I: pulse({ start: 20e-3, width: 1e-3, amplitude: 0.1 }) })],
|
|
639
|
+
grid: { hmin: 50e-9, hmax: 1e-6 },
|
|
640
|
+
}),
|
|
641
|
+
);
|
|
642
|
+
let peak = -1;
|
|
643
|
+
for (let k = 1; k <= 300; k++) {
|
|
644
|
+
const sol = axon.advance(k * 1e-4); // 0.1 ms at a time
|
|
645
|
+
peak = Math.max(peak, sol.interfaces[0].V); // V_m, and sol.interfaces[0].gates = { m, h, n }
|
|
646
|
+
}
|
|
647
|
+
console.log(`peak ${(peak * 1e3).toFixed(1)} mV`); // +12.6: GHK's Na⁺ current is limited by the 50 mM inside (linear: true gives +37.9, HH's own)
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
## Template: an action potential travelling along an axon
|
|
353
651
|
|
|
354
|
-
|
|
652
|
+
Here x runs along the axon. The axoplasm is one region, and the membrane is a port spanning all
|
|
653
|
+
of it: a capacitance plus gated channels to an outside given as a `bath` (the port's V is then
|
|
654
|
+
the outside's φ). `hodgkinHuxley({ T, area })` writes Hodgkin and Huxley's linear channels for
|
|
655
|
+
it, with `area` the membrane per volume, 2/a for radius a. Cable theory isn't put in; it comes
|
|
656
|
+
out of the ions' drift along the axoplasm. Each solution's `ports[k].Vm[j]` and
|
|
657
|
+
`ports[k].gates[name][j]` give the membrane potential and gates at each node of the window. A
|
|
658
|
+
myelinated axon is regions (nodes and internodes), each with a port of its own (an internode's
|
|
659
|
+
is just a small capacitance: myelin).
|
|
660
|
+
|
|
661
|
+
```js
|
|
662
|
+
import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
663
|
+
import { hodgkinHuxley, injector, pulse } from 'driftlet/kit';
|
|
664
|
+
|
|
665
|
+
const T = 291.65, VT = (GAS_CONSTANT * T) / FARADAY, a = 238e-6; // 18.5 °C, a squid giant axon
|
|
666
|
+
const inside = { 'Na+': 50, 'K+': 400, 'Cl-': 52.5, 'A-': 397.5 }; // mM
|
|
667
|
+
// Outside levels that put the Nernst potentials at HH's +50, −77 and −54.4 mV (leak as Cl⁻).
|
|
668
|
+
const outside = { 'Na+': 50 * Math.exp(0.05 / VT), 'K+': 400 * Math.exp(-0.077 / VT), 'Cl-': 52.5 * Math.exp(0.054387 / VT) };
|
|
669
|
+
// One D for the small ions, giving HH's 35.4 Ω·cm.
|
|
670
|
+
const D = 1 / 0.354 / ((FARADAY * FARADAY) / (GAS_CONSTANT * T)) / (50 + 400 + 52.5);
|
|
671
|
+
const sp = (d) => ({ D: d, mu0: 0, cRef: 1000 });
|
|
672
|
+
const sealed = { species: { 'Na+': 'blocked', 'K+': 'blocked', 'Cl-': 'blocked', 'A-': 'blocked' }, phi: 'neutral' };
|
|
673
|
+
const hh = hodgkinHuxley({ T, area: 2 / a });
|
|
674
|
+
const axon = new Device({
|
|
675
|
+
T,
|
|
676
|
+
species: [{ name: 'Na+', z: 1 }, { name: 'K+', z: 1 }, { name: 'Cl-', z: -1 }, { name: 'A-', z: -1 }],
|
|
677
|
+
materials: { axoplasm: { epsr: 0, species: { 'Na+': sp(D), 'K+': sp(D), 'Cl-': sp(D), 'A-': sp(1e-12) } } },
|
|
678
|
+
regions: [{ name: 'axon', material: 'axoplasm', length: 0.06, c0: inside }],
|
|
679
|
+
contacts: { left: sealed, right: sealed },
|
|
680
|
+
ports: [
|
|
681
|
+
{ name: 'membrane', region: 'axon', V: 0, area: 2 / a, capacitance: { C: 0.01, zeroCharge: 0 }, bath: { c: outside }, gates: hh.gates, species: hh.species },
|
|
682
|
+
injector({ name: 'stim', region: 'axon', from: 0, to: 1e-3, species: 'K+', I: pulse({ start: 30e-3, width: 0.2e-3, amplitude: 30 }) }),
|
|
683
|
+
],
|
|
684
|
+
grid: { hmin: 2e-4, hmax: 2e-4 },
|
|
685
|
+
});
|
|
686
|
+
axon.advance(30e-3); // settle to rest (no pump: start from the concentrations, not solve())
|
|
687
|
+
const sol = axon.advance(34e-3, { dtMax: 5e-6, probes: [{ x: 0.02, quantity: 'phi' }, { x: 0.04, quantity: 'phi' }] });
|
|
688
|
+
const { t, probes } = sol.trace, at = (v) => t[v.findIndex((x) => x > 0)]; // (φ inside, the outside's held at 0)
|
|
689
|
+
console.log(`${(0.02 / (at(probes[1]) - at(probes[0]))).toFixed(1)} m/s`); // 18.8 here (finely resolved, 18.73; HH computed 18.8)
|
|
690
|
+
```
|
|
355
691
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
692
|
+
## Transients
|
|
693
|
+
|
|
694
|
+
`advance(t, opts)` steps from the device's present state (its `c0`, or a steady state if you
|
|
695
|
+
called `solve()` first) to time t, adaptively, and returns the solution there with `done` and a
|
|
696
|
+
`trace` of this call's accepted steps (`t`, `current`: the right contact's toward +x, `voltage`:
|
|
697
|
+
V_right − V_left). It stops short when it runs out of `budgetMs` (an animation frame: call again)
|
|
698
|
+
or, with a warning saying why, of `maxSteps` (100000) or of steps that converge. `probes` read
|
|
699
|
+
the device at points inside it, at accepted steps, so give `dtMax` to resolve a signal in time:
|
|
700
|
+
`{ x, species }` (c, or `quantity: 'V'`), `{ x, quantity: 'phi' }`, and at a face
|
|
701
|
+
`{ interface: f, species }` (its flux) or `{ interface: f, gate }`. To stop at an event (a
|
|
702
|
+
voltage cut-off, the end of a diode's storage time), advance in short calls and test after each;
|
|
703
|
+
a driven current held past what the device can pass fails to converge. A device that only stores
|
|
704
|
+
what comes in (a battery host filling at a current) has no steady state: don't `solve()` it,
|
|
705
|
+
`advance()` from its `c0` (docs/statistics.md, insertion hosts). `dev.step(dt)` is one
|
|
706
|
+
backward-Euler step, and `dev.impedance(frequencies, { terminal })` the small-signal Z(f) about
|
|
707
|
+
the steady state (Ω·m²).
|
|
360
708
|
|
|
361
709
|
```js
|
|
362
710
|
import { Device, units, FARADAY } from 'driftlet';
|
|
@@ -376,9 +724,6 @@ const def = build({
|
|
|
376
724
|
});
|
|
377
725
|
const dev = new Device(def);
|
|
378
726
|
const rest = dev.solve(); // the steady state at t = 0 (solve() doesn't advance time)
|
|
379
|
-
// In a page, call advance(tNext, { budgetMs: 12, probes }) once per animation frame, carrying on
|
|
380
|
-
// next frame while sol.done is false (or let recorder() join the traces and keep frames to scrub
|
|
381
|
-
// back through). Probes read at accepted steps; dtMax keeps them close.
|
|
382
727
|
const run = dev.advance(10e-6, { dtMax: 20e-9, probes: [{ x: 1.525e-3, species: 'h+' }] });
|
|
383
728
|
const seen = run.trace.probes[0], k = seen.indexOf(Math.max(...seen));
|
|
384
729
|
const tArrive = run.trace.t[k] - width / 2; // from the pulse's middle
|
|
@@ -386,12 +731,11 @@ console.log(`peak at ${(run.trace.t[k] * 1e6).toFixed(2)} µs, Δp ${units.toPer
|
|
|
386
731
|
```
|
|
387
732
|
|
|
388
733
|
To start from a state instead of simulating how it got there (a packet already injected, a
|
|
389
|
-
salt gradient laid down), give a region's `c0` a profile against the device's x
|
|
390
|
-
`c0: { 'h+': { x: [...], values: [...] } }`,
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
state.
|
|
734
|
+
salt gradient laid down), give a region's `c0` a profile against the device's x,
|
|
735
|
+
`c0: { 'h+': { x: [...], values: [...] } }`, and `advance()` from the new `Device` without
|
|
736
|
+
`solve()` (which would go straight to the steady state). A given `c0` is the starting state, even
|
|
737
|
+
for a species a contact feeds; species without one start at their contact's level, and φ starts
|
|
738
|
+
neutral at each node. When two regions start with different solutions, give each its own `c0`.
|
|
395
739
|
|
|
396
740
|
Transients cost more where a carrier's density spans many decades (minority holes in silicon
|
|
397
741
|
ahead of the pulse) and at strong injection (Δp near n₀); budget for that, or coarsen the grid.
|
|
@@ -401,83 +745,29 @@ opts)` takes the same options, `rec.trace` is the whole run's trace from the sta
|
|
|
401
745
|
`rec.frames` are whole solutions (at the start, then at `times`, every `every` s, or the end of
|
|
402
746
|
each call), with `rec.frame(t)` the one at or before t, for a time slider.
|
|
403
747
|
|
|
404
|
-
**Prefer a curated device to inventing numbers** where one fits: `perovskiteCell({ V, light,
|
|
405
|
-
bulkSRH })` is IonMonger's default perovskite solar cell (mobile iodide vacancies, SRH in the bulk
|
|
406
|
-
and at both faces), and `PEROVSKITE_SCANS` holds IonMonger's J–V scans of it at seven rates;
|
|
407
|
-
`hysteresis(rec.trace, turn)` reads the same figures from yours, so a demo can show its agreement.
|
|
408
|
-
The definition is plain data: print it, edit what your demo varies.
|
|
409
|
-
|
|
410
|
-
## Template: a live page with a slider
|
|
411
|
-
|
|
412
|
-
`live()` merges slider moves made during a solve, warm-starts each solve and keeps the last
|
|
413
|
-
good solution. `bandDiagram()` returns an SVG string. Pass `worker: true` to `live()` to solve in
|
|
414
|
-
a Web Worker (it works with the library loaded from a CDN: a same-origin stub imports it). Calling
|
|
415
|
-
`set()` from `onsolution` is fine, for a quantity the page solves for itself (it queues the next
|
|
416
|
-
solve), as long as it settles rather than ping-pongs.
|
|
417
|
-
|
|
418
|
-
```html
|
|
419
|
-
<!doctype html>
|
|
420
|
-
<meta charset="utf-8">
|
|
421
|
-
<title>pn junction</title>
|
|
422
|
-
<label>bias <input id="V" type="range" min="-0.5" max="0.7" step="0.01" value="0"></label>
|
|
423
|
-
<span id="readout"></span>
|
|
424
|
-
<div id="figure"></div>
|
|
425
|
-
<script type="module">
|
|
426
|
-
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.11.4/src/index.js';
|
|
427
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.11.4/src/kit.js';
|
|
428
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.11.4/src/plot.js';
|
|
429
|
-
|
|
430
|
-
const def = build({
|
|
431
|
-
T: 300,
|
|
432
|
-
library: [semiconductor('Si')],
|
|
433
|
-
stack: [ohmic(0), layer('Si', 1e-6, { donors: units.perCm3(1e17) }), layer('Si', 1e-6, { acceptors: units.perCm3(1e16) }), ohmic(0)],
|
|
434
|
-
bulkReactions: [{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }],
|
|
435
|
-
grid: { hmin: 1e-9, hmax: 20e-9 },
|
|
436
|
-
});
|
|
437
|
-
const slider = document.getElementById('V');
|
|
438
|
-
const dev = live(def, {
|
|
439
|
-
onsolution(sol, info) {
|
|
440
|
-
document.getElementById('figure').innerHTML = bandDiagram(sol, { energy: true, title: `V = ${sol.terminals.right.V.toFixed(2)} V` });
|
|
441
|
-
document.getElementById('readout').textContent = info.failed ? 'no solution here' : `I = ${sol.current.toExponential(2)} A/m²`;
|
|
442
|
-
},
|
|
443
|
-
});
|
|
444
|
-
slider.addEventListener('input', () => dev.set({ contacts: { right: { V: +slider.value } } }));
|
|
445
|
-
</script>
|
|
446
|
-
```
|
|
447
|
-
|
|
448
748
|
## Reading level diagrams
|
|
449
749
|
|
|
450
750
|
What `bandDiagram()` and `traces()` draw, and how to explain it to a reader (docs/visualization.md
|
|
451
751
|
has the long form):
|
|
452
752
|
|
|
453
|
-
- **Plotting in your own style.** `bandDiagram()` is a quick look. For a page of your own, draw
|
|
454
|
-
with your own charting code from `traces(sol, opts)` (in `driftlet/kit`, the same options):
|
|
455
|
-
`{ x, series: [{ id, label, kind, role, slot, y }], regions, faces, range }`, where `kind` says how
|
|
456
|
-
a line reads (`'level'` thick, `'standard'` thin, `'redox'`, `'phi'`), `role` and `slot` keep a
|
|
457
|
-
species' colour stable (`themeColor(role, slot)` in `driftlet/plot` gives ours), labels mark
|
|
458
|
-
subscripts TeX-style (`labelParts(label)` splits them), `NaN` breaks a line, and doubled
|
|
459
|
-
interface nodes share an x (draw them as vertical steps). Or read the solution directly:
|
|
460
|
-
`sol.x`, `sol.V[name]`, `sol.Vstd[name]`, `sol.c[name]`, `sol.phi`; electron energies are
|
|
461
|
-
E = −V (eV) for both carriers' levels and band edges.
|
|
462
|
-
|
|
463
753
|
- **Which picture.** For a device of electrons and holes only, draw the familiar band diagram,
|
|
464
|
-
energy up: `bandDiagram(sol, { energy: true })` (E_c, E_v, E_Fn, E_Fp in eV
|
|
465
|
-
voltages below are the same lines in volts, turned over; use them where
|
|
466
|
-
or for equilibrium between species, and don't present them in place of
|
|
467
|
-
|
|
754
|
+
energy up: `bandDiagram(sol, { energy: true })` (E_c, E_v, E_Fn, E_Fp in eV; electron energies
|
|
755
|
+
are E = −V). The species voltages below are the same lines in volts, turned over; use them where
|
|
756
|
+
ions share the diagram, or for equilibrium between species, and don't present them in place of
|
|
757
|
+
the band diagram.
|
|
468
758
|
- **Fermi level** V_e⁻ = −μ̄_e⁻/F: the circuit voltage, what a voltmeter reads. A cell's voltage is
|
|
469
759
|
the difference between its terminals' V_e⁻.
|
|
470
|
-
- **Redox level** of Ox + z e⁻ ⇌ Red, `level(sol, half)
|
|
471
|
-
|
|
472
|
-
composition. Drawn thick and dashed. At an
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
760
|
+
- **Redox level** of Ox + z e⁻ ⇌ Red, `level(sol, half)` or `levels: [{ half }]` in the plotting
|
|
761
|
+
options: V_e⁻(Ox/Red) = (μ̄_Ox − μ̄_Red)/(zF), where an electrode exchanging electrons by that
|
|
762
|
+
couple would sit in equilibrium with the local composition. Drawn thick and dashed. At an
|
|
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
|
|
476
767
|
concentration. The redox level sits RT/(zF) ln(a_Ox/a_Red) from it (Nernst, locally).
|
|
477
768
|
- **Electrode potentials** are gaps to the SHE's standard level V°_e⁻(SHE): E = V_e⁻(electrode) −
|
|
478
769
|
V°_e⁻(SHE), E° = V°_e⁻(Ox/Red) − V°_e⁻(SHE), η = E − E_eq. With the data library's μ°,
|
|
479
|
-
V°_e⁻(SHE) in water is φ, so `phi: true` draws it.
|
|
480
|
-
place (say, the stirred bulk).
|
|
770
|
+
V°_e⁻(SHE) in water is φ, so `phi: true` draws it.
|
|
481
771
|
- **Species voltage** V_i = μ̄_i/(z_iF) (thick, solid) for every charged species: the Fermi level
|
|
482
772
|
for e⁻, a quasi-Fermi level for e⁻ and h⁺ in a semiconductor, what a reversible electrode reads
|
|
483
773
|
for an ion. It's a display of μ̄_i (the physics, which also covers neutral species): each
|
|
@@ -486,18 +776,24 @@ has the long form):
|
|
|
486
776
|
where V_i is flat. A step at a face means resistance there.
|
|
487
777
|
- **Standard level** V°_i = φ + μ°_i/(z_iF) (thin, solid): a band edge for e⁻ and h⁺; V_i − V°_i =
|
|
488
778
|
(RT/z_iF) ln(c/c_ref) for ideal species. Steps in V°_i at faces (Donnan, junctions, dipoles) are
|
|
489
|
-
bookkeeping.
|
|
779
|
+
bookkeeping.
|
|
490
780
|
- **What's left out.** φ isn't drawn: it's a per-material convention (meaningless in a metal), and
|
|
491
781
|
−eφ isn't any electron's energy; the V°_i ladder shows its variation within a material and means
|
|
492
782
|
something across faces too. The exceptions: across an insulator with no mobile species, a
|
|
493
|
-
plain sloped φ (`phi:
|
|
494
|
-
really is an electron's energy.
|
|
495
|
-
|
|
496
|
-
physical
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
783
|
+
plain sloped φ (`phi: ['SiO2']`) is the line to draw (its slope is the field), and in vacuum −eφ
|
|
784
|
+
really is an electron's energy.
|
|
785
|
+
- **No fixed reference.** Shifting every line together changes nothing, and every gap is
|
|
786
|
+
physical. "vs SHE" and "from vacuum" each need a chosen point, because both levels vary in x.
|
|
787
|
+
- **Gaps between species.** V_i − V_j is free of φ but carries a constant from the elements'
|
|
788
|
+
conventional zeros, so it's read through a reaction. `shifts` (marked ⌇) offsets one species'
|
|
789
|
+
lines to fit a shared axis without changing any gap within that species.
|
|
790
|
+
- **Plotting in your own style.** `traces(sol, opts)` gives `{ x, series: [{ id, label, kind,
|
|
791
|
+
role, slot, y }], regions, faces, range }`: `kind` says how a line reads (`'level'` thick,
|
|
792
|
+
`'standard'` thin, `'redox'` and `'redox-standard'` dashed, `'phi'`), `role` and `slot` keep a
|
|
793
|
+
species' colour stable (`themeColor(role, slot)` in `driftlet/plot` gives ours), labels mark
|
|
794
|
+
subscripts TeX-style (`labelParts(label)` splits them), `NaN` breaks a line, and doubled
|
|
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`.
|
|
501
797
|
- Call V_i the species voltage and say what it is; it's new to most readers. Credit Jamnik and
|
|
502
798
|
Maier (Phys. Chem. Chem. Phys. 3, 1668 (2001)) where V_i rails explain impedance or linear
|
|
503
799
|
response.
|
|
@@ -505,22 +801,18 @@ has the long form):
|
|
|
505
801
|
## Good habits
|
|
506
802
|
|
|
507
803
|
- **Draw the closed-form answer alongside the simulation**, wherever a textbook limit exists
|
|
508
|
-
(Shockley's law, a Nernst potential, a Gaussian pulse, a Warburg arc), as the
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
finite pulse), say why.
|
|
804
|
+
(Shockley's law, a Nernst potential, a Gaussian pulse, a Warburg arc), as the templates do. It
|
|
805
|
+
checks the setup: a solver's output looks authoritative even when an input is wrong, and a
|
|
806
|
+
mismatch is the cheapest way to catch it. Where the two should differ (high injection, a
|
|
807
|
+
finite pulse, the kinetics or IR drop of an electrode), say why.
|
|
512
808
|
- **Stay honestly inside 1D.** driftlet models devices along one coordinate: planar, or with a
|
|
513
|
-
cross-section A(x) (`geometry`: spherical or cylindrical shells, or a profile)
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
walls, a well-conducting porous matrix) is a port with `reactions` and an `area` per volume:
|
|
521
|
-
an electrode spread through a window, not a face at each end.
|
|
522
|
-
- **Check `sol.converged` and `sol.warnings`** on every solve, and pin the CDN version you
|
|
523
|
-
tested against.
|
|
809
|
+
cross-section A(x) (`geometry`: spherical or cylindrical shells, or a profile). Where the real
|
|
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.
|
|
524
816
|
|
|
525
817
|
## Common mistakes
|
|
526
818
|
|
|
@@ -533,15 +825,18 @@ has the long form):
|
|
|
533
825
|
ε > 0 needs an explicit φ law`. Give `{ phi: { type: 'capacitive', C }, zeroCharge }`, or
|
|
534
826
|
`phi: 'neutral'`, or make the electrolyte strictly neutral (`aqueous(names, { epsr: 0 })`)
|
|
535
827
|
when double layers don't matter.
|
|
828
|
+
- **A gate as a contact on an oxide.** The carrier is blocked there (`… 'e-' is blocked at this
|
|
829
|
+
contact`). Make the gate a metal layer with a capacitive face, as in the MOS template.
|
|
536
830
|
- **`ohmic()` on a metal.** Its default holds both e⁻ and h⁺, for semiconductors. On a metal
|
|
537
831
|
layer, write `ohmic(V, ['e-'])`.
|
|
538
832
|
- **Ions on both sides of a reacting face.** Label them: `'Li+(left) = Li+(right)'`, and give
|
|
539
833
|
their link (`species: { 'Li+': 'blocked' }` if they cross only by the reaction).
|
|
540
|
-
- **Unit slips.** D in cm²/s, μ° in eV, capacitance in µF/cm², T in °C.
|
|
541
|
-
numbers that look like these.
|
|
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.
|
|
542
836
|
- **A closed system with no amount.** A species that doesn't reach a contact needs `c0`.
|
|
543
|
-
- **
|
|
544
|
-
included): `… carries a net charge of … mol/m³`.
|
|
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.)
|
|
545
840
|
- **A sharp junction between very different solutions** (3 M KCl against 1 µM traces) can't
|
|
546
841
|
start: grade it over a few thin layers mixing one solution into the other, as at a frit.
|
|
547
842
|
- **A steady solve of a closed cell where every ion can cross.** Its steady state is complete
|
|
@@ -550,17 +845,35 @@ has the long form):
|
|
|
550
845
|
- **The library's data are for 25 °C** (its semiconductors for 300 K: give `T: 300`). At body
|
|
551
846
|
temperature, `T: 310.15` with the same μ° and D is a fair start; D rises about 2% per kelvin
|
|
552
847
|
in water.
|
|
553
|
-
- **Sign of the current.** `sol.current` is
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
848
|
+
- **Sign of the current.** `sol.current` is toward +x: a forward-biased diode with its p side on
|
|
849
|
+
the right carries negative current. A terminal's current, and a current drive `{ I }`, are into
|
|
850
|
+
the device: that same diode is driven forward by a positive `I` at its right contact. A drive of
|
|
851
|
+
the wrong sign fails with `no steady state passes the driven current`, and says so.
|
|
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.
|
|
559
855
|
- **A coefficient without its space.** `'2e- + …'` reads `2e-` as a species name; the error says
|
|
560
856
|
to write `2 e-`.
|
|
561
857
|
- **Changing c0 with `set()`.** A carried state keeps what it holds; `c0` is only the starting
|
|
562
858
|
composition. Make a new `Device` to start over from it.
|
|
563
|
-
- **`solve()` before advancing from a `c0` profile.** It replaces the profile with the steady
|
|
564
|
-
state. Advance from the new `Device` directly. A profile's x is the device's, in m.
|
|
565
859
|
- **An unresolved double layer.** If `sol.warnings` says so, refine the grid (`hmin` below the
|
|
566
860
|
Debye length) or use a neutral model (`epsr: 0`, `phi: 'neutral'`).
|
|
861
|
+
|
|
862
|
+
## Exports
|
|
863
|
+
|
|
864
|
+
- `driftlet`: `Device`, `units`, `DeviceError`, `SolverError`, and the constants `FARADAY`,
|
|
865
|
+
`GAS_CONSTANT`, `EPS0`, `BOLTZMANN`, `ELEMENTARY_CHARGE`, `AVOGADRO` (there's no thermal-voltage
|
|
866
|
+
constant: V_T = GAS_CONSTANT·T/FARADAY).
|
|
867
|
+
- `driftlet/kit`:
|
|
868
|
+
- building: `build`, `combine`, `layer`, `ohmic`, `bath`, `half`, `level`, `SHE`,
|
|
869
|
+
`vacuumLevel`, `vacuumDipole`, `vacuumZeroCharge`, and the data library (`aqueous`,
|
|
870
|
+
`semiconductor`, `metal`, `IONS`, `SEMICONDUCTORS`, `METALS`, `H2O`, `WATER_EPSR`);
|
|
871
|
+
- sources: `pulse`, `square`, `triangle`, `ramp`, `injector`, `recombination`,
|
|
872
|
+
`photogeneration`;
|
|
873
|
+
- channels: `hodgkinHuxley`, `ghkCurrent`;
|
|
874
|
+
- checking: `describe`, `check`, `polarization` (an electrode's reactions at hypothetical
|
|
875
|
+
metal levels, with the solution held: an Evans diagram), `henderson` and `planck`
|
|
876
|
+
(liquid-junction potentials);
|
|
877
|
+
- pages: `live`, `traces`, `recorder`;
|
|
878
|
+
- curated devices: `perovskiteCell`, `PEROVSKITE_SCANS`, `hysteresis`.
|
|
879
|
+
- `driftlet/plot` (a quick look): `bandDiagram`, `levelChart`, `labelParts`, `themeColor`.
|