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.
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.2.4/src/index.js` (and `src/kit.js`, `src/plot.js`).
59
+ `https://cdn.jsdelivr.net/npm/driftlet@1.3.2/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
 
@@ -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 = C - C z z^\mathsf{T} C / (z^\mathsf{T} C z)`$ and $`C = \mathrm{diag}(c)`$.
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
- - For a neutral dilute species it's exactly $`-D_{\mathrm{mix}} \nabla c`$. For a binary salt it
103
- adds $`D_{\mathrm{mix}}`$ to the salt's ambipolar diffusivity and leaves diffusion potentials
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
@@ -112,9 +112,11 @@ regions: [
112
112
  material. In a strictly neutral region, $`\sum z \cdot c_0`$ must be zero.
113
113
  Where two strictly neutral regions meet at a face that holds ions in equilibrium and their
114
114
  `c0` leaves it out of equilibrium (a different fixed charge or immobile ion on each side), the
115
- two nodes at the face start in it, as the face reaches it at $`t = 0^+`$: the ions it passes
116
- shared between them so their $`\bar\mu`$ is equal across it and each side neutral, each
117
- one's amount over the two nodes kept, everything else as `c0` has it.
115
+ two nodes at the face start a transient in it, as the face reaches it at $`t = 0^+`$: the ions
116
+ it passes shared between them so their $`\bar\mu`$ is equal across it and each side neutral,
117
+ each one's amount over the two nodes kept, everything else as `c0` has it. (The first time
118
+ step from a cold start does this; a steady solve starts from `c0` as given, which it reaches
119
+ better from.)
118
120
  An entry can instead be a profile, `{ x: [...], values: [...] }`, against the device's x (m, not
119
121
  the region's own), piecewise linear between its points and constant beyond its ends, so a
120
122
  transient can start from a state it didn't simulate reaching: a packet already injected, a
@@ -541,6 +543,12 @@ levels are that material's, and the end node takes them through a Donnan step.
541
543
  means. For example, a Schottky barrier $`\phi_B`$ on n-type material with
542
544
  $`\mu^\circ_{\mathrm{e}^-} = 0`$ is `zeroCharge: φ_B`.
543
545
 
546
+ An ideally blocking electrode (inert: no reactions, nothing crossing) is a contact with no
547
+ terminal species and no links, `{ V, phi: 'pinned', zeroCharge }`: the solution's diffuse layer
548
+ is its whole double layer (the validation suite's Macdonald impedance is this cell), and a Stern layer in
549
+ series is `phi: { type: 'capacitive', C }` in place of `'pinned'`. (A metal layer behind a
550
+ capacitive face of large C does the same at more cost, and its C is warned of as a unit slip.)
551
+
544
552
  A Schottky contact is that pinned barrier with its carriers passing at their emission velocities:
545
553
  `{ V, terminal: 'e-', species: { 'e-': { type: 'velocity', v }, 'h+': { type: 'velocity', v: vp, offset: 0 } }, phi: 'pinned', zeroCharge: φ_B }`,
546
554
  with Bethe's thermionic-emission velocity $`v = A^* T^2/(F N_c)`$ ($`N_c`$ in mol/m³, the data
@@ -589,6 +597,11 @@ ports: [{
589
597
  }]
590
598
  ```
591
599
 
600
+ The window holds exactly `to − from` of exchange, electrode and capacitance however the grid
601
+ falls: each node whose box reaches into it takes the part of its box inside. A held level
602
+ (`'equilibrium'`) holds the nodes inside the window, so it needs one there. A window of no width
603
+ is the node at that point, its whole box.
604
+
592
605
  On a [conductor region](#materials), a port is a wire to the whole conductor: it takes no window,
593
606
  only the conductor's carrier, and a conductance link's `G` is per area (S/m², a resistance
594
607
  $`R \cdot A`$ to the port's voltage as $`G = 1/(R \cdot A)`$). That's how a floating electrode is tied
@@ -748,8 +761,12 @@ as for reactions, and the window holds the opposite. Each node's charge balance
748
761
  Gauss's law averaged across the section, $`-\partial_x(\varepsilon\,\partial_x\phi) = \rho + a\sigma`$,
749
762
  and at $`\varepsilon = 0`$ neutrality $`\rho + a\sigma = 0`$, the mobile species rearranging to supply
750
763
  it (counter-ions in, co-ions out). The port passes the charging current, $`d(\int a\sigma\,dV)/dt`$,
751
- none in a steady state. Capacitances on overlapping windows add (a channel's top and bottom
752
- gates). A capacitance alone, driven by a current, only charges: it starts uncharged, `advance()`
764
+ none in a steady state. `zeroCharge` is in the frame a contact's is (see the capacitive contact
765
+ above): $`V - \phi`$ at zero charge, with $`\phi`$ the device's own, which a bath or contact sets
766
+ (beside a bath referenced to Cl⁻, with the library's μ°, it sits near −1.4 V). Omitted, it's 0, so
767
+ a port held at V = 0 there starts charged by 1.4 V; to start it uncharged, give `zeroCharge` = V −
768
+ φ at its window (`sol.phi` after a solve). Capacitances on overlapping windows add (a channel's
769
+ top and bottom gates). A capacitance alone, driven by a current, only charges: it starts uncharged, `advance()`
753
770
  follows it, and `solve()` refuses (there's no steady state, as for a gate contact driven by one).
754
771
 
755
772
  ```js nocheck
@@ -999,21 +1016,33 @@ const now = dev.solution(); // snapshot of the current state
999
1016
  as a transient slows, so give `dtMax` to resolve a signal in time.
1000
1017
  - `impedance(frequencies, { terminal, profiles })` solves the steady state, then linearises about
1001
1018
  it: $`Z(f) = \delta V/\delta I`$ in Ω·m² at one terminal (`'right'` by default), with I into the
1002
- device. A held terminal's voltage is perturbed, or a driven one's current; the other terminals
1019
+ device, its input impedance whether it's held or driven by a current (for a driven one, about
1020
+ the state its drive holds: solve() it held, then set its I, solve(), then impedance()); the other terminals
1003
1021
  keep their drives (held ones at AC ground, driven ones open), so at least one other must be held
1004
1022
  for the current to return (with every other one driven, the impedance is infinite, and an error
1005
1023
  says to measure at a driven one). A terminal behind a resistance can't be the one measured: the
1006
1024
  resistance belongs to the external circuit. The grid must resolve how far the signal reaches,
1007
1025
  and nothing warns of it: in a porous electrode, $`\sqrt{\kappa/(\omega C a)}`$ (cells of 2 µm
1008
1026
  against a 3.4 µm depth put Z 7% off, falling as the cells do; at an electrode, the diffusion
1009
- length $`\sqrt{2D/\omega}`$ at the highest frequency). The result is `{ f, Z: { re, im },
1010
- warnings }`, the warnings being the steady state's. With `profiles: true`, `profiles[k]` holds
1027
+ length $`\sqrt{2D/\omega}`$ at the highest frequency, and coarser than that, a port's window
1028
+ edges falling mid-cell moved a faradaic C 17%); and a double layer's capacitance wants cells
1029
+ well inside its Debye length (0.1 M NaCl: hmin λ_D/2.4 read C_dl 2.1% low, 0.1 nm 0.5%, 0.05 nm
1030
+ to 5e-4; the warning about unresolved double layers starts only at λ_D). `relError` is the
1031
+ round-off's, not the grid's. The result is `{ f, Z: { re, im },
1032
+ relError, warnings }`: `relError[k]`, each Z's estimated error relative to $`|Z|`$ (from
1033
+ round-off in the current and the solves' residuals, and cautious: it can exceed the true error
1034
+ many times), and the warnings, the steady state's and one where a Z isn't resolved (an error
1035
+ over 1%: far below a capacitive device's relaxation, almost no current flows, and what does is
1036
+ lost to round-off) or Re Z is under a tenth of its error, relError·|Z|. With `profiles: true`, `profiles[k]` holds
1011
1037
  frequency k's complex profiles, `{ phi, mu: {name}, c: {name} }`, each `{ re, im }` at the
1012
1038
  nodes (`NaN` where a quantity doesn't exist), per unit excitation: a volt, or an A/m² where the
1013
1039
  terminal is driven by a current.
1014
1040
  - `set(patch)` merges plain objects deeply (arrays are replaced), except that a contact given `V`
1015
1041
  or `I` drops the other (and given `I`, any series `R`): `{ contacts: { right: { I: 0 } } }`
1016
- switches it to open circuit, whether it was held or behind a resistance. A patch that changes only the
1042
+ switches it to open circuit, whether it was held or behind a resistance. Given `V` alone, a terminal
1043
+ behind a resistance stays behind it: `{ V, R: undefined }` takes the resistance off (`R: 0` and
1044
+ `null` are refused, and `undefined` doesn't survive JSON, so a worker's patch says it this way in
1045
+ JavaScript). A patch that changes only the
1017
1046
  terminals' drives (`V`, `I`, `R`) updates them in place, cheaply, keeping everything else.
1018
1047
  Otherwise the device is rebuilt: the current state carries over while the species, regions
1019
1048
  and terminals are unchanged (on a new grid, interpolated onto its nodes, so a floating gate
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` | not yet (below) |
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. Concentrations `c[g·n + i]` and coverages
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`), `_directSteady`.
73
+ laws, from `src/laws.js`).
65
74
  - **Applying the laws** is `src/lawRows.js` (`captureLaws`, `captureLocal`, `snapFlat`,
66
- `applyLaws`, `solveBordered`), called from `assemble`, `newton`, `_refine` and the steady
67
- solves; `_replaceRow` and `_j`, which it uses, stay in the solver.
68
- - **Starting state:** `initFromComposition`, `_surfaceStart`.
69
- - **Statistics and assembly:** `computeConcentrations`, `_nodeStatistics`, `assemble` and the
70
- kernels it calls (nodes, segments, faces, bulk reactions, ports, contacts, charge rows),
71
- `_assembleDifference` (with `src/difference.js`).
72
- - **Newton:** `newton`, `_refine`, `_equilibrate`, `_kirchhoff`, `_noteConditioning`.
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:** `impedance` and the helpers after it.
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:** `solveSteady`, `_steadyFromHere`, the continuations (floating, generation,
79
- a terminal's level), `_steadySteps`.
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
- The rest of `src/solver.js` is to be split along these lines, each piece with its contract.
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 the
177
- logarithmic mean of each concentration across the segment. The logarithmic mean makes a neutral
178
- species' mixing flux exactly $`-D_{\mathrm{mix}} \Delta c/h`$. The flux is exactly zero at
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
- A MOS capacitor's gate step now reaches the low-frequency charge in ~70 steps (it took 80,000,
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 4e-3, 7e-6, 3e-11).
545
- Stopping one iteration early on a convergence-rate estimate would save a third of the work,
546
- but leave ~1e-11 in $`\eta`$ per step, which shows up as conservation drift, so it isn't done.
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
@@ -625,10 +661,16 @@ About a steady state, a small sinusoidal excitation at one terminal gives, to fi
625
661
  $`v \cdot K`$, displacement), and likewise $`C`$ and $`C'`$ for the terminal currents. The
626
662
  time-derivative parts are read off two assemblies, $`J(dt) = J + M/dt`$, with a tiny $`dt`$ so that
627
663
  the subtraction loses nothing.
628
- - **At the measured terminal $`T`$,** a held voltage is perturbed ($`\delta V_T = 1`$, and $`\delta I_T`$
629
- read from its $`C`$ row), or a driven current ($`\delta I_T = 1`$, and $`\delta V_T`$ solved for).
630
- $`Z = \delta V_T/\delta I_T`$, with $`I`$ into the device. The other terminals keep their drives:
631
- held ones at AC ground, driven ones open.
664
+ - **At the measured terminal $`T`$,** its voltage is perturbed ($`\delta V_T = 1`$) and
665
+ $`\delta I_T`$ read, held or driven by a current alike: $`Z = \delta V_T/\delta I_T`$ is its input
666
+ impedance either way, with $`I`$ into the device. (Solving a driven one's $`\delta V_T`$ for a unit
667
+ current instead read its current through its own circuit row, at the contact alone: a reverse
668
+ diode's 4.3% off $`dV/dI`$.) The other terminals keep their drives: held ones at AC ground,
669
+ driven ones open. A floating one's circuit row reads its current where that reading's error is
670
+ least: at its contact, or at any cut between it and a port's window, the same current (through a
671
+ metal it's the round-off of $`G\,\Delta\eta`$); and where every terminal but $`T`$ floats, the
672
+ response to $`T`$ with them held (nearly every level shifted alike, its current a difference of
673
+ large terms) is read through the identity that shifting every terminal alike passes nothing.
632
674
 
633
675
  Each frequency costs one complex block-Thomas factorisation (rows scaled by their largest
634
676
  entry), plus one back-substitution per floating terminal for the bordering. Conserved (blocked)
@@ -776,7 +818,11 @@ the terms that cancel:
776
818
  next: there, backward-Euler steps at a huge $`dt`$ (10⁶ × the slowest diffusion time) keep
777
819
  the storage term, growing ×10 (capped) while the state still moves, each spectator's level
778
820
  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.
821
+ storage term), and one last step at the base huge $`dt`$, where pinning is tight. The amounts
822
+ that only those steps keep are checked after each, and a step that moved one fails: where the
823
+ slowest species is very slow (D ~ 1e-22 m²/s), the storage term falls below round-off against a
824
+ fast species' conduction, and a closed Y + Z, Y still in part of its stretch, had converged 1e8
825
+ times too large. The slowest diffusion time counts only materials some region is made of.
780
826
  - **A flat level.** A species that no reaction or port touches, reached by one contact at most,
781
827
  carries no flux in a steady state: every sum of its balance rows from its closed end is the
782
828
  flux through one segment, zero. In an ideal region without flow or mixing that's a level flat
@@ -916,7 +962,11 @@ A solve that fails falls back stage by stage:
916
962
  the direct solve. After a failed step it grows ×2, then ×1.5 more each success, back to ×10.
917
963
  Once steps reach the slowest diffusion time, the direct solve is tried from there, once a
918
964
  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.
965
+ steps from 1e8 to 1e9 s kept failing on the way to the end. The ramp reaches about 1e12 times
966
+ the slowest diffusion time, so a reaction slower than that, which alone settles a closed
967
+ combination (no row of the steady system holds a reaction at equilibrium node by node), isn't
968
+ waited out: the solve fails, or converges where it began, and its warnings name the reaction
969
+ either way. A transient gets there (Y = Z at $`k_f`$ = 1e-12 s⁻¹ in 1610 steps).
920
970
 
921
971
  Each of these carries cases that nothing else does (switched off one at a time, each fails
922
972
  tests or stress cases the others pass).
@@ -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 |
@@ -97,7 +97,7 @@ recipes.
97
97
 
98
98
  The validation suite shows driftlet is right where it converges. Whether it converges, on
99
99
  devices nobody tuned it for, is what `npm run stress` tests. It builds random but plausible
100
- devices in five families:
100
+ devices in six families:
101
101
 
102
102
  | Family | What's drawn |
103
103
  |---|---|
@@ -105,6 +105,7 @@ devices in five families:
105
105
  | MOS capacitors | Si, Ge or GaAs, doped 1e15–1e18 cm⁻³; oxide 1–20 nm; Au, Al or Pt gate at any flat-band offset; with or without generation |
106
106
  | Electrolyte cells | one to three salts from the data library's ions, 0.1 mM–1 M, 1 µm–1 mm; double layers resolved or strictly neutral; bath, reversible or blocking electrodes |
107
107
  | Electrodes | Ag, Cu or Zn deposition, or Fe³⁺/Fe²⁺ on platinum, with Butler–Volmer kinetics over four decades of k₀; with or without a supporting salt; against a second electrode or a bath |
108
+ | Reacting solutions | complexations (Na⁺ + A⁻ = NaA, B + A⁻ = BA⁻, …) in 2–3 regions whose faces block some species, so conserved combinations overlap; against a bath, another bath, closed or a K⁺ electrode; sometimes an electrode at no current reducing a closed M⁺ |
108
109
  | Liquid junctions | two mixed solutions, 1 µM–1 M, sharp or graded, at open circuit |
109
110
 
110
111
  Each device goes through what a user would do with it: a cold solve at no bias, and one at a
@@ -114,26 +115,30 @@ after a voltage step; and the impedance about equilibrium, from far below its sl
114
115
  to 1 GHz. Every result is judged by `check()`, and by what must hold whatever the device:
115
116
  levels flat at equilibrium with no current, warm and cold solves agreeing, the current driven
116
117
  being the one passed, the impedance passive (to 1e-3 of |Z|) and, at low frequency, equal to
117
- the steady dI/dV, a MOS capacitance never above its oxide's, and its gate charge with the back
118
+ the steady dI/dV where it says it's resolved (`relError` under 1%), a MOS capacitance never above its oxide's, and its gate charge with the back
118
119
  contact moved what it is with the gate moved the other way. Each case is seeded from its family and index, so
119
120
  any failure reruns alone (`npm run stress -- semi 79`).
120
121
 
121
- At 500 devices per family (about three minutes on a desktop), 11,880 of 11,881 scenarios pass:
122
+ At 500 devices per family (about five minutes on a desktop), 16,379 of 16,381 scenarios pass:
122
123
 
123
- | Family | Cold at 0 V | Cold at bias | Warm sweep | Open circuit, current or back contact | Transient | Impedance |
124
- |---|---|---|---|---|---|---|
125
- | Semiconductor stacks | 500/500 | 500/500 | 500/500 | 105/105 | 500/500 | 395/395 |
126
- | MOS capacitors | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 |
127
- | Electrolyte cells | 500/500 | 500/500 | 500/500 | | 500/500 | 381/381 |
128
- | Electrodes | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 |
129
- | Liquid junctions | | | | 500/500 | 499/500 | |
124
+ | Family | Cold at 0 V | Cold at bias | Species reversed | Warm sweep | Open circuit, current or back contact | Transient | Impedance |
125
+ |---|---|---|---|---|---|---|---|
126
+ | Semiconductor stacks | 500/500 | 500/500 | 500/500 | 499/500 | 105/105 | 500/500 | 395/395 |
127
+ | MOS capacitors | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 |
128
+ | Electrolyte cells | 500/500 | 500/500 | 500/500 | 500/500 | | 500/500 | 381/381 |
129
+ | Electrodes | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 |
130
+ | Reacting solutions | 500/500 | 500/500 | 500/500 | 500/500 | | 500/500 | |
131
+ | Liquid junctions | | | | | 500/500 | 499/500 | |
130
132
 
131
133
  (Impedance only where the device has an equilibrium to linearise about; open circuit only for
132
134
  lit cells.) The MOS back contact came last: before steady solves held a group of reacting
133
135
  carriers flat where it reaches one contact alone, 43 of the 500 failed it, 5 of them silently
134
- (a gate charge up to 2× off). The one that fails is among the known limits below: a junction on a grid far too
135
- coarse for its double layers.
136
- `bench/stress.json` keeps the summary.
136
+ (a gate charge up to 2× off). Of the two that fail, one is among the known limits below, a junction on a grid far too
137
+ coarse for its double layers; the other is a diode whose current, a small difference of large fluxes, a warm sweep
138
+ reaches to 1.8e-6 of what a cold solve does, against the 1e-6 the check asks: Newton's tolerance, not the physics
139
+ (its own current varies 2.8e-6 along the device, its balances converged).
140
+ `bench/stress.json` keeps the summary, and CI runs `npm run stress -- --check` against it: any
141
+ scenario failing that passes there fails the build.
137
142
 
138
143
  Most of what driftlet's numerics do differently began as a failure here: the impedance's
139
144
  current read across the quietest cut of the device rather than at the contact, GMRES judged by
@@ -174,7 +179,11 @@ disagree (a port's voltage at 0 for −1.42 V, θ 0.05 for 0.015, a contact 24 m
174
179
  the fixes, device by device, so its results above stand. On 1.2.2 the whole sweep, 870 devices,
175
180
  gives the same: 605 agree, 117 transients fail, and 13 differ, each a capacitive contact beside a
176
181
  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.
182
+ in it (1.2.3), 607 agree and 115 fail, the same 13 differing. Those 13 were a missing derivative:
183
+ a contact's current, read from its end box, didn't take the membrane port's voltage that reaches
184
+ it (a cold review of `src/boundaries.js`), so Newton stopped short in a way the tolerance set. With
185
+ it in (1.2.7), the transient settles on the steady state to 1e-14 V, in a third of the steps, and
186
+ 648 agree, 87 transients fail, none differ.
178
187
 
179
188
  ## Consistent: the derivatives
180
189
 
@@ -73,6 +73,19 @@ of exponentially. With one species it's Langmuir filling of sites. The dilute re
73
73
  $`c_{\mathrm{ref}}`$ is kept. To use the half-filling convention of site models
74
74
  ($`e^\zeta = x/(1-x)`$), set `cRef` equal to `cMax`.
75
75
 
76
+ `cMax` from an ion's size: a cube of side $`a`$ per site gives $`c_{\mathrm{max}} = 1/(N_A a^3)`$, so
77
+ $`a`$ = 0.66 nm (a hydrated ion, about) is 5.8 M, and 0.5 nm is 13 M. The model counts sites only:
78
+ D falling with concentration, solvent counterflow and ion pairing are left out. On a material from
79
+ `aqueous()`, add it to the material it returns:
80
+
81
+ ```js
82
+ import { AVOGADRO } from 'driftlet';
83
+ import { aqueous } from 'driftlet/kit';
84
+
85
+ const aq = aqueous(['Cu2+', 'SO42-', 'H+']);
86
+ aq.materials.water.statistics = [{ type: 'lattice', species: ['Cu2+', 'SO42-', 'H+'], cMax: 1 / (AVOGADRO * 0.66e-9 ** 3) }];
87
+ ```
88
+
76
89
  ### Redlich–Kister
77
90
 
78
91
  One species on its own $`c_{\mathrm{max}}`$ sites, with the excess Gibbs energy of mixing per site
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.2.4/src/index.js';
34
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.2.4/src/kit.js';
35
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.2.4/src/plot.js';
33
+ import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.2/src/index.js';
34
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.2/src/kit.js';
35
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.2/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.2.4/src/index.js';
223
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.2.4/src/kit.js';
224
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.2.4/src/plot.js';
222
+ import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.2/src/index.js';
223
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.2/src/kit.js';
224
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.2/src/plot.js';
225
225
 
226
226
  const def = build({
227
227
  T: 300,
@@ -317,6 +317,8 @@ const mos = (Vg, zeroCharge) =>
317
317
  layer('Si', units.um(1), { name: 'Si', acceptors: NA }),
318
318
  ohmic(0),
319
319
  ],
320
+ // The inversion layer's supply: with it, the low-frequency C–V at every frequency, even
321
+ // impedance at 1 MHz. Leave it out for the high-frequency curve (see below the code).
320
322
  ports: [{ name: 'channel', region: 'Si', from: 0, to: units.nm(1), V: 0, terminal: 'e-', species: { 'e-': 'equilibrium' } }],
321
323
  grid: { hmin: units.nm(0.25), hmax: units.nm(50), ratio: 1.15 },
322
324
  });
@@ -383,8 +385,9 @@ electrons' V minus φ in the bulk (with the library's μ°), and a Cl⁻ termina
383
385
  that, so an electrode held at 0 V beside it is strongly anodic: `describe(def)` says where SHE
384
386
  sits, and an offset of that much (`bath(c, 'Cl-', 0, { offset })`) makes the terminal read SHE.
385
387
  The working electrode's current is positive when oxidising. The layer's resistance is in series
386
- (uncompensated IR: keep it thin, or the supporting salt strong); a 100:1 excess of salt still
387
- leaves the limiting current about 1% off pure diffusion.
388
+ (uncompensated IR: keep it thin, or the supporting salt strong); a 100:1 excess of a 1:1 salt
389
+ still leaves the limiting current about 1% off pure diffusion, and migration counts for more with
390
+ more charge: Cu²⁺ in 1 M H₂SO₄ (20:1) reads 3% above it, with no acid 2×.
388
391
 
389
392
  ```js
390
393
  import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driftlet",
3
- "version": "1.2.4",
3
+ "version": "1.3.2",
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": {