driftlet 0.16.2 → 1.0.0
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 +14 -3
- package/docs/device.md +29 -11
- package/docs/kit.md +10 -1
- package/docs/numerics.md +68 -12
- package/docs/statistics.md +2 -1
- package/docs/templates.md +260 -0
- package/llms.txt +107 -299
- package/package.json +15 -3
- package/src/blockTridiagonal.js +73 -1
- package/src/channels.js +6 -4
- package/src/checks.js +78 -3
- package/src/data.js +5 -1
- package/src/device.js +1 -1
- package/src/devices.js +2 -0
- package/src/equation.js +2 -1
- package/src/errors.js +33 -0
- package/src/grid.js +12 -0
- package/src/index.js +21 -10
- package/src/junction.js +3 -1
- package/src/kit.js +1 -1
- package/src/live.js +2 -0
- package/src/particles.js +2 -0
- package/src/plot.js +4 -0
- package/src/polarization.js +2 -1
- package/src/recorder.js +2 -1
- package/src/solution.js +39 -5
- package/src/solver.js +248 -30
- package/src/sources.js +8 -1
- package/src/stack.js +14 -1
- package/src/statistics.js +1 -1
- package/src/traces.js +3 -0
- package/src/types.js +3 -1
- package/types/blockTridiagonal.d.ts +1 -0
- package/types/channels.d.ts +3 -3
- package/types/checks.d.ts +9 -3
- package/types/data.d.ts +3 -3
- package/types/devices.d.ts +1 -1
- package/types/equation.d.ts +1 -1
- package/types/errors.d.ts +8 -0
- package/types/index.d.ts +4 -7
- package/types/junction.d.ts +2 -2
- package/types/kit.d.ts +1 -1
- package/types/live.d.ts +1 -1
- package/types/particles.d.ts +1 -1
- package/types/plot.d.ts +3 -3
- package/types/polarization.d.ts +1 -1
- package/types/recorder.d.ts +1 -1
- package/types/solver.d.ts +8 -1
- package/types/sources.d.ts +7 -7
- package/types/traces.d.ts +1 -1
- package/types/types.d.ts +9 -1
package/README.md
CHANGED
|
@@ -40,8 +40,8 @@ cells, electrodes) put through cold solves, sweeps, transients and impedance, ev
|
|
|
40
40
|
checked. A solve that fails reports it, with what went wrong where it can tell.
|
|
41
41
|
[How far to trust it](docs/reliability.md) has the details, the numbers and the known limits.
|
|
42
42
|
|
|
43
|
-
**Status: 0
|
|
44
|
-
|
|
43
|
+
**Status: 1.0.** The API is stable: a change that breaks working code comes only with a major
|
|
44
|
+
version.
|
|
45
45
|
|
|
46
46
|
## Install
|
|
47
47
|
|
|
@@ -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@1.0.0/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/device.md
CHANGED
|
@@ -173,7 +173,12 @@ defaults to no dipole.
|
|
|
173
173
|
in F/m². Beside a strictly neutral ($`\varepsilon = 0`$) material, the face's charge is held in
|
|
174
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
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).
|
|
176
|
+
than 2%, and how coarse is enough). A face reaction there sees that cell's composition, which
|
|
177
|
+
holding the charge shifts by about $`z\sigma F/(C_{\mathrm{cell}} RT)`$ for an ion of charge $`z`$: a
|
|
178
|
+
Frumkin effect set by the mesh, growing without limit as the cell shrinks (an electrode's DC
|
|
179
|
+
resistance in ferri/ferrocyanide fell from 56.6 to 47.5 Ω·cm² between 300 nm and 1 nm). A
|
|
180
|
+
warning says past 1%; a `zeroCharge` near the face's rest potential, so it holds little charge,
|
|
181
|
+
avoids it on any grid. A sub-grid Gouy–Chapman law is on the [roadmap](../ROADMAP.md).
|
|
177
182
|
|
|
178
183
|
**Species laws** `species` (default: local equilibrium, $`\bar\mu`$ continuous, where the species is
|
|
179
184
|
present on both sides and can move on one at least; blocked otherwise, so trap states with
|
|
@@ -215,11 +220,19 @@ r = k_0 \prod_{\nu<0} (c/c_{\mathrm{ref}})^{|\nu|(1-\alpha)} \prod_{\nu>0} (c/c_
|
|
|
215
220
|
|
|
216
221
|
with $`k_0`$ in mol/(m²·s) and $`\alpha`$ (default 0.5) from 0 to 1 inclusive ($`\alpha = 0`$ makes the
|
|
217
222
|
forward rate plain mass action, independent of the energetics). A conductor's carrier has
|
|
218
|
-
activity 1 and no factor
|
|
223
|
+
activity 1 and no factor, and so has a fixed-activity neutral (H₂, Ag(s), H₂O): the products run
|
|
224
|
+
over the species present. That's mass action with rate constants that depend on the electrical part
|
|
219
225
|
of the affinity: exactly zero at $`A = 0`$, whatever $`k_0`$ and $`\alpha`$. Each participant is made or
|
|
220
226
|
consumed at its side's edge node, just behind any Stern layer, so Frumkin effects arise by
|
|
221
227
|
themselves. A face can carry several reactions; a conductor coupled to two couples settles at their
|
|
222
|
-
mixed potential. Each solution reports the rates as `interfaces[f].rates`.
|
|
228
|
+
mixed potential. Each solution reports the rates as `interfaces[f].rates`. A steady state
|
|
229
|
+
doesn't depend on `k0` where the reaction alone changes what nothing else feeds (a closed pool),
|
|
230
|
+
since it must then be at equilibrium; steady solves hold it there at any `k0`. (Not where a
|
|
231
|
+
terminal driven by a nonzero current feeds that pool, nor for a bulk or port reaction; a steady
|
|
232
|
+
solve that leaves one of those far off its equilibrium says so.) In time, an
|
|
233
|
+
electrode at open circuit with no double-layer capacitance at its face has nothing to keep its
|
|
234
|
+
level but its reactions, and below an exchange current of ~1e-5 A/m² it can't take a step: give
|
|
235
|
+
the face a capacitance (`phi: { type: 'capacitive', C }`), as a real electrode has.
|
|
223
236
|
|
|
224
237
|
**Interface recombination through traps** (SRH at a face) replaces `k0` and `alpha` with
|
|
225
238
|
`srh: { vn, vp, n1 }`, on a reaction consuming one negative and one positive species and making
|
|
@@ -961,15 +974,20 @@ const now = dev.solution(); // snapshot of the current state
|
|
|
961
974
|
says to measure at a driven one). A terminal behind a resistance can't be the one measured: the
|
|
962
975
|
resistance belongs to the external circuit. The grid must resolve how far the signal reaches,
|
|
963
976
|
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
|
|
965
|
-
|
|
966
|
-
|
|
977
|
+
against a 3.4 µm depth put Z 7% off, falling as the cells do; at an electrode, the diffusion
|
|
978
|
+
length $`\sqrt{2D/\omega}`$ at the highest frequency). The result is `{ f, Z: { re, im },
|
|
979
|
+
warnings }`, the warnings being the steady state's. With `profiles: true`, `profiles[k]` holds
|
|
980
|
+
frequency k's complex profiles, `{ phi, mu: {name}, c: {name} }`, each `{ re, im }` at the
|
|
981
|
+
nodes (`NaN` where a quantity doesn't exist), per unit excitation: a volt, or an A/m² where the
|
|
982
|
+
terminal is driven by a current.
|
|
967
983
|
- `set(patch)` merges plain objects deeply (arrays are replaced), except that a contact given `V`
|
|
968
|
-
or `I` drops the other: `{ contacts: { right: { I: 0 } } }`
|
|
984
|
+
or `I` drops the other (and given `I`, any series `R`): `{ contacts: { right: { I: 0 } } }`
|
|
985
|
+
switches it to open circuit, whether it was held or behind a resistance. A patch that changes only the
|
|
969
986
|
terminals' drives (`V`, `I`, `R`) updates them in place, cheaply, keeping everything else.
|
|
970
987
|
Otherwise the device is rebuilt: the current state carries over while the species, regions
|
|
971
988
|
and terminals are unchanged (on a new grid, interpolated onto its nodes, so a floating gate
|
|
972
|
-
keeps its charge
|
|
989
|
+
keeps its charge; `solution()` then shows that interpolation, unsolved, until the next
|
|
990
|
+
`solve()` or step), or restarts from the regions' `c0`. A carried state keeps what it holds: a
|
|
973
991
|
stretch that was closed stays at its amount, and one that a change closes (or splits) keeps
|
|
974
992
|
what it holds at that moment. So a new `c0` doesn't apply to a carried state; to start over
|
|
975
993
|
from `c0`, make a new `Device`. (A bath's composition is a contact's, not a `c0`: changing it
|
|
@@ -1016,9 +1034,9 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
|
|
|
1016
1034
|
| `D[name]` | diffusivity at each node, m²/s (`NaN` where absent, or in a metal) |
|
|
1017
1035
|
| `current`, `terminalVoltage` | current toward +x through the device (A/m², or A through a [cross-section](#geometry)) and $`V_{\mathrm{right}} - V_{\mathrm{left}}`$ |
|
|
1018
1036
|
| `terminals[name]` | `{ V, current }` for each terminal (contacts and ports), current into the device |
|
|
1019
|
-
| `contacts.left/right` | `{ V, flux: {name}, D, current, links: {name} }` at each contact; `links` names how each species that crosses it does (`'equilibrium'`, `'velocity'`, …) |
|
|
1020
|
-
| `
|
|
1021
|
-
| `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) |
|
|
1037
|
+
| `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'`, …) |
|
|
1038
|
+
| `plates.left/right` | charge on a gate or Stern plate, where the contact is capacitive |
|
|
1039
|
+
| `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) |
|
|
1022
1040
|
| `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) |
|
|
1023
1041
|
| `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 |
|
|
1024
1042
|
| `charge` | total charge in the device, C/m² (C through a cross-section) |
|
package/docs/kit.md
CHANGED
|
@@ -419,7 +419,7 @@ console.log(describe(def)); // … warnings: materials.Si.species.e-.D: 36 m²/s
|
|
|
419
419
|
|
|
420
420
|
## Checking a result
|
|
421
421
|
|
|
422
|
-
`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
|
|
423
423
|
page can show that its numbers hold up. Pass the solution `solve()` or `advance()` returned.
|
|
424
424
|
|
|
425
425
|
- **converged**: the solve converged.
|
|
@@ -435,6 +435,15 @@ page can show that its numbers hold up. Pass the solution `solve()` or `advance(
|
|
|
435
435
|
(in equilibrium, flows only of round-off against what it could carry) isn't listed.
|
|
436
436
|
- **conservation**, in a transient: every closed, unreacting stretch keeps what it held plus
|
|
437
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, or the
|
|
442
|
+
device has more than one steady state, which Newton picks by its path (an unstable one too:
|
|
443
|
+
see [numerics](numerics.md#steady-state) for kicking a state to test it).
|
|
444
|
+
That's rare (round 11's fuzzer found it in ~1% of random reacting devices, by this same test),
|
|
445
|
+
but it's the cheapest check there is on a state the other items can't see past. Both solves
|
|
446
|
+
start from the definition, not this state, so it costs two solves.
|
|
438
447
|
- **grid**, in a steady state (unless `refine: false`): the device solved again on a grid twice
|
|
439
448
|
as fine everywhere (half of each `hmin` and `hmax`, the square root of each `ratio`), starting
|
|
440
449
|
from this solution interpolated onto it. The current, each floating terminal's voltage (an open-circuit voltage) and each
|
package/docs/numerics.md
CHANGED
|
@@ -447,7 +447,7 @@ test checks the compact Jacobian column by column on devices that cover every as
|
|
|
447
447
|
### Exact J·v
|
|
448
448
|
|
|
449
449
|
The assembled matrix loses what a flux's two entries share. A Scharfetter–Gummel flux depends on
|
|
450
|
-
$`\eta`$ at its two ends through $`G`$ and $`-G(1 + E)`$, with $`E = \
|
|
450
|
+
$`\eta`$ at its two ends through $`G`$ and $`-G(1 + E)`$, with $`E = \mathrm{expm1}(\Delta\eta)`$;
|
|
451
451
|
for a nearly uniform $`v`$ they cancel, and $`J \cdot v`$ keeps only round-off of $`G|v|`$, which in
|
|
452
452
|
an inversion layer's 0.1 nm cells ($`G`$ ~ 1e11) dwarfs the flux. Where a solve needs
|
|
453
453
|
$`J \cdot v`$ itself (GMRES, in the impedance and in Newton's refined solves), the dilute kernels'
|
|
@@ -470,9 +470,14 @@ which cost four assemblies per product.
|
|
|
470
470
|
## Newton
|
|
471
471
|
|
|
472
472
|
- The update of the device's potentials ($`\hat\phi`$, $`\eta`$) is limited to 10 thermal units per
|
|
473
|
-
iteration by uniform scaling
|
|
474
|
-
|
|
475
|
-
|
|
473
|
+
iteration by uniform scaling: one factor for the whole update, concentrations' $`\eta`$ with
|
|
474
|
+
their $`\hat\phi`$, gates, coverages and floating terminal voltages alike, so the damped update
|
|
475
|
+
keeps Newton's direction. Clipping each unknown at its own limit would not: where it bit,
|
|
476
|
+
$`\eta - z\hat\phi`$ (a concentration) would move by up to a factor $`e^{10}`$ that Newton never
|
|
477
|
+
asked for. Either way damping shapes only the path, since convergence needs a full, undamped
|
|
478
|
+
update. A floating terminal voltage's update doesn't count toward the limit, though: it enters
|
|
479
|
+
only linearly (conductance links, held levels, a capacitive face), so a large swing in it is
|
|
480
|
+
safe. When it did count, a port driven by
|
|
476
481
|
a current pulse, its voltage collapsing by hundreds of volts as the current switched off,
|
|
477
482
|
dragged every unknown along 10 thermal units at a time (and a swing past 1e4 tripped the
|
|
478
483
|
divergence check): the strong-injection Haynes–Shockley benchmark took 55% more
|
|
@@ -516,7 +521,14 @@ which cost four assemblies per product.
|
|
|
516
521
|
~1 per m³ beside a face, are held through conductances 1e-16 of the face flux's coefficient in
|
|
517
522
|
their row, and the elimination cancels them exactly: such devices now solve, even at
|
|
518
523
|
equilibrium. Newton's updates stay meaningful (the factorisation only amplifies the residual,
|
|
519
|
-
so a small update still means a small residual), and its refined solves correct the rest.
|
|
524
|
+
so a small update still means a small residual), and its refined solves correct the rest. So
|
|
525
|
+
is a pivot that's round-off alone without being exactly zero, below 1e-18 of the magnitudes it
|
|
526
|
+
was formed from (the running bound below, carried only for a block whose pivots look small):
|
|
527
|
+
otherwise its value, whatever the round-off happened to leave, scales all that follows. Strictly
|
|
528
|
+
neutral water whose level only the fast couple at its far face held had its last node's pivot
|
|
529
|
+
at 7e-32 with the species listed one way and 3e-16 the other; at 7e-32 the face's block was
|
|
530
|
+
scaled by 1e31, lost its own pivot outright, and the solve failed in that listing alone. (At
|
|
531
|
+
1e-15 a pivot can still hold a digit Newton needs: a port's steady solve then wandered.)
|
|
520
532
|
- When a steady solve fails, the solution's warnings say how nearly singular the system was,
|
|
521
533
|
and where. A running error bound through the factorisation compares each pivot with the
|
|
522
534
|
magnitudes it was formed from, $`\log_{10}(\sum|\mathrm{terms}| / |\mathrm{pivot}|)`$: the digits
|
|
@@ -586,8 +598,11 @@ which cost four assemblies per product.
|
|
|
586
598
|
- A target within round-off of the present time (1e-10 relative), such as an animation
|
|
587
599
|
frame's that lands a hair past a breakpoint just reached, is snapped to rather than
|
|
588
600
|
stepped to: a step of 1e-13 s can't be resolved.
|
|
589
|
-
- Newton starts each step from
|
|
590
|
-
about a third of the iterations.
|
|
601
|
+
- Newton starts each step from its unknowns extrapolated through the last three states, which
|
|
602
|
+
saves about a third of the iterations. Only its unknowns: the slots it never solves for
|
|
603
|
+
include a gauge, an insertion host's $`\hat\phi`$, which extrapolated through steps that grow
|
|
604
|
+
wandered off geometrically (to 1e11 thermal units by 1e5 s), until the levels measured from
|
|
605
|
+
it lost their digits and every step failed.
|
|
591
606
|
- The wall-clock budget is checked between steps.
|
|
592
607
|
|
|
593
608
|
## Small-signal impedance
|
|
@@ -666,11 +681,24 @@ the clock. Nor does it ask whether the steady state is stable: an axon held at a
|
|
|
666
681
|
makes it fire over and over has a resting point all the same, which `solve()` returns, and only
|
|
667
682
|
a transient leaves it.
|
|
668
683
|
|
|
684
|
+
Reactions far from equilibrium can have more than one steady state, and Newton finds whichever
|
|
685
|
+
its path leads to, unstable ones included. Two couples on electrode ports (round 11's fuzzer)
|
|
686
|
+
had a state near the fast couple's equilibrium and one with its reactant at 1e-18 mol/m³; the
|
|
687
|
+
second passed every check, and two listings of three solved to it, yet a 10 mV kick took it to
|
|
688
|
+
the first within 2e5 s (where a long `advance()` from the start goes too). A transient from the
|
|
689
|
+
unstable state doesn't show this either: its steps soon outgrow the growth time (~3000 s here),
|
|
690
|
+
and an implicit step that long damps a growing mode as it damps a decaying one, so it sits there
|
|
691
|
+
to 1e12 s. To test a steady state, kick it (a drive moved for a moment and put back) and
|
|
692
|
+
advance with `dtMax` held below the times you care about, a tenth of each interval as it
|
|
693
|
+
grows: a stable state comes back. (At equilibrium, nothing flowing, a state minimises a convex
|
|
694
|
+
free energy and is stable.)
|
|
695
|
+
|
|
669
696
|
### The steady system
|
|
670
697
|
|
|
671
|
-
Every row of a steady solve that differs from a time step's is a sum of balance rows
|
|
672
|
-
|
|
673
|
-
leaves is exact, however small beside
|
|
698
|
+
Every row of a steady solve that differs from a time step's is a sum of balance rows, or (a
|
|
699
|
+
reaction's affinity) a condition equivalent to one. It's written out in its telescoped form,
|
|
700
|
+
never added up from the rows themselves, so what the sum leaves is exact, however small beside
|
|
701
|
+
the terms that cancel:
|
|
674
702
|
|
|
675
703
|
- **A conserved amount.** A combination of stretches (weights $`w`$) that no reaction changes and
|
|
676
704
|
nothing outside feeds has balance rows that sum to zero identically in a steady state: the
|
|
@@ -751,6 +779,34 @@ leaves is exact, however small beside the terms that cancel:
|
|
|
751
779
|
contact; moved, slow generation through a bulk of GaAs with ~1e-4 electrons per cm³ couldn't
|
|
752
780
|
carry the inversion layer's level with it (of 500 random MOS capacitors, 38 failed and 5
|
|
753
781
|
converged to a gate charge up to 2× off).
|
|
782
|
+
- **A reaction's equilibrium.** A face reaction that alone changes some combination (left out, the
|
|
783
|
+
combination would be conserved: Ag⁺ closed in a cell where Ag⁺ + e⁻ = Ag(s) is its one source, or
|
|
784
|
+
Ag⁺ + AgNO₃ where it also complexes) makes that combination's summed balance its rate, which a
|
|
785
|
+
steady state sets to zero. The rate is a poor row, though: $`k_0`$ times a function of the levels,
|
|
786
|
+
beside the round-off of diffusion's terms in the same balances. An Ag⁺/Ag couple 1e-20 as fast as
|
|
787
|
+
Fe³⁺/Fe²⁺ on the same platinum at open circuit was lost to it, the solve settling where silver
|
|
788
|
+
wasn't (−0.4661 V, against the joint equilibrium's −0.5004 V), and 1e-15 to 1e-18 failed. The
|
|
789
|
+
reaction's affinity, $`a = A_\text{fixed} - \sum \nu \eta = 0`$, says the same at any $`k_0`$
|
|
790
|
+
(each kinetic form vanishes only there), so it stands in for one of the combination's balance
|
|
791
|
+
rows, a pin like an amount's, chosen by the same elimination and ahead of the amounts: the slowest
|
|
792
|
+
reaction first, and a row of one of its own species where one is left, never one picked by where
|
|
793
|
+
the species are listed (the rows had differed with the species reversed, and an electrode's
|
|
794
|
+
equilibrium solved in one listing and failed in the other). The combinations come from the null
|
|
795
|
+
space with that reaction's row left out, any conserved amount added to one being as good (its
|
|
796
|
+
summed balance vanishes too), so that one can be found with a row left to take: silver's e⁻ − Fe³⁺
|
|
797
|
+
beside a closed iron couple whose affinity had Fe³⁺'s row is e⁻ + Fe²⁺ with the iron added. Never
|
|
798
|
+
a metal's carriers' row or a row a terminal feeds, which carry its current (the platinum's
|
|
799
|
+
electrons standing for an iron couple put the electrode at 602 V). And not where a terminal driven
|
|
800
|
+
by a current feeds the combination, since the sum is then the rate plus what that terminal passes;
|
|
801
|
+
a current held at zero passes nothing, so an open-circuit electrode feeding a closed pool of Ag⁺
|
|
802
|
+
is covered (from an iron k₀ of 1e-15 beside it, that settled 78 mV off).
|
|
803
|
+
|
|
804
|
+
Where a reaction can't stand on its affinity (a bulk reaction, whose rate summed over the
|
|
805
|
+
region vanishing needn't make it vanish everywhere, or an electrode port's), a steady solve
|
|
806
|
+
that leaves it with one sign of affinity wherever it acts, though nothing else changes what
|
|
807
|
+
it does, can't be a steady state: its rate is merely too small to see. The solution warns of
|
|
808
|
+
it, rather than pass the start's composition off as steady (a slow complexation had kept its
|
|
809
|
+
starting composition, K = 0.1 for 7.5, without a word).
|
|
754
810
|
- **An island's balance.** A piece of a stretch between its faces that nothing else feeds (no
|
|
755
811
|
contact, port or reaction) holds its level only through its faces' fluxes, which can be far
|
|
756
812
|
weaker than its own conduction: a face conductance of 1e-15 S/m² against a region conducting
|
|
@@ -781,8 +837,8 @@ leaves is exact, however small beside the terms that cancel:
|
|
|
781
837
|
solution stands; and a steady solve that fails outright is tried once more with the summed rows
|
|
782
838
|
in from the start. Such devices now solve directly, their currents right to ~1e-15.
|
|
783
839
|
|
|
784
|
-
All
|
|
785
|
-
island's pin by a shift it solves for, a flat level by holding every node to one. What the
|
|
840
|
+
All four move a weakly held level as a whole, where a factorisation can't: an amount's, an
|
|
841
|
+
affinity's or an island's pin by a shift it solves for, a flat level by holding every node to one. What the
|
|
786
842
|
factorisation still loses elsewhere, Newton's refined solves recover ([Newton](#newton)).
|
|
787
843
|
|
|
788
844
|
### Reaching it
|
package/docs/statistics.md
CHANGED
|
@@ -140,7 +140,8 @@ therefore reproduced exactly, and $`x`$ stays within $`(0, 1)`$ at any potential
|
|
|
140
140
|
curve is checked for monotonicity at construction. A curve far flatter than the ideal term near
|
|
141
141
|
an end (E falling by millivolts over the last few percent of x) can fail that check however
|
|
142
142
|
many points it has, since r then changes as steeply as the logarithm: give the ends some of the
|
|
143
|
-
ideal slope
|
|
143
|
+
ideal slope: adding −a (RT/F) ln(x/(1−x)) to E, with a = 0.1 at points 0.01 apart (less with points
|
|
144
|
+
closer together near the ends), passes.
|
|
144
145
|
|
|
145
146
|
With `A` instead, the curve is the isotherm's, against a reference where the combination's
|
|
146
147
|
chemical potential is 0, with $`g`$ the excess chemical potential ($`\Omega(1-2x)`$ for `A: [Ω]`):
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# More templates
|
|
2
|
+
|
|
3
|
+
Templates for devices outside [llms.txt](../llms.txt)'s core set, written the same way: each
|
|
4
|
+
block runs in the test suite and prints its closed-form check. Read llms.txt first; these build
|
|
5
|
+
on it.
|
|
6
|
+
|
|
7
|
+
- semiconductors: [a Schottky diode](#a-schottky-diode), [a thin-film transistor](#a-thin-film-transistor-a-capacitance-along-a-channel);
|
|
8
|
+
- electrochemistry: [corrosion under a drop](#corrosion-under-a-drop-an-electrode-spread-through-a-film)
|
|
9
|
+
(an electrode spread through a film, and a cross-section that varies);
|
|
10
|
+
- physiology: [a voltage clamp](#a-voltage-clamp), [an action potential](#an-action-potential),
|
|
11
|
+
[one travelling along an axon](#an-action-potential-travelling-along-an-axon).
|
|
12
|
+
|
|
13
|
+
## a Schottky diode
|
|
14
|
+
|
|
15
|
+
A barrier pinned at the metal (`phi: 'pinned'`, `zeroCharge` the barrier height for Si, whose
|
|
16
|
+
$`\mu^\circ_{\mathrm{e}^-}`$ is 0), crossed at the emission velocity: a `velocity` link, $`N_{\mathrm{in}} = v\,(c_{\mathrm{eq}} - c)`$. With
|
|
17
|
+
`'equilibrium'` links (infinite velocity) it's diffusion theory instead, 20 times the current here.
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import { Device, units, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
21
|
+
import { build, layer, ohmic, semiconductor } from 'driftlet/kit';
|
|
22
|
+
|
|
23
|
+
const T = 300, VT = (GAS_CONSTANT * T) / FARADAY, Si = semiconductor('Si');
|
|
24
|
+
const phiB = 0.85, A = 1.12e6; // the barrier (V); Richardson's constant for n-Si, A/(m²K²)
|
|
25
|
+
const v = (A * T * T) / (FARADAY * Si.materials.Si.species['e-'].cRef); // Bethe's emission velocity, 2.2e4 m/s (cRef is N_c)
|
|
26
|
+
const schottky = (V) => ({
|
|
27
|
+
V,
|
|
28
|
+
terminal: 'e-',
|
|
29
|
+
species: { 'e-': { type: 'velocity', v }, 'h+': { type: 'velocity', v, offset: 0 } },
|
|
30
|
+
phi: 'pinned',
|
|
31
|
+
zeroCharge: phiB, // E_c − E_F at the metal
|
|
32
|
+
});
|
|
33
|
+
const dev = new Device(build({ T, library: [Si], stack: [schottky(0), layer('Si', units.um(2), { donors: units.perCm3(1e16) }), ohmic(0)], grid: { hmin: units.nm(1), hmax: units.nm(20) } }));
|
|
34
|
+
for (const V of [0.1, 0.2, 0.3]) {
|
|
35
|
+
dev.set({ contacts: { left: { V } } }); // the metal positive: forward
|
|
36
|
+
const J = dev.solve().current, te = A * T * T * Math.exp(-phiB / VT) * Math.expm1(V / VT);
|
|
37
|
+
console.log(`${V} V: J ${J.toExponential(3)} A/m², thermionic emission ${te.toExponential(3)}`); // about 4% below: the depletion region in series
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The same `velocity` link at an ohmic contact is its surface recombination velocity.
|
|
42
|
+
|
|
43
|
+
## a thin-film transistor (a capacitance along a channel)
|
|
44
|
+
|
|
45
|
+
A **port with a `capacitance`** holds charge $`\sigma = C\,(V - \mathtt{zeroCharge} - \phi)`$ per area of electrode
|
|
46
|
+
along its window, balanced by the region's: a gate along a channel, a porous electrode's double
|
|
47
|
+
layer. Here x runs from source to drain with the gate spread along it: the gradual-channel
|
|
48
|
+
approximation, exact for a thin film until it pinches off.
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
52
|
+
|
|
53
|
+
const T = 300, VT = (GAS_CONSTANT * T) / FARADAY;
|
|
54
|
+
const t = 20e-9, L = 10e-6, W = 100e-6, C = 1e-3, mu = 1e-3; // film, length, width (m); F/m² of gate; m²/(V·s)
|
|
55
|
+
const tft = (Vg, Vd) =>
|
|
56
|
+
new Device({
|
|
57
|
+
T,
|
|
58
|
+
species: [{ name: 'e-', z: -1, cRef: 46 }], // an undoped film's electrons (N_c 2.8e19 cm⁻³)
|
|
59
|
+
materials: { film: { epsr: 0, species: { 'e-': { D: mu * VT, mu0: 0 } } } }, // ε = 0: F·n·t balances σ
|
|
60
|
+
regions: [{ name: 'channel', material: 'film', length: L }],
|
|
61
|
+
contacts: {
|
|
62
|
+
left: { V: 0, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'neutral' }, // source
|
|
63
|
+
right: { V: Vd, terminal: 'e-', species: { 'e-': 'equilibrium' }, phi: 'neutral' }, // drain
|
|
64
|
+
},
|
|
65
|
+
ports: [{ name: 'gate', region: 'channel', V: Vg, area: 1 / t, capacitance: { C, zeroCharge: 0.3 } }],
|
|
66
|
+
grid: { hmin: L / 2000, hmax: L / 100, ratio: 1.05 }, // fine at the drain, where it pinches off
|
|
67
|
+
});
|
|
68
|
+
// The output characteristic at V_G = 2 V: linear, then saturating past V_D ≈ V_G − 0.3 V.
|
|
69
|
+
for (const Vd of [0.1, 0.5, 1, 1.5, 2, 3]) {
|
|
70
|
+
const sol = tft(2, Vd).solve();
|
|
71
|
+
const I = -sol.current * t * W; // sol.current is per m² of the film's section
|
|
72
|
+
console.log(`V_D ${Vd.toFixed(1)} V: I_D ${(I * 1e6).toFixed(2)} µA, σ at the drain ${(sol.ports[0].sigma.at(-1) * 1e3).toFixed(3)} mC/m²`);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
To sweep the gate on one device instead, patch the port: `dev.set({ ports: { gate: { V } } })`.
|
|
77
|
+
An organic electrochemical transistor is the same with a volumetric capacitance C*:
|
|
78
|
+
`area: 1 / t, capacitance: { C: Cstar * t }`.
|
|
79
|
+
|
|
80
|
+
## corrosion under a drop (an electrode spread through a film)
|
|
81
|
+
|
|
82
|
+
A **port with reactions** is an electrode spread through a strictly neutral region: a metal under
|
|
83
|
+
a film of water, a crevice's walls, a porous electrode. Its `terminal` is the metal's carrier,
|
|
84
|
+
`area` its area per volume (1/h under a film h thick), each reaction Butler–Volmer per area, and
|
|
85
|
+
a `capacitance` the double layer. A **`geometry`** gives the device a cross-section A(x): here x
|
|
86
|
+
is the radius of a drop, $`A = 2\pi r\,h(r)`$, and currents and amounts become totals (A, mol).
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
|
90
|
+
import { aqueous, polarization, H2O } from 'driftlet/kit';
|
|
91
|
+
|
|
92
|
+
// Salt water on iron: a drop 3 mm across. The iron dissolves and reduces O₂, which comes in from
|
|
93
|
+
// the air through the drop's top, fastest at the thin rim: the centre corrodes, the rim doesn't.
|
|
94
|
+
const T = 298.15, RT = GAS_CONSTANT * T, R = 3e-3, h0 = 1e-3, cO2 = 0.26, D = 2e-9;
|
|
95
|
+
const lib = aqueous(['Na+', 'Cl-', 'Fe2+', 'OH-', 'H+'], { epsr: 0 }); // strictly neutral water
|
|
96
|
+
lib.species.push({ name: 'O2', z: 0, cRef: 1000 }, { name: 'e-', z: -1 }); // e⁻: the iron's, in no material
|
|
97
|
+
lib.materials.water.species.O2 = { D, mu0: 16.4e3 };
|
|
98
|
+
const r = Array.from({ length: 61 }, (_, j) => (j / 60) * R);
|
|
99
|
+
const h = r.map((x) => Math.max(h0 * (1 - (x / R) * (x / R)), 0.02 * h0)); // the drop's height
|
|
100
|
+
const dev = new Device({
|
|
101
|
+
T,
|
|
102
|
+
species: lib.species,
|
|
103
|
+
materials: lib.materials,
|
|
104
|
+
geometry: { area: { x: r, values: r.map((x, j) => 2 * Math.PI * x * h[j]) } }, // ions squeeze through the thin rim
|
|
105
|
+
regions: [{ name: 'drop', material: 'water', length: R, c0: { 'Na+': 500, 'Cl-': 500 + 2e-6, 'Fe2+': 1e-6, 'OH-': 1e-4, 'H+': 1e-4, O2: cO2 } }],
|
|
106
|
+
contacts: { left: { phi: 'neutral' }, right: { phi: 'neutral' } }, // closed: the centre, and the rim
|
|
107
|
+
bulkReactions: [
|
|
108
|
+
{ equation: 'H+ + OH- = H2O', fixed: { H2O }, kf: { water: 1.4e8 } }, // H2O: water's μ, −237.129 kJ/mol
|
|
109
|
+
// O₂ from the air through the drop: a rate k_f (1 − c/c_sat), k_f = D c_sat/h² (not a port's
|
|
110
|
+
// exchange link, whose rate is linear in μ rather than c)
|
|
111
|
+
{ equation: 'Air = O2', fixed: { Air: 16.4e3 + RT * Math.log(cO2 / 1000) }, kf: { water: { x: r, values: h.map((y) => (D * cO2) / (y * y)) } } },
|
|
112
|
+
],
|
|
113
|
+
ports: [{
|
|
114
|
+
name: 'iron', region: 'drop', terminal: 'e-',
|
|
115
|
+
V: 0, // the only metal and the only path for current, so it passes none: held, it fixes the level φ is read against
|
|
116
|
+
area: { x: r, values: h.map((y) => 1 / y) }, // m² of iron per m³ of water
|
|
117
|
+
capacitance: { C: 0.2, zeroCharge: -0.4 }, // the double layer: the potential moves continuously
|
|
118
|
+
reactions: [
|
|
119
|
+
{ equation: 'Fe2+ + 2 e- = Fe(s)', fixed: { 'Fe(s)': 0 }, k0: 5e-6, alpha: 0.5 },
|
|
120
|
+
{ equation: 'O2 + 2 H2O + 4 e- = 4 OH-', fixed: { H2O }, k0: 3e-9, alpha: 0.125 },
|
|
121
|
+
],
|
|
122
|
+
}],
|
|
123
|
+
grid: { hmin: 5e-6, hmax: 60e-6, ratio: 1.2 },
|
|
124
|
+
});
|
|
125
|
+
const sol = dev.advance(600); // ten minutes
|
|
126
|
+
const iron = sol.ports[0], last = iron.x.length - 1;
|
|
127
|
+
// rates[q][j]: reaction q at node j of the window, forward, mol/(m²·s) of iron; anodic current +
|
|
128
|
+
const anodic = (j) => 2 * FARADAY * -iron.rates[0][j] - 4 * FARADAY * iron.rates[1][j];
|
|
129
|
+
console.log(`iron at ${(iron.V - sol.phi[0]).toFixed(3)} V against the water at the centre (SHE, with table μ°)`);
|
|
130
|
+
console.log(`net current on the iron: ${anodic(0).toFixed(2)} A/m² at the centre, ${anodic(last).toFixed(2)} A/m² at the rim`);
|
|
131
|
+
// The Evans diagram at the centre: each reaction's current with the iron's level moved, the water held.
|
|
132
|
+
const levels = [-0.55, -0.5, -0.45, -0.4].map((v) => v + sol.phi[0]);
|
|
133
|
+
const evans = polarization(dev, sol, { port: 'iron', x: 0 }, levels);
|
|
134
|
+
for (const rx of evans.reactions) console.log(`${rx.equation}: ${Array.from(rx.current, (i) => i.toFixed(2)).join(', ')} A/m²`);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Species can take an electrode's surface sites (`surface`, Langmuir coverages), and a reaction
|
|
138
|
+
with `bare: true` runs only on bare metal: a passive film (docs/device.md).
|
|
139
|
+
|
|
140
|
+
## a voltage clamp
|
|
141
|
+
|
|
142
|
+
A face's `gates` (each a fraction open, $`dx/dt = \alpha(V)(1 - x) - \beta(V)x`$ with $`V = \phi_{\mathrm{right}} - \phi_{\mathrm{left}}`$)
|
|
143
|
+
scale its links (`gates: { m: 3, h: 1 }`). `hodgkinHuxley({ T, linear: true })` (kit) writes the
|
|
144
|
+
squid axon's as Hodgkin and Huxley had them, conductances $`g(V - E)`$. Between two baths without a
|
|
145
|
+
reference species, $`V_{\mathrm{right}} - V_{\mathrm{left}}`$ is $`V_m`$, so a held step is their voltage clamp.
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
149
|
+
import { build, layer, bath, aqueous, hodgkinHuxley } from 'driftlet/kit';
|
|
150
|
+
|
|
151
|
+
const T = 279.45, VT = (GAS_CONSTANT * T) / FARADAY; // HH's 6.3 °C
|
|
152
|
+
const out = { 'Na+': 440, 'K+': 20, 'Cl-': 450, 'A-': 10 }, inside = { 'Na+': 50, 'K+': 400, 'Cl-': 52, 'A-': 398 }; // squid, mM
|
|
153
|
+
const water = aqueous(['Na+', 'K+', 'Cl-'], { epsr: 0 });
|
|
154
|
+
water.species.push({ name: 'A-', z: -1, cRef: 1000 });
|
|
155
|
+
water.materials.water.species['A-'] = { D: 0.5e-9, mu0: 0 };
|
|
156
|
+
const hh = hodgkinHuxley({ T, linear: true });
|
|
157
|
+
const step = -0.01; // from −65 mV to −10 mV at 1 ms
|
|
158
|
+
const dev = new Device(
|
|
159
|
+
build({
|
|
160
|
+
T,
|
|
161
|
+
library: [water],
|
|
162
|
+
stack: [
|
|
163
|
+
bath(out), // the outside, on the left
|
|
164
|
+
layer('water', 1e-6, { c0: out }),
|
|
165
|
+
{ phi: { type: 'capacitive', C: 0.01 }, gates: hh.gates, species: { ...hh.species, 'A-': 'blocked' } },
|
|
166
|
+
layer('water', 1e-6, { c0: inside }),
|
|
167
|
+
bath(inside, { t: [0, 1e-3, 1e-3, 1], values: [-0.065, -0.065, step, step] }),
|
|
168
|
+
],
|
|
169
|
+
grid: { hmin: 20e-9, hmax: 0.2e-6 },
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
dev.solve(); // the rest at −65 mV, the gates at their values there
|
|
173
|
+
const run = dev.advance(11e-3, { dtMax: 0.02e-3, probes: [{ interface: 0, species: 'Na+' }, { interface: 0, species: 'K+' }] });
|
|
174
|
+
const [na, k] = run.trace.probes.map((N) => N.map((x) => -FARADAY * x)); // outward currents, A/m²
|
|
175
|
+
const peak = na.indexOf(Math.min(...na)), EK = VT * Math.log(out['K+'] / inside['K+']);
|
|
176
|
+
console.log(`I_Na peaks ${((run.trace.t[peak] - 1e-3) * 1e3).toFixed(2)} ms into the step, ${(na[peak] * 0.1).toFixed(2)} mA/cm²`); // 0.73 ms, −1.48
|
|
177
|
+
console.log(`g_K after 10 ms: ${((k.at(-1) / (step - EK)) * 0.1).toFixed(2)} mS/cm²`); // 21.12: HH's 36 n∞⁴ is 21.5, which n nearly reaches by 10 ms
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## an action potential
|
|
181
|
+
|
|
182
|
+
Without `linear: true` (given `inside` and `outside`), `hodgkinHuxley` writes GHK permeabilities
|
|
183
|
+
matched to HH's conductances at −65 mV, which rectify as real channels do. A pump-less cell has no
|
|
184
|
+
steady rest (`solve()` finds Donnan), so start the transient from the concentrations given.
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
import { Device } from 'driftlet';
|
|
188
|
+
import { build, layer, bath, aqueous, injector, pulse, hodgkinHuxley } from 'driftlet/kit';
|
|
189
|
+
|
|
190
|
+
const T = 279.45; // HH's 6.3 °C
|
|
191
|
+
const out = { 'Na+': 440, 'K+': 20, 'Cl-': 450, 'A-': 10 }, inside = { 'Na+': 50, 'K+': 400, 'Cl-': 52, 'A-': 398 }; // squid, mM
|
|
192
|
+
const water = aqueous(['Na+', 'K+', 'Cl-'], { epsr: 0 });
|
|
193
|
+
water.species.push({ name: 'A-', z: -1, cRef: 1000 });
|
|
194
|
+
water.materials.water.species['A-'] = { D: 0.5e-9, mu0: 0 };
|
|
195
|
+
const hh = hodgkinHuxley({ inside, outside: out, T }); // GHK channels; outside on the left
|
|
196
|
+
const axon = new Device(
|
|
197
|
+
build({
|
|
198
|
+
T,
|
|
199
|
+
library: [water],
|
|
200
|
+
stack: [
|
|
201
|
+
bath(out, 'Cl-'),
|
|
202
|
+
layer('water', 5e-6, { name: 'outside', c0: out }),
|
|
203
|
+
{ phi: { type: 'capacitive', C: 0.01 }, gates: hh.gates, species: { ...hh.species, 'A-': 'blocked' } },
|
|
204
|
+
layer('water', 20e-6, { name: 'cytoplasm', c0: inside }),
|
|
205
|
+
{ species: { 'Na+': 'blocked', 'K+': 'blocked', 'Cl-': 'blocked', 'A-': 'blocked' }, phi: 'neutral' },
|
|
206
|
+
],
|
|
207
|
+
// the stimulus: 10 µA/cm² of K⁺ for 1 ms at 20 ms, from a pipette in the cytoplasm
|
|
208
|
+
ports: [injector({ region: 'cytoplasm', from: 15e-6, to: 20e-6, species: 'K+', I: pulse({ start: 20e-3, width: 1e-3, amplitude: 0.1 }) })],
|
|
209
|
+
grid: { hmin: 50e-9, hmax: 1e-6 },
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
let peak = -1;
|
|
213
|
+
for (let k = 1; k <= 300; k++) {
|
|
214
|
+
const sol = axon.advance(k * 1e-4); // 0.1 ms at a time
|
|
215
|
+
peak = Math.max(peak, sol.interfaces[0].V); // V_m, and sol.interfaces[0].gates = { m, h, n }
|
|
216
|
+
}
|
|
217
|
+
console.log(`peak ${(peak * 1e3).toFixed(1)} mV`); // +12.6: GHK's Na⁺ current is limited by the 50 mM inside (linear: true gives +37.9, HH's own)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## an action potential travelling along an axon
|
|
221
|
+
|
|
222
|
+
Here x runs along the axon: the axoplasm is one region, and the membrane a port spanning it, a
|
|
223
|
+
capacitance and gated channels to an outside given as a `bath` (the port's V is then the
|
|
224
|
+
outside's $`\phi`$). `hodgkinHuxley({ T, area })` writes HH's channels for it, `area` being the membrane
|
|
225
|
+
per volume, 2/a. Cable theory isn't put in; it comes out of the ions' drift. `ports[k].Vm[j]` and
|
|
226
|
+
`ports[k].gates[name][j]` give the membrane at each node. A myelinated axon is regions, each with
|
|
227
|
+
its own port (an internode's just a small capacitance). Nothing warns of a grid too coarse for
|
|
228
|
+
the spike's front: the speed comes out 3% low on 2 mm cells, 0.2% on 0.5 mm, for a = 238 µm
|
|
229
|
+
(scale $`h`$ with $`\sqrt a`$). The stimulus is a current per volume, so a thinner axon needs it as $`1/a`$.
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
import { Device, FARADAY, GAS_CONSTANT } from 'driftlet';
|
|
233
|
+
import { hodgkinHuxley, injector, pulse } from 'driftlet/kit';
|
|
234
|
+
|
|
235
|
+
const T = 291.65, VT = (GAS_CONSTANT * T) / FARADAY, a = 238e-6; // 18.5 °C, a squid giant axon
|
|
236
|
+
const inside = { 'Na+': 50, 'K+': 400, 'Cl-': 52.5, 'A-': 397.5 }; // mM
|
|
237
|
+
// Outside levels that put the Nernst potentials at HH's +50, −77 and −54.4 mV (leak as Cl⁻).
|
|
238
|
+
const outside = { 'Na+': 50 * Math.exp(0.05 / VT), 'K+': 400 * Math.exp(-0.077 / VT), 'Cl-': 52.5 * Math.exp(0.054387 / VT) };
|
|
239
|
+
// One D for the small ions, giving HH's 35.4 Ω·cm.
|
|
240
|
+
const D = 1 / 0.354 / ((FARADAY * FARADAY) / (GAS_CONSTANT * T)) / (50 + 400 + 52.5);
|
|
241
|
+
const sp = (d) => ({ D: d, mu0: 0, cRef: 1000 });
|
|
242
|
+
const sealed = { species: { 'Na+': 'blocked', 'K+': 'blocked', 'Cl-': 'blocked', 'A-': 'blocked' }, phi: 'neutral' };
|
|
243
|
+
const hh = hodgkinHuxley({ T, area: 2 / a });
|
|
244
|
+
const axon = new Device({
|
|
245
|
+
T,
|
|
246
|
+
species: [{ name: 'Na+', z: 1 }, { name: 'K+', z: 1 }, { name: 'Cl-', z: -1 }, { name: 'A-', z: -1 }],
|
|
247
|
+
materials: { axoplasm: { epsr: 0, species: { 'Na+': sp(D), 'K+': sp(D), 'Cl-': sp(D), 'A-': sp(1e-12) } } },
|
|
248
|
+
regions: [{ name: 'axon', material: 'axoplasm', length: 0.06, c0: inside }],
|
|
249
|
+
contacts: { left: sealed, right: sealed },
|
|
250
|
+
ports: [
|
|
251
|
+
{ name: 'membrane', region: 'axon', V: 0, area: 2 / a, capacitance: { C: 0.01, zeroCharge: 0 }, bath: { c: outside }, gates: hh.gates, species: hh.species },
|
|
252
|
+
injector({ name: 'stim', region: 'axon', from: 0, to: 1e-3, species: 'K+', I: pulse({ start: 30e-3, width: 0.2e-3, amplitude: 30 }) }),
|
|
253
|
+
],
|
|
254
|
+
grid: { hmin: 2e-4, hmax: 2e-4 },
|
|
255
|
+
});
|
|
256
|
+
axon.advance(30e-3); // settle to rest (no pump: start from the concentrations, not solve())
|
|
257
|
+
const sol = axon.advance(34e-3, { dtMax: 5e-6, probes: [{ x: 0.02, quantity: 'phi' }, { x: 0.04, quantity: 'phi' }] });
|
|
258
|
+
const { t, probes } = sol.trace, at = (v) => t[v.findIndex((x) => x > 0)]; // (φ inside, the outside's held at 0)
|
|
259
|
+
console.log(`${(0.02 / (at(probes[1]) - at(probes[0]))).toFixed(1)} m/s`); // 18.8 here (finely resolved, 18.73; HH computed 18.8)
|
|
260
|
+
```
|