driftlet 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +285 -0
  3. package/llms.txt +299 -0
  4. package/package.json +54 -0
  5. package/src/bernoulli.js +29 -0
  6. package/src/blockTridiagonal.js +492 -0
  7. package/src/constants.js +36 -0
  8. package/src/data.js +127 -0
  9. package/src/describe.js +190 -0
  10. package/src/device.js +954 -0
  11. package/src/equation.js +188 -0
  12. package/src/errors.js +8 -0
  13. package/src/fermi.js +106 -0
  14. package/src/grid.js +111 -0
  15. package/src/index.js +207 -0
  16. package/src/kit.js +76 -0
  17. package/src/live.js +217 -0
  18. package/src/merge.js +11 -0
  19. package/src/plot.js +267 -0
  20. package/src/solution.js +246 -0
  21. package/src/solver.js +2884 -0
  22. package/src/stack.js +172 -0
  23. package/src/statistics.js +593 -0
  24. package/src/traces.js +125 -0
  25. package/src/types.js +221 -0
  26. package/src/worker.js +24 -0
  27. package/types/bernoulli.d.ts +4 -0
  28. package/types/blockTridiagonal.d.ts +92 -0
  29. package/types/constants.d.ts +33 -0
  30. package/types/data.d.ts +73 -0
  31. package/types/describe.d.ts +15 -0
  32. package/types/device.d.ts +20 -0
  33. package/types/equation.d.ts +102 -0
  34. package/types/errors.d.ts +3 -0
  35. package/types/fermi.d.ts +3 -0
  36. package/types/grid.d.ts +28 -0
  37. package/types/index.d.ts +103 -0
  38. package/types/kit.d.ts +61 -0
  39. package/types/live.d.ts +101 -0
  40. package/types/merge.d.ts +1 -0
  41. package/types/plot.d.ts +60 -0
  42. package/types/solution.d.ts +18 -0
  43. package/types/solver.d.ts +342 -0
  44. package/types/stack.d.ts +59 -0
  45. package/types/statistics.d.ts +10 -0
  46. package/types/traces.d.ts +118 -0
  47. package/types/types.d.ts +574 -0
@@ -0,0 +1,29 @@
1
+ // The Bernoulli function B(x) = x / (eˣ − 1) and its derivative, evaluated stably.
2
+ //
3
+ // These are the weights of Scharfetter–Gummel fluxes. Near x = 0 both are computed from
4
+ // their Taylor series; elsewhere B uses expm1 (accurate for small |x|, and it overflows
5
+ // gracefully to B → 0 for large positive x). Identity used: B(−x) = B(x) + x.
6
+
7
+ const SERIES_LIMIT = 0.1;
8
+
9
+ /** B(x) = x / (eˣ − 1), with B(0) = 1. */
10
+ export function bernoulli(x) {
11
+ if (Math.abs(x) < SERIES_LIMIT) {
12
+ const x2 = x * x;
13
+ // 1 − x/2 + x²/12 − x⁴/720 + x⁶/30240 − x⁸/1209600
14
+ return 1 - x / 2 + x2 * (1 / 12 + x2 * (-1 / 720 + x2 * (1 / 30240 - x2 / 1209600)));
15
+ }
16
+ return x / Math.expm1(x);
17
+ }
18
+
19
+ /** dB/dx, with B'(0) = −1/2. */
20
+ export function bernoulliDerivative(x) {
21
+ if (Math.abs(x) < SERIES_LIMIT) {
22
+ const x2 = x * x;
23
+ // −1/2 + x/6 − x³/180 + x⁵/5040 − x⁷/151200
24
+ return -0.5 + x * (1 / 6 + x2 * (-1 / 180 + x2 * (1 / 5040 - x2 / 151200)));
25
+ }
26
+ const b = x / Math.expm1(x);
27
+ // B' = (B/x)(1 − B(−x)) = (B/x)(1 − B − x)
28
+ return (b / x) * (1 - b - x);
29
+ }
@@ -0,0 +1,492 @@
1
+ // Block-tridiagonal linear solver (block Thomas algorithm).
2
+ //
3
+ // Solves, for node i = 0 … n−1 with blocks of size m_i,
4
+ // A_i x_{i−1} + B_i x_i + C_i x_{i+1} = d_i
5
+ // where B_i is m_i × m_i, A_i is m_i × m_{i−1} and C_i is m_i × m_{i+1}. Blocks are stored
6
+ // row-major, one after another, in flat Float64Arrays; `offA[i]`, `offB[i]`, `offC[i]` give
7
+ // where each starts and `offX[i]` where node i's entries start in a vector. A_0 and C_{n−1} are
8
+ // ignored, but stored as if the end blocks repeated (m_{−1} = m_0, m_n = m_{n−1}), so with a
9
+ // single size m every block i sits at i·m², as for a uniform array of blocks. A size may be 0
10
+ // (the chain then splits there). All storage is allocated once; factor() and solve() allocate
11
+ // nothing.
12
+ //
13
+ // Within each diagonal block, LU uses partial pivoting, so zero diagonal entries (such as a
14
+ // slot that carries an interface flux instead of a value) are fine. There is no pivoting
15
+ // *between* blocks: that is the usual block Thomas trade-off, safe for the diagonally
16
+ // dominant-ish systems that finite-volume assembly produces.
17
+
18
+ // Offsets of every block and vector segment for block sizes `sizes`.
19
+ function layout(n, m, name) {
20
+ if (!(Number.isInteger(n) && n >= 1)) throw new RangeError(`n must be a positive integer, got ${n}`);
21
+ let sizes;
22
+ if (typeof m === 'number') {
23
+ if (!(Number.isInteger(m) && m >= 1)) throw new RangeError(`m must be a positive integer, got ${m}`);
24
+ sizes = new Int32Array(n).fill(m);
25
+ } else {
26
+ if (!(m && m.length === n)) throw new RangeError(`${name}: give one block size per node (${n})`);
27
+ sizes = Int32Array.from(m);
28
+ for (const v of sizes) if (!(v >= 0)) throw new RangeError(`block sizes must be non-negative integers, got ${v}`);
29
+ }
30
+ const offA = new Int32Array(n + 1), offB = new Int32Array(n + 1), offC = new Int32Array(n + 1), offX = new Int32Array(n + 1);
31
+ for (let i = 0; i < n; i++) {
32
+ const mi = sizes[i], mp = i > 0 ? sizes[i - 1] : mi, mn = i < n - 1 ? sizes[i + 1] : mi;
33
+ offA[i + 1] = offA[i] + mi * mp;
34
+ offB[i + 1] = offB[i] + mi * mi;
35
+ offC[i + 1] = offC[i] + mi * mn;
36
+ offX[i + 1] = offX[i] + mi;
37
+ }
38
+ const maxM = sizes.reduce((a, v) => Math.max(a, v), 0);
39
+ return { sizes, offA, offB, offC, offX, maxM, uniform: typeof m === 'number' ? m : null };
40
+ }
41
+
42
+ export class BlockTridiagonal {
43
+ /**
44
+ * @param {number} n number of block rows (nodes)
45
+ * @param {number | ArrayLike<number>} m block size (unknowns per node), or one size per node
46
+ */
47
+ constructor(n, m) {
48
+ const L = layout(n, m, 'BlockTridiagonal');
49
+ this.n = n;
50
+ /** The block size if every block has the same size, else null. */
51
+ this.m = L.uniform;
52
+ /** Block size of each node. */
53
+ this.sizes = L.sizes;
54
+ this.offA = L.offA;
55
+ this.offB = L.offB;
56
+ this.offC = L.offC;
57
+ this.offX = L.offX;
58
+ /** Total number of unknowns. */
59
+ this.size = L.offX[n];
60
+ /** Sub-diagonal blocks A_i (coupling to node i−1). */
61
+ this.A = new Float64Array(L.offA[n]);
62
+ /** Diagonal blocks B_i. */
63
+ this.B = new Float64Array(L.offB[n]);
64
+ /** Super-diagonal blocks C_i (coupling to node i+1). */
65
+ this.C = new Float64Array(L.offC[n]);
66
+
67
+ this._lu = new Float64Array(L.offB[n]); // LU factors of W_i = B_i − A_i C'_{i−1}
68
+ this._piv = new Int32Array(this.size);
69
+ this._cp = new Float64Array(L.offC[n]); // C'_i = W_i⁻¹ C_i
70
+ this._y = new Float64Array(this.size);
71
+ this._tmp = new Float64Array(L.maxM);
72
+ this.factored = false;
73
+ /** Smallest |pivot| / (largest |entry| of its block) seen in the last factor(). */
74
+ this.minPivotRatio = Infinity;
75
+ /** The block found exactly singular by the last factor(), or −1. */
76
+ this.singularBlock = -1;
77
+ }
78
+
79
+ /** Zero all of A, B, C (ready for fresh assembly). */
80
+ clear() {
81
+ this.A.fill(0);
82
+ this.B.fill(0);
83
+ this.C.fill(0);
84
+ this.factored = false;
85
+ }
86
+
87
+ /**
88
+ * Factorise the current A, B, C. A, B, C are left untouched.
89
+ * Throws if a diagonal block is exactly singular.
90
+ */
91
+ factor() {
92
+ const { n, A, B, C, sizes, offA, offB, offC, offX } = this;
93
+ const lu = this._lu, piv = this._piv, cp = this._cp, tmp = this._tmp;
94
+ let minRatio = Infinity;
95
+ this.singularBlock = -1;
96
+
97
+ for (let i = 0; i < n; i++) {
98
+ const m = sizes[i], o = offB[i];
99
+ if (m === 0) continue;
100
+ // W_i = B_i − A_i C'_{i−1}
101
+ if (i === 0 || sizes[i - 1] === 0) {
102
+ for (let k = 0; k < m * m; k++) lu[o + k] = B[o + k];
103
+ } else {
104
+ const mp = sizes[i - 1], oa = offA[i], op = offC[i - 1]; // C'_{i−1}: mp × m
105
+ for (let r = 0; r < m; r++) {
106
+ for (let c = 0; c < m; c++) {
107
+ let s = B[o + r * m + c];
108
+ for (let k = 0; k < mp; k++) s -= A[oa + r * mp + k] * cp[op + k * m + c];
109
+ lu[o + r * m + c] = s;
110
+ }
111
+ }
112
+ }
113
+ const ratio = luFactor(lu, o, piv, offX[i], m);
114
+ if (!(ratio > 0)) {
115
+ this.factored = false;
116
+ this.singularBlock = i;
117
+ throw new Error(`BlockTridiagonal: diagonal block ${i} is singular`);
118
+ }
119
+ if (ratio < minRatio) minRatio = ratio;
120
+ // C'_i = W_i⁻¹ C_i, column by column
121
+ if (i < n - 1) {
122
+ const mn = sizes[i + 1], oc = offC[i];
123
+ for (let c = 0; c < mn; c++) {
124
+ for (let r = 0; r < m; r++) tmp[r] = C[oc + r * mn + c];
125
+ luSolve(lu, o, piv, offX[i], m, tmp);
126
+ for (let r = 0; r < m; r++) cp[oc + r * mn + c] = tmp[r];
127
+ }
128
+ }
129
+ }
130
+ this.minPivotRatio = minRatio;
131
+ this.factored = true;
132
+ }
133
+
134
+ /**
135
+ * Solve with the current factorisation. `rhs` and `out` are Float64Arrays of length `size`
136
+ * (node-major); they may be the same array.
137
+ */
138
+ solve(rhs, out = new Float64Array(this.size)) {
139
+ if (!this.factored) throw new Error('BlockTridiagonal: call factor() before solve()');
140
+ const { n, A, sizes, offA, offB, offC, offX } = this;
141
+ const lu = this._lu, piv = this._piv, cp = this._cp, y = this._y;
142
+
143
+ // Forward: y_i = W_i⁻¹ (d_i − A_i y_{i−1})
144
+ for (let i = 0; i < n; i++) {
145
+ const m = sizes[i], v = offX[i];
146
+ if (m === 0) continue;
147
+ const mp = i > 0 ? sizes[i - 1] : 0, oa = offA[i], vp = v - mp;
148
+ for (let r = 0; r < m; r++) {
149
+ let s = rhs[v + r];
150
+ for (let k = 0; k < mp; k++) s -= A[oa + r * mp + k] * y[vp + k];
151
+ y[v + r] = s;
152
+ }
153
+ luSolveAt(lu, offB[i], piv, v, m, y, v);
154
+ }
155
+ // Backward: x_i = y_i − C'_i x_{i+1}
156
+ for (let i = n - 1; i >= 0; i--) {
157
+ const m = sizes[i], v = offX[i];
158
+ const mn = i < n - 1 ? sizes[i + 1] : 0, oc = offC[i], vn = v + m;
159
+ for (let r = 0; r < m; r++) {
160
+ let s = y[v + r];
161
+ for (let k = 0; k < mn; k++) s -= cp[oc + r * mn + k] * out[vn + k];
162
+ out[v + r] = s;
163
+ }
164
+ }
165
+ return out;
166
+ }
167
+
168
+ /**
169
+ * After factor(): the digits lost to cancellation in the factorisation, at worst, and the block
170
+ * where that happens. A running error bound: alongside each entry of W_i = B_i − A_i C′_{i−1}
171
+ * and of its LU (with the same partial pivoting), the sum of the magnitudes it was formed from
172
+ * is carried, and each pivot is compared with its sum: log₁₀(Σ|terms| / |pivot|). Many digits
173
+ * lost means a nearly singular system, such as a stiff chain held only weakly at its ends,
174
+ * whose level then rests on the lost digits. Costs about as much as a factorisation. After a
175
+ * factorisation that failed on a singular block, it covers the blocks up to that one.
176
+ * @returns {{ digits: number, block: number }}
177
+ */
178
+ cancellation() {
179
+ if (!this.factored && !(this.singularBlock >= 0)) throw new Error('BlockTridiagonal: call factor() before cancellation()');
180
+ const { A, B, sizes, offA, offB, offC } = this, cp = this._cp;
181
+ const n = this.factored ? this.n : this.singularBlock + 1;
182
+ const mx = sizes.reduce((a, v) => Math.max(a, v), 0);
183
+ const w = new Float64Array(mx * mx), S = new Float64Array(mx * mx);
184
+ let worst = 1, block = -1;
185
+ for (let i = 0; i < n; i++) {
186
+ const m = sizes[i], mp = i > 0 ? sizes[i - 1] : 0, oa = offA[i], ob = offB[i], oc = i > 0 ? offC[i - 1] : 0;
187
+ for (let r = 0; r < m; r++) {
188
+ for (let c = 0; c < m; c++) {
189
+ let v = B[ob + r * m + c], sum = Math.abs(v);
190
+ for (let k = 0; k < mp; k++) {
191
+ const t = A[oa + r * mp + k] * cp[oc + k * m + c];
192
+ v -= t;
193
+ sum += Math.abs(t);
194
+ }
195
+ w[r * m + c] = v;
196
+ S[r * m + c] = sum;
197
+ }
198
+ }
199
+ for (let k = 0; k < m; k++) {
200
+ let p = k;
201
+ for (let r = k + 1; r < m; r++) if (Math.abs(w[r * m + k]) > Math.abs(w[p * m + k])) p = r;
202
+ if (p !== k) {
203
+ for (let c = 0; c < m; c++) {
204
+ [w[k * m + c], w[p * m + c]] = [w[p * m + c], w[k * m + c]];
205
+ [S[k * m + c], S[p * m + c]] = [S[p * m + c], S[k * m + c]];
206
+ }
207
+ }
208
+ const piv = Math.abs(w[k * m + k]), sum = S[k * m + k];
209
+ if (sum > worst * piv) {
210
+ worst = piv === 0 ? Infinity : sum / piv;
211
+ block = i;
212
+ }
213
+ if (piv === 0) continue;
214
+ for (let r = k + 1; r < m; r++) {
215
+ const f = w[r * m + k] / w[k * m + k];
216
+ if (f === 0) continue;
217
+ for (let c = k + 1; c < m; c++) {
218
+ w[r * m + c] -= f * w[k * m + c];
219
+ S[r * m + c] += Math.abs(f) * S[k * m + c];
220
+ }
221
+ }
222
+ }
223
+ }
224
+ return { digits: Math.log10(worst), block };
225
+ }
226
+
227
+ /** out = M·x using the stored (unfactored) blocks. For residual checks and tests. */
228
+ multiply(x, out = new Float64Array(this.size)) {
229
+ const { n, A, B, C, sizes, offA, offB, offC, offX } = this;
230
+ for (let i = 0; i < n; i++) {
231
+ const m = sizes[i], v = offX[i];
232
+ const mp = i > 0 ? sizes[i - 1] : 0, mn = i < n - 1 ? sizes[i + 1] : 0;
233
+ for (let r = 0; r < m; r++) {
234
+ let s = 0;
235
+ for (let k = 0; k < m; k++) s += B[offB[i] + r * m + k] * x[v + k];
236
+ for (let k = 0; k < mp; k++) s += A[offA[i] + r * mp + k] * x[v - mp + k];
237
+ for (let k = 0; k < mn; k++) s += C[offC[i] + r * mn + k] * x[v + m + k];
238
+ out[v + r] = s;
239
+ }
240
+ }
241
+ return out;
242
+ }
243
+ }
244
+
245
+ // In-place LU with partial pivoting of the m×m block at a[o…]. Row swaps are recorded in
246
+ // piv[p…]. Returns min |pivot| / max |entry| (0 if singular).
247
+ function luFactor(a, o, piv, p, m) {
248
+ let maxEntry = 0;
249
+ for (let k = 0; k < m * m; k++) {
250
+ const v = Math.abs(a[o + k]);
251
+ if (v > maxEntry) maxEntry = v;
252
+ }
253
+ if (maxEntry === 0) return 0;
254
+ let minPivot = Infinity;
255
+ for (let k = 0; k < m; k++) {
256
+ let best = k, bestVal = Math.abs(a[o + k * m + k]);
257
+ for (let r = k + 1; r < m; r++) {
258
+ const v = Math.abs(a[o + r * m + k]);
259
+ if (v > bestVal) { best = r; bestVal = v; }
260
+ }
261
+ piv[p + k] = best;
262
+ if (bestVal === 0) return 0;
263
+ if (bestVal < minPivot) minPivot = bestVal;
264
+ if (best !== k) {
265
+ for (let c = 0; c < m; c++) {
266
+ const t = a[o + k * m + c];
267
+ a[o + k * m + c] = a[o + best * m + c];
268
+ a[o + best * m + c] = t;
269
+ }
270
+ }
271
+ const inv = 1 / a[o + k * m + k];
272
+ for (let r = k + 1; r < m; r++) {
273
+ const f = (a[o + r * m + k] *= inv);
274
+ if (f !== 0) for (let c = k + 1; c < m; c++) a[o + r * m + c] -= f * a[o + k * m + c];
275
+ }
276
+ }
277
+ return minPivot / maxEntry;
278
+ }
279
+
280
+ // Solve (LU) x = b in place, b in vec[0…m).
281
+ function luSolve(a, o, piv, p, m, vec) {
282
+ luSolveAt(a, o, piv, p, m, vec, 0);
283
+ }
284
+
285
+ // Solve (LU) x = b in place, b in vec[s…s+m).
286
+ function luSolveAt(a, o, piv, p, m, vec, s) {
287
+ for (let k = 0; k < m; k++) {
288
+ const j = piv[p + k];
289
+ if (j !== k) {
290
+ const t = vec[s + k];
291
+ vec[s + k] = vec[s + j];
292
+ vec[s + j] = t;
293
+ }
294
+ }
295
+ for (let r = 1; r < m; r++) {
296
+ let v = vec[s + r];
297
+ for (let c = 0; c < r; c++) v -= a[o + r * m + c] * vec[s + c];
298
+ vec[s + r] = v;
299
+ }
300
+ for (let r = m - 1; r >= 0; r--) {
301
+ let v = vec[s + r];
302
+ for (let c = r + 1; c < m; c++) v -= a[o + r * m + c] * vec[s + c];
303
+ vec[s + r] = v / a[o + r * m + r];
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Complex block-tridiagonal solver, for small-signal (frequency-domain) problems. Same layout
309
+ * and algorithm as BlockTridiagonal (including per-node block sizes), with real and imaginary
310
+ * parts in separate arrays (Ar/Ai, Br/Bi, Cr/Ci; right-hand sides and solutions as re/im
311
+ * pairs).
312
+ */
313
+ export class ComplexBlockTridiagonal {
314
+ /**
315
+ * @param {number} n number of block rows (nodes)
316
+ * @param {number | ArrayLike<number>} m block size, or one size per node
317
+ */
318
+ constructor(n, m) {
319
+ const L = layout(n, m, 'ComplexBlockTridiagonal');
320
+ this.n = n;
321
+ this.m = L.uniform;
322
+ this.sizes = L.sizes;
323
+ this.offA = L.offA;
324
+ this.offB = L.offB;
325
+ this.offC = L.offC;
326
+ this.offX = L.offX;
327
+ this.size = L.offX[n];
328
+ for (const k of ['Ar', 'Ai']) this[k] = new Float64Array(L.offA[n]);
329
+ for (const k of ['Br', 'Bi', '_lr', '_li']) this[k] = new Float64Array(L.offB[n]);
330
+ for (const k of ['Cr', 'Ci', '_cr', '_ci']) this[k] = new Float64Array(L.offC[n]);
331
+ this._piv = new Int32Array(this.size);
332
+ this._yr = new Float64Array(this.size);
333
+ this._yi = new Float64Array(this.size);
334
+ this._tr = new Float64Array(L.maxM);
335
+ this._ti = new Float64Array(L.maxM);
336
+ }
337
+
338
+ clear() {
339
+ for (const k of ['Ar', 'Ai', 'Br', 'Bi', 'Cr', 'Ci']) this[k].fill(0);
340
+ }
341
+
342
+ factor() {
343
+ const { n, Ar, Ai, Br, Bi, Cr, Ci, _lr: lr, _li: li, _cr: cr, _ci: ci, _piv: piv, _tr: tr, _ti: ti } = this;
344
+ const { sizes, offA, offB, offC, offX } = this;
345
+ for (let i = 0; i < n; i++) {
346
+ const m = sizes[i], o = offB[i];
347
+ if (m === 0) continue;
348
+ const mp = i > 0 ? sizes[i - 1] : 0, oa = offA[i], op = i > 0 ? offC[i - 1] : 0;
349
+ for (let r = 0; r < m; r++) {
350
+ for (let c = 0; c < m; c++) {
351
+ let sr = Br[o + r * m + c], si = Bi[o + r * m + c];
352
+ for (let k = 0; k < mp; k++) {
353
+ const ar = Ar[oa + r * mp + k], ai = Ai[oa + r * mp + k];
354
+ const pr = cr[op + k * m + c], pi = ci[op + k * m + c];
355
+ sr -= ar * pr - ai * pi;
356
+ si -= ar * pi + ai * pr;
357
+ }
358
+ lr[o + r * m + c] = sr;
359
+ li[o + r * m + c] = si;
360
+ }
361
+ }
362
+ if (!cluFactor(lr, li, o, piv, offX[i], m)) throw new Error(`ComplexBlockTridiagonal: diagonal block ${i} is singular`);
363
+ if (i < n - 1) {
364
+ const mn = sizes[i + 1], oc = offC[i];
365
+ for (let c = 0; c < mn; c++) {
366
+ for (let r = 0; r < m; r++) {
367
+ tr[r] = Cr[oc + r * mn + c];
368
+ ti[r] = Ci[oc + r * mn + c];
369
+ }
370
+ cluSolve(lr, li, o, piv, offX[i], m, tr, ti, 0);
371
+ for (let r = 0; r < m; r++) {
372
+ cr[oc + r * mn + c] = tr[r];
373
+ ci[oc + r * mn + c] = ti[r];
374
+ }
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ /** Solve for (re, im) right-hand sides; results written to outRe/outIm. */
381
+ solve(bRe, bIm, outRe, outIm) {
382
+ const { n, Ar, Ai, _lr: lr, _li: li, _cr: cr, _ci: ci, _piv: piv, _yr: yr, _yi: yi } = this;
383
+ const { sizes, offA, offB, offC, offX } = this;
384
+ for (let i = 0; i < n; i++) {
385
+ const m = sizes[i], v = offX[i];
386
+ if (m === 0) continue;
387
+ const mp = i > 0 ? sizes[i - 1] : 0, oa = offA[i], vp = v - mp;
388
+ for (let r = 0; r < m; r++) {
389
+ let sr = bRe[v + r], si = bIm[v + r];
390
+ for (let k = 0; k < mp; k++) {
391
+ const ar = Ar[oa + r * mp + k], ai = Ai[oa + r * mp + k];
392
+ sr -= ar * yr[vp + k] - ai * yi[vp + k];
393
+ si -= ar * yi[vp + k] + ai * yr[vp + k];
394
+ }
395
+ yr[v + r] = sr;
396
+ yi[v + r] = si;
397
+ }
398
+ cluSolve(lr, li, offB[i], piv, v, m, yr, yi, v);
399
+ }
400
+ for (let i = n - 1; i >= 0; i--) {
401
+ const m = sizes[i], v = offX[i];
402
+ const mn = i < n - 1 ? sizes[i + 1] : 0, oc = offC[i], vn = v + m;
403
+ for (let r = 0; r < m; r++) {
404
+ let sr = yr[v + r], si = yi[v + r];
405
+ for (let k = 0; k < mn; k++) {
406
+ const pr = cr[oc + r * mn + k], pi = ci[oc + r * mn + k];
407
+ const xr = outRe[vn + k], xi = outIm[vn + k];
408
+ sr -= pr * xr - pi * xi;
409
+ si -= pr * xi + pi * xr;
410
+ }
411
+ outRe[v + r] = sr;
412
+ outIm[v + r] = si;
413
+ }
414
+ }
415
+ }
416
+ }
417
+
418
+ // Complex LU with partial pivoting (by modulus) of the block at o; false if singular.
419
+ function cluFactor(ar, ai, o, piv, p, m) {
420
+ for (let k = 0; k < m; k++) {
421
+ let best = k, bestVal = Math.hypot(ar[o + k * m + k], ai[o + k * m + k]);
422
+ for (let r = k + 1; r < m; r++) {
423
+ const v = Math.hypot(ar[o + r * m + k], ai[o + r * m + k]);
424
+ if (v > bestVal) {
425
+ best = r;
426
+ bestVal = v;
427
+ }
428
+ }
429
+ piv[p + k] = best;
430
+ if (bestVal === 0) return false;
431
+ if (best !== k) {
432
+ for (let c = 0; c < m; c++) {
433
+ let t = ar[o + k * m + c];
434
+ ar[o + k * m + c] = ar[o + best * m + c];
435
+ ar[o + best * m + c] = t;
436
+ t = ai[o + k * m + c];
437
+ ai[o + k * m + c] = ai[o + best * m + c];
438
+ ai[o + best * m + c] = t;
439
+ }
440
+ }
441
+ const dr = ar[o + k * m + k], di = ai[o + k * m + k], d2 = dr * dr + di * di;
442
+ const invr = dr / d2, invi = -di / d2;
443
+ for (let r = k + 1; r < m; r++) {
444
+ const xr = ar[o + r * m + k], xi = ai[o + r * m + k];
445
+ const fr = xr * invr - xi * invi, fi = xr * invi + xi * invr;
446
+ ar[o + r * m + k] = fr;
447
+ ai[o + r * m + k] = fi;
448
+ if (fr === 0 && fi === 0) continue;
449
+ for (let c = k + 1; c < m; c++) {
450
+ const ur = ar[o + k * m + c], ui = ai[o + k * m + c];
451
+ ar[o + r * m + c] -= fr * ur - fi * ui;
452
+ ai[o + r * m + c] -= fr * ui + fi * ur;
453
+ }
454
+ }
455
+ }
456
+ return true;
457
+ }
458
+
459
+ function cluSolve(ar, ai, o, piv, p, m, vr, vi, s) {
460
+ for (let k = 0; k < m; k++) {
461
+ const j = piv[p + k];
462
+ if (j !== k) {
463
+ let t = vr[s + k];
464
+ vr[s + k] = vr[s + j];
465
+ vr[s + j] = t;
466
+ t = vi[s + k];
467
+ vi[s + k] = vi[s + j];
468
+ vi[s + j] = t;
469
+ }
470
+ }
471
+ for (let r = 1; r < m; r++) {
472
+ let xr = vr[s + r], xi = vi[s + r];
473
+ for (let c = 0; c < r; c++) {
474
+ const lr = ar[o + r * m + c], li = ai[o + r * m + c];
475
+ xr -= lr * vr[s + c] - li * vi[s + c];
476
+ xi -= lr * vi[s + c] + li * vr[s + c];
477
+ }
478
+ vr[s + r] = xr;
479
+ vi[s + r] = xi;
480
+ }
481
+ for (let r = m - 1; r >= 0; r--) {
482
+ let xr = vr[s + r], xi = vi[s + r];
483
+ for (let c = r + 1; c < m; c++) {
484
+ const ur = ar[o + r * m + c], ui = ai[o + r * m + c];
485
+ xr -= ur * vr[s + c] - ui * vi[s + c];
486
+ xi -= ur * vi[s + c] + ui * vr[s + c];
487
+ }
488
+ const dr = ar[o + r * m + r], di = ai[o + r * m + r], d2 = dr * dr + di * di;
489
+ vr[s + r] = (xr * dr + xi * di) / d2;
490
+ vi[s + r] = (xi * dr - xr * di) / d2;
491
+ }
492
+ }
@@ -0,0 +1,36 @@
1
+ // Physical constants (SI, CODATA exact values where defined) and unit helpers.
2
+
3
+ /** Elementary charge, C. */
4
+ export const ELEMENTARY_CHARGE = 1.602176634e-19;
5
+ /** Avogadro constant, 1/mol. */
6
+ export const AVOGADRO = 6.02214076e23;
7
+ /** Boltzmann constant, J/K. */
8
+ export const BOLTZMANN = 1.380649e-23;
9
+ /** Faraday constant, C/mol. */
10
+ export const FARADAY = ELEMENTARY_CHARGE * AVOGADRO;
11
+ /** Molar gas constant, J/(mol·K). */
12
+ export const GAS_CONSTANT = BOLTZMANN * AVOGADRO;
13
+ /** Vacuum permittivity, F/m (CODATA 2022). */
14
+ export const EPS0 = 8.8541878188e-12;
15
+
16
+ /** Unit conversions into driftlet's SI units. */
17
+ export const units = Object.freeze({
18
+ /** mol/L → mol/m³ */
19
+ molar: (x) => x * 1e3,
20
+ /** mmol/L → mol/m³ */
21
+ millimolar: (x) => x,
22
+ /** particles per cm³ → mol/m³ */
23
+ perCm3: (x) => (x * 1e6) / AVOGADRO,
24
+ /** particles per m³ → mol/m³ */
25
+ perM3: (x) => x / AVOGADRO,
26
+ /** eV per particle → J/mol */
27
+ eV: (x) => x * FARADAY,
28
+ /** J/mol → eV per particle */
29
+ toEV: (x) => x / FARADAY,
30
+ /** nm → m */
31
+ nm: (x) => x * 1e-9,
32
+ /** µm → m */
33
+ um: (x) => x * 1e-6,
34
+ /** cm²/s → m²/s */
35
+ cm2PerS: (x) => x * 1e-4,
36
+ });
package/src/data.js ADDED
@@ -0,0 +1,127 @@
1
+ // A small data library for driftlet/kit, every entry with its source (see docs/data.md). Kept
2
+ // small on purpose: a wrong library number is worse than a wrong demo number. Bulk properties
3
+ // only. How levels line up at an interface, and a surface's work function, belong to that
4
+ // interface or surface and are never in here.
5
+
6
+ import { DeviceError } from './errors.js';
7
+ import { FARADAY, GAS_CONSTANT, AVOGADRO } from './constants.js';
8
+
9
+ const fail = (message) => {
10
+ throw new DeviceError(message);
11
+ };
12
+ const freeze = (table) => Object.freeze(Object.fromEntries(Object.entries(table).map(([k, v]) => [k, Object.freeze(v)])));
13
+
14
+ /**
15
+ * Aqueous ions at 25 °C. D: diffusivity at infinite dilution, m²/s (CRC Handbook of Chemistry
16
+ * and Physics, "Ionic conductivity and diffusion at infinite dilution"). mu0: standard Gibbs
17
+ * energy of formation, J/mol, the usual table convention with ΔfG°(H⁺) = 0 (Wagman et al., "The
18
+ * NBS tables of chemical thermodynamic properties", J. Phys. Chem. Ref. Data 11, Suppl. 2, 1982),
19
+ * on the 1 mol/kg standard state, used here with c_ref = 1000 mol/m³ (1 M).
20
+ * Names are the formula followed by the charge.
21
+ */
22
+ export const IONS = freeze({
23
+ 'H+': { z: 1, D: 9.311e-9, mu0: 0 },
24
+ 'Li+': { z: 1, D: 1.029e-9, mu0: -293.31e3 },
25
+ 'Na+': { z: 1, D: 1.334e-9, mu0: -261.905e3 },
26
+ 'K+': { z: 1, D: 1.957e-9, mu0: -283.27e3 },
27
+ 'Ag+': { z: 1, D: 1.648e-9, mu0: 77.107e3 },
28
+ 'Cu2+': { z: 2, D: 0.714e-9, mu0: 65.49e3 },
29
+ 'Zn2+': { z: 2, D: 0.703e-9, mu0: -147.06e3 },
30
+ 'Fe2+': { z: 2, D: 0.719e-9, mu0: -78.9e3 },
31
+ 'Fe3+': { z: 3, D: 0.604e-9, mu0: -4.7e3 },
32
+ 'OH-': { z: -1, D: 5.273e-9, mu0: -157.244e3 },
33
+ 'Cl-': { z: -1, D: 2.032e-9, mu0: -131.228e3 },
34
+ 'NO3-': { z: -1, D: 1.902e-9, mu0: -111.25e3 },
35
+ 'SO42-': { z: -2, D: 1.065e-9, mu0: -744.53e3 },
36
+ });
37
+
38
+ /** μ° of liquid water at 25 °C, J/mol (Wagman et al. 1982), for reactions such as H₂O ⇌ H⁺ + OH⁻. */
39
+ export const H2O = -237.129e3;
40
+
41
+ /** Relative permittivity of water at 25 °C (Malmberg and Maryott, J. Res. NBS 56, 1, 1956). */
42
+ export const WATER_EPSR = 78.3;
43
+
44
+ /**
45
+ * Semiconductors at 300 K (Sze, Physics of Semiconductor Devices, 2nd ed., 1981, appendix):
46
+ * relative permittivity, band gap Eg (eV), electron affinity chi (V), effective densities of
47
+ * states Nc and Nv (cm⁻³), and drift mobilities of lightly doped material mun and mup
48
+ * (cm²/(V·s)).
49
+ */
50
+ export const SEMICONDUCTORS = freeze({
51
+ Si: { epsr: 11.9, Eg: 1.12, chi: 4.05, Nc: 2.8e19, Nv: 1.04e19, mun: 1500, mup: 450 },
52
+ Ge: { epsr: 16.0, Eg: 0.66, chi: 4.0, Nc: 1.04e19, Nv: 6.0e18, mun: 3900, mup: 1900 },
53
+ GaAs: { epsr: 13.1, Eg: 1.424, chi: 4.07, Nc: 4.7e17, Nv: 7.0e18, mun: 8500, mup: 400 },
54
+ });
55
+
56
+ /**
57
+ * Metals at 293 K: resistivity rho (10⁻⁸ Ω·m, CRC Handbook, "Electrical resistivity of pure
58
+ * metals") and the conductivity 1/rho (S/m).
59
+ */
60
+ export const METALS = freeze(
61
+ Object.fromEntries(
62
+ Object.entries({ Ag: 1.587, Cu: 1.678, Au: 2.214, Al: 2.65, Zn: 5.9, Li: 9.28, Pt: 10.5 }).map(([name, rho]) => [name, { rho, conductivity: 1e8 / rho }]),
63
+ ),
64
+ );
65
+
66
+ /**
67
+ * Water with the named ions, as a library piece for `build()`: each ion as a species
68
+ * (c_ref = 1 M) and the material `water` holding them, with D and μ° from `IONS`.
69
+ * @param {string[]} names ions from `IONS`
70
+ * @param {{ epsr?: number, material?: string }} [opts] permittivity (default that of water;
71
+ * 0 makes the solution strictly neutral, without double layers), and the material's name
72
+ * @returns {{ species: object[], materials: Record<string, object> }}
73
+ */
74
+ export function aqueous(names, { epsr = WATER_EPSR, material = 'water' } = {}) {
75
+ if (!Array.isArray(names) || names.length === 0) fail('aqueous: give a list of ions, e.g. aqueous([\'Na+\', \'Cl-\'])');
76
+ const species = [], entries = {};
77
+ for (const name of names) {
78
+ const ion = IONS[name];
79
+ if (!ion) fail(`aqueous: no data for '${name}' (the library has ${Object.keys(IONS).join(', ')})`);
80
+ species.push({ name, z: ion.z, cRef: 1000 });
81
+ entries[name] = { D: ion.D, mu0: ion.mu0 };
82
+ }
83
+ return { species, materials: { [material]: { epsr, species: entries } } };
84
+ }
85
+
86
+ /**
87
+ * A semiconductor from `SEMICONDUCTORS`, as a library piece: electrons `e-` and holes `h+`, with
88
+ * c_ref = Nc and Nv, D from the mobilities by the Einstein relation at 300 K, and μ°_e⁻ = 0 (φ
89
+ * anchored at the conduction band) and μ°_h⁺ = Eg·F. The data are for 300 K, so use `T: 300`.
90
+ * Statistics are ideal (Boltzmann); add Fermi–Dirac to the material for degenerate doping.
91
+ * @param {string} name 'Si', 'Ge' or 'GaAs'
92
+ * @param {{ material?: string }} [opts]
93
+ * @returns {{ species: object[], materials: Record<string, object> }}
94
+ */
95
+ export function semiconductor(name, { material = name } = {}) {
96
+ const sc = SEMICONDUCTORS[name];
97
+ if (!sc) fail(`semiconductor: no data for '${name}' (the library has ${Object.keys(SEMICONDUCTORS).join(', ')})`);
98
+ const VT = (GAS_CONSTANT * 300) / FARADAY;
99
+ const perCm3 = (n) => (n * 1e6) / AVOGADRO;
100
+ return {
101
+ species: [
102
+ { name: 'e-', z: -1 },
103
+ { name: 'h+', z: 1 },
104
+ ],
105
+ materials: {
106
+ [material]: {
107
+ epsr: sc.epsr,
108
+ species: {
109
+ 'e-': { D: sc.mun * 1e-4 * VT, mu0: 0, cRef: perCm3(sc.Nc) },
110
+ 'h+': { D: sc.mup * 1e-4 * VT, mu0: sc.Eg * FARADAY, cRef: perCm3(sc.Nv) },
111
+ },
112
+ },
113
+ },
114
+ };
115
+ }
116
+
117
+ /**
118
+ * A metal from `METALS` as a conductor material, its carrier `e-`.
119
+ * @param {string} name
120
+ * @param {{ material?: string }} [opts]
121
+ * @returns {{ species: object[], materials: Record<string, object> }}
122
+ */
123
+ export function metal(name, { material = name } = {}) {
124
+ const m = METALS[name];
125
+ if (!m) fail(`metal: no data for '${name}' (the library has ${Object.keys(METALS).join(', ')})`);
126
+ return { species: [{ name: 'e-', z: -1 }], materials: { [material]: { conductor: { species: 'e-', conductivity: m.conductivity } } } };
127
+ }