driftlet 0.11.4 → 0.12.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 +6 -2
- package/docs/device.md +105 -1
- package/docs/kit.md +12 -0
- package/docs/numerics.md +8 -1
- package/llms.txt +50 -8
- package/package.json +1 -1
- package/src/channels.js +73 -0
- package/src/describe.js +4 -2
- package/src/device.js +55 -11
- package/src/kit.js +1 -0
- package/src/recorder.js +1 -1
- package/src/solution.js +12 -1
- package/src/solver.js +202 -21
- package/types/channels.d.ts +39 -0
- package/types/kit.d.ts +1 -0
- package/types/recorder.d.ts +7 -3
- package/types/solver.d.ts +28 -3
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ import { bandDiagram } from 'driftlet/plot'; // a level diagram as an SVG str
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Or straight from a CDN in a page, pinned to a version:
|
|
47
|
-
`https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
47
|
+
`https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
|
|
48
48
|
TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
|
|
49
49
|
[`llms.txt`](llms.txt).
|
|
50
50
|
|
|
@@ -141,6 +141,8 @@ them locally, serve the repository root (e.g. `python3 -m http.server`) and open
|
|
|
141
141
|
| **Intercalation host**: cycling between cutoffs against the OCV | **Organic solar cell**: excitons diffusing to a donor/acceptor interface, against theory | **Perovskite hysteresis**: mobile ions and scan rate, against IonMonger |
|
|
142
142
|
| [](https://markblundeberg.github.io/driftlet/demos/channel.html) | [](https://markblundeberg.github.io/driftlet/demos/cell.html) | [](https://markblundeberg.github.io/driftlet/demos/junction.html) |
|
|
143
143
|
| **Charged nanochannel**: selectivity, overlapping Donnan layers and rectification, against Teorell–Meyer–Sievers | **Resting potential**: leaks and the Na⁺/K⁺ pump, against Mullins–Noda; the run-down to Donnan | **Liquid junctions**: a patch pipette's against Henderson, Planck and LJPcalc; a charged frit |
|
|
144
|
+
| [](https://markblundeberg.github.io/driftlet/demos/axon.html) | [](https://markblundeberg.github.io/driftlet/demos/propagation.html) | [](https://markblundeberg.github.io/driftlet/demos/myelin.html) |
|
|
145
|
+
| **Action potential**: Hodgkin and Huxley's gated channels, each ion's driving force through the spike, against the HH equations | **Propagation**: a spike travelling along an axon at HH's speed, out of the ions' drift; which ions carry it | **Myelin**: saltatory conduction, node to node, against a compartmental cable; a race with a bare axon |
|
|
144
146
|
|
|
145
147
|
## Is it for your problem?
|
|
146
148
|
|
|
@@ -156,7 +158,7 @@ The same equations go by different names in different fields. If your problem is
|
|
|
156
158
|
| Double layers, colloids | Poisson–Boltzmann, Gouy–Chapman–Stern, Debye screening, crowding (Bikerman) | [double-layer demo](https://markblundeberg.github.io/driftlet/demos/double-layer.html), [`equilibrium`](test/equilibrium.test.js) test | Gouy–Chapman charge and profile, Kilic–Bazant–Ajdari |
|
|
157
159
|
| Membranes, desalination | Donnan, ion exchange, liquid junctions, water dissociation | [membrane demo](https://markblundeberg.github.io/driftlet/demos/membrane.html), [`equilibrium`](test/equilibrium.test.js), [`neutral`](test/neutral.test.js) tests | Donnan partition, Planck EMF |
|
|
158
160
|
| Solid-state ionics | mixed ionic–electronic conduction, defect chemistry, mobile ions | [`statistics`](test/statistics.test.js), [`reactions`](test/reactions.test.js) tests | mass action from standard potentials |
|
|
159
|
-
| Biophysics, nanofluidics | charged nanochannels and pores, resting potentials, Goldman–Hodgkin–Katz, Nernst, Donnan, pumps and leaks | [charged-nanochannel](https://markblundeberg.github.io/driftlet/demos/channel.html), [resting-potential](https://markblundeberg.github.io/driftlet/demos/cell.html) and [liquid-junction](https://markblundeberg.github.io/driftlet/demos/junction.html) (a patch pipette's) demos, [`pore`](test/pore.test.js) test; a membrane as a face: a capacitor with ion permeabilities, and the Na⁺/K⁺ pump as a reaction on it ([`membrane`](test/membrane.test.js) test) | GHK potential and current–voltage curve; a resolved lipid layer; Mullins–Noda; the pump's static head; Donnan |
|
|
161
|
+
| Biophysics, nanofluidics | charged nanochannels and pores, resting potentials, Goldman–Hodgkin–Katz, Nernst, Donnan, pumps and leaks | [charged-nanochannel](https://markblundeberg.github.io/driftlet/demos/channel.html), [resting-potential](https://markblundeberg.github.io/driftlet/demos/cell.html), [action-potential](https://markblundeberg.github.io/driftlet/demos/axon.html) and [liquid-junction](https://markblundeberg.github.io/driftlet/demos/junction.html) (a patch pipette's) demos, [`pore`](test/pore.test.js) test; a membrane as a face: a capacitor with ion permeabilities, and the Na⁺/K⁺ pump as a reaction on it ([`membrane`](test/membrane.test.js) test); voltage-gated channels with Hodgkin–Huxley kinetics, action potentials, and their propagation along an axon ([`gates`](test/gates.test.js), [`cable`](test/cable.test.js) tests) | GHK potential and current–voltage curve; a resolved lipid layer; Mullins–Noda; the pump's static head; Donnan; HH gating under voltage clamp; the space-clamped HH equations |
|
|
160
162
|
|
|
161
163
|
The tests are worked setups, each with its analytic check, so they double as recipes. The
|
|
162
164
|
[validation table](#validation) lists them all.
|
|
@@ -255,6 +257,8 @@ Every physics feature is tested against analytic results (`npm test`, node's bui
|
|
|
255
257
|
| Schottky barrier (metal region \| n-Si) | surface density from the alignment; depletion charge | 5e-3; 2% |
|
|
256
258
|
| Charged pore or nanochannel (resolved Donnan layers at each mouth) | conductance against salt, down to its plateau, as Teorell–Meyer–Sievers in a long pore (short ones short by end layers ∝ 1/L); the zero-current voltage across a salt gradient; a symmetric pore's I–V odd, rectification with charge on one side or bipolar | 1%; 5e-3 to 1e-4; 1e-9 |
|
|
257
259
|
| Cell membrane (a capacitive face with permeabilities) | GHK resting potential (neutral or resolved solutions) and current–voltage curve; the same as a resolved 5 nm lipid layer; charging at I/C; with the Na⁺/K⁺ pump in a closed cell, Mullins–Noda, the static head 3Δμ̄_Na − 2Δμ̄_K = ΔG_ATP, and Donnan with the pump off | 1e-5 V; 3e-3; 2e-5 V; 1%; 1e-9 |
|
|
260
|
+
| Voltage-gated channels (Hodgkin–Huxley gates on a membrane face) | each gate at α/(α + β) under a voltage clamp, and relaxing at α + β after a step; a squid axon's action potential against the space-clamped HH equations with GHK currents; its impedance at rest (inductive, resonant) against the linearised equations | 1e-12; 2e-4; 1 mV; 5e-4 |
|
|
261
|
+
| A propagating action potential (a squid axon along x: gated membrane port, ions drifting in the axoplasm) | rest at HH's −65 mV and gate values; conduction velocity against the cable equation solved directly, and HH's 18.8 m/s; a myelinated axon's saltatory conduction, node by node, against a compartmental cable | 0.01 mV, 1e-4; 2e-3; 1%; 1 µs of ~25 per node |
|
|
258
262
|
| Liquid junctions in mixtures | Henderson's formula against JPCalc's published values; driftlet's steady junction against Planck by shooting and the stationary Nernst–Planck codes LJPcalc and JLJP, a patch pipette's K-gluconate among them; a free-diffusion junction constant in time, between the two | 0.015 mV; 2e-4 and 0.02 mV; 5e-3 mV |
|
|
259
263
|
| Liquid junction, open circuit | cell EMF 2t₊(RT/F) ln(c₁/c₂); Planck diffusion potential | 1e-4 |
|
|
260
264
|
| Salt diffusion mode from a `c0` profile | c̄ + a·sin(πx/2L) decaying at π²D/4L², D = 2D₊D₋/(D₊ + D₋) | 2e-5 |
|
package/docs/device.md
CHANGED
|
@@ -277,6 +277,68 @@ read from `sol.phi` (or a `'phi'` probe in a transient). A bath contact's termin
|
|
|
277
277
|
its reference species' level instead, what an electrode reversible to that species would read,
|
|
278
278
|
so between two different solutions it differs from $`\Delta\phi`$ by that species' Nernst term.
|
|
279
279
|
|
|
280
|
+
### Voltage-gated channels
|
|
281
|
+
|
|
282
|
+
A permeability can be gated, as Hodgkin and Huxley's Na⁺ and K⁺ channels are: the face lists its
|
|
283
|
+
`gates`, each a fraction $`x`$ in [0, 1] with first-order kinetics in the voltage across the face,
|
|
284
|
+
$`V = \phi_{\mathrm{right}} - \phi_{\mathrm{left}}`$ at its two edge nodes,
|
|
285
|
+
|
|
286
|
+
```math
|
|
287
|
+
\frac{dx}{dt} = \alpha(V)\,(1 - x) - \beta(V)\,x ,
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
and a link names the gates that scale its $`P`$ (or a conductance link's $`G`$), with their exponents:
|
|
291
|
+
$`P\,m^3 h`$ for `{ m: 3, h: 1 }`. (A membrane along a region instead, an axon's, is a
|
|
292
|
+
[port](#a-membrane-through-a-window-gated-channels).)
|
|
293
|
+
Each of $`\alpha`$ and $`\beta`$ is one of NeuroML's three standard forms, with `rate` in 1/s and `midpoint`
|
|
294
|
+
and `scale` in volts:
|
|
295
|
+
|
|
296
|
+
| `type` | rate |
|
|
297
|
+
| --- | --- |
|
|
298
|
+
| `'exp'` | $`\mathtt{rate}\; e^{(V - \mathtt{midpoint})/\mathtt{scale}}`$ |
|
|
299
|
+
| `'sigmoid'` | $`\mathtt{rate} / (1 + e^{(\mathtt{midpoint} - V)/\mathtt{scale}})`$ |
|
|
300
|
+
| `'expLinear'` | $`\mathtt{rate}\; y/(1 - e^{-y})`$, $`y = (V - \mathtt{midpoint})/\mathtt{scale}`$ |
|
|
301
|
+
|
|
302
|
+
With the outside on the left, $`V`$ is the membrane potential $`\phi_{\mathrm{in}} - \phi_{\mathrm{out}}`$. Hodgkin and
|
|
303
|
+
Huxley's squid axon (at their 6.3 °C):
|
|
304
|
+
|
|
305
|
+
```js nocheck
|
|
306
|
+
{
|
|
307
|
+
phi: { type: 'capacitive', C: 0.01 },
|
|
308
|
+
gates: {
|
|
309
|
+
m: { alpha: { type: 'expLinear', rate: 1000, midpoint: -0.040, scale: 0.010 }, beta: { type: 'exp', rate: 4000, midpoint: -0.065, scale: -0.018 } },
|
|
310
|
+
h: { alpha: { type: 'exp', rate: 70, midpoint: -0.065, scale: -0.020 }, beta: { type: 'sigmoid', rate: 1000, midpoint: -0.035, scale: 0.010 } },
|
|
311
|
+
n: { alpha: { type: 'expLinear', rate: 100, midpoint: -0.055, scale: 0.010 }, beta: { type: 'exp', rate: 125, midpoint: -0.065, scale: -0.080 } },
|
|
312
|
+
},
|
|
313
|
+
species: {
|
|
314
|
+
'Na+': { type: 'permeability', P: 1.16e-6, gates: { m: 3, h: 1 } },
|
|
315
|
+
'K+': { type: 'permeability', P: 1.33e-6, gates: { n: 4 } },
|
|
316
|
+
'Cl-': { type: 'permeability', P: 6.4e-9 }, // the leak
|
|
317
|
+
'A-': 'blocked',
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The gates scale permeabilities and nothing else: they hold no charge (the small gating current
|
|
323
|
+
of the channels' voltage sensors is left out, as Hodgkin and Huxley left it out), and each ion
|
|
324
|
+
still crosses by electrodiffusion down its own $`\bar\mu`$, so the dissipation stays positive whatever
|
|
325
|
+
they do. In steady state each gate is at $`\alpha/(\alpha + \beta)`$; each solution reports them as
|
|
326
|
+
`interfaces[f].gates` (`{ m, h, n }`) with the voltage they follow, `interfaces[f].V`.
|
|
327
|
+
|
|
328
|
+
Two things differ from the textbook equations. The currents are GHK's, which rectify: a channel's
|
|
329
|
+
conductance depends on the voltage, so a permeability matches a conductance $`g`$ at one voltage
|
|
330
|
+
only (above, the chord conductances at rest, −65 mV, for Hodgkin and Huxley's 120, 36 and 0.3
|
|
331
|
+
mS/cm²; matched at the reversal potentials instead, the rest depolarises itself and the axon
|
|
332
|
+
fires on its own). And a cell without a pump has no resting steady state: `solve()` finds its
|
|
333
|
+
Donnan equilibrium, every permeant ion level across the membrane, which the real cell reaches
|
|
334
|
+
only over hours. Start a transient from the concentrations given (it settles to rest within
|
|
335
|
+
milliseconds), or keep the gradients with the Na⁺/K⁺ pump on the same face. Started from the
|
|
336
|
+
same state, a squid axon's action potential follows the space-clamped Hodgkin–Huxley equations
|
|
337
|
+
with GHK currents to within 1 mV; and in small signals, with the membrane held at rest between
|
|
338
|
+
two baths, `impedance()` gives the gates' inductance that Cole measured (the impedance inductive
|
|
339
|
+
below ~50 Hz and resonant near 60 Hz), the linearised equations' to 5e-4
|
|
340
|
+
([`gates`](../test/gates.test.js) test).
|
|
341
|
+
|
|
280
342
|
### Faces next to a conductor
|
|
281
343
|
|
|
282
344
|
A conductor has no $`\phi`$, so a face beside it can't take a `dipole` or `step` alignment. Its $`\phi`$
|
|
@@ -432,6 +494,7 @@ any combination:
|
|
|
432
494
|
| `reactions`, `area` | an [electrode spread through the window](#electrodes-spread-through-a-window) | a thin film on a metal, a crevice, a porous electrode |
|
|
433
495
|
| `surface` | [species on that electrode's sites](#an-electrodes-surface-adsorbates-and-passive-films) | adsorbates, a passive film |
|
|
434
496
|
| `capacitance`, `area` | [charge held across a capacitance](#a-capacitance-through-a-window) | a gate along a channel, a double layer |
|
|
497
|
+
| `gates` | [voltage-gated channels](#a-membrane-through-a-window-gated-channels) on its conductance links | an axon's membrane along it |
|
|
435
498
|
|
|
436
499
|
```js nocheck
|
|
437
500
|
ports: [{
|
|
@@ -618,6 +681,44 @@ below threshold through saturation (second order in the grid); a porous electrod
|
|
|
618
681
|
impedance $`\sqrt{r/y}\coth(L\sqrt{ry})`$, the double layer in series with the ions' chemical
|
|
619
682
|
capacitance; and the gate's low-frequency impedance against $`dQ/dV_G`$ from steady states.
|
|
620
683
|
|
|
684
|
+
### A membrane through a window: gated channels
|
|
685
|
+
|
|
686
|
+
A port with a capacitance can carry [gates](#voltage-gated-channels), as a face can: at each node of
|
|
687
|
+
its window, each gate is a fraction open with Hodgkin–Huxley kinetics in the voltage across the
|
|
688
|
+
port's capacitance there, $`V_m = \phi - (V - \mathtt{zeroCharge})`$, and its conductance links name
|
|
689
|
+
the gates that scale them (`G × m³h`). That's a membrane all along a region, between it and an
|
|
690
|
+
outside held by the port: an axon's, with x running along the axon. Per volume of axoplasm, a
|
|
691
|
+
cylinder of radius a has $`2/a`$ of membrane (`area`); held at `V: 0` with `zeroCharge: 0`, the port's
|
|
692
|
+
V is the outside's $`\phi`$, and a charged species' `offset` is its level there against that,
|
|
693
|
+
$`(\mu^\circ + RT\ln(c_{out}/c_{\mathrm{ref}}))/(zF)`$. A conductance link's current is then
|
|
694
|
+
$`G\,(V_{i,out} - V_i)`$, which is $`g\,a_m (E_i - V_m)`$: Hodgkin and Huxley's linear channel, with $`E_i`$ the
|
|
695
|
+
Nernst potential at the inside's concentration there.
|
|
696
|
+
|
|
697
|
+
```js nocheck
|
|
698
|
+
ports: [{
|
|
699
|
+
name: 'membrane', region: 'axon', V: 0, terminal: 'K+', area: 2 / 238e-6, // a squid giant axon
|
|
700
|
+
capacitance: { C: 0.01, zeroCharge: 0 },
|
|
701
|
+
gates: hodgkinHuxley({ /* … */ T }).gates,
|
|
702
|
+
species: {
|
|
703
|
+
'Na+': { type: 'conductance', G: 1200 * 2 / 238e-6, offset: offsetNa, gates: { m: 3, h: 1 } },
|
|
704
|
+
'K+': { type: 'conductance', G: 360 * 2 / 238e-6, offset: offsetK, gates: { n: 4 } },
|
|
705
|
+
'Cl-': { type: 'conductance', G: 3 * 2 / 238e-6, offset: offsetCl }, // the leak
|
|
706
|
+
},
|
|
707
|
+
}]
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
With the axoplasm strictly neutral and the outside held (no resistance outside), the ions'
|
|
711
|
+
drift along x and the membrane's charging are cable theory, without its being put in: a squid
|
|
712
|
+
axon's action potential propagates at Hodgkin and Huxley's speed, 18.73 m/s for their 18.5 °C
|
|
713
|
+
axon (they computed 18.8 by hand), the cable equation's own to 2e-3
|
|
714
|
+
([`cable`](../test/cable.test.js) test). A myelinated axon is regions of one axoplasm, each node
|
|
715
|
+
of Ranvier and each internode its own, with a port each: the nodes' gated, the internodes' a small
|
|
716
|
+
capacitance (myelin) and nothing else. The grid refines at every node's faces, and the spike jumps
|
|
717
|
+
node to node, arriving at each within 1 µs of a compartmental cable's prediction (the same test).
|
|
718
|
+
Each solution's `ports[k]` reports the gates at each node of the window, `gates[name][j]`,
|
|
719
|
+
and the voltage they follow, `Vm[j]`. A gated window can't overlap another port's gated or
|
|
720
|
+
surfaced one.
|
|
721
|
+
|
|
621
722
|
### What a solution reports
|
|
622
723
|
|
|
623
724
|
Each solution's `ports[k]`, as the port has them:
|
|
@@ -630,6 +731,7 @@ Each solution's `ports[k]`, as the port has them:
|
|
|
630
731
|
| `rates[q][j]` | reaction q's forward rate at node j, mol/(m²·s) of electrode (reaction first, then node) |
|
|
631
732
|
| `coverage[name][j]`, `bare[j]` | with a surface: each species' coverage θ, and the bare fraction θ₀ to full precision |
|
|
632
733
|
| `sigma[j]`, `charge` | with a capacitance: σ per area of electrode (C/m²), and the total the port's side holds |
|
|
734
|
+
| `gates[name][j]`, `Vm[j]` | with gates: each gate's fraction open, and the voltage across the capacitance that they follow (V) |
|
|
633
735
|
|
|
634
736
|
The arrays are typed (`Float64Array`), so `Array.from` them before mapping to anything but
|
|
635
737
|
numbers. The kit's [`polarization()`](kit.md#polarization-curves-an-evans-diagram) gives each reaction's rate at a spot of the
|
|
@@ -762,7 +864,9 @@ const now = dev.solution(); // snapshot of the current state
|
|
|
762
864
|
`probes: [{ x, species, quantity, region }]` the trace also reads inside the device,
|
|
763
865
|
as `trace.probes[k]` beside `trace.t`: a species' concentration (`quantity: 'c'`, the default,
|
|
764
866
|
mol/m³) or species voltage (`'V'`), or $`\phi`$ itself (`'phi'`, no species), linearly between the
|
|
765
|
-
nodes around `x` (at an interface, `region` picks the side). That's what a detector at `x` sees.
|
|
867
|
+
nodes around `x` (at an interface, `region` picks the side). That's what a detector at `x` sees.
|
|
868
|
+
At a face, `{ interface: f, species }` reads a species' flux through it (mol/(m²·s), toward +x,
|
|
869
|
+
as `interfaces[f].N`), and `{ interface: f, gate }` a gate's fraction open. Probes read at accepted steps, which grow
|
|
766
870
|
as a transient slows, so give `dtMax` to resolve a signal in time.
|
|
767
871
|
- `impedance(frequencies, { terminal, profiles })` solves the steady state, then linearises about
|
|
768
872
|
it: $`Z(f) = \delta V/\delta I`$ in Ω·m² at one terminal (`'right'` by default), with I into the
|
package/docs/kit.md
CHANGED
|
@@ -142,6 +142,18 @@ Concentrations are held, so these are the curves of that instant (for a mixed po
|
|
|
142
142
|
kinetic picture). Away from the actual level the composition beside the electrode would change,
|
|
143
143
|
which is the transport the full solve accounts for.
|
|
144
144
|
|
|
145
|
+
## Channels
|
|
146
|
+
|
|
147
|
+
**`hodgkinHuxley({ inside, outside, T, g, leak, leakZ, at, names })`**: Hodgkin and Huxley's
|
|
148
|
+
squid-axon channels for a [membrane face](device.md#voltage-gated-channels) with the outside on its
|
|
149
|
+
left: `{ gates, species }`, the face's `gates` (m, h and n, their 1952 rates at 6.3 °C scaled to
|
|
150
|
+
`T` by Q₁₀ = 3) and links for Na⁺ (P·m³h), K⁺ (P·n⁴) and the leak (one ion, `leak`, of charge
|
|
151
|
+
`leakZ`: Cl⁻ by default). The currents are GHK's, which rectify, so each permeability is the one
|
|
152
|
+
whose chord conductance at `at` (−65 mV) is HH's `g` (120, 36 and 0.3 mS/cm²) at the
|
|
153
|
+
concentrations given. Spread it into the face beside its $`\phi`$ law, adding the other species'
|
|
154
|
+
links: `{ phi: { type: 'capacitive', C: 0.01 }, gates: hh.gates, species: { ...hh.species, 'A-': 'blocked' } }`.
|
|
155
|
+
`ghkCurrent(z, inside, outside, V, T)` is the GHK current (outward, A/m²) at unit permeability.
|
|
156
|
+
|
|
145
157
|
## Sources
|
|
146
158
|
|
|
147
159
|
Helpers that write sources as plain data:
|
package/docs/numerics.md
CHANGED
|
@@ -210,9 +210,16 @@ flux node's own rows are the interface laws:
|
|
|
210
210
|
| species, blocked or absent on one side | $`N_f = 0`$ |
|
|
211
211
|
| species, conductance | $`N_f - G V_T (\eta_L - \eta_R)/(z^2 F) = 0`$ |
|
|
212
212
|
| face reaction $`k`$ | $`r_k - \mathrm{rate}_k(\mathrm{state}_L, \mathrm{state}_R) = 0`$, and each participant's edge node takes $`\nu \cdot r_k`$ |
|
|
213
|
+
| gate $`q`$ | $`(x_q - x_q^{\mathrm{old}})/dt - \alpha(V)(1 - x_q) + \beta(V)\,x_q = 0`$, $`V = V_T(\hat\phi_R - \hat\phi_L)`$; a gated permeability's $`P`$ takes $`\prod x^p`$ |
|
|
213
214
|
|
|
214
215
|
Every row couples only neighbours, with no penalty terms. A face reaction's rate is an unknown
|
|
215
|
-
of the face block, so the two edge nodes it couples meet only through the block between them
|
|
216
|
+
of the face block, so the two edge nodes it couples meet only through the block between them;
|
|
217
|
+
so is a gate's fraction open, its row coupling it to the two edge nodes' $`\hat\phi`$, and the gated
|
|
218
|
+
fluxes to it, all within the face's block and its neighbours'. Newton's convergence and the time
|
|
219
|
+
step's error count a gate's change as they count a potential's in thermal units (1e-3 of $`m`$ is
|
|
220
|
+
0.3% of $`m^3`$), and a gate is evaluated in [0, 1] where Newton's iterates stray past its ends. A
|
|
221
|
+
membrane port's gates are unknowns of each window node's block, after its surface's coverages,
|
|
222
|
+
with the same row in the voltage across the port's capacitance there.
|
|
216
223
|
A species can take part in a reaction at a face and also cross it by a link (its flux slot)
|
|
217
224
|
at the same time; where it exists on both sides, that link must be given explicitly. A continuity
|
|
218
225
|
row has a zero diagonal block, but block Thomas still sees a non-singular block there once the
|
package/llms.txt
CHANGED
|
@@ -14,9 +14,9 @@ docs/conventions.md (signs and levels) and docs/data.md (the data library).
|
|
|
14
14
|
From a CDN, pin the version:
|
|
15
15
|
|
|
16
16
|
```js nocheck
|
|
17
|
-
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
18
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
19
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
17
|
+
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/index.js';
|
|
18
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/kit.js';
|
|
19
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/plot.js';
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
@@ -210,8 +210,50 @@ console.log(check(dev, sol).text); // per compartment: Na⁺ leaks into the cyto
|
|
|
210
210
|
```
|
|
211
211
|
|
|
212
212
|
Between two baths instead (the inside one `bath(inside, 'Cl-', { I: 0 })`: held composition, no
|
|
213
|
-
current), V_m is GHK. Without the pump, a closed cell runs down to Donnan equilibrium.
|
|
214
|
-
|
|
213
|
+
current), V_m is GHK. Without the pump, a closed cell runs down to Donnan equilibrium.
|
|
214
|
+
|
|
215
|
+
## Template: an action potential
|
|
216
|
+
|
|
217
|
+
Voltage-gated channels: a face's `gates` (each a fraction open with Hodgkin–Huxley kinetics,
|
|
218
|
+
dx/dt = α(V)(1 − x) − β(V)x, in V = φ_right − φ_left), and a permeability scaled by them
|
|
219
|
+
(`gates: { m: 3, h: 1 }`). `hodgkinHuxley()` in the kit writes the squid axon's: its gates, and
|
|
220
|
+
GHK permeabilities matched to HH's conductances at rest. A pump-less cell has no steady rest
|
|
221
|
+
(`solve()` finds Donnan), so start the transient from the concentrations given: the membrane
|
|
222
|
+
settles within milliseconds.
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
import { Device } from 'driftlet';
|
|
226
|
+
import { build, layer, bath, aqueous, injector, pulse, hodgkinHuxley } from 'driftlet/kit';
|
|
227
|
+
|
|
228
|
+
const T = 279.45; // HH's 6.3 °C
|
|
229
|
+
const out = { 'Na+': 440, 'K+': 20, 'Cl-': 450, 'A-': 10 }, inside = { 'Na+': 50, 'K+': 400, 'Cl-': 52, 'A-': 398 }; // squid, mM
|
|
230
|
+
const water = aqueous(['Na+', 'K+', 'Cl-'], { epsr: 0 });
|
|
231
|
+
water.species.push({ name: 'A-', z: -1, cRef: 1000 });
|
|
232
|
+
water.materials.water.species['A-'] = { D: 0.5e-9, mu0: 0 };
|
|
233
|
+
const hh = hodgkinHuxley({ inside, outside: out, T }); // outside on the left
|
|
234
|
+
const axon = new Device(
|
|
235
|
+
build({
|
|
236
|
+
T,
|
|
237
|
+
library: [water],
|
|
238
|
+
stack: [
|
|
239
|
+
bath(out, 'Cl-'),
|
|
240
|
+
layer('water', 5e-6, { name: 'outside', c0: out }),
|
|
241
|
+
{ phi: { type: 'capacitive', C: 0.01 }, gates: hh.gates, species: { ...hh.species, 'A-': 'blocked' } },
|
|
242
|
+
layer('water', 20e-6, { name: 'cytoplasm', c0: inside }),
|
|
243
|
+
{ species: { 'Na+': 'blocked', 'K+': 'blocked', 'Cl-': 'blocked', 'A-': 'blocked' }, phi: 'neutral' },
|
|
244
|
+
],
|
|
245
|
+
// the stimulus: 10 µA/cm² of K⁺ for 1 ms at 20 ms, from a pipette in the cytoplasm
|
|
246
|
+
ports: [injector({ region: 'cytoplasm', from: 15e-6, to: 20e-6, species: 'K+', I: pulse({ start: 20e-3, width: 1e-3, amplitude: 0.1 }) })],
|
|
247
|
+
grid: { hmin: 50e-9, hmax: 1e-6 },
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
let peak = -1;
|
|
251
|
+
for (let k = 1; k <= 300; k++) {
|
|
252
|
+
const sol = axon.advance(k * 1e-4); // 0.1 ms at a time
|
|
253
|
+
peak = Math.max(peak, sol.interfaces[0].V); // V_m, and sol.interfaces[0].gates = { m, h, n }
|
|
254
|
+
}
|
|
255
|
+
console.log(`peak ${(peak * 1e3).toFixed(1)} mV`); // ~ +12 mV: GHK's Na⁺ current is limited by the 50 mM inside
|
|
256
|
+
```
|
|
215
257
|
|
|
216
258
|
## Template: an electrode in a stirred solution
|
|
217
259
|
|
|
@@ -423,9 +465,9 @@ solve), as long as it settles rather than ping-pongs.
|
|
|
423
465
|
<span id="readout"></span>
|
|
424
466
|
<div id="figure"></div>
|
|
425
467
|
<script type="module">
|
|
426
|
-
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
427
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
428
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
468
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/index.js';
|
|
469
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/kit.js';
|
|
470
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.12.0/src/plot.js';
|
|
429
471
|
|
|
430
472
|
const def = build({
|
|
431
473
|
T: 300,
|
package/package.json
CHANGED
package/src/channels.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// driftlet/kit: voltage-gated channels as plain definition data. Hodgkin and Huxley's squid axon:
|
|
2
|
+
// its gates, and permeabilities for its Na⁺, K⁺ and leak channels, for a membrane face.
|
|
3
|
+
|
|
4
|
+
import { DeviceError } from './errors.js';
|
|
5
|
+
import { FARADAY, GAS_CONSTANT } from './constants.js';
|
|
6
|
+
|
|
7
|
+
const fail = (message) => {
|
|
8
|
+
throw new DeviceError(message);
|
|
9
|
+
};
|
|
10
|
+
const need = (ok, message) => ok || fail(message);
|
|
11
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
12
|
+
|
|
13
|
+
// Hodgkin and Huxley (1952), J. Physiol. 117, 500, at their 6.3 °C, in the modern convention
|
|
14
|
+
// (V = φ_in − φ_out, rest at −65 mV), in SI: rates in 1/s, voltages in V.
|
|
15
|
+
const mV = 1e-3;
|
|
16
|
+
const HH_GATES = {
|
|
17
|
+
m: { alpha: { type: 'expLinear', rate: 1000, midpoint: -40 * mV, scale: 10 * mV }, beta: { type: 'exp', rate: 4000, midpoint: -65 * mV, scale: -18 * mV } },
|
|
18
|
+
h: { alpha: { type: 'exp', rate: 70, midpoint: -65 * mV, scale: -20 * mV }, beta: { type: 'sigmoid', rate: 1000, midpoint: -35 * mV, scale: 10 * mV } },
|
|
19
|
+
n: { alpha: { type: 'expLinear', rate: 100, midpoint: -55 * mV, scale: 10 * mV }, beta: { type: 'exp', rate: 125, midpoint: -65 * mV, scale: -80 * mV } },
|
|
20
|
+
};
|
|
21
|
+
const HH_T = 273.15 + 6.3;
|
|
22
|
+
|
|
23
|
+
// The GHK current (outward, A/m²) of an ion at unit permeability, at voltage V (inside − outside).
|
|
24
|
+
export function ghkCurrent(z, inside, outside, V, T) {
|
|
25
|
+
const u = (z * V * FARADAY) / (GAS_CONSTANT * T);
|
|
26
|
+
if (Math.abs(u) < 1e-9) return z * FARADAY * (inside - outside);
|
|
27
|
+
return (z * FARADAY * u * (inside - outside * Math.exp(-u))) / -Math.expm1(-u);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hodgkin and Huxley's squid-axon channels, for a membrane face with the outside on its left
|
|
32
|
+
* (so the gates' voltage, φ_right − φ_left, is the membrane potential): the face's `gates`, and
|
|
33
|
+
* `species` links for Na⁺ (P·m³h), K⁺ (P·n⁴) and the leak (ungated), to spread into the face
|
|
34
|
+
* beside its φ law, `{ phi: { type: 'capacitive', C: 0.01 }, ...hh }`, adding links (or blocks)
|
|
35
|
+
* for any other species.
|
|
36
|
+
*
|
|
37
|
+
* The currents are GHK's, which rectify, so a permeability matches a conductance at one voltage
|
|
38
|
+
* only: each is the one whose chord conductance at `at` (rest, −65 mV) is Hodgkin and Huxley's
|
|
39
|
+
* (`g`: 120, 36 and 0.3 mS/cm²), at the concentrations given. The leak is carried by one ion
|
|
40
|
+
* (`leak`, of charge `leakZ`: Cl⁻ by default, whose Nernst level in squid is near their leak
|
|
41
|
+
* reversal, −54 mV). The
|
|
42
|
+
* rates are theirs at 6.3 °C, scaled to `T` by Q₁₀ = 3.
|
|
43
|
+
* @param {{ inside: object, outside: object, T?: number, g?: { Na?: number, K?: number, leak?: number },
|
|
44
|
+
* leak?: string, leakZ?: number, at?: number, names?: { Na?: string, K?: string } }} opts concentrations in
|
|
45
|
+
* mol/m³ by species name, K, S/m², V
|
|
46
|
+
* @returns {{ gates: object, species: object }}
|
|
47
|
+
*/
|
|
48
|
+
export function hodgkinHuxley({ inside, outside, T = HH_T, g = {}, leak = 'Cl-', leakZ = -1, at = -65 * mV, names = {} } = {}) {
|
|
49
|
+
need(inside && outside && typeof inside === 'object' && typeof outside === 'object', 'hodgkinHuxley: give inside and outside concentrations (mol/m³) by species name');
|
|
50
|
+
need(num(T) && T > 0, `hodgkinHuxley: T must be a temperature in K, got ${T}`);
|
|
51
|
+
need(num(at), `hodgkinHuxley: at must be a voltage (V), got ${at}`);
|
|
52
|
+
const G = { Na: 1200, K: 360, leak: 3, ...g };
|
|
53
|
+
const Na = names.Na ?? 'Na+', K = names.K ?? 'K+';
|
|
54
|
+
const q10 = Math.exp(Math.log(3) * ((T - HH_T) / 10)); // Q₁₀ = 3
|
|
55
|
+
const scaled = (r) => ({ ...r, rate: r.rate * q10 });
|
|
56
|
+
const gates = Object.fromEntries(Object.entries(HH_GATES).map(([k, gt]) => [k, { alpha: scaled(gt.alpha), beta: scaled(gt.beta) }]));
|
|
57
|
+
const P = (name, z, gval, what) => {
|
|
58
|
+
need(num(inside[name]) && inside[name] > 0 && num(outside[name]) && outside[name] > 0, `hodgkinHuxley: give ${name}'s concentration inside and outside (mol/m³), for its ${what}`);
|
|
59
|
+
need(num(gval) && gval > 0, `hodgkinHuxley: g.${what} must be a conductance > 0 (S/m²), got ${gval}`);
|
|
60
|
+
const E = ((GAS_CONSTANT * T) / (z * FARADAY)) * Math.log(outside[name] / inside[name]);
|
|
61
|
+
need(Math.abs(at - E) > 1e-4, `hodgkinHuxley: ${name}'s Nernst level, ${(E * 1e3).toFixed(1)} mV, is at the calibration voltage; choose another 'at'`);
|
|
62
|
+
return (gval * (at - E)) / ghkCurrent(z, inside[name], outside[name], at, T);
|
|
63
|
+
};
|
|
64
|
+
need(Number.isInteger(leakZ) && leakZ !== 0, `hodgkinHuxley: leakZ must be the leak ion's charge number, got ${leakZ}`);
|
|
65
|
+
return {
|
|
66
|
+
gates,
|
|
67
|
+
species: {
|
|
68
|
+
[Na]: { type: 'permeability', P: P(Na, 1, G.Na, 'Na channels'), gates: { m: 3, h: 1 } },
|
|
69
|
+
[K]: { type: 'permeability', P: P(K, 1, G.K, 'K channels'), gates: { n: 4 } },
|
|
70
|
+
[leak]: { type: 'permeability', P: P(leak, leakZ, G.leak, 'leak') },
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/describe.js
CHANGED
|
@@ -201,10 +201,12 @@ export function describe(def) {
|
|
|
201
201
|
const rx = (idef.reactions ?? []).map((r) => (r.equation ?? equation(r.left, r.right)) + kinetics(r));
|
|
202
202
|
const laws = species.flatMap((sp, i) => {
|
|
203
203
|
const l = itf.links[i];
|
|
204
|
-
|
|
204
|
+
const gated = (l.gates ?? []).map(([q, p]) => itf.gates[q].name + (p > 1 ? `^${p}` : '')).join('·');
|
|
205
|
+
return l.type === 'permeability' ? [`${sp.name} ${num(l.P)} m/s${gated ? ` × ${gated}` : ''}`] : l.type === 'conductance' ? [`${sp.name} ${num(l.G)} S/m²`] : [];
|
|
205
206
|
});
|
|
207
|
+
const gates = (itf.gates ?? []).map((g) => g.name);
|
|
206
208
|
lines.push(
|
|
207
|
-
` ${L.name} | ${R.name}: ${law}${align}${laws.length ? `; crossing by a law: ${laws.join(', ')}` : ''}${blocked.length ? `; blocked: ${blocked.join(', ')}` : ''}${rx.length ? `; reactions: ${rx.join('; ')}` : ''}`,
|
|
209
|
+
` ${L.name} | ${R.name}: ${law}${align}${laws.length ? `; crossing by a law: ${laws.join(', ')}` : ''}${blocked.length ? `; blocked: ${blocked.join(', ')}` : ''}${rx.length ? `; reactions: ${rx.join('; ')}` : ''}${gates.length ? `; gates (voltage-dependent, see interfaces[f].gates): ${gates.join(', ')}` : ''}`,
|
|
208
210
|
);
|
|
209
211
|
});
|
|
210
212
|
}
|
package/src/device.js
CHANGED
|
@@ -348,12 +348,12 @@ export function normalizeDevice(def) {
|
|
|
348
348
|
need(nodes.length > 0, `ports[${k}]: the window [${port.from}, ${port.to}] m holds no grid node; widen it or refine the grid`);
|
|
349
349
|
port.nodes = Int32Array.from(nodes);
|
|
350
350
|
});
|
|
351
|
-
// A node's surface belongs to one electrode.
|
|
351
|
+
// A node's surface belongs to one electrode, and its gates to one membrane.
|
|
352
352
|
const surfaced = new Int32Array(grid.nNodes).fill(-1);
|
|
353
353
|
ports.forEach((port, k) => {
|
|
354
|
-
if (port.surface.length === 0) return;
|
|
354
|
+
if (port.surface.length === 0 && port.gates.length === 0) return;
|
|
355
355
|
for (const g of port.nodes) {
|
|
356
|
-
need(surfaced[g] < 0, `ports[${k}]: its window overlaps ports[${surfaced[g]}]'s, and both have a surface; a spot of metal has one surface`);
|
|
356
|
+
need(surfaced[g] < 0, `ports[${k}]: its window overlaps ports[${surfaced[g]}]'s, and both have a surface or gates; a spot of metal has one surface, and of a membrane one set of channels`);
|
|
357
357
|
surfaced[g] = k;
|
|
358
358
|
}
|
|
359
359
|
});
|
|
@@ -669,7 +669,7 @@ function normalizeGeometry(g) {
|
|
|
669
669
|
// the window, `area` (m²/m³) of it per volume.
|
|
670
670
|
function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT) {
|
|
671
671
|
need(isObject(pdef), `${path} must be an object`);
|
|
672
|
-
fields(pdef, path, ['name', 'region', 'from', 'to', 'V', 'I', 'R', 'terminal', 'species', 'reactions', 'area', 'surface', 'capacitance']);
|
|
672
|
+
fields(pdef, path, ['name', 'region', 'from', 'to', 'V', 'I', 'R', 'terminal', 'species', 'reactions', 'area', 'surface', 'capacitance', 'gates']);
|
|
673
673
|
let r;
|
|
674
674
|
if (Number.isInteger(pdef.region)) r = pdef.region;
|
|
675
675
|
else r = regions.findIndex((reg) => reg.name === pdef.region);
|
|
@@ -690,6 +690,9 @@ function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT
|
|
|
690
690
|
const reactions = portReactions(pdef, path, mat, terminal, species, speciesIndex, RT, surface);
|
|
691
691
|
surface.forEach((sp, s) => need(reactions.some((rx) => rx.part.some((p) => p.side === 2 && p.s === s)), `${path}.surface.${sp.name}: no reaction of the port makes or uses it`));
|
|
692
692
|
const capacitance = portCapacitance(pdef, path, mat);
|
|
693
|
+
// Gates (a membrane's channels): they follow the voltage across the port's capacitance.
|
|
694
|
+
const gates = normalizeGates(pdef.gates, path, mat, mat);
|
|
695
|
+
need(gates.length === 0 || capacitance !== null, `${path}.gates: a gate follows the voltage across the port's capacitance (a membrane's), so give the port one`);
|
|
693
696
|
need(pdef.species === undefined ? reactions.length > 0 || capacitance !== null : isObject(pdef.species), `${path}.species must map species names to port links`);
|
|
694
697
|
const links = species.map(() => ({ type: 'blocked' }));
|
|
695
698
|
for (const [sname, raw] of Object.entries(pdef.species ?? {})) {
|
|
@@ -698,7 +701,7 @@ function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT
|
|
|
698
701
|
const i = speciesIndex.get(sname);
|
|
699
702
|
const link = typeof raw === 'string' ? { type: raw } : raw;
|
|
700
703
|
need(isObject(link) && ['blocked', 'equilibrium', 'conductance', 'exchange'].includes(link.type), `${lpath}.type must be one of blocked, equilibrium, conductance, exchange`);
|
|
701
|
-
fields(link, lpath, LINK_FIELDS[link.type]);
|
|
704
|
+
fields(link, lpath, link.type === 'conductance' ? [...LINK_FIELDS.conductance, 'gates'] : LINK_FIELDS[link.type]);
|
|
702
705
|
if (link.type === 'blocked') continue;
|
|
703
706
|
need(mat.present[i], `${lpath}: '${sname}' is absent from ${reg.name} (material '${mat.name}')`);
|
|
704
707
|
if (mat.conductor) need(i === mat.conductor.i, `${lpath}: a conductor exchanges only its carrier, ${species[mat.conductor.i].name}`);
|
|
@@ -718,6 +721,7 @@ function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT
|
|
|
718
721
|
// On a metal, G is a lumped conductance per area (S/m²), spread over its thickness.
|
|
719
722
|
if (link.type === 'conductance') level.G = mat.conductor ? positive(link.G, `${lpath}.G (S/m², for a conductor)`) / reg.length : positive(link.G, `${lpath}.G (S/m³)`);
|
|
720
723
|
if (link.type === 'exchange') level.k = positive(link.k, `${lpath}.k (mol/(m³·s))`);
|
|
724
|
+
if (link.type === 'conductance') level.gates = gatedBy(link.gates, gates, lpath, path);
|
|
721
725
|
links[i] = { type: link.type, ...level };
|
|
722
726
|
}
|
|
723
727
|
need(links.some((l) => l.type !== 'blocked') || reactions.length > 0 || capacitance !== null, `${path}.species: the port exchanges no species`);
|
|
@@ -731,7 +735,7 @@ function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT
|
|
|
731
735
|
area = typeof pdef.area === 'number' ? { value: positive(pdef.area, `${path}.area (m²/m³)`) } : profile(pdef.area, `${path}.area`, 'areas per volume (m²/m³)', (a) => a >= 0, 'an area per volume ≥ 0');
|
|
732
736
|
} else need(pdef.area === undefined, `${path}.area: only an electrode (a port with reactions or a capacitance) has an area`);
|
|
733
737
|
need(surface.length === 0 || reactions.length > 0, `${path}.surface: only an electrode (a port with reactions) has a surface`);
|
|
734
|
-
return { name: pdef.name ?? path, region: r, from, to, span: reg.length, drive, terminal, species: links, passes, reactions, area, surface, capacitance };
|
|
738
|
+
return { name: pdef.name ?? path, region: r, from, to, span: reg.length, drive, terminal, species: links, passes, reactions, area, surface, capacitance, gates };
|
|
735
739
|
}
|
|
736
740
|
|
|
737
741
|
// A capacitance spread through the window: a gate along a channel, an electrode's double layer.
|
|
@@ -860,7 +864,7 @@ function normalizeInterface(idef, f, regions, materials, species, speciesIndex,
|
|
|
860
864
|
// macroscopic limit; the alignment then drops out. 'capacitive': a Helmholtz layer,
|
|
861
865
|
// D = C (Δφ − dipole). Between two ε = 0 (strictly neutral) materials the default is neutral.
|
|
862
866
|
if (matL.conductor || matR.conductor) return normalizeConductorInterface(idef, where, matL, matR, species, speciesIndex, RT);
|
|
863
|
-
fields(idef, where, ['phi', 'dipole', 'step', 'sheetCharge', 'species', 'reactions']);
|
|
867
|
+
fields(idef, where, ['phi', 'dipole', 'step', 'sheetCharge', 'species', 'reactions', 'gates']);
|
|
864
868
|
const bothNeutral = (matL.epsr === 0 && matR.epsr === 0) || matL.phiFree || matR.phiFree;
|
|
865
869
|
const rawPhi = idef.phi ?? (bothNeutral ? 'neutral' : 'pinned');
|
|
866
870
|
const phi = typeof rawPhi === 'string' ? { type: rawPhi } : { ...rawPhi };
|
|
@@ -916,6 +920,7 @@ function normalizeInterface(idef, f, regions, materials, species, speciesIndex,
|
|
|
916
920
|
// blocked, an ohmic interface conductance G (S/m²), or a permeability P (m/s): electrodiffusion
|
|
917
921
|
// through a thin membrane in a constant field, Goldman–Hodgkin–Katz.
|
|
918
922
|
const links = defaultInterfaceLinks(matL, matR, species);
|
|
923
|
+
const gates = normalizeGates(idef.gates, where, matL, matR);
|
|
919
924
|
if (idef.species !== undefined) {
|
|
920
925
|
need(isObject(idef.species), `${where}.species must map species names to interface links`);
|
|
921
926
|
for (const [sname, raw] of Object.entries(idef.species)) {
|
|
@@ -924,7 +929,7 @@ function normalizeInterface(idef, f, regions, materials, species, speciesIndex,
|
|
|
924
929
|
const i = speciesIndex.get(sname);
|
|
925
930
|
const link = typeof raw === 'string' ? { type: raw } : raw;
|
|
926
931
|
need(isObject(link) && INTERFACE_LINK_TYPES.has(link.type), `${lpath}.type must be one of ${[...INTERFACE_LINK_TYPES].join(', ')}`);
|
|
927
|
-
fields(link, lpath, link.type === 'conductance' ? ['type', 'G'] : link.type === 'permeability' ? ['type', 'P'] : ['type']);
|
|
932
|
+
fields(link, lpath, link.type === 'conductance' ? ['type', 'G', 'gates'] : link.type === 'permeability' ? ['type', 'P', 'gates'] : ['type']);
|
|
928
933
|
if (link.type !== 'blocked') need(matL.present[i] && matR.present[i], `${lpath}: '${sname}' must be present on both sides`);
|
|
929
934
|
if (link.type === 'conductance') {
|
|
930
935
|
need(species[i].z !== 0, `${lpath}: a conductance link needs a charged species`);
|
|
@@ -934,13 +939,52 @@ function normalizeInterface(idef, f, regions, materials, species, speciesIndex,
|
|
|
934
939
|
positive(link.P, `${lpath}.P (m/s)`);
|
|
935
940
|
need(matL.modelOf[i] < 0 && matR.modelOf[i] < 0, `${lpath}: a permeability link needs ideal (dilute) statistics for '${sname}' on both sides`);
|
|
936
941
|
}
|
|
937
|
-
|
|
942
|
+
// Gated: P (or G) × Π x_q^p over the face's gates (a channel open with probability m³h, say).
|
|
943
|
+
links[i] = { ...link, gates: gatedBy(link.gates, gates, lpath, where) };
|
|
938
944
|
}
|
|
939
945
|
}
|
|
940
946
|
|
|
941
947
|
const reactions = normalizeFaceReactions(idef, where, matL, matR, species, speciesIndex, RT);
|
|
942
948
|
checkReactingLinks(reactions, idef, where, matL, matR, species);
|
|
943
|
-
return { phi, dipole, sheetCharge, links, reactions, conductor: null };
|
|
949
|
+
return { phi, dipole, sheetCharge, links, reactions, gates, conductor: null };
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// A face's gates (Hodgkin–Huxley): each a fraction x in [0, 1] with first-order kinetics in the
|
|
953
|
+
// voltage across the face, V = φ_right − φ_left,
|
|
954
|
+
// dx/dt = α(V) (1 − x) − β(V) x,
|
|
955
|
+
// α and β each one of NeuroML's three standard forms, with rate in 1/s and midpoint and scale in
|
|
956
|
+
// volts: 'exp', rate·e^((V − midpoint)/scale); 'sigmoid', rate/(1 + e^((midpoint − V)/scale));
|
|
957
|
+
// 'expLinear', rate·y/(1 − e^(−y)) with y = (V − midpoint)/scale. They scale permeabilities
|
|
958
|
+
// (a link's gates), and nothing else: a gate holds no charge and exchanges no free energy.
|
|
959
|
+
const GATE_RATE_TYPES = ['exp', 'sigmoid', 'expLinear'];
|
|
960
|
+
// A link's gates, { m: 3, h: 1 }, as [[gate index, exponent]], against the gates defined at `where`.
|
|
961
|
+
function gatedBy(raw, gates, lpath, where) {
|
|
962
|
+
if (raw === undefined) return [];
|
|
963
|
+
need(isObject(raw), `${lpath}.gates must map gate names to exponents, e.g. { m: 3, h: 1 }`);
|
|
964
|
+
return Object.entries(raw).map(([gname, p]) => {
|
|
965
|
+
const q = gates.findIndex((gt) => gt.name === gname);
|
|
966
|
+
need(q >= 0, `${lpath}.gates.${gname}: no such gate${gates.length ? ` (there are ${gates.map((gt) => gt.name).join(', ')})` : ` (define it in ${where}.gates)`}`);
|
|
967
|
+
need(Number.isInteger(p) && p >= 1 && p <= 8, `${lpath}.gates.${gname}: the exponent must be a whole number from 1 to 8, got ${JSON.stringify(p)}`);
|
|
968
|
+
return [q, p];
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
function normalizeGates(raw, where, matL, matR) {
|
|
972
|
+
if (raw === undefined) return [];
|
|
973
|
+
need(isObject(raw), `${where}.gates must map gate names to { alpha, beta }`);
|
|
974
|
+
need(!matL.phiFree && !matR.phiFree, `${where}.gates: a gate follows the voltage across the face, which needs φ on both sides`);
|
|
975
|
+
return Object.entries(raw).map(([name, gdef]) => {
|
|
976
|
+
const gpath = `${where}.gates.${name}`;
|
|
977
|
+
need(isObject(gdef), `${gpath} must be { alpha, beta }`);
|
|
978
|
+
fields(gdef, gpath, ['alpha', 'beta']);
|
|
979
|
+
const rate = (r, rpath) => {
|
|
980
|
+
need(isObject(r) && GATE_RATE_TYPES.includes(r.type), `${rpath} must be { type, rate, midpoint, scale } with type one of ${GATE_RATE_TYPES.join(', ')}`);
|
|
981
|
+
fields(r, rpath, ['type', 'rate', 'midpoint', 'scale']);
|
|
982
|
+
const scale = finite(r.scale, `${rpath}.scale (V)`);
|
|
983
|
+
need(scale !== 0, `${rpath}.scale (V) must not be zero`);
|
|
984
|
+
return { type: r.type, rate: positive(r.rate, `${rpath}.rate (1/s)`), midpoint: finite(r.midpoint, `${rpath}.midpoint (V)`), scale };
|
|
985
|
+
};
|
|
986
|
+
return { name, alpha: rate(gdef.alpha, `${gpath}.alpha`), beta: rate(gdef.beta, `${gpath}.beta`) };
|
|
987
|
+
});
|
|
944
988
|
}
|
|
945
989
|
|
|
946
990
|
// A conductor (a metal, or a fast ion conductor): only its one mobile carrier, whose single
|
|
@@ -1036,7 +1080,7 @@ function normalizeConductorInterface(idef, where, matL, matR, species, speciesIn
|
|
|
1036
1080
|
}
|
|
1037
1081
|
const reactions = normalizeFaceReactions(idef, where, matL, matR, species, speciesIndex, RT);
|
|
1038
1082
|
checkReactingLinks(reactions, idef, where, matL, matR, species);
|
|
1039
|
-
return { phi, dipole: 0, zeroCharge, sheetCharge: 0, links, reactions, conductor: { side, i: metal.i } };
|
|
1083
|
+
return { phi, dipole: 0, zeroCharge, sheetCharge: 0, links, reactions, gates: [], conductor: { side, i: metal.i } };
|
|
1040
1084
|
}
|
|
1041
1085
|
|
|
1042
1086
|
function defaultInterfaceLinks(matL, matR, species) {
|
package/src/kit.js
CHANGED
|
@@ -10,6 +10,7 @@ export { parseEquation, stoichiometry, half, SHE, level } from './equation.js';
|
|
|
10
10
|
export { traces, speciesRole, typeset } from './traces.js';
|
|
11
11
|
export { live } from './live.js';
|
|
12
12
|
export { pulse, square, triangle, ramp, injector, recombination, photogeneration } from './sources.js';
|
|
13
|
+
export { hodgkinHuxley, ghkCurrent } from './channels.js';
|
|
13
14
|
export { describe, unitWarnings } from './describe.js';
|
|
14
15
|
export { recorder } from './recorder.js';
|
|
15
16
|
export { henderson, planck } from './junction.js';
|
package/src/recorder.js
CHANGED
|
@@ -15,7 +15,7 @@ const now = () => (typeof performance !== 'undefined' ? performance.now() : Date
|
|
|
15
15
|
* neither, at the end of each `advance()` call. The trace (`t`, `current`, `voltage`, and
|
|
16
16
|
* `probes` if given) runs from the start through every accepted step of every call.
|
|
17
17
|
* @param {import('./index.js').Device} device
|
|
18
|
-
* @param {{ every?: number, times?: number[], probes?: { x: number, species?: string, quantity?: 'c' | 'V' | 'phi', region?: string | number }[] }} [opts]
|
|
18
|
+
* @param {{ every?: number, times?: number[], probes?: ({ x: number, species?: string, quantity?: 'c' | 'V' | 'phi', region?: string | number } | { interface: number, species?: string, gate?: string })[] }} [opts]
|
|
19
19
|
*/
|
|
20
20
|
export function recorder(device, opts = {}) {
|
|
21
21
|
const { every, times, probes } = opts;
|
package/src/solution.js
CHANGED
|
@@ -118,6 +118,11 @@ export function makeSolution(solver, result = {}) {
|
|
|
118
118
|
const sets = (side) => ['pinned', 'bulk'].includes(model.contacts[side].phi.type);
|
|
119
119
|
out.sigma = Float64Array.from(port.nodes, (g) => ((g === 0 && sets('left')) || (g === last && sets('right')) ? 0 : Cs * (solver.termV[2 + k] - zeroCharge - phi[g])));
|
|
120
120
|
}
|
|
121
|
+
if (port.gates.length > 0) {
|
|
122
|
+
// A membrane's gates at each node, and the voltage they follow, φ − (V − zeroCharge).
|
|
123
|
+
out.gates = Object.fromEntries(port.gates.map((gate, q) => [gate.name, Float64Array.from(port.nodes, (g) => u[solver.blockOfNode[g] * M + 1 + n + port.surface.length + q])]));
|
|
124
|
+
out.Vm = Float64Array.from(port.nodes, (g) => solver._portVoltage(k, g));
|
|
125
|
+
}
|
|
121
126
|
if (port.area !== null) {
|
|
122
127
|
out.x = Float64Array.from(port.nodes, (g) => model.grid.x[g]);
|
|
123
128
|
out.area = Float64Array.from(solver.portArea[k]);
|
|
@@ -148,7 +153,13 @@ export function makeSolution(solver, result = {}) {
|
|
|
148
153
|
const N = {};
|
|
149
154
|
for (let i = 0; i < n; i++) N[species[i].name] = u[b * M + 1 + i];
|
|
150
155
|
const rates = itf.reactions.map((_, k) => u[b * M + 1 + n + k]); // mol/(m²·s), forward
|
|
151
|
-
|
|
156
|
+
const out = { left: model.regions[f].name, right: model.regions[f + 1].name, dipole: itf.dipole, sheetCharge: itf.sheetCharge, D: u[b * M], N, rates };
|
|
157
|
+
if (itf.gates.length > 0) {
|
|
158
|
+
// Each gate's fraction open, and the voltage across the face that drives them.
|
|
159
|
+
out.gates = Object.fromEntries(itf.gates.map((gate, q) => [gate.name, u[solver._gateSlot(f, q)]]));
|
|
160
|
+
out.V = solver._faceVoltage(f);
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
152
163
|
});
|
|
153
164
|
|
|
154
165
|
// Bulk reactions: each one's forward rate at every node (mol/(m³·s), NaN where it doesn't run),
|
package/src/solver.js
CHANGED
|
@@ -261,17 +261,39 @@ export class SolverError extends Error {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// A gate's rate law (see normalizeGates in device.js) at voltage V: [rate, ∂rate/∂V], 1/s and 1/(s·V).
|
|
265
|
+
function gateRate(law, V) {
|
|
266
|
+
const { rate, midpoint, scale } = law;
|
|
267
|
+
if (law.type === 'exp') {
|
|
268
|
+
const v = rate * Math.exp((V - midpoint) / scale);
|
|
269
|
+
return [v, v / scale];
|
|
270
|
+
}
|
|
271
|
+
if (law.type === 'sigmoid') {
|
|
272
|
+
const e = Math.exp((midpoint - V) / scale), d = 1 + e;
|
|
273
|
+
return Number.isFinite(e) ? [rate / d, (rate * e) / (scale * d * d)] : [0, 0];
|
|
274
|
+
}
|
|
275
|
+
// expLinear: rate·y/(1 − e^−y), smooth through y = 0 (rate there) and → rate·y for large y
|
|
276
|
+
const y = (V - midpoint) / scale;
|
|
277
|
+
if (Math.abs(y) < 1e-6) return [rate * (1 + y / 2), rate / (2 * scale)];
|
|
278
|
+
if (y < -700) return [0, 0];
|
|
279
|
+
const em = -Math.expm1(-y), ey = Math.exp(-y); // 1 − e^−y, e^−y
|
|
280
|
+
return [(rate * y) / em, (rate * (em - y * ey)) / (em * em * scale)];
|
|
281
|
+
}
|
|
282
|
+
|
|
264
283
|
export class Solver {
|
|
265
284
|
constructor(model) {
|
|
266
285
|
this.model = model;
|
|
267
286
|
const { grid, species, materials, regions, contacts } = model;
|
|
268
287
|
const n = species.length;
|
|
269
288
|
// Slots per block: φ̂ (or D, or a conductor's segment flux J), one per species, and at a face
|
|
270
|
-
// one per face reaction (its rate), as many as the
|
|
271
|
-
|
|
289
|
+
// one per face reaction (its rate) and one per gate (its fraction open), as many as the
|
|
290
|
+
// busiest face has.
|
|
291
|
+
const nRx = model.interfaces.reduce((m, itf) => Math.max(m, itf.reactions.length + itf.gates.length), 0);
|
|
272
292
|
// (and at an electrode port's nodes, one per surface species: its coverage's η)
|
|
273
293
|
const nSurf = model.ports.reduce((m, port) => Math.max(m, port.surface.length), 0);
|
|
274
|
-
|
|
294
|
+
// (and a membrane port's gates, after them: each one's fraction open)
|
|
295
|
+
const nPortSlots = model.ports.reduce((m, port) => Math.max(m, port.surface.length + port.gates.length), 0);
|
|
296
|
+
const M = n + 1 + Math.max(nRx, nPortSlots);
|
|
275
297
|
const nNodes = grid.nNodes;
|
|
276
298
|
const nFaces = regions.length - 1;
|
|
277
299
|
const nB = nNodes + nFaces;
|
|
@@ -306,6 +328,20 @@ export class Solver {
|
|
|
306
328
|
// The bare fraction θ₀, directly (1 − Σθ cancels as θ → 1); 1 where there's no surface.
|
|
307
329
|
this.th0 = new Float64Array(nNodes).fill(1);
|
|
308
330
|
this.th0Old = new Float64Array(nNodes).fill(1);
|
|
331
|
+
// Gates: each one's slot (at a face, after its reaction rates; at a membrane port's nodes, after
|
|
332
|
+
// the surface's coverages) and value at the step's start. Faces' first, then ports', node by
|
|
333
|
+
// node through each window.
|
|
334
|
+
this.gatePort = new Int32Array(nNodes).fill(-1);
|
|
335
|
+
model.ports.forEach((port, k) => port.gates.length > 0 && port.nodes.forEach((g) => (this.gatePort[g] = k)));
|
|
336
|
+
this.gateList = [
|
|
337
|
+
...model.interfaces.flatMap((itf, f) => itf.gates.map((gate, q) => ({ f, q, gate, o: this.blockOfFace[f] * M + 1 + n + itf.reactions.length + q }))),
|
|
338
|
+
...model.ports.flatMap((port, k) => Array.from(port.nodes).flatMap((g) => port.gates.map((gate, q) => ({ port: k, g, q, gate, o: this.blockOfNode[g] * M + 1 + n + port.surface.length + q })))),
|
|
339
|
+
];
|
|
340
|
+
this.gateOld = new Float64Array(this.gateList.length);
|
|
341
|
+
this.gateIndex = new Int32Array(nFaces); // face f's first gate in gateList
|
|
342
|
+
let j = 0;
|
|
343
|
+
for (let f = 0; f < nFaces; j += model.interfaces[f].gates.length, f++) this.gateIndex[f] = j;
|
|
344
|
+
this.portGateIndex = model.ports.map((port) => ((j += port.nodes.length * port.gates.length), j - port.nodes.length * port.gates.length));
|
|
309
345
|
|
|
310
346
|
// Per-node material data, flattened [g·n + i].
|
|
311
347
|
this.present = new Uint8Array(nNodes * n);
|
|
@@ -574,12 +610,14 @@ export class Solver {
|
|
|
574
610
|
for (let i = 0; i < n; i++) active[b * M + 1 + i] = this.present[g * n + i];
|
|
575
611
|
const k = this.surfPort[g];
|
|
576
612
|
if (k >= 0) for (let q = 0; q < model.ports[k].surface.length; q++) active[b * M + 1 + n + q] = 1;
|
|
613
|
+
const kg = this.gatePort[g], pg = model.ports[kg];
|
|
614
|
+
if (kg >= 0) for (let q = 0; q < pg.gates.length; q++) active[b * M + 1 + n + pg.surface.length + q] = 1;
|
|
577
615
|
}
|
|
578
616
|
model.interfaces.forEach((itf, f) => {
|
|
579
617
|
const bf = this.blockOfFace[f];
|
|
580
618
|
active[bf * M] = itf.phi.type === 'neutral' ? 0 : 1;
|
|
581
619
|
for (let i = 0; i < n; i++) active[bf * M + 1 + i] = itf.links[i].type === 'blocked' ? 0 : 1;
|
|
582
|
-
for (let k = 0; k < itf.reactions.length; k++) active[bf * M + 1 + n + k] = 1;
|
|
620
|
+
for (let k = 0; k < itf.reactions.length + itf.gates.length; k++) active[bf * M + 1 + n + k] = 1;
|
|
583
621
|
});
|
|
584
622
|
const sizes = new Int32Array(nB), loc = (this.loc = new Int32Array(nB * M).fill(-1));
|
|
585
623
|
for (let b = 0; b < nB; b++) for (let r = 0; r < M; r++) if (active[b * M + r]) loc[b * M + r] = sizes[b]++;
|
|
@@ -1393,6 +1431,11 @@ export class Solver {
|
|
|
1393
1431
|
});
|
|
1394
1432
|
if (w > 0) this.termV[k] = port.capacitance.zeroCharge + sum / w;
|
|
1395
1433
|
}
|
|
1434
|
+
// Gates start open as far as the starting voltage across their face holds them, α/(α + β).
|
|
1435
|
+
for (const gt of this.gateList) {
|
|
1436
|
+
const { gate, o } = gt, V = this._gateVoltage(gt), a = gateRate(gate.alpha, V)[0], b = gateRate(gate.beta, V)[0];
|
|
1437
|
+
u[o] = a + b > 0 ? a / (a + b) : 0;
|
|
1438
|
+
}
|
|
1396
1439
|
// A floating electrode spread through a port (without a double layer) starts where its
|
|
1397
1440
|
// reactions pass the current it's set (none, behind a resistance): at its mixed potential in
|
|
1398
1441
|
// the start's composition, not level with a held terminal, which can be volts away and pass
|
|
@@ -1601,6 +1644,7 @@ export class Solver {
|
|
|
1601
1644
|
const { grid, materials, regions } = model;
|
|
1602
1645
|
sys.clear();
|
|
1603
1646
|
res.fill(0);
|
|
1647
|
+
this.dtNow = dt;
|
|
1604
1648
|
if (this.lin) this.lin.reset(1 / dt);
|
|
1605
1649
|
this.computeConcentrations();
|
|
1606
1650
|
|
|
@@ -1752,7 +1796,7 @@ export class Solver {
|
|
|
1752
1796
|
// A start-of-step net charge (round-off in a solved state) isn't carried over: the neutrality
|
|
1753
1797
|
// row holds the new state neutral.
|
|
1754
1798
|
_chargeRows(dt) {
|
|
1755
|
-
const { n, M, res, c, cOld, z, sys, loc, present, termB, termC } = this, R = this.rix, F = FARADAY;
|
|
1799
|
+
const { n, M, res, c, cOld, z, sys, loc, present, termB, termC } = this, R = this.rix, F = FARADAY, live = this._liveTerms();
|
|
1756
1800
|
const { A, B, C, sizes, offA, offB, offC } = sys, vol = this.model.grid.vol, last = this.nB - 1;
|
|
1757
1801
|
for (const g of this.chargeNodes) {
|
|
1758
1802
|
const b = this.blockOfNode[g], lb = b * M, m = sizes[b], mp = b > 0 ? sizes[b - 1] : 0, mn = b < last ? sizes[b + 1] : 0;
|
|
@@ -1776,7 +1820,7 @@ export class Solver {
|
|
|
1776
1820
|
for (let q = 0, o = offB[b] + lk * m, s = offB[b] + li * m; q < m; q++) B[o + q] += w * B[s + q];
|
|
1777
1821
|
for (let q = 0, o = offC[b] + lk * mn, s = offC[b] + li * mn; q < mn; q++) C[o + q] += w * C[s + q];
|
|
1778
1822
|
res[rk] += w * res[ri];
|
|
1779
|
-
for (
|
|
1823
|
+
for (const t of live) termB[t][rk] += w * termB[t][ri];
|
|
1780
1824
|
if (this.lin) this.lin.combine(rk, ri, w);
|
|
1781
1825
|
}
|
|
1782
1826
|
// Columns: φ̂' = φ̂ with η fixed becomes φ̂ with η_i shifted by z_i, wherever node g's
|
|
@@ -1787,7 +1831,7 @@ export class Solver {
|
|
|
1787
1831
|
for (let r = 0, o = offB[b]; r < m; r++) B[o + r * m + p] += zi * B[o + r * m + li];
|
|
1788
1832
|
if (b > 0) for (let r = 0, mr = mp, o = offC[b - 1]; r < mr; r++) C[o + r * m + p] += zi * C[o + r * m + li];
|
|
1789
1833
|
if (b < last) for (let r = 0, mr = mn, o = offA[b + 1]; r < mr; r++) A[o + r * m + p] += zi * A[o + r * m + li];
|
|
1790
|
-
for (
|
|
1834
|
+
for (const t of live) termC[t][R[lb]] += zi * termC[t][R[lb + 1 + i]];
|
|
1791
1835
|
}
|
|
1792
1836
|
// Storage on the other charged balances, and neutrality: no φ̂' terms, exactly.
|
|
1793
1837
|
const v = vol[g], oB = offB[b];
|
|
@@ -2036,14 +2080,21 @@ export class Solver {
|
|
|
2036
2080
|
// materials). Between like solutions it's Goldman–Hodgkin–Katz's flux,
|
|
2037
2081
|
// N = P·zu·(c_L − c_R e^{zu})/(e^{zu} − 1), u = (φ_R − φ_L)/V_T,
|
|
2038
2082
|
// and it's exactly zero where μ̄ is level.
|
|
2039
|
-
const gl = grid.regionEnd[f], gr = gl + 1, kl = gl * n + i, kr = gr * n + i, zi = z[i],
|
|
2083
|
+
const gl = grid.regionEnd[f], gr = gl + 1, kl = gl * n + i, kr = gr * n + i, zi = z[i], link = itf.links[i];
|
|
2040
2084
|
const shift = this.mu0hat[kr] - this.mu0hat[kl] - Math.log(this.cRef[kr] / this.cRef[kl]);
|
|
2041
2085
|
const d = zi * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M])) + shift;
|
|
2042
2086
|
const E = Math.expm1(-deta); // η_R − η_L
|
|
2087
|
+
// Gated: P × Π x^p, each x taken in [0, 1] (Newton's iterates can stray past its ends).
|
|
2088
|
+
const gated = link.gates ?? [], gx = (q) => u[this._gateSlot(f, q)];
|
|
2089
|
+
const P = link.P * (gated.length ? this._open(gated, gx) : 1);
|
|
2043
2090
|
const gBc = P * bernoulli(d) * this.c[kl];
|
|
2044
2091
|
const dNdd = -P * bernoulliDerivative(d) * this.c[kl] * E;
|
|
2045
2092
|
res[R[o]] = u[o] + gBc * E;
|
|
2046
2093
|
this._j(bf, r, bf, r, 1);
|
|
2094
|
+
for (const [q, p] of gated) {
|
|
2095
|
+
const dO = this._dOpen(gated, q, p, gx);
|
|
2096
|
+
if (dO !== 0) this._j(bf, r, bf, 1 + n + itf.reactions.length + q, link.P * bernoulli(d) * this.c[kl] * E * dO);
|
|
2097
|
+
}
|
|
2047
2098
|
this._j(bf, r, bL, r, -gBc);
|
|
2048
2099
|
this._j(bf, r, bR, r, gBc * (E + 1));
|
|
2049
2100
|
if (zi !== 0) {
|
|
@@ -2051,12 +2102,18 @@ export class Solver {
|
|
|
2051
2102
|
this._j(bf, r, bR, 0, -zi * dNdd);
|
|
2052
2103
|
}
|
|
2053
2104
|
} else {
|
|
2054
|
-
// conductance: J = G (V_L − V_R), V = V_T η / z ⇒ N = G V_T (η_L − η_R) / (z² F)
|
|
2055
|
-
|
|
2105
|
+
// conductance: J = G (V_L − V_R), V = V_T η / z ⇒ N = G V_T (η_L − η_R) / (z² F); gated,
|
|
2106
|
+
// G times Π x^p
|
|
2107
|
+
const gated = itf.links[i].gates ?? [], gx = (q) => u[this._gateSlot(f, q)];
|
|
2108
|
+
const gG0 = (itf.links[i].G * VT) / (z[i] * z[i] * F), gG = gG0 * (gated.length ? this._open(gated, gx) : 1);
|
|
2056
2109
|
res[R[o]] = u[o] - gG * deta;
|
|
2057
2110
|
this._j(bf, r, bf, r, 1);
|
|
2058
2111
|
this._j(bf, r, bL, r, -gG);
|
|
2059
2112
|
this._j(bf, r, bR, r, gG);
|
|
2113
|
+
for (const [q, p] of gated) {
|
|
2114
|
+
const dO = this._dOpen(gated, q, p, gx);
|
|
2115
|
+
if (dO !== 0) this._j(bf, r, bf, 1 + n + itf.reactions.length + q, -gG0 * deta * dO);
|
|
2116
|
+
}
|
|
2060
2117
|
}
|
|
2061
2118
|
res[R[bL * M + r]] += Af * u[o];
|
|
2062
2119
|
this._j(bL, r, bf, r, Af);
|
|
@@ -2064,6 +2121,46 @@ export class Solver {
|
|
|
2064
2121
|
this._j(bR, r, bf, r, -Af);
|
|
2065
2122
|
}
|
|
2066
2123
|
itf.reactions.forEach((rx, k) => this._faceReaction(rx, k, f, bf, bL, bR, Af));
|
|
2124
|
+
if (itf.gates.length > 0) this._faceGates(f, bf, bL, bR);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// Full index of face f's gate q (its fraction open).
|
|
2128
|
+
_gateSlot(f, q) {
|
|
2129
|
+
return this.blockOfFace[f] * this.M + 1 + this.n + this.model.interfaces[f].reactions.length + q;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// The voltage a gate follows: across its face, or across its port's capacitance at its node.
|
|
2133
|
+
_gateVoltage(gt) {
|
|
2134
|
+
return gt.port === undefined ? this._faceVoltage(gt.f) : this._portVoltage(gt.port, gt.g);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// The voltage across port k's capacitance at node g, φ − (V − zeroCharge) (V): a membrane's,
|
|
2138
|
+
// inside (the region) minus outside.
|
|
2139
|
+
_portVoltage(k, g) {
|
|
2140
|
+
const { M, u, uLo, VT } = this, b = this.blockOfNode[g], port = this.model.ports[k];
|
|
2141
|
+
return VT * (u[b * M] + uLo[b * M]) - (this.termV[2 + k] - port.capacitance.zeroCharge);
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// The voltage across face f, φ_right − φ_left (V).
|
|
2145
|
+
_faceVoltage(f) {
|
|
2146
|
+
const { M, u, uLo, VT } = this, bf = this.blockOfFace[f], bL = bf - 1, bR = bf + 1;
|
|
2147
|
+
return VT * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M]));
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// Face f's gates, each a row (x − x_old)/dt = α(1 − x) − βx in the voltage across the face.
|
|
2151
|
+
_faceGates(f, bf, bL, bR) {
|
|
2152
|
+
const { res, VT } = this, R = this.rix, itf = this.model.interfaces[f], dt = this.dtNow;
|
|
2153
|
+
const V = this._faceVoltage(f), k0 = 1 + this.n + itf.reactions.length;
|
|
2154
|
+
itf.gates.forEach((gate, q) => {
|
|
2155
|
+
const o = this._gateSlot(f, q), x = this.u[o];
|
|
2156
|
+
const [a, da] = gateRate(gate.alpha, V), [b, db] = gateRate(gate.beta, V);
|
|
2157
|
+
const store = Number.isFinite(dt) ? 1 / dt : 0, j = this.gateIndex[f] + q;
|
|
2158
|
+
res[R[o]] = store * (x - this.gateOld[j]) - (a * (1 - x) - b * x);
|
|
2159
|
+
this._j(bf, k0 + q, bf, k0 + q, store + a + b);
|
|
2160
|
+
const dV = -(da * (1 - x) - db * x) * VT; // per unit φ̂_R, and minus that per unit φ̂_L
|
|
2161
|
+
this._j(bf, k0 + q, bR, 0, dV);
|
|
2162
|
+
this._j(bf, k0 + q, bL, 0, -dV);
|
|
2163
|
+
});
|
|
2067
2164
|
}
|
|
2068
2165
|
|
|
2069
2166
|
// Scharfetter–Gummel with non-ideal statistics. The excess ex = ζ − ln(c/c_ref) acts as an
|
|
@@ -2345,16 +2442,26 @@ export class Solver {
|
|
|
2345
2442
|
}
|
|
2346
2443
|
}
|
|
2347
2444
|
|
|
2445
|
+
// The terminals whose ∂res/∂V (termB) and ∂I/∂x (termC) are kept, exactly: the floating ones,
|
|
2446
|
+
// which the Newton solve borders on, or all of them (allTerminals: for the impedance, which reads
|
|
2447
|
+
// its terminal's). A held terminal's are otherwise left as they fall (written but not kept
|
|
2448
|
+
// consistent), which spares a device with dozens of ports (an axon's nodes of Ranvier) the cost
|
|
2449
|
+
// of carrying each one's two dense vectors through every assembly.
|
|
2450
|
+
_liveTerms() {
|
|
2451
|
+
if (this.allTerminals) return this._allTerms ?? (this._allTerms = this.terms.map((_, k) => k));
|
|
2452
|
+
return this.floating;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2348
2455
|
// Every terminal's terms: its voltage held or floating, how the residual depends on it
|
|
2349
2456
|
// (termB), its current into the device (termI) and how that depends on the state (termC).
|
|
2350
2457
|
_terminals(dt) {
|
|
2351
2458
|
this._refreshSources();
|
|
2352
|
-
for (
|
|
2459
|
+
for (const k of this._liveTerms()) {
|
|
2353
2460
|
this.termB[k].fill(0);
|
|
2354
2461
|
this.termC[k].fill(0);
|
|
2355
|
-
this.termDI[k] = 0;
|
|
2356
|
-
this.termI[k] = 0;
|
|
2357
2462
|
}
|
|
2463
|
+
this.termDI.fill(0);
|
|
2464
|
+
this.termI.fill(0);
|
|
2358
2465
|
this.qg.fill(0);
|
|
2359
2466
|
this.qgSlope.fill(0);
|
|
2360
2467
|
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt, false));
|
|
@@ -2417,8 +2524,11 @@ export class Solver {
|
|
|
2417
2524
|
B[R[o]] += -z[i] / VT;
|
|
2418
2525
|
continue;
|
|
2419
2526
|
}
|
|
2420
|
-
// conductance: s = G V_T (η_out − η)/(z² F); exchange: s = k (η_out − η)
|
|
2421
|
-
|
|
2527
|
+
// conductance: s = G V_T (η_out − η)/(z² F); exchange: s = k (η_out − η). Gated, G times
|
|
2528
|
+
// Π x^p, the gates at this node (each taken in [0, 1]).
|
|
2529
|
+
const kk0 = link.type === 'conductance' ? (link.G * VT) / (z[i] * z[i] * F) : link.k;
|
|
2530
|
+
const gated = link.gates ?? [], q0 = 1 + n + port.surface.length;
|
|
2531
|
+
const open = gated.length ? this._open(gated, (q) => u[b * M + q0 + q]) : 1, kk = kk0 * open;
|
|
2422
2532
|
res[R[o]] -= v * kk * deta;
|
|
2423
2533
|
this._j(b, r, b, r, v * kk);
|
|
2424
2534
|
flux[i] += v * kk * deta;
|
|
@@ -2427,6 +2537,12 @@ export class Solver {
|
|
|
2427
2537
|
C[R[o]] += -zF * v * kk;
|
|
2428
2538
|
this.termDI[k] += (zF * v * kk * z[i]) / VT;
|
|
2429
2539
|
}
|
|
2540
|
+
for (const [q, p] of gated) {
|
|
2541
|
+
const d = this._dOpen(gated, q, p, (qq) => u[b * M + q0 + qq]);
|
|
2542
|
+
if (d === 0) continue;
|
|
2543
|
+
this._j(b, r, b, q0 + q, -v * kk0 * deta * d);
|
|
2544
|
+
if (z[i] !== 0) C[R[b * M + q0 + q]] += zF * v * kk0 * deta * d;
|
|
2545
|
+
}
|
|
2430
2546
|
}
|
|
2431
2547
|
}
|
|
2432
2548
|
if (holds) {
|
|
@@ -2435,6 +2551,7 @@ export class Solver {
|
|
|
2435
2551
|
}
|
|
2436
2552
|
port.reactions.forEach((rx, x) => this._portReaction(port, rx, this.portArea[k - 2], this.portRates[k - 2][x], flux, k));
|
|
2437
2553
|
if (port.capacitance) this._portCapacitance(port, k, dt);
|
|
2554
|
+
if (port.gates.length > 0) this._portGates(port, k, dt);
|
|
2438
2555
|
if (port.surface.length > 0 && Number.isFinite(dt)) this._surfaceStorage(port, dt);
|
|
2439
2556
|
}
|
|
2440
2557
|
|
|
@@ -2503,6 +2620,38 @@ export class Solver {
|
|
|
2503
2620
|
// on the port's side, so each node's charge balance (its φ row) gains vol·a·σ, and the port
|
|
2504
2621
|
// passes the charging current d(Σ vol·a·σ)/dt (none in a steady state). An end node whose φ its
|
|
2505
2622
|
// contact sets is left to the contact.
|
|
2623
|
+
// A membrane port's gates, at each node of its window: (x − x_old)/dt = α(1 − x) − βx in the
|
|
2624
|
+
// voltage across its capacitance there, φ − (V − zeroCharge).
|
|
2625
|
+
_portGates(port, k, dt) {
|
|
2626
|
+
const { n, res, VT } = this, R = this.rix, B = this.termB[k], store = Number.isFinite(dt) ? 1 / dt : 0, q0 = 1 + n + port.surface.length;
|
|
2627
|
+
port.nodes.forEach((g, w) => {
|
|
2628
|
+
const b = this.blockOfNode[g], V = this._portVoltage(k - 2, g);
|
|
2629
|
+
port.gates.forEach((gate, q) => {
|
|
2630
|
+
const o = b * this.M + q0 + q, x = this.u[o], j = this.portGateIndex[k - 2] + w * port.gates.length + q;
|
|
2631
|
+
const [a, da] = gateRate(gate.alpha, V), [bb, db] = gateRate(gate.beta, V);
|
|
2632
|
+
res[R[o]] = store * (x - this.gateOld[j]) - (a * (1 - x) - bb * x);
|
|
2633
|
+
this._j(b, q0 + q, b, q0 + q, store + a + bb);
|
|
2634
|
+
const s = -(da * (1 - x) - db * x); // ∂/∂V_m, with V_m = V_T φ̂ − V + zeroCharge
|
|
2635
|
+
this._j(b, q0 + q, b, 0, s * VT);
|
|
2636
|
+
B[R[o]] += -s;
|
|
2637
|
+
});
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
// Π x^p over a link's gates (each x taken in [0, 1]), and its derivative in gate q.
|
|
2642
|
+
_open(gated, x) {
|
|
2643
|
+
let open = 1;
|
|
2644
|
+
for (const [q, p] of gated) open *= powi(Math.min(1, Math.max(0, x(q))), p);
|
|
2645
|
+
return open;
|
|
2646
|
+
}
|
|
2647
|
+
_dOpen(gated, q, p, x) {
|
|
2648
|
+
const xq = x(q);
|
|
2649
|
+
if (!(xq > 0 && xq < 1)) return 0;
|
|
2650
|
+
let others = 1;
|
|
2651
|
+
for (const [q2, p2] of gated) if (q2 !== q) others *= powi(Math.min(1, Math.max(0, x(q2))), p2);
|
|
2652
|
+
return p * powi(xq, p - 1) * others;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2506
2655
|
_portCapacitance(port, k, dt) {
|
|
2507
2656
|
const { M, u, uLo, res, VT } = this, R = this.rix, vol = this.model.grid.vol, area = this.portArea[k - 2];
|
|
2508
2657
|
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
@@ -2738,7 +2887,7 @@ export class Solver {
|
|
|
2738
2887
|
|
|
2739
2888
|
// Scale every row by its largest Jacobian entry (in place, residual too).
|
|
2740
2889
|
_equilibrate() {
|
|
2741
|
-
const { nB, sys, res, termB } = this;
|
|
2890
|
+
const { nB, sys, res, termB } = this, live = this._liveTerms();
|
|
2742
2891
|
const { A, B, C, sizes, offA, offB, offC, offX } = sys;
|
|
2743
2892
|
for (let b = 0; b < nB; b++) {
|
|
2744
2893
|
const m = sizes[b], mp = b > 0 ? sizes[b - 1] : 0, mn = b < nB - 1 ? sizes[b + 1] : 0;
|
|
@@ -2758,7 +2907,7 @@ export class Solver {
|
|
|
2758
2907
|
for (let k = 0; k < m; k++) B[ob + k] *= s;
|
|
2759
2908
|
for (let k = 0; k < mn; k++) C[oc + k] *= s;
|
|
2760
2909
|
res[offX[b] + r] *= s;
|
|
2761
|
-
for (
|
|
2910
|
+
for (const k of live) termB[k][offX[b] + r] *= s; // (each terminal's ∂/∂V)
|
|
2762
2911
|
}
|
|
2763
2912
|
}
|
|
2764
2913
|
}
|
|
@@ -2883,6 +3032,7 @@ export class Solver {
|
|
|
2883
3032
|
const k = this.surfPort[g]; // an electrode surface's coverages, as η
|
|
2884
3033
|
if (k >= 0) for (let q = 0; q < this.model.ports[k].surface.length; q++) mx = Math.max(mx, Math.abs(delta[R[b * M + 1 + n + q]]));
|
|
2885
3034
|
}
|
|
3035
|
+
for (const gt of this.gateList) mx = Math.max(mx, Math.abs(delta[R[gt.o]])); // a gate's fraction open
|
|
2886
3036
|
return mx;
|
|
2887
3037
|
}
|
|
2888
3038
|
|
|
@@ -3104,6 +3254,7 @@ export class Solver {
|
|
|
3104
3254
|
this.termVPrev.set(this.termV);
|
|
3105
3255
|
this.computeConcentrations();
|
|
3106
3256
|
const cN = Float64Array.from(this.c), thN = Float64Array.from(this.th), th0N = Float64Array.from(this.th0);
|
|
3257
|
+
const gN = Float64Array.from(this.gateList, (gt) => this.u[gt.o] + this.uLo[gt.o]);
|
|
3107
3258
|
// Contact displacement before the step: as it was at the end of the previous step (under
|
|
3108
3259
|
// the parameters then), so a gate-voltage change shows up as displacement current.
|
|
3109
3260
|
if (!this.contactDEnd || !this.portQEnd) {
|
|
@@ -3124,6 +3275,7 @@ export class Solver {
|
|
|
3124
3275
|
for (let k = 0; k < cOld.length; k++) cOld[k] = b1 * cN[k] - b2 * prev.c[k];
|
|
3125
3276
|
for (let k = 0; k < thN.length; k++) this.thOld[k] = b1 * thN[k] - b2 * prev.th[k];
|
|
3126
3277
|
for (let k = 0; k < th0N.length; k++) this.th0Old[k] = b1 * th0N[k] - b2 * prev.th0[k];
|
|
3278
|
+
for (let k = 0; k < gN.length; k++) this.gateOld[k] = b1 * gN[k] - b2 * prev.g[k];
|
|
3127
3279
|
this.contactDStart = { left: b1 * DN.left - b2 * prev.D.left, right: b1 * DN.right - b2 * prev.D.right };
|
|
3128
3280
|
this.portQStart = QN.map((q, k) => b1 * q - b2 * prev.Q[k]);
|
|
3129
3281
|
dtEff = dt / a0;
|
|
@@ -3131,6 +3283,7 @@ export class Solver {
|
|
|
3131
3283
|
this.cOld.set(cN);
|
|
3132
3284
|
this.thOld.set(thN);
|
|
3133
3285
|
this.th0Old.set(th0N);
|
|
3286
|
+
this.gateOld.set(gN);
|
|
3134
3287
|
this.contactDStart = DN;
|
|
3135
3288
|
this.portQStart = QN;
|
|
3136
3289
|
}
|
|
@@ -3162,7 +3315,7 @@ export class Solver {
|
|
|
3162
3315
|
}
|
|
3163
3316
|
result.bdf = bdf;
|
|
3164
3317
|
if (result.converged) {
|
|
3165
|
-
this.history.unshift({ t: this.time, dt, u: Float64Array.from(this.uPrev), c: cN, th: thN, th0: th0N, D: DN, Q: QN });
|
|
3318
|
+
this.history.unshift({ t: this.time, dt, u: Float64Array.from(this.uPrev), c: cN, th: thN, th0: th0N, g: gN, D: DN, Q: QN });
|
|
3166
3319
|
if (this.history.length > 3) this.history.length = 3;
|
|
3167
3320
|
this.time = tEnd;
|
|
3168
3321
|
this.lastDt = dtEff;
|
|
@@ -3363,9 +3516,22 @@ export class Solver {
|
|
|
3363
3516
|
const fail = (m) => {
|
|
3364
3517
|
throw new DeviceError(m);
|
|
3365
3518
|
};
|
|
3366
|
-
if (!Array.isArray(probes)) fail('advance: probes must be an array of { x, species, quantity, region }');
|
|
3519
|
+
if (!Array.isArray(probes)) fail('advance: probes must be an array of { x, species, quantity, region }, or { interface, species } / { interface, gate }');
|
|
3367
3520
|
const plan = probes.map((p, k) => {
|
|
3368
3521
|
const path = `advance: probes[${k}]`;
|
|
3522
|
+
if (p?.interface !== undefined) {
|
|
3523
|
+
// At a face: a species' flux through it (mol/(m²·s), toward +x), or a gate's fraction open.
|
|
3524
|
+
const f = p.interface, itf = this.model.interfaces[f];
|
|
3525
|
+
if (!(Number.isInteger(f) && itf)) fail(`${path}.interface: no interface ${JSON.stringify(f)} (they're numbered from 0, left to right)`);
|
|
3526
|
+
if (p.gate !== undefined) {
|
|
3527
|
+
const q = itf.gates.findIndex((gt) => gt.name === p.gate);
|
|
3528
|
+
if (q < 0) fail(`${path}.gate: interfaces[${f}] has no gate ${JSON.stringify(p.gate)}${itf.gates.length ? ` (it has ${itf.gates.map((gt) => gt.name).join(', ')})` : ''}`);
|
|
3529
|
+
return { o: this._gateSlot(f, q) };
|
|
3530
|
+
}
|
|
3531
|
+
const i = species.findIndex((sp) => sp.name === p.species);
|
|
3532
|
+
if (i < 0) fail(`${path}.species: no species ${JSON.stringify(p.species)}`);
|
|
3533
|
+
return { o: this.blockOfFace[f] * this.M + 1 + i };
|
|
3534
|
+
}
|
|
3369
3535
|
const quantity = p?.quantity ?? 'c';
|
|
3370
3536
|
if (quantity !== 'c' && quantity !== 'V' && quantity !== 'phi') fail(`${path}.quantity must be 'c' (mol/m³), 'V' (the species voltage) or 'phi' (φ, V)`);
|
|
3371
3537
|
const i = quantity === 'phi' ? -1 : species.findIndex((sp) => sp.name === p?.species);
|
|
@@ -3397,7 +3563,7 @@ export class Solver {
|
|
|
3397
3563
|
};
|
|
3398
3564
|
return {
|
|
3399
3565
|
out,
|
|
3400
|
-
read: () => plan.map((q) => (1 - q.w) * at(q, q.g) + q.w * at(q, q.g + 1)),
|
|
3566
|
+
read: () => plan.map((q) => (q.o !== undefined ? (this.loc[q.o] < 0 ? 0 : this.u[q.o]) : (1 - q.w) * at(q, q.g) + q.w * at(q, q.g + 1))),
|
|
3401
3567
|
push: (values) => values.forEach((v, k) => out[k].push(v)),
|
|
3402
3568
|
};
|
|
3403
3569
|
}
|
|
@@ -3415,6 +3581,7 @@ export class Solver {
|
|
|
3415
3581
|
const k = this.surfPort[g]; // an electrode surface's coverages, as η
|
|
3416
3582
|
if (k >= 0) for (let q = 0; q < this.model.ports[k].surface.length; q++) mx = Math.max(mx, Math.abs(u[b * M + 1 + n + q] - ref[b * M + 1 + n + q]));
|
|
3417
3583
|
}
|
|
3584
|
+
for (const { o } of this.gateList) mx = Math.max(mx, Math.abs(u[o] - ref[o])); // (a fraction: 1e-3 of m³ is 0.3% of P)
|
|
3418
3585
|
return mx;
|
|
3419
3586
|
}
|
|
3420
3587
|
|
|
@@ -3458,6 +3625,7 @@ export class Solver {
|
|
|
3458
3625
|
cOld: Float64Array.from(this.cOld),
|
|
3459
3626
|
thOld: Float64Array.from(this.thOld),
|
|
3460
3627
|
th0Old: Float64Array.from(this.th0Old),
|
|
3628
|
+
gateOld: Float64Array.from(this.gateOld),
|
|
3461
3629
|
time: this.time,
|
|
3462
3630
|
lastDt: this.lastDt,
|
|
3463
3631
|
atSteady: this.atSteady,
|
|
@@ -3480,6 +3648,7 @@ export class Solver {
|
|
|
3480
3648
|
this.cOld.set(s.cOld);
|
|
3481
3649
|
this.thOld.set(s.thOld);
|
|
3482
3650
|
this.th0Old.set(s.th0Old);
|
|
3651
|
+
this.gateOld.set(s.gateOld);
|
|
3483
3652
|
this.time = s.time;
|
|
3484
3653
|
this.lastDt = s.lastDt;
|
|
3485
3654
|
this.atSteady = s.atSteady;
|
|
@@ -3515,7 +3684,17 @@ export class Solver {
|
|
|
3515
3684
|
* Z = δV/δI with I into the device (positive real part for a passive device).
|
|
3516
3685
|
* @param {ArrayLike<number>} frequencies Hz
|
|
3517
3686
|
*/
|
|
3518
|
-
impedance(frequencies,
|
|
3687
|
+
impedance(frequencies, opts) {
|
|
3688
|
+
const was = this.allTerminals;
|
|
3689
|
+
this.allTerminals = true; // (it reads its terminal's ∂res/∂V and ∂I/∂x)
|
|
3690
|
+
try {
|
|
3691
|
+
return this._impedance(frequencies, opts);
|
|
3692
|
+
} finally {
|
|
3693
|
+
this.allTerminals = was;
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
_impedance(frequencies, { profiles = false, terminal = 'right' } = {}) {
|
|
3519
3698
|
const { M, nB, sys, VT } = this;
|
|
3520
3699
|
const kT = this.terms.findIndex((t) => t.name === terminal);
|
|
3521
3700
|
if (kT < 0) throw new SolverError(`impedance: no terminal named '${terminal}' (${this.terms.map((t) => t.name).join(', ')})`);
|
|
@@ -3527,6 +3706,7 @@ export class Solver {
|
|
|
3527
3706
|
this.cOld.set(this.c);
|
|
3528
3707
|
this.thOld.set(this.th);
|
|
3529
3708
|
this.th0Old.set(this.th0);
|
|
3709
|
+
this.gateList.forEach((gt, j) => (this.gateOld[j] = this.u[gt.o]));
|
|
3530
3710
|
this.assemble(Infinity);
|
|
3531
3711
|
this.contactDStart = { ...this.contactD };
|
|
3532
3712
|
this.portQStart = Float64Array.from(this.portQ);
|
|
@@ -3955,6 +4135,7 @@ export class Solver {
|
|
|
3955
4135
|
// Jacobian are left partial (the next assemble() starts afresh). Concentrations must be current.
|
|
3956
4136
|
_assembleBookkeeping(dt) {
|
|
3957
4137
|
const { model } = this;
|
|
4138
|
+
this.dtNow = dt;
|
|
3958
4139
|
const { grid, materials, regions } = model;
|
|
3959
4140
|
this.res.fill(0);
|
|
3960
4141
|
for (let r = 0; r < regions.length; r++) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function ghkCurrent(z: any, inside: any, outside: any, V: any, T: any): number;
|
|
2
|
+
/**
|
|
3
|
+
* Hodgkin and Huxley's squid-axon channels, for a membrane face with the outside on its left
|
|
4
|
+
* (so the gates' voltage, φ_right − φ_left, is the membrane potential): the face's `gates`, and
|
|
5
|
+
* `species` links for Na⁺ (P·m³h), K⁺ (P·n⁴) and the leak (ungated), to spread into the face
|
|
6
|
+
* beside its φ law, `{ phi: { type: 'capacitive', C: 0.01 }, ...hh }`, adding links (or blocks)
|
|
7
|
+
* for any other species.
|
|
8
|
+
*
|
|
9
|
+
* The currents are GHK's, which rectify, so a permeability matches a conductance at one voltage
|
|
10
|
+
* only: each is the one whose chord conductance at `at` (rest, −65 mV) is Hodgkin and Huxley's
|
|
11
|
+
* (`g`: 120, 36 and 0.3 mS/cm²), at the concentrations given. The leak is carried by one ion
|
|
12
|
+
* (`leak`, of charge `leakZ`: Cl⁻ by default, whose Nernst level in squid is near their leak
|
|
13
|
+
* reversal, −54 mV). The
|
|
14
|
+
* rates are theirs at 6.3 °C, scaled to `T` by Q₁₀ = 3.
|
|
15
|
+
* @param {{ inside: object, outside: object, T?: number, g?: { Na?: number, K?: number, leak?: number },
|
|
16
|
+
* leak?: string, leakZ?: number, at?: number, names?: { Na?: string, K?: string } }} opts concentrations in
|
|
17
|
+
* mol/m³ by species name, K, S/m², V
|
|
18
|
+
* @returns {{ gates: object, species: object }}
|
|
19
|
+
*/
|
|
20
|
+
export function hodgkinHuxley({ inside, outside, T, g, leak, leakZ, at, names }?: {
|
|
21
|
+
inside: object;
|
|
22
|
+
outside: object;
|
|
23
|
+
T?: number;
|
|
24
|
+
g?: {
|
|
25
|
+
Na?: number;
|
|
26
|
+
K?: number;
|
|
27
|
+
leak?: number;
|
|
28
|
+
};
|
|
29
|
+
leak?: string;
|
|
30
|
+
leakZ?: number;
|
|
31
|
+
at?: number;
|
|
32
|
+
names?: {
|
|
33
|
+
Na?: string;
|
|
34
|
+
K?: string;
|
|
35
|
+
};
|
|
36
|
+
}): {
|
|
37
|
+
gates: object;
|
|
38
|
+
species: object;
|
|
39
|
+
};
|
package/types/kit.d.ts
CHANGED
|
@@ -61,6 +61,7 @@ export { build, combine, layer, ohmic, bath } from "./stack.js";
|
|
|
61
61
|
export { parseEquation, stoichiometry, half, SHE, level } from "./equation.js";
|
|
62
62
|
export { traces, speciesRole, typeset } from "./traces.js";
|
|
63
63
|
export { pulse, square, triangle, ramp, injector, recombination, photogeneration } from "./sources.js";
|
|
64
|
+
export { hodgkinHuxley, ghkCurrent } from "./channels.js";
|
|
64
65
|
export { describe, unitWarnings } from "./describe.js";
|
|
65
66
|
export { henderson, planck } from "./junction.js";
|
|
66
67
|
export { perovskiteCell, PEROVSKITE_SCANS, hysteresis } from "./devices.js";
|
package/types/recorder.d.ts
CHANGED
|
@@ -4,17 +4,21 @@
|
|
|
4
4
|
* neither, at the end of each `advance()` call. The trace (`t`, `current`, `voltage`, and
|
|
5
5
|
* `probes` if given) runs from the start through every accepted step of every call.
|
|
6
6
|
* @param {import('./index.js').Device} device
|
|
7
|
-
* @param {{ every?: number, times?: number[], probes?: { x: number, species?: string, quantity?: 'c' | 'V' | 'phi', region?: string | number }[] }} [opts]
|
|
7
|
+
* @param {{ every?: number, times?: number[], probes?: ({ x: number, species?: string, quantity?: 'c' | 'V' | 'phi', region?: string | number } | { interface: number, species?: string, gate?: string })[] }} [opts]
|
|
8
8
|
*/
|
|
9
9
|
export function recorder(device: import("./index.js").Device, opts?: {
|
|
10
10
|
every?: number;
|
|
11
11
|
times?: number[];
|
|
12
|
-
probes?: {
|
|
12
|
+
probes?: ({
|
|
13
13
|
x: number;
|
|
14
14
|
species?: string;
|
|
15
15
|
quantity?: "c" | "V" | "phi";
|
|
16
16
|
region?: string | number;
|
|
17
|
-
}
|
|
17
|
+
} | {
|
|
18
|
+
interface: number;
|
|
19
|
+
species?: string;
|
|
20
|
+
gate?: string;
|
|
21
|
+
})[];
|
|
18
22
|
}): {
|
|
19
23
|
advance: (tEnd: number, aopts?: object) => import("./types.js").Solution;
|
|
20
24
|
frame: (t: number) => import("./types.js").Solution;
|
package/types/solver.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export class Solver {
|
|
|
23
23
|
thOld: Float64Array<ArrayBuffer>;
|
|
24
24
|
th0: Float64Array<any>;
|
|
25
25
|
th0Old: Float64Array<any>;
|
|
26
|
+
gatePort: Int32Array<any>;
|
|
27
|
+
gateList: any[];
|
|
28
|
+
gateOld: Float64Array<ArrayBuffer>;
|
|
29
|
+
gateIndex: Int32Array<ArrayBuffer>;
|
|
30
|
+
portGateIndex: any;
|
|
26
31
|
present: Uint8Array<ArrayBuffer>;
|
|
27
32
|
cRef: Float64Array<ArrayBuffer>;
|
|
28
33
|
mu0hat: Float64Array<ArrayBuffer>;
|
|
@@ -181,6 +186,7 @@ export class Solver {
|
|
|
181
186
|
_j(rb: any, rs: any, cb: any, cs: any, v: any): void;
|
|
182
187
|
/** Assemble residual and Jacobian for a backward-Euler step of size dt. */
|
|
183
188
|
assemble(dt: any): void;
|
|
189
|
+
dtNow: any;
|
|
184
190
|
transformed: any;
|
|
185
191
|
_nodeConductor(g: any, dt: any): void;
|
|
186
192
|
_nodesDilute(g0: any, g1: any, dt: any): void;
|
|
@@ -192,6 +198,11 @@ export class Solver {
|
|
|
192
198
|
_segmentDisplacement(s: any, bL: any, bR: any, mat: any, h: any): void;
|
|
193
199
|
_segmentsDilute(g0: any, g1: any, region: any, mat: any, vel: any): void;
|
|
194
200
|
_face(f: any): void;
|
|
201
|
+
_gateSlot(f: any, q: any): any;
|
|
202
|
+
_gateVoltage(gt: any): number;
|
|
203
|
+
_portVoltage(k: any, g: any): number;
|
|
204
|
+
_faceVoltage(f: any): number;
|
|
205
|
+
_faceGates(f: any, bf: any, bL: any, bR: any): void;
|
|
195
206
|
_segmentConcentrated(s: any, bL: any, bR: any, mat: any, h: any, vel: any): void;
|
|
196
207
|
_segmentConductor(s: any, bL: any, bR: any, mat: any, h: any): void;
|
|
197
208
|
_segmentMixing(s: any, bL: any, bR: any, Dm: any, h: any, mat: any): void;
|
|
@@ -207,11 +218,16 @@ export class Solver {
|
|
|
207
218
|
Ca: number;
|
|
208
219
|
};
|
|
209
220
|
_faceReaction(rx: any, k: any, f: any, bf: any, bL: any, bR: any, Af: any): void;
|
|
221
|
+
_liveTerms(): any;
|
|
222
|
+
_allTerms: any;
|
|
210
223
|
_terminals(dt: any): void;
|
|
211
224
|
_circuit(k: any): void;
|
|
212
225
|
_captureRow(b: any, s: any, w: any, into: any): any;
|
|
213
226
|
_port(port: any, flux: any, k: any, dt: any, holds: any): void;
|
|
214
227
|
_portReaction(port: any, rx: any, area: any, rates: any, flux: any, k: any): void;
|
|
228
|
+
_portGates(port: any, k: any, dt: any): void;
|
|
229
|
+
_open(gated: any, x: any): number;
|
|
230
|
+
_dOpen(gated: any, q: any, p: any, x: any): number;
|
|
215
231
|
_portCapacitance(port: any, k: any, dt: any): void;
|
|
216
232
|
_surfaceStorage(port: any, dt: any): void;
|
|
217
233
|
_contact(side: any, dt: any): void;
|
|
@@ -310,6 +326,7 @@ export class Solver {
|
|
|
310
326
|
cOld: Float64Array<ArrayBuffer>;
|
|
311
327
|
thOld: Float64Array<ArrayBuffer>;
|
|
312
328
|
th0Old: Float64Array<ArrayBuffer>;
|
|
329
|
+
gateOld: Float64Array<ArrayBuffer>;
|
|
313
330
|
time: number;
|
|
314
331
|
lastDt: number;
|
|
315
332
|
atSteady: boolean;
|
|
@@ -343,14 +360,22 @@ export class Solver {
|
|
|
343
360
|
* Z = δV/δI with I into the device (positive real part for a passive device).
|
|
344
361
|
* @param {ArrayLike<number>} frequencies Hz
|
|
345
362
|
*/
|
|
346
|
-
impedance(frequencies: ArrayLike<number>,
|
|
363
|
+
impedance(frequencies: ArrayLike<number>, opts: any): {
|
|
364
|
+
f: Float64Array<ArrayBuffer>;
|
|
365
|
+
Z: {
|
|
366
|
+
re: Float64Array<any>;
|
|
367
|
+
im: Float64Array<any>;
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
allTerminals: any;
|
|
371
|
+
_impedance(frequencies: any, { profiles, terminal }?: {
|
|
347
372
|
profiles?: boolean;
|
|
348
373
|
terminal?: string;
|
|
349
374
|
}): {
|
|
350
375
|
f: Float64Array<ArrayBuffer>;
|
|
351
376
|
Z: {
|
|
352
|
-
re: Float64Array<
|
|
353
|
-
im: Float64Array<
|
|
377
|
+
re: Float64Array<any>;
|
|
378
|
+
im: Float64Array<any>;
|
|
354
379
|
};
|
|
355
380
|
};
|
|
356
381
|
_regionShift(yr: any, yi: any): Float64Array<any>[];
|