driftlet 1.2.4 → 1.3.2

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.
@@ -0,0 +1,206 @@
1
+ // The equations at one time, assembled: every row's residual and its Jacobian, for Newton's
2
+ // iteration (src/newton.js), the time steps' bookkeeping and the small signal.
3
+ //
4
+ // CONTRACT
5
+ //
6
+ // These are methods of the Solver (src/solver.js installs them on its prototype), as are the
7
+ // kernels they call: the regions' in src/regions.js and the faces' and terminals' in
8
+ // src/boundaries.js, each with its own contract under this one. The layout they write into
9
+ // (blocks, slots, row o = block·M + slot, its compact index R = rix[o], loc[o] < 0 for an
10
+ // inactive slot, u + uLo, terminals 0, 1, 2 + k) is in docs/internals.md.
11
+ //
12
+ // The discretisation. Every region boundary is a doubled grid node (one per side) with a
13
+ // zero-volume flux block between them (the face's), so the Jacobian stays block-tridiagonal. A
14
+ // node's slots: φ̂ = Fφ/RT (in a metal, its carrier's flux J along the segment to its right),
15
+ // η_i = μ̄_i/RT, and at a port's nodes its coverages and gates. A face's: its displacement D, each
16
+ // linked species' flux N_i (toward +x, per area), each of its reactions' rate, each gate's
17
+ // fraction. Geometry is in the grid (src/grid.js): a node's box vol[g] = ∫A dx, the area A at a
18
+ // node, a segment's h = length/segArea. The rows are totals over a box (mol/s and C/s, or C):
19
+ // Gauss (φ̂ row): Σ D out of the box − vol·ρ = 0, ρ = F Σ z_i c_i + ρ_fixed (+ a port's
20
+ // capacitive charge, a face's sheet charge)
21
+ // balance (η_i): vol·(c_i − c_i,old)/dt + Σ N_i out − Σ N_i in + vol·Σ ν r (consumed: a
22
+ // reactant's ν > 0) − (made) − (a port's or contact's supply) = 0
23
+ // a face's slot: unknown − its law (D, N_i, a rate), or the law itself (a pinned jump, μ̄
24
+ // continuous), or a gate's (x − x_old)/dt − α(1 − x) + βx
25
+ // A flux along a segment is computed once and added with opposite signs to its two nodes, so
26
+ // sums of balance rows over boxes telescope exactly. At dt = Infinity there is no storage term
27
+ // (a steady system), and a gate's or surface's storage drops out the same way.
28
+ //
29
+ // assemble(dt), in order:
30
+ // 1. sys cleared and res zeroed; dtNow = dt; in a steady solve at dt = ∞, snapFlat (src/lawRows.js).
31
+ // 2. computeConcentrations (c, K, ex, coverages, from u + uLo).
32
+ // 3. Each region by its kind: a conductor's nodes and segments; else its nodes (dilute or
33
+ // concentrated), its bulk reactions, then per segment the displacement, eddy mixing, a
34
+ // concentrated region's fluxes; a dilute region's fluxes last.
35
+ // 4. Each face (_face).
36
+ // 5. The terminals (_terminals): the ports, the contacts, the floating ones' circuit rows.
37
+ // 6. applyLaws(dt) (src/lawRows.js): rows the laws take in place of the rows above.
38
+ // 7. On a time step with strictly neutral nodes (combining), _chargeRows: those nodes' rows
39
+ // and unknowns transformed (transformed = true; Newton maps the update back).
40
+ //
41
+ // What it writes:
42
+ // res (compact): each active row's residual as above; sys: its Jacobian, ∂res/∂(unknowns),
43
+ // by _j into the three blocks; inactive rows and columns go nowhere (rix = N, the sink).
44
+ // termB[k]: ∂res/∂V_k (per volt) for terminal k; termC[k]: ∂I_k/∂(unknowns); termI[k]: the
45
+ // current into the device through terminal k (A, planar: A/m²); termDI[k], termDX[k][j]:
46
+ // ∂ of k's circuit residual by V_k and V_j; termRes[k]: a floating k's circuit residual. Kept
47
+ // exactly only for _liveTerms() (the floating terminals, or all where allTerminals).
48
+ // The terminals' readouts (src/boundaries.js): contactFlux, contactD, portFlux, portQ, qg,
49
+ // qgSlope, portRates, contactQ; and s.pins (applyLaws).
50
+ // Where this.lin is set (only by _assembleDifference), the dilute kernels' Jacobian terms go into
51
+ // it, in difference form (src/difference.js), and the rest into sys, which is then not Newton's.
52
+ //
53
+ // Invariants (each says what a reviewer should be able to check here):
54
+ // 1. Every Jacobian entry is the derivative of the residual as assembled: in (φ̂, η, the face
55
+ // and port slots), by V_k in termB, and in the transformed unknowns after 7.
56
+ // (test/jacobian.test.js checks it against central differences, on the devices it lists.)
57
+ // 2. res, sys and the live terminals' terms depend only on the state (u, uLo, termV), dt, the
58
+ // sources' time and what a step or solve set (cOld, the gates' and coverages' old values,
59
+ // the charges at the step's start, steady, combining, the laws' references), never on an
60
+ // earlier assembly's leftovers: assemble(dt) twice at one state gives the same, bit for bit.
61
+ // (A held terminal's termB and termC aren't cleared, so they aren't kept.)
62
+ // 3. A kernel adds into res and sys (+=) and sets a row only where the row is its own (a face's
63
+ // slots, a metal's flux slot, a port's gates) or it replaces the row whole (a contact's or
64
+ // a port's held level, a contact's φ law; the laws in 6), and no kernel adds to a replaced
65
+ // row after it is replaced. A row read as a flux before it is replaced (a contact's, a held
66
+ // level's) has every other kernel's terms in it by then.
67
+ // 4. _assembleDifference(dt): res and the terminal terms as assemble(dt) gives them; its
68
+ // matrix plus its difference terms is assemble(dt)'s Jacobian (J·v equal to round-off);
69
+ // this.sys and this.lin as they were.
70
+ // 5. _assembleBookkeeping(dt): contactFlux, contactD, portFlux, portQ and termI bit for bit
71
+ // as assemble(dt) would set them at the same state (one a steady assembly has already
72
+ // snapped flat: it doesn't snap), with sys, res, termB, termC and termDI elsewhere left
73
+ // partial, not to be read; it doesn't recompute concentrations (they must be current).
74
+ // Tests: every test (every solve assembles); test/jacobian.test.js (invariant 1), test/layout.test.js
75
+ // (difference form, the bookkeeping's readouts).
76
+
77
+ import { BlockTridiagonal } from './blockTridiagonal.js';
78
+ import { DifferenceTerms } from './difference.js';
79
+ import { applyLaws, snapFlat } from './lawRows.js';
80
+
81
+ export class Assembly {
82
+ // Add ∂(row rb, slot rs)/∂(block cb, slot cs) to the Jacobian (nothing if either slot isn't
83
+ // an unknown).
84
+ _j(rb, rs, cb, cs, v) {
85
+ const M = this.M, r = this.loc[rb * M + rs], c = this.loc[cb * M + cs];
86
+ if (r < 0 || c < 0) return;
87
+ const sys = this.sys, sz = sys.sizes;
88
+ if (cb === rb) sys.B[sys.offB[rb] + r * sz[rb] + c] += v;
89
+ else if (cb === rb - 1) sys.A[sys.offA[rb] + r * sz[cb] + c] += v;
90
+ else if (cb === rb + 1) sys.C[sys.offC[rb] + r * sz[cb] + c] += v;
91
+ else throw new Error(`internal: non-tridiagonal coupling ${rb}→${cb}`);
92
+ }
93
+
94
+ /** Assemble residual and Jacobian for a backward-Euler step of size dt. */
95
+ assemble(dt) {
96
+ const { model, res, sys } = this;
97
+ const { grid, materials, regions } = model;
98
+ sys.clear();
99
+ res.fill(0);
100
+ this.dtNow = dt;
101
+ if (this.lin) this.lin.reset(1 / dt);
102
+ if (this.steady && dt === Infinity) snapFlat(this);
103
+ this.computeConcentrations();
104
+
105
+ // Regions, each assembled by the kernel for its kind: a conductor (its carrier only), a
106
+ // dilute region (ideal statistics, the fast path) or a concentrated one (any statistics).
107
+ for (let r = 0; r < regions.length; r++) {
108
+ const reg = regions[r], mat = materials[reg.material];
109
+ const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
110
+ if (mat.conductor) {
111
+ for (let g = g0; g <= g1; g++) this._nodeConductor(g, dt);
112
+ for (let s = g0; s < g1; s++) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s] / grid.segArea[s]);
113
+ continue;
114
+ }
115
+ if (mat.ideal) this._nodesDilute(g0, g1, dt);
116
+ else for (let g = g0; g <= g1; g++) this._nodeConcentrated(g, dt);
117
+ const rxs = this.rxsIn[reg.material];
118
+ if (rxs.length > 0) for (let g = g0; g <= g1; g++) this._bulkReactions(g, rxs);
119
+ for (let s = g0; s < g1; s++) {
120
+ const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s]; // (a length over the cross-section: fluxes are totals)
121
+ this._segmentDisplacement(s, bL, bR, mat, h);
122
+ if (reg.mixing > 0) this._segmentMixing(s, bL, bR, reg.mixing, h, mat);
123
+ if (!mat.ideal) this._segmentConcentrated(s, bL, bR, mat, h, reg.velocity);
124
+ }
125
+ if (mat.ideal) this._segmentsDilute(g0, g1, r, mat, reg.velocity);
126
+ }
127
+
128
+ // Faces: the flux node carries D, the linked species' fluxes and the reaction rates.
129
+ for (let f = 0; f < this.nFaces; f++) this._face(f);
130
+
131
+ // Terminals: ports (after every other term at their nodes, so a held level can read its
132
+ // flux), then contacts, then the circuit rows of the floating ones.
133
+ this._terminals(dt);
134
+ // The laws' rows (src/lawRows.js): a time step's circuit laws; a steady system's levels held
135
+ // flat, affinities, conserved amounts, islands' balances, and the pins held aside of them.
136
+ applyLaws(this, dt);
137
+ this.transformed = this.combining && dt !== Infinity;
138
+ if (this.transformed) this._chargeRows(dt);
139
+ }
140
+
141
+ // Zero one row of the Jacobian (all three blocks), ready to be replaced.
142
+ _replaceRow(b, rs) {
143
+ const l = this.loc[b * this.M + rs];
144
+ if (l < 0) return;
145
+ if (this.lin) this.lin.kill(this.rix[b * this.M + rs]);
146
+ const { sys, nB } = this, sz = sys.sizes, m = sz[b];
147
+ sys.B.fill(0, sys.offB[b] + l * m, sys.offB[b] + (l + 1) * m);
148
+ if (b > 0) sys.A.fill(0, sys.offA[b] + l * sz[b - 1], sys.offA[b] + (l + 1) * sz[b - 1]);
149
+ if (b < nB - 1) sys.C.fill(0, sys.offC[b] + l * sz[b + 1], sys.offC[b] + (l + 1) * sz[b + 1]);
150
+ }
151
+
152
+ // The Jacobian at dt with the dilute kernels' terms kept aside in difference form (see
153
+ // DifferenceTerms), the rest assembled into a matrix of its own. The residual and terminal terms
154
+ // come out as from assemble(dt); the Newton matrix is left as it was.
155
+ _assembleDifference(dt) {
156
+ const keep = this.sys;
157
+ const rest = this.sysRest ?? (this.sysRest = new BlockTridiagonal(keep.n, keep.sizes));
158
+ const n = this.n, nN = this.nNodes;
159
+ const lin = this.diffTerms ?? (this.diffTerms = new DifferenceTerms(keep.size, nN * n * n, nN * n, 2 * nN * n * n));
160
+ this.sys = rest;
161
+ this.lin = lin;
162
+ try {
163
+ this.assemble(dt);
164
+ } finally {
165
+ this.sys = keep;
166
+ this.lin = null;
167
+ }
168
+ return { rest, lin };
169
+ }
170
+
171
+ // The contact and port readouts (fluxes, displacements, the last segment's current) at the
172
+ // current state, from only the boxes they're read from. Each of those boxes gets the same terms
173
+ // in the same order as in assemble(), so the readouts are identical; other residuals and the
174
+ // Jacobian are left partial (the next assemble() starts afresh). Concentrations must be current.
175
+ _assembleBookkeeping(dt) {
176
+ const { model } = this;
177
+ this.dtNow = dt;
178
+ const { grid, materials, regions } = model;
179
+ this.res.fill(0);
180
+ for (let r = 0; r < regions.length; r++) {
181
+ const { nodes, segs } = this.bookkeeping[r];
182
+ if (nodes.length === 0 && segs.length === 0) continue;
183
+ const reg = regions[r], mat = materials[reg.material];
184
+ if (mat.conductor) {
185
+ for (const g of nodes) this._nodeConductor(g, dt);
186
+ for (const s of segs) this._segmentConductor(s, s + r, s + r + 1, mat, grid.segLength[s] / grid.segArea[s]);
187
+ continue;
188
+ }
189
+ for (const g of nodes) {
190
+ if (mat.ideal) this._nodesDilute(g, g, dt);
191
+ else this._nodeConcentrated(g, dt);
192
+ }
193
+ const rxs = this.rxsIn[reg.material];
194
+ if (rxs.length > 0) for (const g of nodes) this._bulkReactions(g, rxs);
195
+ for (const s of segs) {
196
+ const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s]; // (a length over the cross-section: fluxes are totals)
197
+ this._segmentDisplacement(s, bL, bR, mat, h);
198
+ if (reg.mixing > 0) this._segmentMixing(s, bL, bR, reg.mixing, h, mat);
199
+ if (!mat.ideal) this._segmentConcentrated(s, bL, bR, mat, h, reg.velocity);
200
+ }
201
+ if (mat.ideal) for (const s of segs) this._segmentsDilute(s, s + 1, r, mat, reg.velocity);
202
+ }
203
+ for (const f of this.bookkeepingFaces) this._face(f);
204
+ this._terminals(dt);
205
+ }
206
+ }
package/src/bernoulli.js CHANGED
@@ -27,3 +27,35 @@ export function bernoulliDerivative(x) {
27
27
  // B' = (B/x)(1 − B(−x)) = (B/x)(1 − B − x)
28
28
  return (b / x) * (1 - b - x);
29
29
  }
30
+
31
+ // Past a drift of SG_FAR thermal units across a segment, a Scharfetter–Gummel flux's B(d) heads
32
+ // for underflow and its expm1(Δη) for overflow (both by d ≈ 710, where their product is 0·∞):
33
+ // the product is then taken whole.
34
+ export const SG_FAR = 300;
35
+
36
+ /**
37
+ * A Scharfetter–Gummel flux N = −g·B(d)·c_L·expm1(Δη) and its derivatives, into `out`: [N, ∂N/∂d,
38
+ * g·B(d)·c_L (= ∂N/∂η_L), g·B(d)·c_L·e^{Δη} (= −∂N/∂η_R), −g·expm1(Δη)·(B(d) + B′(d)) (the
39
+ * non-ideal kernel's ∂N/∂ζ_L per unit K_L)]. d is the drift, Δη the difference of levels, so
40
+ * that c_R = c_L e^{Δη−d}. From d = SG_FAR on, the same flux from the downstream end, N =
41
+ * g·B(−d)·c_R·expm1(−Δη), whose factors stay finite; ∂N/∂d = N·B′(d)/B(d).
42
+ */
43
+ export function sgFlux(g, d, deta, cL, cR, out) {
44
+ if (d < SG_FAR) {
45
+ const E = Math.expm1(deta), B = bernoulli(d), Bp = bernoulliDerivative(d), gBc = g * B * cL;
46
+ out[0] = -gBc * E;
47
+ out[1] = -g * Bp * cL * E;
48
+ out[2] = gBc;
49
+ out[3] = gBc * (E + 1);
50
+ out[4] = -g * E * (B + Bp);
51
+ return out;
52
+ }
53
+ const B = bernoulli(d), up = g * bernoulli(-d) * cR, L = (1 - B - d) / d; // L = B′/B
54
+ out[0] = up * Math.expm1(-deta);
55
+ out[1] = out[0] * L;
56
+ out[2] = g * B * cL;
57
+ out[3] = up;
58
+ // (B·expm1(Δη), taken whole: d e^{Δη−d}(1 − e^{−Δη})/(1 − e^{−d}))
59
+ out[4] = -g * ((d * Math.exp(deta - d) * -Math.expm1(-deta)) / -Math.expm1(-d)) * (1 + L);
60
+ return out;
61
+ }