driftlet 0.9.0 → 0.11.4
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 +47 -37
- package/docs/device.md +142 -96
- package/docs/kit.md +5 -0
- package/docs/numerics.md +197 -26
- package/docs/reliability.md +122 -0
- package/llms.txt +111 -7
- package/package.json +3 -1
- package/src/blockTridiagonal.js +17 -4
- package/src/checks.js +44 -33
- package/src/describe.js +15 -9
- package/src/device.js +16 -5
- package/src/difference.js +142 -0
- package/src/index.js +39 -10
- package/src/polarization.js +51 -16
- package/src/solution.js +29 -11
- package/src/solver.js +1115 -165
- package/src/types.js +4 -0
- package/types/blockTridiagonal.d.ts +9 -1
- package/types/checks.d.ts +3 -3
- package/types/difference.d.ts +30 -0
- package/types/index.d.ts +3 -1
- package/types/live.d.ts +1 -0
- package/types/polarization.d.ts +8 -2
- package/types/solver.d.ts +65 -12
- package/types/types.d.ts +6 -0
package/docs/numerics.md
CHANGED
|
@@ -358,7 +358,9 @@ reaction) and that nothing outside feeds ($`w = 0`$ on stretches reached by a co
|
|
|
358
358
|
the null space of that stoichiometry. A stretch in none of them is fed. So Ag⁺ between silver
|
|
359
359
|
electrodes is fed (Ag⁺ + e⁻ ⇌ Ag(s), the electrons fed by the contacts), and the steady equations
|
|
360
360
|
are solved directly; Fe³⁺ and Fe²⁺ between platinum electrodes aren't, since Fe³⁺ + e⁻ ⇌ Fe²⁺
|
|
361
|
-
conserves the iron whatever the electrons do, so their total is kept by huge steps (below).
|
|
361
|
+
conserves the iron whatever the electrons do, so their total is kept by huge steps (below). An
|
|
362
|
+
electrode surface's species are columns too, holding $`\Gamma \sum v\,a\,\theta`$, so species fed only
|
|
363
|
+
through one (A⁺ + e⁻ = S, S + e⁻ = B⁻) conserve their total with what the surface holds. A
|
|
362
364
|
floating conductor starts uncharged, with its carrier's level in equilibrium with the first
|
|
363
365
|
reaction on its left face that takes it.
|
|
364
366
|
|
|
@@ -366,7 +368,8 @@ reaction on its left face that takes it.
|
|
|
366
368
|
|
|
367
369
|
A port adds a source per volume to the balance rows of the nodes in its window, after every
|
|
368
370
|
other term at those nodes and before the contacts. A held (`'equilibrium'`) level replaces the
|
|
369
|
-
balance row with a Dirichlet row
|
|
371
|
+
balance row with a Dirichlet row, after every port's other terms (another port's electrode can
|
|
372
|
+
react in the same window). The port's flux is then that row's residual, read just
|
|
370
373
|
before replacement, exactly as at a contact. Conductance and exchange links are linear in
|
|
371
374
|
$`(\bar\mu_{\mathrm{out}} - \bar\mu)`$. Everything stays on the node's own block. A contact's flux
|
|
372
375
|
readout at an end node already includes any port source there, so contact and port fluxes always
|
|
@@ -417,6 +420,29 @@ instead of $`n \cdot M^3`$, which pays wherever species are confined to some reg
|
|
|
417
420
|
against KCl, three unknowns of five at every node, it's a fifth of the work. A finite-difference
|
|
418
421
|
test checks the compact Jacobian column by column on devices that cover every assembly path.
|
|
419
422
|
|
|
423
|
+
### Exact J·v
|
|
424
|
+
|
|
425
|
+
The assembled matrix loses what a flux's two entries share. A Scharfetter–Gummel flux depends on
|
|
426
|
+
$`\eta`$ at its two ends through $`G`$ and $`-G(1 + E)`$, with $`E = \operatorname{expm1}(\Delta\eta)`$;
|
|
427
|
+
for a nearly uniform $`v`$ they cancel, and $`J \cdot v`$ keeps only round-off of $`G|v|`$, which in
|
|
428
|
+
an inversion layer's 0.1 nm cells ($`G`$ ~ 1e11) dwarfs the flux. Where a solve needs
|
|
429
|
+
$`J \cdot v`$ itself (GMRES, in the impedance and in Newton's refined solves), the dilute kernels'
|
|
430
|
+
terms are kept aside instead, each evaluated on differences first:
|
|
431
|
+
|
|
432
|
+
```math
|
|
433
|
+
\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),
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
with $`a = -GE`$ (through $`c_L`$), $`b = -G(1 + E)`$ and $`d`$ the drift's. A node's storage and space
|
|
437
|
+
charge depend on $`\delta\eta_i - z_i\,\delta\hat\phi`$ alone, the displacement on
|
|
438
|
+
$`\delta\hat\phi_R - \delta\hat\phi_L`$. Everything else (faces, contacts, ports, reactions,
|
|
439
|
+
concentrated and metal regions) is assembled into a matrix of its own. Rows that a contact or a
|
|
440
|
+
steady solve replaces drop the kept terms; the transformed rows of strictly neutral nodes combine
|
|
441
|
+
them as they combine the matrix's. A region's uniform shift ($`z_i s`$ on each $`\eta_i`$, $`s`$ on
|
|
442
|
+
$`\hat\phi`$) then gives exactly zero inside it, as it must. It replaced a central difference of
|
|
443
|
+
the residual, whose truncation (~1e-9 of $`|J||v|`$) swamped the answer for such a shift, and
|
|
444
|
+
which cost four assemblies per product.
|
|
445
|
+
|
|
420
446
|
## Newton
|
|
421
447
|
|
|
422
448
|
- The update of the device's potentials ($`\hat\phi`$, $`\eta`$) is limited to 10 thermal units per
|
|
@@ -436,8 +462,37 @@ test checks the compact Jacobian column by column on devices that cover every as
|
|
|
436
462
|
much reduced by the change of variables below), converges quadratically to ~1e-9 and then
|
|
437
463
|
rattles there. Short steps come right after every waveform breakpoint, so without this a
|
|
438
464
|
cyclic voltammogram in a neutral electrolyte stalled at its turns.
|
|
439
|
-
-
|
|
440
|
-
|
|
465
|
+
- **Refined solves, where the factorisation loses a mode.** The assembled $`J`$ holds a flux's
|
|
466
|
+
dependence on $`\eta`$ at its two ends as two entries; where they're huge, eliminating them loses
|
|
467
|
+
what they share to round-off. In a MOS capacitor's inversion layer the electrons' cells, 0.1 nm
|
|
468
|
+
wide, are coupled about $`h^2/(D\,dt)`$ ~ 1e-18 times more tightly than they store charge over a
|
|
469
|
+
second, so the layer's overall level, set by its storage and the trickle of minority electrons
|
|
470
|
+
from the bulk, is lost, and Newton's update there is noise. When Newton stalls short of
|
|
471
|
+
convergence (updates below 1e-4 that stopped shrinking quadratically), or its undamped updates
|
|
472
|
+
grow twice running (a bipolar stack's floating base, whose holes are held ~1e14 more weakly
|
|
473
|
+
than they move within it, kept Newton wandering a thermal unit at a time), the solve is refined:
|
|
474
|
+
GMRES on $`J\delta = r`$, the factorised system as the preconditioner and $`J \cdot v`$ exact, each
|
|
475
|
+
flux on its $`\eta`$ difference (see [exact J·v](#exact-jv)), as the impedance does. A lost mode
|
|
476
|
+
takes GMRES a couple of iterations. The refined update is kept where GMRES converged and it
|
|
477
|
+
moves no level by a thermal unit or more; otherwise refining stops for the rest of the solve.
|
|
478
|
+
A MOS capacitor's gate step now reaches the low-frequency charge in ~70 steps (it took 80,000,
|
|
479
|
+
and ended 1.5% off). Once refined, a solve stays refined: a GaAs junction without
|
|
480
|
+
recombination, whose minority carriers reach it only from the contacts, passes ~1.5e-7 A/m²,
|
|
481
|
+
which plain solves past the first refinement left to round-off (cold and warm solves 1e-3
|
|
482
|
+
apart), and refined ones give to 1e-14. (When $`J \cdot v`$ was a central difference of the
|
|
483
|
+
residual, its noise made refining past the first lost mode chase round-off, so it was kept
|
|
484
|
+
only where its correction dwarfed the plain update.)
|
|
485
|
+
- Clear divergence (device updates beyond 1e4 after the first iteration, or ten times the
|
|
486
|
+
first after six) bails out early, so the caller can take a smaller step. The first update is
|
|
487
|
+
exempt: light flooding a population that starts at ~1e3 per m³ predicts, linearised in
|
|
488
|
+
$`\eta`$, a change of 1e10 thermal units, which damping covers in a few iterations; bailing
|
|
489
|
+
out there had a lit phototransistor's pseudo-transient fail at every step above 1e-21 s.
|
|
490
|
+
- A pivot that cancels to exactly zero in the block factorisation is perturbed to 1e-15 of its
|
|
491
|
+
block's largest entry (static pivoting) rather than failing the solve. GaAs minority carriers,
|
|
492
|
+
~1 per m³ beside a face, are held through conductances 1e-16 of the face flux's coefficient in
|
|
493
|
+
their row, and the elimination cancels them exactly: such devices now solve, even at
|
|
494
|
+
equilibrium. Newton's updates stay meaningful (the factorisation only amplifies the residual,
|
|
495
|
+
so a small update still means a small residual), and its refined solves correct the rest.
|
|
441
496
|
- When a steady solve fails, the solution's warnings say how nearly singular the system was,
|
|
442
497
|
and where. A running error bound through the factorisation compares each pivot with the
|
|
443
498
|
magnitudes it was formed from, $`\log_{10}(\sum|\mathrm{terms}| / |\mathrm{pivot}|)`$: the digits
|
|
@@ -478,8 +533,22 @@ test checks the compact Jacobian column by column on devices that cover every as
|
|
|
478
533
|
$`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
|
|
479
534
|
backward Euler, scaled by $`h/(2h + h_1)`$. The first step, with no history, is checked by step
|
|
480
535
|
doubling.
|
|
536
|
+
- A first step whose error doesn't shrink with it is taken anyway. A jump in a level held at a
|
|
537
|
+
boundary (a bath's, after a voltage step) starts a profile self-similar in $`x/\sqrt{t}`$, so a
|
|
538
|
+
first step of any length errs alike (0.11 thermal units over ten decades of step, in a
|
|
539
|
+
0.6 M electrolyte with resolved double layers), down to the grid's own diffusion time,
|
|
540
|
+
where Newton runs out of digits. Three tries in a row, each within a factor 2 of the last,
|
|
541
|
+
mark the error as the jump's; backward Euler damps it as it should, and the steps grow
|
|
542
|
+
from there. Transients that start with a jump take 5–30% fewer factorisations for it.
|
|
543
|
+
- The state a first step starts from needn't satisfy the algebraic equations ($`\hat\phi`$ in a
|
|
544
|
+
strictly neutral material, an interface's unknowns), after a start or a jump; they jump in
|
|
545
|
+
the first instant. So the history starts from the first step's half step, not from there,
|
|
546
|
+
and the second step is backward Euler: a predictor through the starting state saw the jump
|
|
547
|
+
as error that shrank only as fast as the step, and a junction from a sharp boundary at a
|
|
548
|
+
tight tolerance stepped down into round-off.
|
|
481
549
|
- The error is measured in thermal units over every state potential ($`\hat\phi`$ where defined,
|
|
482
|
-
each present $`\eta`$,
|
|
550
|
+
each present $`\eta`$, an electrode surface's coverages as their $`\eta`$; a floating terminal's
|
|
551
|
+
voltage follows from these). That includes species far below their
|
|
483
552
|
largest concentration, such as minority carriers ahead of a diffusion front. Weighting
|
|
484
553
|
them down (a mixed relative/absolute tolerance,
|
|
485
554
|
$`\delta\eta \cdot c/(c + 10^{-10} c_{\mathrm{max}})`$) took a third fewer steps on a pn turn-on,
|
|
@@ -487,9 +556,9 @@ test checks the compact Jacobian column by column on devices that cover every as
|
|
|
487
556
|
current it cost 50% more.
|
|
488
557
|
- A step is rejected above `tol`. The next step size is
|
|
489
558
|
$`h \cdot \min(2, \max(0.2, 0.9\, (\mathtt{tol}/\mathrm{err})^{1/(p+1)}))`$.
|
|
490
|
-
- Newton failure quarters the step
|
|
491
|
-
|
|
492
|
-
|
|
559
|
+
- Newton failure quarters the step. (Strictly neutral materials had tried longer first steps
|
|
560
|
+
instead, shorter ones being worse conditioned there; with their ends and starts fixed, no
|
|
561
|
+
case needs it.)
|
|
493
562
|
- A target within round-off of the present time (1e-10 relative), such as an animation
|
|
494
563
|
frame's that lands a hair past a breakpoint just reached, is snapped to rather than
|
|
495
564
|
stepped to: a step of 1e-13 s can't be resolved.
|
|
@@ -523,15 +592,42 @@ species make $`J`$ singular, but $`J + i\omega M`$ isn't for $`\omega > 0`$: at
|
|
|
523
592
|
device looks like a capacitor, as it should.
|
|
524
593
|
|
|
525
594
|
The factorised system is used as a preconditioner, not trusted alone: each solve runs GMRES with
|
|
526
|
-
$`J \cdot v`$
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
is
|
|
534
|
-
|
|
595
|
+
an exact $`J \cdot v`$ (see [exact J·v](#exact-jv)). The assembled $`J`$ holds a flux's dependence
|
|
596
|
+
on $`\eta_L`$ and $`\eta_R`$ as two entries; in an inversion layer both are huge (~1e11), and for a
|
|
597
|
+
nearly uniform $`\delta\eta`$ they cancel to round-off of order 1e-5. That round-off had given a
|
|
598
|
+
MOS capacitor's inversion layer, fed by minority diffusion from a bulk with ~1e3 electrons per cm³
|
|
599
|
+
(a time constant of minutes), an exchange path that followed the gate at 1 Hz. Where the plain
|
|
600
|
+
solve is already accurate, one application of the operator confirms it. GMRES judges convergence only by the true residual,
|
|
601
|
+
recomputed at each restart: its own running estimate can drift far from it where the
|
|
602
|
+
preconditioner is nearly singular (an inversion layer's exchange, lost in the assembled
|
|
603
|
+
$`J`$, at a frequency of µHz), and had declared solves converged that weren't. It keeps its
|
|
604
|
+
best iterate, never worse than the factorised solve.
|
|
605
|
+
|
|
606
|
+
The current is read where its error is smallest. At a contact it is a sum of terms that can
|
|
607
|
+
be far larger than itself: across a junction without recombination, between heavily doped
|
|
608
|
+
sides, what passes is minority carriers of ~1e8 per m³, while the contact's terms are its
|
|
609
|
+
majority conductances times a $`\delta\eta`$ cancelling its neighbours' to 1e-17, and their
|
|
610
|
+
round-off reads as a constant conductance of order 1 S/m², a silicon p⁺n⁺ junction's capacitance
|
|
611
|
+
9× off at 100 Hz. Between the two contacts of a device without ports, the total current
|
|
612
|
+
(conduction plus displacement, $`i\omega\,\delta D`$) is the same through every cut, so it is also
|
|
613
|
+
read across each segment of an ideal region, linearised as the Scharfetter–Gummel flux is, and
|
|
614
|
+
the reading with the least estimated error is taken: in that junction, the depletion region's;
|
|
615
|
+
in a MOS capacitor, the oxide's displacement. A reading's error is its terms' round-off, and the
|
|
616
|
+
solves' error in $`\delta x`$ (their residual, relative to its largest entry) through its
|
|
617
|
+
coefficients: a contact's reading is a flux too (a held contact's balance row is replaced by its
|
|
618
|
+
level, so nothing solved pins it), and in a neutral bulk the current crosses a huge conductance
|
|
619
|
+
on a tiny gradient.
|
|
620
|
+
|
|
621
|
+
Where even the best reading's estimated error exceeds 1% of the current, the response is split
|
|
622
|
+
in two: a uniform shift of each region (every $`\eta_i`$ by $`z_i s_r`$ and $`\hat\phi`$ by $`s_r`$, with
|
|
623
|
+
$`s_r`$ fitted to the solve so far), which changes no flux and no concentration, so its $`J \cdot e`$ is
|
|
624
|
+
exactly zero inside each region and is taken at the faces and ends;
|
|
625
|
+
and the rest, small, solved for to GMRES's floor with its error now relative to itself, and read
|
|
626
|
+
separately. An electrolyte at its open circuit needs it: the response to its terminal is nearly
|
|
627
|
+
such a shift, and its current is the slope of levels uniform to 1e-14 of the response, more than
|
|
628
|
+
a double holds. A redox electrode's DC conductance had come out 2–40% low; now it matches the
|
|
629
|
+
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
|
|
630
|
+
only resolved to ~1e-4 of $`|Z|`$.
|
|
535
631
|
|
|
536
632
|
## Steady state
|
|
537
633
|
|
|
@@ -540,6 +636,28 @@ take about twice as long as before.
|
|
|
540
636
|
because slow physics can take seconds, far beyond any $`L^2/D`$ estimate, and stepping would
|
|
541
637
|
never finish. An example is exponentially scarce minority carriers slowly filling an inversion
|
|
542
638
|
layer behind a Schottky contact.
|
|
639
|
+
- **A flat level,** where a stretch's steady state is known outright: a species that no reaction
|
|
640
|
+
or port touches, reached by one contact only (blocked at the other end), carries no flux in a
|
|
641
|
+
steady state, so its level is that contact's throughout. Steady solves (not the impedance,
|
|
642
|
+
where it carries a current) pin it there in place of its balances, rather than find it
|
|
643
|
+
through its own conduction, which can be all but nothing: a MOS capacitor's inversion
|
|
644
|
+
electrons reach the back contact only through a bulk with ~1e3 of them per cm³, and found that
|
|
645
|
+
way, the solve converged at some gate voltages and grids and not others. (Not through flow or
|
|
646
|
+
mixing, nor a concentrated material's cross-diffusion, where zero flux isn't a flat level.)
|
|
647
|
+
At a face inside such a stretch whose link holds the level continuous, the face's flux of the
|
|
648
|
+
species, which only the replaced edge balances set, is pinned to zero too.
|
|
649
|
+
- **Continuation for a terminal driven by a current.** A cold steady solve at open circuit can
|
|
650
|
+
fail where the same state is easy warm (a solar cell in dim light on a fine grid). The
|
|
651
|
+
terminal is held at a voltage instead, its steady solves having their own continuation, and
|
|
652
|
+
the voltage marched from the start's, the step growing, until the current crosses its target:
|
|
653
|
+
up where the current is below it, since raising a terminal's voltage raises the current into
|
|
654
|
+
a passive device (the other way if 20 V brings no crossing). Bisection narrows the crossing
|
|
655
|
+
to 0.1 mV, and the terminal is floated from beside it. Floated, the system can lose what held
|
|
656
|
+
it doesn't (a closed Fe³⁺/Fe²⁺ cell driven near its limit, Fe³⁺ 20 orders below Fe²⁺: 33 digits
|
|
657
|
+
short), but the current-driven steady state is the held one at the voltage that passes the
|
|
658
|
+
target, so where the floated solve fails, held solves find that voltage instead, by regula
|
|
659
|
+
falsi within the bracket, to 1e-10 of the current. The voltage continuation's own start,
|
|
660
|
+
with the terminals level, ramps the light up when there is some.
|
|
543
661
|
- **Spectators** (a species blocked all round, mobile throughout its stretch) are solved
|
|
544
662
|
directly too. In steady state the sum of a spectator's balance rows over its stretch is zero
|
|
545
663
|
identically, so one of them (the first node's) is redundant, and it's replaced by the
|
|
@@ -547,7 +665,14 @@ take about twice as long as before.
|
|
|
547
665
|
bordered: the matrix is factorised with a pin (an identity row) in its place, which makes the
|
|
548
666
|
stretch's level a well-conditioned unknown, and the response to a unit pin (one extra
|
|
549
667
|
back-substitution per spectator) is added in the amount that satisfies the constraint, from
|
|
550
|
-
a $`k \times k`$ system for $`k`$ spectators.
|
|
668
|
+
a $`k \times k`$ system for $`k`$ spectators. Within one ideal region (no flow or mixing), a
|
|
669
|
+
spectator's level is flat at steady state, at a value its amount fixes, so its other balance
|
|
670
|
+
rows are replaced by $`\eta_g - \eta_{g-1} = 0`$, rather than found through its own conduction.
|
|
671
|
+
Past a closed zinc cell's limiting current, extended space charge at the cathode excludes the
|
|
672
|
+
blocked sulfate to ~1e-25 mol/m³, and the chain of conductances through it had lost the steady
|
|
673
|
+
system 14 digits: steady solves failed from 0.7 V, and took 37,000 iterations at 0.6 V. (Not
|
|
674
|
+
across a face: the rows that would hold the level there and pin its flux leave a diagonal
|
|
675
|
+
block singular.)
|
|
551
676
|
- **Conserved combinations** (moieties) of reacting stretches are solved the same way: the
|
|
552
677
|
total iron of Fe³⁺, Fe²⁺ and FeCl²⁺ in a closed cell with a complexation reaction, say. The
|
|
553
678
|
basis of combinations $`w`$ comes from the null space of the stoichiometry. In steady state the
|
|
@@ -561,6 +686,36 @@ take about twice as long as before.
|
|
|
561
686
|
larger by $`D \cdot dt/(hL)`$, around 1e12 for a micron-scale cell (see
|
|
562
687
|
[metal regions](#metal-regions) for the same cancellation). Newton then needs dozens of
|
|
563
688
|
iterations, or fails.
|
|
689
|
+
- **Islands**: the pieces of a stretch between its faces, where nothing else feeds a piece (no
|
|
690
|
+
contact, port or reaction). In steady state such a piece's level is held only through its
|
|
691
|
+
faces' fluxes, which can be far weaker than its own conduction: a face conductance of
|
|
692
|
+
1e-15 S/m² against a region conducting ~4e2 S/m, or neighbours that conduct 1e20 times less
|
|
693
|
+
than it. Eliminated, the level was lost to round-off below anything Newton could see (the
|
|
694
|
+
plain solves gave no update along it, so nothing stalled), and solves converged to a wrong one
|
|
695
|
+
silently: currents 2× or 16× off. Summed over the piece's nodes, its balance rows are its
|
|
696
|
+
faces' flux unknowns alone, $`A_R u_R - A_L u_L`$: the internal fluxes cancel exactly, by
|
|
697
|
+
construction rather than in round-off. Across a face that holds $`\bar\mu`$ level, $`u`$ is
|
|
698
|
+
set only by the edge balances, and eliminated it can come out of the piece's own, as
|
|
699
|
+
$`G\,\Delta\eta`$ with its two $`\eta`$ all but equal; so there the outside edge node's
|
|
700
|
+
balance is added too, $`u`$ cancels, and the outside's first segment carries the flux. (Seen
|
|
701
|
+
through one face of two, a level behind neighbours 1e20 times less conductive was overshot
|
|
702
|
+
twofold, and Newton swung it from one contact's level to the other's.) That sum can replace
|
|
703
|
+
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
|
|
704
|
+
$`\hat\phi`$ and the other species do along with it, so no mode needs guessing; refined solves
|
|
705
|
+
carry the pinned rows too.
|
|
706
|
+
|
|
707
|
+
Newton exact along such a level can be worse than Newton blind to it, far from the solution
|
|
708
|
+
(cold at bias, a bipolar stack's floating base took 1e5 thermal units in its first update), so
|
|
709
|
+
the summed rows go in only once the plain solves converge. An island at the wrong level then
|
|
710
|
+
shows: it takes in a different current than it passes on, so the terminal currents don't add
|
|
711
|
+
up to zero (to 1e-9 of the largest). Newton then carries on with the summed rows, refined from
|
|
712
|
+
their second update. A current that tiny against the conduction around it rides on $`\eta`$
|
|
713
|
+
differences (~1e-28 for 5 fA/m² through 1e4 mol/m³ at D = 1e-4 m²/s) that the last update
|
|
714
|
+
leaves known only to its own round-off, so where the currents still don't add up, one more
|
|
715
|
+
update polishes them. If Newton doesn't converge again within a dozen updates (a GaAs stack's
|
|
716
|
+
minority carriers, whose own rows rest on static pivots, at 1e-17 A/m² of noise), the plain
|
|
717
|
+
solution stands; and a steady solve that fails outright is tried once more with the summed rows
|
|
718
|
+
in from the start. Such devices now solve directly, their currents right to ~1e-15.
|
|
564
719
|
- **Immobile combinations** (trap states X⁰ and X⁻ under e⁻ + X⁰ = X⁻, with D = 0) conserve
|
|
565
720
|
node by node, since nothing carries them anywhere. At each node the combination's weighted sum
|
|
566
721
|
replaces one of its balance rows, kept at what it was when the solve began. The row is local
|
|
@@ -578,16 +733,26 @@ take about twice as long as before.
|
|
|
578
733
|
- **Generation continuation.** A device with generation reactions (species made only from, or
|
|
579
734
|
turned only into, fixed reservoirs, such as photogeneration from a photon reservoir) can be
|
|
580
735
|
held far from equilibrium even with its terminals level, where bias continuation can't help.
|
|
581
|
-
If a direct solve fails there, the generation rates are scaled down to 10
|
|
736
|
+
If a direct solve fails there, the generation rates are scaled down to 10⁻³⁰ and ramped back
|
|
582
737
|
up, ×100 a step while each solve converges (warm from the last) and by the square root of the
|
|
583
|
-
factor when one doesn't
|
|
584
|
-
|
|
738
|
+
factor when one doesn't, giving up when that factor falls below 1.5 (crawling, which time
|
|
739
|
+
steps do better); the pseudo-transient ramp then starts from the dimmer light's solution
|
|
740
|
+
rather than cold. (At 10⁻¹², a GaAs layer's generation still exceeded its equilibrium
|
|
741
|
+
recombination, ~3e-24 mol/m³ of minority electrons, a billionfold: the first step was the
|
|
742
|
+
whole jump.) An illuminated 80 µm silicon diode solves cold this way in about 80
|
|
743
|
+
iterations. (A lit n⁺pn stack with a floating base crawled for 20,000 iterations: at a
|
|
744
|
+
trillionth of the light, the base's level is held by couplings of ~1e-17.)
|
|
585
745
|
- **If that fails too,** source continuation ramps the right terminal's voltage to its target.
|
|
586
746
|
It ramps from the voltage of the last converged solve when the state is that solution, and
|
|
587
|
-
otherwise from level terminals, where a cold start is consistent.
|
|
747
|
+
otherwise from level terminals, where a cold start is consistent. (A device first solved
|
|
748
|
+
after a `set()` was laid out for its new voltages; if the level start fails from there, it's
|
|
749
|
+
laid out again, level, and retried.) The ramp step starts at
|
|
588
750
|
1/8 of the way, grows ×1.5 on success and shrinks ×4 on failure.
|
|
589
751
|
- **If Newton still fails,** $`dt`$ ramps up from a small value (pseudo-transient continuation),
|
|
590
|
-
ending in the direct solve where applicable.
|
|
752
|
+
ending in the direct solve where applicable. After a failed step it grows ×2, then ×1.5 more
|
|
753
|
+
each success, back to ×10. Once steps reach the slowest diffusion time, the direct solve is
|
|
754
|
+
tried from there, once a decade of $`dt`$ (the state kept if it fails): with immobile traps and
|
|
755
|
+
ions of D = 1e-17 m²/s, steps from 1e8 to 1e9 s kept failing on the way to the giant step.
|
|
591
756
|
|
|
592
757
|
`solve()` does not advance the clock.
|
|
593
758
|
|
|
@@ -627,8 +792,10 @@ was, so storage and neutrality don't see it. Only the fluxes do. But in $`(\hat\
|
|
|
627
792
|
and neutrality see it as pairs of huge entries that cancel, and round-off in that cancellation, of
|
|
628
793
|
order storage/flux ~ $`h^2/(D \cdot dt)`$ times the range of concentrations, swamps the fluxes that
|
|
629
794
|
fix it: a trace ion beside 3 M KCl lost about ten digits, and Newton stalled. So at each node that
|
|
630
|
-
stays neutral (interior nodes,
|
|
631
|
-
|
|
795
|
+
stays neutral (interior nodes, edges at `neutral` faces, and a device's end where its contact is
|
|
796
|
+
closed to every ion and leaves $`\phi`$ alone; not the edge of a capacitive or pinned face, which
|
|
797
|
+
holds the face's charge, nor an end whose contact holds a level), assembled without storage and
|
|
798
|
+
neutrality:
|
|
632
799
|
- rows: the balance of the most abundant charged species (by $`z^2 c`$) becomes
|
|
633
800
|
$`\sum (z_i/z_k) \times`$ each balance, which without storage is current continuity;
|
|
634
801
|
- columns: the unknowns become $`\hat\phi'`$ and $`\eta'_i = \eta_i - z_i \hat\phi`$ (the update is
|
|
@@ -636,7 +803,11 @@ pinned face, which holds the face's charge), assembled without storage and neutr
|
|
|
636
803
|
- then storage (on the other balances) and neutrality go in, exactly, in those terms.
|
|
637
804
|
|
|
638
805
|
A start-of-step net charge (round-off in a solved state) isn't carried over: the neutrality row
|
|
639
|
-
holds the new state neutral.
|
|
806
|
+
holds the new state neutral. An end node that its contact holds (a bath, an electrode's level)
|
|
807
|
+
keeps its plain rows, but the contact's current there is read before the ions' storage goes in,
|
|
808
|
+
and a bulk law doesn't count the box's net charge as displacement: under neutrality neither
|
|
809
|
+
carries charge, and read from the box's balance their round-off (∝ c/dt) cancelled only roughly.
|
|
810
|
+
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
|
|
640
811
|
storage at $`dt = \infty`$, and the impedance reads the storage matrix from them).
|
|
641
812
|
|
|
642
813
|
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.
|
|
18
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
19
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
17
|
+
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.11.4/src/index.js';
|
|
18
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.11.4/src/kit.js';
|
|
19
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.11.4/src/plot.js';
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
@@ -34,7 +34,9 @@ From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
|
34
34
|
and h⁺). In voltage units the conduction band sits below the valence band. `NaN` marks where a
|
|
35
35
|
species is absent.
|
|
36
36
|
- **Current**: `sol.current` is positive toward +x through the device. `sol.terminals[name]` gives
|
|
37
|
-
each terminal's `{ V, current }`, current into the device.
|
|
37
|
+
each terminal's `{ V, current }`, current into the device. Currents, fluxes and amounts are per
|
|
38
|
+
m² of a planar device; with a `geometry` (spherical, cylindrical or any cross-section A(x)) they
|
|
39
|
+
are totals through it.
|
|
38
40
|
- **Nothing physical is defaulted.** Every interface between different materials needs an
|
|
39
41
|
alignment (`dipole` or `step`), or `phi: 'neutral'` for a macroscopic model. Every metal face
|
|
40
42
|
next to a dielectric needs its φ law. Every species that doesn't reach a contact needs its
|
|
@@ -247,6 +249,108 @@ const dev = new Device(electrode(rest.terminals.left.V + 0.2));
|
|
|
247
249
|
console.log(`anodic current ${dev.solve().terminals.left.current.toFixed(2)} A/m²`);
|
|
248
250
|
```
|
|
249
251
|
|
|
252
|
+
## Template: corrosion under a drop (an electrode spread through a film)
|
|
253
|
+
|
|
254
|
+
A **port with reactions** is an electrode spread through a window of a region: a metal under a
|
|
255
|
+
thin film of water, a crevice's walls, a porous electrode. Its `terminal` is the metal's carrier,
|
|
256
|
+
`area` the electrode's area per volume of the window (1/h under a film h thick), and each
|
|
257
|
+
reaction is Butler–Volmer per area, as at a face. The region must be strictly neutral (ε = 0); a
|
|
258
|
+
`capacitance` adds the double layer. A **`geometry`** gives the device a cross-section A(x): here
|
|
259
|
+
x is the radius of a drop seen from its centre, A = 2πr·h(r), and every current and amount becomes
|
|
260
|
+
a total through it (A, mol), not per m².
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
264
|
+
import { aqueous, polarization } from 'driftlet/kit';
|
|
265
|
+
|
|
266
|
+
// Salt water on iron: a drop 3 mm across. The iron dissolves and reduces O₂, which comes in from
|
|
267
|
+
// the air through the drop's top, fastest at the thin rim: the centre corrodes, the rim doesn't.
|
|
268
|
+
const T = 298.15, RT = GAS_CONSTANT * T, R = 3e-3, h0 = 1e-3, cO2 = 0.26, D = 2e-9;
|
|
269
|
+
const lib = aqueous(['Na+', 'Cl-', 'Fe2+', 'OH-', 'H+'], { epsr: 0 }); // strictly neutral water
|
|
270
|
+
lib.species.push({ name: 'O2', z: 0, cRef: 1000 }, { name: 'e-', z: -1 }); // e⁻: the iron's, in no material
|
|
271
|
+
lib.materials.water.species.O2 = { D, mu0: 16.4e3 };
|
|
272
|
+
const r = Array.from({ length: 61 }, (_, j) => (j / 60) * R);
|
|
273
|
+
const h = r.map((x) => Math.max(h0 * (1 - (x / R) * (x / R)), 0.02 * h0)); // the drop's height
|
|
274
|
+
const dev = new Device({
|
|
275
|
+
T,
|
|
276
|
+
species: lib.species,
|
|
277
|
+
materials: lib.materials,
|
|
278
|
+
geometry: { area: { x: r, values: r.map((x, j) => 2 * Math.PI * x * h[j]) } }, // ions squeeze through the thin rim
|
|
279
|
+
regions: [{ name: 'drop', material: 'water', length: R, c0: { 'Na+': 500, 'Cl-': 500 + 2e-6, 'Fe2+': 1e-6, 'OH-': 1e-4, 'H+': 1e-4, O2: cO2 } }],
|
|
280
|
+
contacts: { left: { phi: 'neutral' }, right: { phi: 'neutral' } }, // closed: the centre, and the rim
|
|
281
|
+
bulkReactions: [
|
|
282
|
+
{ equation: 'H+ + OH- = H2O', fixed: { H2O: -237.129e3 }, kf: { water: 1.4e8 } },
|
|
283
|
+
// O₂ from the air through the drop: a rate k_f (1 − c/c_sat), k_f = D c_sat/h² (not a port's
|
|
284
|
+
// exchange link, whose rate is linear in μ rather than c)
|
|
285
|
+
{ equation: 'Air = O2', fixed: { Air: 16.4e3 + RT * Math.log(cO2 / 1000) }, kf: { water: { x: r, values: h.map((y) => (D * cO2) / (y * y)) } } },
|
|
286
|
+
],
|
|
287
|
+
ports: [{
|
|
288
|
+
name: 'iron', region: 'drop', terminal: 'e-',
|
|
289
|
+
V: 0, // the only metal and the only path for current, so it passes none: held, it fixes the level φ is read against
|
|
290
|
+
area: { x: r, values: h.map((y) => 1 / y) }, // m² of iron per m³ of water
|
|
291
|
+
capacitance: { C: 0.2, zeroCharge: -0.4 }, // the double layer: the potential moves continuously
|
|
292
|
+
reactions: [
|
|
293
|
+
{ equation: 'Fe2+ + 2 e- = Fe(s)', fixed: { 'Fe(s)': 0 }, k0: 5e-6, alpha: 0.5 },
|
|
294
|
+
{ equation: 'O2 + 2 H2O + 4 e- = 4 OH-', fixed: { H2O: -237.129e3 }, k0: 3e-9, alpha: 0.125 },
|
|
295
|
+
],
|
|
296
|
+
}],
|
|
297
|
+
grid: { hmin: 5e-6, hmax: 60e-6, ratio: 1.2 },
|
|
298
|
+
});
|
|
299
|
+
const sol = dev.advance(600); // ten minutes
|
|
300
|
+
const iron = sol.ports[0], last = iron.x.length - 1;
|
|
301
|
+
// rates[q][j]: reaction q at node j of the window, forward, mol/(m²·s) of iron; anodic current +
|
|
302
|
+
const anodic = (j) => 2 * FARADAY * -iron.rates[0][j] - 4 * FARADAY * iron.rates[1][j];
|
|
303
|
+
console.log(`iron at ${(iron.V - sol.phi[0]).toFixed(3)} V against the water at the centre (SHE, with table μ°)`);
|
|
304
|
+
console.log(`net current on the iron: ${anodic(0).toFixed(2)} A/m² at the centre, ${anodic(last).toFixed(2)} A/m² at the rim`);
|
|
305
|
+
// The Evans diagram at the centre: each reaction's current with the iron's level moved, the water held.
|
|
306
|
+
const levels = [-0.55, -0.5, -0.45, -0.4].map((v) => v + sol.phi[0]);
|
|
307
|
+
const evans = polarization(dev, sol, { port: 'iron', x: 0 }, levels);
|
|
308
|
+
for (const rx of evans.reactions) console.log(`${rx.equation}: ${Array.from(rx.current, (i) => i.toFixed(2)).join(', ')} A/m²`);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
On an electrode's surface, species can take its sites (`surface: { name: { mu0, capacity } }`,
|
|
312
|
+
Langmuir coverages), and a reaction with `bare: true` runs only on bare metal: a passive film
|
|
313
|
+
(docs/device.md, "An electrode's surface").
|
|
314
|
+
|
|
315
|
+
## Template: a thin-film transistor (a capacitance along a channel)
|
|
316
|
+
|
|
317
|
+
A **port with a `capacitance`** holds charge σ = C (V − zeroCharge − φ) per area of electrode
|
|
318
|
+
along its window, balanced by the region (Gauss's law, or neutrality at ε = 0): a gate along a
|
|
319
|
+
channel, a porous electrode's double layer. In 1D a field-effect transistor is its channel, x from
|
|
320
|
+
source to drain, with the gate spread along it: the gradual-channel approximation, exact for a thin
|
|
321
|
+
film until it pinches off.
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
325
|
+
|
|
326
|
+
const T = 300, VT = (GAS_CONSTANT * T) / FARADAY;
|
|
327
|
+
const t = 20e-9, L = 10e-6, W = 100e-6, C = 1e-3, mu = 1e-3; // film, length, width (m); F/m² of gate; m²/(V·s)
|
|
328
|
+
const tft = (Vg, Vd) =>
|
|
329
|
+
new Device({
|
|
330
|
+
T,
|
|
331
|
+
species: [{ name: 'e-', z: -1, cRef: 46 }], // an undoped film's electrons (N_c 2.8e19 cm⁻³)
|
|
332
|
+
materials: { film: { epsr: 0, species: { 'e-': { D: mu * VT, mu0: 0 } } } }, // ε = 0: F·n·t balances σ
|
|
333
|
+
regions: [{ name: 'channel', material: 'film', length: L }],
|
|
334
|
+
contacts: {
|
|
335
|
+
left: { V: 0, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'neutral' }, // source
|
|
336
|
+
right: { V: Vd, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'neutral' }, // drain
|
|
337
|
+
},
|
|
338
|
+
ports: [{ name: 'gate', region: 'channel', V: Vg, area: 1 / t, capacitance: { C, zeroCharge: 0.3 } }],
|
|
339
|
+
grid: { hmin: L / 2000, hmax: L / 100, ratio: 1.05 }, // fine at the drain, where it pinches off
|
|
340
|
+
});
|
|
341
|
+
// The output characteristic at V_G = 2 V: linear, then saturating past V_D ≈ V_G − 0.3 V.
|
|
342
|
+
for (const Vd of [0.1, 0.5, 1, 1.5, 2, 3]) {
|
|
343
|
+
const sol = tft(2, Vd).solve();
|
|
344
|
+
const I = -sol.current * t * W; // sol.current is per m² of the film's section
|
|
345
|
+
console.log(`V_D ${Vd.toFixed(1)} V: I_D ${(I * 1e6).toFixed(2)} µA, σ at the drain ${(sol.ports[0].sigma.at(-1) * 1e3).toFixed(3)} mC/m²`);
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
To sweep the gate on one device instead, patch the port by name: `dev.set({ ports: { gate: { V } } })`
|
|
350
|
+
(a drive only, so it's cheap and keeps the state as a warm start). An organic electrochemical
|
|
351
|
+
transistor is the same with a volumetric capacitance C*: `area: 1 / t, capacitance: { C: Cstar * t }`
|
|
352
|
+
(docs/device.md, "A capacitance through a window").
|
|
353
|
+
|
|
250
354
|
## Template: a transient, with a pulse injected sideways
|
|
251
355
|
|
|
252
356
|
A **port** feeds a window of nodes inside a region from the side (a point contact, an injector,
|
|
@@ -319,9 +423,9 @@ solve), as long as it settles rather than ping-pongs.
|
|
|
319
423
|
<span id="readout"></span>
|
|
320
424
|
<div id="figure"></div>
|
|
321
425
|
<script type="module">
|
|
322
|
-
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
323
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
324
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
426
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.11.4/src/index.js';
|
|
427
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.11.4/src/kit.js';
|
|
428
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.11.4/src/plot.js';
|
|
325
429
|
|
|
326
430
|
const def = build({
|
|
327
431
|
T: 300,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driftlet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.4",
|
|
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": {
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"scripts": {
|
|
30
30
|
"test": "node --test",
|
|
31
31
|
"bench": "node bench/bench.js",
|
|
32
|
+
"hard": "node bench/hard.js",
|
|
33
|
+
"stress": "node bench/stress.js",
|
|
32
34
|
"smoke": "node tools/smoke.js",
|
|
33
35
|
"shots": "node tools/shots.js",
|
|
34
36
|
"prepublishOnly": "npm test && npm run types",
|