driftlet 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/docs/data.md +125 -10
- package/docs/device.md +32 -12
- 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 +39 -18
- package/src/errors.js +18 -0
- package/src/index.js +3 -1
- package/src/kit.js +1 -1
- package/src/polarization.js +2 -2
- package/src/solution.js +14 -7
- package/src/solver.js +48 -14
- package/src/stack.js +2 -1
- package/src/types.js +4 -1
- package/types/data.d.ts +42 -5
- package/types/errors.d.ts +8 -0
- package/types/kit.d.ts +1 -1
- package/types/solver.d.ts +2 -0
- package/types/types.d.ts +7 -1
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.
|
|
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 } } }]
|
|
@@ -479,10 +490,10 @@ contacts: {
|
|
|
479
490
|
- `{ type: 'exchange', k, mu }` (neutral species):
|
|
480
491
|
$`N_{\mathrm{in}} = k \cdot (\mu_{\mathrm{out}} - \mu)/RT`$, k in mol/(m²·s).
|
|
481
492
|
- `{ type: 'velocity', v, offset }` (or `mu`, for a neutral species): a surface velocity,
|
|
482
|
-
$`N_{\mathrm{in}} = v\,(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
level difference
|
|
493
|
+
$`N_{\mathrm{in}} = v\,c\,(e^{(\bar\mu_{\mathrm{out}} - \bar\mu)/RT} - 1)`$ with v in m/s: for dilute
|
|
494
|
+
species that's $`v\,(c_{\mathrm{eq}} - c)`$, $`c_{\mathrm{eq}}`$ the end node's concentration in
|
|
495
|
+
equilibrium with the outside level, and with any statistics it keeps the flux's sign that of
|
|
496
|
+
the level difference (though it isn't then $`v\,(c_{\mathrm{eq}} - c)`$, which saturates). It lies between `'blocked'` (v → 0) and
|
|
486
497
|
`'equilibrium'` (v → ∞): a contact's surface recombination velocity, or thermionic emission
|
|
487
498
|
over a Schottky barrier (below).
|
|
488
499
|
- `'blocked'`: no flux.
|
|
@@ -666,10 +677,16 @@ An electrode port can carry species on its surface, sharing its sites: an adsorb
|
|
|
666
677
|
or a passive film taken as a coverage. Each has a coverage $`\theta`$ at every node of the window,
|
|
667
678
|
Langmuir statistics, $`\mu = \mu^\circ + RT\ln(\theta/\theta_0)`$ with $`\theta_0 = 1 - \sum\theta`$ the
|
|
668
679
|
bare fraction, and `capacity` $`\Gamma`$, mol of sites per m² of electrode. Reactions name them like
|
|
669
|
-
species, and
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
680
|
+
species, and their kinetics are Langmuir: in the Butler–Volmer prefactor each surface species
|
|
681
|
+
counts at its coverage $`\theta`$ (as a solution species does at $`c/c_{\mathrm{ref}}`$), and a reaction
|
|
682
|
+
that fills $`\Delta`$ sites going forward holds $`m = \max(\Delta, 0)`$ bare ones in its transition
|
|
683
|
+
state, so its forward rate goes as $`\theta_0^{\,m}`$ and its backward as $`\theta_0^{\,m-\Delta}`$.
|
|
684
|
+
Adsorption goes on at a rate ∝ $`\theta_0`$ and off ∝ $`\theta`$, an adsorbed couple converts at a
|
|
685
|
+
rate ∝ its coverage, and $`2\mathrm{H_{ads}} \to \mathrm{H_2}`$ goes as $`\theta_H^2`$ and back as
|
|
686
|
+
$`\theta_0^2`$. A reaction with `bare: true` runs only on bare metal, needing a bare site even
|
|
687
|
+
where it fills none ($`m \ge 1`$): a metal dissolving, blocked by a film. Equilibrium is exact
|
|
688
|
+
throughout, from the affinity. A species' `theta0` is its starting coverage (not the bare
|
|
689
|
+
fraction). Surface species carry no charge, so an adsorbed redox couple
|
|
673
690
|
balances its electrons through the solution: `'Oads + e- = Rads + Cl-'`, its coverages at
|
|
674
691
|
$`\theta_O/\theta_R = e^{(E - E^\circ)/V_T}`$ against the chloride's level.
|
|
675
692
|
|
|
@@ -679,7 +696,7 @@ ports: [{
|
|
|
679
696
|
surface: { 'Fe(OH)2': { mu0: -490e3, capacity: 2e-5, theta0: 1e-6 } }, // theta0: the start
|
|
680
697
|
reactions: [
|
|
681
698
|
{ equation: 'Fe2+ + 2 e- = Fe(s)', fixed: { 'Fe(s)': 0 }, k0: 5e-6, alpha: 0.5, bare: true }, // dissolves where bare
|
|
682
|
-
{ equation: 'Fe(s) + 2 OH- = Fe(OH)2 + 2 e-', fixed: { 'Fe(s)': 0 }, k0: 1e-4, alpha: 0.5
|
|
699
|
+
{ equation: 'Fe(s) + 2 OH- = Fe(OH)2 + 2 e-', fixed: { 'Fe(s)': 0 }, k0: 1e-4, alpha: 0.5 }, // the film forms (in minutes), on bare sites
|
|
683
700
|
{ equation: 'O2 + 2 H2O + 4 e- = 4 OH-', fixed: { H2O: -237.13e3 }, k0: 3e-9, alpha: 0.125 }, // on film or not
|
|
684
701
|
],
|
|
685
702
|
}]
|
|
@@ -994,6 +1011,9 @@ const now = dev.solution(); // snapshot of the current state
|
|
|
994
1011
|
mid-run, `set({ contacts: { right: { bath: { c } } } })`, is a step in the sample.) Either way the time stepping restarts its
|
|
995
1012
|
order, as after any discontinuity, but a carried state goes on at the step size an adaptive
|
|
996
1013
|
transient had reached, so a page may change a rate constant or a diffusivity every frame.
|
|
1014
|
+
To change the temperature, give the materials for it in the same patch,
|
|
1015
|
+
`dev.set({ T: 350, materials: semiconductor('Si', { T: 350 }).materials })`: a `T` alone
|
|
1016
|
+
changes kT but not the materials' data (a material that carries its `T` then warns).
|
|
997
1017
|
|
|
998
1018
|
For example, a silver nitrate cell between silver electrodes: its impedance spectrum, then the
|
|
999
1019
|
current transient after a voltage step.
|
|
@@ -1041,7 +1061,7 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
|
|
|
1041
1061
|
| `bulkReactions[k]` | `{ rate, forward, nu, regions, total }`: each bulk reaction's net forward rate at every node (mol/(m³·s), `NaN` where it doesn't run), its one-way forward rate (the backward is `forward − rate`), what one forward reaction makes of each species (`nu`, negative for what it consumes), and integrated over each region and the device (mol/(m²·s)), as the balances count it: a charge-balance check is J = F(generation − recombination), which the kit's `check()` does for every species |
|
|
1042
1062
|
| `charge` | total charge in the device, C/m² (C through a cross-section) |
|
|
1043
1063
|
| `conservation` | per species stretch: amount, reference, intake through contacts, and drift, relative: (amount − reference − intake) over the largest of them |
|
|
1044
|
-
| `warnings` | e.g. unresolved double layers, conventions a statistics model relies on, and for a failed solve, where the system is nearly singular |
|
|
1064
|
+
| `warnings` | e.g. unresolved double layers, conventions a statistics model relies on, a material's data for another temperature, and for a failed solve, why (a driven current it can't pass, where the system is nearly singular). A failed steady solve's state isn't a solution, so the warnings about a solution's accuracy (the grid, double layers, space charge reaching a contact) are left out of it |
|
|
1045
1065
|
| `converged`, `iterations`, `steps`, `substeps`, `history`, `time`, `T` | solver bookkeeping, and the temperature |
|
|
1046
1066
|
| `steady` | whether this is a converged steady state (from `solve()`), not a transient's: what `check()` in the kit decides its checks by |
|
|
1047
1067
|
| `done`, `rejected`, `trace` | from `advance()`: whether `tEnd` was reached; rejected steps; `{ t, current, voltage }` per accepted step |
|
package/docs/kit.md
CHANGED
|
@@ -68,6 +68,15 @@ console.log(`I = ${sol.current.toFixed(1)} A/m²`);
|
|
|
68
68
|
- **`library`** takes pieces with `species` and `materials`, merged with any given directly
|
|
69
69
|
(`combine()` does the merging on its own). A species or material given twice must be given
|
|
70
70
|
identically.
|
|
71
|
+
- **`trap(lib, { density, tauN, tauP, n1, charge, name, material, T })`** adds a trap level that
|
|
72
|
+
holds charge to a semiconductor's library piece: its two states as immobile species
|
|
73
|
+
(`X0`/`X-` for `charge: 'acceptor'`, the default; `X+`/`X0` for `'donor'`), filled and emptied
|
|
74
|
+
by capturing electrons and holes. It returns `{ library, bulkReactions, c0 }`: the library to
|
|
75
|
+
give `build()` in place of `lib`, the two capture reactions, and the trap's total for each
|
|
76
|
+
layer that has it (`layer('Si', L, { donors, c0: t.c0 })`). In steady state it recombines at the
|
|
77
|
+
`srh` law's rate with the same `tauN`, `tauP` (s, at that density) and `n1` (mol/m³, default
|
|
78
|
+
n_i); unlike `srh`, its charge is in Poisson's equation
|
|
79
|
+
([device](device.md#bulk-reactions)).
|
|
71
80
|
- Nested lists in the stack are flattened, so a helper can return several items.
|
|
72
81
|
- Reactions pass through as written (see below), and so do `T`, `grid`, `ports` and `geometry`.
|
|
73
82
|
|
|
@@ -395,7 +404,11 @@ warnings:
|
|
|
395
404
|
`warnings` check this again against the solved concentrations);
|
|
396
405
|
- an outside (a bath, a contact's levels) out of equilibrium with a bulk reaction that runs at the
|
|
397
406
|
contact, which would run there for ever: H⁺ and OH⁻ in a bath a little off $`K_w`$ pass a current
|
|
398
|
-
at zero bias
|
|
407
|
+
at zero bias;
|
|
408
|
+
- the definition's own warnings, which every solution repeats: a material whose data are for
|
|
409
|
+
another temperature than the device's (`semiconductor('Si')` is 300 K's; the device's default
|
|
410
|
+
is 298.15 K), a starting profile the grid doesn't resolve, conventions a statistics model
|
|
411
|
+
relies on.
|
|
399
412
|
|
|
400
413
|
They're heuristics, prompts to check, never errors.
|
|
401
414
|
|
|
@@ -424,14 +437,17 @@ page can show that its numbers hold up. Pass the solution `solve()` or `advance(
|
|
|
424
437
|
|
|
425
438
|
- **converged**: the solve converged.
|
|
426
439
|
- **warnings**: the solution's warnings (unresolved double layers, steep profiles, a weakly held
|
|
427
|
-
part) and the definition's likely unit slips. They're heuristics, so they're marked `?`, a
|
|
440
|
+
part, a material's data for another temperature) and the definition's likely unit slips. They're heuristics, so they're marked `?`, a
|
|
428
441
|
prompt to look, and don't fail the check.
|
|
429
442
|
- **balance**, in a steady state: each species' ledger, what comes in through each terminal and
|
|
430
443
|
what each reaction (bulk, or at a face) makes or uses, in mol/(m²·s) and for a charged species
|
|
431
444
|
as a current. The terms must sum to zero. A species that a face lets through only by a law (a
|
|
432
445
|
membrane's permeability, a conductance) or by a reaction has a ledger on each side, with what
|
|
433
446
|
crosses as a term: in a cell, Na⁺ leaks into the cytoplasm as fast as the pump puts it out. Under light it's J = F(G − R), and it says where
|
|
434
|
-
every carrier went: collected, or recombined in the bulk or at which face.
|
|
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.
|
|
34
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
35
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
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.
|
|
213
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
214
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
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.
|
|
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
|
});
|