driftlet 1.0.0 → 1.1.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 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.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
59
+ `https://cdn.jsdelivr.net/npm/driftlet@1.1.0/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
@@ -53,16 +53,37 @@ with that reaction should hold too.
53
53
  Fe³⁺ and Fe²⁺ gives $`E^\circ(\mathrm{Fe}^{3+}/\mathrm{Fe}^{2+})`$ = 0.769 V, against the
54
54
  tabulated 0.771 V, which is what a Fe³⁺/Fe²⁺ redox demo needs.
55
55
 
56
- ## Semiconductors, 300 K
56
+ ## Semiconductors, 300 K and other temperatures
57
57
 
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,
58
+ `semiconductor(name, { material, T })` 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 T,
60
60
  $`\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).
61
+ $`\mu^\circ_{\mathrm{h}^+} = E_g \cdot F`$. The table is for 300 K, the default; `T` (150 to 500 K)
62
+ moves it as [below](#at-other-temperatures). Statistics are ideal (Boltzmann); add Fermi–Dirac
63
+ statistics to the material for degenerate doping. The data are in `SEMICONDUCTORS`, in the
64
+ source's units (cm⁻³, cm²/(V·s), eV). In the material they're in SI: $`N_c`$ and $`N_v`$ are the
65
+ `cRef` of e⁻ and h⁺ in mol/m³, at `materials.Si.species['e-'].cRef` and `['h+'].cRef` (the
66
+ `species` list carries only names and charges).
67
+
68
+ **Give the device the same `T`.** The material's gap, $`N_c`$, $`N_v`$ and D are for the
69
+ temperature it was made at, and it carries that temperature as `materials.Si.T`. A device at
70
+ another temperature warns, in `sol.warnings` (so `check()`) and `describe()`, naming both
71
+ temperatures. The device's own default is 298.15 K (25 °C, the ions' temperature), not the
72
+ table's 300 K, so a semiconductor device says `T: 300`: at 298.15 K the 300 K data's
73
+ intrinsic density is 13% low. A material written by hand has no `T` and is never warned about.
74
+
75
+ To change the temperature of a live device, give it the material for the new T in the same
76
+ `set()`. The state carries over as a warm start:
77
+
78
+ ```js nocheck
79
+ dev.set({ T: 350, materials: semiconductor('Si', { T: 350 }).materials });
80
+ ```
81
+
82
+ A `set({ T })` alone changes kT but not the material's data, and warns. (A material with a
83
+ `trap()` in it: remake it with `trap(semiconductor('Si', { T }), …)` the same way. The trap
84
+ takes its material's T.)
64
85
 
65
- | | $`\varepsilon_r`$ | $`E_g`$ (eV) | $`\chi`$ (V) | $`N_c`$ (cm⁻³) | $`N_v`$ (cm⁻³) | $`\mu_n`$ (cm²/(V·s)) | $`\mu_p`$ (cm²/(V·s)) |
86
+ | | $`\varepsilon_r`$ | $`E_g`$ (eV) | $`\chi`$ (eV) | $`N_c`$ (cm⁻³) | $`N_v`$ (cm⁻³) | $`\mu_n`$ (cm²/(V·s)) | $`\mu_p`$ (cm²/(V·s)) |
66
87
  |---|---|---|---|---|---|---|---|
67
88
  | Si | 11.9 | 1.12 | 4.05 | 2.8 × 10¹⁹ | 1.04 × 10¹⁹ | 1500 | 450 |
68
89
  | Ge | 16.0 | 0.66 | 4.0 | 1.04 × 10¹⁹ | 6.0 × 10¹⁸ | 3900 | 1900 |
@@ -70,14 +91,108 @@ doping. The data are in `SEMICONDUCTORS`, in the source's units (cm⁻³, cm²/(
70
91
 
71
92
  - Source: Sze, *Physics of Semiconductor Devices*, 2nd ed. (1981), appendix "Properties of Ge,
72
93
  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.
94
+ - The electron affinity $`\chi`$ (an energy, in eV) is for vacuum-level estimates with
95
+ `vacuumDipole` (anchor `'e-'`, `offset: chi`, the same number in volts; see the
96
+ [alignment guide](alignment.md)). It isn't used otherwise, since an alignment is never implied.
76
97
  - **Silicon's intrinsic concentration**: these $`N_c`$, $`N_v`$ and $`E_g`$ give $`n_i`$ = 6.7 × 10⁹ cm⁻³.
77
98
  Sze quotes 1.45 × 10¹⁰ (an older measurement), and the accepted value is 9.65 × 10⁹. The
78
99
  library keeps the band data self-consistent rather than matching a quoted $`n_i`$. For Ge and
79
100
  GaAs, the data reproduce the quoted $`n_i`$ (2.4 × 10¹³ and 1.79 × 10⁶) to within 10–15%.
80
101
 
102
+ ### At other temperatures
103
+
104
+ With `T`, each 300 K number moves by a standard temperature law:
105
+
106
+ - **Band gap**, by Varshni's form $`E_g(T) = E_{g0} - \alpha T^2/(T + \beta)`$ (Varshni, Physica 34,
107
+ 149, 1967), with Thurmond's parameters as tabulated by Sze (Thurmond, J. Electrochem. Soc. 122,
108
+ 1133, 1975). Only the shift from 300 K is used, on the table's gap,
109
+ $`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
110
+ table exactly. Varshni's own value at 300 K differs from the table by a few meV.
111
+ - **$`N_c`$ and $`N_v`$** $`\propto T^{3/2}`$, from parabolic bands with fixed effective masses.
112
+ - **Mobilities**, by the power laws for scattering by lattice vibrations,
113
+ $`\mu \propto T^{-\gamma}`$ (Sze, ch. 1). They are approximate and hold for lightly doped material.
114
+ Where scattering by ionized impurities matters (heavy doping, or the cold end of the range), the real
115
+ mobility is lower and rises less on cooling. D is then $`\mu kT/q`$ at T, so it falls more slowly
116
+ than μ.
117
+ - **Electron affinity** $`\chi`$ and **$`\varepsilon_r`$** stay at their 300 K values.
118
+
119
+ | | $`E_{g0}`$ (eV) | $`\alpha`$ (10⁻⁴ eV/K) | $`\beta`$ (K) | Varshni at 300 K (eV) | $`\gamma_n`$ | $`\gamma_p`$ |
120
+ |---|---|---|---|---|---|---|
121
+ | Si | 1.170 | 4.73 | 636 | 1.1245 | 2.42 | 2.20 |
122
+ | Ge | 0.7437 | 4.774 | 235 | 0.6634 | 1.66 | 2.33 |
123
+ | GaAs | 1.519 | 5.405 | 204 | 1.4225 | 1.0 | 2.1 |
124
+
125
+ Outside 150–500 K the call is refused: Varshni's fits and the lattice-limited mobilities are
126
+ not meant for the cold, where impurities and freeze-out take over. With these laws, silicon's
127
+ $`n_i`$ grows 8.3% per kelvin near room temperature. In the tests, a silicon cell's
128
+ open-circuit voltage falls 2.14 mV/K at 300 K. That lies within Green's
129
+ $`\mathrm{d}V_{oc}/\mathrm{d}T = -(E_{g0}/q - V_{oc} + \gamma kT/q)/T`$ for the 1.5 ≤ γ ≤ 3 that
130
+ its transport allows, and the γ = 1.67 it implies matches its two parts of $`J_0`$.
131
+
132
+ **A diode's saturation current** follows from these laws. With $`n_i^2 \propto T^3 e^{-E_g/kT}`$
133
+ and $`D = \mu kT/q \propto T^{1-\gamma}`$ ($`\gamma`$ of the minority carrier that dominates),
134
+ a short base, $`J_0 \propto n_i^2 D/W`$, gives
135
+
136
+ ```math
137
+ 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 .
138
+ ```
139
+
140
+ So an Arrhenius plot of $`J_0`$ doesn't read the gap. $`E_g - T\,\mathrm{d}E_g/\mathrm{d}T`$ is
141
+ the gap extrapolated linearly to 0 K, 1.196 eV for silicon at 300 K, and the prefactor adds
142
+ $`(4-\gamma)kT`$, 0.041 eV for its electrons, about 1.24 eV in all. A long base,
143
+ $`J_0 \propto n_i^2\sqrt{D/\tau}`$, has $`(1-\gamma)/2`$ in place of $`1-\gamma`$, with τ as you
144
+ give it (the library doesn't move it with T).
145
+
146
+ **Hot, lightly doped junctions disappear.** $`n_i`$ grows fast: silicon's is 3 × 10¹³ cm⁻³ at
147
+ 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`$
148
+ approaches the doping, both sides go intrinsic and the junction (its built-in voltage, its
149
+ rectification) fades away. Compare $`n_i(T)`$ with the doping before reading a trend in V(T)
150
+ across that range.
151
+
152
+ ### Alloys and other materials from these
153
+
154
+ A material the library lacks can start from a near relative's entry, with its own numbers put
155
+ in. For example, Al₀.₃Ga₀.₇As from GaAs: give the holes' `mu0` the alloy's gap ($`E_g F`$), and set
156
+ whatever else differs, such as the mobilities and $`N_c`$, $`N_v`$. A field you don't set keeps
157
+ GaAs's value, its electron mobility of 8500 cm²/(V·s) included. Then give each face between the
158
+ two its band offset as a `step` on e⁻, $`\Delta E_c`$ right minus left (the valence-band offset
159
+ follows from the two gaps). `semiconductor(name, { T })` moves only the named material's own data,
160
+ by its laws: numbers you put in stay as given, so at another temperature move them yourself. The
161
+ material still carries the `T` it was made at, for the warning.
162
+
163
+ ```js
164
+ import { Device, units, GAS_CONSTANT, FARADAY } from 'driftlet';
165
+ import { build, layer, ohmic, semiconductor } from 'driftlet/kit';
166
+
167
+ // The usual estimates for x = 0.3 (E_g = 1.424 + 1.247x eV, ΔE_c ≈ 0.62 ΔE_g,
168
+ // m_e = 0.067 + 0.083x); check them against your source.
169
+ const T = 300, VT = (GAS_CONSTANT * T) / FARADAY, x = 0.3;
170
+ const gap = 1.424 + 1.247 * x, dEc = 0.62 * (gap - 1.424);
171
+ const alloy = semiconductor('GaAs', { material: 'AlGaAs', T }), m = alloy.materials.AlGaAs;
172
+ m.species['h+'] = { ...m.species['h+'], mu0: units.eV(gap) };
173
+ m.species['e-'] = {
174
+ ...m.species['e-'],
175
+ D: 1000e-4 * VT, // μ_n = 1000 cm²/(V·s), a rough figure
176
+ cRef: m.species['e-'].cRef * ((0.067 + 0.083 * x) / 0.067) ** 1.5, // N_c ∝ m_e^1.5
177
+ };
178
+ // A heterojunction diode, n-AlGaAs | p-GaAs, forward biased.
179
+ const dev = new Device(build({
180
+ T,
181
+ library: [semiconductor('GaAs', { T }), alloy],
182
+ stack: [
183
+ ohmic(0), layer('AlGaAs', 1e-6, { donors: units.perCm3(1e17) }),
184
+ { step: { species: 'e-', value: units.eV(-dEc) } }, // GaAs's conduction band is lower
185
+ layer('GaAs', 1e-6, { acceptors: units.perCm3(1e17) }), ohmic(1.0),
186
+ ],
187
+ bulkReactions: [{ equation: 'e- + h+ = 0', kf: { GaAs: 1e8, AlGaAs: 1e8 } }],
188
+ }));
189
+ const sol = dev.solve();
190
+ // The wider gap keeps holes out of the n side: nearly all the current is electrons injected
191
+ // into the GaAs.
192
+ const { 'e-': e, 'h+': h } = sol.contacts.left.flux;
193
+ console.log(sol.warnings.length, (e / -h).toExponential(1)); // 0, 2.5e+7
194
+ ```
195
+
81
196
  ## Metals, 20 °C
82
197
 
83
198
  `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).
@@ -418,7 +423,13 @@ those materials.
418
423
  none (`'e- + h+ = 0'`):
419
424
  $`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
425
  for [interface SRH](#interfaces) (default midgap). It's what explicit trap species reduce to when
421
- the traps are few and fast, without them.
426
+ the traps are few and fast, without them. Where their charge matters, write them out: two
427
+ immobile species (D = 0) for the trap's charge states, filled and emptied by
428
+ `'e- + X0 = X-'` and `'X- + h+ = X0'` (acceptor-like) at rates $`k_n = 1/(\tau_n N_t)`$ and
429
+ $`k_p = 1/(\tau_p N_t)`$, the filled state's $`\mu^\circ`$ an electron's $`\mu`$ at $`n_1`$ above the
430
+ empty one's. In steady state they recombine at the rate above exactly, each node keeps its
431
+ own trap total, and the trapped charge is in Poisson's equation. The kit's
432
+ [`trap()`](kit.md#a-device-as-a-stack) writes them from the same `{ tauN, tauP, n1 }` and a density.
422
433
 
423
434
  ```js nocheck
424
435
  bulkReactions: [{ equation: 'e- + h+ = 0', srh: { Si: { tauN: 1e-6, tauP: 1e-6 } } }]
@@ -479,10 +490,10 @@ contacts: {
479
490
  - `{ type: 'exchange', k, mu }` (neutral species):
480
491
  $`N_{\mathrm{in}} = k \cdot (\mu_{\mathrm{out}} - \mu)/RT`$, k in mol/(m²·s).
481
492
  - `{ type: 'velocity', v, offset }` (or `mu`, for a neutral species): a surface velocity,
482
- $`N_{\mathrm{in}} = v\,(c_{\mathrm{eq}} - c)`$ with v in m/s and $`c_{\mathrm{eq}}`$ the end node's
483
- concentration in equilibrium with the outside level, i.e.
484
- $`v\,c\,(e^{(\bar\mu_{\mathrm{out}} - \bar\mu)/RT} - 1)`$, which keeps the flux's sign that of the
485
- level difference whatever the statistics. It lies between `'blocked'` (v → 0) and
493
+ $`N_{\mathrm{in}} = v\,c\,(e^{(\bar\mu_{\mathrm{out}} - \bar\mu)/RT} - 1)`$ with v in m/s: for dilute
494
+ species that's $`v\,(c_{\mathrm{eq}} - c)`$, $`c_{\mathrm{eq}}`$ the end node's concentration in
495
+ equilibrium with the outside level, and with any statistics it keeps the flux's sign that of
496
+ the level difference (though it isn't then $`v\,(c_{\mathrm{eq}} - c)`$, which saturates). It lies between `'blocked'` (v → 0) and
486
497
  `'equilibrium'` (v → ∞): a contact's surface recombination velocity, or thermionic emission
487
498
  over a Schottky barrier (below).
488
499
  - `'blocked'`: no flux.
@@ -666,10 +677,16 @@ An electrode port can carry species on its surface, sharing its sites: an adsorb
666
677
  or a passive film taken as a coverage. Each has a coverage $`\theta`$ at every node of the window,
667
678
  Langmuir statistics, $`\mu = \mu^\circ + RT\ln(\theta/\theta_0)`$ with $`\theta_0 = 1 - \sum\theta`$ the
668
679
  bare fraction, and `capacity` $`\Gamma`$, mol of sites per m² of electrode. Reactions name them like
669
- species, and a reaction with `bare: true` runs only on bare metal: its rate is multiplied by
670
- $`\theta_0`$, both ways, so equilibrium stays exact. Written for adsorption itself, that's Langmuir
671
- kinetics (on at a rate ∝ $`\theta_0`$, off ∝ $`\theta`$). A species' `theta0` is its starting
672
- coverage (not the bare fraction). Surface species carry no charge, so an adsorbed redox couple
680
+ species, and their kinetics are Langmuir: in the Butler–Volmer prefactor each surface species
681
+ counts at its coverage $`\theta`$ (as a solution species does at $`c/c_{\mathrm{ref}}`$), and a reaction
682
+ that fills $`\Delta`$ sites going forward holds $`m = \max(\Delta, 0)`$ bare ones in its transition
683
+ state, so its forward rate goes as $`\theta_0^{\,m}`$ and its backward as $`\theta_0^{\,m-\Delta}`$.
684
+ Adsorption goes on at a rate ∝ $`\theta_0`$ and off ∝ $`\theta`$, an adsorbed couple converts at a
685
+ rate ∝ its coverage, and $`2\mathrm{H_{ads}} \to \mathrm{H_2}`$ goes as $`\theta_H^2`$ and back as
686
+ $`\theta_0^2`$. A reaction with `bare: true` runs only on bare metal, needing a bare site even
687
+ where it fills none ($`m \ge 1`$): a metal dissolving, blocked by a film. Equilibrium is exact
688
+ throughout, from the affinity. A species' `theta0` is its starting coverage (not the bare
689
+ fraction). Surface species carry no charge, so an adsorbed redox couple
673
690
  balances its electrons through the solution: `'Oads + e- = Rads + Cl-'`, its coverages at
674
691
  $`\theta_O/\theta_R = e^{(E - E^\circ)/V_T}`$ against the chloride's level.
675
692
 
@@ -679,7 +696,7 @@ ports: [{
679
696
  surface: { 'Fe(OH)2': { mu0: -490e3, capacity: 2e-5, theta0: 1e-6 } }, // theta0: the start
680
697
  reactions: [
681
698
  { equation: 'Fe2+ + 2 e- = Fe(s)', fixed: { 'Fe(s)': 0 }, k0: 5e-6, alpha: 0.5, bare: true }, // dissolves where bare
682
- { equation: 'Fe(s) + 2 OH- = Fe(OH)2 + 2 e-', fixed: { 'Fe(s)': 0 }, k0: 1e-4, alpha: 0.5, bare: true }, // the film forms (in minutes)
699
+ { equation: 'Fe(s) + 2 OH- = Fe(OH)2 + 2 e-', fixed: { 'Fe(s)': 0 }, k0: 1e-4, alpha: 0.5 }, // the film forms (in minutes), on bare sites
683
700
  { equation: 'O2 + 2 H2O + 4 e- = 4 OH-', fixed: { H2O: -237.13e3 }, k0: 3e-9, alpha: 0.125 }, // on film or not
684
701
  ],
685
702
  }]
@@ -994,6 +1011,9 @@ const now = dev.solution(); // snapshot of the current state
994
1011
  mid-run, `set({ contacts: { right: { bath: { c } } } })`, is a step in the sample.) Either way the time stepping restarts its
995
1012
  order, as after any discontinuity, but a carried state goes on at the step size an adaptive
996
1013
  transient had reached, so a page may change a rate constant or a diffusivity every frame.
1014
+ To change the temperature, give the materials for it in the same patch,
1015
+ `dev.set({ T: 350, materials: semiconductor('Si', { T: 350 }).materials })`: a `T` alone
1016
+ changes kT but not the materials' data (a material that carries its `T` then warns).
997
1017
 
998
1018
  For example, a silver nitrate cell between silver electrodes: its impedance spectrum, then the
999
1019
  current transient after a voltage step.
@@ -1041,7 +1061,7 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
1041
1061
  | `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 |
1042
1062
  | `charge` | total charge in the device, C/m² (C through a cross-section) |
1043
1063
  | `conservation` | per species stretch: amount, reference, intake through contacts, and drift, relative: (amount − reference − intake) over the largest of them |
1044
- | `warnings` | e.g. unresolved double layers, conventions a statistics model relies on, and for a failed solve, where the system is nearly singular |
1064
+ | `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 |
1045
1065
  | `converged`, `iterations`, `steps`, `substeps`, `history`, `time`, `T` | solver bookkeeping, and the temperature |
1046
1066
  | `steady` | whether this is a converged steady state (from `solve()`), not a transient's: what `check()` in the kit decides its checks by |
1047
1067
  | `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.
@@ -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 |
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.0/src/index.js';
34
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0.0/src/kit.js';
35
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0.0/src/plot.js';
33
+ import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.0/src/index.js';
34
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.0/src/kit.js';
35
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.0/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.0/src/index.js';
213
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0.0/src/kit.js';
214
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0.0/src/plot.js';
222
+ import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.0/src/index.js';
223
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.0/src/kit.js';
224
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.0/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.0",
3
+ "version": "1.1.0",
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
  });