driftlet 0.10.0 → 0.12.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/docs/numerics.md CHANGED
@@ -210,9 +210,16 @@ flux node's own rows are the interface laws:
210
210
  | species, blocked or absent on one side | $`N_f = 0`$ |
211
211
  | species, conductance | $`N_f - G V_T (\eta_L - \eta_R)/(z^2 F) = 0`$ |
212
212
  | face reaction $`k`$ | $`r_k - \mathrm{rate}_k(\mathrm{state}_L, \mathrm{state}_R) = 0`$, and each participant's edge node takes $`\nu \cdot r_k`$ |
213
+ | gate $`q`$ | $`(x_q - x_q^{\mathrm{old}})/dt - \alpha(V)(1 - x_q) + \beta(V)\,x_q = 0`$, $`V = V_T(\hat\phi_R - \hat\phi_L)`$; a gated permeability's $`P`$ takes $`\prod x^p`$ |
213
214
 
214
215
  Every row couples only neighbours, with no penalty terms. A face reaction's rate is an unknown
215
- of the face block, so the two edge nodes it couples meet only through the block between them.
216
+ of the face block, so the two edge nodes it couples meet only through the block between them;
217
+ so is a gate's fraction open, its row coupling it to the two edge nodes' $`\hat\phi`$, and the gated
218
+ fluxes to it, all within the face's block and its neighbours'. Newton's convergence and the time
219
+ step's error count a gate's change as they count a potential's in thermal units (1e-3 of $`m`$ is
220
+ 0.3% of $`m^3`$), and a gate is evaluated in [0, 1] where Newton's iterates stray past its ends. A
221
+ membrane port's gates are unknowns of each window node's block, after its surface's coverages,
222
+ with the same row in the voltage across the port's capacitance there.
216
223
  A species can take part in a reaction at a face and also cross it by a link (its flux slot)
217
224
  at the same time; where it exists on both sides, that link must be given explicitly. A continuity
218
225
  row has a zero diagonal block, but block Thomas still sees a non-singular block there once the
@@ -420,6 +427,29 @@ instead of $`n \cdot M^3`$, which pays wherever species are confined to some reg
420
427
  against KCl, three unknowns of five at every node, it's a fifth of the work. A finite-difference
421
428
  test checks the compact Jacobian column by column on devices that cover every assembly path.
422
429
 
430
+ ### Exact J·v
431
+
432
+ The assembled matrix loses what a flux's two entries share. A Scharfetter–Gummel flux depends on
433
+ $`\eta`$ at its two ends through $`G`$ and $`-G(1 + E)`$, with $`E = \operatorname{expm1}(\Delta\eta)`$;
434
+ for a nearly uniform $`v`$ they cancel, and $`J \cdot v`$ keeps only round-off of $`G|v|`$, which in
435
+ an inversion layer's 0.1 nm cells ($`G`$ ~ 1e11) dwarfs the flux. Where a solve needs
436
+ $`J \cdot v`$ itself (GMRES, in the impedance and in Newton's refined solves), the dilute kernels'
437
+ terms are kept aside instead, each evaluated on differences first:
438
+
439
+ ```math
440
+ \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),
441
+ ```
442
+
443
+ with $`a = -GE`$ (through $`c_L`$), $`b = -G(1 + E)`$ and $`d`$ the drift's. A node's storage and space
444
+ charge depend on $`\delta\eta_i - z_i\,\delta\hat\phi`$ alone, the displacement on
445
+ $`\delta\hat\phi_R - \delta\hat\phi_L`$. Everything else (faces, contacts, ports, reactions,
446
+ concentrated and metal regions) is assembled into a matrix of its own. Rows that a contact or a
447
+ steady solve replaces drop the kept terms; the transformed rows of strictly neutral nodes combine
448
+ them as they combine the matrix's. A region's uniform shift ($`z_i s`$ on each $`\eta_i`$, $`s`$ on
449
+ $`\hat\phi`$) then gives exactly zero inside it, as it must. It replaced a central difference of
450
+ the residual, whose truncation (~1e-9 of $`|J||v|`$) swamped the answer for such a shift, and
451
+ which cost four assemblies per product.
452
+
423
453
  ## Newton
424
454
 
425
455
  - The update of the device's potentials ($`\hat\phi`$, $`\eta`$) is limited to 10 thermal units per
@@ -439,8 +469,37 @@ test checks the compact Jacobian column by column on devices that cover every as
439
469
  much reduced by the change of variables below), converges quadratically to ~1e-9 and then
440
470
  rattles there. Short steps come right after every waveform breakpoint, so without this a
441
471
  cyclic voltammogram in a neutral electrolyte stalled at its turns.
442
- - Clear divergence (device updates beyond 1e4, or ten times the first after six iterations)
443
- bails out early, so the caller can take a smaller step.
472
+ - **Refined solves, where the factorisation loses a mode.** The assembled $`J`$ holds a flux's
473
+ dependence on $`\eta`$ at its two ends as two entries; where they're huge, eliminating them loses
474
+ what they share to round-off. In a MOS capacitor's inversion layer the electrons' cells, 0.1 nm
475
+ wide, are coupled about $`h^2/(D\,dt)`$ ~ 1e-18 times more tightly than they store charge over a
476
+ second, so the layer's overall level, set by its storage and the trickle of minority electrons
477
+ from the bulk, is lost, and Newton's update there is noise. When Newton stalls short of
478
+ convergence (updates below 1e-4 that stopped shrinking quadratically), or its undamped updates
479
+ grow twice running (a bipolar stack's floating base, whose holes are held ~1e14 more weakly
480
+ than they move within it, kept Newton wandering a thermal unit at a time), the solve is refined:
481
+ GMRES on $`J\delta = r`$, the factorised system as the preconditioner and $`J \cdot v`$ exact, each
482
+ flux on its $`\eta`$ difference (see [exact J·v](#exact-jv)), as the impedance does. A lost mode
483
+ takes GMRES a couple of iterations. The refined update is kept where GMRES converged and it
484
+ moves no level by a thermal unit or more; otherwise refining stops for the rest of the solve.
485
+ A MOS capacitor's gate step now reaches the low-frequency charge in ~70 steps (it took 80,000,
486
+ and ended 1.5% off). Once refined, a solve stays refined: a GaAs junction without
487
+ recombination, whose minority carriers reach it only from the contacts, passes ~1.5e-7 A/m²,
488
+ which plain solves past the first refinement left to round-off (cold and warm solves 1e-3
489
+ apart), and refined ones give to 1e-14. (When $`J \cdot v`$ was a central difference of the
490
+ residual, its noise made refining past the first lost mode chase round-off, so it was kept
491
+ only where its correction dwarfed the plain update.)
492
+ - Clear divergence (device updates beyond 1e4 after the first iteration, or ten times the
493
+ first after six) bails out early, so the caller can take a smaller step. The first update is
494
+ exempt: light flooding a population that starts at ~1e3 per m³ predicts, linearised in
495
+ $`\eta`$, a change of 1e10 thermal units, which damping covers in a few iterations; bailing
496
+ out there had a lit phototransistor's pseudo-transient fail at every step above 1e-21 s.
497
+ - A pivot that cancels to exactly zero in the block factorisation is perturbed to 1e-15 of its
498
+ block's largest entry (static pivoting) rather than failing the solve. GaAs minority carriers,
499
+ ~1 per m³ beside a face, are held through conductances 1e-16 of the face flux's coefficient in
500
+ their row, and the elimination cancels them exactly: such devices now solve, even at
501
+ equilibrium. Newton's updates stay meaningful (the factorisation only amplifies the residual,
502
+ so a small update still means a small residual), and its refined solves correct the rest.
444
503
  - When a steady solve fails, the solution's warnings say how nearly singular the system was,
445
504
  and where. A running error bound through the factorisation compares each pivot with the
446
505
  magnitudes it was formed from, $`\log_{10}(\sum|\mathrm{terms}| / |\mathrm{pivot}|)`$: the digits
@@ -481,6 +540,13 @@ test checks the compact Jacobian column by column on devices that cover every as
481
540
  $`C_c = h^3(1+\omega)^2/(\omega(1+2\omega))`$ and $`C_p = h(h+h_1)(h+h_1+h_2)`$; linear after
482
541
  backward Euler, scaled by $`h/(2h + h_1)`$. The first step, with no history, is checked by step
483
542
  doubling.
543
+ - A first step whose error doesn't shrink with it is taken anyway. A jump in a level held at a
544
+ boundary (a bath's, after a voltage step) starts a profile self-similar in $`x/\sqrt{t}`$, so a
545
+ first step of any length errs alike (0.11 thermal units over ten decades of step, in a
546
+ 0.6 M electrolyte with resolved double layers), down to the grid's own diffusion time,
547
+ where Newton runs out of digits. Three tries in a row, each within a factor 2 of the last,
548
+ mark the error as the jump's; backward Euler damps it as it should, and the steps grow
549
+ from there. Transients that start with a jump take 5–30% fewer factorisations for it.
484
550
  - The state a first step starts from needn't satisfy the algebraic equations ($`\hat\phi`$ in a
485
551
  strictly neutral material, an interface's unknowns), after a start or a jump; they jump in
486
552
  the first instant. So the history starts from the first step's half step, not from there,
@@ -497,9 +563,9 @@ test checks the compact Jacobian column by column on devices that cover every as
497
563
  current it cost 50% more.
498
564
  - A step is rejected above `tol`. The next step size is
499
565
  $`h \cdot \min(2, \max(0.2, 0.9\, (\mathtt{tol}/\mathrm{err})^{1/(p+1)}))`$.
500
- - Newton failure quarters the step; on the first step after a start or a jump in a device
501
- with a strictly neutral material, longer steps are tried first, since there shorter ones are
502
- worse conditioned.
566
+ - Newton failure quarters the step. (Strictly neutral materials had tried longer first steps
567
+ instead, shorter ones being worse conditioned there; with their ends and starts fixed, no
568
+ case needs it.)
503
569
  - A target within round-off of the present time (1e-10 relative), such as an animation
504
570
  frame's that lands a hair past a breakpoint just reached, is snapped to rather than
505
571
  stepped to: a step of 1e-13 s can't be resolved.
@@ -533,15 +599,42 @@ species make $`J`$ singular, but $`J + i\omega M`$ isn't for $`\omega > 0`$: at
533
599
  device looks like a capacitor, as it should.
534
600
 
535
601
  The factorised system is used as a preconditioner, not trusted alone: each solve runs GMRES with
536
- $`J \cdot v`$ taken from the residual itself, by a central difference ($`\eta`$ perturbed in the
537
- state's low word, $`\hat\phi`$ and the face unknowns in the high one). The assembled $`J`$ holds a
538
- flux's dependence on $`\eta_L`$ and $`\eta_R`$ as two entries; in an inversion layer both are huge
539
- (~1e11), and for a nearly uniform $`\delta\eta`$ they cancel to round-off of order 1e-5, while the
540
- residual takes the $`\eta`$ difference first, in double-double. That round-off had given a MOS
541
- capacitor's inversion layer, fed by minority diffusion from a bulk with ~1e3 electrons per cm³ (a
542
- time constant of minutes), an exchange path that followed the gate at 1 Hz. Where the plain solve
543
- is already accurate, one application of the operator confirms it; the bench's impedance cases
544
- take about twice as long as before.
602
+ an exact $`J \cdot v`$ (see [exact J·v](#exact-jv)). The assembled $`J`$ holds a flux's dependence
603
+ on $`\eta_L`$ and $`\eta_R`$ as two entries; in an inversion layer both are huge (~1e11), and for a
604
+ nearly uniform $`\delta\eta`$ they cancel to round-off of order 1e-5. That round-off had given a
605
+ MOS capacitor's inversion layer, fed by minority diffusion from a bulk with ~1e3 electrons per cm³
606
+ (a time constant of minutes), an exchange path that followed the gate at 1 Hz. Where the plain
607
+ solve is already accurate, one application of the operator confirms it. GMRES judges convergence only by the true residual,
608
+ recomputed at each restart: its own running estimate can drift far from it where the
609
+ preconditioner is nearly singular (an inversion layer's exchange, lost in the assembled
610
+ $`J`$, at a frequency of µHz), and had declared solves converged that weren't. It keeps its
611
+ best iterate, never worse than the factorised solve.
612
+
613
+ The current is read where its error is smallest. At a contact it is a sum of terms that can
614
+ be far larger than itself: across a junction without recombination, between heavily doped
615
+ sides, what passes is minority carriers of ~1e8 per m³, while the contact's terms are its
616
+ majority conductances times a $`\delta\eta`$ cancelling its neighbours' to 1e-17, and their
617
+ round-off reads as a constant conductance of order 1 S/m², a silicon p⁺n⁺ junction's capacitance
618
+ 9× off at 100 Hz. Between the two contacts of a device without ports, the total current
619
+ (conduction plus displacement, $`i\omega\,\delta D`$) is the same through every cut, so it is also
620
+ read across each segment of an ideal region, linearised as the Scharfetter–Gummel flux is, and
621
+ the reading with the least estimated error is taken: in that junction, the depletion region's;
622
+ in a MOS capacitor, the oxide's displacement. A reading's error is its terms' round-off, and the
623
+ solves' error in $`\delta x`$ (their residual, relative to its largest entry) through its
624
+ coefficients: a contact's reading is a flux too (a held contact's balance row is replaced by its
625
+ level, so nothing solved pins it), and in a neutral bulk the current crosses a huge conductance
626
+ on a tiny gradient.
627
+
628
+ Where even the best reading's estimated error exceeds 1% of the current, the response is split
629
+ in two: a uniform shift of each region (every $`\eta_i`$ by $`z_i s_r`$ and $`\hat\phi`$ by $`s_r`$, with
630
+ $`s_r`$ fitted to the solve so far), which changes no flux and no concentration, so its $`J \cdot e`$ is
631
+ exactly zero inside each region and is taken at the faces and ends;
632
+ and the rest, small, solved for to GMRES's floor with its error now relative to itself, and read
633
+ separately. An electrolyte at its open circuit needs it: the response to its terminal is nearly
634
+ such a shift, and its current is the slope of levels uniform to 1e-14 of the response, more than
635
+ a double holds. A redox electrode's DC conductance had come out 2–40% low; now it matches the
636
+ steady dI/dV to six digits. (Not with ports, whose exchange sees the shift.) The real part of a nearly ideal capacitor, 1e-7 of $`|Z|`$, is still
637
+ only resolved to ~1e-4 of $`|Z|`$.
545
638
 
546
639
  ## Steady state
547
640
 
@@ -550,6 +643,28 @@ take about twice as long as before.
550
643
  because slow physics can take seconds, far beyond any $`L^2/D`$ estimate, and stepping would
551
644
  never finish. An example is exponentially scarce minority carriers slowly filling an inversion
552
645
  layer behind a Schottky contact.
646
+ - **A flat level,** where a stretch's steady state is known outright: a species that no reaction
647
+ or port touches, reached by one contact only (blocked at the other end), carries no flux in a
648
+ steady state, so its level is that contact's throughout. Steady solves (not the impedance,
649
+ where it carries a current) pin it there in place of its balances, rather than find it
650
+ through its own conduction, which can be all but nothing: a MOS capacitor's inversion
651
+ electrons reach the back contact only through a bulk with ~1e3 of them per cm³, and found that
652
+ way, the solve converged at some gate voltages and grids and not others. (Not through flow or
653
+ mixing, nor a concentrated material's cross-diffusion, where zero flux isn't a flat level.)
654
+ At a face inside such a stretch whose link holds the level continuous, the face's flux of the
655
+ species, which only the replaced edge balances set, is pinned to zero too.
656
+ - **Continuation for a terminal driven by a current.** A cold steady solve at open circuit can
657
+ fail where the same state is easy warm (a solar cell in dim light on a fine grid). The
658
+ terminal is held at a voltage instead, its steady solves having their own continuation, and
659
+ the voltage marched from the start's, the step growing, until the current crosses its target:
660
+ up where the current is below it, since raising a terminal's voltage raises the current into
661
+ a passive device (the other way if 20 V brings no crossing). Bisection narrows the crossing
662
+ to 0.1 mV, and the terminal is floated from beside it. Floated, the system can lose what held
663
+ it doesn't (a closed Fe³⁺/Fe²⁺ cell driven near its limit, Fe³⁺ 20 orders below Fe²⁺: 33 digits
664
+ short), but the current-driven steady state is the held one at the voltage that passes the
665
+ target, so where the floated solve fails, held solves find that voltage instead, by regula
666
+ falsi within the bracket, to 1e-10 of the current. The voltage continuation's own start,
667
+ with the terminals level, ramps the light up when there is some.
553
668
  - **Spectators** (a species blocked all round, mobile throughout its stretch) are solved
554
669
  directly too. In steady state the sum of a spectator's balance rows over its stretch is zero
555
670
  identically, so one of them (the first node's) is redundant, and it's replaced by the
@@ -557,7 +672,14 @@ take about twice as long as before.
557
672
  bordered: the matrix is factorised with a pin (an identity row) in its place, which makes the
558
673
  stretch's level a well-conditioned unknown, and the response to a unit pin (one extra
559
674
  back-substitution per spectator) is added in the amount that satisfies the constraint, from
560
- a $`k \times k`$ system for $`k`$ spectators.
675
+ a $`k \times k`$ system for $`k`$ spectators. Within one ideal region (no flow or mixing), a
676
+ spectator's level is flat at steady state, at a value its amount fixes, so its other balance
677
+ rows are replaced by $`\eta_g - \eta_{g-1} = 0`$, rather than found through its own conduction.
678
+ Past a closed zinc cell's limiting current, extended space charge at the cathode excludes the
679
+ blocked sulfate to ~1e-25 mol/m³, and the chain of conductances through it had lost the steady
680
+ system 14 digits: steady solves failed from 0.7 V, and took 37,000 iterations at 0.6 V. (Not
681
+ across a face: the rows that would hold the level there and pin its flux leave a diagonal
682
+ block singular.)
561
683
  - **Conserved combinations** (moieties) of reacting stretches are solved the same way: the
562
684
  total iron of Fe³⁺, Fe²⁺ and FeCl²⁺ in a closed cell with a complexation reaction, say. The
563
685
  basis of combinations $`w`$ comes from the null space of the stoichiometry. In steady state the
@@ -571,6 +693,36 @@ take about twice as long as before.
571
693
  larger by $`D \cdot dt/(hL)`$, around 1e12 for a micron-scale cell (see
572
694
  [metal regions](#metal-regions) for the same cancellation). Newton then needs dozens of
573
695
  iterations, or fails.
696
+ - **Islands**: the pieces of a stretch between its faces, where nothing else feeds a piece (no
697
+ contact, port or reaction). In steady state such a piece's level is held only through its
698
+ faces' fluxes, which can be far weaker than its own conduction: a face conductance of
699
+ 1e-15 S/m² against a region conducting ~4e2 S/m, or neighbours that conduct 1e20 times less
700
+ than it. Eliminated, the level was lost to round-off below anything Newton could see (the
701
+ plain solves gave no update along it, so nothing stalled), and solves converged to a wrong one
702
+ silently: currents 2× or 16× off. Summed over the piece's nodes, its balance rows are its
703
+ faces' flux unknowns alone, $`A_R u_R - A_L u_L`$: the internal fluxes cancel exactly, by
704
+ construction rather than in round-off. Across a face that holds $`\bar\mu`$ level, $`u`$ is
705
+ set only by the edge balances, and eliminated it can come out of the piece's own, as
706
+ $`G\,\Delta\eta`$ with its two $`\eta`$ all but equal; so there the outside edge node's
707
+ balance is added too, $`u`$ cancels, and the outside's first segment carries the flux. (Seen
708
+ through one face of two, a level behind neighbours 1e20 times less conductive was overshot
709
+ twofold, and Newton swung it from one contact's level to the other's.) That sum can replace
710
+ the first node's row, bordered like a spectator's amount. The response to the unit pin is the island's uniform shift, with whatever
711
+ $`\hat\phi`$ and the other species do along with it, so no mode needs guessing; refined solves
712
+ carry the pinned rows too.
713
+
714
+ Newton exact along such a level can be worse than Newton blind to it, far from the solution
715
+ (cold at bias, a bipolar stack's floating base took 1e5 thermal units in its first update), so
716
+ the summed rows go in only once the plain solves converge. An island at the wrong level then
717
+ shows: it takes in a different current than it passes on, so the terminal currents don't add
718
+ up to zero (to 1e-9 of the largest). Newton then carries on with the summed rows, refined from
719
+ their second update. A current that tiny against the conduction around it rides on $`\eta`$
720
+ differences (~1e-28 for 5 fA/m² through 1e4 mol/m³ at D = 1e-4 m²/s) that the last update
721
+ leaves known only to its own round-off, so where the currents still don't add up, one more
722
+ update polishes them. If Newton doesn't converge again within a dozen updates (a GaAs stack's
723
+ minority carriers, whose own rows rest on static pivots, at 1e-17 A/m² of noise), the plain
724
+ solution stands; and a steady solve that fails outright is tried once more with the summed rows
725
+ in from the start. Such devices now solve directly, their currents right to ~1e-15.
574
726
  - **Immobile combinations** (trap states X⁰ and X⁻ under e⁻ + X⁰ = X⁻, with D = 0) conserve
575
727
  node by node, since nothing carries them anywhere. At each node the combination's weighted sum
576
728
  replaces one of its balance rows, kept at what it was when the solve began. The row is local
@@ -588,16 +740,26 @@ take about twice as long as before.
588
740
  - **Generation continuation.** A device with generation reactions (species made only from, or
589
741
  turned only into, fixed reservoirs, such as photogeneration from a photon reservoir) can be
590
742
  held far from equilibrium even with its terminals level, where bias continuation can't help.
591
- If a direct solve fails there, the generation rates are scaled down to 10⁻¹² and ramped back
743
+ If a direct solve fails there, the generation rates are scaled down to 10⁻³⁰ and ramped back
592
744
  up, ×100 a step while each solve converges (warm from the last) and by the square root of the
593
- factor when one doesn't. An illuminated 80 µm silicon diode solves cold this way in about 80
594
- iterations.
745
+ factor when one doesn't, giving up when that factor falls below 1.5 (crawling, which time
746
+ steps do better); the pseudo-transient ramp then starts from the dimmer light's solution
747
+ rather than cold. (At 10⁻¹², a GaAs layer's generation still exceeded its equilibrium
748
+ recombination, ~3e-24 mol/m³ of minority electrons, a billionfold: the first step was the
749
+ whole jump.) An illuminated 80 µm silicon diode solves cold this way in about 80
750
+ iterations. (A lit n⁺pn stack with a floating base crawled for 20,000 iterations: at a
751
+ trillionth of the light, the base's level is held by couplings of ~1e-17.)
595
752
  - **If that fails too,** source continuation ramps the right terminal's voltage to its target.
596
753
  It ramps from the voltage of the last converged solve when the state is that solution, and
597
- otherwise from level terminals, where a cold start is consistent. The ramp step starts at
754
+ otherwise from level terminals, where a cold start is consistent. (A device first solved
755
+ after a `set()` was laid out for its new voltages; if the level start fails from there, it's
756
+ laid out again, level, and retried.) The ramp step starts at
598
757
  1/8 of the way, grows ×1.5 on success and shrinks ×4 on failure.
599
758
  - **If Newton still fails,** $`dt`$ ramps up from a small value (pseudo-transient continuation),
600
- ending in the direct solve where applicable.
759
+ ending in the direct solve where applicable. After a failed step it grows ×2, then ×1.5 more
760
+ each success, back to ×10. Once steps reach the slowest diffusion time, the direct solve is
761
+ tried from there, once a decade of $`dt`$ (the state kept if it fails): with immobile traps and
762
+ ions of D = 1e-17 m²/s, steps from 1e8 to 1e9 s kept failing on the way to the giant step.
601
763
 
602
764
  `solve()` does not advance the clock.
603
765
 
@@ -648,7 +810,11 @@ neutrality:
648
810
  - then storage (on the other balances) and neutrality go in, exactly, in those terms.
649
811
 
650
812
  A start-of-step net charge (round-off in a solved state) isn't carried over: the neutrality row
651
- holds the new state neutral. Steady solves and the impedance keep the plain rows (there's no
813
+ holds the new state neutral. An end node that its contact holds (a bath, an electrode's level)
814
+ keeps its plain rows, but the contact's current there is read before the ions' storage goes in,
815
+ and a bulk law doesn't count the box's net charge as displacement: under neutrality neither
816
+ carries charge, and read from the box's balance their round-off (∝ c/dt) cancelled only roughly.
817
+ A bath floating on 1 µM beside 3 M KCl had rattled at 2e-5 thermal units on steps of 1e-14 s. Steady solves and the impedance keep the plain rows (there's no
652
818
  storage at $`dt = \infty`$, and the impedance reads the storage matrix from them).
653
819
 
654
820
  A sub-grid Gouy–Chapman law, treating the diffuse layers analytically when $`\lambda_D \ll h`$, is on
@@ -0,0 +1,122 @@
1
+ # How far to trust it
2
+
3
+ A drift–diffusion solver is quick to start and slow to finish. The first version solves a pn
4
+ junction; the trouble comes after, with what real devices ask of it:
5
+
6
+ - concentrations spanning 30 orders of magnitude in one device (majority carriers beside
7
+ minority ones, a salt beside the ions its double layer excludes);
8
+ - double layers a millionth of the device's length, resolved or not;
9
+ - species whose amount is fixed rather than their level (a blocked ion, a floating base, a
10
+ closed cell's salt), which makes the steady equations singular;
11
+ - populations held only by couplings of 1e-17 (an inversion layer fed by ~1e3 minority
12
+ carriers per cm³), which a factorisation loses to round-off;
13
+ - terminals driven by a current, which may be beyond what the device can pass;
14
+ - small-signal currents read from sums whose terms are 1e17 times bigger.
15
+
16
+ Each of these makes a plain Newton solver fail, or worse, converge to the wrong answer. driftlet
17
+ has met them, mostly through the two kinds of testing below, and its numerics are built around
18
+ them ([numerics](numerics.md)). This page says what that testing covers and what it found, and
19
+ what is known not to work.
20
+
21
+ ## Correct: the validation suite
22
+
23
+ Every physics feature is checked against an analytic result, or an independent code where
24
+ there isn't one: `npm test`, over 300 tests, each a worked device with its closed form. They include
25
+ Shockley's diode equation, depletion and MOS charge, collection theory under Beer–Lambert light,
26
+ Gouy–Chapman and Kilic–Bazant–Ajdari double layers, Donnan and Teorell–Meyer–Sievers
27
+ partitions, Goldman–Hodgkin–Katz, the limiting current and Butler–Volmer kinetics, the
28
+ Macdonald and finite-length Warburg impedances, Cottrell and spherical diffusion, and the
29
+ second law (the free energy in through the terminals equals what's dissipated, every term
30
+ non-negative). Against other codes: IonMonger's perovskite J–V hysteresis (finite elements,
31
+ Octave), and liquid-junction potentials against JPCalc, LJPcalc and JLJP. The README's
32
+ [validation table](../README.md#validation) lists each with its tolerance.
33
+
34
+ Each solution can check itself too: `check()` from `driftlet/kit` balances every species'
35
+ ledger (what comes in through each terminal, is made or destroyed), the conservation
36
+ bookkeeping of a transient, and the grid (the device solved again on a grid twice as fine).
37
+
38
+ ## Robust: random devices
39
+
40
+ The validation suite shows driftlet is right where it converges. Whether it converges, on
41
+ devices nobody tuned it for, is what `npm run stress` tests. It builds random but plausible
42
+ devices in five families:
43
+
44
+ | Family | What's drawn |
45
+ |---|---|
46
+ | Semiconductor stacks | Si, Ge or GaAs; 1–3 layers n, p or intrinsic, doping 1e14–1e19 cm⁻³, 50 nm–10 µm each; no recombination, band-to-band or SRH (τ from 1 ns to 10 µs); sometimes lit; sometimes a Schottky contact |
47
+ | 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 |
48
+ | 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 |
49
+ | 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 |
50
+ | Liquid junctions | two mixed solutions, 1 µM–1 M, sharp or graded, at open circuit |
51
+
52
+ Each device goes through what a user would do with it: a cold solve at no bias, and one at a
53
+ bias; a warm sweep to that bias; open circuit, where it's lit; a current drive (a fraction of
54
+ what passes at 1 V); a transient after a voltage step; and the impedance about equilibrium,
55
+ from far below its slowest relaxation to 1 GHz. Every result is judged by `check()`, and by what must hold whatever the device:
56
+ levels flat at equilibrium with no current, warm and cold solves agreeing, the current driven
57
+ being the one passed, the impedance passive (to 1e-3 of |Z|) and, at low frequency, equal to
58
+ the steady dI/dV,
59
+ and a MOS capacitance never above its oxide's. Each case is seeded from its family and index, so
60
+ any failure reruns alone (`npm run stress -- semi 79`).
61
+
62
+ At 500 devices per family (about three minutes on a desktop), 11,380 of 11,381 scenarios pass:
63
+
64
+ | Family | Cold at 0 V | Cold at bias | Warm sweep | Open circuit or current | Transient | Impedance |
65
+ |---|---|---|---|---|---|---|
66
+ | Semiconductor stacks | 500/500 | 500/500 | 500/500 | 105/105 | 500/500 | 395/395 |
67
+ | MOS capacitors | 500/500 | 500/500 | 500/500 | | 500/500 | 500/500 |
68
+ | Electrolyte cells | 500/500 | 500/500 | 500/500 | | 500/500 | 381/381 |
69
+ | Electrodes | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 |
70
+ | Liquid junctions | | | | 500/500 | 499/500 | |
71
+
72
+ (Impedance only where the device has an equilibrium to linearise about; open circuit only for
73
+ lit cells.) The one that fails is among the known limits below: a junction on a grid far too
74
+ coarse for its double layers.
75
+ `bench/stress.json` keeps the summary.
76
+
77
+ Most of what driftlet's numerics do differently began as a failure here: the impedance's
78
+ current read across the quietest cut of the device rather than at the contact, GMRES judged by
79
+ its true residual, blocked species held flat in steady solves, continuations for a current drive
80
+ and for light, and Newton refining its solves where a factorisation loses a mode, with an exact
81
+ $`J \cdot v`$.
82
+
83
+ ## Hard cases
84
+
85
+ `npm run hard` keeps a corpus of named cases that were hard once (sharp 3 M | 1 µM junctions on
86
+ short steps, an inversion layer filling over minutes, a solar cell's open circuit on a 0.25 nm
87
+ grid, corroding drops), with their step and factorisation counts against a committed baseline,
88
+ so a change that makes one worse shows.
89
+
90
+ ## Known limits
91
+
92
+ - **A population held only through couplings far below double precision** is solved in steady
93
+ state by refining Newton's solves, as far as its own equations resolve it: a bipolar stack's
94
+ floating base at bias (a p⁺ base whose holes are held ~1e14 more weakly than they move within
95
+ it) is, and a closed Fe³⁺/Fe²⁺ cell driven by a current near its limit, where Fe³⁺ falls 20
96
+ orders below Fe²⁺, is found as the held voltage that passes it. Where a solve fails, its warning
97
+ says where it lost its digits (a transient gets there). A region held only through its faces (by
98
+ conductances 1e-24 of its own, or by neighbours conducting 1e20 times less) is solved directly
99
+ through its balance summed over it, where its own fluxes cancel exactly
100
+ ([islands](numerics.md#steady-state)): its current comes out right to ~1e-15, where it was
101
+ up to 16× off. A population held that weakly within a region, or fed by a reaction, isn't
102
+ covered, and its solve can still converge to the wrong level; the terminal currents then
103
+ don't add up, which is the thing to check.
104
+ - **The impedance is resolved to about 1e-4 of |Z|** where its solves are hardest: the real
105
+ part of a nearly ideal capacitor (a closed redox cell with almost none of one partner), 1e-7 of
106
+ |Z|, can come out slightly negative. Its DC limit matches the steady dI/dV in every stress case.
107
+ - **Strictly neutral regions on very short steps** lose digits at a neutral face between very
108
+ different solutions (3 M against 1 µM at steps of 1e-11 s).
109
+ - **An unresolved double layer** (a cell coarser than the Debye length) gives a charge that
110
+ depends on the grid, and in a sharp junction between very different solutions can stop a
111
+ transient; the solution warns. So does a steep profile on too coarse a cell.
112
+ - **A surface covered to a bare fraction below ~1e-10** barely affects anything any more, and
113
+ the solver struggles with it (the hard corpus's passivating drop without a double layer stops
114
+ partway through its hour); the solution warns, and suggests a less stable film.
115
+ - **Currents below ~0.1 nA/m²** (a GaAs junction's leakage without recombination) aren't resolved
116
+ between solves; such a current is zero for any practical purpose.
117
+ - **A failed solve says so:** `converged: false`, with a warning naming where the system lost
118
+ its digits, or which current no voltage reaches. In these tests, every steady state and
119
+ transient reported converged has passed `check()` and the invariants above.
120
+ - What driftlet doesn't model at all (more than one dimension, cross-diffusion, heat, optics,
121
+ field-dependent mobility) is in the [README](../README.md#what-it-doesnt-do), and open
122
+ numerical work in the [roadmap](../ROADMAP.md).
package/llms.txt CHANGED
@@ -14,9 +14,9 @@ docs/conventions.md (signs and levels) and docs/data.md (the data library).
14
14
  From a CDN, pin the version:
15
15
 
16
16
  ```js nocheck
17
- import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.10.0/src/index.js';
18
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.10.0/src/kit.js';
19
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.10.0/src/plot.js';
17
+ import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/index.js';
18
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/kit.js';
19
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/plot.js';
20
20
  ```
21
21
 
22
22
  From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
@@ -210,8 +210,50 @@ console.log(check(dev, sol).text); // per compartment: Na⁺ leaks into the cyto
210
210
  ```
211
211
 
212
212
  Between two baths instead (the inside one `bath(inside, 'Cl-', { I: 0 })`: held composition, no
213
- current), V_m is GHK. Without the pump, a closed cell runs down to Donnan equilibrium. driftlet
214
- has no voltage-gated channels; a permeability can be changed with `set()` between steps.
213
+ current), V_m is GHK. Without the pump, a closed cell runs down to Donnan equilibrium.
214
+
215
+ ## Template: an action potential
216
+
217
+ Voltage-gated channels: a face's `gates` (each a fraction open with Hodgkin–Huxley kinetics,
218
+ dx/dt = α(V)(1 − x) − β(V)x, in V = φ_right − φ_left), and a permeability scaled by them
219
+ (`gates: { m: 3, h: 1 }`). `hodgkinHuxley()` in the kit writes the squid axon's: its gates, and
220
+ GHK permeabilities matched to HH's conductances at rest. A pump-less cell has no steady rest
221
+ (`solve()` finds Donnan), so start the transient from the concentrations given: the membrane
222
+ settles within milliseconds.
223
+
224
+ ```js
225
+ import { Device } from 'driftlet';
226
+ import { build, layer, bath, aqueous, injector, pulse, hodgkinHuxley } from 'driftlet/kit';
227
+
228
+ const T = 279.45; // HH's 6.3 °C
229
+ const out = { 'Na+': 440, 'K+': 20, 'Cl-': 450, 'A-': 10 }, inside = { 'Na+': 50, 'K+': 400, 'Cl-': 52, 'A-': 398 }; // squid, mM
230
+ const water = aqueous(['Na+', 'K+', 'Cl-'], { epsr: 0 });
231
+ water.species.push({ name: 'A-', z: -1, cRef: 1000 });
232
+ water.materials.water.species['A-'] = { D: 0.5e-9, mu0: 0 };
233
+ const hh = hodgkinHuxley({ inside, outside: out, T }); // outside on the left
234
+ const axon = new Device(
235
+ build({
236
+ T,
237
+ library: [water],
238
+ stack: [
239
+ bath(out, 'Cl-'),
240
+ layer('water', 5e-6, { name: 'outside', c0: out }),
241
+ { phi: { type: 'capacitive', C: 0.01 }, gates: hh.gates, species: { ...hh.species, 'A-': 'blocked' } },
242
+ layer('water', 20e-6, { name: 'cytoplasm', c0: inside }),
243
+ { species: { 'Na+': 'blocked', 'K+': 'blocked', 'Cl-': 'blocked', 'A-': 'blocked' }, phi: 'neutral' },
244
+ ],
245
+ // the stimulus: 10 µA/cm² of K⁺ for 1 ms at 20 ms, from a pipette in the cytoplasm
246
+ ports: [injector({ region: 'cytoplasm', from: 15e-6, to: 20e-6, species: 'K+', I: pulse({ start: 20e-3, width: 1e-3, amplitude: 0.1 }) })],
247
+ grid: { hmin: 50e-9, hmax: 1e-6 },
248
+ }),
249
+ );
250
+ let peak = -1;
251
+ for (let k = 1; k <= 300; k++) {
252
+ const sol = axon.advance(k * 1e-4); // 0.1 ms at a time
253
+ peak = Math.max(peak, sol.interfaces[0].V); // V_m, and sol.interfaces[0].gates = { m, h, n }
254
+ }
255
+ console.log(`peak ${(peak * 1e3).toFixed(1)} mV`); // ~ +12 mV: GHK's Na⁺ current is limited by the 50 mM inside
256
+ ```
215
257
 
216
258
  ## Template: an electrode in a stirred solution
217
259
 
@@ -346,6 +388,11 @@ for (const Vd of [0.1, 0.5, 1, 1.5, 2, 3]) {
346
388
  }
347
389
  ```
348
390
 
391
+ To sweep the gate on one device instead, patch the port by name: `dev.set({ ports: { gate: { V } } })`
392
+ (a drive only, so it's cheap and keeps the state as a warm start). An organic electrochemical
393
+ transistor is the same with a volumetric capacitance C*: `area: 1 / t, capacitance: { C: Cstar * t }`
394
+ (docs/device.md, "A capacitance through a window").
395
+
349
396
  ## Template: a transient, with a pulse injected sideways
350
397
 
351
398
  A **port** feeds a window of nodes inside a region from the side (a point contact, an injector,
@@ -418,9 +465,9 @@ solve), as long as it settles rather than ping-pongs.
418
465
  <span id="readout"></span>
419
466
  <div id="figure"></div>
420
467
  <script type="module">
421
- import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.10.0/src/index.js';
422
- import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.10.0/src/kit.js';
423
- import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.10.0/src/plot.js';
468
+ import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/index.js';
469
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/kit.js';
470
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/plot.js';
424
471
 
425
472
  const def = build({
426
473
  T: 300,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driftlet",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Small, dependency-free 1D drift–diffusion–reaction (Poisson–Nernst–Planck) solver for interactive web demos",
5
5
  "type": "module",
6
6
  "exports": {
@@ -30,6 +30,7 @@
30
30
  "test": "node --test",
31
31
  "bench": "node bench/bench.js",
32
32
  "hard": "node bench/hard.js",
33
+ "stress": "node bench/stress.js",
33
34
  "smoke": "node tools/smoke.js",
34
35
  "shots": "node tools/shots.js",
35
36
  "prepublishOnly": "npm test && npm run types",
@@ -74,6 +74,14 @@ export class BlockTridiagonal {
74
74
  this.minPivotRatio = Infinity;
75
75
  /** The block found exactly singular by the last factor(), or −1. */
76
76
  this.singularBlock = -1;
77
+ /**
78
+ * Static pivoting: an exactly zero pivot (all its candidates cancelled to nothing) is
79
+ * replaced by 1e-15 of its block's largest entry rather than thrown on, for a caller that
80
+ * refines its solves (and counted in `perturbedPivots`).
81
+ */
82
+ this.staticPivots = false;
83
+ /** How many pivots the last factor() replaced. */
84
+ this.perturbedPivots = 0;
77
85
  }
78
86
 
79
87
  /** Zero all of A, B, C (ready for fresh assembly). */
@@ -86,13 +94,14 @@ export class BlockTridiagonal {
86
94
 
87
95
  /**
88
96
  * Factorise the current A, B, C. A, B, C are left untouched.
89
- * Throws if a diagonal block is exactly singular.
97
+ * Throws if a diagonal block is exactly singular (unless `staticPivots`).
90
98
  */
91
99
  factor() {
92
100
  const { n, A, B, C, sizes, offA, offB, offC, offX } = this;
93
101
  const lu = this._lu, piv = this._piv, cp = this._cp, tmp = this._tmp;
94
102
  let minRatio = Infinity;
95
103
  this.singularBlock = -1;
104
+ this.perturbedPivots = 0;
96
105
 
97
106
  for (let i = 0; i < n; i++) {
98
107
  const m = sizes[i], o = offB[i];
@@ -110,7 +119,7 @@ export class BlockTridiagonal {
110
119
  }
111
120
  }
112
121
  }
113
- const ratio = luFactor(lu, o, piv, offX[i], m);
122
+ const ratio = luFactor(lu, o, piv, offX[i], m, this);
114
123
  if (!(ratio > 0)) {
115
124
  this.factored = false;
116
125
  this.singularBlock = i;
@@ -244,7 +253,7 @@ export class BlockTridiagonal {
244
253
 
245
254
  // In-place LU with partial pivoting of the m×m block at a[o…]. Row swaps are recorded in
246
255
  // piv[p…]. Returns min |pivot| / max |entry| (0 if singular).
247
- function luFactor(a, o, piv, p, m) {
256
+ function luFactor(a, o, piv, p, m, perturb = null) {
248
257
  let maxEntry = 0;
249
258
  for (let k = 0; k < m * m; k++) {
250
259
  const v = Math.abs(a[o + k]);
@@ -259,7 +268,11 @@ function luFactor(a, o, piv, p, m) {
259
268
  if (v > bestVal) { best = r; bestVal = v; }
260
269
  }
261
270
  piv[p + k] = best;
262
- if (bestVal === 0) return 0;
271
+ if (bestVal === 0) {
272
+ if (!perturb?.staticPivots) return 0;
273
+ bestVal = a[o + best * m + k] = 1e-15 * maxEntry;
274
+ perturb.perturbedPivots++;
275
+ }
263
276
  if (bestVal < minPivot) minPivot = bestVal;
264
277
  if (best !== k) {
265
278
  for (let c = 0; c < m; c++) {