driftlet 0.4.0 → 0.6.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 +8 -4
- package/docs/device.md +33 -3
- package/docs/kit.md +123 -0
- package/docs/numerics.md +6 -2
- package/docs/visualization.md +9 -0
- package/llms.txt +46 -11
- package/package.json +1 -1
- package/src/checks.js +268 -0
- package/src/describe.js +11 -6
- package/src/device.js +50 -3
- package/src/devices.js +135 -0
- package/src/index.js +11 -2
- package/src/kit.js +3 -0
- package/src/plot.js +3 -2
- package/src/recorder.js +89 -0
- package/src/solution.js +19 -1
- package/src/solver.js +209 -12
- package/src/stack.js +1 -1
- package/src/traces.js +23 -10
- package/src/types.js +4 -1
- package/types/checks.d.ts +59 -0
- package/types/devices.d.ts +77 -0
- package/types/index.d.ts +3 -2
- package/types/kit.d.ts +3 -0
- package/types/plot.d.ts +2 -1
- package/types/recorder.d.ts +27 -0
- package/types/solution.d.ts +1 -0
- package/types/solver.d.ts +18 -1
- package/types/stack.d.ts +1 -0
- package/types/traces.d.ts +7 -2
- package/types/types.d.ts +15 -0
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ The same equations go by different names in different fields. If your problem is
|
|
|
31
31
|
| If you work on | you may call it | start from | checked against |
|
|
32
32
|
|---|---|---|---|
|
|
33
33
|
| Semiconductor devices | drift–diffusion, van Roosbroeck, quasi-Fermi levels, Scharfetter–Gummel; pn, Schottky, MOS, heterojunctions | [first example](#a-semiconductor-junction), [pn](https://markblundeberg.github.io/driftlet/demos/pn.html) and [MOS](https://markblundeberg.github.io/driftlet/demos/mos.html) demos, [`contacts`](test/contacts.test.js), [`metal`](test/metal.test.js) tests | exact built-in potential, Shockley J–V, depletion charge, MOS C–V |
|
|
34
|
-
| Solar cells | photogeneration, radiative and SRH recombination | [solar](https://markblundeberg.github.io/driftlet/demos/solar.html)
|
|
34
|
+
| Solar cells | photogeneration, radiative and SRH recombination | [solar](https://markblundeberg.github.io/driftlet/demos/solar.html) [organic (excitons)](https://markblundeberg.github.io/driftlet/demos/organic.html) and [perovskite (mobile ions)](https://markblundeberg.github.io/driftlet/demos/perovskite.html) demos, [`ionmonger`](test/ionmonger.test.js), [`reactions`](test/reactions.test.js) and [`generation`](test/generation.test.js) tests: generation is a reaction from a photon reservoir (uniform, or Beer–Lambert with `photogeneration()`), SRH as a rate law, in the bulk and at faces | `J_sc = qG(L_n + L_p + W)`; J_sc under Beer–Lambert against collection theory; Shockley J–V in the dark |
|
|
35
35
|
| Electrochemistry | Nernst–Planck, concentration polarization, limiting current, Butler–Volmer, Warburg, cyclic voltammetry, salt bridges and liquid junctions | [second example](#an-electrochemical-cell), [cyclic voltammetry](https://markblundeberg.github.io/driftlet/demos/redox.html), [Daniell cell](https://markblundeberg.github.io/driftlet/demos/daniell.html), [saturation](https://markblundeberg.github.io/driftlet/demos/saturation.html) and [impedance](https://markblundeberg.github.io/driftlet/demos/impedance.html) demos, [`circuit`](test/circuit.test.js), [`kinetics`](test/kinetics.test.js), [`impedance`](test/impedance.test.js) tests | `i_lim·tanh(V/4V_T)`, Butler–Volmer closed form, finite-length Warburg |
|
|
36
36
|
| Batteries, intercalation | OCV, insertion hosts, chemical diffusion | [insertion demo](https://markblundeberg.github.io/driftlet/demos/insertion.html), [`statistics`](test/statistics.test.js) test | composition vs OCV, π²D/4L² relaxation |
|
|
37
37
|
| Double layers, colloids | Poisson–Boltzmann, Gouy–Chapman–Stern, Debye screening, crowding (Bikerman) | [double-layer demo](https://markblundeberg.github.io/driftlet/demos/double-layer.html), [`equilibrium`](test/equilibrium.test.js) test | Gouy–Chapman charge and profile, Kilic–Bazant–Ajdari |
|
|
@@ -55,7 +55,7 @@ import { bandDiagram } from 'driftlet/plot'; // a level diagram as an SVG str
|
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
Or straight from a CDN in a page, pinned to a version:
|
|
58
|
-
`https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
58
|
+
`https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
|
|
59
59
|
TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
|
|
60
60
|
[`llms.txt`](llms.txt).
|
|
61
61
|
|
|
@@ -128,7 +128,9 @@ Solutions are plain objects of `Float64Array`s, ready to plot (fresh arrays from
|
|
|
128
128
|
and the voltage views `V`, `Vstd`. They also carry the terminals' voltages and currents, fluxes,
|
|
129
129
|
conservation bookkeeping and warnings; see [the device reference](docs/device.md). The
|
|
130
130
|
[kit](docs/kit.md) adds reactions written as equations (`'Ag+ + e- = Ag(s)'`), redox levels,
|
|
131
|
-
level diagrams, slider demos
|
|
131
|
+
level diagrams, slider demos, a transient recorder, `describe()` (which flags likely unit slips),
|
|
132
|
+
`check()` (a solution's ledgers and a grid-convergence check),
|
|
133
|
+
and curated devices with reference results: IonMonger's perovskite cell, with its J–V scans.
|
|
132
134
|
|
|
133
135
|
## Demos
|
|
134
136
|
|
|
@@ -146,7 +148,7 @@ them locally, serve the repository root (e.g. `python3 -m http.server`) and open
|
|
|
146
148
|
| **Cyclic voltammetry**: a Fermi level against a redox level | **Daniell cell**: a real salt bridge, leaking, ion by ion | **Saturation**: Ag \| AgNO₃ \| Ag under a square wave, up to its limiting current |
|
|
147
149
|
| [](https://markblundeberg.github.io/driftlet/demos/impedance.html) | [](https://markblundeberg.github.io/driftlet/demos/membrane.html) | [](https://markblundeberg.github.io/driftlet/demos/double-layer.html) |
|
|
148
150
|
| **Impedance**: the Warburg arc, and the cell's small-signal response inside | **Ion-exchange membrane**: Donnan steps, ion by ion, against TMS theory | **Double layer**: dilute vs crowded ions, against closed forms |
|
|
149
|
-
| [](https://markblundeberg.github.io/driftlet/demos/insertion.html) | [](https://markblundeberg.github.io/driftlet/demos/organic.html) |
|
|
151
|
+
| [](https://markblundeberg.github.io/driftlet/demos/insertion.html) | [](https://markblundeberg.github.io/driftlet/demos/organic.html) | [](https://markblundeberg.github.io/driftlet/demos/perovskite.html) |
|
|
150
152
|
| **Intercalation host**: cycling between cutoffs against the OCV | | |
|
|
151
153
|
|
|
152
154
|
## How to think about it
|
|
@@ -235,6 +237,8 @@ Every physics feature is tested against analytic results (`npm test`, node's bui
|
|
|
235
237
|
| A packet laid down as a `c0` profile | the same rates from a Gaussian at t = 0: μ*E t, σ₀² + 2D*t, e^(−t/τ*) | 3e-4; 1e-2; 1e-3 |
|
|
236
238
|
| Beer–Lambert photogeneration in an n⁺p cell | J_sc = qΦ∫αe^(−αx)η(x)dx with the emitter's, depletion layer's and base's collection, α from 1e3 to 1e7 /m on one coarse grid | 5e-4 (3e-3 in the emitter) |
|
|
237
239
|
| Organic bilayer: excitons | J_sc from excitons diffusing to the donor/acceptor interface (Beer–Lambert, a blocking anode, the splitting reaction's finite velocity); V_oc from charge-transfer detailed balance | 5e-4; 1e-6 V |
|
|
240
|
+
| SRH recombination | bulk: G = R_SRH(n, p) with n = N_D + p, midgap and shallow traps, low to high injection; at a face: zero in the dark, the law between the two edge nodes, every pair collected or recombined | 1e-6; 1e-9 |
|
|
241
|
+
| Perovskite J–V hysteresis, against IonMonger | an independent code (finite elements, Octave): mobile iodide vacancies, interface SRH, scans from 1 mV/s to 1 kV/s — hysteresis index, P_max, V_oc, the whole loop (as current where it's gentle, as a voltage offset near V_oc, where it falls at up to 1100 mA/cm² per volt) | 1.5e-3; 0.05 mW/cm²; 2 mV; 0.03 mA/cm² and 1 mV (0.010 and 0.31 mV found) |
|
|
238
242
|
| Illuminated long pn diode | `J_sc = qG(L_n + L_p + W)` from a cold start; superposition at low injection | 5e-3; 1e-2 |
|
|
239
243
|
| Schottky barrier (metal region \| n-Si) | surface density from the alignment; depletion charge | 5e-3; 2% |
|
|
240
244
|
| Liquid junction, open circuit | cell EMF 2t₊(RT/F) ln(c₁/c₂); Planck diffusion potential | 1e-4 |
|
package/docs/device.md
CHANGED
|
@@ -203,13 +203,29 @@ Butler–Volmer,
|
|
|
203
203
|
r = k_0 \prod_{\nu<0} (c/c_{\mathrm{ref}})^{|\nu|(1-\alpha)} \prod_{\nu>0} (c/c_{\mathrm{ref}})^{\nu\alpha} \left(e^{\alpha a} - e^{-(1-\alpha)a}\right)
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
-
with $`k_0`$ in mol/(m²·s) and $`\alpha`$ (default 0.5)
|
|
206
|
+
with $`k_0`$ in mol/(m²·s) and $`\alpha`$ (default 0.5) from 0 to 1 inclusive ($`\alpha = 0`$ makes the
|
|
207
|
+
forward rate plain mass action, independent of the energetics). A conductor's carrier has
|
|
207
208
|
activity 1 and no factor. That's mass action with rate constants that depend on the electrical part
|
|
208
209
|
of the affinity: exactly zero at $`A = 0`$, whatever $`k_0`$ and $`\alpha`$. Each participant is made or
|
|
209
210
|
consumed at its side's edge node, just behind any Stern layer, so Frumkin effects arise by
|
|
210
211
|
themselves. A face can carry several reactions; a conductor coupled to two couples settles at their
|
|
211
212
|
mixed potential. Each solution reports the rates as `interfaces[f].rates`.
|
|
212
213
|
|
|
214
|
+
**Interface recombination through traps** (SRH at a face) replaces `k0` and `alpha` with
|
|
215
|
+
`srh: { vn, vp, n1 }`, on a reaction consuming one negative and one positive species and making
|
|
216
|
+
none, such as `'e-(left) + h+(right) = 0'`:
|
|
217
|
+
|
|
218
|
+
```math
|
|
219
|
+
r = \frac{n\,p\,(1 - e^{-A/RT})}{(n + n_1)/v_p + (p + p_1)/v_n}, \qquad p_1 = \frac{n\,p\,e^{-A/RT}}{n_1}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
with n and p each at its own side's edge node, the capture velocities $`v_n`$ and $`v_p`$ in m/s,
|
|
223
|
+
and $`n_1`$ (mol/m³) the negative species' concentration with its level at the trap (default:
|
|
224
|
+
$`n_1 = p_1 = n_i`$, a midgap trap). Taking $`p_1`$ from the state keeps equilibrium exact whatever
|
|
225
|
+
lies between the two sides (a band offset, a $`\phi`$ jump). Where one carrier is plentiful the
|
|
226
|
+
rate saturates at the other's capture, $`r \to v_p\,p`$: holes reaching an electron-rich layer
|
|
227
|
+
recombine at $`v_p`$ however many electrons wait there, which plain mass action can't do.
|
|
228
|
+
|
|
213
229
|
### Faces next to a conductor
|
|
214
230
|
|
|
215
231
|
A conductor has no $`\phi`$, so a face beside it can't take a `dipole` or `step` alignment. Its $`\phi`$
|
|
@@ -253,6 +269,17 @@ $`A = 0`$. `kf` maps material
|
|
|
253
269
|
names to forward rate constants (units making r mol/(m³·s)), and the reaction runs only in
|
|
254
270
|
those materials.
|
|
255
271
|
|
|
272
|
+
**SRH (trap-assisted) recombination** replaces `kf` with `srh`, a map from material names to
|
|
273
|
+
`{ tauN, tauP, n1 }`, on a reaction consuming one negative and one positive species and making
|
|
274
|
+
none (`'e- + h+ = 0'`):
|
|
275
|
+
$`r = n p (1 - e^{-A/RT}) / (\tau_p (n + n_1) + \tau_n (p + p_1))`$, with lifetimes in s and $`n_1`$ as
|
|
276
|
+
for [interface SRH](#interfaces) (default midgap). It's what explicit trap species reduce to when
|
|
277
|
+
the traps are few and fast, without them.
|
|
278
|
+
|
|
279
|
+
```js nocheck
|
|
280
|
+
bulkReactions: [{ equation: 'e- + h+ = 0', srh: { Si: { tauN: 1e-6, tauP: 1e-6 } } }]
|
|
281
|
+
```
|
|
282
|
+
|
|
256
283
|
**Generation** is a reaction from a reservoir: photogeneration is
|
|
257
284
|
$`\text{photon} \to \mathrm{e}^- + \mathrm{h}^+`$, with the photons a fixed participant whose $`\mu`$
|
|
258
285
|
sits well above the gap, so that $`e^{-A/RT}`$ is negligible and the rate is `kf` itself, the
|
|
@@ -486,7 +513,8 @@ const now = dev.solution(); // snapshot of the current state
|
|
|
486
513
|
be the one measured: the resistance belongs to the external circuit. With `profiles: true`, each
|
|
487
514
|
frequency also returns complex profiles of $`\delta\phi`$, $`\delta\bar\mu`$ and $`\delta c`$ per unit
|
|
488
515
|
excitation.
|
|
489
|
-
- `set(patch)` merges plain objects deeply (arrays are replaced)
|
|
516
|
+
- `set(patch)` merges plain objects deeply (arrays are replaced), except that a contact given `V`
|
|
517
|
+
or `I` drops the other: `{ contacts: { right: { I: 0 } } }` switches it to open circuit. A patch that changes only the
|
|
490
518
|
terminals' drives (`V`, `I`, `R`) updates them in place, cheaply, keeping everything else.
|
|
491
519
|
Otherwise the device is rebuilt: the current state carries over while the grid and species
|
|
492
520
|
are unchanged, or restarts from the regions' `c0`. A carried state keeps what it holds: a
|
|
@@ -535,11 +563,13 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
|
|
|
535
563
|
| `contacts.left/right` | `{ V, flux: {name}, D, current }` at each contact |
|
|
536
564
|
| `gates.left/right` | charge on a gate or Stern plate, where the contact is capacitive |
|
|
537
565
|
| `ports[k]` | `{ name, V, flux: {name}, current }`: what each internal port brings into the device |
|
|
538
|
-
| `interfaces[f]` | `{ dipole, sheetCharge, D, N: {name}, rates }`: what crosses
|
|
566
|
+
| `interfaces[f]` | `{ left, right, dipole, sheetCharge, D, N: {name}, rates }`: the names of the regions the face joins, what crosses it by its links, and each face reaction's rate (mol/(m²·s)) |
|
|
567
|
+
| `bulkReactions[k]` | `{ rate, regions, total }`: each bulk reaction's forward rate at every node (mol/(m³·s), `NaN` where it doesn't run), and integrated over each region and the device (mol/(m²·s)), as the balances count it: a charge-balance check is J = F(generation − recombination), which the kit's `check()` does for every species |
|
|
539
568
|
| `charge` | total charge in the device, C/m² |
|
|
540
569
|
| `conservation` | per species stretch: amount, reference, intake through contacts, drift |
|
|
541
570
|
| `warnings` | e.g. unresolved double layers, conventions a statistics model relies on, and for a failed solve, where the system is nearly singular |
|
|
542
571
|
| `converged`, `iterations`, `steps`, `substeps`, `history`, `time` | solver bookkeeping |
|
|
572
|
+
| `steady` | whether this is a converged steady state (from `solve()`), not a transient's: what `check()` in the kit decides its checks by |
|
|
543
573
|
| `done`, `rejected`, `trace` | from `advance()`: whether `tEnd` was reached; rejected steps; `{ t, current, voltage }` per accepted step |
|
|
544
574
|
|
|
545
575
|
A stretch is a run of regions in which a species is present and connected. Its `drift`
|
package/docs/kit.md
CHANGED
|
@@ -169,6 +169,71 @@ const seen = run.trace.probes[0], k = seen.indexOf(Math.max(...seen));
|
|
|
169
169
|
console.log(`the pulse passes at ${(run.trace.t[k] * 1e6).toFixed(2)} µs, Δp ≈ ${units.toPerCm3(seen[k]).toExponential(1)} cm⁻³`);
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
+
## Recording a transient
|
|
173
|
+
|
|
174
|
+
`recorder(device, { every, times, probes })` runs a transient in pieces and keeps what a demo
|
|
175
|
+
needs to show it: `trace`, the terminal current and voltage (and the probes' readings) at every
|
|
176
|
+
accepted step from the start, joined up across calls; and `frames`, whole solutions to scrub back
|
|
177
|
+
through or draw side by side. Frames are kept at the start, then at each of `times` or every
|
|
178
|
+
`every` seconds, which the stepping lands on exactly; with neither, at the end of each call.
|
|
179
|
+
Its `advance(tEnd, opts)` takes `Device.advance()`'s options, so an animation calls it once per
|
|
180
|
+
frame with a `budgetMs` (`done` says whether it got there), and a run in budgets is step for
|
|
181
|
+
step the run made in one go. `frame(t)` is the frame at or just before t.
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
import { Device } from 'driftlet';
|
|
185
|
+
import { build, layer, bath, recorder } from 'driftlet/kit';
|
|
186
|
+
|
|
187
|
+
// A salt step relaxing: 100 mM KCl in from a bath, into 10 mM held behind a blocking wall.
|
|
188
|
+
const dev = new Device(
|
|
189
|
+
build({
|
|
190
|
+
species: [
|
|
191
|
+
{ name: 'K+', z: 1, cRef: 1000 },
|
|
192
|
+
{ name: 'Cl-', z: -1, cRef: 1000 },
|
|
193
|
+
],
|
|
194
|
+
materials: { water: { epsr: 0, species: { 'K+': { D: 1.96e-9, mu0: 0 }, 'Cl-': { D: 2.03e-9, mu0: 0 } } } },
|
|
195
|
+
stack: [bath({ 'K+': 100, 'Cl-': 100 }, 'Cl-'), layer('water', 100e-6, { c0: { 'K+': 10, 'Cl-': 10 } }), { species: { 'K+': 'blocked', 'Cl-': 'blocked' }, phi: 'neutral' }],
|
|
196
|
+
grid: { hmin: 2e-6, hmax: 2e-6 },
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
const rec = recorder(dev, { every: 1, probes: [{ x: 100e-6, species: 'K+' }] });
|
|
200
|
+
rec.advance(10, { tol: 1e-4 });
|
|
201
|
+
console.log(`${rec.frames.length} frames, ${rec.trace.t.length} steps; at the wall after 5 s: ${rec.frame(5).c['K+'].at(-1).toFixed(2)} mol/m³`);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Curated devices
|
|
205
|
+
|
|
206
|
+
Whole devices from a published parameter set, with reference results from an independent code
|
|
207
|
+
to check a run against, so a demo starts from numbers that someone has measured or
|
|
208
|
+
benchmarked rather than invented ones. Each returns an ordinary definition (print it to see
|
|
209
|
+
every parameter, or edit it).
|
|
210
|
+
|
|
211
|
+
- **`perovskiteCell({ V, light, bulkSRH })`**: IonMonger's default planar perovskite solar
|
|
212
|
+
cell (Courtier et al., J. Comput. Electron. 18, 1435 (2019)): TiO₂ (100 nm) | MAPbI₃ (400 nm)
|
|
213
|
+
with mobile iodide vacancies over an equal immobile background | spiro-OMeTAD (200 nm), at
|
|
214
|
+
298 K, lit through the TiO₂ (1.4e21 photons/(m²·s) at α = 1.3e7 /m, times `light`). SRH at
|
|
215
|
+
both faces and, unless `bulkSRH: false`, in the perovskite. `V` drives the spiro side's contact,
|
|
216
|
+
so photocurrent is positive. `PEROVSKITE_SCANS` holds IonMonger's own J–V scans of it (steady
|
|
217
|
+
at 1.2 V under light, then 1.2 → 0 → 1.2 V, at seven rates from 1 mV/s to 1 kV/s): per scan
|
|
218
|
+
the current at 0 V, each sweep's maximum power and V_oc, and the hysteresis index, with bulk
|
|
219
|
+
SRH (`full`) and without (`noBulkSRH`). driftlet's test suite reproduces them (and the whole
|
|
220
|
+
loops, to 0.03 mA/cm² where they're gentle and 1 mV where steep). `hysteresis(trace, turn)`
|
|
221
|
+
reads the same figures from a run's trace.
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
import { Device } from 'driftlet';
|
|
225
|
+
import { perovskiteCell, PEROVSKITE_SCANS, hysteresis, recorder } from 'driftlet/kit';
|
|
226
|
+
|
|
227
|
+
const rate = 10, half = 1.2 / rate; // V/s
|
|
228
|
+
const dev = new Device(perovskiteCell({ V: 1.2 }));
|
|
229
|
+
dev.solve();
|
|
230
|
+
dev.set({ contacts: { right: { V: { t: [0, half, 2 * half], values: [1.2, 0, 1.2] } } } });
|
|
231
|
+
const rec = recorder(dev);
|
|
232
|
+
rec.advance(2 * half, { tol: 1e-4, dtMax: (2 * half) / 480 });
|
|
233
|
+
const ours = hysteresis(rec.trace, half), theirs = PEROVSKITE_SCANS.full.find((s) => s.rate === rate);
|
|
234
|
+
console.log(`P_max ${ours.rev.Pmax.toFixed(1)} W/m² going down (IonMonger ${theirs.rev.Pmax}), hysteresis index ${ours.hi.toFixed(4)} (${theirs.hi})`);
|
|
235
|
+
```
|
|
236
|
+
|
|
172
237
|
## Plotting
|
|
173
238
|
|
|
174
239
|
Solutions are plain arrays, ready for any plotting toolkit. Two helpers save the bookkeeping of
|
|
@@ -189,6 +254,12 @@ a level diagram:
|
|
|
189
254
|
shown (`speciesRole(sol, name)` gives a species' own). Undefined values are `NaN` (break the line
|
|
190
255
|
there), and doubled interface nodes share an x, so steps draw as vertical lines. It also gives
|
|
191
256
|
the regions (`{ name, material, x0, x1 }`), the faces' positions and a suggested range.
|
|
257
|
+
- `energy: true` (in `traces` and `bandDiagram`) gives the familiar band diagram instead, energy
|
|
258
|
+
up: electrons and holes as electron energies in eV (E_c and E_v from the standard levels, E_Fn
|
|
259
|
+
and E_Fp, or E_F where there are no holes, from the levels), with redox levels and $`\phi`$ (as
|
|
260
|
+
$`-q\phi`$) turned over too and shifts in eV. Ions have no band, so asking for one is an error.
|
|
261
|
+
It's the right picture for semiconductor devices; species voltages earn their place where ions
|
|
262
|
+
share the diagram (see [which to use](visualization.md)).
|
|
192
263
|
- `bandDiagram(sol, opts)` in `driftlet/plot` draws them as an SVG string, with no DOM needed, in
|
|
193
264
|
the style [reading level diagrams](visualization.md) explains: species voltages thick and solid, standard levels thin and solid, redox
|
|
194
265
|
levels thick and dashed, standard redox levels thin and dashed, $`\phi`$ thin, dotted and grey;
|
|
@@ -257,6 +328,58 @@ const def = build({
|
|
|
257
328
|
console.log(describe(def)); // … warnings: materials.Si.species.e-.D: 36 m²/s is beyond any real diffusivity …
|
|
258
329
|
```
|
|
259
330
|
|
|
331
|
+
## Checking a result
|
|
332
|
+
|
|
333
|
+
`check(device, sol, { refine, tol })` checks a solved device, and says what it compared, so a
|
|
334
|
+
page can show that its numbers hold up. Pass the solution `solve()` or `advance()` returned.
|
|
335
|
+
|
|
336
|
+
- **converged**: the solve converged.
|
|
337
|
+
- **warnings**: the solution's warnings (unresolved double layers, steep profiles, a weakly held
|
|
338
|
+
part) and the definition's likely unit slips. They're heuristics, so they're marked `?`, a
|
|
339
|
+
prompt to look, and don't fail the check.
|
|
340
|
+
- **balance**, in a steady state: each species' ledger, what comes in through each terminal and
|
|
341
|
+
what each reaction (bulk, or at a face) makes or uses, in mol/(m²·s) and for a charged species
|
|
342
|
+
as a current. The terms must sum to zero. Under light it's J = F(G − R), and it says where
|
|
343
|
+
every carrier went: collected, or recombined in the bulk or at which face. A species at rest
|
|
344
|
+
(in equilibrium, flows only of round-off against what it could carry) isn't listed.
|
|
345
|
+
- **conservation**, in a transient: every closed, unreacting stretch keeps what it held plus
|
|
346
|
+
what came in.
|
|
347
|
+
- **grid**, in a steady state (unless `refine: false`): the device solved again on a grid twice
|
|
348
|
+
as fine everywhere (half of each `hmin` and `hmax`, the square root of each `ratio`), starting
|
|
349
|
+
from this solution interpolated onto it. The current, each floating terminal's voltage (an open-circuit voltage) and each
|
|
350
|
+
region's charge must agree to `tol` (relative; default 1e-2, about what a plot shows; a
|
|
351
|
+
benchmark wants 1e-3 or less). The discretisation is second order, so the change estimates
|
|
352
|
+
this grid's own error (about ¾ of it). It costs one more solve, so run it once, not on every
|
|
353
|
+
slider move.
|
|
354
|
+
|
|
355
|
+
It returns `{ ok, items, text }`: `ok` when nothing failed, an item per check
|
|
356
|
+
(`{ name, ok, summary, details }`, with `ok: null` for a prompt to look), and `text`, a line
|
|
357
|
+
per check marked `ok`, `FAIL` or `?`, ready to log or put on the page.
|
|
358
|
+
|
|
359
|
+
```js
|
|
360
|
+
import { Device, units } from 'driftlet';
|
|
361
|
+
import { build, layer, ohmic, semiconductor, photogeneration, check } from 'driftlet/kit';
|
|
362
|
+
|
|
363
|
+
// An n⁺p silicon cell under blue-green light (α = 1e5 /m), at 0.5 V.
|
|
364
|
+
const W = units.um(60.5);
|
|
365
|
+
const dev = new Device(
|
|
366
|
+
build({
|
|
367
|
+
T: 300,
|
|
368
|
+
library: [semiconductor('Si')],
|
|
369
|
+
stack: [ohmic(0), layer('Si', units.um(0.5), { donors: units.perCm3(1e19) }), layer('Si', units.um(60), { acceptors: units.perCm3(1e16) }), ohmic(0.5)],
|
|
370
|
+
bulkReactions: [{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }, photogeneration({ material: 'Si', flux: 3e-3, alpha: 1e5, mu: units.eV(3), to: W })],
|
|
371
|
+
grid: { hmin: units.nm(1), hmax: units.um(1) },
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
const report = check(dev, dev.solve());
|
|
375
|
+
console.log(report.text);
|
|
376
|
+
// ok balance: 2 species' sources and sinks sum to zero …
|
|
377
|
+
// e-: photon = e- + h+: +2.99e-3 (289 A/m²); left contact: -2.23e-3 (-215 A/m²); …
|
|
378
|
+
// FAIL grid: on a grid twice as fine (172 → 339 nodes), the largest change is region 0 (Si)
|
|
379
|
+
// charge, … (0.021, over 0.01: refine the grid …): the current is fine (1e-4), but the
|
|
380
|
+
// emitter's depletion charge is 2 % off on this grid; hmin: units.nm(0.25) fixes it.
|
|
381
|
+
```
|
|
382
|
+
|
|
260
383
|
## Live demos
|
|
261
384
|
|
|
262
385
|
`live(def, opts)` wraps a device for sliders. Call `set(patch)` as often as a control moves
|
package/docs/numerics.md
CHANGED
|
@@ -545,8 +545,12 @@ take about twice as long as before.
|
|
|
545
545
|
larger by $`D \cdot dt/(hL)`$, around 1e12 for a micron-scale cell (see
|
|
546
546
|
[metal regions](#metal-regions) for the same cancellation). Newton then needs dozens of
|
|
547
547
|
iterations, or fails.
|
|
548
|
-
- **
|
|
549
|
-
|
|
548
|
+
- **Immobile combinations** (trap states X⁰ and X⁻ under e⁻ + X⁰ = X⁻, with D = 0) conserve
|
|
549
|
+
node by node, since nothing carries them anywhere. At each node the combination's weighted sum
|
|
550
|
+
replaces one of its balance rows, kept at what it was when the solve began. The row is local
|
|
551
|
+
to that node's block, so nothing is bordered, and the solve goes direct.
|
|
552
|
+
- **Otherwise** (an immobile spectator on its own, or a combination that includes a floating
|
|
553
|
+
conductor or mixes mobile and immobile stretches), backward-Euler steps at a huge $`dt`$ (10⁶ × the slowest
|
|
550
554
|
diffusion time) keep the storage term, which pins each conserved amount exactly: sum a
|
|
551
555
|
species' rows and the fluxes cancel. $`dt`$ grows ×10 (capped) while the state still moves.
|
|
552
556
|
Before each huge step, each spectator's level is shifted uniformly to restore its amount
|
package/docs/visualization.md
CHANGED
|
@@ -7,6 +7,15 @@ electronic levels, Fermi levels and redox levels, which connect directly to elec
|
|
|
7
7
|
and other familiar quantities. The second half covers the species voltages of every charged
|
|
8
8
|
species, ions included. For how to draw them, see [the kit's plotting helpers](kit.md#plotting).
|
|
9
9
|
|
|
10
|
+
**Which to use.** For electrons and holes alone (a pn junction, a solar cell, a MOS capacitor),
|
|
11
|
+
draw the familiar band diagram, energy up: `bandDiagram(sol, { energy: true })` gives E_c and
|
|
12
|
+
E_v (thin) and the quasi-Fermi levels E_Fn and E_Fp (thick), in eV, with the conduction band on
|
|
13
|
+
top. Species voltages are the same lines in volts, turned over, and don't add anything there.
|
|
14
|
+
They earn their place where the familiar picture can't go: ions and other charged species that
|
|
15
|
+
have no band (a vacancy's level drifting out of flat as the ions lag a voltage sweep), and
|
|
16
|
+
questions of equilibrium between species (an electrode against a redox couple). Bring them in
|
|
17
|
+
there, as an extra tool, not in place of the band diagram.
|
|
18
|
+
|
|
10
19
|
## Electron levels
|
|
11
20
|
|
|
12
21
|
### The Fermi level
|
package/llms.txt
CHANGED
|
@@ -14,9 +14,9 @@ docs/conventions.md (signs and levels) and docs/data.md (the data library).
|
|
|
14
14
|
From a CDN, pin the version:
|
|
15
15
|
|
|
16
16
|
```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.
|
|
17
|
+
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/index.js';
|
|
18
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/kit.js';
|
|
19
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/plot.js';
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
@@ -40,19 +40,27 @@ From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
|
40
40
|
next to a dielectric needs its φ law. Every species that doesn't reach a contact needs its
|
|
41
41
|
initial amount (`c0`). An error names the path and says what to give.
|
|
42
42
|
- **Check before trusting.** `sol.converged` must be true, and read `sol.warnings`.
|
|
43
|
-
`describe(def)` (kit) summarises a definition and flags likely unit slips.
|
|
43
|
+
`describe(def)` (kit) summarises a definition and flags likely unit slips. Once a device
|
|
44
|
+
works, run `check(dev, sol)` (kit) and show or log its `text`: each species' ledger (what
|
|
45
|
+
every terminal and reaction brings in or uses, summing to zero: J = F(G − R) under light),
|
|
46
|
+
conservation in transients, and the same device on a grid twice as fine (a FAIL means the
|
|
47
|
+
answer depends on the grid: refine it). It costs a solve, so run it once, not per frame.
|
|
44
48
|
- **Exports.** `driftlet`: `Device`, `units`, and the constants `FARADAY`, `GAS_CONSTANT`,
|
|
45
49
|
`EPS0`, `BOLTZMANN`, `ELEMENTARY_CHARGE`, `AVOGADRO` (there's no thermal-voltage constant:
|
|
46
50
|
V_T = GAS_CONSTANT·T/FARADAY). `driftlet/kit`: `build`, `layer`, `ohmic`, `bath`, `half`,
|
|
47
51
|
`level`, `SHE`, `describe`, `live`, `traces`, the sources (`pulse`, `square`, `triangle`, `ramp`,
|
|
48
52
|
`injector`, `recombination`, `photogeneration`), the data library (`aqueous`, `semiconductor`,
|
|
49
|
-
`metal`, `IONS`, …)
|
|
53
|
+
`metal`, `IONS`, …), `check` (a solution's checks), `recorder` (a transient's trace and frames across calls), the curated
|
|
54
|
+
`perovskiteCell` with IonMonger's `PEROVSKITE_SCANS` and `hysteresis`, and the vacuum-level helpers. `driftlet/plot` (a quick look; `traces()` is the data for your own charts): `bandDiagram`, `levelChart`, `labelParts` (labels mark subscripts as `V_{e⁻}`), `themeColor`.
|
|
50
55
|
- **Reactions** are written as equations, `{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }`, in
|
|
51
56
|
`new Device`, `build()` and `set()` alike. A coefficient is a number and a space (`2 e-`).
|
|
52
57
|
- **Rate constants carry units.** A bulk reaction's rate is `kf · Π c_reactants` in mol/(m³·s),
|
|
53
58
|
with c in mol/m³ (and 1e16 cm⁻³ is only 0.0166 mol/m³). Band-to-band recombination,
|
|
54
59
|
`e- + h+ = 0`, gives a minority lifetime τ = 1/(kf·N) with N the majority density in mol/m³:
|
|
55
|
-
`kf: 1e8` is τ ≈ 0.6 µs at 1e16 cm⁻³.
|
|
60
|
+
`kf: 1e8` is τ ≈ 0.6 µs at 1e16 cm⁻³. Trap-assisted (SRH) recombination is
|
|
61
|
+
`{ equation: 'e- + h+ = 0', srh: { Si: { tauN, tauP } } }` (lifetimes in s), and at a face
|
|
62
|
+
`{ equation: 'e-(left) + h+(right) = 0', srh: { vn, vp } }` (velocities in m/s); both default to a
|
|
63
|
+
midgap trap. Generation is a reaction from a reservoir, rate `kf` in
|
|
56
64
|
mol/(m³·s) (`units.perCm3` converts cm⁻³·s⁻¹ too); light absorbed as it goes in is
|
|
57
65
|
`photogeneration({ material, flux, alpha, mu: units.eV(3), from, to })` (flux in
|
|
58
66
|
mol/(m²·s), α in 1/m), a `kf` that varies with x. A face reaction's `k0` is in mol/(m²·s);
|
|
@@ -214,7 +222,8 @@ const def = build({
|
|
|
214
222
|
const dev = new Device(def);
|
|
215
223
|
const rest = dev.solve(); // the steady state at t = 0 (solve() doesn't advance time)
|
|
216
224
|
// In a page, call advance(tNext, { budgetMs: 12, probes }) once per animation frame, carrying on
|
|
217
|
-
// next frame while sol.done is false
|
|
225
|
+
// next frame while sol.done is false (or let recorder() join the traces and keep frames to scrub
|
|
226
|
+
// back through). Probes read at accepted steps; dtMax keeps them close.
|
|
218
227
|
const run = dev.advance(10e-6, { dtMax: 20e-9, probes: [{ x: 1.525e-3, species: 'h+' }] });
|
|
219
228
|
const seen = run.trace.probes[0], k = seen.indexOf(Math.max(...seen));
|
|
220
229
|
const tArrive = run.trace.t[k] - width / 2; // from the pulse's middle
|
|
@@ -231,6 +240,17 @@ state.
|
|
|
231
240
|
Transients cost more where a carrier's density spans many decades (minority holes in silicon
|
|
232
241
|
ahead of the pulse) and at strong injection (Δp near n₀); budget for that, or coarsen the grid.
|
|
233
242
|
|
|
243
|
+
`recorder(dev, { every, times, probes })` (kit) runs a transient in pieces: its `advance(tEnd,
|
|
244
|
+
opts)` takes the same options, `rec.trace` is the whole run's trace from the start, and
|
|
245
|
+
`rec.frames` are whole solutions (at the start, then at `times`, every `every` s, or the end of
|
|
246
|
+
each call), with `rec.frame(t)` the one at or before t, for a time slider.
|
|
247
|
+
|
|
248
|
+
**Prefer a curated device to inventing numbers** where one fits: `perovskiteCell({ V, light,
|
|
249
|
+
bulkSRH })` is IonMonger's default perovskite solar cell (mobile iodide vacancies, SRH in the bulk
|
|
250
|
+
and at both faces), and `PEROVSKITE_SCANS` holds IonMonger's J–V scans of it at seven rates;
|
|
251
|
+
`hysteresis(rec.trace, turn)` reads the same figures from yours, so a demo can show its agreement.
|
|
252
|
+
The definition is plain data: print it, edit what your demo varies.
|
|
253
|
+
|
|
234
254
|
## Template: a live page with a slider
|
|
235
255
|
|
|
236
256
|
`live()` merges slider moves made during a solve, warm-starts each solve and keeps the last
|
|
@@ -245,9 +265,9 @@ a Web Worker.
|
|
|
245
265
|
<span id="readout"></span>
|
|
246
266
|
<div id="figure"></div>
|
|
247
267
|
<script type="module">
|
|
248
|
-
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
249
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
250
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
268
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/index.js';
|
|
269
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/kit.js';
|
|
270
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.6.0/src/plot.js';
|
|
251
271
|
|
|
252
272
|
const def = build({
|
|
253
273
|
T: 300,
|
|
@@ -259,7 +279,7 @@ const def = build({
|
|
|
259
279
|
const slider = document.getElementById('V');
|
|
260
280
|
const dev = live(def, {
|
|
261
281
|
onsolution(sol, info) {
|
|
262
|
-
document.getElementById('figure').innerHTML = bandDiagram(sol, { title: `V = ${sol.terminals.right.V.toFixed(2)} V` });
|
|
282
|
+
document.getElementById('figure').innerHTML = bandDiagram(sol, { energy: true, title: `V = ${sol.terminals.right.V.toFixed(2)} V` });
|
|
263
283
|
document.getElementById('readout').textContent = info.failed ? 'no solution here' : `I = ${sol.current.toExponential(2)} A/m²`;
|
|
264
284
|
},
|
|
265
285
|
});
|
|
@@ -272,6 +292,21 @@ slider.addEventListener('input', () => dev.set({ contacts: { right: { V: +slider
|
|
|
272
292
|
What `bandDiagram()` and `traces()` draw, and how to explain it to a reader (docs/visualization.md
|
|
273
293
|
has the long form):
|
|
274
294
|
|
|
295
|
+
- **Plotting in your own style.** `bandDiagram()` is a quick look. For a page of your own, draw
|
|
296
|
+
with your own charting code from `traces(sol, opts)` (in `driftlet/kit`, the same options):
|
|
297
|
+
`{ x, series: [{ id, label, kind, role, slot, y }], regions, faces, range }`, where `kind` says how
|
|
298
|
+
a line reads (`'level'` thick, `'standard'` thin, `'redox'`, `'phi'`), `role` and `slot` keep a
|
|
299
|
+
species' colour stable (`themeColor(role, slot)` in `driftlet/plot` gives ours), labels mark
|
|
300
|
+
subscripts TeX-style (`labelParts(label)` splits them), `NaN` breaks a line, and doubled
|
|
301
|
+
interface nodes share an x (draw them as vertical steps). Or read the solution directly:
|
|
302
|
+
`sol.x`, `sol.V[name]`, `sol.Vstd[name]`, `sol.c[name]`, `sol.phi`; electron energies are
|
|
303
|
+
E = −V (eV) for both carriers' levels and band edges.
|
|
304
|
+
|
|
305
|
+
- **Which picture.** For a device of electrons and holes only, draw the familiar band diagram,
|
|
306
|
+
energy up: `bandDiagram(sol, { energy: true })` (E_c, E_v, E_Fn, E_Fp in eV). The species
|
|
307
|
+
voltages below are the same lines in volts, turned over; use them where ions share the diagram,
|
|
308
|
+
or for equilibrium between species, and don't present them in place of the band diagram.
|
|
309
|
+
|
|
275
310
|
- **Fermi level** V_e⁻ = −μ̄_e⁻/F: the circuit voltage, what a voltmeter reads. A cell's voltage is
|
|
276
311
|
the difference between its terminals' V_e⁻.
|
|
277
312
|
- **Redox level** of Ox + z e⁻ ⇌ Red, `level(sol, half)`: V_e⁻(Ox/Red) = (μ̄_Ox − μ̄_Red)/(zF), where
|