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 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.14.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
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): `'equilibrium'`, `'blocked'`, or
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. That's mass action with rate constants that depend on the electrical part
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 anchor what nothing else holds. In a 1D MOS capacitor without generation,
578
- inversion electrons can only arrive by minority-carrier diffusion from the back contact (weeks),
579
- so the inversion layer's Fermi level is undetermined to within round-off and no steady state can
580
- be computed. A port holding the electrons beside the oxide, as source and drain would, anchors it,
581
- and the device shows the low-frequency C–V.
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, and a steady state
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 (blocked species, reactive moieties) are kept exactly. If a direct solve fails at a
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, or a device that only stores what comes in, which has no steady state at a current), the
881
- solve fails with a warning giving the range of currents the held voltages passed. A solve that
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`. Options: `tol`,
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 an interface, `region` picks the side). That's what a detector at `x` sees.
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). A terminal behind a resistance can't
908
- be the one measured: the resistance belongs to the external circuit. With `profiles: true`, each
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 } } }` switches it to open circuit. A patch that changes only the
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 grid and species
915
- are unchanged, or restarts from the regions' `c0`. A carried state keeps what it holds: a
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`. Either way the time stepping restarts its
919
- order, as after any discontinuity.
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`, the material's far end. It's
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, bulk reactions, and characteristic times (dielectric relaxation,
382
- diffusion across each region). It ends with warnings:
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
- - `step(dt)` moves the dots on by dt of the device's time. `update(sol)` takes a new solution;
533
- with `{ resample: true }` (a jump between steady states) each cell's dots are thinned or added
534
- to by the change in what it should hold, which keeps them a Poisson sample while moving as few
535
- as can be. Without it (a transient) they move on at the new rates.
536
- - The swarm has `dots` (`{ species, cell, x }`), `cells` (`{ x0, x1, region }`), `sea[name]`
537
- (1 per sea cell), `expected(name)` (dots per cell on average), `events` from the last step
538
- (`{ species, kind, x, dir }`: `'in'`, `'out'`, `'made'`, `'unmade'`, `'cross'`) and
539
- `crossed[name]` (`{ up, down }` at each boundary, counts you zero when you like).
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';