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 +2 -2
- package/docs/data.md +125 -10
- package/docs/device.md +17 -3
- package/docs/kit.md +19 -3
- package/docs/reliability.md +1 -0
- package/docs/templates.md +1 -1
- package/llms.txt +31 -16
- package/package.json +1 -1
- package/src/checks.js +7 -2
- package/src/data.js +100 -11
- package/src/describe.js +1 -1
- package/src/device.js +30 -15
- package/src/errors.js +18 -0
- package/src/index.js +3 -1
- package/src/kit.js +1 -1
- package/src/solution.js +14 -7
- package/src/stack.js +2 -1
- package/src/types.js +2 -0
- package/types/data.d.ts +42 -5
- package/types/errors.d.ts +8 -0
- package/types/kit.d.ts +1 -1
- package/types/types.d.ts +5 -0
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
|
|
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
|
|
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
|
|
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
|
|
62
|
-
Statistics are ideal (Boltzmann); add Fermi–Dirac
|
|
63
|
-
doping. The data are in `SEMICONDUCTORS`, in the
|
|
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`$ (
|
|
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
|
|
74
|
-
|
|
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.
|
|
447
|
+
every carrier went: collected, or recombined in the bulk or at which face. Each reaction is a
|
|
448
|
+
term named by its equation; two with the same equation (radiative and SRH recombination, both
|
|
449
|
+
`e- + h+ = 0`) are told apart by their place, `e- + h+ = 0 (bulkReactions[1])`, and
|
|
450
|
+
`sol.bulkReactions[k]` has each one's rates. A species at rest
|
|
435
451
|
(in equilibrium, flows only of round-off against what it could carry) isn't listed.
|
|
436
452
|
- **conservation**, in a transient: every closed, unreacting stretch keeps what it held plus
|
|
437
453
|
what came in.
|
package/docs/reliability.md
CHANGED
|
@@ -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⁺
|
|
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
|
|
34
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0
|
|
35
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0
|
|
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
|
|
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
|
|
81
|
-
so n_i = √(N_c N_v) e^(−E_g/2RT), 6.7e9 cm⁻³ for Si.
|
|
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.
|
|
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
|
-
|
|
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
|
|
213
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0
|
|
214
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.0
|
|
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
|
|
645
|
-
|
|
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
|
|
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 =
|
|
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 = `${
|
|
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 (
|
|
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
|
|
90
|
-
* anchored at the conduction band) and μ°_h⁺ = Eg·F. The data are for 300 K
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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
|
|
19
|
-
* anchored at the conduction band) and μ°_h⁺ = Eg·F. The data are for 300 K
|
|
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 (
|
|
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
|
/**
|