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 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.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): `'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
@@ -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 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.
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, and a steady state
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 (blocked species, reactive moieties) are kept exactly. If a direct solve fails at a
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, 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.
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`. Options: `tol`,
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 an interface, `region` picks the side). That's what a detector at `x` sees.
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). A terminal behind a resistance can't
908
- be the one measured: the resistance belongs to the external circuit. With `profiles: true`, each
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 grid and species
915
- are unchanged, or restarts from the regions' `c0`. A carried state keeps what it holds: a
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`. Either way the time stepping restarts its
919
- order, as after any discontinuity.
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`, 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
 
@@ -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
- - `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).
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';