driftlet 0.14.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/alignment.md +6 -3
- package/docs/data.md +3 -1
- package/docs/device.md +87 -28
- package/docs/kit.md +47 -16
- package/docs/numerics.md +193 -114
- package/docs/reliability.md +15 -9
- package/docs/statistics.md +28 -4
- package/llms.txt +68 -38
- package/package.json +1 -1
- package/src/blockTridiagonal.js +12 -6
- package/src/checks.js +11 -4
- package/src/describe.js +46 -5
- package/src/device.js +33 -10
- package/src/index.js +19 -7
- package/src/particles.js +131 -55
- package/src/solution.js +145 -8
- package/src/solver.js +708 -260
- package/src/sources.js +10 -2
- package/src/stack.js +16 -6
- package/src/statistics.js +7 -3
- package/types/particles.d.ts +3 -6
- package/types/solver.d.ts +43 -29
- package/types/stack.d.ts +6 -4
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ import { bandDiagram } from 'driftlet/plot'; // a level diagram as an SVG str
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Or straight from a CDN in a page, pinned to a version:
|
|
59
|
-
`https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
59
|
+
`https://cdn.jsdelivr.net/npm/driftlet@0.16.2/src/index.js` (and `src/kit.js`, `src/plot.js`).
|
|
60
60
|
TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
|
|
61
61
|
[`llms.txt`](llms.txt).
|
|
62
62
|
|
package/docs/alignment.md
CHANGED
|
@@ -83,7 +83,9 @@ def.interfaces = [{
|
|
|
83
83
|
|
|
84
84
|
`vacuumDipole` gives $`\phi_R - \phi_L = (V_{\mathrm{vac}} - \phi)_L - (V_{\mathrm{vac}} - \phi)_R`$,
|
|
85
85
|
each side's $`V_{\mathrm{vac}} - \phi`$ coming from its anchor and offset (`vacuumLevel` gives one
|
|
86
|
-
side's).
|
|
86
|
+
side's). Its two sides are the face's, left then right as the stack runs: given the other way
|
|
87
|
+
round, the dipole changes sign, and nothing can tell (a photoanode's band edges came out 0.5 V
|
|
88
|
+
off that way).
|
|
87
89
|
|
|
88
90
|
Where a conductor meets a material, at a conductor region's face or at a contact's capacitive
|
|
89
91
|
or pinned law, the conductor's side is its work function W from its Fermi level, and the
|
|
@@ -120,8 +122,9 @@ $`\mathrm{pzc} \approx W/e - 4.44\,\mathrm{V}`$ on the SHE scale (on the usual c
|
|
|
120
122
|
|
|
121
123
|
**Semiconductor | electrolyte.** Electron affinity against the absolute SHE places the band edges on
|
|
122
124
|
the SHE scale, $`E_c \approx -(\chi - 4.44)\,\mathrm{eV}`$ relative to SHE: the usual
|
|
123
|
-
photoelectrochemistry estimate:
|
|
124
|
-
`vacuumDipole(def, { material: 'TiO2', anchor: 'e-', offset: χ }, { material: 'water', anchor: 'H+', offset: 4.44 })
|
|
125
|
+
photoelectrochemistry estimate, for TiO₂ on the left of the water:
|
|
126
|
+
`vacuumDipole(def, { material: 'TiO2', anchor: 'e-', offset: χ }, { material: 'water', anchor: 'H+', offset: 4.44 })`
|
|
127
|
+
(the anchor needs `H+` among the species).
|
|
125
128
|
|
|
126
129
|
**Solvent | solvent.** Each solvent's own absolute-SHE-type value (its Trasatti-type offset)
|
|
127
130
|
aligns them, exactly as Anderson's rule aligns two semiconductors.
|
package/docs/data.md
CHANGED
|
@@ -17,7 +17,9 @@ for `build()`: each ion a species with $`c_{\mathrm{ref}}`$ = 1000 mol/m³ (1 M)
|
|
|
17
17
|
`water` with its D and $`\mu^\circ`$. `epsr` defaults to water's 78.30 at 25 °C (Malmberg and
|
|
18
18
|
Maryott 1956); `epsr: 0` makes the solution strictly neutral, for macroscopic models without
|
|
19
19
|
double layers. The data are in `IONS`, and $`\mu^\circ`$ of liquid water is `H2O`
|
|
20
|
-
(−237.129 kJ/mol), for reactions such as H₂O ⇌ H⁺ + OH⁻.
|
|
20
|
+
(−237.129 kJ/mol), for reactions such as H₂O ⇌ H⁺ + OH⁻. Together they give $`K_w`$ =
|
|
21
|
+
1.011e-14 at 25 °C (pK_w 13.995): neutral water holds 1.0055e-4 mol/m³ of each, which a bath
|
|
22
|
+
with that reaction should hold too.
|
|
21
23
|
|
|
22
24
|
| Ion | z | D (10⁻⁹ m²/s) | $`\mu^\circ`$ (kJ/mol) |
|
|
23
25
|
|---|---|---|---|
|
package/docs/device.md
CHANGED
|
@@ -170,10 +170,14 @@ defaults to no dipole.
|
|
|
170
170
|
The alignment drops out and must not be given. This is the default between two $`\varepsilon = 0`$
|
|
171
171
|
materials, and next to an insertion host (where $`\phi`$ is undefined).
|
|
172
172
|
- `{ type: 'capacitive', C }`: a Helmholtz layer, $`D = -C \cdot (\Delta\phi - \mathtt{dipole})`$, C
|
|
173
|
-
in F/m².
|
|
173
|
+
in F/m². Beside a strictly neutral ($`\varepsilon = 0`$) material, the face's charge is held in
|
|
174
|
+
the cell next to it, which acts as a diffuse layer half that cell wide, $`F^2 \sum_i z_i^2 c_i \, h/2RT`$
|
|
175
|
+
in series with C. So keep that cell coarse rather than fine (a warning says when it costs C more
|
|
176
|
+
than 2%, and how coarse is enough).
|
|
174
177
|
|
|
175
178
|
**Species laws** `species` (default: local equilibrium, $`\bar\mu`$ continuous, where the species is
|
|
176
|
-
present on both sides; blocked otherwise
|
|
179
|
+
present on both sides and can move on one at least; blocked otherwise, so trap states with
|
|
180
|
+
$`D = 0`$ stay in their regions): `'equilibrium'`, `'blocked'`, or
|
|
177
181
|
`{ type: 'conductance', G }` ($`J = G \cdot (V_L - V_R)`$, G in S/m², charged species), or
|
|
178
182
|
`{ type: 'permeability', P }` (P in m/s; see [membranes](#membranes)). A species that
|
|
179
183
|
takes part in a reaction at the face and exists on both sides has no default: give its link
|
|
@@ -370,7 +374,11 @@ The conductor's carrier can continue across as a species (e⁻ into a semiconduc
|
|
|
370
374
|
`'equilibrium'` by default). An electrode is a conductor region with `reactions` at its face,
|
|
371
375
|
the metal's electrons taking part on its side at its Fermi level, behind a contact that holds
|
|
372
376
|
the electrons (`{ V, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'bulk' }`). A
|
|
373
|
-
floating conductor region with reactions on both faces is a bipolar electrode
|
|
377
|
+
floating conductor region with reactions on both faces is a bipolar electrode; between two
|
|
378
|
+
capacitive faces and nothing else, a floating gate, which keeps its charge
|
|
379
|
+
$`D_{\mathrm{right}} - D_{\mathrm{left}}`$ (its faces' `interfaces[f].D`) through any steady solve. A
|
|
380
|
+
port on it (a wire to the whole conductor, with a `conductance`) charges it, held at a voltage,
|
|
381
|
+
and leaves it charged, set to `I: 0`.
|
|
374
382
|
|
|
375
383
|
## Bulk reactions
|
|
376
384
|
|
|
@@ -466,25 +474,37 @@ contacts: {
|
|
|
466
474
|
over a Schottky barrier (below).
|
|
467
475
|
- `'blocked'`: no flux.
|
|
468
476
|
|
|
477
|
+
A contact holding several species in equilibrium fixes the composition at its end: with Ag⁺ and
|
|
478
|
+
e⁻ both held in a silver halide, its electron and hole densities follow from their levels and
|
|
479
|
+
neutrality, not from `c0` (so a different fixed charge changes them). Read them from the solution.
|
|
480
|
+
|
|
469
481
|
**`bath`** (instead of `species` and `phi`): the outside phase is a neutral composition `c`
|
|
470
482
|
held in place by a charged `reference` species (as for a reversible reference electrode, e.g.
|
|
471
483
|
Cl⁻ for Ag/AgCl), which is the terminal. Every bath species is in equilibrium at the level its
|
|
472
484
|
composition implies, and the $`\phi`$ law is `'bulk'`. An optional `offset` places the reference
|
|
473
485
|
species relative to V. Without a `reference`, V is the bath's own $`\phi`$: an ideal salt bridge
|
|
474
486
|
(no junction potential), the convention by which membrane potentials are $`\phi`$ differences.
|
|
475
|
-
Between two such baths, a held $`V_{\mathrm{right}} - V_{\mathrm{left}}`$ is a voltage clamp.
|
|
487
|
+
Between two such baths, a held $`V_{\mathrm{right}} - V_{\mathrm{left}}`$ is a voltage clamp. The bath is the end
|
|
488
|
+
material at that composition (which then balances any fixed charge there, as an insertion host's
|
|
489
|
+
background carriers do). A solution against a charged end region (an ion exchanger, a gel) names
|
|
490
|
+
its own material, `bath: { c, material: 'water' }`: its composition is then neutral by itself, its
|
|
491
|
+
levels are that material's, and the end node takes them through a Donnan step.
|
|
476
492
|
|
|
477
493
|
**`phi`**, required whenever any species or reaction connects at the contact (default
|
|
478
494
|
`'neutral'`):
|
|
479
495
|
- `'bulk'`: the end node is plain bulk, locally neutral, with no double layer at the contact
|
|
480
|
-
(ohmic contacts, baths). The outside takes whatever surface charge that needs.
|
|
496
|
+
(ohmic contacts, baths). The outside takes whatever surface charge that needs. A double layer
|
|
497
|
+
or depletion region that reaches it is cut off there (σ 58% low for a crowded layer 5 Debye
|
|
498
|
+
lengths from the contact), which the solution's `warnings` say: lengthen the region.
|
|
481
499
|
- `'neutral'`: no charge at the face ($`D = 0`$), as for an internal `'neutral'` face.
|
|
482
500
|
- `{ type: 'capacitive', C }` with the contact's `zeroCharge`: a gate or Stern layer to a conductor
|
|
483
501
|
at the terminal voltage V. The displacement into the device is
|
|
484
502
|
$`C \cdot ((V - \mathtt{zeroCharge}) - \phi_{\mathrm{edge}})`$, so `zeroCharge` is the value of
|
|
485
503
|
$`V - \phi_{\mathrm{edge}}`$ at which the interface carries no charge: the potential of zero charge
|
|
486
504
|
(pzc) of an electrode, or for a gate the flat-band voltage less the semiconductor's bulk $`\phi`$
|
|
487
|
-
($`V_{\mathrm{FB}} = \mathtt{zeroCharge} + \phi_{\mathrm{bulk}}`$).
|
|
505
|
+
($`V_{\mathrm{FB}} = \mathtt{zeroCharge} + \phi_{\mathrm{bulk}}`$). So to start uncharged at
|
|
506
|
+
V = 0, give `zeroCharge` = −φ_bulk, which the material's composition sets (`sol.phi` far from
|
|
507
|
+
the face, after a solve).
|
|
488
508
|
- `'pinned'` with `zeroCharge`: the $`C \to \infty`$ limit,
|
|
489
509
|
$`\phi_{\mathrm{edge}} = V - \mathtt{zeroCharge}`$. This is what a "fixed $`\phi`$" boundary honestly
|
|
490
510
|
means. For example, a Schottky barrier $`\phi_B`$ on n-type material with
|
|
@@ -574,11 +594,15 @@ that, use a [bulk reaction](#bulk-reactions) with the outside phase as a fixed p
|
|
|
574
594
|
$`k_f (1 - c/c_{sat})`$; a profile gives $`k_f`$ its spatial variation ($`D c_{sat}/h^2`$ under a film
|
|
575
595
|
of thickness $`h(x)`$).
|
|
576
596
|
|
|
577
|
-
A held level can also
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
597
|
+
A held level can also stand in for a supply. In a 1D MOS capacitor without a port, inversion
|
|
598
|
+
electrons arrive only by generation or by diffusion from the back contact. The steady state
|
|
599
|
+
still solves (to equilibrium), but they can't follow a signal faster than that supply: the
|
|
600
|
+
impedance gives the low-frequency C–V only below a knee at about 1/(2πτ_c), τ_c the time the
|
|
601
|
+
inversion charge takes to refill (10 s with a 1 µs lifetime; hours by diffusion alone through a
|
|
602
|
+
wafer), and the high-frequency curve above it. A port holding
|
|
603
|
+
the electrons beside the oxide, as source and drain would, supplies them at once, and the device
|
|
604
|
+
shows the low-frequency C–V at any frequency (a conductance link instead puts the knee where you
|
|
605
|
+
choose).
|
|
582
606
|
|
|
583
607
|
### Electrodes spread through a window
|
|
584
608
|
|
|
@@ -631,7 +655,10 @@ Langmuir statistics, $`\mu = \mu^\circ + RT\ln(\theta/\theta_0)`$ with $`\theta_
|
|
|
631
655
|
bare fraction, and `capacity` $`\Gamma`$, mol of sites per m² of electrode. Reactions name them like
|
|
632
656
|
species, and a reaction with `bare: true` runs only on bare metal: its rate is multiplied by
|
|
633
657
|
$`\theta_0`$, both ways, so equilibrium stays exact. Written for adsorption itself, that's Langmuir
|
|
634
|
-
kinetics (on at a rate ∝ $`\theta_0`$, off ∝ $`\theta`$).
|
|
658
|
+
kinetics (on at a rate ∝ $`\theta_0`$, off ∝ $`\theta`$). A species' `theta0` is its starting
|
|
659
|
+
coverage (not the bare fraction). Surface species carry no charge, so an adsorbed redox couple
|
|
660
|
+
balances its electrons through the solution: `'Oads + e- = Rads + Cl-'`, its coverages at
|
|
661
|
+
$`\theta_O/\theta_R = e^{(E - E^\circ)/V_T}`$ against the chloride's level.
|
|
635
662
|
|
|
636
663
|
```js nocheck
|
|
637
664
|
ports: [{
|
|
@@ -666,8 +693,9 @@ Cautions:
|
|
|
666
693
|
|
|
667
694
|
Validated in test/coverage.test.js: the Langmuir isotherm against the electrode's potential (to
|
|
668
695
|
1e-9), filling against its exponential and its charge $`F\,\Gamma\,\Delta\theta`$ per area of electrode,
|
|
669
|
-
the active–passive curve with blocking exactly $`(1-\theta)`$ times Butler–Volmer,
|
|
670
|
-
through a surface intermediate where the transient ends
|
|
696
|
+
the active–passive curve with blocking exactly $`(1-\theta)`$ times Butler–Volmer, a steady state
|
|
697
|
+
through a surface intermediate where the transient ends, and an adsorbed couple at Nernst from a
|
|
698
|
+
cold start.
|
|
671
699
|
|
|
672
700
|
### A capacitance through a window
|
|
673
701
|
|
|
@@ -815,7 +843,10 @@ options at all, each region is graded from `hmin` = 1/1000 to `hmax` = 1/20 of i
|
|
|
815
843
|
geometrically from `hmin` at both ends of each region up to `hmax`, scaled to fit exactly. Each
|
|
816
844
|
region boundary becomes a pair of nodes at the same x, one per side. The grid is never refined
|
|
817
845
|
automatically. If a double layer that the model resolves is coarser than the local Debye
|
|
818
|
-
length, the solution's `warnings` say so.
|
|
846
|
+
length, the solution's `warnings` say so. That's a floor, not an accuracy: a strongly polarised
|
|
847
|
+
layer screens over its local length (counter-ions piled up at the wall: 0.5 nm at 1 V on
|
|
848
|
+
0.1 mM, where λ_D is 30 nm), and σ to ~1% wants cells a few times finer than that (0.3 nm cells
|
|
849
|
+
there left σ 0.7% high, without a warning). Halve `hmin` and compare.
|
|
819
850
|
|
|
820
851
|
Grade the grid toward wherever a profile is steep: double layers, and electrodes where a species
|
|
821
852
|
is depleted. Near a limiting current the depleted species' profile is steep in a thin layer at
|
|
@@ -824,6 +855,21 @@ cells exceeds the limiting current by 1.4% at 0.5 V, while `{ hmin: 10e-9, hmax:
|
|
|
824
855
|
within 0.1% with as many nodes. Where a species carrying the current is steep across a region's
|
|
825
856
|
end cells in this way, the solution's `warnings` say so, with a rough estimate of the excess.
|
|
826
857
|
|
|
858
|
+
A homogeneous reaction feeding an electrode (a CE mechanism) works within its reaction layer,
|
|
859
|
+
$`\sqrt{D/k}`$, which wants cells of a third of it for ~0.5% and a tenth for ~0.1% (20% off at
|
|
860
|
+
ten times that, unwarned).
|
|
861
|
+
|
|
862
|
+
Past a limiting current, the depleted layer at a membrane or an electrode is nanometres thick,
|
|
863
|
+
and anything that happens in it (water splitting, say) needs cells a fraction of that: a bulk
|
|
864
|
+
water-splitting reaction at an ion-exchange membrane gave 2.7 times the limiting current on
|
|
865
|
+
50 nm cells and 1.03 times on 0.2 nm ones. Only `check()` catches it.
|
|
866
|
+
|
|
867
|
+
In a transient, a diffusion layer grows from the electrode as $`\sqrt{Dt}`$, through cells up to
|
|
868
|
+
`hmax` wide, and nothing warns while they're coarse: give `hmax` a small fraction of
|
|
869
|
+
$`\sqrt{Dt}`$ at the earliest time that matters. In a chronopotentiometry run, cells half
|
|
870
|
+
$`\sqrt{D\tau}`$ wide put the surface concentration 0.5% off, a fifteenth 0.06% (the error goes
|
|
871
|
+
as $`h^2`$).
|
|
872
|
+
|
|
827
873
|
## Geometry
|
|
828
874
|
|
|
829
875
|
A device is planar unless it says otherwise: a slab whose cross-section $`A`$ is 1 m², so every
|
|
@@ -871,22 +917,27 @@ const now = dev.solution(); // snapshot of the current state
|
|
|
871
917
|
|
|
872
918
|
- `solve()` finds the steady state from the current state without advancing time. If every
|
|
873
919
|
species is fed by a contact, it solves the steady equations directly. Otherwise conserved
|
|
874
|
-
amounts
|
|
920
|
+
amounts are kept exactly: blocked species, reactive moieties, and whatever a terminal driven
|
|
921
|
+
at no current is the only way into (a closed battery at open circuit keeps its charge, the
|
|
922
|
+
state of charge it holds when the solve begins; a gate left floating keeps the charge on its
|
|
923
|
+
capacitance). Driven at any other current, such a terminal only stores what comes in (a host
|
|
924
|
+
filling, a capacitor charging), which has no steady state, and the solve says so at once. If a direct solve fails at a
|
|
875
925
|
bias, it ramps the contacts' voltages from where they were last solved (the left one first,
|
|
876
926
|
where it moved), or from level terminals on a cold start; with generation reactions (below),
|
|
877
927
|
it ramps their rates up from nearly nothing. A terminal driven by a current (open circuit,
|
|
878
928
|
say) is held at a voltage instead, the voltage marched until the current crosses its target,
|
|
879
929
|
and floated from there. If 20 V either way brings no crossing (a current beyond the limiting
|
|
880
|
-
one,
|
|
881
|
-
solve fails
|
|
882
|
-
fails reports the state it failed in, then leaves the device as it was before it.
|
|
930
|
+
one), the solve fails with a warning giving the range of currents the held voltages passed. A
|
|
931
|
+
solve that fails reports the state it failed in, then leaves the device as it was before it.
|
|
883
932
|
- `step(dt, { method })` advances the transient by dt seconds, halving internally where Newton
|
|
884
933
|
needs it. `method` is `'be'` (backward Euler, the default) or `'bdf2'`.
|
|
885
934
|
- `advance(tEnd, opts)` integrates adaptively to `tEnd` with variable-step BDF2, controlling
|
|
886
935
|
the local error per step to `tol` (default 1e-3) in thermal units of every potential ($`\phi`$ and
|
|
887
936
|
each $`\bar\mu/RT`$): roughly 0.1% in concentrations per step. That's local: over a long decay
|
|
888
937
|
the errors add up, and a small current at the end of one can be tens of percent off, so tighten
|
|
889
|
-
`tol` (1e-5, say) before fitting a time constant to a tail. It lands exactly on `tEnd
|
|
938
|
+
`tol` (1e-5, say) before fitting a time constant to a tail. It lands exactly on `tEnd`, an
|
|
939
|
+
absolute time (a solution's `time` is where the device is); a NaN does nothing, and its
|
|
940
|
+
`warnings` say so. Options: `tol`,
|
|
890
941
|
`dt0` (first step), `dtMax`, `budgetMs` (return after this much wall time, with
|
|
891
942
|
`done: false`), `maxSteps`, `method`. The step size carries over between calls, so an
|
|
892
943
|
animation can call `advance(tNext, { budgetMs })` once per frame. The solution adds `done`,
|
|
@@ -897,26 +948,34 @@ const now = dev.solution(); // snapshot of the current state
|
|
|
897
948
|
`probes: [{ x, species, quantity, region }]` the trace also reads inside the device,
|
|
898
949
|
as `trace.probes[k]` beside `trace.t`: a species' concentration (`quantity: 'c'`, the default,
|
|
899
950
|
mol/m³) or species voltage (`'V'`), or $`\phi`$ itself (`'phi'`, no species), linearly between the
|
|
900
|
-
nodes around `x` (at
|
|
951
|
+
nodes around `x` (at a face, the side where the species is; where both sides have it, `region`
|
|
952
|
+
says which). That's what a detector at `x` sees.
|
|
901
953
|
At a face, `{ interface: f, species }` reads a species' flux through it (mol/(m²·s), toward +x,
|
|
902
954
|
as `interfaces[f].N`), and `{ interface: f, gate }` a gate's fraction open. Probes read at accepted steps, which grow
|
|
903
955
|
as a transient slows, so give `dtMax` to resolve a signal in time.
|
|
904
956
|
- `impedance(frequencies, { terminal, profiles })` solves the steady state, then linearises about
|
|
905
957
|
it: $`Z(f) = \delta V/\delta I`$ in Ω·m² at one terminal (`'right'` by default), with I into the
|
|
906
958
|
device. A held terminal's voltage is perturbed, or a driven one's current; the other terminals
|
|
907
|
-
keep their drives (held ones at AC ground, driven ones open)
|
|
908
|
-
|
|
959
|
+
keep their drives (held ones at AC ground, driven ones open), so at least one other must be held
|
|
960
|
+
for the current to return (with every other one driven, the impedance is infinite, and an error
|
|
961
|
+
says to measure at a driven one). A terminal behind a resistance can't be the one measured: the
|
|
962
|
+
resistance belongs to the external circuit. The grid must resolve how far the signal reaches,
|
|
963
|
+
and nothing warns of it: in a porous electrode, $`\sqrt{\kappa/(\omega C a)}`$ (cells of 2 µm
|
|
964
|
+
against a 3.4 µm depth put Z 7% off, falling as the cells do). With `profiles: true`, each
|
|
909
965
|
frequency also returns complex profiles of $`\delta\phi`$, $`\delta\bar\mu`$ and $`\delta c`$ per unit
|
|
910
966
|
excitation.
|
|
911
967
|
- `set(patch)` merges plain objects deeply (arrays are replaced), except that a contact given `V`
|
|
912
968
|
or `I` drops the other: `{ contacts: { right: { I: 0 } } }` switches it to open circuit. A patch that changes only the
|
|
913
969
|
terminals' drives (`V`, `I`, `R`) updates them in place, cheaply, keeping everything else.
|
|
914
|
-
Otherwise the device is rebuilt: the current state carries over while the
|
|
915
|
-
are unchanged
|
|
970
|
+
Otherwise the device is rebuilt: the current state carries over while the species, regions
|
|
971
|
+
and terminals are unchanged (on a new grid, interpolated onto its nodes, so a floating gate
|
|
972
|
+
keeps its charge), or restarts from the regions' `c0`. A carried state keeps what it holds: a
|
|
916
973
|
stretch that was closed stays at its amount, and one that a change closes (or splits) keeps
|
|
917
974
|
what it holds at that moment. So a new `c0` doesn't apply to a carried state; to start over
|
|
918
|
-
from `c0`, make a new `Device`.
|
|
919
|
-
|
|
975
|
+
from `c0`, make a new `Device`. (A bath's composition is a contact's, not a `c0`: changing it
|
|
976
|
+
mid-run, `set({ contacts: { right: { bath: { c } } } })`, is a step in the sample.) Either way the time stepping restarts its
|
|
977
|
+
order, as after any discontinuity, but a carried state goes on at the step size an adaptive
|
|
978
|
+
transient had reached, so a page may change a rate constant or a diffusivity every frame.
|
|
920
979
|
|
|
921
980
|
For example, a silver nitrate cell between silver electrodes: its impedance spectrum, then the
|
|
922
981
|
current transient after a voltage step.
|
|
@@ -963,7 +1022,7 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
|
|
|
963
1022
|
| `interfaces[f]` | `{ left, right, dipole, sheetCharge, D, N: {name}, rates, links: {name} }`: the names of the regions the face joins, what crosses it by its links, each face reaction's rate (mol/(m²·s)), and how each species that crosses it does; for those crossing by permeability, `oneWay: { name: [toward +x, toward −x] }`, the one-way fluxes whose difference is `N` (Ussing's unidirectional fluxes) |
|
|
964
1023
|
| `bulkReactions[k]` | `{ rate, forward, nu, regions, total }`: each bulk reaction's net forward rate at every node (mol/(m³·s), `NaN` where it doesn't run), its one-way forward rate (the backward is `forward − rate`), what one forward reaction makes of each species (`nu`, negative for what it consumes), and integrated over each region and the device (mol/(m²·s)), as the balances count it: a charge-balance check is J = F(generation − recombination), which the kit's `check()` does for every species |
|
|
965
1024
|
| `charge` | total charge in the device, C/m² (C through a cross-section) |
|
|
966
|
-
| `conservation` | per species stretch: amount, reference, intake through contacts, drift |
|
|
1025
|
+
| `conservation` | per species stretch: amount, reference, intake through contacts, and drift, relative: (amount − reference − intake) over the largest of them |
|
|
967
1026
|
| `warnings` | e.g. unresolved double layers, conventions a statistics model relies on, and for a failed solve, where the system is nearly singular |
|
|
968
1027
|
| `converged`, `iterations`, `steps`, `substeps`, `history`, `time`, `T` | solver bookkeeping, and the temperature |
|
|
969
1028
|
| `steady` | whether this is a converged steady state (from `solve()`), not a transient's: what `check()` in the kit decides its checks by |
|
package/docs/kit.md
CHANGED
|
@@ -57,13 +57,14 @@ console.log(`I = ${sol.current.toFixed(1)} A/m²`);
|
|
|
57
57
|
- **Contacts** are the first and last items. `ohmic(drive, carriers)` holds each carrier in
|
|
58
58
|
equilibrium with a metal at the terminal voltage (default `['e-', 'h+']`, an
|
|
59
59
|
infinite-recombination contact; `['e-']` alone is selective, and is the current collector on
|
|
60
|
-
a metal region). `bath(c, reference, drive, { offset })` is a bath whose terminal voltage is
|
|
60
|
+
a metal region). `bath(c, reference, drive, { offset, material })` is a bath whose terminal voltage is
|
|
61
61
|
the level of its `reference` species, as an electrode reversible to it would read (`offset`
|
|
62
62
|
places that species relative to the terminal voltage, e.g. to read a solution's potentials
|
|
63
63
|
against SHE). `bath(c, drive)`, without a reference, is a bath whose terminal voltage is its
|
|
64
64
|
φ: an ideal salt bridge, the convention of membrane potentials, so that between two such baths
|
|
65
65
|
V_right − V_left is the φ difference a voltage clamp sets. A drive is a voltage, a waveform, or
|
|
66
|
-
`{ V }`, `{ I }`, `{ V, R }`.
|
|
66
|
+
`{ V }`, `{ I }`, `{ V, R }`. A bath against a charged layer (an ion exchanger) gives its own
|
|
67
|
+
`material`, as `bath(c, 0, { material: 'water' })`: a Donnan step then joins them at the contact.
|
|
67
68
|
- **`library`** takes pieces with `species` and `materials`, merged with any given directly
|
|
68
69
|
(`combine()` does the merging on its own). A species or material given twice must be given
|
|
69
70
|
identically.
|
|
@@ -186,14 +187,17 @@ Helpers that write sources as plain data:
|
|
|
186
187
|
- **`photogeneration({ material, flux, alpha, mu, from, to, makes })`**: light absorbed as it goes in
|
|
187
188
|
(Beer–Lambert). A photon flux (mol/(m²·s); a power P at photon energy E is
|
|
188
189
|
$`P/(E N_A)`$) enters the material at x = `from` heading toward +x and generates pairs at
|
|
189
|
-
$`G(x) = \Phi\alpha e^{-\alpha(x - \mathrm{from})}`$ until `to`,
|
|
190
|
+
$`G(x) = \Phi\alpha e^{-\alpha(x - \mathrm{from})}`$ until `to`, and nothing outside (so `from` and
|
|
191
|
+
`to` can pick out one layer of a material that's also elsewhere). It's
|
|
190
192
|
photon ⇌ e⁻ + h⁺ from a photon reservoir at $`\mu`$ = `mu` (J/mol, well above the gap, e.g.
|
|
191
193
|
`units.eV(3)`), with `kf` a profile against x. `makes` replaces the right-hand side: in an
|
|
192
194
|
organic semiconductor, light makes neutral excitons, `makes: 'X'`. The table generates exactly what the light
|
|
193
195
|
loses, $`\Phi(1 - e^{-\alpha(\mathrm{to} - \mathrm{from})})`$, and the solver averages it
|
|
194
196
|
over each node's box, so the total stays right on a grid coarser than $`1/\alpha`$ (where
|
|
195
197
|
carriers start within a cell still needs the grid). Light through several materials is one
|
|
196
|
-
call per material, each with the flux that reaches it.
|
|
198
|
+
call per material, each with the flux that reaches it. To switch the light during a run (a
|
|
199
|
+
pulse, a slider), `set()` the device's `bulkReactions` with a new flux: the state carries over,
|
|
200
|
+
and so does an adaptive transient's step size.
|
|
197
201
|
|
|
198
202
|
Read a transient at points inside the device with `advance(t, { probes })` (see the
|
|
199
203
|
[device reference](device.md#using-a-device)). And `units` converts back out of SI for display:
|
|
@@ -378,15 +382,20 @@ console.log(svg.length); // in a page: element.innerHTML = svg
|
|
|
378
382
|
|
|
379
383
|
`describe(def)` validates a definition and returns a readable summary: each region with its
|
|
380
384
|
doping, Debye length and the grid at its ends, each face's law, alignment, blocked species and
|
|
381
|
-
reactions, the contacts
|
|
382
|
-
|
|
385
|
+
reactions, the contacts (a bath read through a reference ion says where its SHE level sits
|
|
386
|
+
against the terminal voltage: 1.37 V below for 0.5 M Cl⁻ on table $`\mu^\circ`$), bulk reactions, and
|
|
387
|
+
characteristic times (dielectric relaxation, diffusion across each region). It ends with
|
|
388
|
+
warnings:
|
|
383
389
|
|
|
384
390
|
- numbers that look like unit slips (`unitWarnings(def)` gives these alone): a D beyond 1 m²/s
|
|
385
391
|
(cm²/s?), a nonzero $`\mu^\circ`$ under 100 J/mol (eV or volts?), a capacitance over 10 F/m²
|
|
386
392
|
(µF/cm²?), a concentration over 1000 M, lengths over a metre or under an atom, a T under
|
|
387
393
|
200 K (°C?), and so on;
|
|
388
394
|
- double layers the grid won't resolve: end cells coarser than the Debye length (the solution's
|
|
389
|
-
`warnings` check this again against the solved concentrations)
|
|
395
|
+
`warnings` check this again against the solved concentrations);
|
|
396
|
+
- an outside (a bath, a contact's levels) out of equilibrium with a bulk reaction that runs at the
|
|
397
|
+
contact, which would run there for ever: H⁺ and OH⁻ in a bath a little off $`K_w`$ pass a current
|
|
398
|
+
at zero bias.
|
|
390
399
|
|
|
391
400
|
They're heuristics, prompts to check, never errors.
|
|
392
401
|
|
|
@@ -525,18 +534,40 @@ reactions), and enter and leave through the contacts and any metal.
|
|
|
525
534
|
`oneWay`), so the dots crossing each way are Ussing's unidirectional fluxes, as a tracer
|
|
526
535
|
experiment counts them.
|
|
527
536
|
- Concentrations span decades, so one weight can't draw them all. A cell that would hold more
|
|
528
|
-
than `cap` dots is a **sea** instead: drawn as shading, holding no dots, exchanging them with
|
|
537
|
+
than `cap` dots (for each width asked for that it spans: a density) is a **sea** instead: drawn as shading, holding no dots, exchanging them with
|
|
529
538
|
its neighbours as a contact's reservoir does. A junction then shows its majority carriers as
|
|
530
539
|
seas and its minority carriers as dots. Crossings between two seas through a membrane are
|
|
531
540
|
`events`.
|
|
532
|
-
- `
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
541
|
+
- Options: `species` (which to draw; default every species with a concentration), `dots`
|
|
542
|
+
(about how many per species, for the default `weight`), or `weight` itself (mol/m² per dot,
|
|
543
|
+
one for all or `{ name: weight }`; fixed for the swarm's life), `cap` (default ∞: no seas),
|
|
544
|
+
`cells` (a target), `random`.
|
|
545
|
+
- `step(dt)` moves the dots on by dt of the device's time, and returns the swarm. A cell a dot
|
|
546
|
+
would hop out of many times in one step (more than `maxHops`/4, default 200) is a sea for
|
|
547
|
+
that step and is then drawn afresh, so a step far longer than the hop time costs little and
|
|
548
|
+
is still a sample (only its dots' paths are lost). A NaN or negative dt (a first frame, a clock
|
|
549
|
+
that stepped back) moves nothing.
|
|
550
|
+
- `update(sol)` takes a new solution. On a transient's frames, the dots move on at the new rates.
|
|
551
|
+
With `{ resample: true }`, for a jump between steady states only (never a transient's frames,
|
|
552
|
+
whose path it would lose), each cell's dots are thinned or added to by the change in what it
|
|
553
|
+
should hold, which keeps them a Poisson sample while moving as few as can be. A solution on
|
|
554
|
+
another grid (a resized device) redraws the lattice and places the dots afresh.
|
|
555
|
+
- The swarm has `dots` (`{ species, cell, x }`, placed within a cell by its concentration; each
|
|
556
|
+
keeps its object while it lives, so a page can give it fields of its own, a height say),
|
|
557
|
+
`cells` (`{ x0, x1, region }`: grid nodes in runs as near `L/cells` wide as the nodes allow,
|
|
558
|
+
never across a region boundary or into a metal, so the count and widths come out a little off
|
|
559
|
+
what was asked),
|
|
560
|
+
`weight[name]`, `sea[name]` (1 per sea cell), `expected(name)` (dots per cell on average),
|
|
561
|
+
`events` from the last step (`{ species, kind, x, dir }`: `'in'`, `'out'`, `'made'`,
|
|
562
|
+
`'unmade'`, `'cross'`) and `crossed[name]` (`{ up, down }`: dots across boundary k, at
|
|
563
|
+
`cells[k].x0`, toward +x and −x, k = 0 … cells.length, counted until you zero them). A sea
|
|
564
|
+
is a reservoir, not a sample: nothing is counted between two seas (except through a
|
|
565
|
+
permeable face) and nothing made or unmade in one is an event.
|
|
566
|
+
- It's a sample, so its averages are noisy in the usual ways. Successive steps are correlated
|
|
567
|
+
over a hop time, h²/D for a cell h wide, so a cell's count averages to its expectation only
|
|
568
|
+
over many of those. And where the two-way traffic dwarfs the net (a dense species beside a
|
|
569
|
+
sea, a majority carrier anywhere), the net crossing is the small difference of two large
|
|
570
|
+
Poisson counts: its noise is √(up + down), not √(net).
|
|
540
571
|
|
|
541
572
|
```js
|
|
542
573
|
import { Device, GAS_CONSTANT } from 'driftlet';
|