driftlet 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/data.md +6 -0
- package/docs/device.md +3 -1
- package/docs/numerics.md +10 -5
- package/docs/reliability.md +0 -3
- package/llms.txt +6 -6
- package/package.json +1 -1
- package/src/data.js +4 -2
- package/src/device.js +10 -4
- package/src/difference.js +42 -7
- package/src/solver.js +164 -68
- package/types/data.d.ts +2 -1
- package/types/difference.d.ts +8 -2
- package/types/solver.d.ts +13 -2
package/README.md
CHANGED
|
@@ -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.1.
|
|
59
|
+
`https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/index.js` (and `src/kit.js`, `src/plot.js`).
|
|
60
60
|
TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
|
|
61
61
|
[`llms.txt`](llms.txt).
|
|
62
62
|
|
package/docs/data.md
CHANGED
|
@@ -21,6 +21,12 @@ double layers. The data are in `IONS`, and $`\mu^\circ`$ of liquid water is `H2O
|
|
|
21
21
|
1.011e-14 at 25 °C (pK_w 13.995): neutral water holds 1.0055e-4 mol/m³ of each, which a bath
|
|
22
22
|
with that reaction should hold too.
|
|
23
23
|
|
|
24
|
+
The data are 25 °C's, and the material says so (`T: 298.15`): a device at another temperature
|
|
25
|
+
warns that they don't hold there. Its μ° (so equilibrium constants and electrode potentials),
|
|
26
|
+
permittivity and diffusivities would all move: $`K_w`$ by ~6× between 6 and 25 °C. Scaling them
|
|
27
|
+
(μ° by the standard entropies, ε, D by water's viscosity) is planned, not done. Where only
|
|
28
|
+
transport between solutions of the same ions matters (a membrane's channels), μ° cancels.
|
|
29
|
+
|
|
24
30
|
| Ion | z | D (10⁻⁹ m²/s) | $`\mu^\circ`$ (kJ/mol) |
|
|
25
31
|
|---|---|---|---|
|
|
26
32
|
| H+ | +1 | 9.311 | 0 |
|
package/docs/device.md
CHANGED
|
@@ -250,7 +250,9 @@ r = \frac{n\,p\,(1 - e^{-A/RT})}{(n + n_1)/v_p + (p + p_1)/v_n}, \qquad p_1 = \f
|
|
|
250
250
|
with n and p each at its own side's edge node, the capture velocities $`v_n`$ and $`v_p`$ in m/s,
|
|
251
251
|
and $`n_1`$ (mol/m³) the negative species' concentration with its level at the trap (default:
|
|
252
252
|
$`n_1 = p_1 = n_i`$, a midgap trap). Taking $`p_1`$ from the state keeps equilibrium exact whatever
|
|
253
|
-
lies between the two sides (a band offset, a $`\phi`$ jump).
|
|
253
|
+
lies between the two sides (a band offset, a $`\phi`$ jump). With Fermi–Dirac carriers, each
|
|
254
|
+
one's emission scales by its own $`\gamma = \mathcal{F}_{1/2}(\zeta)/e^\zeta`$, by detailed balance:
|
|
255
|
+
$`n_1`$ becomes $`\gamma_n n_1`$, and $`p_1`$ keeps the product. Where one carrier is plentiful the
|
|
254
256
|
rate saturates at the other's capture, $`r \to v_p\,p`$: holes reaching an electron-rich layer
|
|
255
257
|
recombine at $`v_p`$ however many electrons wait there, which plain mass action can't do.
|
|
256
258
|
|
package/docs/numerics.md
CHANGED
|
@@ -450,8 +450,8 @@ The assembled matrix loses what a flux's two entries share. A Scharfetter–Gumm
|
|
|
450
450
|
$`\eta`$ at its two ends through $`G`$ and $`-G(1 + E)`$, with $`E = \mathrm{expm1}(\Delta\eta)`$;
|
|
451
451
|
for a nearly uniform $`v`$ they cancel, and $`J \cdot v`$ keeps only round-off of $`G|v|`$, which in
|
|
452
452
|
an inversion layer's 0.1 nm cells ($`G`$ ~ 1e11) dwarfs the flux. Where a solve needs
|
|
453
|
-
$`J \cdot v`$ itself (GMRES, in the impedance and in Newton's refined solves), the
|
|
454
|
-
|
|
453
|
+
$`J \cdot v`$ itself (GMRES, in the impedance and in Newton's refined solves), the kernels' terms
|
|
454
|
+
are kept aside instead, each evaluated on differences first:
|
|
455
455
|
|
|
456
456
|
```math
|
|
457
457
|
\delta N = a\,(\delta\eta_L - z\,\delta\hat\phi_L) + b\,(\delta\eta_R - \delta\eta_L) + d\,(\delta\hat\phi_R - \delta\hat\phi_L),
|
|
@@ -459,8 +459,13 @@ terms are kept aside instead, each evaluated on differences first:
|
|
|
459
459
|
|
|
460
460
|
with $`a = -GE`$ (through $`c_L`$), $`b = -G(1 + E)`$ and $`d`$ the drift's. A node's storage and space
|
|
461
461
|
charge depend on $`\delta\eta_i - z_i\,\delta\hat\phi`$ alone, the displacement on
|
|
462
|
-
$`\delta\hat\phi_R - \delta\hat\phi_L`$.
|
|
463
|
-
|
|
462
|
+
$`\delta\hat\phi_R - \delta\hat\phi_L`$. Non-ideal statistics couple the species ($`c_i`$
|
|
463
|
+
depends on every $`\zeta_j`$), so there a node's storage of $`i`$ has a term in each
|
|
464
|
+
$`\delta\eta_j - z_j\,\delta\hat\phi`$, through $`K_{ij}`$, and a flux has side terms in those at
|
|
465
|
+
either end (through $`c_L`$ and the excess potentials in its drift); each is in the same
|
|
466
|
+
differences. (Until 1.1.1 they were assembled directly, and a Fermi–Dirac MOS capacitor in
|
|
467
|
+
inversion, barely degenerate, came out with a negative resistance.) Everything else (faces,
|
|
468
|
+
contacts, ports, reactions, metal regions) is assembled into a matrix of its own. Rows that a contact or a
|
|
464
469
|
steady solve replaces drop the kept terms; the transformed rows of strictly neutral nodes combine
|
|
465
470
|
them as they combine the matrix's. A region's uniform shift ($`z_i s`$ on each $`\eta_i`$, $`s`$ on
|
|
466
471
|
$`\hat\phi`$) then gives exactly zero inside it, as it must. It replaced a central difference of
|
|
@@ -649,7 +654,7 @@ majority conductances times a $`\delta\eta`$ cancelling its neighbours' to 1e-17
|
|
|
649
654
|
round-off reads as a constant conductance of order 1 S/m², a silicon p⁺n⁺ junction's capacitance
|
|
650
655
|
9× off at 100 Hz. Between the two contacts of a device without ports, the total current
|
|
651
656
|
(conduction plus displacement, $`i\omega\,\delta D`$) is the same through every cut, so it is also
|
|
652
|
-
read across each segment of
|
|
657
|
+
read across each segment of a region (with any statistics), linearised as its flux is, and
|
|
653
658
|
the reading with the least estimated error is taken: in that junction, the depletion region's;
|
|
654
659
|
in a MOS capacitor, the oxide's displacement. A reading's error is its terms' round-off, and the
|
|
655
660
|
solves' error in $`\delta x`$ (their residual, relative to its largest entry) through its
|
package/docs/reliability.md
CHANGED
|
@@ -173,9 +173,6 @@ so a change that makes one worse shows.
|
|
|
173
173
|
- **A surface covered to a bare fraction below ~1e-10** barely affects anything any more, and
|
|
174
174
|
the solver struggles with it (the hard corpus's passivating drop without a double layer stops
|
|
175
175
|
partway through its hour); the solution warns, and suggests a less stable film.
|
|
176
|
-
- **A double layer across more than ~350 thermal voltages** (between 9 and 9.25 V at 25 °C, in a
|
|
177
|
-
crowded lattice-gas electrolyte) doesn't solve, from a cold start or warm, and its warning
|
|
178
|
-
doesn't say why. Ionic-liquid windows (a few volts) are well inside it.
|
|
179
176
|
- **Currents below ~0.1 nA/m²** (a GaAs junction's leakage without recombination) aren't resolved
|
|
180
177
|
between solves; such a current is zero for any practical purpose.
|
|
181
178
|
- **A failed solve says so:** `converged: false`, with a warning naming where the system lost
|
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.1.
|
|
34
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.
|
|
35
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.
|
|
33
|
+
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/index.js';
|
|
34
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/kit.js';
|
|
35
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/plot.js';
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`. Every export is listed at the end.
|
|
@@ -219,9 +219,9 @@ a CDN needs a long virtual-time budget, 30 s.)
|
|
|
219
219
|
<span id="readout"></span>
|
|
220
220
|
<div id="figure"></div>
|
|
221
221
|
<script type="module">
|
|
222
|
-
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.
|
|
223
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.
|
|
224
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.
|
|
222
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/index.js';
|
|
223
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/kit.js';
|
|
224
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1.1/src/plot.js';
|
|
225
225
|
|
|
226
226
|
const def = build({
|
|
227
227
|
T: 300,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driftlet",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Dependency-free 1D drift–diffusion–reaction (Poisson–Nernst–Planck) solver for semiconductors, electrochemistry and nerves, fast enough for live web demos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/data.js
CHANGED
|
@@ -66,7 +66,8 @@ export const METALS = freeze(
|
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Water with the named ions, as a library piece for `build()`: each ion as a species
|
|
69
|
-
* (c_ref = 1 M) and the material `water` holding them, with D and μ° from `IONS`.
|
|
69
|
+
* (c_ref = 1 M) and the material `water` holding them, with D and μ° from `IONS`. The data are
|
|
70
|
+
* 25 °C's, and the material says so (T: 298.15): a device at another temperature warns.
|
|
70
71
|
* @param {string[]} names ions from `IONS`
|
|
71
72
|
* @param {{ epsr?: number, material?: string }} [opts] permittivity (default that of water;
|
|
72
73
|
* 0 makes the solution strictly neutral, without double layers), and the material's name
|
|
@@ -82,7 +83,8 @@ export function aqueous(names, { epsr = WATER_EPSR, material = 'water' } = {}) {
|
|
|
82
83
|
species.push({ name, z: ion.z, cRef: 1000 });
|
|
83
84
|
entries[name] = { D: ion.D, mu0: ion.mu0 };
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
+
// (25 °C data: a device at another temperature warns)
|
|
87
|
+
return { species, materials: { [material]: { epsr, species: entries, T: 298.15 } } };
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
// How the semiconductors' data move with temperature, for semiconductor(name, { T }):
|
package/src/device.js
CHANGED
|
@@ -308,14 +308,20 @@ export function normalizeDevice(def) {
|
|
|
308
308
|
};
|
|
309
309
|
});
|
|
310
310
|
|
|
311
|
-
// A material whose data are for another temperature than the device's
|
|
312
|
-
// and mobilities
|
|
311
|
+
// A material whose data are for another temperature than the device's is silently wrong there
|
|
312
|
+
// (a semiconductor's n_i, gap and mobilities; water's μ°, ε and D): say so (for each material a
|
|
313
|
+
// region uses).
|
|
313
314
|
materials.forEach((m, k) => {
|
|
314
315
|
if (m.dataT === undefined || Math.abs(m.dataT - T) <= 0.01 || !regions.some((reg) => reg.material === k)) return;
|
|
316
|
+
const own = Object.keys(def.materials[m.name].species ?? {}), semi = own.includes('e-') && own.includes('h+');
|
|
315
317
|
warnings.push(
|
|
316
318
|
`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
|
-
|
|
318
|
-
|
|
319
|
+
(semi
|
|
320
|
+
? `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}, ` +
|
|
321
|
+
`or remake the material for ${T} K (semiconductor(name, { T: ${T} }))`
|
|
322
|
+
: `so they don't hold there: its μ° (so equilibrium constants and electrode potentials, Kw's by ~6× between 6 and 25 °C), ` +
|
|
323
|
+
`permittivity and diffusivities are ${m.dataT} K's. Give the device T: ${m.dataT}, or give the material data for ${T} K ` +
|
|
324
|
+
`and its T (aqueous() has none at other temperatures yet)`),
|
|
319
325
|
);
|
|
320
326
|
});
|
|
321
327
|
|
package/src/difference.js
CHANGED
|
@@ -8,13 +8,19 @@
|
|
|
8
8
|
// storage and space charge depend on δη_i − z_i δφ̂ alone; the displacement on δφ̂_R − δφ̂_L. A
|
|
9
9
|
// uniform shift of a region (z_i s on each η_i, s on φ̂) then gives exactly zero, as it must.
|
|
10
10
|
//
|
|
11
|
+
// Non-ideal statistics couple the species: c_i depends on every ζ_j, so a node's storage of i
|
|
12
|
+
// has a term in each δη_j − z_j δφ̂ (K_ij), and a flux has side terms in those at either end (its
|
|
13
|
+
// c_L and the excess potentials in its drift). Each is a term in the same differences, so the
|
|
14
|
+
// shift still gives exactly zero.
|
|
15
|
+
//
|
|
11
16
|
// Indices are compact (the solver's rix), with the sink (index N) for an unknown that isn't one:
|
|
12
17
|
// vectors passed in are read there as zero.
|
|
13
18
|
export class DifferenceTerms {
|
|
14
|
-
constructor(N, nodes, segments) {
|
|
19
|
+
constructor(N, nodes, segments, sides = 0) {
|
|
15
20
|
this.N = N;
|
|
16
21
|
this.dead = new Uint8Array(N + 1); // rows replaced since (a contact's level, a held stretch)
|
|
17
|
-
this.
|
|
22
|
+
this.nR = new Int32Array(nodes); // node term: its balance row, η column, φ̂ column (= Gauss row)
|
|
23
|
+
this.nI = new Int32Array(nodes);
|
|
18
24
|
this.nP = new Int32Array(nodes);
|
|
19
25
|
this.nZ = new Float64Array(nodes);
|
|
20
26
|
this.nMass = new Float64Array(nodes); // storage per unit (δη − z δφ̂), times 1/dt (or iω)
|
|
@@ -24,6 +30,9 @@ export class DifferenceTerms {
|
|
|
24
30
|
this.sC = new Float64Array(3 * segments); // a, b, d
|
|
25
31
|
this.dI = new Int32Array(2 * segments); // displacement: φ̂_L, φ̂_R
|
|
26
32
|
this.dK = new Float64Array(segments);
|
|
33
|
+
this.xI = new Int32Array(4 * sides); // a flux's side term: its two rows, an η column, its φ̂ column
|
|
34
|
+
this.xZ = new Float64Array(sides);
|
|
35
|
+
this.xC = new Float64Array(sides);
|
|
27
36
|
this.combos = []; // [rk, ri, w]: row rk += w × row ri (strictly neutral nodes' charge rows)
|
|
28
37
|
this.t = new Float64Array(N + 1);
|
|
29
38
|
this.x = new Float64Array(N + 1);
|
|
@@ -33,13 +42,16 @@ export class DifferenceTerms {
|
|
|
33
42
|
// Afresh for an assembly; `rate` multiplies storage in captured rows (1/dt).
|
|
34
43
|
reset(rate = 0) {
|
|
35
44
|
this.rate = rate;
|
|
36
|
-
this.nn = this.ns = this.nd = 0;
|
|
45
|
+
this.nn = this.ns = this.nd = this.nx = 0;
|
|
37
46
|
this.dead.fill(0);
|
|
38
47
|
this.combos.length = 0;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
// Storage in row r (mass) and charge in Gauss row p, per unit δη_i − z δφ̂ (row r = i, unless
|
|
51
|
+
// non-ideal statistics couple the species).
|
|
52
|
+
node(i, p, z, mass, charge, r = i) {
|
|
42
53
|
const k = this.nn++;
|
|
54
|
+
this.nR[k] = r;
|
|
43
55
|
this.nI[k] = i;
|
|
44
56
|
this.nP[k] = p;
|
|
45
57
|
this.nZ[k] = z;
|
|
@@ -59,6 +71,17 @@ export class DifferenceTerms {
|
|
|
59
71
|
this.sC[3 * k + 2] = d;
|
|
60
72
|
}
|
|
61
73
|
|
|
74
|
+
// A flux from row rL to row rR, per unit δη_i − z δφ̂ at one end.
|
|
75
|
+
side(rL, rR, i, p, z, w) {
|
|
76
|
+
const k = this.nx++;
|
|
77
|
+
this.xI[4 * k] = rL;
|
|
78
|
+
this.xI[4 * k + 1] = rR;
|
|
79
|
+
this.xI[4 * k + 2] = i;
|
|
80
|
+
this.xI[4 * k + 3] = p;
|
|
81
|
+
this.xZ[k] = z;
|
|
82
|
+
this.xC[k] = w;
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
displacement(pL, pR, k) {
|
|
63
86
|
const q = this.nd++;
|
|
64
87
|
this.dI[2 * q] = pL;
|
|
@@ -83,7 +106,7 @@ export class DifferenceTerms {
|
|
|
83
106
|
for (let k = 0; k < this.nn; k++) {
|
|
84
107
|
const i = this.nI[k], p = this.nP[k], z = this.nZ[k];
|
|
85
108
|
let f = 0;
|
|
86
|
-
if (row ===
|
|
109
|
+
if (row === this.nR[k]) f += mass * this.nMass[k];
|
|
87
110
|
if (row === p) f += this.nCharge[k];
|
|
88
111
|
if (f === 0) continue;
|
|
89
112
|
add(i, f);
|
|
@@ -99,6 +122,12 @@ export class DifferenceTerms {
|
|
|
99
122
|
add(this.sI[4 * k + 2], s * (-z * a - d));
|
|
100
123
|
add(this.sI[4 * k + 3], s * d);
|
|
101
124
|
}
|
|
125
|
+
for (let k = 0; k < this.nx; k++) {
|
|
126
|
+
const s = row === this.xI[4 * k] ? 1 : row === this.xI[4 * k + 1] ? -1 : 0;
|
|
127
|
+
if (s === 0) continue;
|
|
128
|
+
add(this.xI[4 * k + 2], s * this.xC[k]);
|
|
129
|
+
add(this.xI[4 * k + 3], -s * this.xZ[k] * this.xC[k]);
|
|
130
|
+
}
|
|
102
131
|
for (let q = 0; q < this.nd; q++) {
|
|
103
132
|
const pL = this.dI[2 * q], pR = this.dI[2 * q + 1], s = row === pL ? 1 : row === pR ? -1 : 0;
|
|
104
133
|
if (s === 0) continue;
|
|
@@ -114,10 +143,10 @@ export class DifferenceTerms {
|
|
|
114
143
|
for (let j = 0; j < N; j++) x[j] = v[j];
|
|
115
144
|
x[N] = 0;
|
|
116
145
|
t.fill(0);
|
|
117
|
-
const { nI, nP, nZ, nMass, nCharge } = this;
|
|
146
|
+
const { nR, nI, nP, nZ, nMass, nCharge } = this;
|
|
118
147
|
for (let k = 0; k < this.nn; k++) {
|
|
119
148
|
const i = nI[k], p = nP[k], w = x[i] - nZ[k] * x[p];
|
|
120
|
-
t[
|
|
149
|
+
t[nR[k]] += mass * nMass[k] * w;
|
|
121
150
|
t[p] += steady * nCharge[k] * w;
|
|
122
151
|
}
|
|
123
152
|
if (steady !== 0) {
|
|
@@ -128,6 +157,12 @@ export class DifferenceTerms {
|
|
|
128
157
|
t[iL] += dN;
|
|
129
158
|
t[iR] -= dN;
|
|
130
159
|
}
|
|
160
|
+
const { xI, xZ, xC } = this;
|
|
161
|
+
for (let k = 0; k < this.nx; k++) {
|
|
162
|
+
const dN = steady * xC[k] * (x[xI[4 * k + 2]] - xZ[k] * x[xI[4 * k + 3]]);
|
|
163
|
+
t[xI[4 * k]] += dN;
|
|
164
|
+
t[xI[4 * k + 1]] -= dN;
|
|
165
|
+
}
|
|
131
166
|
for (let q = 0; q < this.nd; q++) {
|
|
132
167
|
const pL = dI[2 * q], pR = dI[2 * q + 1], dD = -steady * dK[q] * (x[pR] - x[pL]);
|
|
133
168
|
t[pL] += dD;
|
package/src/solver.js
CHANGED
|
@@ -661,7 +661,7 @@ export class Solver {
|
|
|
661
661
|
}
|
|
662
662
|
this.res = new Float64Array(N + 1);
|
|
663
663
|
this.delta = new Float64Array(N + 1);
|
|
664
|
-
this.
|
|
664
|
+
this.rowDiv = new Float64Array(N + 1).fill(1); // what each row is divided by in the factorised system
|
|
665
665
|
}
|
|
666
666
|
|
|
667
667
|
_factor() {
|
|
@@ -801,6 +801,19 @@ export class Solver {
|
|
|
801
801
|
this._laws();
|
|
802
802
|
}
|
|
803
803
|
|
|
804
|
+
// A stretch's amount from its regions' starting concentrations (c0), per unit area, before
|
|
805
|
+
// any state exists: Infinity where one isn't given (set by a level, or doping), to order it last.
|
|
806
|
+
_roughAmount(st) {
|
|
807
|
+
const { regions, grid } = this.model;
|
|
808
|
+
let t = 0;
|
|
809
|
+
for (let q = st.regions[0]; q <= st.regions[1]; q++) {
|
|
810
|
+
const reg = regions[q], c0 = reg.c0Profile[st.species] ? profileAt(reg.c0Profile[st.species], grid.x[grid.regionStart[q]]) : reg.c0[st.species];
|
|
811
|
+
if (!(c0 >= 0)) return Infinity;
|
|
812
|
+
t += c0 * (grid.x[grid.regionEnd[q]] - grid.x[grid.regionStart[q]]);
|
|
813
|
+
}
|
|
814
|
+
return t;
|
|
815
|
+
}
|
|
816
|
+
|
|
804
817
|
// Whether a stretch's species moves through it (mobile in every region, or a metal's carrier),
|
|
805
818
|
// or stays still in every region (D = 0: trap states, conserved node by node).
|
|
806
819
|
_moves(st) {
|
|
@@ -1305,7 +1318,18 @@ export class Solver {
|
|
|
1305
1318
|
// Reduced row echelon form; the free columns give the null space. Immobile stretches' columns
|
|
1306
1319
|
// go first, so that a combination of them alone (trap states, X⁰ + X⁻) comes out on its own,
|
|
1307
1320
|
// to be kept node by node, apart from any that moves (e⁻ − X⁰, the electrons the traps hold).
|
|
1308
|
-
|
|
1321
|
+
// Then the stretches by their starting amounts, least first, and by name: a trace species
|
|
1322
|
+
// pivoted after an abundant one comes out added to it (A + B ⇌ C with B at 1 M and A at 1e-14
|
|
1323
|
+
// gave B + C and A − B, where A + C was lost to round-off), and the basis followed the listing.
|
|
1324
|
+
const still = (k) => (k < S && this._still(this.stretches[k]) ? 1 : 0);
|
|
1325
|
+
const rough = this.stretches.map((st) => this._roughAmount(st)), name = (k) => model.species[this.stretches[k].species].name;
|
|
1326
|
+
const order = [...Array(C).keys()].sort((a, b) => {
|
|
1327
|
+
if (still(a) !== still(b)) return still(b) - still(a);
|
|
1328
|
+
if (a >= S || b >= S) return a - b;
|
|
1329
|
+
if (rough[a] !== rough[b]) return rough[a] - rough[b];
|
|
1330
|
+
const na = name(a), nb = name(b);
|
|
1331
|
+
return na < nb ? -1 : na > nb ? 1 : a - b;
|
|
1332
|
+
});
|
|
1309
1333
|
const pivots = [];
|
|
1310
1334
|
let r = 0;
|
|
1311
1335
|
for (const col of order) {
|
|
@@ -2270,10 +2294,23 @@ export class Solver {
|
|
|
2270
2294
|
return;
|
|
2271
2295
|
}
|
|
2272
2296
|
let q = this.rhoFixed[g], dq = 0;
|
|
2297
|
+
const lin = this.lin;
|
|
2273
2298
|
for (let i = 0; i < n; i++) {
|
|
2274
2299
|
const k = g * n + i, r = 1 + i;
|
|
2275
2300
|
if (!this.present[k]) continue;
|
|
2276
2301
|
const Kz = this._Kz(g, i);
|
|
2302
|
+
if (lin) {
|
|
2303
|
+
// In difference form (see DifferenceTerms): storage of i and its charge, in each
|
|
2304
|
+
// δη_j − z_j δφ̂ through K_ij.
|
|
2305
|
+
res[R[b * M + r]] += (v * (c[k] - this.cOld[k])) / dt;
|
|
2306
|
+
q += F * z[i] * c[k];
|
|
2307
|
+
const Kg = g * n * n + i * n;
|
|
2308
|
+
for (let j = 0; j < n; j++) {
|
|
2309
|
+
const Kij = this.nodeIdeal[g] ? (i === j ? c[k] : 0) : this.K[Kg + j];
|
|
2310
|
+
if (Kij !== 0) lin.node(R[b * M + 1 + j], R[b * M], z[j], v * Kij, -v * F * z[i] * Kij, R[b * M + r]);
|
|
2311
|
+
}
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2277
2314
|
this._storage(g, i, dt, Kz);
|
|
2278
2315
|
q += F * z[i] * c[k];
|
|
2279
2316
|
dq += F * z[i] * Kz;
|
|
@@ -2282,7 +2319,7 @@ export class Solver {
|
|
|
2282
2319
|
// (where no charge responds and no field reaches, φ isn't defined, nor an unknown)
|
|
2283
2320
|
if (!this.phiUndefined[g]) {
|
|
2284
2321
|
res[R[b * M]] -= v * q;
|
|
2285
|
-
this._j(b, 0, b, 0, v * dq);
|
|
2322
|
+
if (!lin) this._j(b, 0, b, 0, v * dq);
|
|
2286
2323
|
}
|
|
2287
2324
|
}
|
|
2288
2325
|
|
|
@@ -2420,7 +2457,7 @@ export class Solver {
|
|
|
2420
2457
|
aHi += rx.nu[p] * u[b * M + 1 + rx.sp[p]];
|
|
2421
2458
|
aLo += rx.nu[p] * uLo[b * M + 1 + rx.sp[p]];
|
|
2422
2459
|
}
|
|
2423
|
-
const { rate, Cn, Cp, Ca } = this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], aHi + aLo);
|
|
2460
|
+
const { rate, Cn, Cp, Ca } = this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], aHi + aLo, ...this._srhDegen(g, g, law));
|
|
2424
2461
|
dr.fill(0);
|
|
2425
2462
|
this._dlnc(g, law.n.i, Cn, dr);
|
|
2426
2463
|
this._dlnc(g, law.p.i, Cp, dr);
|
|
@@ -2690,39 +2727,28 @@ export class Solver {
|
|
|
2690
2727
|
// still exactly zero at equilibrium. c_L and ex depend on every ζ at their node through K.
|
|
2691
2728
|
_segmentConcentrated(s, bL, bR, mat, h, vel) {
|
|
2692
2729
|
const R = this.rix;
|
|
2693
|
-
const { n, M,
|
|
2694
|
-
const
|
|
2730
|
+
const { n, M, res, z, K } = this;
|
|
2731
|
+
const KL = s * n * n, KR = (s + 1) * n * n;
|
|
2695
2732
|
for (let i = 0; i < n; i++) {
|
|
2696
2733
|
if (!mat.present[i] || mat.D[i] === 0) continue;
|
|
2697
2734
|
const r = 1 + i, zi = z[i];
|
|
2698
|
-
const
|
|
2699
|
-
const g = mat.D[i] / h;
|
|
2700
|
-
const pe = (vel * h) / mat.D[i];
|
|
2701
|
-
const d = zi * (u[bR * M] - u[bL * M]) + ex[gR * n + i] - ex[gL * n + i] - pe;
|
|
2702
|
-
const deta = u[bR * M + r] - u[bL * M + r] + (uLo[bR * M + r] - uLo[bL * M + r]) - pe;
|
|
2703
|
-
const E = Math.expm1(deta);
|
|
2704
|
-
const B = bernoulli(d), Bp = bernoulliDerivative(d);
|
|
2705
|
-
const gBc = g * B * cL;
|
|
2706
|
-
const N = -gBc * E;
|
|
2707
|
-
const dNdd = -g * Bp * cL * E;
|
|
2708
|
-
// Left node: ∂N/∂ζ_Lj = −gE(B + B′)K_L,ij − dNdd·δ_ij, plus the direct η_L,i term.
|
|
2709
|
-
const fL = -g * E * (B + Bp);
|
|
2710
|
-
jL[0] = -fL * this._Kz(gL, i);
|
|
2711
|
-
for (let j = 0; j < n; j++) jL[1 + j] = fL * K[KL + i * n + j];
|
|
2712
|
-
jL[r] += -dNdd + gBc * (E + 1);
|
|
2713
|
-
// Right node: ∂N/∂ζ_Rj = dNdd·(δ_ij − K_R,ij/c_R), plus the direct η_R,i term.
|
|
2714
|
-
jR.fill(0);
|
|
2715
|
-
if (cR > 0) {
|
|
2716
|
-
const fR = -dNdd / cR;
|
|
2717
|
-
jR[0] = -fR * this._Kz(gR, i);
|
|
2718
|
-
for (let j = 0; j < n; j++) jR[1 + j] = fR * K[KR + i * n + j];
|
|
2719
|
-
} else {
|
|
2720
|
-
jR[0] = dNdd * zi; // K/c → δ as c → 0
|
|
2721
|
-
jR[r] -= dNdd;
|
|
2722
|
-
}
|
|
2723
|
-
jR[r] += dNdd - gBc * (E + 1);
|
|
2735
|
+
const { N, E, gBc, dNdd, fL, cR } = this._fluxConcentrated(s, bL, bR, mat, h, vel, i);
|
|
2724
2736
|
res[R[bL * M + r]] += N;
|
|
2725
2737
|
res[R[bR * M + r]] -= N;
|
|
2738
|
+
if (this.lin) {
|
|
2739
|
+
// In difference form (see DifferenceTerms): b on Δη_i and z_i·∂N/∂d on Δφ̂, and the rest
|
|
2740
|
+
// as side terms in each δη_j − z_j δφ̂ at either end (c_L and the excess potentials).
|
|
2741
|
+
const rL = R[bL * M + r], rR = R[bR * M + r], pL = R[bL * M], pR = R[bR * M];
|
|
2742
|
+
this.lin.segment(rL, rR, pL, pR, zi, 0, -gBc * (E + 1), zi * dNdd);
|
|
2743
|
+
for (let j = 0; j < n; j++) {
|
|
2744
|
+
const wL = fL * K[KL + i * n + j] - (j === i ? dNdd : 0);
|
|
2745
|
+
const wR = cR > 0 ? dNdd * ((j === i ? 1 : 0) - K[KR + i * n + j] / cR) : 0;
|
|
2746
|
+
if (wL !== 0) this.lin.side(rL, rR, R[bL * M + 1 + j], pL, z[j], wL);
|
|
2747
|
+
if (wR !== 0) this.lin.side(rL, rR, R[bR * M + 1 + j], pR, z[j], wR);
|
|
2748
|
+
}
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
const { jL, jR } = this;
|
|
2726
2752
|
for (let k = 0; k < M; k++) {
|
|
2727
2753
|
if (jL[k] !== 0) {
|
|
2728
2754
|
this._j(bL, r, bL, k, jL[k]);
|
|
@@ -2736,6 +2762,40 @@ export class Solver {
|
|
|
2736
2762
|
}
|
|
2737
2763
|
}
|
|
2738
2764
|
|
|
2765
|
+
// Species i's flux across segment s of a non-ideal region, toward +x, and its derivatives in
|
|
2766
|
+
// this.jL and this.jR (over the unknowns of the blocks bL and bR).
|
|
2767
|
+
_fluxConcentrated(s, bL, bR, mat, h, vel, i) {
|
|
2768
|
+
const { n, M, u, uLo, c, z, K, ex, jL, jR } = this;
|
|
2769
|
+
const gL = s, gR = s + 1, KL = gL * n * n, KR = gR * n * n, r = 1 + i, zi = z[i];
|
|
2770
|
+
const cL = c[gL * n + i], cR = c[gR * n + i];
|
|
2771
|
+
const g = mat.D[i] / h;
|
|
2772
|
+
const pe = (vel * h) / mat.D[i];
|
|
2773
|
+
const d = zi * (u[bR * M] - u[bL * M]) + ex[gR * n + i] - ex[gL * n + i] - pe;
|
|
2774
|
+
const deta = u[bR * M + r] - u[bL * M + r] + (uLo[bR * M + r] - uLo[bL * M + r]) - pe;
|
|
2775
|
+
const E = Math.expm1(deta);
|
|
2776
|
+
const B = bernoulli(d), Bp = bernoulliDerivative(d);
|
|
2777
|
+
const gBc = g * B * cL;
|
|
2778
|
+
const N = -gBc * E;
|
|
2779
|
+
const dNdd = -g * Bp * cL * E;
|
|
2780
|
+
// Left node: ∂N/∂ζ_Lj = −gE(B + B′)K_L,ij − dNdd·δ_ij, plus the direct η_L,i term.
|
|
2781
|
+
const fL = -g * E * (B + Bp);
|
|
2782
|
+
jL[0] = -fL * this._Kz(gL, i);
|
|
2783
|
+
for (let j = 0; j < n; j++) jL[1 + j] = fL * K[KL + i * n + j];
|
|
2784
|
+
jL[r] += -dNdd + gBc * (E + 1);
|
|
2785
|
+
// Right node: ∂N/∂ζ_Rj = dNdd·(δ_ij − K_R,ij/c_R), plus the direct η_R,i term.
|
|
2786
|
+
jR.fill(0);
|
|
2787
|
+
if (cR > 0) {
|
|
2788
|
+
const fR = -dNdd / cR;
|
|
2789
|
+
jR[0] = -fR * this._Kz(gR, i);
|
|
2790
|
+
for (let j = 0; j < n; j++) jR[1 + j] = fR * K[KR + i * n + j];
|
|
2791
|
+
} else {
|
|
2792
|
+
jR[0] = dNdd * zi; // K/c → δ as c → 0
|
|
2793
|
+
jR[r] -= dNdd;
|
|
2794
|
+
}
|
|
2795
|
+
jR[r] += dNdd - gBc * (E + 1);
|
|
2796
|
+
return { N, E, gBc, dNdd, fL, cR };
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2739
2799
|
// Ohmic conduction in a metal, in mixed form. The flux J (slot 0 of the left node) obeys
|
|
2740
2800
|
// η_R − η_L + J/g = 0, g = σ RT/(z²F² h) (Ohm's law, J = −g Δη),
|
|
2741
2801
|
// and enters the two nodes' carrier balances as outflow and inflow.
|
|
@@ -2856,7 +2916,7 @@ export class Solver {
|
|
|
2856
2916
|
aLo += rx.nu[p] * uLo[b * M + 1 + rx.sp[p]];
|
|
2857
2917
|
}
|
|
2858
2918
|
const law = rx.srh?.[this.nodeMaterial[g]];
|
|
2859
|
-
if (law) return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], aHi + aLo).rate;
|
|
2919
|
+
if (law) return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], aHi + aLo, ...this._srhDegen(g, g, law)).rate;
|
|
2860
2920
|
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g];
|
|
2861
2921
|
for (let p = 0; p < rx.sp.length; p++) if (rx.nu[p] > 0) P *= rx.nu[p] === 1 ? c[g * n + rx.sp[p]] : powi(c[g * n + rx.sp[p]], rx.nu[p]);
|
|
2862
2922
|
return P * -Math.expm1(-(aHi + aLo));
|
|
@@ -2870,7 +2930,7 @@ export class Solver {
|
|
|
2870
2930
|
if (law) {
|
|
2871
2931
|
let a = rx.fixedA;
|
|
2872
2932
|
for (let p = 0; p < rx.sp.length; p++) a += rx.nu[p] * (u[b * M + 1 + rx.sp[p]] + uLo[b * M + 1 + rx.sp[p]]);
|
|
2873
|
-
return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], a).forward;
|
|
2933
|
+
return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], a, ...this._srhDegen(g, g, law)).forward;
|
|
2874
2934
|
}
|
|
2875
2935
|
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g];
|
|
2876
2936
|
for (let p = 0; p < rx.sp.length; p++) if (rx.nu[p] > 0) P *= rx.nu[p] === 1 ? c[g * n + rx.sp[p]] : powi(c[g * n + rx.sp[p]], rx.nu[p]);
|
|
@@ -2884,7 +2944,7 @@ export class Solver {
|
|
|
2884
2944
|
let a = rx.fixedA;
|
|
2885
2945
|
for (let p = 0; p < rx.sp.length; p++) a += rx.nu[p] * (u[b * M + 1 + rx.sp[p]] + uLo[b * M + 1 + rx.sp[p]]);
|
|
2886
2946
|
const law = rx.srh?.[this.nodeMaterial[g]];
|
|
2887
|
-
if (law) return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], a).oneWay;
|
|
2947
|
+
if (law) return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], a, ...this._srhDegen(g, g, law)).oneWay;
|
|
2888
2948
|
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g];
|
|
2889
2949
|
for (let p = 0; p < rx.sp.length; p++) if (rx.nu[p] > 0) P *= rx.nu[p] === 1 ? c[g * n + rx.sp[p]] : powi(c[g * n + rx.sp[p]], rx.nu[p]);
|
|
2890
2950
|
return P * Math.max(1, Math.exp(-a));
|
|
@@ -2892,21 +2952,46 @@ export class Solver {
|
|
|
2892
2952
|
|
|
2893
2953
|
// SRH kinetics (see srhLaw in device.js): the rate, and its sensitivities to ln n, ln p and
|
|
2894
2954
|
// a = A/RT. With no n₁ given, the trap is midgap: n₁ = p₁ = n_i = √(n p e^{−a}).
|
|
2895
|
-
|
|
2955
|
+
// SRH at electron and hole concentrations cn, cp and affinity a. n₁p₁ = n p e^{−a} (n_i² for
|
|
2956
|
+
// Boltzmann carriers), and under degeneracy each carrier's emission scales with its own
|
|
2957
|
+
// γ = e^{−ex} (its excess chemical potential ex, c/K = ∂ln c/∂ζ its slope): n₁ = γ_n n₁₀ and
|
|
2958
|
+
// p₁ = γ_p p₁₀, n₁₀ the law's n1, or n_i at midgap. (γ_n γ_p split evenly onto both, as it was,
|
|
2959
|
+
// put a degenerate n⁺ region's γ_n on its holes' emission too.) The rate's derivatives in ln n,
|
|
2960
|
+
// ln p and a. For large negative a (strong extraction), np·(1 − e^{−a}) as −n p e^{−a}.
|
|
2961
|
+
_srhRate(law, cn, cp, a, xn = 0, xp = 0, sn = 1, sp = 1) {
|
|
2896
2962
|
const { tn, tp } = law, lnnp = Math.log(cn) + Math.log(cp);
|
|
2897
|
-
let
|
|
2963
|
+
let lnn1, kN = 0, kP = 0, kA = 0; // ln n₁ = kN ln n + kP ln p + kA a + const
|
|
2898
2964
|
if (Number.isNaN(law.n1)) {
|
|
2899
|
-
|
|
2900
|
-
kN =
|
|
2965
|
+
lnn1 = (lnnp - a - xn + xp) / 2; // γ_n n_i, with n_i² = n p e^{−a}/(γ_n γ_p)
|
|
2966
|
+
kN = (2 - sn) / 2;
|
|
2967
|
+
kP = sp / 2;
|
|
2901
2968
|
kA = -0.5;
|
|
2902
|
-
} else
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2969
|
+
} else {
|
|
2970
|
+
lnn1 = Math.log(law.n1) - xn;
|
|
2971
|
+
kN = 1 - sn;
|
|
2972
|
+
}
|
|
2973
|
+
const n1 = Math.exp(lnn1), p1 = Math.exp(lnnp - a - lnn1), np = cn * cp;
|
|
2974
|
+
const den = tp * (cn + n1) + tn * (cp + p1);
|
|
2975
|
+
// np(1 − e^{−a}), with e^{−a} folded into np where it would overflow
|
|
2976
|
+
const f = -Math.expm1(-a), num = a > -700 ? np * f : np - Math.exp(lnnp - a);
|
|
2977
|
+
const rate = num / den;
|
|
2906
2978
|
const dDn = tp * cn + tp * n1 * kN + tn * p1 * (1 - kN);
|
|
2907
2979
|
const dDp = tp * n1 * kP + tn * cp + tn * p1 * (1 - kP);
|
|
2908
2980
|
const dDa = tp * n1 * kA + tn * p1 * (-1 - kA);
|
|
2909
|
-
return { rate, forward: np / den, oneWay: (np / den) * Math.max(1, Math.exp(-a)), Cn: rate * (1 - dDn / den), Cp: rate * (1 - dDp / den), Ca: (np / den) * (1 - f) - (rate * dDa) / den };
|
|
2981
|
+
return { rate, forward: np / den, oneWay: a > -700 ? (np / den) * Math.max(1, Math.exp(-a)) : Math.exp(lnnp - a) / den, Cn: rate * (1 - dDn / den), Cp: rate * (1 - dDp / den), Ca: (a > -700 ? (np / den) * (1 - f) : Math.exp(lnnp - a) / den) - (rate * dDa) / den };
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// _srhRate's degeneracy arguments: the electrons' at node gn, the holes' at gq.
|
|
2985
|
+
_srhDegen(gn, gq, law) {
|
|
2986
|
+
const [xn, sn] = this._degen(gn, law.n.i), [xp, sp] = this._degen(gq, law.p.i);
|
|
2987
|
+
return [xn, xp, sn, sp];
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// A carrier's excess chemical potential at node g and ∂ln c/∂ζ there (0 and 1 if ideal).
|
|
2991
|
+
_degen(g, i) {
|
|
2992
|
+
if (this.nodeIdeal[g]) return [0, 1];
|
|
2993
|
+
const n = this.n, c = this.c[g * n + i], K = this.K[g * n * n + i * n + i];
|
|
2994
|
+
return [this.ex[g * n + i], c > 0 && K > 0 ? c / K : 1];
|
|
2910
2995
|
}
|
|
2911
2996
|
|
|
2912
2997
|
// A face reaction (see normalizeFaceReactions). Its rate r_k is an unknown of the face block,
|
|
@@ -2949,7 +3034,7 @@ export class Solver {
|
|
|
2949
3034
|
} else if (rx.srh) {
|
|
2950
3035
|
// SRH, with n and p each at its own side's edge node.
|
|
2951
3036
|
const { n: pn, p: pp } = rx.srh, gn = pn.side ? gR : gL, gq = pp.side ? gR : gL;
|
|
2952
|
-
const s = this._srhRate(rx.srh, c[gn * n + pn.i], c[gq * n + pp.i], aHi + aLo);
|
|
3037
|
+
const s = this._srhRate(rx.srh, c[gn * n + pn.i], c[gq * n + pp.i], aHi + aLo, ...this._srhDegen(gn, gq, { n: pn, p: pp }));
|
|
2953
3038
|
rate = s.rate;
|
|
2954
3039
|
this._dlnc(gn, pn.i, s.Cn, pn.side ? dR : dL);
|
|
2955
3040
|
this._dlnc(gq, pp.i, s.Cp, pp.side ? dR : dL);
|
|
@@ -3049,7 +3134,7 @@ export class Solver {
|
|
|
3049
3134
|
const r = 1 + i, target = this.portEta(port, i), zF = z[i] * F;
|
|
3050
3135
|
for (const g of port.nodes) {
|
|
3051
3136
|
const b = this.blockOfNode[g], o = b * M + r, v = vol[g];
|
|
3052
|
-
const deta = target -
|
|
3137
|
+
const deta = target - u[o] - uLo[o]; // (μ̄_out − μ̄)/RT
|
|
3053
3138
|
if (link.type === 'equilibrium') {
|
|
3054
3139
|
// a device end node's level is its contact's business, where the contact links it
|
|
3055
3140
|
if ((g === 0 && this.model.contacts.left.species[i].type !== 'blocked') || (g === this.nNodes - 1 && this.model.contacts.right.species[i].type !== 'blocked')) continue;
|
|
@@ -3294,7 +3379,7 @@ export class Solver {
|
|
|
3294
3379
|
const link = ct.species[i];
|
|
3295
3380
|
const o = b * M + 1 + i;
|
|
3296
3381
|
if (link.type === 'exchange') {
|
|
3297
|
-
res[R[o]] -= Ac * link.k * (link.mu / model.RT -
|
|
3382
|
+
res[R[o]] -= Ac * link.k * (link.mu / model.RT - u[o] - uLo[o]);
|
|
3298
3383
|
this._j(b, 1 + i, b, 1 + i, Ac * link.k);
|
|
3299
3384
|
} else if (link.type === 'conductance') {
|
|
3300
3385
|
const Vi = (VT * (u[o] + uLo[o])) / z[i];
|
|
@@ -3303,7 +3388,7 @@ export class Solver {
|
|
|
3303
3388
|
B[R[o]] += (-Ac * link.G) / (z[i] * F);
|
|
3304
3389
|
} else if (link.type === 'velocity') {
|
|
3305
3390
|
// N_in = v c (e^(η_out − η) − 1): v (c_eq − c), c_eq in equilibrium with the outside.
|
|
3306
|
-
const ci = c[g * n + i], E = Math.exp(Math.min(this.contactEta(side, i) -
|
|
3391
|
+
const ci = c[g * n + i], E = Math.exp(Math.min(this.contactEta(side, i) - u[o] - uLo[o], 700));
|
|
3307
3392
|
const N = Ac * link.v * ci * (E - 1), d = this.dA;
|
|
3308
3393
|
res[R[o]] -= N;
|
|
3309
3394
|
d.fill(0);
|
|
@@ -3321,7 +3406,7 @@ export class Solver {
|
|
|
3321
3406
|
const o = b * M + 1 + i;
|
|
3322
3407
|
this._replaceRow(b, 1 + i);
|
|
3323
3408
|
this._j(b, 1 + i, b, 1 + i, 1);
|
|
3324
|
-
res[R[o]] = u[o]
|
|
3409
|
+
res[R[o]] = u[o] - this.contactEta(side, i) + uLo[o]; // (the big terms first: u + uLo rounds to u)
|
|
3325
3410
|
if (z[i] !== 0) B[R[o]] += -z[i] / VT;
|
|
3326
3411
|
}
|
|
3327
3412
|
|
|
@@ -3360,7 +3445,7 @@ export class Solver {
|
|
|
3360
3445
|
if (link.type === 'pinned') {
|
|
3361
3446
|
// φ_edge = V_t − zeroCharge (the C → ∞ limit).
|
|
3362
3447
|
this._j(b, 0, b, 0, 1);
|
|
3363
|
-
res[R[b * M]] = u[b * M]
|
|
3448
|
+
res[R[b * M]] = u[b * M] - (Vt - link.zeroCharge) / VT + uLo[b * M];
|
|
3364
3449
|
B[R[b * M]] += -1 / VT;
|
|
3365
3450
|
} else {
|
|
3366
3451
|
// Plain bulk: local neutrality.
|
|
@@ -3422,14 +3507,14 @@ export class Solver {
|
|
|
3422
3507
|
const { M, u, uLo, res, z, VT, model } = this, R = this.rix;
|
|
3423
3508
|
for (const st of this.flatStretches) {
|
|
3424
3509
|
const i = st.species, open = st.leftOpen || st.rightOpen;
|
|
3425
|
-
const { k, hi, lo } = this._flatLevel(st)
|
|
3510
|
+
const { k, hi, lo } = this._flatLevel(st);
|
|
3426
3511
|
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
3427
3512
|
const b = this.blockOfNode[g], o = b * M + 1 + i;
|
|
3428
3513
|
if (this.loc[o] < 0) continue;
|
|
3429
3514
|
this._replaceRow(b, 1 + i);
|
|
3430
3515
|
for (const B of this.termB) B[R[o]] = 0; // (the row's old terminal terms: the end node's own link)
|
|
3431
3516
|
this._j(b, 1 + i, b, 1 + i, 1);
|
|
3432
|
-
res[R[o]] = u[o] + uLo[o] -
|
|
3517
|
+
res[R[o]] = u[o] - hi + (uLo[o] - lo); // (the big terms first: hi + lo rounds to hi)
|
|
3433
3518
|
if (open && z[i] !== 0) this.termB[k][R[o]] += -z[i] / VT;
|
|
3434
3519
|
}
|
|
3435
3520
|
// A face inside the stretch whose link holds the level continuous carries the species' flux
|
|
@@ -3482,7 +3567,8 @@ export class Solver {
|
|
|
3482
3567
|
if (b < nB - 1) sys.C.fill(0, sys.offC[b] + l * sz[b + 1], sys.offC[b] + (l + 1) * sz[b + 1]);
|
|
3483
3568
|
}
|
|
3484
3569
|
|
|
3485
|
-
// Scale every row by its largest Jacobian entry (in place, residual too)
|
|
3570
|
+
// Scale every row by its largest Jacobian entry (in place, residual too), dividing by it: its
|
|
3571
|
+
// reciprocal overflows where it's subnormal (a co-ion crowded out of a double layer, ~1e-310).
|
|
3486
3572
|
_equilibrate() {
|
|
3487
3573
|
const { nB, sys, res, termB } = this, live = this._liveTerms();
|
|
3488
3574
|
const { A, B, C, sizes, offA, offB, offC, offX } = sys;
|
|
@@ -3495,16 +3581,16 @@ export class Solver {
|
|
|
3495
3581
|
for (let k = 0; k < m; k++) mx = Math.max(mx, Math.abs(B[ob + k]));
|
|
3496
3582
|
for (let k = 0; k < mn; k++) mx = Math.max(mx, Math.abs(C[oc + k]));
|
|
3497
3583
|
if (mx === 0) {
|
|
3498
|
-
this.
|
|
3584
|
+
this.rowDiv[offX[b] + r] = 1;
|
|
3499
3585
|
continue;
|
|
3500
3586
|
}
|
|
3501
|
-
const
|
|
3502
|
-
this.
|
|
3503
|
-
for (let k = 0; k < mp; k++) A[oa + k]
|
|
3504
|
-
for (let k = 0; k < m; k++) B[ob + k]
|
|
3505
|
-
for (let k = 0; k < mn; k++) C[oc + k]
|
|
3506
|
-
res[offX[b] + r]
|
|
3507
|
-
for (const k of live) termB[k][offX[b] + r]
|
|
3587
|
+
const d = mx;
|
|
3588
|
+
this.rowDiv[offX[b] + r] = d;
|
|
3589
|
+
for (let k = 0; k < mp; k++) A[oa + k] /= d;
|
|
3590
|
+
for (let k = 0; k < m; k++) B[ob + k] /= d;
|
|
3591
|
+
for (let k = 0; k < mn; k++) C[oc + k] /= d;
|
|
3592
|
+
res[offX[b] + r] /= d;
|
|
3593
|
+
for (const k of live) termB[k][offX[b] + r] /= d; // (each terminal's ∂/∂V)
|
|
3508
3594
|
}
|
|
3509
3595
|
}
|
|
3510
3596
|
}
|
|
@@ -3515,7 +3601,8 @@ export class Solver {
|
|
|
3515
3601
|
_assembleDifference(dt) {
|
|
3516
3602
|
const keep = this.sys;
|
|
3517
3603
|
const rest = this.sysRest ?? (this.sysRest = new BlockTridiagonal(keep.n, keep.sizes));
|
|
3518
|
-
const
|
|
3604
|
+
const n = this.n, nN = this.nNodes;
|
|
3605
|
+
const lin = this.diffTerms ?? (this.diffTerms = new DifferenceTerms(keep.size, nN * n * n, nN * n, 2 * nN * n * n));
|
|
3519
3606
|
this.sys = rest;
|
|
3520
3607
|
this.lin = lin;
|
|
3521
3608
|
try {
|
|
@@ -3536,7 +3623,7 @@ export class Solver {
|
|
|
3536
3623
|
// solve's and leave refined (in the transformed unknowns, as _solveBordered gives them); false
|
|
3537
3624
|
// if refining didn't help, leaving them as they came.
|
|
3538
3625
|
_refine(dt, delta, deltaV) {
|
|
3539
|
-
const { res, termRes, termB, termC, termDI,
|
|
3626
|
+
const { res, termRes, termB, termC, termDI, rowDiv, floating: fl } = this;
|
|
3540
3627
|
const N = this.sys.size, K = fl.length, pins = this.pins, R = this.rix;
|
|
3541
3628
|
const save = { res: Float64Array.from(res), termRes: Float64Array.from(termRes), termB: termB.map((b) => Float64Array.from(b)), termC: termC.map((c) => Float64Array.from(c)), termDI: Float64Array.from(termDI), transformed: this.transformed, pinRes: this.pins.map((p) => p.res) };
|
|
3542
3629
|
const restoreArrays = () => {
|
|
@@ -3568,7 +3655,7 @@ export class Solver {
|
|
|
3568
3655
|
for (let j = 0; j < N; j++) {
|
|
3569
3656
|
let t = jv[j];
|
|
3570
3657
|
for (let a = 0; a < K; a++) t += Bk[a][j] * x[N + a];
|
|
3571
|
-
out[j] = t
|
|
3658
|
+
out[j] = t / rowDiv[j];
|
|
3572
3659
|
}
|
|
3573
3660
|
for (let a = 0; a < K; a++) {
|
|
3574
3661
|
let t = Dk[a] * x[N + a];
|
|
@@ -4755,9 +4842,9 @@ export class Solver {
|
|
|
4755
4842
|
}
|
|
4756
4843
|
|
|
4757
4844
|
// The cuts where the impedance may read a small-signal current. Each is the linearised total
|
|
4758
|
-
// current toward +x through one segment s of
|
|
4759
|
-
//
|
|
4760
|
-
// displacement's. Without ports it's the same through every cut; with them, only between a
|
|
4845
|
+
// current toward +x through one segment s of a region, Σ (g + iω d)·δu over the unknowns idx:
|
|
4846
|
+
// the Scharfetter–Gummel fluxes' derivatives (as in _segmentsDilute, or _segmentConcentrated
|
|
4847
|
+
// for non-ideal statistics) and the displacement's. Without ports it's the same through every cut; with them, only between a
|
|
4761
4848
|
// contact and the nearest port's window (see _impedance).
|
|
4762
4849
|
_currentCuts() {
|
|
4763
4850
|
const { model, n, M, u, uLo, c, z } = this, F = FARADAY;
|
|
@@ -4765,7 +4852,7 @@ export class Solver {
|
|
|
4765
4852
|
const cuts = [];
|
|
4766
4853
|
for (let r = 0; r < regions.length; r++) {
|
|
4767
4854
|
const reg = regions[r], mat = materials[reg.material];
|
|
4768
|
-
if (mat.conductor ||
|
|
4855
|
+
if (mat.conductor || reg.mixing > 0) continue;
|
|
4769
4856
|
const vel = reg.velocity;
|
|
4770
4857
|
for (let s = grid.regionStart[r]; s < grid.regionEnd[r]; s++) {
|
|
4771
4858
|
const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s];
|
|
@@ -4779,6 +4866,15 @@ export class Solver {
|
|
|
4779
4866
|
const phiL = u[bL * M], phiR = u[bR * M], hasPhi = !this.phiUndefined[s];
|
|
4780
4867
|
for (let i = 0; i < n; i++) {
|
|
4781
4868
|
if (z[i] === 0 || !mat.present[i] || mat.D[i] === 0) continue;
|
|
4869
|
+
if (!mat.ideal) {
|
|
4870
|
+
this._fluxConcentrated(s, bL, bR, mat, h, vel, i);
|
|
4871
|
+
for (let k = 0; k < M; k++) {
|
|
4872
|
+
if (k === 0 && !hasPhi) continue;
|
|
4873
|
+
if (this.jL[k] !== 0) add(bL * M + k, z[i] * F * this.jL[k], 0);
|
|
4874
|
+
if (this.jR[k] !== 0) add(bR * M + k, z[i] * F * this.jR[k], 0);
|
|
4875
|
+
}
|
|
4876
|
+
continue;
|
|
4877
|
+
}
|
|
4782
4878
|
const zi = z[i], r1 = 1 + i, cL = c[s * n + i], g = mat.D[i] / h;
|
|
4783
4879
|
const pe = (vel * h) / mat.D[i];
|
|
4784
4880
|
const d = zi * (phiR - phiL) - pe;
|
package/types/data.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Water with the named ions, as a library piece for `build()`: each ion as a species
|
|
3
|
-
* (c_ref = 1 M) and the material `water` holding them, with D and μ° from `IONS`.
|
|
3
|
+
* (c_ref = 1 M) and the material `water` holding them, with D and μ° from `IONS`. The data are
|
|
4
|
+
* 25 °C's, and the material says so (T: 298.15): a device at another temperature warns.
|
|
4
5
|
* @param {string[]} names ions from `IONS`
|
|
5
6
|
* @param {{ epsr?: number, material?: string }} [opts] permittivity (default that of water;
|
|
6
7
|
* 0 makes the solution strictly neutral, without double layers), and the material's name
|
package/types/difference.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export class DifferenceTerms {
|
|
2
|
-
constructor(N: any, nodes: any, segments: any);
|
|
2
|
+
constructor(N: any, nodes: any, segments: any, sides?: number);
|
|
3
3
|
N: any;
|
|
4
4
|
dead: Uint8Array<any>;
|
|
5
|
+
nR: Int32Array<any>;
|
|
5
6
|
nI: Int32Array<any>;
|
|
6
7
|
nP: Int32Array<any>;
|
|
7
8
|
nZ: Float64Array<any>;
|
|
@@ -12,6 +13,9 @@ export class DifferenceTerms {
|
|
|
12
13
|
sC: Float64Array<ArrayBuffer>;
|
|
13
14
|
dI: Int32Array<ArrayBuffer>;
|
|
14
15
|
dK: Float64Array<any>;
|
|
16
|
+
xI: Int32Array<ArrayBuffer>;
|
|
17
|
+
xZ: Float64Array<ArrayBuffer>;
|
|
18
|
+
xC: Float64Array<ArrayBuffer>;
|
|
15
19
|
combos: any[];
|
|
16
20
|
t: Float64Array<any>;
|
|
17
21
|
x: Float64Array<any>;
|
|
@@ -20,8 +24,10 @@ export class DifferenceTerms {
|
|
|
20
24
|
nn: number;
|
|
21
25
|
ns: number;
|
|
22
26
|
nd: number;
|
|
23
|
-
|
|
27
|
+
nx: number;
|
|
28
|
+
node(i: any, p: any, z: any, mass: any, charge: any, r?: any): void;
|
|
24
29
|
segment(iL: any, iR: any, pL: any, pR: any, z: any, a: any, b: any, d: any): void;
|
|
30
|
+
side(rL: any, rR: any, i: any, p: any, z: any, w: any): void;
|
|
25
31
|
displacement(pL: any, pR: any, k: any): void;
|
|
26
32
|
kill(row: any): void;
|
|
27
33
|
combine(rk: any, ri: any, w: any): void;
|
package/types/solver.d.ts
CHANGED
|
@@ -131,7 +131,7 @@ export class Solver {
|
|
|
131
131
|
fullOf: Int32Array<ArrayBuffer>;
|
|
132
132
|
res: Float64Array<ArrayBuffer>;
|
|
133
133
|
delta: Float64Array<ArrayBuffer>;
|
|
134
|
-
|
|
134
|
+
rowDiv: Float64Array<ArrayBuffer>;
|
|
135
135
|
_factor(): void;
|
|
136
136
|
_solveLinear(rhs: any, delta: any): void;
|
|
137
137
|
_findStretches(): void;
|
|
@@ -139,6 +139,7 @@ export class Solver {
|
|
|
139
139
|
stretchOf: Int32Array<ArrayBuffer>;
|
|
140
140
|
moieties: Float64Array<any>[];
|
|
141
141
|
flatStretches: any[];
|
|
142
|
+
_roughAmount(st: any): number;
|
|
142
143
|
_moves(st: any): boolean;
|
|
143
144
|
_still(st: any): boolean;
|
|
144
145
|
_feeds(k: any, st: any): any;
|
|
@@ -226,6 +227,14 @@ export class Solver {
|
|
|
226
227
|
_faceVoltage(f: any): number;
|
|
227
228
|
_faceGates(f: any, bf: any, bL: any, bR: any): void;
|
|
228
229
|
_segmentConcentrated(s: any, bL: any, bR: any, mat: any, h: any, vel: any): void;
|
|
230
|
+
_fluxConcentrated(s: any, bL: any, bR: any, mat: any, h: any, vel: any, i: any): {
|
|
231
|
+
N: number;
|
|
232
|
+
E: number;
|
|
233
|
+
gBc: number;
|
|
234
|
+
dNdd: number;
|
|
235
|
+
fL: number;
|
|
236
|
+
cR: number;
|
|
237
|
+
};
|
|
229
238
|
_segmentConductor(s: any, bL: any, bR: any, mat: any, h: any): void;
|
|
230
239
|
_segmentMixing(s: any, bL: any, bR: any, Dm: any, h: any, mat: any): void;
|
|
231
240
|
mixWork: Float64Array<any>[];
|
|
@@ -233,7 +242,7 @@ export class Solver {
|
|
|
233
242
|
bulkRate(rx: any, g: any): number;
|
|
234
243
|
bulkForward(rx: any, g: any): any;
|
|
235
244
|
bulkOneWay(rx: any, g: any): number;
|
|
236
|
-
_srhRate(law: any, cn: any, cp: any, a: any): {
|
|
245
|
+
_srhRate(law: any, cn: any, cp: any, a: any, xn?: number, xp?: number, sn?: number, sp?: number): {
|
|
237
246
|
rate: number;
|
|
238
247
|
forward: number;
|
|
239
248
|
oneWay: number;
|
|
@@ -241,6 +250,8 @@ export class Solver {
|
|
|
241
250
|
Cp: number;
|
|
242
251
|
Ca: number;
|
|
243
252
|
};
|
|
253
|
+
_srhDegen(gn: any, gq: any, law: any): number[];
|
|
254
|
+
_degen(g: any, i: any): number[];
|
|
244
255
|
_faceReaction(rx: any, k: any, f: any, bf: any, bL: any, bR: any, Af: any): void;
|
|
245
256
|
_liveTerms(): any;
|
|
246
257
|
_allTerms: any;
|