driftlet 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/conventions.md +8 -3
- package/docs/device.md +10 -1
- package/docs/internals.md +33 -14
- package/docs/numerics.md +55 -11
- package/docs/reliability.md +7 -3
- package/llms.txt +6 -6
- package/package.json +1 -1
- package/src/assemble.js +206 -0
- package/src/bernoulli.js +32 -0
- package/src/boundaries.js +795 -0
- package/src/device.js +32 -6
- package/src/impedance.js +755 -0
- package/src/index.js +24 -1
- package/src/newton.js +587 -0
- package/src/regions.js +1009 -0
- package/src/solution.js +19 -2
- package/src/solver.js +166 -3640
- package/src/start.js +476 -0
- package/src/steady.js +754 -0
- package/src/timeSteps.js +4 -1
- package/src/types.js +3 -0
- package/types/assemble.d.ts +18 -0
- package/types/bernoulli.d.ts +9 -0
- package/types/boundaries.d.ts +24 -0
- package/types/impedance.d.ts +53 -0
- package/types/newton.d.ts +7 -0
- package/types/regions.d.ts +61 -0
- package/types/solver.d.ts +12 -220
- package/types/start.d.ts +6 -0
- package/types/steady.d.ts +78 -0
- package/types/types.d.ts +6 -0
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.
|
|
59
|
+
`https://cdn.jsdelivr.net/npm/driftlet@1.3.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
|
|
60
60
|
TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
|
|
61
61
|
[`llms.txt`](llms.txt).
|
|
62
62
|
|
package/docs/conventions.md
CHANGED
|
@@ -94,13 +94,18 @@ Two optional transport terms describe a moving fluid:
|
|
|
94
94
|
|
|
95
95
|
- **Advection** by an imposed velocity $`v`$ adds $`v \cdot c_i`$ to every mobile species' flux.
|
|
96
96
|
- **Eddy mixing** adds $`-(D_{\mathrm{mix}}/RT) P \nabla\bar\mu`$, with
|
|
97
|
-
$`P =
|
|
97
|
+
$`P = K - K z z^\mathsf{T} K / (z^\mathsf{T} K z)`$ and $`K = \partial c/\partial\zeta`$ (the
|
|
98
|
+
statistics' $`K`$; $`\mathrm{diag}(c)`$ when dilute).
|
|
98
99
|
- It's an Onsager term: symmetric, positive semi-definite, and zero at equilibrium (flat
|
|
99
100
|
$`\bar\mu`$), so double layers at rest are untouched.
|
|
100
101
|
- The projection removes any current ($`z^\mathsf{T} P = 0`$). Eddies move neutral fluid, so
|
|
101
102
|
they mix salt and stir concentration gradients but conduct nothing.
|
|
102
|
-
-
|
|
103
|
-
|
|
103
|
+
- Since $`K\nabla\zeta = \nabla c`$, it's $`-D_{\mathrm{mix}} \nabla c`$ for every species in a
|
|
104
|
+
neutral solution, under any statistics: eddies carry parcels of fluid, so they mix
|
|
105
|
+
concentration, not activity. (Molecular diffusion differs on purpose: a molecule's own random
|
|
106
|
+
walk is driven by the thermodynamic force, so its mobility is $`D c`$, above.) For a neutral
|
|
107
|
+
dilute species it's $`-D_{\mathrm{mix}} \nabla c`$ anywhere. For a binary salt it adds
|
|
108
|
+
$`D_{\mathrm{mix}}`$ to the salt's ambipolar diffusivity and leaves diffusion potentials
|
|
104
109
|
unchanged.
|
|
105
110
|
|
|
106
111
|
## Materials, regions, interfaces
|
package/docs/device.md
CHANGED
|
@@ -589,6 +589,11 @@ ports: [{
|
|
|
589
589
|
}]
|
|
590
590
|
```
|
|
591
591
|
|
|
592
|
+
The window holds exactly `to − from` of exchange, electrode and capacitance however the grid
|
|
593
|
+
falls: each node whose box reaches into it takes the part of its box inside. A held level
|
|
594
|
+
(`'equilibrium'`) holds the nodes inside the window, so it needs one there. A window of no width
|
|
595
|
+
is the node at that point, its whole box.
|
|
596
|
+
|
|
592
597
|
On a [conductor region](#materials), a port is a wire to the whole conductor: it takes no window,
|
|
593
598
|
only the conductor's carrier, and a conductance link's `G` is per area (S/m², a resistance
|
|
594
599
|
$`R \cdot A`$ to the port's voltage as $`G = 1/(R \cdot A)`$). That's how a floating electrode is tied
|
|
@@ -1007,7 +1012,11 @@ const now = dev.solution(); // snapshot of the current state
|
|
|
1007
1012
|
and nothing warns of it: in a porous electrode, $`\sqrt{\kappa/(\omega C a)}`$ (cells of 2 µm
|
|
1008
1013
|
against a 3.4 µm depth put Z 7% off, falling as the cells do; at an electrode, the diffusion
|
|
1009
1014
|
length $`\sqrt{2D/\omega}`$ at the highest frequency). The result is `{ f, Z: { re, im },
|
|
1010
|
-
warnings }`,
|
|
1015
|
+
relError, warnings }`: `relError[k]`, each Z's estimated error relative to $`|Z|`$ (from
|
|
1016
|
+
round-off in the current and the solves' residuals, and cautious: it can exceed the true error
|
|
1017
|
+
many times), and the warnings, the steady state's and one where a Z isn't resolved (an error
|
|
1018
|
+
over 1%: far below a capacitive device's relaxation, almost no current flows, and what does is
|
|
1019
|
+
lost to round-off) or Re Z is under a tenth of its error, relError·|Z|. With `profiles: true`, `profiles[k]` holds
|
|
1011
1020
|
frequency k's complex profiles, `{ phi, mu: {name}, c: {name} }`, each `{ re, im }` at the
|
|
1012
1021
|
nodes (`NaN` where a quantity doesn't exist), per unit excitation: a volt, or an A/m² where the
|
|
1013
1022
|
terminal is driven by a current.
|
package/docs/internals.md
CHANGED
|
@@ -16,10 +16,17 @@ them is wrong: fix both, with a test, in the same commit.
|
|
|
16
16
|
| `src/laws.js` | yes: the steady system's conservation laws |
|
|
17
17
|
| `src/lawRows.js` | yes: the laws written into the Newton system, and the bordered solve |
|
|
18
18
|
| `src/timeSteps.js` | yes: one step (backward Euler, BDF2), advance's halving, integrate's error control |
|
|
19
|
+
| `src/start.js` | yes: the cold start, from each region's c0 and the contacts' and ports' levels |
|
|
20
|
+
| `src/newton.js` | yes: Newton's iteration, its scaled rows, GMRES-refined solves and the islands' round |
|
|
21
|
+
| `src/assemble.js` | yes: the equations assembled, the conventions of every row, and the order of the terms |
|
|
22
|
+
| `src/regions.js` | yes: statistics, storage, fluxes along segments, bulk reactions, the neutral nodes' charge rows |
|
|
23
|
+
| `src/boundaries.js` | yes: faces (φ laws, links, reactions, gates), ports, contacts, circuit rows |
|
|
24
|
+
| `src/steady.js` | yes: steady solves, their stages and continuations, the pseudo-transient ramp |
|
|
25
|
+
| `src/impedance.js` | yes: the small-signal impedance, its readouts and profiles |
|
|
19
26
|
| `src/difference.js` | header: the dilute kernels in difference form |
|
|
20
27
|
| `src/blockTridiagonal.js` | header: the block Thomas solver |
|
|
21
28
|
| `src/statistics.js` | header: each model's `evaluate` / `invert` |
|
|
22
|
-
| `src/solver.js` |
|
|
29
|
+
| `src/solver.js` | no: setup, bookkeeping and drivers, described below |
|
|
23
30
|
|
|
24
31
|
## From a definition to a solution
|
|
25
32
|
|
|
@@ -46,7 +53,9 @@ them is wrong: fix both, with a test, in the same commit.
|
|
|
46
53
|
its block (−1: inactive), `rix[o]` its index in compact vectors, with `rix = N` (the sink) for
|
|
47
54
|
inactive ones. `res`, `delta` and the Jacobian (`sys`, a `BlockTridiagonal`) are compact.
|
|
48
55
|
- **Unknowns** are `u + uLo`, a double-double, so that differences of η keep their digits; the
|
|
49
|
-
Jacobian and solves are plain doubles.
|
|
56
|
+
Jacobian and solves are plain doubles. (η is read as `u + uLo` wherever a difference of levels
|
|
57
|
+
is taken; φ̂ in the statistics and the displacement, a face's own slots and the gates as `u`
|
|
58
|
+
alone.) Concentrations `c[g·n + i]` and coverages
|
|
50
59
|
`th[g·nSurf + q]` follow from `u` (`computeConcentrations`).
|
|
51
60
|
- **Terminals:** 0 the left contact, 1 the right, `2 + k` port k. A floating terminal (driven by
|
|
52
61
|
a current, or behind a resistance) has its voltage as an extra unknown and its circuit row
|
|
@@ -61,24 +70,34 @@ By method, in file order:
|
|
|
61
70
|
|
|
62
71
|
- **Setup:** the constructor (layout, per-node data, φ-undefined regions, terminals),
|
|
63
72
|
`_activeSlots`, `_findStretches` (stretches, flat stretches), `_laws` and `_structure` (the
|
|
64
|
-
laws, from `src/laws.js`)
|
|
73
|
+
laws, from `src/laws.js`).
|
|
65
74
|
- **Applying the laws** is `src/lawRows.js` (`captureLaws`, `captureLocal`, `snapFlat`,
|
|
66
|
-
`applyLaws`, `solveBordered`), called from `assemble`,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- **
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
`applyLaws`, `solveBordered`), called from `assemble`, Newton and the steady solves.
|
|
76
|
+
- **Starting state** is `src/start.js` (`initFromComposition`, which the Solver's own calls);
|
|
77
|
+
`_surfaceStart`, which the API uses too, stays in the solver.
|
|
78
|
+
- **Assembly** is `src/assemble.js` (`assemble`, `_assembleDifference` with `src/difference.js`,
|
|
79
|
+
`_assembleBookkeeping`, and `_j` and `_replaceRow`, which write the Jacobian), with its kernels
|
|
80
|
+
in `src/regions.js` (`computeConcentrations` and the statistics, nodes, segments, bulk
|
|
81
|
+
reactions and their rates, `_chargeRows` and `_untransform`) and `src/boundaries.js` (faces,
|
|
82
|
+
`_terminals`: ports, contacts, circuit rows). Their methods are the Solver's own (installed on
|
|
83
|
+
its prototype), since every term reads and writes its arrays.
|
|
84
|
+
- **Newton** is `src/newton.js` (the iteration, its row scaling, the GMRES-refined solves, the
|
|
85
|
+
islands' second round), which the Solver's own `newton` calls; `_factor` stays in the solver.
|
|
73
86
|
- **Time steps** are `src/timeSteps.js` (`step`, `advance`, `integrate`, the predictor and the
|
|
74
87
|
error norm), which the Solver's own `step`, `advance` and `integrate` call; `_snapshot` /
|
|
75
88
|
`_restore`, `_warmFrom` and `_probes`, which the API uses too, stay in the solver.
|
|
76
|
-
- **Small signal
|
|
89
|
+
- **Small signal** is `src/impedance.js` (`impedance` and its helpers: the current cuts, the
|
|
90
|
+
regions' uniform shift, the profiles; the complex GMRES and small dense solve), methods of the
|
|
91
|
+
Solver like the assembly's.
|
|
77
92
|
- **Bookkeeping:** `segmentFluxes`, `faceOneWay`, `_assembleBookkeeping`, the contacts' intake.
|
|
78
|
-
- **Steady solves
|
|
79
|
-
a
|
|
93
|
+
- **Steady solves** are `src/steady.js` (`solveSteady`, `_steadyFromHere`, the continuations:
|
|
94
|
+
floating, generation, a contact's voltage; `_steadySteps`, the pseudo-transient ramp, and
|
|
95
|
+
`_renormalizeSpectators`; `_directSteady`, `slowestTime`, `fastestTime`, `stuckReactions`),
|
|
96
|
+
methods of the Solver like the assembly's.
|
|
80
97
|
|
|
81
|
-
|
|
98
|
+
What stays in `src/solver.js` is the setup, the bookkeeping, the probes and snapshots, and the thin
|
|
99
|
+
drivers that call the modules: about a thousand lines, with no contract of its own beyond this
|
|
100
|
+
section.
|
|
82
101
|
|
|
83
102
|
## Reviewing one module
|
|
84
103
|
|
package/docs/numerics.md
CHANGED
|
@@ -171,13 +171,25 @@ potential and the quasi-Fermi difference are both shifted by the cell Péclet nu
|
|
|
171
171
|
$`\mathrm{Pe} = v h/D`$, $`\Delta \to \Delta - \mathrm{Pe}`$, $`\Delta\eta \to \Delta\eta - \mathrm{Pe}`$.
|
|
172
172
|
That's the exact constant-flux solution of $`N = v c - D(c' + c \psi')`$ across the cell, so pure
|
|
173
173
|
convection–diffusion is exact on any grid, upwinding itself automatically at large $`\mathrm{Pe}`$.
|
|
174
|
+
Past a drift of 300 thermal units across a cell (flow toward −x past a cell Péclet number of 300),
|
|
175
|
+
the flux is taken from its downstream end, $`g B(-\Delta) c_R \operatorname{expm1}(-\Delta\eta)`$, the
|
|
176
|
+
same flux, since there $`B(\Delta)`$ underflows and $`\operatorname{expm1}(\Delta\eta)`$ overflows.
|
|
174
177
|
|
|
175
178
|
**Eddy mixing** is discretised on each segment as
|
|
176
|
-
$`N_i = -(D_{\mathrm{mix}}/h) \sum_j \bar P_{ij} \Delta\eta_j`$, where $`\bar P`$ is built from
|
|
177
|
-
|
|
178
|
-
species' mixing flux exactly $`-D_{\mathrm{mix}} \Delta c/h`$.
|
|
179
|
+
$`N_i = -(D_{\mathrm{mix}}/h) \sum_j \bar P_{ij} \Delta\eta_j`$, where $`\bar P`$ is built from a
|
|
180
|
+
segment mobility $`\bar K`$. Between ideal ends $`\bar K`$ is the logarithmic mean of each
|
|
181
|
+
concentration, which makes a neutral species' mixing flux exactly $`-D_{\mathrm{mix}} \Delta c/h`$.
|
|
182
|
+
Between crowded ends it's $`\bar K = \tfrac12(R_L K_L R_L + R_R K_R R_R)`$ with
|
|
183
|
+
$`R = \mathrm{diag}(\sqrt{\bar c/c})`$ at each end: each end's $`K`$ rescaled to the logarithmic
|
|
184
|
+
means and averaged. That's the dilute form where statistics are ideal, positive definite, and
|
|
185
|
+
smooth in the state, and in a neutral solution it gives $`-D_{\mathrm{mix}} \Delta c/h`$ to second
|
|
186
|
+
order in the cells. (A secant $`\bar K`$, exact in $`\Delta c`$, can be built from the ends' $`c`$ and
|
|
187
|
+
$`\zeta`$ alone, but not smoothly: where a solution is uniform its correction depends on the
|
|
188
|
+
direction of $`\Delta\zeta`$, a kink right where a salt carries current.) The flux is exactly zero at
|
|
179
189
|
equilibrium, and exactly current-free ($`z^\mathsf{T}\bar P = 0`$ for any $`\bar P`$ built this way). Its
|
|
180
|
-
full Jacobian, including the dependence of $`\bar P`$ on both ends, keeps Newton quadratic.
|
|
190
|
+
full Jacobian, including the dependence of $`\bar P`$ on both ends, keeps Newton quadratic. Through
|
|
191
|
+
$`K_L`$ and $`K_R`$ that needs $`\partial K/\partial\zeta`$, which the statistics models don't give, so
|
|
192
|
+
it's taken by central differences of the model (a step of 1e-5, accurate to about 1e-10).
|
|
181
193
|
|
|
182
194
|
With non-ideal statistics, $`\ln c = \zeta - \mathrm{ex}`$, where the excess
|
|
183
195
|
$`\mathrm{ex} = \zeta - \ln(c/c_{\mathrm{ref}})`$ is zero for ideal statistics. The excess enters
|
|
@@ -508,8 +520,9 @@ which cost four assemblies per product.
|
|
|
508
520
|
GMRES on $`J\delta = r`$, the factorised system as the preconditioner and $`J \cdot v`$ exact, each
|
|
509
521
|
flux on its $`\eta`$ difference (see [exact J·v](#exact-jv)), as the impedance does. A lost mode
|
|
510
522
|
takes GMRES a couple of iterations. The refined update is kept where GMRES converged and it
|
|
511
|
-
moves no level by a thermal unit or more; otherwise refining stops for the rest of the solve
|
|
512
|
-
|
|
523
|
+
moves no level by a thermal unit or more; otherwise refining stops for the rest of the solve
|
|
524
|
+
(on a device marked as below, for two iterations, three times at most).
|
|
525
|
+
A MOS capacitor's gate step now reaches the low-frequency charge in ~30 steps (it took 80,000,
|
|
513
526
|
and ended 1.5% off). Once refined, a solve stays refined: a GaAs junction without
|
|
514
527
|
recombination, whose minority carriers reach it only from the contacts, passes ~1.5e-7 A/m²,
|
|
515
528
|
which plain solves past the first refinement left to round-off (cold and warm solves 1e-3
|
|
@@ -541,9 +554,32 @@ which cost four assemblies per product.
|
|
|
541
554
|
conductances, loses about $`\log_{10}(g/G)`$ and fails near 15. It's not reported for solves that
|
|
542
555
|
converge, since digits can be lost harmlessly too (a saturated species whose $`\eta`$ nothing
|
|
543
556
|
depends on).
|
|
544
|
-
- The last iteration usually only confirms convergence (updates go like
|
|
545
|
-
|
|
546
|
-
|
|
557
|
+
- The last iteration usually only confirms convergence (updates go like 1e-2, 1e-9, 1e-15). A
|
|
558
|
+
time step stops one iteration early where the convergence rate says the next update would be
|
|
559
|
+
under the tolerance: with $`\theta`$ the last update over the one before (under 0.1, the update
|
|
560
|
+
under 1e-6), $`\theta/(1-\theta)`$ of the update (Hairer and Wanner's estimate of the error
|
|
561
|
+
left) under 1e-10. That saves a third of a typical step's work. Steady solves don't, since
|
|
562
|
+
their acceptance is checked against the residual anyway.
|
|
563
|
+
- **A mode the plain solves never see, in time steps.** Neither a plain update nor the residual
|
|
564
|
+
shows a lost mode. Newton accepted a MOS capacitor's gate step at updates of 1e-10 while the
|
|
565
|
+
inversion layer's level sat 2.4e-4 thermal units off, the residual 7e-15 there and at the
|
|
566
|
+
solution alike, and the gate's charge ended 3.4e-5 off the steady one (a cold review of
|
|
567
|
+
`src/newton.js` found it: a tight re-solve from each accepted state was the instrument). So a
|
|
568
|
+
refinement that corrects a time step's nearly converged plain update (under 1e-4) by more than
|
|
569
|
+
its own size and 1e-6, by a correction the next update keeps, marks the device: from then on
|
|
570
|
+
its time steps are refined throughout, converge only on a refined update, and don't stop
|
|
571
|
+
early on the rate. The gate step then ends 4e-10 off, in fewer factorisations (121 against
|
|
572
|
+
222; 2 steps rejected, not 5). A correction the next update undoes is $`J \cdot v`$'s own
|
|
573
|
+
round-off in a row of huge entries (one ulp's worth, $`2^{-13}`$ or $`2^{-19}`$ exactly, on
|
|
574
|
+
healthy devices), and marks nothing. A lost mode can also never stall Newton at all: a floating
|
|
575
|
+
gate at rest in inversion converged quadratically at every step while the extrapolated guess's
|
|
576
|
+
error in the layer's level grew, 8.5e-5 V by 1e5 s, and by 1e7 s took 63,000 steps. So a time
|
|
577
|
+
step's 1st, 2nd, 4th, 8th… and every 32nd convergence on a device not yet marked is probed
|
|
578
|
+
with one refined update at the accepted state; one over 1e-8 thermal units (about a dozen over
|
|
579
|
+
the suite, which the next update undid on healthy devices) sends Newton on, refining, and the
|
|
580
|
+
next update decides as above. The gate then stays within 1e-14 V, in 7 steps to 1e7 s. The
|
|
581
|
+
probes and the refining cost the suite 2.5% more assemblies and 1.3% more factorisations;
|
|
582
|
+
strictly neutral transients, marked, came out up to 1.7e-5 off and now 1.9e-6.
|
|
547
583
|
- Tried and rejected for large jumps (the retry at 3 thermal units per iteration takes 30–50
|
|
548
584
|
iterations for a 1.4 V jump on a pn diode): per-component logarithmic damping, as in
|
|
549
585
|
SPICE junction limiting, and an adaptive limit that grows while updates shrink. Both
|
|
@@ -776,7 +812,11 @@ the terms that cancel:
|
|
|
776
812
|
next: there, backward-Euler steps at a huge $`dt`$ (10⁶ × the slowest diffusion time) keep
|
|
777
813
|
the storage term, growing ×10 (capped) while the state still moves, each spectator's level
|
|
778
814
|
first shifted uniformly to restore its amount exactly (round-off creeps through the vanishing
|
|
779
|
-
storage term), and one last step at the base huge $`dt`$, where pinning is tight.
|
|
815
|
+
storage term), and one last step at the base huge $`dt`$, where pinning is tight. The amounts
|
|
816
|
+
that only those steps keep are checked after each, and a step that moved one fails: where the
|
|
817
|
+
slowest species is very slow (D ~ 1e-22 m²/s), the storage term falls below round-off against a
|
|
818
|
+
fast species' conduction, and a closed Y + Z, Y still in part of its stretch, had converged 1e8
|
|
819
|
+
times too large. The slowest diffusion time counts only materials some region is made of.
|
|
780
820
|
- **A flat level.** A species that no reaction or port touches, reached by one contact at most,
|
|
781
821
|
carries no flux in a steady state: every sum of its balance rows from its closed end is the
|
|
782
822
|
flux through one segment, zero. In an ideal region without flow or mixing that's a level flat
|
|
@@ -916,7 +956,11 @@ A solve that fails falls back stage by stage:
|
|
|
916
956
|
the direct solve. After a failed step it grows ×2, then ×1.5 more each success, back to ×10.
|
|
917
957
|
Once steps reach the slowest diffusion time, the direct solve is tried from there, once a
|
|
918
958
|
decade of $`dt`$ (the state kept if it fails): with immobile traps and ions of D = 1e-17 m²/s,
|
|
919
|
-
steps from 1e8 to 1e9 s kept failing on the way to the end.
|
|
959
|
+
steps from 1e8 to 1e9 s kept failing on the way to the end. The ramp reaches about 1e12 times
|
|
960
|
+
the slowest diffusion time, so a reaction slower than that, which alone settles a closed
|
|
961
|
+
combination (no row of the steady system holds a reaction at equilibrium node by node), isn't
|
|
962
|
+
waited out: the solve fails, or converges where it began, and its warnings name the reaction
|
|
963
|
+
either way. A transient gets there (Y = Z at $`k_f`$ = 1e-12 s⁻¹ in 1610 steps).
|
|
920
964
|
|
|
921
965
|
Each of these carries cases that nothing else does (switched off one at a time, each fails
|
|
922
966
|
tests or stress cases the others pass).
|
package/docs/reliability.md
CHANGED
|
@@ -76,9 +76,9 @@ recipes.
|
|
|
76
76
|
| Second law | free energy in through the terminals (Σ N μ̄ over every species) equals the dissipation, every term ≥ 0: a pn diode, an open-circuit junction running on chemical input alone, electrodes with a bipolar plate, a port and a contact behind conductances | 1e-10 |
|
|
77
77
|
| Transients | RC charging of a gated island; water relaxation rate | 1%; 2e-3 |
|
|
78
78
|
| Time integration | BE first order, BDF2 second order; adaptive error control | ratios 2, 4 |
|
|
79
|
-
| Advection and eddy mixing | exact convection–diffusion profile; `D + D_mix`; junction EMF unchanged by mixing | 1e-12; 1e-12; 1e-5 |
|
|
79
|
+
| Advection and eddy mixing | exact convection–diffusion profile; `D + D_mix`; junction EMF unchanged by mixing, ideal and crowded; crowded mixing −D_mix Δc (a neutral species on a lattice, second order; ions, lattice and Debye–Hückel, on 20 cells) | 1e-12; 1e-12; 1e-5; ratios 4, 1e-4 |
|
|
80
80
|
| Metal regions | ohmic; Schottky face and MOS gate equal their contact forms; Ag \| AgNO₃ \| Ag with metal electrodes; bipolar electrode at V/2 | 1e-12; 1e-9; 1e-9; 1e-9 |
|
|
81
|
-
| Internal ports | transmission line σV tanh(L/λ)/λ (O(h²)); held level; MOS low-frequency C–V with a grounded channel, and without one the same equilibrium, and a gate step filling the inversion layer to it | 1e-4; 1e-9; 2e-3; 1e-6; 1e-3 |
|
|
81
|
+
| Internal ports | transmission line σV tanh(L/λ)/λ (O(h²)); a window's exchange and electrode exactly `to − from` on any grid; held level; MOS low-frequency C–V with a grounded channel, and without one the same equilibrium, and a gate step filling the inversion layer to it | 1e-4; 1e-9; 1e-9; 2e-3; 1e-6; 1e-3 |
|
|
82
82
|
| Cross-sections (spherical, cylindrical, any A(x)) | steady diffusion to a sphere and a cylinder (exact on any grid); Cottrell with the spherical term; uptake by a sphere filling from its surface; Debye–Hückel around a charged sphere, potential and charge | 1e-10; 3e-4; 2e-4; 1e-3, 3e-4 |
|
|
83
83
|
| A capacitance through a port | a thin-film transistor against the charge-sheet model (below threshold to saturation, second order); de Levie impedance; gate impedance against dQ/dV | 5e-4; 1e-3; 1e-6 |
|
|
84
84
|
| Electrode surfaces (coverage) | Langmuir isotherm against potential; the charge to fill a surface; the active–passive curve, blocking exactly (1 − θ) | 1e-9; 1e-3; 1e-9 |
|
|
@@ -174,7 +174,11 @@ disagree (a port's voltage at 0 for −1.42 V, θ 0.05 for 0.015, a contact 24 m
|
|
|
174
174
|
the fixes, device by device, so its results above stand. On 1.2.2 the whole sweep, 870 devices,
|
|
175
175
|
gives the same: 605 agree, 117 transients fail, and 13 differ, each a capacitive contact beside a
|
|
176
176
|
membrane capacitor where the transient loses K⁺. Since a face out of equilibrium in `c0` starts
|
|
177
|
-
in it (1.2.3), 607 agree and 115 fail, the same 13 differing.
|
|
177
|
+
in it (1.2.3), 607 agree and 115 fail, the same 13 differing. Those 13 were a missing derivative:
|
|
178
|
+
a contact's current, read from its end box, didn't take the membrane port's voltage that reaches
|
|
179
|
+
it (a cold review of `src/boundaries.js`), so Newton stopped short in a way the tolerance set. With
|
|
180
|
+
it in (1.2.7), the transient settles on the steady state to 1e-14 V, in a third of the steps, and
|
|
181
|
+
648 agree, 87 transients fail, none differ.
|
|
178
182
|
|
|
179
183
|
## Consistent: the derivatives
|
|
180
184
|
|
package/llms.txt
CHANGED
|
@@ -30,9 +30,9 @@ materials, insertion hosts), data.md and reliability.md (what's validated, and t
|
|
|
30
30
|
From a CDN, pin the version you tested against:
|
|
31
31
|
|
|
32
32
|
```js nocheck
|
|
33
|
-
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
34
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
35
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
33
|
+
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.0/src/index.js';
|
|
34
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.0/src/kit.js';
|
|
35
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.0/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.
|
|
223
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
224
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.
|
|
222
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.0/src/index.js';
|
|
223
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.0/src/kit.js';
|
|
224
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.0/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.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Dependency-free 1D drift–diffusion–reaction (Poisson–Nernst–Planck) solver for semiconductors, electrochemistry and nerves, fast enough for live web demos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/assemble.js
ADDED
|
@@ -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
|
+
}
|