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 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.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
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). Where one carrier is plentiful the
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 dilute kernels'
454
- terms are kept aside instead, each evaluated on differences first:
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`$. Everything else (faces, contacts, ports, reactions,
463
- concentrated and metal regions) is assembled into a matrix of its own. Rows that a contact or a
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 an ideal region, linearised as the Scharfetter–Gummel flux is, and
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
@@ -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.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';
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.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';
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.0",
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
- return { species, materials: { [material]: { epsr, species: entries } } };
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 gives the wrong n_i, gap
312
- // and mobilities, silently: say so (for each material a region uses).
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
- `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
+ (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.nI = new Int32Array(nodes); // node term: η column (= its balance row), φ̂ column (= Gauss row)
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
- node(i, p, z, mass, charge) {
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 === i) f += mass * this.nMass[k];
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[i] += mass * nMass[k] * w;
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.rowScale = new Float64Array(N + 1).fill(1); // each row's scaling in the factorised system
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
- const order = [...Array(C).keys()].sort((a, b) => (b < S && this._still(this.stretches[b])) - (a < S && this._still(this.stretches[a])));
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, u, uLo, res, c, z, K, ex, jL, jR } = this;
2694
- const gL = s, gR = s + 1, KL = gL * n * n, KR = gR * n * n;
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 cL = c[gL * n + i], cR = c[gR * n + i];
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
- _srhRate(law, cn, cp, a) {
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 n1, kN = 0, kP = 0, kA = 0; // ln n₁ = kN ln n + kP ln p + kA a + const
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
- n1 = Math.exp((lnnp - a) / 2);
2900
- kN = kP = 0.5;
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 n1 = law.n1;
2903
- const p1 = Math.exp(lnnp - a - Math.log(n1)), np = cn * cp;
2904
- const den = tp * (cn + n1) + tn * (cp + p1), f = -Math.expm1(-a);
2905
- const rate = (np * f) / den;
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 - (u[o] + uLo[o]); // (μ̄_out − μ̄)/RT
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 - (u[o] + uLo[o]));
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) - (u[o] + uLo[o]), 700));
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] + uLo[o] - this.contactEta(side, i);
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] + uLo[b * M] - (Vt - link.zeroCharge) / VT;
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), level = hi + lo;
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] - level;
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.rowScale[offX[b] + r] = 1;
3584
+ this.rowDiv[offX[b] + r] = 1;
3499
3585
  continue;
3500
3586
  }
3501
- const s = 1 / mx;
3502
- this.rowScale[offX[b] + r] = s;
3503
- for (let k = 0; k < mp; k++) A[oa + k] *= s;
3504
- for (let k = 0; k < m; k++) B[ob + k] *= s;
3505
- for (let k = 0; k < mn; k++) C[oc + k] *= s;
3506
- res[offX[b] + r] *= s;
3507
- for (const k of live) termB[k][offX[b] + r] *= s; // (each terminal's ∂/∂V)
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 lin = this.diffTerms ?? (this.diffTerms = new DifferenceTerms(keep.size, this.nNodes * this.n, this.nNodes * this.n));
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, rowScale, floating: fl } = this;
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 * rowScale[j];
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 an ideal region, Σ (g + iω d)·δu over the
4759
- // unknowns idx: the Scharfetter–Gummel fluxes' derivatives (as in _segmentsDilute) and the
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 || !mat.ideal || reg.mixing > 0) continue;
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
@@ -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
- node(i: any, p: any, z: any, mass: any, charge: any): void;
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
- rowScale: Float64Array<ArrayBuffer>;
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;