driftlet 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/conventions.md +2 -2
- package/docs/data.md +9 -0
- package/docs/device.md +9 -6
- package/docs/numerics.md +23 -10
- package/docs/reliability.md +23 -3
- package/docs/statistics.md +1 -0
- package/llms.txt +6 -6
- package/package.json +2 -1
- package/src/channels.js +12 -7
- package/src/checks.js +6 -4
- package/src/data.js +5 -3
- package/src/describe.js +1 -1
- package/src/device.js +13 -8
- package/src/devices.js +9 -1
- package/src/difference.js +42 -7
- package/src/equation.js +14 -3
- package/src/index.js +6 -3
- package/src/junction.js +9 -3
- package/src/kit.js +1 -1
- package/src/live.js +14 -9
- package/src/particles.js +4 -1
- package/src/plot.js +8 -4
- package/src/solution.js +1 -0
- package/src/solver.js +329 -110
- package/src/stack.js +5 -1
- package/src/statistics.js +31 -10
- package/src/traces.js +10 -7
- package/src/types.js +96 -29
- package/types/data.d.ts +2 -1
- package/types/difference.d.ts +8 -2
- package/types/index.d.ts +14 -5
- package/types/kit.d.ts +1 -1
- package/types/plot.d.ts +2 -2
- package/types/solution.d.ts +1 -0
- package/types/solver.d.ts +20 -3
- package/types/types.d.ts +179 -26
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ import { bandDiagram } from 'driftlet/plot'; // a level diagram as an SVG str
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Or straight from a CDN in a page, pinned to a version:
|
|
59
|
-
`https://cdn.jsdelivr.net/npm/driftlet@1.1
|
|
59
|
+
`https://cdn.jsdelivr.net/npm/driftlet@1.2.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/conventions.md
CHANGED
|
@@ -133,8 +133,8 @@ A contact is an interface with an **outside phase** whose levels are known, like
|
|
|
133
133
|
with a fully known node. The outside phase's charged levels form a rigid ladder,
|
|
134
134
|
$`V_i = V + \mathtt{offset}_i`$, which the external circuit slides by the **terminal voltage** $`V`$
|
|
135
135
|
(the voltage of a named terminal species). The laws joining it to the device (equilibrium,
|
|
136
|
-
blocked, conductance; and for $`\phi`$, bulk, neutral, capacitive,
|
|
137
|
-
internal faces. Reactions sit at faces between regions: an electrode is a conductor region with
|
|
136
|
+
blocked, conductance, exchange, velocity; and for $`\phi`$, bulk, neutral, capacitive, pinned)
|
|
137
|
+
are those of internal faces, with the outside phase as the other side. Reactions sit at faces between regions: an electrode is a conductor region with
|
|
138
138
|
reactions at its face.
|
|
139
139
|
|
|
140
140
|
- **Species in equilibrium** with the outside phase sit at $`V_i = V + \mathtt{offset}_i`$. The
|
package/docs/data.md
CHANGED
|
@@ -21,6 +21,15 @@ 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; if the
|
|
29
|
+
D don't matter either, taking the data as another temperature's is a fair approximation, made
|
|
30
|
+
by setting the material's `T` (the cell and axon demos do, having checked that D scaled by
|
|
31
|
+
T/η moves nothing they show).
|
|
32
|
+
|
|
24
33
|
| Ion | z | D (10⁻⁹ m²/s) | $`\mu^\circ`$ (kJ/mol) |
|
|
25
34
|
|---|---|---|---|
|
|
26
35
|
| H+ | +1 | 9.311 | 0 |
|
package/docs/device.md
CHANGED
|
@@ -250,7 +250,9 @@ r = \frac{n\,p\,(1 - e^{-A/RT})}{(n + n_1)/v_p + (p + p_1)/v_n}, \qquad p_1 = \f
|
|
|
250
250
|
with n and p each at its own side's edge node, the capture velocities $`v_n`$ and $`v_p`$ in m/s,
|
|
251
251
|
and $`n_1`$ (mol/m³) the negative species' concentration with its level at the trap (default:
|
|
252
252
|
$`n_1 = p_1 = n_i`$, a midgap trap). Taking $`p_1`$ from the state keeps equilibrium exact whatever
|
|
253
|
-
lies between the two sides (a band offset, a $`\phi`$ jump).
|
|
253
|
+
lies between the two sides (a band offset, a $`\phi`$ jump). With Fermi–Dirac carriers, each
|
|
254
|
+
one's emission scales by its own $`\gamma = \mathcal{F}_{1/2}(\zeta)/e^\zeta`$, by detailed balance:
|
|
255
|
+
$`n_1`$ becomes $`\gamma_n n_1`$, and $`p_1`$ keeps the product. Where one carrier is plentiful the
|
|
254
256
|
rate saturates at the other's capture, $`r \to v_p\,p`$: holes reaching an electron-rich layer
|
|
255
257
|
recombine at $`v_p`$ however many electrons wait there, which plain mass action can't do.
|
|
256
258
|
|
|
@@ -514,8 +516,8 @@ background carriers do). A solution against a charged end region (an ion exchang
|
|
|
514
516
|
its own material, `bath: { c, material: 'water' }`: its composition is then neutral by itself, its
|
|
515
517
|
levels are that material's, and the end node takes them through a Donnan step.
|
|
516
518
|
|
|
517
|
-
**`phi`**, required whenever any species
|
|
518
|
-
`'
|
|
519
|
+
**`phi`**, required whenever any species connects at the contact (default `'neutral'`, or
|
|
520
|
+
`'bulk'` with a bath):
|
|
519
521
|
- `'bulk'`: the end node is plain bulk, locally neutral, with no double layer at the contact
|
|
520
522
|
(ohmic contacts, baths). The outside takes whatever surface charge that needs. A double layer
|
|
521
523
|
or depletion region that reaches it is cut off there (σ 58% low for a crowded layer 5 Debye
|
|
@@ -870,7 +872,8 @@ advance it, `advance()` and `step()` do.
|
|
|
870
872
|
|
|
871
873
|
`{ hmin, hmax, ratio = 1.2, minCells = 8 }`, per device, overridable per region. With no grid
|
|
872
874
|
options at all, each region is graded from `hmin` = 1/1000 to `hmax` = 1/20 of its length. Cells grow
|
|
873
|
-
geometrically from `hmin` at both ends of each region up to `hmax`, scaled to fit exactly
|
|
875
|
+
geometrically from `hmin` at both ends of each region up to `hmax`, scaled to fit exactly
|
|
876
|
+
(`hLeft` and `hRight` set one end's size instead, for a region fine at one end only). Each
|
|
874
877
|
region boundary becomes a pair of nodes at the same x, one per side. The grid is never refined
|
|
875
878
|
automatically. If a double layer that the model resolves is coarser than the local Debye
|
|
876
879
|
length, the solution's `warnings` say so. That's a floor, not an accuracy: a strongly polarised
|
|
@@ -1059,12 +1062,12 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
|
|
|
1059
1062
|
| `ports[k]` | `{ name, V, flux: {name}, current }`: what each internal port brings into the device (an electrode's `flux` is of what its reactions make and use in the solution, its carrier's being outside); an electrode's rates, coverages and charge [as well](#what-a-solution-reports) |
|
|
1060
1063
|
| `interfaces[f]` | `{ left, right, dipole, sheetCharge, D, N: {name}, rates, links: {name} }`: the names of the regions the face joins, what crosses it by its links, each face reaction's rate (mol/(m²·s)), and how each species that crosses it does; for those crossing by permeability, `oneWay: { name: [toward +x, toward −x] }`, the one-way fluxes whose difference is `N` (Ussing's unidirectional fluxes) |
|
|
1061
1064
|
| `bulkReactions[k]` | `{ rate, forward, nu, regions, total }`: each bulk reaction's net forward rate at every node (mol/(m³·s), `NaN` where it doesn't run), its one-way forward rate (the backward is `forward − rate`), what one forward reaction makes of each species (`nu`, negative for what it consumes), and integrated over each region and the device (mol/(m²·s)), as the balances count it: a charge-balance check is J = F(generation − recombination), which the kit's `check()` does for every species |
|
|
1062
|
-
| `charge` | total charge in the device, C/m² (C through a cross-section) |
|
|
1065
|
+
| `charge` | total charge in the device, C/m² (C through a cross-section): its regions' space charge and its faces' sheet charges, not the charge on a capacitive contact's or port's plates, so under a gate's bias it's the gate's opposite |
|
|
1063
1066
|
| `conservation` | per species stretch: amount, reference, intake through contacts, and drift, relative: (amount − reference − intake) over the largest of them |
|
|
1064
1067
|
| `warnings` | e.g. unresolved double layers, conventions a statistics model relies on, a material's data for another temperature, and for a failed solve, why (a driven current it can't pass, where the system is nearly singular). A failed steady solve's state isn't a solution, so the warnings about a solution's accuracy (the grid, double layers, space charge reaching a contact) are left out of it |
|
|
1065
1068
|
| `converged`, `iterations`, `steps`, `substeps`, `history`, `time`, `T` | solver bookkeeping, and the temperature |
|
|
1066
1069
|
| `steady` | whether this is a converged steady state (from `solve()`), not a transient's: what `check()` in the kit decides its checks by |
|
|
1067
|
-
| `done`, `rejected`, `trace` | from `advance()`: whether `tEnd` was reached; rejected steps; `{ t, current, voltage }` per accepted step |
|
|
1070
|
+
| `done`, `rejected`, `trace` | from `advance()`: whether `tEnd` was reached; rejected steps; `{ t, current, voltage }` per accepted step, and `probes` if asked |
|
|
1068
1071
|
|
|
1069
1072
|
A stretch is a run of regions in which a species is present and connected. Its `drift`
|
|
1070
1073
|
compares its amount with the reference amount plus everything that came in through the
|
package/docs/numerics.md
CHANGED
|
@@ -450,8 +450,8 @@ The assembled matrix loses what a flux's two entries share. A Scharfetter–Gumm
|
|
|
450
450
|
$`\eta`$ at its two ends through $`G`$ and $`-G(1 + E)`$, with $`E = \mathrm{expm1}(\Delta\eta)`$;
|
|
451
451
|
for a nearly uniform $`v`$ they cancel, and $`J \cdot v`$ keeps only round-off of $`G|v|`$, which in
|
|
452
452
|
an inversion layer's 0.1 nm cells ($`G`$ ~ 1e11) dwarfs the flux. Where a solve needs
|
|
453
|
-
$`J \cdot v`$ itself (GMRES, in the impedance and in Newton's refined solves), the
|
|
454
|
-
|
|
453
|
+
$`J \cdot v`$ itself (GMRES, in the impedance and in Newton's refined solves), the kernels' terms
|
|
454
|
+
are kept aside instead, each evaluated on differences first:
|
|
455
455
|
|
|
456
456
|
```math
|
|
457
457
|
\delta N = a\,(\delta\eta_L - z\,\delta\hat\phi_L) + b\,(\delta\eta_R - \delta\eta_L) + d\,(\delta\hat\phi_R - \delta\hat\phi_L),
|
|
@@ -459,8 +459,13 @@ terms are kept aside instead, each evaluated on differences first:
|
|
|
459
459
|
|
|
460
460
|
with $`a = -GE`$ (through $`c_L`$), $`b = -G(1 + E)`$ and $`d`$ the drift's. A node's storage and space
|
|
461
461
|
charge depend on $`\delta\eta_i - z_i\,\delta\hat\phi`$ alone, the displacement on
|
|
462
|
-
$`\delta\hat\phi_R - \delta\hat\phi_L`$.
|
|
463
|
-
|
|
462
|
+
$`\delta\hat\phi_R - \delta\hat\phi_L`$. Non-ideal statistics couple the species ($`c_i`$
|
|
463
|
+
depends on every $`\zeta_j`$), so there a node's storage of $`i`$ has a term in each
|
|
464
|
+
$`\delta\eta_j - z_j\,\delta\hat\phi`$, through $`K_{ij}`$, and a flux has side terms in those at
|
|
465
|
+
either end (through $`c_L`$ and the excess potentials in its drift); each is in the same
|
|
466
|
+
differences. (Until 1.1.1 they were assembled directly, and a Fermi–Dirac MOS capacitor in
|
|
467
|
+
inversion, barely degenerate, came out with a negative resistance.) Everything else (faces,
|
|
468
|
+
contacts, ports, reactions, metal regions) is assembled into a matrix of its own. Rows that a contact or a
|
|
464
469
|
steady solve replaces drop the kept terms; the transformed rows of strictly neutral nodes combine
|
|
465
470
|
them as they combine the matrix's. A region's uniform shift ($`z_i s`$ on each $`\eta_i`$, $`s`$ on
|
|
466
471
|
$`\hat\phi`$) then gives exactly zero inside it, as it must. It replaced a central difference of
|
|
@@ -649,7 +654,7 @@ majority conductances times a $`\delta\eta`$ cancelling its neighbours' to 1e-17
|
|
|
649
654
|
round-off reads as a constant conductance of order 1 S/m², a silicon p⁺n⁺ junction's capacitance
|
|
650
655
|
9× off at 100 Hz. Between the two contacts of a device without ports, the total current
|
|
651
656
|
(conduction plus displacement, $`i\omega\,\delta D`$) is the same through every cut, so it is also
|
|
652
|
-
read across each segment of
|
|
657
|
+
read across each segment of a region (with any statistics), linearised as its flux is, and
|
|
653
658
|
the reading with the least estimated error is taken: in that junction, the depletion region's;
|
|
654
659
|
in a MOS capacitor, the oxide's displacement. A reading's error is its terms' round-off, and the
|
|
655
660
|
solves' error in $`\delta x`$ (their residual, relative to its largest entry) through its
|
|
@@ -720,13 +725,19 @@ the terms that cancel:
|
|
|
720
725
|
charging a capacitance (a gate, a port's capacitance) is the same, its charge in place of an
|
|
721
726
|
amount; and an electrode port's reactions draw on what the electrode holds, its charge
|
|
722
727
|
$`Q/(zF)`$ a column of its own under a current, so a port at no current reducing a closed
|
|
723
|
-
M⁺ (M⁺ + e⁻ = M(s)) keeps M⁺ + Q/F, or with no capacitance, M⁺ alone.
|
|
724
|
-
|
|
728
|
+
M⁺ (M⁺ + e⁻ = M(s)) keeps M⁺ + Q/F, or with no capacitance, M⁺ alone. Whatever such a
|
|
729
|
+
terminal both passes and stores is kept together, as its current counts both: a capacitive
|
|
730
|
+
port or contact at no current that passes K⁺ keeps $`n_K + Q/F`$ (a contact's Q its
|
|
731
|
+
displacement $`D_\text{in}`$), and two floating electrodes sharing a closed Fe³⁺ keep the iron
|
|
732
|
+
with both their charges, the law in one's circuit row moving with the other's voltage too.
|
|
733
|
+
At any other current such a terminal only stores what comes in, which has no steady state,
|
|
734
|
+
and the solve says so at once.
|
|
725
735
|
- an **immobile** combination (trap states X⁰ and X⁻ under e⁻ + X⁰ = X⁻, with D = 0) keeps it
|
|
726
736
|
node by node, since nothing carries it anywhere, as do an electrode's surface species
|
|
727
737
|
(adsorbates that only turn into each other keep their total on every site), its row local,
|
|
728
|
-
one per node in its own block
|
|
729
|
-
|
|
738
|
+
one per node in its own block, a coverage counted as the sites it fills per volume,
|
|
739
|
+
$`\Gamma a \theta`$, beside the concentrations. (The null space is taken with the immobile and
|
|
740
|
+
surface columns first, so that these come out apart from the combinations that move.) A combination of immobile
|
|
730
741
|
stretches with a moving one (the electrons the traps hold, $`e^- - X^0`$, in a closed film) is
|
|
731
742
|
an amount like any other, its immobile parts summed over their nodes;
|
|
732
743
|
- a **floating metal** keeps the charge on its faces, its sheets' displacement.
|
|
@@ -791,7 +802,9 @@ the terms that cancel:
|
|
|
791
802
|
rows, a pin like an amount's, chosen by the same elimination and ahead of the amounts: the slowest
|
|
792
803
|
reaction first, and a row of one of its own species where one is left, never one picked by where
|
|
793
804
|
the species are listed (the rows had differed with the species reversed, and an electrode's
|
|
794
|
-
equilibrium solved in one listing and failed in the other).
|
|
805
|
+
equilibrium solved in one listing and failed in the other). A reaction that changes its
|
|
806
|
+
combination only through immobile species needs none: its rate already stands alone in their
|
|
807
|
+
balance at the face's node. The combinations come from the null
|
|
795
808
|
space with that reaction's row left out, any conserved amount added to one being as good (its
|
|
796
809
|
summed balance vanishes too), so that one can be found with a row left to take: silver's e⁻ − Fe³⁺
|
|
797
810
|
beside a closed iron couple whose affinity had Fe³⁺'s row is e⁻ + Fe²⁺ with the iron added. Never
|
package/docs/reliability.md
CHANGED
|
@@ -141,6 +141,29 @@ its true residual, blocked species held flat in steady solves, continuations for
|
|
|
141
141
|
and for light, and Newton refining its solves where a factorisation loses a mode, with an exact
|
|
142
142
|
$`J \cdot v`$.
|
|
143
143
|
|
|
144
|
+
## Conserved: the law sweep
|
|
145
|
+
|
|
146
|
+
A steady solve replaces one redundant equation per conserved combination with the combination's
|
|
147
|
+
amount (see [numerics](numerics.md#the-steady-system)). A combination left out, or weighed wrongly,
|
|
148
|
+
gives a state that converges and is steady but holds the wrong amounts, which only a transient
|
|
149
|
+
from the same start shows. `npm run lawsweep` builds 810 small devices from the features at the
|
|
150
|
+
edges of that bookkeeping: a floating, resistive or held electrode, with or without a
|
|
151
|
+
capacitance, with adsorbates or an immobile species that adsorbs; a second floating electrode
|
|
152
|
+
or a membrane capacitor; a capacitive or ion-passing contact at no current; a face that blocks
|
|
153
|
+
an ion or makes an immobile species. Each is solved directly and run as a transient for 1e6 s,
|
|
154
|
+
and where the transient converged, finished and moves no further over 100× as long, the two
|
|
155
|
+
must agree on every species' amount, every surface's sites and every floating terminal's
|
|
156
|
+
voltage.
|
|
157
|
+
|
|
158
|
+
Every reaction starts at its equilibrium. On 1.2.1, 675 devices are built (135 combinations
|
|
159
|
+
repeat others); 579 agree, and 83 transients fail to converge. In the other 13 the transient
|
|
160
|
+
is the one that's off: it loses a conserved K⁺ total in proportion to its tolerance (3e-5 of it
|
|
161
|
+
at tol 1e-9) when a capacitive contact, a membrane capacitor and a held electrode share the
|
|
162
|
+
device, where the steady solve keeps it to round-off. The sweep found its first law the day it
|
|
163
|
+
was written (an immobile species adsorbing on an electrode over part of its region); Gemini 4
|
|
164
|
+
Argon's review found nine more by hand (one through a device it suggested), each now in
|
|
165
|
+
`test/laws.test.js`.
|
|
166
|
+
|
|
144
167
|
## Hard cases
|
|
145
168
|
|
|
146
169
|
`npm run hard` keeps a corpus of named cases that were hard once (sharp 3 M | 1 µM junctions on
|
|
@@ -173,9 +196,6 @@ so a change that makes one worse shows.
|
|
|
173
196
|
- **A surface covered to a bare fraction below ~1e-10** barely affects anything any more, and
|
|
174
197
|
the solver struggles with it (the hard corpus's passivating drop without a double layer stops
|
|
175
198
|
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
199
|
- **Currents below ~0.1 nA/m²** (a GaAs junction's leakage without recombination) aren't resolved
|
|
180
200
|
between solves; such a current is zero for any practical purpose.
|
|
181
201
|
- **A failed solve says so:** `converged: false`, with a warning naming where the system lost
|
package/docs/statistics.md
CHANGED
|
@@ -44,6 +44,7 @@ most one model.
|
|
|
44
44
|
|
|
45
45
|
| Model | Species | $`c(\zeta)`$ | Parameters |
|
|
46
46
|
|---|---|---|---|
|
|
47
|
+
| `ideal` | any | $`c_{\mathrm{ref}} e^\zeta`$, the default, named only to say so | none |
|
|
47
48
|
| `fermi-dirac` | any, each independently | $`c_{\mathrm{ref}} \mathcal{F}_{1/2}(\zeta)`$ (3D band); $`c_{\mathrm{ref}} \ln(1 + e^\zeta)`$ with `order: 0` (2D) | `order`: 1/2 (default) or 0 |
|
|
48
49
|
| `lattice` | any, sharing sites | $`c_a = c_{\mathrm{max}} w_a / (1 + \sum w)`$, $`w_a = (c_{\mathrm{ref},a}/c_{\mathrm{max}}) e^{\zeta_a}`$ | `cMax`, site density, mol/m³ |
|
|
49
50
|
| `redlich-kister` | one | on its own sites: $`\zeta = \ln(x/(1-x)) + \ln(c_{\mathrm{max}}/c_{\mathrm{ref}}) + (g(x) - g(0))/RT`$, $`x = c/c_{\mathrm{max}}`$ | `cMax`; `A`, J/mol |
|
package/llms.txt
CHANGED
|
@@ -30,9 +30,9 @@ materials, insertion hosts), data.md and reliability.md (what's validated, and t
|
|
|
30
30
|
From a CDN, pin the version you tested against:
|
|
31
31
|
|
|
32
32
|
```js nocheck
|
|
33
|
-
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1
|
|
34
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1
|
|
35
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1
|
|
33
|
+
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.2.1/src/index.js';
|
|
34
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.2.1/src/kit.js';
|
|
35
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.2.1/src/plot.js';
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`. Every export is listed at the end.
|
|
@@ -219,9 +219,9 @@ a CDN needs a long virtual-time budget, 30 s.)
|
|
|
219
219
|
<span id="readout"></span>
|
|
220
220
|
<div id="figure"></div>
|
|
221
221
|
<script type="module">
|
|
222
|
-
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1
|
|
223
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1
|
|
224
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.1
|
|
222
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.2.1/src/index.js';
|
|
223
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.2.1/src/kit.js';
|
|
224
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.2.1/src/plot.js';
|
|
225
225
|
|
|
226
226
|
const def = build({
|
|
227
227
|
T: 300,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driftlet",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.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": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"stress": "node bench/stress.js",
|
|
34
34
|
"smoke": "node tools/smoke.js",
|
|
35
35
|
"shots": "node tools/shots.js",
|
|
36
|
+
"lawsweep": "node tools/lawsweep.js",
|
|
36
37
|
"prepublishOnly": "npm test && npm run types",
|
|
37
38
|
"types": "rm -rf types && npx -y -p typescript@5 tsc --allowJs --declaration --emitDeclarationOnly --target es2022 --module nodenext --moduleResolution nodenext --outDir types src/index.js src/kit.js src/plot.js"
|
|
38
39
|
},
|
package/src/channels.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { DeviceError, checkOptions } from './errors.js';
|
|
5
5
|
import { FARADAY, GAS_CONSTANT } from './constants.js';
|
|
6
|
+
import { bernoulli } from './bernoulli.js';
|
|
6
7
|
|
|
7
8
|
const fail = (message) => {
|
|
8
9
|
throw new DeviceError(message);
|
|
@@ -22,9 +23,10 @@ const HH_T = 273.15 + 6.3;
|
|
|
22
23
|
|
|
23
24
|
// The GHK current (outward, A/m²) of an ion at unit permeability, at voltage V (inside − outside).
|
|
24
25
|
export function ghkCurrent(z, inside, outside, V, T) {
|
|
26
|
+
// zF u (c_in − c_out e^−u)/(1 − e^−u) = zF (c_in B(−u) − c_out B(u)), B(x) = x/(eˣ − 1): finite
|
|
27
|
+
// at any u (the plain form was ∞/∞ past u = −710)
|
|
25
28
|
const u = (z * V * FARADAY) / (GAS_CONSTANT * T);
|
|
26
|
-
|
|
27
|
-
return (z * FARADAY * u * (inside - outside * Math.exp(-u))) / -Math.expm1(-u);
|
|
29
|
+
return z * FARADAY * (inside * bernoulli(-u) - outside * bernoulli(u));
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -59,6 +61,8 @@ export function hodgkinHuxley({ inside, outside, T, g = {}, leak = 'Cl-', leakZ
|
|
|
59
61
|
// T sets the gates' rates, so it must be the device's: never defaulted (their 6.3 °C, under a body-temperature device, doubled a spike's peak, silently).
|
|
60
62
|
need(num(T) && T > 0, `hodgkinHuxley: T must be the device's temperature in K (their own experiments were at 279.45 K, 6.3 °C), got ${T}`);
|
|
61
63
|
need(num(at), `hodgkinHuxley: at must be a voltage (V), got ${at}`);
|
|
64
|
+
checkOptions('hodgkinHuxley: g', g, ['Na', 'K', 'leak']);
|
|
65
|
+
checkOptions('hodgkinHuxley: names', names, ['Na', 'K']);
|
|
62
66
|
const G = { Na: 1200, K: 360, leak: 3, ...g };
|
|
63
67
|
const Na = names.Na ?? 'Na+', K = names.K ?? 'K+';
|
|
64
68
|
const q10 = Math.exp(Math.log(3) * ((T - HH_T) / 10)); // Q₁₀ = 3
|
|
@@ -79,9 +83,10 @@ export function hodgkinHuxley({ inside, outside, T, g = {}, leak = 'Cl-', leakZ
|
|
|
79
83
|
};
|
|
80
84
|
}
|
|
81
85
|
need(inside && outside && typeof inside === 'object' && typeof outside === 'object', 'hodgkinHuxley: give inside and outside concentrations (mol/m³) by species name (or, for a membrane port, its area)');
|
|
82
|
-
const P = (name, z,
|
|
86
|
+
const P = (name, z, key, what) => {
|
|
87
|
+
const gval = G[key];
|
|
83
88
|
need(num(inside[name]) && inside[name] > 0 && num(outside[name]) && outside[name] > 0, `hodgkinHuxley: give ${name}'s concentration inside and outside (mol/m³), for its ${what}`);
|
|
84
|
-
need(num(gval) && gval > 0, `hodgkinHuxley: g.${
|
|
89
|
+
need(num(gval) && gval > 0, `hodgkinHuxley: g.${key} must be a conductance > 0 (S/m²), got ${gval}`);
|
|
85
90
|
const E = ((GAS_CONSTANT * T) / (z * FARADAY)) * Math.log(outside[name] / inside[name]);
|
|
86
91
|
need(Math.abs(at - E) > 1e-4, `hodgkinHuxley: ${name}'s Nernst level, ${(E * 1e3).toFixed(1)} mV, is at the calibration voltage; choose another 'at'`);
|
|
87
92
|
return (gval * (at - E)) / ghkCurrent(z, inside[name], outside[name], at, T);
|
|
@@ -90,9 +95,9 @@ export function hodgkinHuxley({ inside, outside, T, g = {}, leak = 'Cl-', leakZ
|
|
|
90
95
|
return {
|
|
91
96
|
gates,
|
|
92
97
|
species: {
|
|
93
|
-
[Na]: { type: 'permeability', P: P(Na, 1,
|
|
94
|
-
[K]: { type: 'permeability', P: P(K, 1,
|
|
95
|
-
[leak]: { type: 'permeability', P: P(leak, leakZ,
|
|
98
|
+
[Na]: { type: 'permeability', P: P(Na, 1, 'Na', 'Na channels'), gates: { m: 3, h: 1 } },
|
|
99
|
+
[K]: { type: 'permeability', P: P(K, 1, 'K', 'K channels'), gates: { n: 4 } },
|
|
100
|
+
[leak]: { type: 'permeability', P: P(leak, leakZ, 'leak', 'leak') },
|
|
96
101
|
},
|
|
97
102
|
};
|
|
98
103
|
}
|
package/src/checks.js
CHANGED
|
@@ -86,7 +86,7 @@ export function check(device, sol, { refine = true, tol = 1e-2, order = true } =
|
|
|
86
86
|
if (steady && refine && order && model.species.length > 1) items.push(orderCheck(device, sol, ledger.details.gross));
|
|
87
87
|
// A transient's answer depends on its whole history (and on drives set along the way), so
|
|
88
88
|
// only a run of its own on a finer grid can check it.
|
|
89
|
-
|
|
89
|
+
if (!steady && refine) items.push({ name: 'grid', ok: null, summary: 'not checked in a transient: run it again on a grid twice as fine (hmin and hmax halved) and compare' });
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
const ok = sol.converged && items.every((it) => it.ok !== false);
|
|
@@ -96,7 +96,7 @@ export function check(device, sol, { refine = true, tol = 1e-2, order = true } =
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// The units of totals through the cross-section: per m² of a planar device, or whole.
|
|
99
|
-
const unitsOf = (model) => (model.geometry.type === 'planar' ? { rate: 'mol/(m²·s)', current: 'A/m²', charge: 'C/m²' } : { rate: 'mol/s', current: 'A', charge: 'C' });
|
|
99
|
+
const unitsOf = (model) => (model.geometry.type === 'planar' ? { rate: 'mol/(m²·s)', current: 'A/m²', charge: 'C/m²', amount: 'mol/m²' } : { rate: 'mol/s', current: 'A', charge: 'C', amount: 'mol' });
|
|
100
100
|
|
|
101
101
|
// Each species' steady ledger: in through the terminals, made by the reactions. Rates in
|
|
102
102
|
// mol/(m²·s) (mol/s if not planar), positive into the device or made.
|
|
@@ -262,6 +262,8 @@ function gridCheck(device, sol, tol, gross) {
|
|
|
262
262
|
const out = { ratio: Math.sqrt(g.ratio ?? 1.2), minCells: 2 * (g.minCells ?? 8) };
|
|
263
263
|
if (g.hmin !== undefined) out.hmin = g.hmin / 2;
|
|
264
264
|
if (g.hmax !== undefined) out.hmax = g.hmax / 2;
|
|
265
|
+
if (g.hLeft !== undefined) out.hLeft = g.hLeft / 2;
|
|
266
|
+
if (g.hRight !== undefined) out.hRight = g.hRight / 2;
|
|
265
267
|
return { ...reg, grid: out };
|
|
266
268
|
}),
|
|
267
269
|
};
|
|
@@ -334,7 +336,7 @@ function gridCheck(device, sol, tol, gross) {
|
|
|
334
336
|
summary:
|
|
335
337
|
`on a grid twice as fine (${sol.x.length} → ${other.x.length} nodes), the largest change is ${worst.what}, ${sig(worst.a, 5)} → ${sig(worst.b, 5)} ${worst.unit} ` +
|
|
336
338
|
`(${sig(worst.rel, 2)}${ok ? '' : `, over ${sig(tol, 1)}: refine the grid (hmin, hmax), most likely where profiles are steepest`})`,
|
|
337
|
-
details: { diffs, nodes: [sol.x.length, other.x.length] },
|
|
339
|
+
details: { diffs, nodes: [sol.x.length, other.x.length], grids: fine.regions.map((reg) => reg.grid) }, // (grids: the finer one's, by region)
|
|
338
340
|
};
|
|
339
341
|
}
|
|
340
342
|
|
|
@@ -374,7 +376,7 @@ function orderCheck(device, sol, gross) {
|
|
|
374
376
|
const vol = model.grid.vol, amount = (s, name) => s.x.reduce((m, _, g) => m + (Number.isFinite(s.c[name][g]) ? vol[g] * s.c[name][g] : 0), 0);
|
|
375
377
|
const amounts = model.species.map((sp) => [sp.name, amount(a, sp.name), amount(b, sp.name)]);
|
|
376
378
|
const most = Math.max(0, ...amounts.map(([, x, y]) => Math.max(Math.abs(x), Math.abs(y))));
|
|
377
|
-
for (const [name, x, y] of amounts) diffs.push({ what: `${name} amount`, a: x, b: y, rel: Math.abs(y - x) / (1e-6 * Math.max(Math.abs(x), Math.abs(y)) + 1e-12 * most), unit:
|
|
379
|
+
for (const [name, x, y] of amounts) diffs.push({ what: `${name} amount`, a: x, b: y, rel: Math.abs(y - x) / (1e-6 * Math.max(Math.abs(x), Math.abs(y)) + 1e-12 * most), unit: unitsOf(model).amount });
|
|
378
380
|
// Agreeing to near round-off (a µV, a millionth of an amount): ok. Within a thousand times that
|
|
379
381
|
// (a mV, a thousandth), enough for any plot, but a weakly held part of one solve not quite
|
|
380
382
|
// steady: look. Beyond: one of them isn't a steady state.
|
package/src/data.js
CHANGED
|
@@ -66,7 +66,8 @@ export const METALS = freeze(
|
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Water with the named ions, as a library piece for `build()`: each ion as a species
|
|
69
|
-
* (c_ref = 1 M) and the material `water` holding them, with D and μ° from `IONS`.
|
|
69
|
+
* (c_ref = 1 M) and the material `water` holding them, with D and μ° from `IONS`. The data are
|
|
70
|
+
* 25 °C's, and the material says so (T: 298.15): a device at another temperature warns.
|
|
70
71
|
* @param {string[]} names ions from `IONS`
|
|
71
72
|
* @param {{ epsr?: number, material?: string }} [opts] permittivity (default that of water;
|
|
72
73
|
* 0 makes the solution strictly neutral, without double layers), and the material's name
|
|
@@ -82,7 +83,8 @@ export function aqueous(names, { epsr = WATER_EPSR, material = 'water' } = {}) {
|
|
|
82
83
|
species.push({ name, z: ion.z, cRef: 1000 });
|
|
83
84
|
entries[name] = { D: ion.D, mu0: ion.mu0 };
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
+
// (25 °C data: a device at another temperature warns)
|
|
87
|
+
return { species, materials: { [material]: { epsr, species: entries, T: 298.15 } } };
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
// How the semiconductors' data move with temperature, for semiconductor(name, { T }):
|
|
@@ -183,7 +185,7 @@ export function trap(lib, { density, tauN, tauP, n1, charge = 'acceptor', name =
|
|
|
183
185
|
if (typeof name !== 'string' || !/^[A-Za-z][\w()]*$/.test(name)) fail(`trap: name must be a plain name for the trap (its states are name0 and name- or name+), got ${JSON.stringify(name)}`);
|
|
184
186
|
const RT = GAS_CONSTANT * T, cRefE = el.cRef ?? lib.species.find((sp) => sp.name === 'e-')?.cRef;
|
|
185
187
|
const cRefH = ho.cRef ?? lib.species.find((sp) => sp.name === 'h+')?.cRef;
|
|
186
|
-
const ni = Math.sqrt(cRefE * cRefH * Math.exp(-(ho.mu0
|
|
188
|
+
const ni = Math.sqrt(cRefE * cRefH * Math.exp(-(ho.mu0 + el.mu0) / RT));
|
|
187
189
|
const half = n1 === undefined ? ni : pos(n1, 'n1 must be the electron density that half-fills the trap > 0 (mol/m³)');
|
|
188
190
|
// The filled state's μ° sits an electron's μ at n₁ above the empty one's: half full at n = n₁.
|
|
189
191
|
const [empty, full] = charge === 'acceptor' ? [`${name}0`, `${name}-`] : [`${name}+`, `${name}0`];
|
package/src/describe.js
CHANGED
|
@@ -265,7 +265,7 @@ export function describe(def) {
|
|
|
265
265
|
lines.push(` port ${port.name}: ${drive(p, model.geometry.type === 'planar')}, in ${regions[port.region].name}`);
|
|
266
266
|
// an electrode spread through the window
|
|
267
267
|
if (port.reactions.length) lines.push(` an electrode (its ${species[port.terminal].name} at the port's level), ${typeof p.area === 'number' ? `${p.area} m²` : 'a profile of m²'} of it per m³, reacting:`);
|
|
268
|
-
|
|
268
|
+
(p.reactions ?? []).forEach((rx, j) => lines.push(` ${rx.equation}: k0 ${rx.k0} mol/(m²·s), α ${port.reactions[j].alpha}${rx.bare ? ', on bare metal' : ''}`)); // (α as used: 0.5 if not given)
|
|
269
269
|
if (port.capacitance) lines.push(` a capacitance of ${num(port.capacitance.C)} F/m² through the window (zero charge at ${num(port.capacitance.zeroCharge)} V), ${typeof p.area === 'number' ? `${p.area} m²` : 'a profile of m²'} per m³`);
|
|
270
270
|
if (port.surface.length) lines.push(` its surface: ${port.surface.map((sp) => sp.name).join(', ')} on ${num(port.surface[0].capacity)} mol/m² of sites (Langmuir)`);
|
|
271
271
|
});
|
package/src/device.js
CHANGED
|
@@ -18,7 +18,7 @@ export { DeviceError };
|
|
|
18
18
|
const SPECIES_LINK_TYPES = new Set(['blocked', 'equilibrium', 'conductance', 'exchange', 'velocity']);
|
|
19
19
|
const INTERFACE_LINK_TYPES = new Set(['equilibrium', 'blocked', 'conductance', 'permeability']);
|
|
20
20
|
const PHI_LINK_TYPES = new Set(['bulk', 'neutral', 'capacitive', 'pinned']);
|
|
21
|
-
const GRID_FIELDS = ['hmin', 'hmax', 'ratio', 'minCells'];
|
|
21
|
+
const GRID_FIELDS = ['hmin', 'hmax', 'ratio', 'minCells', 'hLeft', 'hRight'];
|
|
22
22
|
// The fields of each kind of species link (to an outside phase, or across a face).
|
|
23
23
|
const LINK_FIELDS = {
|
|
24
24
|
blocked: ['type'],
|
|
@@ -308,14 +308,20 @@ export function normalizeDevice(def) {
|
|
|
308
308
|
};
|
|
309
309
|
});
|
|
310
310
|
|
|
311
|
-
// A material whose data are for another temperature than the device's
|
|
312
|
-
// and mobilities
|
|
311
|
+
// A material whose data are for another temperature than the device's is silently wrong there
|
|
312
|
+
// (a semiconductor's n_i, gap and mobilities; water's μ°, ε and D): say so (for each material a
|
|
313
|
+
// region uses).
|
|
313
314
|
materials.forEach((m, k) => {
|
|
314
315
|
if (m.dataT === undefined || Math.abs(m.dataT - T) <= 0.01 || !regions.some((reg) => reg.material === k)) return;
|
|
316
|
+
const own = Object.keys(def.materials[m.name].species ?? {}), semi = own.includes('e-') && own.includes('h+');
|
|
315
317
|
warnings.push(
|
|
316
318
|
`materials.${m.name}: its data are for ${m.dataT} K, but the device is at ${T} K${def.T === undefined ? ' (the default, as it gives no T)' : ''}, ` +
|
|
317
|
-
|
|
318
|
-
|
|
319
|
+
(semi
|
|
320
|
+
? `so they don't hold there (a semiconductor's n_i, gap and mobilities are ${m.dataT} K's): give the device T: ${m.dataT}, ` +
|
|
321
|
+
`or remake the material for ${T} K (semiconductor(name, { T: ${T} }))`
|
|
322
|
+
: `so they don't hold there: its μ° (so equilibrium constants and electrode potentials, Kw's by ~6× between 6 and 25 °C), ` +
|
|
323
|
+
`permittivity and diffusivities are ${m.dataT} K's. Give the device T: ${m.dataT}, or give the material data for ${T} K ` +
|
|
324
|
+
`and its T (aqueous() has none at other temperatures yet)`),
|
|
319
325
|
);
|
|
320
326
|
});
|
|
321
327
|
|
|
@@ -698,7 +704,7 @@ export function nextBreakpoint(src, t) {
|
|
|
698
704
|
if (src.value !== undefined) return Infinity;
|
|
699
705
|
const { t: ts, repeat } = src, n = ts.length;
|
|
700
706
|
if (!repeat) {
|
|
701
|
-
for (let k = 0; k < n; k++) if (ts[k] > t
|
|
707
|
+
for (let k = 0; k < n; k++) if (ts[k] > t + 1e-12 * Math.abs(t) + 1e-300) return ts[k]; // (t·(1 + ε) is below t for t < 0)
|
|
702
708
|
return Infinity;
|
|
703
709
|
}
|
|
704
710
|
const T = ts[n - 1] - ts[0], cycle = Math.floor((t - ts[0]) / T);
|
|
@@ -1249,7 +1255,7 @@ function normalizeContact(cdef, side, region, materials, species, speciesIndex,
|
|
|
1249
1255
|
}
|
|
1250
1256
|
if (link.type === 'conductance') {
|
|
1251
1257
|
// Ohmic exchange with an outside reservoir at V_i = V + offset: J = G·(V_out − V_i).
|
|
1252
|
-
need(species[i].z !== 0, `${lpath}: a conductance link needs a charged species`);
|
|
1258
|
+
need(species[i].z !== 0, `${lpath}: a conductance link needs a charged species (a neutral one exchanges by { type: 'exchange', k, mu })`);
|
|
1253
1259
|
const offset = link.offset ?? (terminal === i ? 0 : undefined);
|
|
1254
1260
|
need(offset !== undefined, `${lpath}.offset: give V_i − V_terminal (V) of the outside reservoir`);
|
|
1255
1261
|
links[i] = { type: 'conductance', G: positive(link.G, `${lpath}.G`), offset: finite(offset, `${lpath}.offset`) };
|
|
@@ -1277,7 +1283,6 @@ function normalizeContact(cdef, side, region, materials, species, speciesIndex,
|
|
|
1277
1283
|
links[i] = { type: 'exchange', k: positive(link.k, `${lpath}.k (mol/(m²·s))`), mu: finite(link.mu, `${lpath}.mu (the outside μ, J/mol)`) };
|
|
1278
1284
|
continue;
|
|
1279
1285
|
}
|
|
1280
|
-
if (link.type === 'conductance') need(species[i].z !== 0, `${lpath}: a neutral species exchanges by { type: 'exchange', k, mu }`);
|
|
1281
1286
|
if (link.type === 'equilibrium') {
|
|
1282
1287
|
// In equilibrium with the outside phase, whose levels are known: a Dirichlet condition.
|
|
1283
1288
|
if (species[i].z === 0) {
|
package/src/devices.js
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
import { AVOGADRO, FARADAY, GAS_CONSTANT, units } from './constants.js';
|
|
6
6
|
import { photogeneration } from './sources.js';
|
|
7
7
|
import { drive } from './stack.js';
|
|
8
|
-
import { checkOptions } from './errors.js';
|
|
8
|
+
import { DeviceError, checkOptions, shown } from './errors.js';
|
|
9
|
+
|
|
10
|
+
const need = (ok, message) => {
|
|
11
|
+
if (!ok) throw new DeviceError(message);
|
|
12
|
+
};
|
|
9
13
|
|
|
10
14
|
const m3 = (x) => x / AVOGADRO, eV = units.eV;
|
|
11
15
|
|
|
@@ -35,6 +39,8 @@ const P = {
|
|
|
35
39
|
*/
|
|
36
40
|
export function perovskiteCell({ V = 1.2, light = 1, bulkSRH = true } = {}) {
|
|
37
41
|
checkOptions('perovskiteCell', arguments[0], ['V', 'light', 'bulkSRH']);
|
|
42
|
+
need(Number.isFinite(light) && light >= 0, `perovskiteCell: light must be the illumination in suns, ≥ 0, got ${shown(light)}`);
|
|
43
|
+
need(typeof bulkSRH === 'boolean', `perovskiteCell: bulkSRH must be true or false, got ${shown(bulkSRH)}`);
|
|
38
44
|
const { T } = P, VT = (GAS_CONSTANT * T) / FARADAY;
|
|
39
45
|
const ni = Math.sqrt(P.gc * P.gv) * Math.exp(-(P.Ec - P.Ev) / (2 * VT));
|
|
40
46
|
const n0 = P.gc * Math.exp((P.EcE + VT * Math.log(P.dE / P.gcE) - P.Ec) / VT); // the perovskite's n at the TiO₂'s Fermi level
|
|
@@ -124,6 +130,8 @@ export const PEROVSKITE_SCANS = Object.freeze({
|
|
|
124
130
|
* @param {number} turn s
|
|
125
131
|
*/
|
|
126
132
|
export function hysteresis(trace, turn) {
|
|
133
|
+
need(trace && trace.t?.length > 0 && trace.voltage?.length === trace.t.length && trace.current?.length === trace.t.length, 'hysteresis: give a trace { t, voltage, current } (an advance()\'s, say)');
|
|
134
|
+
need(Number.isFinite(turn) && turn > trace.t[0] && turn < trace.t[trace.t.length - 1], `hysteresis: turn must be the time the scan turns, inside the trace (${shown(trace.t[0])} to ${shown(trace.t[trace.t.length - 1])} s), got ${shown(turn)}`);
|
|
127
135
|
const out = {};
|
|
128
136
|
for (const [name, on] of [['rev', (t) => t <= turn], ['fwd', (t) => t >= turn]]) {
|
|
129
137
|
const pts = [];
|
package/src/difference.js
CHANGED
|
@@ -8,13 +8,19 @@
|
|
|
8
8
|
// storage and space charge depend on δη_i − z_i δφ̂ alone; the displacement on δφ̂_R − δφ̂_L. A
|
|
9
9
|
// uniform shift of a region (z_i s on each η_i, s on φ̂) then gives exactly zero, as it must.
|
|
10
10
|
//
|
|
11
|
+
// Non-ideal statistics couple the species: c_i depends on every ζ_j, so a node's storage of i
|
|
12
|
+
// has a term in each δη_j − z_j δφ̂ (K_ij), and a flux has side terms in those at either end (its
|
|
13
|
+
// c_L and the excess potentials in its drift). Each is a term in the same differences, so the
|
|
14
|
+
// shift still gives exactly zero.
|
|
15
|
+
//
|
|
11
16
|
// Indices are compact (the solver's rix), with the sink (index N) for an unknown that isn't one:
|
|
12
17
|
// vectors passed in are read there as zero.
|
|
13
18
|
export class DifferenceTerms {
|
|
14
|
-
constructor(N, nodes, segments) {
|
|
19
|
+
constructor(N, nodes, segments, sides = 0) {
|
|
15
20
|
this.N = N;
|
|
16
21
|
this.dead = new Uint8Array(N + 1); // rows replaced since (a contact's level, a held stretch)
|
|
17
|
-
this.
|
|
22
|
+
this.nR = new Int32Array(nodes); // node term: its balance row, η column, φ̂ column (= Gauss row)
|
|
23
|
+
this.nI = new Int32Array(nodes);
|
|
18
24
|
this.nP = new Int32Array(nodes);
|
|
19
25
|
this.nZ = new Float64Array(nodes);
|
|
20
26
|
this.nMass = new Float64Array(nodes); // storage per unit (δη − z δφ̂), times 1/dt (or iω)
|
|
@@ -24,6 +30,9 @@ export class DifferenceTerms {
|
|
|
24
30
|
this.sC = new Float64Array(3 * segments); // a, b, d
|
|
25
31
|
this.dI = new Int32Array(2 * segments); // displacement: φ̂_L, φ̂_R
|
|
26
32
|
this.dK = new Float64Array(segments);
|
|
33
|
+
this.xI = new Int32Array(4 * sides); // a flux's side term: its two rows, an η column, its φ̂ column
|
|
34
|
+
this.xZ = new Float64Array(sides);
|
|
35
|
+
this.xC = new Float64Array(sides);
|
|
27
36
|
this.combos = []; // [rk, ri, w]: row rk += w × row ri (strictly neutral nodes' charge rows)
|
|
28
37
|
this.t = new Float64Array(N + 1);
|
|
29
38
|
this.x = new Float64Array(N + 1);
|
|
@@ -33,13 +42,16 @@ export class DifferenceTerms {
|
|
|
33
42
|
// Afresh for an assembly; `rate` multiplies storage in captured rows (1/dt).
|
|
34
43
|
reset(rate = 0) {
|
|
35
44
|
this.rate = rate;
|
|
36
|
-
this.nn = this.ns = this.nd = 0;
|
|
45
|
+
this.nn = this.ns = this.nd = this.nx = 0;
|
|
37
46
|
this.dead.fill(0);
|
|
38
47
|
this.combos.length = 0;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
// Storage in row r (mass) and charge in Gauss row p, per unit δη_i − z δφ̂ (row r = i, unless
|
|
51
|
+
// non-ideal statistics couple the species).
|
|
52
|
+
node(i, p, z, mass, charge, r = i) {
|
|
42
53
|
const k = this.nn++;
|
|
54
|
+
this.nR[k] = r;
|
|
43
55
|
this.nI[k] = i;
|
|
44
56
|
this.nP[k] = p;
|
|
45
57
|
this.nZ[k] = z;
|
|
@@ -59,6 +71,17 @@ export class DifferenceTerms {
|
|
|
59
71
|
this.sC[3 * k + 2] = d;
|
|
60
72
|
}
|
|
61
73
|
|
|
74
|
+
// A flux from row rL to row rR, per unit δη_i − z δφ̂ at one end.
|
|
75
|
+
side(rL, rR, i, p, z, w) {
|
|
76
|
+
const k = this.nx++;
|
|
77
|
+
this.xI[4 * k] = rL;
|
|
78
|
+
this.xI[4 * k + 1] = rR;
|
|
79
|
+
this.xI[4 * k + 2] = i;
|
|
80
|
+
this.xI[4 * k + 3] = p;
|
|
81
|
+
this.xZ[k] = z;
|
|
82
|
+
this.xC[k] = w;
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
displacement(pL, pR, k) {
|
|
63
86
|
const q = this.nd++;
|
|
64
87
|
this.dI[2 * q] = pL;
|
|
@@ -83,7 +106,7 @@ export class DifferenceTerms {
|
|
|
83
106
|
for (let k = 0; k < this.nn; k++) {
|
|
84
107
|
const i = this.nI[k], p = this.nP[k], z = this.nZ[k];
|
|
85
108
|
let f = 0;
|
|
86
|
-
if (row ===
|
|
109
|
+
if (row === this.nR[k]) f += mass * this.nMass[k];
|
|
87
110
|
if (row === p) f += this.nCharge[k];
|
|
88
111
|
if (f === 0) continue;
|
|
89
112
|
add(i, f);
|
|
@@ -99,6 +122,12 @@ export class DifferenceTerms {
|
|
|
99
122
|
add(this.sI[4 * k + 2], s * (-z * a - d));
|
|
100
123
|
add(this.sI[4 * k + 3], s * d);
|
|
101
124
|
}
|
|
125
|
+
for (let k = 0; k < this.nx; k++) {
|
|
126
|
+
const s = row === this.xI[4 * k] ? 1 : row === this.xI[4 * k + 1] ? -1 : 0;
|
|
127
|
+
if (s === 0) continue;
|
|
128
|
+
add(this.xI[4 * k + 2], s * this.xC[k]);
|
|
129
|
+
add(this.xI[4 * k + 3], -s * this.xZ[k] * this.xC[k]);
|
|
130
|
+
}
|
|
102
131
|
for (let q = 0; q < this.nd; q++) {
|
|
103
132
|
const pL = this.dI[2 * q], pR = this.dI[2 * q + 1], s = row === pL ? 1 : row === pR ? -1 : 0;
|
|
104
133
|
if (s === 0) continue;
|
|
@@ -114,10 +143,10 @@ export class DifferenceTerms {
|
|
|
114
143
|
for (let j = 0; j < N; j++) x[j] = v[j];
|
|
115
144
|
x[N] = 0;
|
|
116
145
|
t.fill(0);
|
|
117
|
-
const { nI, nP, nZ, nMass, nCharge } = this;
|
|
146
|
+
const { nR, nI, nP, nZ, nMass, nCharge } = this;
|
|
118
147
|
for (let k = 0; k < this.nn; k++) {
|
|
119
148
|
const i = nI[k], p = nP[k], w = x[i] - nZ[k] * x[p];
|
|
120
|
-
t[
|
|
149
|
+
t[nR[k]] += mass * nMass[k] * w;
|
|
121
150
|
t[p] += steady * nCharge[k] * w;
|
|
122
151
|
}
|
|
123
152
|
if (steady !== 0) {
|
|
@@ -128,6 +157,12 @@ export class DifferenceTerms {
|
|
|
128
157
|
t[iL] += dN;
|
|
129
158
|
t[iR] -= dN;
|
|
130
159
|
}
|
|
160
|
+
const { xI, xZ, xC } = this;
|
|
161
|
+
for (let k = 0; k < this.nx; k++) {
|
|
162
|
+
const dN = steady * xC[k] * (x[xI[4 * k + 2]] - xZ[k] * x[xI[4 * k + 3]]);
|
|
163
|
+
t[xI[4 * k]] += dN;
|
|
164
|
+
t[xI[4 * k + 1]] -= dN;
|
|
165
|
+
}
|
|
131
166
|
for (let q = 0; q < this.nd; q++) {
|
|
132
167
|
const pL = dI[2 * q], pR = dI[2 * q + 1], dD = -steady * dK[q] * (x[pR] - x[pL]);
|
|
133
168
|
t[pL] += dD;
|