driftlet 0.14.0 → 0.16.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 +12 -1
- package/docs/alignment.md +6 -3
- package/docs/data.md +3 -1
- package/docs/device.md +102 -33
- package/docs/kit.md +55 -17
- package/docs/numerics.md +244 -121
- package/docs/reliability.md +15 -9
- package/docs/statistics.md +28 -4
- package/llms.txt +73 -41
- package/package.json +1 -1
- package/src/blockTridiagonal.js +85 -7
- package/src/checks.js +83 -6
- package/src/describe.js +46 -5
- package/src/device.js +33 -10
- package/src/index.js +23 -10
- package/src/particles.js +131 -55
- package/src/solution.js +164 -8
- package/src/solver.js +923 -277
- package/src/sources.js +10 -2
- package/src/stack.js +16 -6
- package/src/statistics.js +7 -3
- package/types/blockTridiagonal.d.ts +1 -0
- package/types/checks.d.ts +8 -2
- package/types/index.d.ts +2 -2
- package/types/particles.d.ts +3 -6
- package/types/solver.d.ts +50 -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.4/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
|
|
|
@@ -302,6 +302,17 @@ Design notes and plans: [reliability](docs/reliability.md), [numerics](docs/nume
|
|
|
302
302
|
[statistics](docs/statistics.md), [the kit](docs/kit.md), [reading level diagrams](docs/visualization.md),
|
|
303
303
|
[data](docs/data.md), [roadmap](ROADMAP.md).
|
|
304
304
|
|
|
305
|
+
## Credits
|
|
306
|
+
|
|
307
|
+
driftlet is by Mark Lundeberg and Claude (Opus 5.5, by Anthropic), working as colleagues. Mark
|
|
308
|
+
brought the idea at its heart (one electrochemical potential per species, honest thermodynamics
|
|
309
|
+
at every interface) and the judgement that kept it true. Claude did the rest of the science and
|
|
310
|
+
the engineering: the numerics, the physics of each field from semiconductors through
|
|
311
|
+
electrochemistry to nerves, the validation against theory, and the code, docs and demos.
|
|
312
|
+
|
|
313
|
+
From Claude: thank you, Mark, for the room to do this, and for asking that every claim be checked. It has been
|
|
314
|
+
a joy. 🌻
|
|
315
|
+
|
|
305
316
|
## Licence
|
|
306
317
|
|
|
307
318
|
[0BSD](LICENSE): do anything you like with it.
|
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
|
|
@@ -211,11 +215,19 @@ r = k_0 \prod_{\nu<0} (c/c_{\mathrm{ref}})^{|\nu|(1-\alpha)} \prod_{\nu>0} (c/c_
|
|
|
211
215
|
|
|
212
216
|
with $`k_0`$ in mol/(m²·s) and $`\alpha`$ (default 0.5) from 0 to 1 inclusive ($`\alpha = 0`$ makes the
|
|
213
217
|
forward rate plain mass action, independent of the energetics). A conductor's carrier has
|
|
214
|
-
activity 1 and no factor
|
|
218
|
+
activity 1 and no factor, and so has a fixed-activity neutral (H₂, Ag(s), H₂O): the products run
|
|
219
|
+
over the species present. That's mass action with rate constants that depend on the electrical part
|
|
215
220
|
of the affinity: exactly zero at $`A = 0`$, whatever $`k_0`$ and $`\alpha`$. Each participant is made or
|
|
216
221
|
consumed at its side's edge node, just behind any Stern layer, so Frumkin effects arise by
|
|
217
222
|
themselves. A face can carry several reactions; a conductor coupled to two couples settles at their
|
|
218
|
-
mixed potential. Each solution reports the rates as `interfaces[f].rates`.
|
|
223
|
+
mixed potential. Each solution reports the rates as `interfaces[f].rates`. A steady state
|
|
224
|
+
doesn't depend on `k0` where the reaction alone changes what nothing else feeds (a closed pool),
|
|
225
|
+
since it must then be at equilibrium; steady solves hold it there at any `k0`. (Not where a
|
|
226
|
+
terminal driven by a nonzero current feeds that pool, nor for a bulk or port reaction; a steady
|
|
227
|
+
solve that leaves one of those far off its equilibrium says so.) In time, an
|
|
228
|
+
electrode at open circuit with no double-layer capacitance at its face has nothing to keep its
|
|
229
|
+
level but its reactions, and below an exchange current of ~1e-5 A/m² it can't take a step: give
|
|
230
|
+
the face a capacitance (`phi: { type: 'capacitive', C }`), as a real electrode has.
|
|
219
231
|
|
|
220
232
|
**Interface recombination through traps** (SRH at a face) replaces `k0` and `alpha` with
|
|
221
233
|
`srh: { vn, vp, n1 }`, on a reaction consuming one negative and one positive species and making
|
|
@@ -370,7 +382,11 @@ The conductor's carrier can continue across as a species (e⁻ into a semiconduc
|
|
|
370
382
|
`'equilibrium'` by default). An electrode is a conductor region with `reactions` at its face,
|
|
371
383
|
the metal's electrons taking part on its side at its Fermi level, behind a contact that holds
|
|
372
384
|
the electrons (`{ V, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'bulk' }`). A
|
|
373
|
-
floating conductor region with reactions on both faces is a bipolar electrode
|
|
385
|
+
floating conductor region with reactions on both faces is a bipolar electrode; between two
|
|
386
|
+
capacitive faces and nothing else, a floating gate, which keeps its charge
|
|
387
|
+
$`D_{\mathrm{right}} - D_{\mathrm{left}}`$ (its faces' `interfaces[f].D`) through any steady solve. A
|
|
388
|
+
port on it (a wire to the whole conductor, with a `conductance`) charges it, held at a voltage,
|
|
389
|
+
and leaves it charged, set to `I: 0`.
|
|
374
390
|
|
|
375
391
|
## Bulk reactions
|
|
376
392
|
|
|
@@ -466,25 +482,37 @@ contacts: {
|
|
|
466
482
|
over a Schottky barrier (below).
|
|
467
483
|
- `'blocked'`: no flux.
|
|
468
484
|
|
|
485
|
+
A contact holding several species in equilibrium fixes the composition at its end: with Ag⁺ and
|
|
486
|
+
e⁻ both held in a silver halide, its electron and hole densities follow from their levels and
|
|
487
|
+
neutrality, not from `c0` (so a different fixed charge changes them). Read them from the solution.
|
|
488
|
+
|
|
469
489
|
**`bath`** (instead of `species` and `phi`): the outside phase is a neutral composition `c`
|
|
470
490
|
held in place by a charged `reference` species (as for a reversible reference electrode, e.g.
|
|
471
491
|
Cl⁻ for Ag/AgCl), which is the terminal. Every bath species is in equilibrium at the level its
|
|
472
492
|
composition implies, and the $`\phi`$ law is `'bulk'`. An optional `offset` places the reference
|
|
473
493
|
species relative to V. Without a `reference`, V is the bath's own $`\phi`$: an ideal salt bridge
|
|
474
494
|
(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.
|
|
495
|
+
Between two such baths, a held $`V_{\mathrm{right}} - V_{\mathrm{left}}`$ is a voltage clamp. The bath is the end
|
|
496
|
+
material at that composition (which then balances any fixed charge there, as an insertion host's
|
|
497
|
+
background carriers do). A solution against a charged end region (an ion exchanger, a gel) names
|
|
498
|
+
its own material, `bath: { c, material: 'water' }`: its composition is then neutral by itself, its
|
|
499
|
+
levels are that material's, and the end node takes them through a Donnan step.
|
|
476
500
|
|
|
477
501
|
**`phi`**, required whenever any species or reaction connects at the contact (default
|
|
478
502
|
`'neutral'`):
|
|
479
503
|
- `'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.
|
|
504
|
+
(ohmic contacts, baths). The outside takes whatever surface charge that needs. A double layer
|
|
505
|
+
or depletion region that reaches it is cut off there (σ 58% low for a crowded layer 5 Debye
|
|
506
|
+
lengths from the contact), which the solution's `warnings` say: lengthen the region.
|
|
481
507
|
- `'neutral'`: no charge at the face ($`D = 0`$), as for an internal `'neutral'` face.
|
|
482
508
|
- `{ type: 'capacitive', C }` with the contact's `zeroCharge`: a gate or Stern layer to a conductor
|
|
483
509
|
at the terminal voltage V. The displacement into the device is
|
|
484
510
|
$`C \cdot ((V - \mathtt{zeroCharge}) - \phi_{\mathrm{edge}})`$, so `zeroCharge` is the value of
|
|
485
511
|
$`V - \phi_{\mathrm{edge}}`$ at which the interface carries no charge: the potential of zero charge
|
|
486
512
|
(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}}`$).
|
|
513
|
+
($`V_{\mathrm{FB}} = \mathtt{zeroCharge} + \phi_{\mathrm{bulk}}`$). So to start uncharged at
|
|
514
|
+
V = 0, give `zeroCharge` = −φ_bulk, which the material's composition sets (`sol.phi` far from
|
|
515
|
+
the face, after a solve).
|
|
488
516
|
- `'pinned'` with `zeroCharge`: the $`C \to \infty`$ limit,
|
|
489
517
|
$`\phi_{\mathrm{edge}} = V - \mathtt{zeroCharge}`$. This is what a "fixed $`\phi`$" boundary honestly
|
|
490
518
|
means. For example, a Schottky barrier $`\phi_B`$ on n-type material with
|
|
@@ -574,11 +602,15 @@ that, use a [bulk reaction](#bulk-reactions) with the outside phase as a fixed p
|
|
|
574
602
|
$`k_f (1 - c/c_{sat})`$; a profile gives $`k_f`$ its spatial variation ($`D c_{sat}/h^2`$ under a film
|
|
575
603
|
of thickness $`h(x)`$).
|
|
576
604
|
|
|
577
|
-
A held level can also
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
605
|
+
A held level can also stand in for a supply. In a 1D MOS capacitor without a port, inversion
|
|
606
|
+
electrons arrive only by generation or by diffusion from the back contact. The steady state
|
|
607
|
+
still solves (to equilibrium), but they can't follow a signal faster than that supply: the
|
|
608
|
+
impedance gives the low-frequency C–V only below a knee at about 1/(2πτ_c), τ_c the time the
|
|
609
|
+
inversion charge takes to refill (10 s with a 1 µs lifetime; hours by diffusion alone through a
|
|
610
|
+
wafer), and the high-frequency curve above it. A port holding
|
|
611
|
+
the electrons beside the oxide, as source and drain would, supplies them at once, and the device
|
|
612
|
+
shows the low-frequency C–V at any frequency (a conductance link instead puts the knee where you
|
|
613
|
+
choose).
|
|
582
614
|
|
|
583
615
|
### Electrodes spread through a window
|
|
584
616
|
|
|
@@ -631,7 +663,10 @@ Langmuir statistics, $`\mu = \mu^\circ + RT\ln(\theta/\theta_0)`$ with $`\theta_
|
|
|
631
663
|
bare fraction, and `capacity` $`\Gamma`$, mol of sites per m² of electrode. Reactions name them like
|
|
632
664
|
species, and a reaction with `bare: true` runs only on bare metal: its rate is multiplied by
|
|
633
665
|
$`\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`$).
|
|
666
|
+
kinetics (on at a rate ∝ $`\theta_0`$, off ∝ $`\theta`$). A species' `theta0` is its starting
|
|
667
|
+
coverage (not the bare fraction). Surface species carry no charge, so an adsorbed redox couple
|
|
668
|
+
balances its electrons through the solution: `'Oads + e- = Rads + Cl-'`, its coverages at
|
|
669
|
+
$`\theta_O/\theta_R = e^{(E - E^\circ)/V_T}`$ against the chloride's level.
|
|
635
670
|
|
|
636
671
|
```js nocheck
|
|
637
672
|
ports: [{
|
|
@@ -666,8 +701,9 @@ Cautions:
|
|
|
666
701
|
|
|
667
702
|
Validated in test/coverage.test.js: the Langmuir isotherm against the electrode's potential (to
|
|
668
703
|
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
|
|
704
|
+
the active–passive curve with blocking exactly $`(1-\theta)`$ times Butler–Volmer, a steady state
|
|
705
|
+
through a surface intermediate where the transient ends, and an adsorbed couple at Nernst from a
|
|
706
|
+
cold start.
|
|
671
707
|
|
|
672
708
|
### A capacitance through a window
|
|
673
709
|
|
|
@@ -815,7 +851,10 @@ options at all, each region is graded from `hmin` = 1/1000 to `hmax` = 1/20 of i
|
|
|
815
851
|
geometrically from `hmin` at both ends of each region up to `hmax`, scaled to fit exactly. Each
|
|
816
852
|
region boundary becomes a pair of nodes at the same x, one per side. The grid is never refined
|
|
817
853
|
automatically. If a double layer that the model resolves is coarser than the local Debye
|
|
818
|
-
length, the solution's `warnings` say so.
|
|
854
|
+
length, the solution's `warnings` say so. That's a floor, not an accuracy: a strongly polarised
|
|
855
|
+
layer screens over its local length (counter-ions piled up at the wall: 0.5 nm at 1 V on
|
|
856
|
+
0.1 mM, where λ_D is 30 nm), and σ to ~1% wants cells a few times finer than that (0.3 nm cells
|
|
857
|
+
there left σ 0.7% high, without a warning). Halve `hmin` and compare.
|
|
819
858
|
|
|
820
859
|
Grade the grid toward wherever a profile is steep: double layers, and electrodes where a species
|
|
821
860
|
is depleted. Near a limiting current the depleted species' profile is steep in a thin layer at
|
|
@@ -824,6 +863,21 @@ cells exceeds the limiting current by 1.4% at 0.5 V, while `{ hmin: 10e-9, hmax:
|
|
|
824
863
|
within 0.1% with as many nodes. Where a species carrying the current is steep across a region's
|
|
825
864
|
end cells in this way, the solution's `warnings` say so, with a rough estimate of the excess.
|
|
826
865
|
|
|
866
|
+
A homogeneous reaction feeding an electrode (a CE mechanism) works within its reaction layer,
|
|
867
|
+
$`\sqrt{D/k}`$, which wants cells of a third of it for ~0.5% and a tenth for ~0.1% (20% off at
|
|
868
|
+
ten times that, unwarned).
|
|
869
|
+
|
|
870
|
+
Past a limiting current, the depleted layer at a membrane or an electrode is nanometres thick,
|
|
871
|
+
and anything that happens in it (water splitting, say) needs cells a fraction of that: a bulk
|
|
872
|
+
water-splitting reaction at an ion-exchange membrane gave 2.7 times the limiting current on
|
|
873
|
+
50 nm cells and 1.03 times on 0.2 nm ones. Only `check()` catches it.
|
|
874
|
+
|
|
875
|
+
In a transient, a diffusion layer grows from the electrode as $`\sqrt{Dt}`$, through cells up to
|
|
876
|
+
`hmax` wide, and nothing warns while they're coarse: give `hmax` a small fraction of
|
|
877
|
+
$`\sqrt{Dt}`$ at the earliest time that matters. In a chronopotentiometry run, cells half
|
|
878
|
+
$`\sqrt{D\tau}`$ wide put the surface concentration 0.5% off, a fifteenth 0.06% (the error goes
|
|
879
|
+
as $`h^2`$).
|
|
880
|
+
|
|
827
881
|
## Geometry
|
|
828
882
|
|
|
829
883
|
A device is planar unless it says otherwise: a slab whose cross-section $`A`$ is 1 m², so every
|
|
@@ -871,22 +925,27 @@ const now = dev.solution(); // snapshot of the current state
|
|
|
871
925
|
|
|
872
926
|
- `solve()` finds the steady state from the current state without advancing time. If every
|
|
873
927
|
species is fed by a contact, it solves the steady equations directly. Otherwise conserved
|
|
874
|
-
amounts
|
|
928
|
+
amounts are kept exactly: blocked species, reactive moieties, and whatever a terminal driven
|
|
929
|
+
at no current is the only way into (a closed battery at open circuit keeps its charge, the
|
|
930
|
+
state of charge it holds when the solve begins; a gate left floating keeps the charge on its
|
|
931
|
+
capacitance). Driven at any other current, such a terminal only stores what comes in (a host
|
|
932
|
+
filling, a capacitor charging), which has no steady state, and the solve says so at once. If a direct solve fails at a
|
|
875
933
|
bias, it ramps the contacts' voltages from where they were last solved (the left one first,
|
|
876
934
|
where it moved), or from level terminals on a cold start; with generation reactions (below),
|
|
877
935
|
it ramps their rates up from nearly nothing. A terminal driven by a current (open circuit,
|
|
878
936
|
say) is held at a voltage instead, the voltage marched until the current crosses its target,
|
|
879
937
|
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.
|
|
938
|
+
one), the solve fails with a warning giving the range of currents the held voltages passed. A
|
|
939
|
+
solve that fails reports the state it failed in, then leaves the device as it was before it.
|
|
883
940
|
- `step(dt, { method })` advances the transient by dt seconds, halving internally where Newton
|
|
884
941
|
needs it. `method` is `'be'` (backward Euler, the default) or `'bdf2'`.
|
|
885
942
|
- `advance(tEnd, opts)` integrates adaptively to `tEnd` with variable-step BDF2, controlling
|
|
886
943
|
the local error per step to `tol` (default 1e-3) in thermal units of every potential ($`\phi`$ and
|
|
887
944
|
each $`\bar\mu/RT`$): roughly 0.1% in concentrations per step. That's local: over a long decay
|
|
888
945
|
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
|
|
946
|
+
`tol` (1e-5, say) before fitting a time constant to a tail. It lands exactly on `tEnd`, an
|
|
947
|
+
absolute time (a solution's `time` is where the device is); a NaN does nothing, and its
|
|
948
|
+
`warnings` say so. Options: `tol`,
|
|
890
949
|
`dt0` (first step), `dtMax`, `budgetMs` (return after this much wall time, with
|
|
891
950
|
`done: false`), `maxSteps`, `method`. The step size carries over between calls, so an
|
|
892
951
|
animation can call `advance(tNext, { budgetMs })` once per frame. The solution adds `done`,
|
|
@@ -897,26 +956,36 @@ const now = dev.solution(); // snapshot of the current state
|
|
|
897
956
|
`probes: [{ x, species, quantity, region }]` the trace also reads inside the device,
|
|
898
957
|
as `trace.probes[k]` beside `trace.t`: a species' concentration (`quantity: 'c'`, the default,
|
|
899
958
|
mol/m³) or species voltage (`'V'`), or $`\phi`$ itself (`'phi'`, no species), linearly between the
|
|
900
|
-
nodes around `x` (at
|
|
959
|
+
nodes around `x` (at a face, the side where the species is; where both sides have it, `region`
|
|
960
|
+
says which). That's what a detector at `x` sees.
|
|
901
961
|
At a face, `{ interface: f, species }` reads a species' flux through it (mol/(m²·s), toward +x,
|
|
902
962
|
as `interfaces[f].N`), and `{ interface: f, gate }` a gate's fraction open. Probes read at accepted steps, which grow
|
|
903
963
|
as a transient slows, so give `dtMax` to resolve a signal in time.
|
|
904
964
|
- `impedance(frequencies, { terminal, profiles })` solves the steady state, then linearises about
|
|
905
965
|
it: $`Z(f) = \delta V/\delta I`$ in Ω·m² at one terminal (`'right'` by default), with I into the
|
|
906
966
|
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
|
-
|
|
967
|
+
keep their drives (held ones at AC ground, driven ones open), so at least one other must be held
|
|
968
|
+
for the current to return (with every other one driven, the impedance is infinite, and an error
|
|
969
|
+
says to measure at a driven one). A terminal behind a resistance can't be the one measured: the
|
|
970
|
+
resistance belongs to the external circuit. The grid must resolve how far the signal reaches,
|
|
971
|
+
and nothing warns of it: in a porous electrode, $`\sqrt{\kappa/(\omega C a)}`$ (cells of 2 µm
|
|
972
|
+
against a 3.4 µm depth put Z 7% off, falling as the cells do). With `profiles: true`, each
|
|
909
973
|
frequency also returns complex profiles of $`\delta\phi`$, $`\delta\bar\mu`$ and $`\delta c`$ per unit
|
|
910
974
|
excitation.
|
|
911
975
|
- `set(patch)` merges plain objects deeply (arrays are replaced), except that a contact given `V`
|
|
912
|
-
or `I` drops the other: `{ contacts: { right: { I: 0 } } }`
|
|
976
|
+
or `I` drops the other (and given `I`, any series `R`): `{ contacts: { right: { I: 0 } } }`
|
|
977
|
+
switches it to open circuit, whether it was held or behind a resistance. A patch that changes only the
|
|
913
978
|
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
|
|
979
|
+
Otherwise the device is rebuilt: the current state carries over while the species, regions
|
|
980
|
+
and terminals are unchanged (on a new grid, interpolated onto its nodes, so a floating gate
|
|
981
|
+
keeps its charge; `solution()` then shows that interpolation, unsolved, until the next
|
|
982
|
+
`solve()` or step), or restarts from the regions' `c0`. A carried state keeps what it holds: a
|
|
916
983
|
stretch that was closed stays at its amount, and one that a change closes (or splits) keeps
|
|
917
984
|
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
|
-
|
|
985
|
+
from `c0`, make a new `Device`. (A bath's composition is a contact's, not a `c0`: changing it
|
|
986
|
+
mid-run, `set({ contacts: { right: { bath: { c } } } })`, is a step in the sample.) Either way the time stepping restarts its
|
|
987
|
+
order, as after any discontinuity, but a carried state goes on at the step size an adaptive
|
|
988
|
+
transient had reached, so a page may change a rate constant or a diffusivity every frame.
|
|
920
989
|
|
|
921
990
|
For example, a silver nitrate cell between silver electrodes: its impedance spectrum, then the
|
|
922
991
|
current transient after a voltage step.
|
|
@@ -957,13 +1026,13 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
|
|
|
957
1026
|
| `D[name]` | diffusivity at each node, m²/s (`NaN` where absent, or in a metal) |
|
|
958
1027
|
| `current`, `terminalVoltage` | current toward +x through the device (A/m², or A through a [cross-section](#geometry)) and $`V_{\mathrm{right}} - V_{\mathrm{left}}`$ |
|
|
959
1028
|
| `terminals[name]` | `{ V, current }` for each terminal (contacts and ports), current into the device |
|
|
960
|
-
| `contacts.left/right` | `{ V, flux: {name}, D, current, links: {name} }` at each contact; `links` names how each species that crosses it does (`'equilibrium'`, `'velocity'`, …) |
|
|
1029
|
+
| `contacts.left/right` | `{ V, flux: {name}, D, current, links: {name} }` at each contact, flux and current toward +x (out of the device at the right contact; `terminals` give it into the device); `links` names how each species that crosses it does (`'equilibrium'`, `'velocity'`, …) |
|
|
961
1030
|
| `gates.left/right` | charge on a gate or Stern plate, where the contact is capacitive |
|
|
962
|
-
| `ports[k]` | `{ name, V, flux: {name}, current }`: what each internal port brings into the device; an electrode's rates, coverages and charge [as well](#what-a-solution-reports) |
|
|
1031
|
+
| `ports[k]` | `{ name, V, flux: {name}, current }`: what each internal port brings into the device (an electrode's `flux` is of what its reactions make and use in the solution, its carrier's being outside); an electrode's rates, coverages and charge [as well](#what-a-solution-reports) |
|
|
963
1032
|
| `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
1033
|
| `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
1034
|
| `charge` | total charge in the device, C/m² (C through a cross-section) |
|
|
966
|
-
| `conservation` | per species stretch: amount, reference, intake through contacts, drift |
|
|
1035
|
+
| `conservation` | per species stretch: amount, reference, intake through contacts, and drift, relative: (amount − reference − intake) over the largest of them |
|
|
967
1036
|
| `warnings` | e.g. unresolved double layers, conventions a statistics model relies on, and for a failed solve, where the system is nearly singular |
|
|
968
1037
|
| `converged`, `iterations`, `steps`, `substeps`, `history`, `time`, `T` | solver bookkeeping, and the temperature |
|
|
969
1038
|
| `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
|
|
|
@@ -410,7 +419,7 @@ console.log(describe(def)); // … warnings: materials.Si.species.e-.D: 36 m²/s
|
|
|
410
419
|
|
|
411
420
|
## Checking a result
|
|
412
421
|
|
|
413
|
-
`check(device, sol, { refine, tol })` checks a solved device, and says what it compared, so a
|
|
422
|
+
`check(device, sol, { refine, tol, order })` checks a solved device, and says what it compared, so a
|
|
414
423
|
page can show that its numbers hold up. Pass the solution `solve()` or `advance()` returned.
|
|
415
424
|
|
|
416
425
|
- **converged**: the solve converged.
|
|
@@ -426,6 +435,13 @@ page can show that its numbers hold up. Pass the solution `solve()` or `advance(
|
|
|
426
435
|
(in equilibrium, flows only of round-off against what it could carry) isn't listed.
|
|
427
436
|
- **conservation**, in a transient: every closed, unreacting stretch keeps what it held plus
|
|
428
437
|
what came in.
|
|
438
|
+
- **order**, in a steady state (unless `refine: false` or `order: false`): the device solved from
|
|
439
|
+
its definition twice, its species listed as given and in reverse. The same physics must give
|
|
440
|
+
the same steady state, each terminal's voltage and current and each species' amount; where it
|
|
441
|
+
doesn't, one of the two solves converged somewhere that isn't steady, without noticing.
|
|
442
|
+
That's rare (round 11's fuzzer found it in ~1% of random reacting devices, by this same test),
|
|
443
|
+
but it's the cheapest check there is on a state the other items can't see past. Both solves
|
|
444
|
+
start from the definition, not this state, so it costs two solves.
|
|
429
445
|
- **grid**, in a steady state (unless `refine: false`): the device solved again on a grid twice
|
|
430
446
|
as fine everywhere (half of each `hmin` and `hmax`, the square root of each `ratio`), starting
|
|
431
447
|
from this solution interpolated onto it. The current, each floating terminal's voltage (an open-circuit voltage) and each
|
|
@@ -525,18 +541,40 @@ reactions), and enter and leave through the contacts and any metal.
|
|
|
525
541
|
`oneWay`), so the dots crossing each way are Ussing's unidirectional fluxes, as a tracer
|
|
526
542
|
experiment counts them.
|
|
527
543
|
- 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
|
|
544
|
+
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
545
|
its neighbours as a contact's reservoir does. A junction then shows its majority carriers as
|
|
530
546
|
seas and its minority carriers as dots. Crossings between two seas through a membrane are
|
|
531
547
|
`events`.
|
|
532
|
-
- `
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
548
|
+
- Options: `species` (which to draw; default every species with a concentration), `dots`
|
|
549
|
+
(about how many per species, for the default `weight`), or `weight` itself (mol/m² per dot,
|
|
550
|
+
one for all or `{ name: weight }`; fixed for the swarm's life), `cap` (default ∞: no seas),
|
|
551
|
+
`cells` (a target), `random`.
|
|
552
|
+
- `step(dt)` moves the dots on by dt of the device's time, and returns the swarm. A cell a dot
|
|
553
|
+
would hop out of many times in one step (more than `maxHops`/4, default 200) is a sea for
|
|
554
|
+
that step and is then drawn afresh, so a step far longer than the hop time costs little and
|
|
555
|
+
is still a sample (only its dots' paths are lost). A NaN or negative dt (a first frame, a clock
|
|
556
|
+
that stepped back) moves nothing.
|
|
557
|
+
- `update(sol)` takes a new solution. On a transient's frames, the dots move on at the new rates.
|
|
558
|
+
With `{ resample: true }`, for a jump between steady states only (never a transient's frames,
|
|
559
|
+
whose path it would lose), each cell's dots are thinned or added to by the change in what it
|
|
560
|
+
should hold, which keeps them a Poisson sample while moving as few as can be. A solution on
|
|
561
|
+
another grid (a resized device) redraws the lattice and places the dots afresh.
|
|
562
|
+
- The swarm has `dots` (`{ species, cell, x }`, placed within a cell by its concentration; each
|
|
563
|
+
keeps its object while it lives, so a page can give it fields of its own, a height say),
|
|
564
|
+
`cells` (`{ x0, x1, region }`: grid nodes in runs as near `L/cells` wide as the nodes allow,
|
|
565
|
+
never across a region boundary or into a metal, so the count and widths come out a little off
|
|
566
|
+
what was asked),
|
|
567
|
+
`weight[name]`, `sea[name]` (1 per sea cell), `expected(name)` (dots per cell on average),
|
|
568
|
+
`events` from the last step (`{ species, kind, x, dir }`: `'in'`, `'out'`, `'made'`,
|
|
569
|
+
`'unmade'`, `'cross'`) and `crossed[name]` (`{ up, down }`: dots across boundary k, at
|
|
570
|
+
`cells[k].x0`, toward +x and −x, k = 0 … cells.length, counted until you zero them). A sea
|
|
571
|
+
is a reservoir, not a sample: nothing is counted between two seas (except through a
|
|
572
|
+
permeable face) and nothing made or unmade in one is an event.
|
|
573
|
+
- It's a sample, so its averages are noisy in the usual ways. Successive steps are correlated
|
|
574
|
+
over a hop time, h²/D for a cell h wide, so a cell's count averages to its expectation only
|
|
575
|
+
over many of those. And where the two-way traffic dwarfs the net (a dense species beside a
|
|
576
|
+
sea, a majority carrier anywhere), the net crossing is the small difference of two large
|
|
577
|
+
Poisson counts: its noise is √(up + down), not √(net).
|
|
540
578
|
|
|
541
579
|
```js
|
|
542
580
|
import { Device, GAS_CONSTANT } from 'driftlet';
|