driftlet 0.1.1 → 0.2.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/docs/kit.md ADDED
@@ -0,0 +1,299 @@
1
+ # The kit
2
+
3
+ `driftlet/kit` is the porcelain: helpers that write the plain device definition for you, so a
4
+ device can be set up the way it's drawn and from familiar quantities. It never makes a physical
5
+ choice. Alignments, offsets, rate constants and every other physical input stay yours to give,
6
+ and what the kit produces is an ordinary definition you can print, edit and pass to
7
+ `new Device` (see the [device reference](device.md)).
8
+
9
+ Its data library, with sources, is described in [data](data.md).
10
+
11
+ ## A device as a stack
12
+
13
+ `build()` takes the device left to right: a contact, then layers and the faces between them,
14
+ then a contact.
15
+
16
+ ```js
17
+ import { Device, units } from 'driftlet';
18
+ import { build, layer, ohmic } from 'driftlet/kit';
19
+
20
+ const Si = {
21
+ species: [
22
+ { name: 'e-', z: -1 },
23
+ { name: 'h+', z: 1 },
24
+ ],
25
+ materials: {
26
+ Si: {
27
+ epsr: 11.7,
28
+ species: {
29
+ 'e-': { D: 36e-4, mu0: 0, cRef: units.perCm3(2.8e19) },
30
+ 'h+': { D: 12e-4, mu0: units.eV(1.12), cRef: units.perCm3(1.04e19) },
31
+ },
32
+ },
33
+ },
34
+ };
35
+
36
+ const def = build({
37
+ library: [Si],
38
+ stack: [
39
+ ohmic(0),
40
+ layer('Si', units.um(1), { donors: units.perCm3(1e17) }),
41
+ layer('Si', units.um(1), { acceptors: units.perCm3(1e16) }),
42
+ ohmic(0.5),
43
+ ],
44
+ bulkReactions: [{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }],
45
+ grid: { hmin: 1e-9, hmax: 20e-9 },
46
+ });
47
+ console.log(JSON.stringify(def.regions)); // plain data: fixedCharge, in C/m³
48
+ const sol = new Device(def).solve();
49
+ console.log(`I = ${sol.current.toFixed(1)} A/m²`);
50
+ ```
51
+
52
+ - **Layers** are regions: anything with a `material`. `layer(material, length, more)` writes
53
+ one. Doping can be given as `donors` and `acceptors` (mol/m³) in place of `fixedCharge`.
54
+ - **Faces** are anything else between layers: an interface's fields, exactly as in
55
+ `interfaces[f]`. Two adjacent layers with no face between them get the default face, which
56
+ between different materials is an error asking for the alignment, as always.
57
+ - **Contacts** are the first and last items. `ohmic(drive, carriers)` holds each carrier in
58
+ equilibrium with a metal at the terminal voltage (default `['e-', 'h+']`, an
59
+ infinite-recombination contact; `['e-']` alone is selective, and is the current collector on
60
+ a metal region). `bath(c, reference, drive, { offset })` is a bath (`offset` places the reference species
61
+ relative to the terminal voltage, e.g. to read a solution's potentials against SHE). A drive is a voltage, a waveform, or
62
+ `{ V }`, `{ I }`, `{ V, R }`.
63
+ - **`library`** takes pieces with `species` and `materials`, merged with any given directly
64
+ (`combine()` does the merging on its own). A species or material given twice must be given
65
+ identically.
66
+ - Nested lists in the stack are flattened, so a helper can return several items.
67
+ - Reactions pass through as written (see below).
68
+
69
+ ## Reactions and half-reactions
70
+
71
+ Reactions are written as equations, `'Ag+ + e- = Ag(s)'`, in the definition itself (see the
72
+ [device reference](device.md#interfaces)), so `build()`, `new Device` and `set()` all take them.
73
+ A coefficient is an integer and a space (`2 e-`); names may start with digits (`3He`).
74
+
75
+ `half(equation, fixed)` writes a half-reaction, with electrons `e-` on one side, as plain
76
+ `{ equation, fixed }`: spread it into a face reaction with its kinetics. `level(sol, half)` is the
77
+ electronic level it implies at each node, as an electron voltage: where an electrode exchanging
78
+ electrons by that couple would sit in equilibrium with the local composition: the couple's redox
79
+ level (`NaN` where a participant is absent; see [reading level diagrams](visualization.md)). With `{ standard: true }` every species
80
+ is at its reference concentration instead. `SHE` is 2 H⁺ + 2 e⁻ ⇌ H₂ with
81
+ $`\mu_{\mathrm{H_2}} = 0`$, so its standard level is $`\phi + \mu^\circ_{\mathrm{H}^+}/F`$.
82
+
83
+ ```js
84
+ import { Device } from 'driftlet';
85
+ import { build, layer, ohmic, half, level } from 'driftlet/kit';
86
+
87
+ const silver = half('Ag+ + e- = Ag(s)', { 'Ag(s)': 0 });
88
+ const stern = { phi: { type: 'capacitive', C: 0.2 }, zeroCharge: 0.1 }; // an assumed pzc
89
+ const def = build({
90
+ species: [
91
+ { name: 'Ag+', z: 1, cRef: 1000 },
92
+ { name: 'NO3-', z: -1, cRef: 1000 },
93
+ { name: 'e-', z: -1 },
94
+ ],
95
+ materials: {
96
+ water: { epsr: 78.5, species: { 'Ag+': { D: 1.65e-9, mu0: 77.1e3 }, 'NO3-': { D: 1.9e-9, mu0: -111.3e3 } } },
97
+ Ag: { conductor: { species: 'e-', conductivity: 6.3e7 } },
98
+ },
99
+ stack: [
100
+ ohmic(0, ['e-']),
101
+ layer('Ag', 1e-6),
102
+ { ...stern, reactions: [{ ...silver, k0: 1e-3, alpha: 0.5 }] },
103
+ layer('water', 10e-6, { c0: { 'Ag+': 10, 'NO3-': 10 } }),
104
+ { ...stern, reactions: [{ ...silver, k0: 1e-3, alpha: 0.5 }] },
105
+ layer('Ag', 1e-6),
106
+ ohmic(0.05, ['e-']),
107
+ ],
108
+ grid: { hmin: 0.1e-9, hmax: 100e-9, ratio: 1.15 },
109
+ });
110
+ console.log(JSON.stringify(def.interfaces[0].reactions[0])); // { left: { e-: -1, Ag(s): 1 }, right: { Ag+: -1 }, … }
111
+ const sol = new Device(def).solve();
112
+ const redox = level(sol, silver); // V_Ag⁺(x) in the solution
113
+ console.log(`redox level at mid-cell: ${redox[sol.x.length >> 1].toFixed(4)} V`);
114
+ ```
115
+
116
+ ## Sources
117
+
118
+ Helpers that write sources as plain data:
119
+
120
+ - **Waveforms** for a terminal's `V` or `I`: `pulse({ start, width, amplitude, rise, base })`,
121
+ `square({ high, low, period, duty })`, `triangle({ from, to, period })` (a voltammogram's sweep)
122
+ and `ramp({ from, to, duration, start })`. Each returns `{ t, values }` (with `repeat` for the
123
+ periodic ones), times from the device's time 0. A pulse keeps its area,
124
+ `amplitude · width`, whatever its `rise`; with no rise its edges are jumps, which the time
125
+ stepping handles exactly, so it must start after t = 0 (where a device starts, and `solve()`
126
+ reads it).
127
+ - **`injector({ name, region, from, to, species, I, headroom })`**: a port that injects a species
128
+ into a window at a driven current (a number or a waveform), spread evenly. Its conductance is
129
+ set small enough that at the peak current the port sits `headroom` volts (default 2.5) above
130
+ the window, so every node takes the same share, like a current source.
131
+ - **`recombination({ material, tau, majority })`**: band-to-band recombination e⁻ + h⁺ ⇌ 0 as
132
+ a bulk reaction, from the low-injection minority lifetime it gives at that majority density,
133
+ $`k_f = 1/(\tau \cdot n_{\mathrm{maj}})`$.
134
+
135
+ Read a transient at points inside the device with `advance(t, { probes })` (see the
136
+ [device reference](device.md#using-a-device)). And `units` converts back out of SI for display:
137
+ `toPerCm3`, `toMolar`, `toCm2PerS`, `toUm`, `toNm`, `toEV`.
138
+
139
+ ```js
140
+ import { Device, FARADAY, units } from 'driftlet';
141
+ import { build, layer, ohmic, semiconductor, pulse, injector, recombination } from 'driftlet/kit';
142
+
143
+ // Holes injected for 0.5 µs into n-Ge under 10 V/cm (Haynes–Shockley), read 1 mm downstream.
144
+ const ND = units.perCm3(1e15), I = (0.01 * ND * FARADAY * 50e-6) / 0.5e-6; // 1% of n₀ over 50 µm
145
+ const dev = new Device(
146
+ build({
147
+ T: 300,
148
+ library: [semiconductor('Ge')],
149
+ stack: [ohmic(3), layer('Ge', 3e-3, { name: 'bar', donors: ND }), ohmic(0)],
150
+ bulkReactions: [recombination({ material: 'Ge', tau: 20e-6, majority: ND })],
151
+ ports: [injector({ region: 'bar', from: 0.475e-3, to: 0.525e-3, species: 'h+', I: pulse({ width: 0.5e-6, amplitude: I, rise: 1e-9 }) })],
152
+ grid: { hmin: 10e-6, hmax: 10e-6 },
153
+ }),
154
+ );
155
+ dev.solve();
156
+ const run = dev.advance(8e-6, { dtMax: 20e-9, probes: [{ x: 1.5e-3, species: 'h+' }] });
157
+ const seen = run.trace.probes[0], k = seen.indexOf(Math.max(...seen));
158
+ console.log(`the pulse passes at ${(run.trace.t[k] * 1e6).toFixed(2)} µs, Δp ≈ ${units.toPerCm3(seen[k]).toExponential(1)} cm⁻³`);
159
+ ```
160
+
161
+ ## Plotting
162
+
163
+ Solutions are plain arrays, ready for any plotting toolkit. Two helpers save the bookkeeping of
164
+ a level diagram:
165
+
166
+ - `traces(sol, { species, standard, phi, levels, labels, shifts })` (in `driftlet/kit`) returns
167
+ its lines as data: per charged species its voltage $`V_i`$ (`kind: 'level'`) and standard level
168
+ $`V^\circ_i`$ (`'standard'`, the band edges for e⁻ and h⁺), optionally $`\phi`$ (`phi: true`, or a
169
+ list of regions to draw it in only, by name or material, such as an insulator's:
170
+ `phi: ['SiO2']`; see [what's left out](visualization.md#whats-left-out-and-why)) and
171
+ half-reaction levels (`levels: [{ half, label, standard }]`, kinds `'redox'` and
172
+ `'redox-standard'`). Labels typeset the species as a subscript (`V_{SO₄²⁻}`, the species as
173
+ `typeset(name, z)` writes it); `labels` renames lines by id (`{ 'V:e-': 'Fermi level' }`).
174
+ `shifts` gives species display offsets in volts (`{ 'K+': 3.5 }`), moving all of a species'
175
+ lines together so that widely separated species share one readable plot; the label says so
176
+ (⌇). Each line has a colour `role` (`'electron'` for e⁻, `'cation'` (holes too), `'anion'`,
177
+ `'redox'`, `'phi'`) and a `slot` within it, so a colour follows its species whichever lines are
178
+ shown (`speciesRole(sol, name)` gives a species' own). Undefined values are `NaN` (break the line
179
+ there), and doubled interface nodes share an x, so steps draw as vertical lines. It also gives
180
+ the regions (`{ name, material, x0, x1 }`), the faces' positions and a suggested range.
181
+ - `bandDiagram(sol, opts)` in `driftlet/plot` draws them as an SVG string, with no DOM needed, in
182
+ the style [reading level diagrams](visualization.md) explains: species voltages thick and solid, standard levels thin and solid, redox
183
+ levels thick and dashed, standard redox levels thin and dashed, $`\phi`$ thin, dotted and grey;
184
+ electrons steel blue, cations warm colours, anions cool ones, redox levels blue-violets (a
185
+ palette checked for colour-vision deficiency, light and dark); a ⌇ across a shifted species,
186
+ with a note. The colours are CSS custom properties (`--driftlet-electron`,
187
+ `--driftlet-cation-1` …, `--driftlet-anion-1` …, `--driftlet-redox-1` …, `--driftlet-ink`, …)
188
+ with defaults of no specificity, so a rule such as `.figure svg { --driftlet-electron: … }`
189
+ restyles them; `THEME` and `themeColor(role, slot, { dark })` give them to other charts.
190
+ `xlim` (m) zooms into a window, with ticks and range fitted to it; `ytick(v)` writes the y
191
+ axis's tick labels. `levelChart(traces, opts)` draws traces you've edited, or any profiles
192
+ against x ($`\log_{10}`$ concentrations, say, with `ytick` writing powers of ten). Labels, titles and
193
+ the y label mark subscripts TeX-style, `V_{e⁻}` or `C_ox` (a run of letters, digits and
194
+ charge signs), drawn lowered and smaller; tooltips and the accessible name read `V_e⁻`.
195
+ `labelParts(label)` splits a label into its plain and subscript runs, for other renderers.
196
+
197
+ ```js
198
+ import { Device } from 'driftlet';
199
+ import { build, layer, bath } from 'driftlet/kit';
200
+ import { bandDiagram } from 'driftlet/plot';
201
+
202
+ const def = build({
203
+ species: [
204
+ { name: 'Na+', z: 1, cRef: 1000 },
205
+ { name: 'Cl-', z: -1, cRef: 1000 },
206
+ ],
207
+ materials: { water: { epsr: 78.5, species: { 'Na+': { D: 1.33e-9, mu0: -261.9e3 }, 'Cl-': { D: 2.03e-9, mu0: -131.2e3 } } } },
208
+ stack: [bath({ 'Na+': 100, 'Cl-': 100 }, 'Cl-'), layer('water', 10e-6), bath({ 'Na+': 10, 'Cl-': 10 }, 'Cl-', { I: 0 })],
209
+ grid: { hmin: 0.2e-9, hmax: 50e-9, ratio: 1.15 },
210
+ });
211
+ const svg = bandDiagram(new Device(def).solve(), { title: 'liquid junction', phi: true, xlim: [0, 1e-6] });
212
+ console.log(svg.length); // in a page: element.innerHTML = svg
213
+ ```
214
+
215
+ ## Checking a definition
216
+
217
+ `describe(def)` validates a definition and returns a readable summary: each region with its
218
+ doping, Debye length and the grid at its ends, each face's law, alignment, blocked species and
219
+ reactions, the contacts, bulk reactions, and characteristic times (dielectric relaxation,
220
+ diffusion across each region). It ends with warnings:
221
+
222
+ - numbers that look like unit slips (`unitWarnings(def)` gives these alone): a D beyond 1 m²/s
223
+ (cm²/s?), a nonzero $`\mu^\circ`$ under 100 J/mol (eV or volts?), a capacitance over 10 F/m²
224
+ (µF/cm²?), a concentration over 1000 M, lengths over a metre or under an atom, a T under
225
+ 200 K (°C?), and so on;
226
+ - double layers the grid won't resolve: end cells coarser than the Debye length (the solution's
227
+ `warnings` check this again against the solved concentrations).
228
+
229
+ They're heuristics, prompts to check, never errors.
230
+
231
+ ```js
232
+ import { units } from 'driftlet';
233
+ import { build, layer, ohmic, describe } from 'driftlet/kit';
234
+
235
+ const def = build({
236
+ species: [
237
+ { name: 'e-', z: -1 },
238
+ { name: 'h+', z: 1 },
239
+ ],
240
+ materials: {
241
+ Si: { epsr: 11.7, species: { 'e-': { D: 36, mu0: 0, cRef: units.perCm3(2.8e19) }, 'h+': { D: 12e-4, mu0: units.eV(1.12), cRef: units.perCm3(1.04e19) } } },
242
+ },
243
+ stack: [ohmic(0), layer('Si', 1e-6, { name: 'n', donors: units.perCm3(1e17) }), layer('Si', 1e-6, { name: 'p', acceptors: units.perCm3(1e16) }), ohmic(0)],
244
+ grid: { hmin: 1e-9, hmax: 20e-9 },
245
+ });
246
+ console.log(describe(def)); // … warnings: materials.Si.species.e-.D: 36 m²/s is beyond any real diffusivity …
247
+ ```
248
+
249
+ ## Live demos
250
+
251
+ `live(def, opts)` wraps a device for sliders. Call `set(patch)` as often as a control moves
252
+ (the patch merges into the definition, as `Device.set()` does), and draw what comes back:
253
+
254
+ ```js nocheck
255
+ const dev = live(def, { worker: true, onsolution: (sol, info) => (figure.innerHTML = bandDiagram(sol)) });
256
+ slider.oninput = () => dev.set({ contacts: { right: { V: +slider.value } } });
257
+ ```
258
+
259
+ - Solves run one at a time, each warm-started from the last. Changes made while one runs are
260
+ merged and solved together, so a fast slider never queues up stale work. On the page's own
261
+ thread, each solve is its own task, so the page paints and takes input in between.
262
+ - A change that fails from the warm start is ramped to from the last good state, in more and
263
+ more steps (every number that differs is interpolated), then tried from scratch. If all of
264
+ that fails, the last good solution stays, with `info.failed` and the solver's `warnings`.
265
+ An invalid change does the same, with `info.error`. `info.ramp` says how many ramp steps were
266
+ needed, `info.ms` how long it took.
267
+ - `set()` returns a promise of `{ solution, info }`, and `ready` is the first solve's.
268
+ `solution` is always the last good one.
269
+ - `worker: true` solves in a Web Worker (a module worker), keeping the page responsive however
270
+ long a solve takes. It works with the library loaded from a CDN too.
271
+
272
+ ```js
273
+ import { units } from 'driftlet';
274
+ import { build, layer, ohmic, live } from 'driftlet/kit';
275
+
276
+ const dev = live(
277
+ build({
278
+ species: [
279
+ { name: 'e-', z: -1 },
280
+ { name: 'h+', z: 1 },
281
+ ],
282
+ materials: {
283
+ Si: { epsr: 11.7, species: { 'e-': { D: 36e-4, mu0: 0, cRef: units.perCm3(2.8e19) }, 'h+': { D: 12e-4, mu0: units.eV(1.12), cRef: units.perCm3(1.04e19) } } },
284
+ },
285
+ stack: [ohmic(0), layer('Si', 1e-6, { donors: units.perCm3(1e17) }), layer('Si', 1e-6, { acceptors: units.perCm3(1e16) }), ohmic(0)],
286
+ bulkReactions: [{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }],
287
+ grid: { hmin: 1e-9, hmax: 20e-9 },
288
+ }),
289
+ );
290
+ for (const V of [0.1, 0.2, 0.3, 0.4]) dev.set({ contacts: { right: { V } } }); // as a slider would
291
+ const { solution, info } = await dev.set({ contacts: { right: { V: 0.5 } } });
292
+ console.log(`I(0.5 V) = ${solution.current.toFixed(1)} A/m², in ${info.ms.toFixed(1)} ms`);
293
+ ```
294
+
295
+ ## Alignment from vacuum levels
296
+
297
+ `vacuumLevel`, `vacuumDipole` and `vacuumZeroCharge` turn vacuum-level estimates (electron
298
+ affinities, work functions, Trasatti's absolute SHE) into an interface's alignment. See the
299
+ [alignment guide](alignment.md).
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 74" width="300" height="74" role="img" aria-label="driftlet">
2
+ <path d="M4 18 C 34 18, 46 20, 58 34 S 72 54, 80 54" fill="none" stroke="#e8584f" stroke-width="5" stroke-linecap="round"/>
3
+ <path d="M4 34 C 34 34, 46 36, 58 50 S 72 70, 80 70" fill="none" stroke="#e8584f" stroke-width="1.8" stroke-linecap="round" opacity="0.75"/>
4
+ <path d="M44.9 9.7 L38.9 6.1 M41.0 13.4 L32.8 8.5" stroke="#ffb3ab" stroke-width="1.8" stroke-linecap="round" opacity="0.6"/>
5
+ <circle cx="52.2" cy="17.1" r="6.2" fill="#ffb3ab"/>
6
+ <text x="96" y="52" fill="#ffffff" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="46" font-weight="600" letter-spacing="-0.5">driftlet</text>
7
+ </svg>
package/docs/logo.svg ADDED
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 74" width="300" height="74" role="img" aria-label="driftlet">
2
+ <path d="M4 18 C 34 18, 46 20, 58 34 S 72 54, 80 54" fill="none" stroke="#d1362f" stroke-width="5" stroke-linecap="round"/>
3
+ <path d="M4 34 C 34 34, 46 36, 58 50 S 72 70, 80 70" fill="none" stroke="#d1362f" stroke-width="1.8" stroke-linecap="round" opacity="0.75"/>
4
+ <path d="M44.9 9.7 L38.9 6.1 M41.0 13.4 L32.8 8.5" stroke="#8a1d18" stroke-width="1.8" stroke-linecap="round" opacity="0.6"/>
5
+ <circle cx="52.2" cy="17.1" r="6.2" fill="#8a1d18"/>
6
+ <text x="96" y="52" fill="#0b0b0b" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="46" font-weight="600" letter-spacing="-0.5">driftlet</text>
7
+ </svg>