driftlet 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  **driftlet simulates charged species moving through one-dimensional devices**: semiconductor
12
12
  junctions and solar cells, electrochemical cells and batteries, double layers, membranes and
13
- nerves. It's a small JavaScript library with no dependencies, fast enough to solve live in a
13
+ nerves. It's a JavaScript library with no dependencies, fast enough to solve live in a
14
14
  web page as you drag a slider.
15
15
 
16
16
  [Live demos](https://markblundeberg.github.io/driftlet/demos/) · [npm](https://www.npmjs.com/package/driftlet) ·
@@ -56,7 +56,7 @@ import { bandDiagram } from 'driftlet/plot'; // a level diagram as an SVG str
56
56
  ```
57
57
 
58
58
  Or straight from a CDN in a page, pinned to a version:
59
- `https://cdn.jsdelivr.net/npm/driftlet@1.0.1/src/index.js` (and `src/kit.js`, `src/plot.js`).
59
+ `https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/index.js` (and `src/kit.js`, `src/plot.js`).
60
60
  TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
61
61
  [`llms.txt`](llms.txt).
62
62
 
package/docs/data.md CHANGED
@@ -21,6 +21,12 @@ double layers. The data are in `IONS`, and $`\mu^\circ`$ of liquid water is `H2O
21
21
  1.011e-14 at 25 °C (pK_w 13.995): neutral water holds 1.0055e-4 mol/m³ of each, which a bath
22
22
  with that reaction should hold too.
23
23
 
24
+ The data are 25 °C's, and the material says so (`T: 298.15`): a device at another temperature
25
+ warns that they don't hold there. Its μ° (so equilibrium constants and electrode potentials),
26
+ permittivity and diffusivities would all move: $`K_w`$ by ~6× between 6 and 25 °C. Scaling them
27
+ (μ° by the standard entropies, ε, D by water's viscosity) is planned, not done. Where only
28
+ transport between solutions of the same ions matters (a membrane's channels), μ° cancels.
29
+
24
30
  | Ion | z | D (10⁻⁹ m²/s) | $`\mu^\circ`$ (kJ/mol) |
25
31
  |---|---|---|---|
26
32
  | H+ | +1 | 9.311 | 0 |
@@ -53,16 +59,37 @@ with that reaction should hold too.
53
59
  Fe³⁺ and Fe²⁺ gives $`E^\circ(\mathrm{Fe}^{3+}/\mathrm{Fe}^{2+})`$ = 0.769 V, against the
54
60
  tabulated 0.771 V, which is what a Fe³⁺/Fe²⁺ redox demo needs.
55
61
 
56
- ## Semiconductors, 300 K
62
+ ## Semiconductors, 300 K and other temperatures
57
63
 
58
- `semiconductor(name, { material })` returns electrons `e-` and holes `h+` and the material:
59
- $`c_{\mathrm{ref}} = N_c`$ and $`N_v`$, D from the mobilities by the Einstein relation at 300 K,
64
+ `semiconductor(name, { material, T })` returns electrons `e-` and holes `h+` and the material:
65
+ $`c_{\mathrm{ref}} = N_c`$ and $`N_v`$, D from the mobilities by the Einstein relation at T,
60
66
  $`\mu^\circ_{\mathrm{e}^-} = 0`$ (so $`\phi`$ sits at the conduction band) and
61
- $`\mu^\circ_{\mathrm{h}^+} = E_g \cdot F`$. The data are for 300 K, so give the device `T: 300`.
62
- Statistics are ideal (Boltzmann); add Fermi–Dirac statistics to the material for degenerate
63
- doping. The data are in `SEMICONDUCTORS`, in the source's units (cm⁻³, cm²/(V·s), eV).
67
+ $`\mu^\circ_{\mathrm{h}^+} = E_g \cdot F`$. The table is for 300 K, the default; `T` (150 to 500 K)
68
+ moves it as [below](#at-other-temperatures). Statistics are ideal (Boltzmann); add Fermi–Dirac
69
+ statistics to the material for degenerate doping. The data are in `SEMICONDUCTORS`, in the
70
+ source's units (cm⁻³, cm²/(V·s), eV). In the material they're in SI: $`N_c`$ and $`N_v`$ are the
71
+ `cRef` of e⁻ and h⁺ in mol/m³, at `materials.Si.species['e-'].cRef` and `['h+'].cRef` (the
72
+ `species` list carries only names and charges).
73
+
74
+ **Give the device the same `T`.** The material's gap, $`N_c`$, $`N_v`$ and D are for the
75
+ temperature it was made at, and it carries that temperature as `materials.Si.T`. A device at
76
+ another temperature warns, in `sol.warnings` (so `check()`) and `describe()`, naming both
77
+ temperatures. The device's own default is 298.15 K (25 °C, the ions' temperature), not the
78
+ table's 300 K, so a semiconductor device says `T: 300`: at 298.15 K the 300 K data's
79
+ intrinsic density is 13% low. A material written by hand has no `T` and is never warned about.
80
+
81
+ To change the temperature of a live device, give it the material for the new T in the same
82
+ `set()`. The state carries over as a warm start:
83
+
84
+ ```js nocheck
85
+ dev.set({ T: 350, materials: semiconductor('Si', { T: 350 }).materials });
86
+ ```
87
+
88
+ A `set({ T })` alone changes kT but not the material's data, and warns. (A material with a
89
+ `trap()` in it: remake it with `trap(semiconductor('Si', { T }), …)` the same way. The trap
90
+ takes its material's T.)
64
91
 
65
- | | $`\varepsilon_r`$ | $`E_g`$ (eV) | $`\chi`$ (V) | $`N_c`$ (cm⁻³) | $`N_v`$ (cm⁻³) | $`\mu_n`$ (cm²/(V·s)) | $`\mu_p`$ (cm²/(V·s)) |
92
+ | | $`\varepsilon_r`$ | $`E_g`$ (eV) | $`\chi`$ (eV) | $`N_c`$ (cm⁻³) | $`N_v`$ (cm⁻³) | $`\mu_n`$ (cm²/(V·s)) | $`\mu_p`$ (cm²/(V·s)) |
66
93
  |---|---|---|---|---|---|---|---|
67
94
  | Si | 11.9 | 1.12 | 4.05 | 2.8 × 10¹⁹ | 1.04 × 10¹⁹ | 1500 | 450 |
68
95
  | Ge | 16.0 | 0.66 | 4.0 | 1.04 × 10¹⁹ | 6.0 × 10¹⁸ | 3900 | 1900 |
@@ -70,14 +97,108 @@ doping. The data are in `SEMICONDUCTORS`, in the source's units (cm⁻³, cm²/(
70
97
 
71
98
  - Source: Sze, *Physics of Semiconductor Devices*, 2nd ed. (1981), appendix "Properties of Ge,
72
99
  Si and GaAs at 300 K". The mobilities are for lightly doped material.
73
- - The electron affinity $`\chi`$ is for vacuum-level estimates with `vacuumDipole` (anchor `'e-'`,
74
- see the [alignment guide](alignment.md)). It isn't used otherwise, since an alignment is never
75
- implied.
100
+ - The electron affinity $`\chi`$ (an energy, in eV) is for vacuum-level estimates with
101
+ `vacuumDipole` (anchor `'e-'`, `offset: chi`, the same number in volts; see the
102
+ [alignment guide](alignment.md)). It isn't used otherwise, since an alignment is never implied.
76
103
  - **Silicon's intrinsic concentration**: these $`N_c`$, $`N_v`$ and $`E_g`$ give $`n_i`$ = 6.7 × 10⁹ cm⁻³.
77
104
  Sze quotes 1.45 × 10¹⁰ (an older measurement), and the accepted value is 9.65 × 10⁹. The
78
105
  library keeps the band data self-consistent rather than matching a quoted $`n_i`$. For Ge and
79
106
  GaAs, the data reproduce the quoted $`n_i`$ (2.4 × 10¹³ and 1.79 × 10⁶) to within 10–15%.
80
107
 
108
+ ### At other temperatures
109
+
110
+ With `T`, each 300 K number moves by a standard temperature law:
111
+
112
+ - **Band gap**, by Varshni's form $`E_g(T) = E_{g0} - \alpha T^2/(T + \beta)`$ (Varshni, Physica 34,
113
+ 149, 1967), with Thurmond's parameters as tabulated by Sze (Thurmond, J. Electrochem. Soc. 122,
114
+ 1133, 1975). Only the shift from 300 K is used, on the table's gap,
115
+ $`E_g(T) = E_g^{\text{table}} + [E_g^{\text{V}}(T) - E_g^{\text{V}}(300\ \mathrm{K})]`$, so that 300 K is the
116
+ table exactly. Varshni's own value at 300 K differs from the table by a few meV.
117
+ - **$`N_c`$ and $`N_v`$** $`\propto T^{3/2}`$, from parabolic bands with fixed effective masses.
118
+ - **Mobilities**, by the power laws for scattering by lattice vibrations,
119
+ $`\mu \propto T^{-\gamma}`$ (Sze, ch. 1). They are approximate and hold for lightly doped material.
120
+ Where scattering by ionized impurities matters (heavy doping, or the cold end of the range), the real
121
+ mobility is lower and rises less on cooling. D is then $`\mu kT/q`$ at T, so it falls more slowly
122
+ than μ.
123
+ - **Electron affinity** $`\chi`$ and **$`\varepsilon_r`$** stay at their 300 K values.
124
+
125
+ | | $`E_{g0}`$ (eV) | $`\alpha`$ (10⁻⁴ eV/K) | $`\beta`$ (K) | Varshni at 300 K (eV) | $`\gamma_n`$ | $`\gamma_p`$ |
126
+ |---|---|---|---|---|---|---|
127
+ | Si | 1.170 | 4.73 | 636 | 1.1245 | 2.42 | 2.20 |
128
+ | Ge | 0.7437 | 4.774 | 235 | 0.6634 | 1.66 | 2.33 |
129
+ | GaAs | 1.519 | 5.405 | 204 | 1.4225 | 1.0 | 2.1 |
130
+
131
+ Outside 150–500 K the call is refused: Varshni's fits and the lattice-limited mobilities are
132
+ not meant for the cold, where impurities and freeze-out take over. With these laws, silicon's
133
+ $`n_i`$ grows 8.3% per kelvin near room temperature. In the tests, a silicon cell's
134
+ open-circuit voltage falls 2.14 mV/K at 300 K. That lies within Green's
135
+ $`\mathrm{d}V_{oc}/\mathrm{d}T = -(E_{g0}/q - V_{oc} + \gamma kT/q)/T`$ for the 1.5 ≤ γ ≤ 3 that
136
+ its transport allows, and the γ = 1.67 it implies matches its two parts of $`J_0`$.
137
+
138
+ **A diode's saturation current** follows from these laws. With $`n_i^2 \propto T^3 e^{-E_g/kT}`$
139
+ and $`D = \mu kT/q \propto T^{1-\gamma}`$ ($`\gamma`$ of the minority carrier that dominates),
140
+ a short base, $`J_0 \propto n_i^2 D/W`$, gives
141
+
142
+ ```math
143
+ J_0 \propto T^{3 + 1 - \gamma}\, e^{-E_g(T)/kT}, \qquad E_a = -k\,\frac{\mathrm{d}\ln J_0}{\mathrm{d}(1/T)} = E_g - T\frac{\mathrm{d}E_g}{\mathrm{d}T} + (4 - \gamma)\,kT .
144
+ ```
145
+
146
+ So an Arrhenius plot of $`J_0`$ doesn't read the gap. $`E_g - T\,\mathrm{d}E_g/\mathrm{d}T`$ is
147
+ the gap extrapolated linearly to 0 K, 1.196 eV for silicon at 300 K, and the prefactor adds
148
+ $`(4-\gamma)kT`$, 0.041 eV for its electrons, about 1.24 eV in all. A long base,
149
+ $`J_0 \propto n_i^2\sqrt{D/\tau}`$, has $`(1-\gamma)/2`$ in place of $`1-\gamma`$, with τ as you
150
+ give it (the library doesn't move it with T).
151
+
152
+ **Hot, lightly doped junctions disappear.** $`n_i`$ grows fast: silicon's is 3 × 10¹³ cm⁻³ at
153
+ 450 K and 1.6 × 10¹⁴ at 500 K, germanium's 6 × 10¹⁵ at 450 K and 2 × 10¹⁶ at 500 K. Where $`n_i`$
154
+ approaches the doping, both sides go intrinsic and the junction (its built-in voltage, its
155
+ rectification) fades away. Compare $`n_i(T)`$ with the doping before reading a trend in V(T)
156
+ across that range.
157
+
158
+ ### Alloys and other materials from these
159
+
160
+ A material the library lacks can start from a near relative's entry, with its own numbers put
161
+ in. For example, Al₀.₃Ga₀.₇As from GaAs: give the holes' `mu0` the alloy's gap ($`E_g F`$), and set
162
+ whatever else differs, such as the mobilities and $`N_c`$, $`N_v`$. A field you don't set keeps
163
+ GaAs's value, its electron mobility of 8500 cm²/(V·s) included. Then give each face between the
164
+ two its band offset as a `step` on e⁻, $`\Delta E_c`$ right minus left (the valence-band offset
165
+ follows from the two gaps). `semiconductor(name, { T })` moves only the named material's own data,
166
+ by its laws: numbers you put in stay as given, so at another temperature move them yourself. The
167
+ material still carries the `T` it was made at, for the warning.
168
+
169
+ ```js
170
+ import { Device, units, GAS_CONSTANT, FARADAY } from 'driftlet';
171
+ import { build, layer, ohmic, semiconductor } from 'driftlet/kit';
172
+
173
+ // The usual estimates for x = 0.3 (E_g = 1.424 + 1.247x eV, ΔE_c ≈ 0.62 ΔE_g,
174
+ // m_e = 0.067 + 0.083x); check them against your source.
175
+ const T = 300, VT = (GAS_CONSTANT * T) / FARADAY, x = 0.3;
176
+ const gap = 1.424 + 1.247 * x, dEc = 0.62 * (gap - 1.424);
177
+ const alloy = semiconductor('GaAs', { material: 'AlGaAs', T }), m = alloy.materials.AlGaAs;
178
+ m.species['h+'] = { ...m.species['h+'], mu0: units.eV(gap) };
179
+ m.species['e-'] = {
180
+ ...m.species['e-'],
181
+ D: 1000e-4 * VT, // μ_n = 1000 cm²/(V·s), a rough figure
182
+ cRef: m.species['e-'].cRef * ((0.067 + 0.083 * x) / 0.067) ** 1.5, // N_c ∝ m_e^1.5
183
+ };
184
+ // A heterojunction diode, n-AlGaAs | p-GaAs, forward biased.
185
+ const dev = new Device(build({
186
+ T,
187
+ library: [semiconductor('GaAs', { T }), alloy],
188
+ stack: [
189
+ ohmic(0), layer('AlGaAs', 1e-6, { donors: units.perCm3(1e17) }),
190
+ { step: { species: 'e-', value: units.eV(-dEc) } }, // GaAs's conduction band is lower
191
+ layer('GaAs', 1e-6, { acceptors: units.perCm3(1e17) }), ohmic(1.0),
192
+ ],
193
+ bulkReactions: [{ equation: 'e- + h+ = 0', kf: { GaAs: 1e8, AlGaAs: 1e8 } }],
194
+ }));
195
+ const sol = dev.solve();
196
+ // The wider gap keeps holes out of the n side: nearly all the current is electrons injected
197
+ // into the GaAs.
198
+ const { 'e-': e, 'h+': h } = sol.contacts.left.flux;
199
+ console.log(sol.warnings.length, (e / -h).toExponential(1)); // 0, 2.5e+7
200
+ ```
201
+
81
202
  ## Metals, 20 °C
82
203
 
83
204
  `metal(name, { material })` returns a conductor material carrying `e-`, with its conductivity
package/docs/device.md CHANGED
@@ -15,7 +15,7 @@ too (`FARADAY`, `GAS_CONSTANT`, `EPS0`, …).
15
15
 
16
16
  | Key | Meaning |
17
17
  |---|---|
18
- | `T` | temperature, K (default 298.15) |
18
+ | `T` | temperature, K (default 298.15, 25 °C; the library's semiconductors are for 300 K, so give them `T: 300`) |
19
19
  | `species` | list of mobile species |
20
20
  | `materials` | named materials: bulk properties |
21
21
  | `regions` | the device, left to right: each a material plus length, fixed charge, composition |
@@ -55,6 +55,11 @@ Per species present: `D` (m²/s, the coefficient in the flux $`N = -(D c/RT)\nab
55
55
  (standard chemical potential, J/mol) and `cRef` (the concentration that `mu0` refers to; falls back
56
56
  to the species' `cRef`, and must exist one way or the other). Species not listed are absent.
57
57
 
58
+ A material may say what temperature its numbers are for, `T` (K). `semiconductor()` sets it.
59
+ The device then warns, in its solutions' `warnings`, if that isn't its own `T` (to 0.01 K), since
60
+ the material's gap, densities of states and D are then another temperature's. A material without
61
+ `T` is taken as written.
62
+
58
63
  In a material, only *neutral combinations* of standard potentials mean anything physically.
59
64
  Shifting every `mu0` by $`z_i F s`$ just moves the material's $`\phi`$, its bookkeeping anchor. See
60
65
  [conventions](conventions.md).
@@ -245,7 +250,9 @@ r = \frac{n\,p\,(1 - e^{-A/RT})}{(n + n_1)/v_p + (p + p_1)/v_n}, \qquad p_1 = \f
245
250
  with n and p each at its own side's edge node, the capture velocities $`v_n`$ and $`v_p`$ in m/s,
246
251
  and $`n_1`$ (mol/m³) the negative species' concentration with its level at the trap (default:
247
252
  $`n_1 = p_1 = n_i`$, a midgap trap). Taking $`p_1`$ from the state keeps equilibrium exact whatever
248
- lies between the two sides (a band offset, a $`\phi`$ jump). Where one carrier is plentiful the
253
+ lies between the two sides (a band offset, a $`\phi`$ jump). With Fermi–Dirac carriers, each
254
+ one's emission scales by its own $`\gamma = \mathcal{F}_{1/2}(\zeta)/e^\zeta`$, by detailed balance:
255
+ $`n_1`$ becomes $`\gamma_n n_1`$, and $`p_1`$ keeps the product. Where one carrier is plentiful the
249
256
  rate saturates at the other's capture, $`r \to v_p\,p`$: holes reaching an electron-rich layer
250
257
  recombine at $`v_p`$ however many electrons wait there, which plain mass action can't do.
251
258
 
@@ -418,7 +425,13 @@ those materials.
418
425
  none (`'e- + h+ = 0'`):
419
426
  $`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
420
427
  for [interface SRH](#interfaces) (default midgap). It's what explicit trap species reduce to when
421
- the traps are few and fast, without them.
428
+ the traps are few and fast, without them. Where their charge matters, write them out: two
429
+ immobile species (D = 0) for the trap's charge states, filled and emptied by
430
+ `'e- + X0 = X-'` and `'X- + h+ = X0'` (acceptor-like) at rates $`k_n = 1/(\tau_n N_t)`$ and
431
+ $`k_p = 1/(\tau_p N_t)`$, the filled state's $`\mu^\circ`$ an electron's $`\mu`$ at $`n_1`$ above the
432
+ empty one's. In steady state they recombine at the rate above exactly, each node keeps its
433
+ own trap total, and the trapped charge is in Poisson's equation. The kit's
434
+ [`trap()`](kit.md#a-device-as-a-stack) writes them from the same `{ tauN, tauP, n1 }` and a density.
422
435
 
423
436
  ```js nocheck
424
437
  bulkReactions: [{ equation: 'e- + h+ = 0', srh: { Si: { tauN: 1e-6, tauP: 1e-6 } } }]
@@ -1000,6 +1013,9 @@ const now = dev.solution(); // snapshot of the current state
1000
1013
  mid-run, `set({ contacts: { right: { bath: { c } } } })`, is a step in the sample.) Either way the time stepping restarts its
1001
1014
  order, as after any discontinuity, but a carried state goes on at the step size an adaptive
1002
1015
  transient had reached, so a page may change a rate constant or a diffusivity every frame.
1016
+ To change the temperature, give the materials for it in the same patch,
1017
+ `dev.set({ T: 350, materials: semiconductor('Si', { T: 350 }).materials })`: a `T` alone
1018
+ changes kT but not the materials' data (a material that carries its `T` then warns).
1003
1019
 
1004
1020
  For example, a silver nitrate cell between silver electrodes: its impedance spectrum, then the
1005
1021
  current transient after a voltage step.
@@ -1047,7 +1063,7 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
1047
1063
  | `bulkReactions[k]` | `{ rate, forward, nu, regions, total }`: each bulk reaction's net forward rate at every node (mol/(m³·s), `NaN` where it doesn't run), its one-way forward rate (the backward is `forward − rate`), what one forward reaction makes of each species (`nu`, negative for what it consumes), 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 |
1048
1064
  | `charge` | total charge in the device, C/m² (C through a cross-section) |
1049
1065
  | `conservation` | per species stretch: amount, reference, intake through contacts, and drift, relative: (amount − reference − intake) over the largest of them |
1050
- | `warnings` | e.g. unresolved double layers, conventions a statistics model relies on, and for a failed solve, where the system is nearly singular |
1066
+ | `warnings` | e.g. unresolved double layers, conventions a statistics model relies on, a material's data for another temperature, and for a failed solve, why (a driven current it can't pass, where the system is nearly singular). A failed steady solve's state isn't a solution, so the warnings about a solution's accuracy (the grid, double layers, space charge reaching a contact) are left out of it |
1051
1067
  | `converged`, `iterations`, `steps`, `substeps`, `history`, `time`, `T` | solver bookkeeping, and the temperature |
1052
1068
  | `steady` | whether this is a converged steady state (from `solve()`), not a transient's: what `check()` in the kit decides its checks by |
1053
1069
  | `done`, `rejected`, `trace` | from `advance()`: whether `tEnd` was reached; rejected steps; `{ t, current, voltage }` per accepted step |
package/docs/kit.md CHANGED
@@ -68,6 +68,15 @@ console.log(`I = ${sol.current.toFixed(1)} A/m²`);
68
68
  - **`library`** takes pieces with `species` and `materials`, merged with any given directly
69
69
  (`combine()` does the merging on its own). A species or material given twice must be given
70
70
  identically.
71
+ - **`trap(lib, { density, tauN, tauP, n1, charge, name, material, T })`** adds a trap level that
72
+ holds charge to a semiconductor's library piece: its two states as immobile species
73
+ (`X0`/`X-` for `charge: 'acceptor'`, the default; `X+`/`X0` for `'donor'`), filled and emptied
74
+ by capturing electrons and holes. It returns `{ library, bulkReactions, c0 }`: the library to
75
+ give `build()` in place of `lib`, the two capture reactions, and the trap's total for each
76
+ layer that has it (`layer('Si', L, { donors, c0: t.c0 })`). In steady state it recombines at the
77
+ `srh` law's rate with the same `tauN`, `tauP` (s, at that density) and `n1` (mol/m³, default
78
+ n_i); unlike `srh`, its charge is in Poisson's equation
79
+ ([device](device.md#bulk-reactions)).
71
80
  - Nested lists in the stack are flattened, so a helper can return several items.
72
81
  - Reactions pass through as written (see below), and so do `T`, `grid`, `ports` and `geometry`.
73
82
 
@@ -395,7 +404,11 @@ warnings:
395
404
  `warnings` check this again against the solved concentrations);
396
405
  - an outside (a bath, a contact's levels) out of equilibrium with a bulk reaction that runs at the
397
406
  contact, which would run there for ever: H⁺ and OH⁻ in a bath a little off $`K_w`$ pass a current
398
- at zero bias.
407
+ at zero bias;
408
+ - the definition's own warnings, which every solution repeats: a material whose data are for
409
+ another temperature than the device's (`semiconductor('Si')` is 300 K's; the device's default
410
+ is 298.15 K), a starting profile the grid doesn't resolve, conventions a statistics model
411
+ relies on.
399
412
 
400
413
  They're heuristics, prompts to check, never errors.
401
414
 
@@ -424,14 +437,17 @@ page can show that its numbers hold up. Pass the solution `solve()` or `advance(
424
437
 
425
438
  - **converged**: the solve converged.
426
439
  - **warnings**: the solution's warnings (unresolved double layers, steep profiles, a weakly held
427
- part) and the definition's likely unit slips. They're heuristics, so they're marked `?`, a
440
+ part, a material's data for another temperature) and the definition's likely unit slips. They're heuristics, so they're marked `?`, a
428
441
  prompt to look, and don't fail the check.
429
442
  - **balance**, in a steady state: each species' ledger, what comes in through each terminal and
430
443
  what each reaction (bulk, or at a face) makes or uses, in mol/(m²·s) and for a charged species
431
444
  as a current. The terms must sum to zero. A species that a face lets through only by a law (a
432
445
  membrane's permeability, a conductance) or by a reaction has a ledger on each side, with what
433
446
  crosses as a term: in a cell, Na⁺ leaks into the cytoplasm as fast as the pump puts it out. Under light it's J = F(G − R), and it says where
434
- every carrier went: collected, or recombined in the bulk or at which face. A species at rest
447
+ every carrier went: collected, or recombined in the bulk or at which face. Each reaction is a
448
+ term named by its equation; two with the same equation (radiative and SRH recombination, both
449
+ `e- + h+ = 0`) are told apart by their place, `e- + h+ = 0 (bulkReactions[1])`, and
450
+ `sol.bulkReactions[k]` has each one's rates. A species at rest
435
451
  (in equilibrium, flows only of round-off against what it could carry) isn't listed.
436
452
  - **conservation**, in a transient: every closed, unreacting stretch keeps what it held plus
437
453
  what came in.
package/docs/numerics.md CHANGED
@@ -450,8 +450,8 @@ The assembled matrix loses what a flux's two entries share. A Scharfetter–Gumm
450
450
  $`\eta`$ at its two ends through $`G`$ and $`-G(1 + E)`$, with $`E = \mathrm{expm1}(\Delta\eta)`$;
451
451
  for a nearly uniform $`v`$ they cancel, and $`J \cdot v`$ keeps only round-off of $`G|v|`$, which in
452
452
  an inversion layer's 0.1 nm cells ($`G`$ ~ 1e11) dwarfs the flux. Where a solve needs
453
- $`J \cdot v`$ itself (GMRES, in the impedance and in Newton's refined solves), the dilute kernels'
454
- terms are kept aside instead, each evaluated on differences first:
453
+ $`J \cdot v`$ itself (GMRES, in the impedance and in Newton's refined solves), the kernels' terms
454
+ are kept aside instead, each evaluated on differences first:
455
455
 
456
456
  ```math
457
457
  \delta N = a\,(\delta\eta_L - z\,\delta\hat\phi_L) + b\,(\delta\eta_R - \delta\eta_L) + d\,(\delta\hat\phi_R - \delta\hat\phi_L),
@@ -459,8 +459,13 @@ terms are kept aside instead, each evaluated on differences first:
459
459
 
460
460
  with $`a = -GE`$ (through $`c_L`$), $`b = -G(1 + E)`$ and $`d`$ the drift's. A node's storage and space
461
461
  charge depend on $`\delta\eta_i - z_i\,\delta\hat\phi`$ alone, the displacement on
462
- $`\delta\hat\phi_R - \delta\hat\phi_L`$. Everything else (faces, contacts, ports, reactions,
463
- concentrated and metal regions) is assembled into a matrix of its own. Rows that a contact or a
462
+ $`\delta\hat\phi_R - \delta\hat\phi_L`$. Non-ideal statistics couple the species ($`c_i`$
463
+ depends on every $`\zeta_j`$), so there a node's storage of $`i`$ has a term in each
464
+ $`\delta\eta_j - z_j\,\delta\hat\phi`$, through $`K_{ij}`$, and a flux has side terms in those at
465
+ either end (through $`c_L`$ and the excess potentials in its drift); each is in the same
466
+ differences. (Until 1.1.1 they were assembled directly, and a Fermi–Dirac MOS capacitor in
467
+ inversion, barely degenerate, came out with a negative resistance.) Everything else (faces,
468
+ contacts, ports, reactions, metal regions) is assembled into a matrix of its own. Rows that a contact or a
464
469
  steady solve replaces drop the kept terms; the transformed rows of strictly neutral nodes combine
465
470
  them as they combine the matrix's. A region's uniform shift ($`z_i s`$ on each $`\eta_i`$, $`s`$ on
466
471
  $`\hat\phi`$) then gives exactly zero inside it, as it must. It replaced a central difference of
@@ -649,7 +654,7 @@ majority conductances times a $`\delta\eta`$ cancelling its neighbours' to 1e-17
649
654
  round-off reads as a constant conductance of order 1 S/m², a silicon p⁺n⁺ junction's capacitance
650
655
  9× off at 100 Hz. Between the two contacts of a device without ports, the total current
651
656
  (conduction plus displacement, $`i\omega\,\delta D`$) is the same through every cut, so it is also
652
- read across each segment of an ideal region, linearised as the Scharfetter–Gummel flux is, and
657
+ read across each segment of a region (with any statistics), linearised as its flux is, and
653
658
  the reading with the least estimated error is taken: in that junction, the depletion region's;
654
659
  in a MOS capacitor, the oxide's displacement. A reading's error is its terms' round-off, and the
655
660
  solves' error in $`\delta x`$ (their residual, relative to its largest entry) through its
@@ -58,6 +58,7 @@ recipes.
58
58
  | 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 |
59
59
  | 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) |
60
60
  | Illuminated long pn diode | `J_sc = qG(L_n + L_p + W)` from a cold start; superposition at low injection | 5e-3; 1e-2 |
61
+ | A silicon cell's V_oc against temperature (`semiconductor('Si', { T })`) | Green's dV_oc/dT = −(E_g0/q − V_oc + γkT/q)/T for 1.5 ≤ γ ≤ 3 (−2.12 to −2.25 mV/K); the γ it implies against its two parts of J₀, 1.66; n_i's growth, 8.3%/K, against 3/2T + E_g0/2kT² | within the band (−2.14 found); ±0.1 (1.67 found); 1e-4 |
61
62
  | Schottky barrier (metal region \| n-Si) | surface density from the alignment; depletion charge | 5e-3; 2% |
62
63
  | Surface velocity at a contact | minority electrons through a base to a contact of velocity S, n₀(e^(V/V_T) − 1)/(W/D + 1/S), S from 10 to 1e5 m/s; a Schottky diode's thermionic emission, short of A*T²e^(−φ_B/V_T)(e^(V/V_T) − 1) by its depletion region in series, the same swept as built cold | 1e-5; 3–7%; 1e-6 |
63
64
  | Charged pore or nanochannel (resolved Donnan layers at each mouth) | conductance against salt, down to its plateau, as Teorell–Meyer–Sievers in a long pore (short ones short by end layers ∝ 1/L); the zero-current voltage across a salt gradient; a symmetric pore's I–V odd, rectification with charge on one side or bipolar | 1%; 5e-3 to 1e-4; 1e-9 |
@@ -172,9 +173,6 @@ so a change that makes one worse shows.
172
173
  - **A surface covered to a bare fraction below ~1e-10** barely affects anything any more, and
173
174
  the solver struggles with it (the hard corpus's passivating drop without a double layer stops
174
175
  partway through its hour); the solution warns, and suggests a less stable film.
175
- - **A double layer across more than ~350 thermal voltages** (between 9 and 9.25 V at 25 °C, in a
176
- crowded lattice-gas electrolyte) doesn't solve, from a cold start or warm, and its warning
177
- doesn't say why. Ionic-liquid windows (a few volts) are well inside it.
178
176
  - **Currents below ~0.1 nA/m²** (a GaAs junction's leakage without recombination) aren't resolved
179
177
  between solves; such a current is zero for any practical purpose.
180
178
  - **A failed solve says so:** `converged: false`, with a warning naming where the system lost
package/docs/templates.md CHANGED
@@ -214,7 +214,7 @@ for (let k = 1; k <= 300; k++) {
214
214
  const sol = axon.advance(k * 1e-4); // 0.1 ms at a time
215
215
  peak = Math.max(peak, sol.interfaces[0].V); // V_m, and sol.interfaces[0].gates = { m, h, n }
216
216
  }
217
- 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)
217
+ console.log(`peak ${(peak * 1e3).toFixed(1)} mV`); // +12.6: GHK's currents rectify; matched at rest, at positive V Na⁺'s conductance is lower and K⁺'s higher (linear: true gives +37.9, HH's own)
218
218
  ```
219
219
 
220
220
  ## an action potential travelling along an axon
package/llms.txt CHANGED
@@ -30,9 +30,9 @@ materials, insertion hosts), data.md and reliability.md (what's validated, and t
30
30
  From a CDN, pin the version you tested against:
31
31
 
32
32
  ```js nocheck
33
- import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0.1/src/index.js';
34
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0.1/src/kit.js';
35
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0.1/src/plot.js';
33
+ import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/index.js';
34
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/kit.js';
35
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/plot.js';
36
36
  ```
37
37
 
38
38
  From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`. Every export is listed at the end.
@@ -73,12 +73,16 @@ From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`. Every export is listed at
73
73
  are regions (give one a `name` to refer to it), and any other object between two layers is
74
74
  the face between them. `ohmic()`, `bath()` and the like write contacts; a plain object works.
75
75
  - **The library** is pieces with `species` and `materials`: `semiconductor('Si')` (Si, Ge, GaAs, at
76
- 300 K), `aqueous(names)` (H⁺ Li⁺ Na⁺ K⁺ Ag⁺ Cu²⁺ Zn²⁺ Fe²⁺ Fe³⁺ OH⁻ Cl⁻ NO₃⁻ SO₄²⁻ in water, at 25
76
+ 300 K; `semiconductor('Si', { T: 350 })` moves them, 150–500 K, for a device at that `T`),
77
+ `aqueous(names)` (H⁺ Li⁺ Na⁺ K⁺ Ag⁺ Cu²⁺ Zn²⁺ Fe²⁺ Fe³⁺ OH⁻ Cl⁻ NO₃⁻ SO₄²⁻ in water, at 25
77
78
  °C; `{ epsr: 0 }` makes it strictly neutral, with no double layers), `metal('Ag')` (Ag Cu Au Al Zn
78
79
  Li Pt), all from `driftlet/kit`. A semiconductor's textbook numbers are in `SEMICONDUCTORS.Si`
79
80
  (`Eg` and `chi` in eV, `Nc` and `Nv` in cm⁻³, `mun` and `mup` in cm²/(V·s), `epsr`); in the
80
- material, e⁻'s and h⁺'s `cRef` are N_c and N_v in mol/m³ and h⁺'s `mu0` is the gap (e⁻'s is 0),
81
- so n_i = √(N_c N_v) e^(−E_g/2RT), 6.7e9 cm⁻³ for Si. A species it lacks is pushed into both lists
81
+ material, `materials.Si.species['e-'].cRef` and `['h+'].cRef` are N_c and N_v in mol/m³ and
82
+ h⁺'s `mu0` is the gap (e⁻'s is 0), so n_i = √(N_c N_v) e^(−E_g/2RT), 6.7e9 cm⁻³ for Si. The
83
+ material carries the T its data are for (`materials.Si.T`), and a device at another T warns
84
+ (its default is 298.15 K, so say `T: 300`). An alloy starts from a relative's entry with your
85
+ numbers put in (docs/data.md has AlGaAs from GaAs). A species it lacks is pushed into both lists
82
86
  (`z: 0` for a neutral one): `lib.species.push({ name: 'A-', z: -1, cRef: 1000 })`,
83
87
  `lib.materials.water.species['A-'] = { D, mu0 }`. Only μ° differences within a reaction matter:
84
88
  for a redox couple μ°_red − μ°_ox = −F·E° (against SHE, the library's μ°_H⁺ being 0). An
@@ -115,7 +119,11 @@ From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`. Every export is listed at
115
119
  bimolecular, as is the kit's `recombination()`, it shortens at high injection. SRH is `srh: { Si:
116
120
  { tauN, tauP } }` in place of `kf` (s), or at a face `{ equation: 'e-(left) + h+(right) = 0',
117
121
  srh: { vn, vp } }` (m/s); with τ_N ≪ τ_p it gives n-type silicon's holes a lifetime τ_p at any
118
- injection. Light is a reaction from a reservoir (`photon = e- + h+`, uniform), or absorbed as
122
+ injection. `srh` carries no charge; for traps whose charge matters (dense or deep ones, trapped
123
+ charge bending the bands), `trap(semiconductor('Si'), { density, tauN, tauP, n1, charge:
124
+ 'acceptor' })` writes the trap's two states as immobile species and returns `{ library,
125
+ bulkReactions, c0 }` (c0 into each layer that has the trap): the same rate in steady state, its
126
+ charge in Poisson. Light is a reaction from a reservoir (`photon = e- + h+`, uniform), or absorbed as
119
127
  it goes in, `photogeneration({ material, flux, alpha, mu: units.eV(3), from, to })`, `from` and
120
128
  `to` being x in the device (a port's window is instead m from its region's left end). To switch
121
129
  it during a run, `set()` the device's whole `bulkReactions` list, recombination included: in
@@ -168,7 +176,7 @@ import { Device, units } from 'driftlet';
168
176
  import { build, layer, ohmic, semiconductor, describe } from 'driftlet/kit';
169
177
 
170
178
  const def = build({
171
- T: 300, // the library's semiconductor data are for 300 K
179
+ T: 300, // the library's semiconductor data are for 300 K, unless semiconductor('Si', { T })
172
180
  library: [semiconductor('Si')],
173
181
  stack: [
174
182
  ohmic(0), // e⁻ and h⁺ in equilibrium with a metal at 0 V
@@ -176,7 +184,9 @@ const def = build({
176
184
  layer('Si', units.um(1), { name: 'p', acceptors: units.perCm3(1e16) }),
177
185
  ohmic(0.5), // the p side at +0.5 V: forward bias
178
186
  ],
179
- bulkReactions: [{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }], // recombination: τ = 1/(kf·N) ≈ 0.6 µs at N = 1e16 cm⁻³
187
+ // recombination: τ = 1/(kf·N) ≈ 0.6 µs at N = 1e16 cm⁻³, so L = √(Dτ) ≈ 50 µm ≫ 1 µm: a short
188
+ // diode, whose Shockley J₀ takes each side's quasi-neutral width (W − x_d) in place of L
189
+ bulkReactions: [{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }],
180
190
  grid: { hmin: units.nm(1), hmax: units.nm(20) },
181
191
  });
182
192
  console.log(describe(def));
@@ -209,9 +219,9 @@ a CDN needs a long virtual-time budget, 30 s.)
209
219
  <span id="readout"></span>
210
220
  <div id="figure"></div>
211
221
  <script type="module">
212
- import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0.1/src/index.js';
213
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0.1/src/kit.js';
214
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0.1/src/plot.js';
222
+ import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/index.js';
223
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/kit.js';
224
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/plot.js';
215
225
 
216
226
  const def = build({
217
227
  T: 300,
@@ -641,8 +651,13 @@ those:
641
651
  - **Unit slips.** D in cm²/s, μ° in eV, µF/cm², mS/cm², T in °C. `describe(def)` flags them.
642
652
  - **A couple's μ° with the wrong sign** solves happily at −E° for +E°: μ°_red − μ°_ox = −F·E°.
643
653
  Check the rest potential against E°, as the stirred-solution template does.
644
- - **The library's data are for 25 °C** (its semiconductors for 300 K: give `T: 300`). At body
645
- temperature the same μ° and D are a fair start; D rises about 2% per kelvin in water.
654
+ - **The library's data are for 25 °C** (its semiconductors for 300 K: give `T: 300`, or the
655
+ same `T` to `semiconductor(name, { T })`, which moves the gap, N_c, N_v and the mobilities; a
656
+ mismatch warns). On a live device change both at once, `dev.set({ T, materials:
657
+ semiconductor('Si', { T }).materials })`. A diode's J₀ ∝ T^(4−γ) e^(−E_g/kT), so its Arrhenius
658
+ slope is E_g − T dE_g/dT + (4−γ)kT (≈ 1.24 eV for Si), not E_g. Hot and lightly doped, n_i
659
+ passes the doping (Ge 6e15 cm⁻³ at 450 K) and the junction fades: compare before fitting V(T).
660
+ At body temperature the same μ° and D are a fair start; D rises about 2% per kelvin in water.
646
661
  - **A steady solve of a closed cell where every ion can cross**: its steady state is complete
647
662
  mixing. To watch a junction or a salt bridge, `advance()` from the unmixed `c0`.
648
663
  - **A sharp junction between very different solutions** (3 M KCl against 1 µM) can't start:
@@ -663,7 +678,7 @@ those:
663
678
  - `driftlet/kit`:
664
679
  - building: `build`, `combine`, `layer`, `ohmic`, `bath`, `half`, `level`, `SHE`,
665
680
  `vacuumLevel`, `vacuumDipole`, `vacuumZeroCharge`, and the data library (`aqueous`,
666
- `semiconductor`, `metal`, `IONS`, `SEMICONDUCTORS`, `METALS`, `H2O`, `WATER_EPSR`);
681
+ `semiconductor`, `metal`, `trap`, `IONS`, `SEMICONDUCTORS`, `METALS`, `H2O`, `WATER_EPSR`);
667
682
  - sources: `pulse`, `square`, `triangle`, `ramp`, `injector`, `recombination`,
668
683
  `photogeneration`;
669
684
  - channels: `hodgkinHuxley`, `ghkCurrent`;
@@ -674,4 +689,4 @@ those:
674
689
  `particles`;
675
690
  - curated devices: `perovskiteCell`, `PEROVSKITE_SCANS`, `hysteresis`.
676
691
  - `driftlet/plot` (a quick look): `bandDiagram`, `levelChart`, `labelParts`, `themeColor`,
677
- `THEME`.
692
+ `THEME`. These draw profiles in x; an I–V curve or a transient you draw yourself.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driftlet",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "Dependency-free 1D drift–diffusion–reaction (Poisson–Nernst–Planck) solver for semiconductors, electrochemistry and nerves, fast enough for live web demos",
5
5
  "type": "module",
6
6
  "exports": {
package/src/checks.js CHANGED
@@ -134,8 +134,12 @@ function balance(device, sol) {
134
134
  if (through('right', i)) add(i, last, 'right contact', -sol.contacts.right.flux[sp.name]);
135
135
  model.ports.forEach((port, k) => add(i, port.region, `port ${sol.ports[k].name}`, sol.ports[k].flux[sp.name]));
136
136
  });
137
+ // (two reactions with one equation, radiative and SRH recombination say, keep a line each,
138
+ // named by their place in the list)
139
+ const unique = (labels, where) => labels.map((w, k) => (labels.indexOf(w) === labels.lastIndexOf(w) ? w : `${w} (${where}[${k}])`));
140
+ const bulkWhat = unique(model.reactions.map((_, k) => label(def.bulkReactions?.[k])), 'bulkReactions');
137
141
  model.reactions.forEach((rx, k) => {
138
- const what = label(def.bulkReactions?.[k]);
142
+ const what = bulkWhat[k];
139
143
  sol.bulkReactions[k].regions.forEach((total, r) => {
140
144
  const all = [...rx.reactants, ...rx.products].map((p) => p.i);
141
145
  for (const { i, nu } of rx.reactants) add(i, r, what, -nu * total, all);
@@ -153,8 +157,9 @@ function balance(device, sol) {
153
157
  add(i, f, `across face ${f} (${type})`, -N);
154
158
  add(i, f + 1, `across face ${f} (${type})`, N);
155
159
  });
160
+ const faceWhat = unique(itf.reactions.map((_, k) => label(def.interfaces?.[f]?.reactions?.[k])), 'reactions');
156
161
  itf.reactions.forEach((rx, k) => {
157
- const rate = Af * sol.interfaces[f].rates[k], what = `${label(def.interfaces?.[f]?.reactions?.[k])} at face ${f}`;
162
+ const rate = Af * sol.interfaces[f].rates[k], what = `${faceWhat[k]} at face ${f}`;
158
163
  const all = rx.part.map((p) => p.i);
159
164
  for (const { i, nu, side } of rx.part) add(i, f + side, what, nu * rate, all);
160
165
  });