driftlet 1.0.1 → 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.1/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 } } }]
@@ -1000,6 +1011,9 @@ const now = dev.solution(); // snapshot of the current state
1000
1011
  mid-run, `set({ contacts: { right: { bath: { c } } } })`, is a step in the sample.) Either way the time stepping restarts its
1001
1012
  order, as after any discontinuity, but a carried state goes on at the step size an adaptive
1002
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).
1003
1017
 
1004
1018
  For example, a silver nitrate cell between silver electrodes: its impedance spectrum, then the
1005
1019
  current transient after a voltage step.
@@ -1047,7 +1061,7 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
1047
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 |
1048
1062
  | `charge` | total charge in the device, C/m² (C through a cross-section) |
1049
1063
  | `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 |
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 |
1051
1065
  | `converged`, `iterations`, `steps`, `substeps`, `history`, `time`, `T` | solver bookkeeping, and the temperature |
1052
1066
  | `steady` | whether this is a converged steady state (from `solve()`), not a transient's: what `check()` in the kit decides its checks by |
1053
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.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.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.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.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.1",
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
  });
package/src/data.js CHANGED
@@ -3,8 +3,9 @@
3
3
  // only. How levels line up at an interface, and a surface's work function, belong to that
4
4
  // interface or surface and are never in here.
5
5
 
6
- import { DeviceError, checkOptions } from './errors.js';
6
+ import { DeviceError, checkOptions, shown } from './errors.js';
7
7
  import { FARADAY, GAS_CONSTANT, AVOGADRO } from './constants.js';
8
+ import { powr } from './pow.js';
8
9
 
9
10
  const fail = (message) => {
10
11
  throw new DeviceError(message);
@@ -43,7 +44,7 @@ export const WATER_EPSR = 78.3;
43
44
 
44
45
  /**
45
46
  * Semiconductors at 300 K (Sze, Physics of Semiconductor Devices, 2nd ed., 1981, appendix):
46
- * relative permittivity, band gap Eg (eV), electron affinity chi (V), effective densities of
47
+ * relative permittivity, band gap Eg (eV), electron affinity chi (eV), effective densities of
47
48
  * states Nc and Nv (cm⁻³), and drift mobilities of lightly doped material mun and mup
48
49
  * (cm²/(V·s)).
49
50
  */
@@ -84,21 +85,49 @@ export function aqueous(names, { epsr = WATER_EPSR, material = 'water' } = {}) {
84
85
  return { species, materials: { [material]: { epsr, species: entries } } };
85
86
  }
86
87
 
88
+ // How the semiconductors' data move with temperature, for semiconductor(name, { T }):
89
+ // - the gap by Varshni's form, Eg(T) = Eg0 − αT²/(T + β) (Y. P. Varshni, Physica 34, 149, 1967),
90
+ // with Thurmond's parameters (C. D. Thurmond, J. Electrochem. Soc. 122, 1133, 1975, as
91
+ // tabulated in Sze, 2nd ed.): Eg0 (eV), alpha (eV/K), beta (K). Only the shift from 300 K is
92
+ // used, on the table's Eg, so T = 300 K is the table: Varshni's own Eg(300) differs from it by
93
+ // 4.5 meV (Si, 1.1245), 3.4 meV (Ge, 0.6634) and 1.5 meV (GaAs, 1.4225).
94
+ // - Nc and Nv ∝ T^1.5 (parabolic bands, effective masses held fixed).
95
+ // - mobilities ∝ T^−gn and T^−gp, the lattice-limited power laws for lightly doped material
96
+ // (Sze, 2nd ed., ch. 1): approximate, and wrong where impurity scattering matters (heavy
97
+ // doping, or cold). D follows from μ and T by Einstein's relation, D = μkT/q.
98
+ // - the electron affinity chi isn't moved.
99
+ const WITH_T = {
100
+ Si: { Eg0: 1.17, alpha: 4.73e-4, beta: 636, gn: 2.42, gp: 2.2 },
101
+ Ge: { Eg0: 0.7437, alpha: 4.774e-4, beta: 235, gn: 1.66, gp: 2.33 },
102
+ GaAs: { Eg0: 1.519, alpha: 5.405e-4, beta: 204, gn: 1.0, gp: 2.1 },
103
+ };
104
+ const T_RANGE = [150, 500];
105
+
87
106
  /**
88
107
  * A semiconductor from `SEMICONDUCTORS`, as a library piece: electrons `e-` and holes `h+`, with
89
- * c_ref = Nc and Nv, D from the mobilities by the Einstein relation at 300 K, and μ°_e⁻ = 0 (φ
90
- * anchored at the conduction band) and μ°_h⁺ = Eg·F. The data are for 300 K, so use `T: 300`.
108
+ * c_ref = Nc and Nv, D from the mobilities by the Einstein relation at T, and μ°_e⁻ = 0 (φ
109
+ * anchored at the conduction band) and μ°_h⁺ = Eg·F. The data are for 300 K; another T (150 to
110
+ * 500 K) moves them: the gap by Varshni's shift from 300 K, Nc and Nv as T^1.5, the mobilities by
111
+ * lattice-limited power laws (light doping). The material carries its `T`, and a device at
112
+ * another temperature warns: give the device the same T.
91
113
  * Statistics are ideal (Boltzmann); add Fermi–Dirac to the material for degenerate doping.
92
114
  * @param {string} name 'Si', 'Ge' or 'GaAs'
93
- * @param {{ material?: string }} [opts]
115
+ * @param {{ material?: string, T?: number }} [opts] the material's name, and the temperature
116
+ * (K, default 300, the table's)
94
117
  * @returns {{ species: object[], materials: Record<string, object> }}
95
118
  */
96
- export function semiconductor(name, { material = name } = {}) {
97
- if (arguments[1]?.T !== undefined) fail('semiconductor: the data are for 300 K and take no T: give the device T: 300, or write your own material for another temperature (SEMICONDUCTORS has the numbers)');
98
- checkOptions('semiconductor', arguments[1], ['material']);
119
+ export function semiconductor(name, { material = name, T = 300 } = {}) {
120
+ checkOptions('semiconductor', arguments[1], ['material', 'T']);
99
121
  const sc = SEMICONDUCTORS[name];
100
122
  if (!sc) fail(`semiconductor: no data for '${name}' (the library has ${Object.keys(SEMICONDUCTORS).join(', ')})`);
101
- const VT = (GAS_CONSTANT * 300) / FARADAY;
123
+ if (typeof T !== 'number') fail(`semiconductor: T must be a number, the temperature in K, got ${shown(T)}`);
124
+ if (!(Number.isFinite(T) && T >= T_RANGE[0] && T <= T_RANGE[1])) {
125
+ fail(`semiconductor: T must be the temperature in K, from ${T_RANGE[0]} to ${T_RANGE[1]} (the range its temperature laws are good for), got ${T}`);
126
+ }
127
+ const { alpha, beta, gn, gp } = WITH_T[name];
128
+ const t = T / 300, varshni = (T) => (alpha * T * T) / (T + beta);
129
+ const Eg = sc.Eg - (varshni(T) - varshni(300));
130
+ const VT = (GAS_CONSTANT * T) / FARADAY;
102
131
  const perCm3 = (n) => (n * 1e6) / AVOGADRO;
103
132
  return {
104
133
  species: [
@@ -107,16 +136,76 @@ export function semiconductor(name, { material = name } = {}) {
107
136
  ],
108
137
  materials: {
109
138
  [material]: {
139
+ T, // the temperature the data are for: a device at another warns
110
140
  epsr: sc.epsr,
111
141
  species: {
112
- 'e-': { D: sc.mun * 1e-4 * VT, mu0: 0, cRef: perCm3(sc.Nc) },
113
- 'h+': { D: sc.mup * 1e-4 * VT, mu0: sc.Eg * FARADAY, cRef: perCm3(sc.Nv) },
142
+ 'e-': { D: sc.mun * powr(t, -gn) * 1e-4 * VT, mu0: 0, cRef: perCm3(sc.Nc * powr(t, 1.5)) },
143
+ 'h+': { D: sc.mup * powr(t, -gp) * 1e-4 * VT, mu0: Eg * FARADAY, cRef: perCm3(sc.Nv * powr(t, 1.5)) },
114
144
  },
115
145
  },
116
146
  },
117
147
  };
118
148
  }
119
149
 
150
+ /**
151
+ * A trap level in a semiconductor that holds charge: its two charge states as immobile species
152
+ * (D = 0), filled and emptied by capturing carriers, e⁻ + X⁰ = X⁻ and X⁻ + h⁺ = X⁰ (acceptor-like)
153
+ * or X⁺ + e⁻ = X⁰ and X⁰ + h⁺ = X⁺ (donor-like). In steady state it recombines at exactly the
154
+ * `srh` law's rate with the same τ_n, τ_p and n₁, and its charge is in Poisson's equation, which
155
+ * the `srh` law (the trap's occupancy eliminated) leaves out. Each node keeps its own total.
156
+ * @param {{ species: object[], materials: Record<string, object> }} lib a library entry with e⁻
157
+ * and h⁺ (semiconductor()), returned with the trap's states added
158
+ * @param {{ density: number, tauN: number, tauP: number, n1?: number, charge?: 'acceptor' | 'donor',
159
+ * name?: string, material?: string, T?: number }} opts density N_t (mol/m³); τ_n = 1/(c_n N_t)
160
+ * and τ_p = 1/(c_p N_t) (s) at that density; n₁, the electron density that half-fills it
161
+ * (mol/m³; default n_i, midgap); T (K, default the material's T, else 300)
162
+ * @returns {{ library: object, bulkReactions: object[], c0: Record<string, number> }} the library
163
+ * for build(), the capture reactions, and c0 for each layer holding the trap (its total N_t)
164
+ */
165
+ export function trap(lib, { density, tauN, tauP, n1, charge = 'acceptor', name = 'X', material, T } = {}) {
166
+ checkOptions('trap', arguments[1], ['density', 'tauN', 'tauP', 'n1', 'charge', 'name', 'material', 'T']);
167
+ if (!lib || typeof lib !== 'object' || typeof lib.materials !== 'object') fail('trap: give the library entry first, semiconductor(\'Si\') say, then { density, tauN, tauP }');
168
+ const names = Object.keys(lib.materials);
169
+ const m = material ?? (names.length === 1 ? names[0] : fail(`trap: the library has materials ${names.join(', ')}: say which, with material`));
170
+ const mat = lib.materials[m];
171
+ if (!mat) fail(`trap: no material '${m}' in the library (it has ${names.join(', ')})`);
172
+ const el = mat.species?.['e-'], ho = mat.species?.['h+'];
173
+ if (!el || !ho) fail(`trap: material '${m}' needs e- and h+ (a semiconductor)`);
174
+ const pos = (v, what) => (Number.isFinite(v) && v > 0 ? v : fail(`trap: ${what}, got ${shown(v)}`));
175
+ pos(density, 'density must be the trap density N_t > 0 (mol/m³; units.perCm3(1e15), say)');
176
+ pos(tauN, 'tauN must be the electron lifetime τ_n = 1/(c_n N_t) > 0 (s)');
177
+ pos(tauP, 'tauP must be the hole lifetime τ_p = 1/(c_p N_t) > 0 (s)');
178
+ // (the material's own temperature, where it gives one: semiconductor()'s does)
179
+ if (T === undefined) T = mat.T ?? 300;
180
+ pos(T, 'T must be the temperature in K');
181
+ if (mat.T !== undefined && Math.abs(T - mat.T) > 0.01) fail(`trap: T is ${T} K, but material '${m}' is for ${mat.T} K; leave T out (the trap takes the material's), or remake the material for ${T} K`);
182
+ if (charge !== 'acceptor' && charge !== 'donor') fail(`trap: charge must be 'acceptor' (0/−) or 'donor' (+/0), got ${JSON.stringify(charge)}`);
183
+ if (typeof name !== 'string' || !/^[A-Za-z][\w()]*$/.test(name)) fail(`trap: name must be a plain name for the trap (its states are name0 and name- or name+), got ${JSON.stringify(name)}`);
184
+ const RT = GAS_CONSTANT * T, cRefE = el.cRef ?? lib.species.find((sp) => sp.name === 'e-')?.cRef;
185
+ const cRefH = ho.cRef ?? lib.species.find((sp) => sp.name === 'h+')?.cRef;
186
+ const ni = Math.sqrt(cRefE * cRefH * Math.exp(-(ho.mu0 - el.mu0) / RT));
187
+ const half = n1 === undefined ? ni : pos(n1, 'n1 must be the electron density that half-fills the trap > 0 (mol/m³)');
188
+ // The filled state's μ° sits an electron's μ at n₁ above the empty one's: half full at n = n₁.
189
+ const [empty, full] = charge === 'acceptor' ? [`${name}0`, `${name}-`] : [`${name}+`, `${name}0`];
190
+ const zEmpty = charge === 'acceptor' ? 0 : 1;
191
+ const states = {
192
+ [empty]: { D: 0, mu0: 0, cRef: density },
193
+ [full]: { D: 0, mu0: el.mu0 + RT * Math.log(half / cRefE), cRef: density },
194
+ };
195
+ return {
196
+ library: {
197
+ species: [...lib.species, { name: empty, z: zEmpty }, { name: full, z: zEmpty - 1 }],
198
+ materials: { ...lib.materials, [m]: { ...mat, species: { ...mat.species, ...states } } },
199
+ },
200
+ bulkReactions: [
201
+ { equation: `e- + ${empty} = ${full}`, kf: { [m]: 1 / (tauN * density) } },
202
+ { equation: `${full} + h+ = ${empty}`, kf: { [m]: 1 / (tauP * density) } },
203
+ ],
204
+ // (each node keeps the total, X⁰ + X⁻ say; the split is the solve's to find)
205
+ c0: { [empty]: density / 2, [full]: density / 2 },
206
+ };
207
+ }
208
+
120
209
  /**
121
210
  * A metal from `METALS` as a conductor material, its carrier `e-`.
122
211
  * @param {string} name
package/src/describe.js CHANGED
@@ -138,7 +138,7 @@ export function describe(def) {
138
138
  const { species, materials, regions, grid } = model;
139
139
  const RT = GAS_CONSTANT * model.T;
140
140
  const lines = [];
141
- const warnings = unitWarnings(def);
141
+ const warnings = [...model.warnings, ...unitWarnings(def)]; // (the definition's own: a material's data at another T, …)
142
142
  const total = grid.x[grid.x.length - 1] - grid.x[0];
143
143
  lines.push(`${regions.length} region${regions.length > 1 ? 's' : ''}, ${si(total, 'm')}, ${grid.x.length} nodes, T = ${model.T} K`);
144
144
  const geo = model.geometry;
package/src/device.js CHANGED
@@ -9,7 +9,7 @@
9
9
  import { buildGrid, applyGeometry } from './grid.js';
10
10
  import { FARADAY, GAS_CONSTANT } from './constants.js';
11
11
  import { normalizeStatistics } from './statistics.js';
12
- import { DeviceError } from './errors.js';
12
+ import { DeviceError, shown } from './errors.js';
13
13
  import { stoichiometry as equationStoichiometry, faceSides, parseEquation } from './equation.js';
14
14
 
15
15
  export { DeviceError };
@@ -47,12 +47,12 @@ function fields(obj, path, allowed) {
47
47
  }
48
48
 
49
49
  function finite(v, path) {
50
- need(isFiniteNumber(v), `${path} must be a finite number, got ${JSON.stringify(v)}`);
50
+ need(isFiniteNumber(v), `${path} must be a finite number, got ${shown(v)}`);
51
51
  return v;
52
52
  }
53
53
 
54
54
  function positive(v, path) {
55
- need(isFiniteNumber(v) && v > 0, `${path} must be a positive number, got ${JSON.stringify(v)}`);
55
+ need(isFiniteNumber(v) && v > 0, `${path} must be a positive number, got ${shown(v)}`);
56
56
  return v;
57
57
  }
58
58
 
@@ -108,9 +108,9 @@ function profile(v, path, what, ok, okText) {
108
108
  `${path} must be a profile { x, values }: two arrays of the same length, positions (m, the device's x) and ${what}`,
109
109
  );
110
110
  v.x.forEach((xk, k) => {
111
- need(isFiniteNumber(xk) && (k === 0 || xk > v.x[k - 1]), `${path}.x must be finite and strictly increasing, but x[${k}] is ${JSON.stringify(xk)}`);
111
+ need(isFiniteNumber(xk) && (k === 0 || xk > v.x[k - 1]), `${path}.x must be finite and strictly increasing, but x[${k}] is ${shown(xk)}`);
112
112
  });
113
- v.values.forEach((y, k) => need(isFiniteNumber(y) && ok(y), `${path}.values[${k}] must be ${okText}, got ${JSON.stringify(y)}`));
113
+ v.values.forEach((y, k) => need(isFiniteNumber(y) && ok(y), `${path}.values[${k}] must be ${okText}, got ${shown(y)}`));
114
114
  return { x: Float64Array.from(v.x), values: Float64Array.from(v.values) };
115
115
  }
116
116
 
@@ -138,7 +138,7 @@ function srhLaw(sdef, path, list, species, timeKeys) {
138
138
  }
139
139
 
140
140
  function nonNegative(v, path) {
141
- need(isFiniteNumber(v) && v >= 0, `${path} must be a non-negative number, got ${JSON.stringify(v)}`);
141
+ need(isFiniteNumber(v) && v >= 0, `${path} must be a non-negative number, got ${shown(v)}`);
142
142
  return v;
143
143
  }
144
144
 
@@ -151,7 +151,7 @@ export function normalizeDevice(def) {
151
151
  need(isObject(def), 'device definition must be an object');
152
152
  fields(def, 'device', ['T', 'species', 'materials', 'regions', 'interfaces', 'bulkReactions', 'contacts', 'ports', 'grid', 'geometry']);
153
153
 
154
- const T = def.T === undefined ? 298.15 : positive(def.T, 'T');
154
+ const T = def.T === undefined ? 298.15 : positive(def.T, 'T (the temperature, K)');
155
155
  const RT = GAS_CONSTANT * T;
156
156
 
157
157
  // --- species
@@ -164,7 +164,7 @@ export function normalizeDevice(def) {
164
164
  fields(sp, path, ['name', 'z', 'cRef']);
165
165
  need(typeof sp.name === 'string' && sp.name.length > 0, `${path}.name must be a non-empty string`);
166
166
  need(!speciesIndex.has(sp.name), `${path}.name: duplicate species '${sp.name}'`);
167
- need(Number.isInteger(sp.z), `${path}.z (charge number) must be an integer, got ${JSON.stringify(sp.z)}`);
167
+ need(Number.isInteger(sp.z), `${path}.z (charge number) must be an integer, got ${shown(sp.z)}`);
168
168
  const cRef = sp.cRef === undefined ? undefined : positive(sp.cRef, `${path}.cRef`);
169
169
  speciesIndex.set(sp.name, i);
170
170
  species.push({ name: sp.name, z: sp.z, cRef });
@@ -179,13 +179,15 @@ export function normalizeDevice(def) {
179
179
  for (const [mname, mat] of Object.entries(def.materials)) {
180
180
  const path = `materials.${mname}`;
181
181
  need(isObject(mat), `${path} must be an object`);
182
+ // The temperature a material's data are for, where it says (semiconductor()'s do).
183
+ const dataT = mat.T === undefined ? undefined : positive(mat.T, `${path}.T (the temperature its data are for, K)`);
182
184
  if (mat.conductor !== undefined) {
183
185
  materialIndex.set(mname, materials.length);
184
- materials.push(normalizeConductor(mat, mname, path, species, speciesIndex));
186
+ materials.push({ ...normalizeConductor(mat, mname, path, species, speciesIndex), dataT });
185
187
  continue;
186
188
  }
187
189
  // ε = 0 makes the material strictly neutral: Poisson becomes local neutrality there.
188
- fields(mat, path, ['epsr', 'species', 'statistics']);
190
+ fields(mat, path, ['epsr', 'species', 'statistics', 'T']);
189
191
  const epsr = nonNegative(mat.epsr, `${path}.epsr`);
190
192
  need(isObject(mat.species), `${path}.species must be an object mapping species names to parameters`);
191
193
  const present = new Uint8Array(nSpecies);
@@ -205,7 +207,7 @@ export function normalizeDevice(def) {
205
207
  need(c !== undefined, `${spath}.cRef: no reference concentration (give it here or on the species)`);
206
208
  cRef[i] = positive(c, `${spath}.cRef`);
207
209
  }
208
- const m = { name: mname, epsr, present, D, mu0, cRef };
210
+ const m = { name: mname, epsr, present, D, mu0, cRef, dataT };
209
211
  const st = normalizeStatistics({ ...m, statistics: mat.statistics }, path, species, speciesIndex, RT, { need, finite, positive });
210
212
  m.models = st.models;
211
213
  m.modelOf = st.modelOf;
@@ -285,7 +287,7 @@ export function normalizeDevice(def) {
285
287
  c0Profile[i] = profile(v, at, 'concentrations (mol/m³)', (ck) => ck > 0, `a concentration > 0 (mol/m³)${none}`);
286
288
  continue;
287
289
  }
288
- need(isFiniteNumber(v) && v > 0, `${at} must be a concentration > 0 (mol/m³) or a profile { x, values }, got ${JSON.stringify(v)}${none}`);
290
+ need(isFiniteNumber(v) && v > 0, `${at} must be a concentration > 0 (mol/m³) or a profile { x, values }, got ${shown(v)}${none}`);
289
291
  c0[i] = v;
290
292
  }
291
293
  }
@@ -306,6 +308,17 @@ export function normalizeDevice(def) {
306
308
  };
307
309
  });
308
310
 
311
+ // A material whose data are for another temperature than the device's gives the wrong n_i, gap
312
+ // and mobilities, silently: say so (for each material a region uses).
313
+ materials.forEach((m, k) => {
314
+ if (m.dataT === undefined || Math.abs(m.dataT - T) <= 0.01 || !regions.some((reg) => reg.material === k)) return;
315
+ warnings.push(
316
+ `materials.${m.name}: its data are for ${m.dataT} K, but the device is at ${T} K${def.T === undefined ? ' (the default, as it gives no T)' : ''}, ` +
317
+ `so they don't hold there (a semiconductor's n_i, gap and mobilities are ${m.dataT} K's): give the device T: ${m.dataT}, ` +
318
+ `or remake the material for ${T} K (semiconductor(name, { T: ${T} }))`,
319
+ );
320
+ });
321
+
309
322
  // --- interfaces (one per face between consecutive regions)
310
323
  const nFaces = regions.length - 1;
311
324
  const idefs = def.interfaces ?? [];
@@ -490,7 +503,7 @@ function stoichiometry(map, path, fixed, fixedPath, species, speciesIndex, RT) {
490
503
  const list = [];
491
504
  let fixedA = 0, charge = 0, nFixed = 0;
492
505
  for (const [name, nu] of Object.entries(map)) {
493
- need(Number.isInteger(nu) && nu !== 0, `${path}.${name} must be a non-zero integer, got ${JSON.stringify(nu)}`);
506
+ need(Number.isInteger(nu) && nu !== 0, `${path}.${name} must be a non-zero integer, got ${shown(nu)}`);
494
507
  if (speciesIndex.has(name)) {
495
508
  const i = speciesIndex.get(name);
496
509
  list.push({ i, nu });
@@ -1048,7 +1061,7 @@ function gatedBy(raw, gates, lpath, where) {
1048
1061
  return Object.entries(raw).map(([gname, p]) => {
1049
1062
  const q = gates.findIndex((gt) => gt.name === gname);
1050
1063
  need(q >= 0, `${lpath}.gates.${gname}: no such gate${gates.length ? ` (there are ${gates.map((gt) => gt.name).join(', ')})` : ` (define it in ${where}.gates)`}`);
1051
- need(Number.isInteger(p) && p >= 1 && p <= 8, `${lpath}.gates.${gname}: the exponent must be a whole number from 1 to 8, got ${JSON.stringify(p)}`);
1064
+ need(Number.isInteger(p) && p >= 1 && p <= 8, `${lpath}.gates.${gname}: the exponent must be a whole number from 1 to 8, got ${shown(p)}`);
1052
1065
  return [q, p];
1053
1066
  });
1054
1067
  }
@@ -1081,6 +1094,8 @@ function normalizeConductor(mat, mname, path, species, speciesIndex) {
1081
1094
  for (const k of ['epsr', 'species', 'statistics']) {
1082
1095
  need(mat[k] === undefined, `${path}.${k}: a conductor takes only { conductor: { species, conductivity } }`);
1083
1096
  }
1097
+ fields(mat, path, ['conductor', 'T']);
1098
+ fields(mdef, `${path}.conductor`, ['species', 'conductivity']);
1084
1099
  need(speciesIndex.has(mdef.species), `${path}.conductor.species: unknown species ${JSON.stringify(mdef.species)}${known(speciesIndex)}`);
1085
1100
  const i = speciesIndex.get(mdef.species);
1086
1101
  need(species[i].z !== 0, `${path}.conductor.species: the conductor's carrier must be charged`);
@@ -1175,7 +1190,7 @@ function defaultInterfaceLinks(matL, matR, species) {
1175
1190
 
1176
1191
  function transferCoefficient(v, path) {
1177
1192
  if (v === undefined) return 0.5;
1178
- need(isFiniteNumber(v) && v >= 0 && v <= 1, `${path} must be between 0 and 1, got ${JSON.stringify(v)}`);
1193
+ need(isFiniteNumber(v) && v >= 0 && v <= 1, `${path} must be between 0 and 1, got ${shown(v)}`);
1179
1194
  return v;
1180
1195
  }
1181
1196
 
package/src/errors.js CHANGED
@@ -39,3 +39,21 @@ export function checkOptions(where, opts, allowed, Kind = DeviceError) {
39
39
  throw new Kind(`${where}: no option '${k}'${near ? ` (did you mean '${near}'?)` : ''}; the options are ${allowed.join(', ')}`);
40
40
  }
41
41
  }
42
+
43
+ /**
44
+ * A value as an error message shows it: a number as itself (NaN and Infinity too, which JSON
45
+ * would print as null), a string quoted and called one ("350", a string, where a number was
46
+ * wanted).
47
+ * @param {unknown} v
48
+ * @returns {string}
49
+ */
50
+ export function shown(v) {
51
+ if (typeof v === 'number' || typeof v === 'bigint' || v === undefined) return String(v);
52
+ if (typeof v === 'string') return `${JSON.stringify(v)} (a string)`;
53
+ if (typeof v === 'function') return 'a function';
54
+ try {
55
+ return JSON.stringify(v, (k, x) => (typeof x === 'number' && !Number.isFinite(x) ? String(x) : x)) ?? String(v);
56
+ } catch {
57
+ return String(v);
58
+ }
59
+ }
package/src/index.js CHANGED
@@ -187,7 +187,9 @@ export class Device {
187
187
  // A solve that fails reports the state it failed in, and leaves the device as it was: a
188
188
  // device with no steady state (a host filling at a current) can still advance() from there.
189
189
  const snap = this.solver._snapshot();
190
- const sol = makeSolution(this.solver, this.solver.solveSteady(opts));
190
+ const result = this.solver.solveSteady(opts);
191
+ // (a failed solve's state isn't a solution: its accuracy warnings are left out)
192
+ const sol = makeSolution(this.solver, result.converged ? result : { ...result, unsolved: true });
191
193
  if (!sol.converged) this.solver._restore(snap);
192
194
  return sol;
193
195
  }
package/src/kit.js CHANGED
@@ -18,7 +18,7 @@ export { check } from './checks.js';
18
18
  export { particles } from './particles.js';
19
19
  export { polarization } from './polarization.js';
20
20
  export { perovskiteCell, PEROVSKITE_SCANS, hysteresis } from './devices.js';
21
- export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal } from './data.js';
21
+ export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal, trap } from './data.js';
22
22
 
23
23
  const fail = (message) => {
24
24
  throw new DeviceError(message);
package/src/solution.js CHANGED
@@ -292,6 +292,13 @@ export function makeSolution(solver, result = {}) {
292
292
  'Check the definition with describe() (from driftlet/kit); approach this state in smaller steps (a sweep, or advance() in time); or refine the grid.',
293
293
  );
294
294
  }
295
+ // The warnings below judge a solution's accuracy (its grid, its double layers). Of the state a
296
+ // steady solve gave up in, which isn't a solution, they say nothing useful (a driven current
297
+ // it couldn't pass leaves the device at ±26 V, where depletion reaches the contacts), so they're
298
+ // left out there; the two about the state itself (degenerate carriers, a species denser than
299
+ // any material) stay, as possible causes, and say they're of that state. (A failed time step
300
+ // restores the last good state, which is a solution: its warnings all stand.)
301
+ const solved = !result.unsolved, stopped = solved ? '' : ' (in the state the unconverged solve stopped in)';
295
302
  const debye = (g) => {
296
303
  const mat = model.materials[model.regions[grid.nodeRegion[g]].material];
297
304
  const s2 = solver.screening(g); // zᵀKz: Σ z² c for ideal statistics
@@ -347,7 +354,7 @@ export function makeSolution(solver, result = {}) {
347
354
  );
348
355
  };
349
356
  interfaces.forEach((itf, f) => {
350
- if (itf.phi.type === 'neutral') return;
357
+ if (itf.phi.type === 'neutral' || !solved) return;
351
358
  const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1];
352
359
  check(gL, grid.segLength[gL - 1], `interfaces[${f}] (left side)`);
353
360
  check(gR, grid.segLength[gR], `interfaces[${f}] (right side)`);
@@ -357,11 +364,11 @@ export function makeSolution(solver, result = {}) {
357
364
  boxed(gR, grid.segLength[gR], itf.phi, gL, `interfaces[${f}] (right side)`, sigma, reacting(1));
358
365
  });
359
366
  const resolves = (ct) => ct.phi.type === 'capacitive' || ct.phi.type === 'pinned';
360
- if (resolves(contacts.left)) {
367
+ if (solved && resolves(contacts.left)) {
361
368
  check(0, grid.segLength[0], 'contacts.left');
362
369
  boxed(0, grid.segLength[0], contacts.left.phi, -1, 'contacts.left');
363
370
  }
364
- if (resolves(contacts.right)) {
371
+ if (solved && resolves(contacts.right)) {
365
372
  check(nNodes - 1, grid.segLength[nNodes - 2], 'contacts.right');
366
373
  boxed(nNodes - 1, grid.segLength[nNodes - 2], contacts.right.phi, -1, 'contacts.right');
367
374
  }
@@ -380,7 +387,7 @@ export function makeSolution(solver, result = {}) {
380
387
  }
381
388
  if (top > mat.cRef[i]) {
382
389
  sol.warnings.push(
383
- `${reg.name}: ${sp.name} reaches ${top.toExponential(2)} mol/m³ at x = ${at.toExponential(3)} m, above its cRef (${mat.cRef[i].toExponential(2)}, the band's effective density of states), where Boltzmann statistics no longer hold: ` +
390
+ `${reg.name}${stopped}: ${sp.name} reaches ${top.toExponential(2)} mol/m³ at x = ${at.toExponential(3)} m, above its cRef (${mat.cRef[i].toExponential(2)}, the band's effective density of states), where Boltzmann statistics no longer hold: ` +
384
391
  'give the material Fermi–Dirac statistics (docs/statistics.md), or ask whether this state is physical (a surface driven past what can carry its carriers away).',
385
392
  );
386
393
  }
@@ -410,7 +417,7 @@ export function makeSolution(solver, result = {}) {
410
417
  }
411
418
  if (top > 1e6) {
412
419
  sol.warnings.push(
413
- `${reg.name}: ${sp.name} reaches ${top.toExponential(2)} mol/m³ at x = ${at.toExponential(3)} m, denser than any material: ` +
420
+ `${reg.name}${stopped}: ${sp.name} reaches ${top.toExponential(2)} mol/m³ at x = ${at.toExponential(3)} m, denser than any material: ` +
414
421
  'a level set where the device cannot follow it. Check what holds it there (the V or bath of a contact, its mu0 and cRef) with describe() from driftlet/kit.',
415
422
  );
416
423
  }
@@ -421,7 +428,7 @@ export function makeSolution(solver, result = {}) {
421
428
  // ρ λ_D at the node beside it; against the region's own space charge (half Σ|ρ| over it, the
422
429
  // two signs of a double layer), warn past 1e-3.
423
430
  for (const side of ['left', 'right']) {
424
- if (contacts[side].phi.type !== 'bulk') continue;
431
+ if (contacts[side].phi.type !== 'bulk' || !solved) continue;
425
432
  const r = side === 'left' ? 0 : model.regions.length - 1, a = grid.regionStart[r], b = grid.regionEnd[r];
426
433
  const mat = model.materials[model.regions[r].material];
427
434
  if (!(mat.epsr > 0) || mat.conductor || b - a < 3) continue;
@@ -458,7 +465,7 @@ export function makeSolution(solver, result = {}) {
458
465
  // a concentration ratio r across it). Flag a region's end cell whose field is well above its
459
466
  // neighbour's while its concentration changes steeply, for species carrying the current. (A
460
467
  // pn depletion region's field changes smoothly from cell to cell, which SG handles.)
461
- if (Number.isFinite(sol.current)) {
468
+ if (solved && Number.isFinite(sol.current)) {
462
469
  const share = species.map((sp, i) =>
463
470
  Math.max(Math.abs(sp.z * (sol.contacts.left.flux[sp.name] ?? 0)), Math.abs(sp.z * (sol.contacts.right.flux[sp.name] ?? 0))),
464
471
  );
package/src/stack.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // that you didn't write: it only moves things to where the definition keeps them, and converts
5
5
  // the kit's doping shorthand (donors and acceptors).
6
6
 
7
- import { DeviceError, checkOptions } from './errors.js';
7
+ import { DeviceError, checkOptions, shown } from './errors.js';
8
8
  import { FARADAY } from './constants.js';
9
9
 
10
10
  const fail = (message) => {
@@ -131,6 +131,7 @@ const STACK_FIELDS = ['T', 'library', 'species', 'materials', 'stack', 'bulkReac
131
131
  export function build(def) {
132
132
  if (!isObject(def)) fail('build: give { library, stack, … }');
133
133
  fields(def, 'build', STACK_FIELDS);
134
+ if (def.T !== undefined && !(typeof def.T === 'number' && Number.isFinite(def.T) && def.T > 0)) fail(`build.T must be the temperature in K, a positive number, got ${shown(def.T)}`);
134
135
  if (def.library !== undefined && !Array.isArray(def.library)) fail('build.library must be a list of { species, materials }');
135
136
  const { species, materials } = combine(...(def.library ?? []), { species: def.species ?? [], materials: def.materials ?? {} });
136
137
 
package/src/types.js CHANGED
@@ -39,6 +39,8 @@
39
39
  * @property {{ species: string, conductivity: number }} [conductor] a metal or fast ion
40
40
  * conductor: its one carrier and conductivity (S/m); its only level is the carrier's (a metal's
41
41
  * Fermi level), and φ is undefined inside
42
+ * @property {number} [T] the temperature its data are for (K): a device at another temperature
43
+ * warns (semiconductor() sets it)
42
44
  */
43
45
 
44
46
  /**
package/types/data.d.ts CHANGED
@@ -15,19 +15,56 @@ export function aqueous(names: string[], { epsr, material }?: {
15
15
  };
16
16
  /**
17
17
  * A semiconductor from `SEMICONDUCTORS`, as a library piece: electrons `e-` and holes `h+`, with
18
- * c_ref = Nc and Nv, D from the mobilities by the Einstein relation at 300 K, and μ°_e⁻ = 0 (φ
19
- * anchored at the conduction band) and μ°_h⁺ = Eg·F. The data are for 300 K, so use `T: 300`.
18
+ * c_ref = Nc and Nv, D from the mobilities by the Einstein relation at T, and μ°_e⁻ = 0 (φ
19
+ * anchored at the conduction band) and μ°_h⁺ = Eg·F. The data are for 300 K; another T (150 to
20
+ * 500 K) moves them: the gap by Varshni's shift from 300 K, Nc and Nv as T^1.5, the mobilities by
21
+ * lattice-limited power laws (light doping). The material carries its `T`, and a device at
22
+ * another temperature warns: give the device the same T.
20
23
  * Statistics are ideal (Boltzmann); add Fermi–Dirac to the material for degenerate doping.
21
24
  * @param {string} name 'Si', 'Ge' or 'GaAs'
22
- * @param {{ material?: string }} [opts]
25
+ * @param {{ material?: string, T?: number }} [opts] the material's name, and the temperature
26
+ * (K, default 300, the table's)
23
27
  * @returns {{ species: object[], materials: Record<string, object> }}
24
28
  */
25
- export function semiconductor(name: string, { material }?: {
29
+ export function semiconductor(name: string, { material, T }?: {
26
30
  material?: string;
31
+ T?: number;
27
32
  }, ...args: any[]): {
28
33
  species: object[];
29
34
  materials: Record<string, object>;
30
35
  };
36
+ /**
37
+ * A trap level in a semiconductor that holds charge: its two charge states as immobile species
38
+ * (D = 0), filled and emptied by capturing carriers, e⁻ + X⁰ = X⁻ and X⁻ + h⁺ = X⁰ (acceptor-like)
39
+ * or X⁺ + e⁻ = X⁰ and X⁰ + h⁺ = X⁺ (donor-like). In steady state it recombines at exactly the
40
+ * `srh` law's rate with the same τ_n, τ_p and n₁, and its charge is in Poisson's equation, which
41
+ * the `srh` law (the trap's occupancy eliminated) leaves out. Each node keeps its own total.
42
+ * @param {{ species: object[], materials: Record<string, object> }} lib a library entry with e⁻
43
+ * and h⁺ (semiconductor()), returned with the trap's states added
44
+ * @param {{ density: number, tauN: number, tauP: number, n1?: number, charge?: 'acceptor' | 'donor',
45
+ * name?: string, material?: string, T?: number }} opts density N_t (mol/m³); τ_n = 1/(c_n N_t)
46
+ * and τ_p = 1/(c_p N_t) (s) at that density; n₁, the electron density that half-fills it
47
+ * (mol/m³; default n_i, midgap); T (K, default the material's T, else 300)
48
+ * @returns {{ library: object, bulkReactions: object[], c0: Record<string, number> }} the library
49
+ * for build(), the capture reactions, and c0 for each layer holding the trap (its total N_t)
50
+ */
51
+ export function trap(lib: {
52
+ species: object[];
53
+ materials: Record<string, object>;
54
+ }, { density, tauN, tauP, n1, charge, name, material, T }?: {
55
+ density: number;
56
+ tauN: number;
57
+ tauP: number;
58
+ n1?: number;
59
+ charge?: "acceptor" | "donor";
60
+ name?: string;
61
+ material?: string;
62
+ T?: number;
63
+ }, ...args: any[]): {
64
+ library: object;
65
+ bulkReactions: object[];
66
+ c0: Record<string, number>;
67
+ };
31
68
  /**
32
69
  * A metal from `METALS` as a conductor material, its carrier `e-`.
33
70
  * @param {string} name
@@ -57,7 +94,7 @@ export const H2O: -237.129e3;
57
94
  export const WATER_EPSR: 78.3;
58
95
  /**
59
96
  * Semiconductors at 300 K (Sze, Physics of Semiconductor Devices, 2nd ed., 1981, appendix):
60
- * relative permittivity, band gap Eg (eV), electron affinity chi (V), effective densities of
97
+ * relative permittivity, band gap Eg (eV), electron affinity chi (eV), effective densities of
61
98
  * states Nc and Nv (cm⁻³), and drift mobilities of lightly doped material mun and mup
62
99
  * (cm²/(V·s)).
63
100
  */
package/types/errors.d.ts CHANGED
@@ -6,6 +6,14 @@
6
6
  * @param {new (message: string) => Error} [Kind]
7
7
  */
8
8
  export function checkOptions(where: string, opts: unknown, allowed: string[], Kind?: new (message: string) => Error): void;
9
+ /**
10
+ * A value as an error message shows it: a number as itself (NaN and Infinity too, which JSON
11
+ * would print as null), a string quoted and called one ("350", a string, where a number was
12
+ * wanted).
13
+ * @param {unknown} v
14
+ * @returns {string}
15
+ */
16
+ export function shown(v: unknown): string;
9
17
  export class DeviceError extends Error {
10
18
  constructor(message: any);
11
19
  }
package/types/kit.d.ts CHANGED
@@ -66,4 +66,4 @@ export { hodgkinHuxley, ghkCurrent } from "./channels.js";
66
66
  export { describe, unitWarnings } from "./describe.js";
67
67
  export { henderson, planck } from "./junction.js";
68
68
  export { perovskiteCell, PEROVSKITE_SCANS, hysteresis } from "./devices.js";
69
- export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal } from "./data.js";
69
+ export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal, trap } from "./data.js";
package/types/types.d.ts CHANGED
@@ -88,6 +88,11 @@ export type MaterialDef = {
88
88
  species: string;
89
89
  conductivity: number;
90
90
  };
91
+ /**
92
+ * the temperature its data are for (K): a device at another temperature
93
+ * warns (semiconductor() sets it)
94
+ */
95
+ T?: number;
91
96
  };
92
97
  export type GridOptions = {
93
98
  /**