driftlet 0.12.0 → 0.14.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 +48 -76
- package/docs/device.md +61 -26
- package/docs/kit.md +71 -4
- package/docs/logo-dark.svg +3 -2
- package/docs/logo.svg +3 -2
- package/docs/reliability.md +60 -3
- package/docs/statistics.md +10 -0
- package/llms.txt +622 -391
- package/package.json +1 -1
- package/src/channels.js +27 -4
- package/src/checks.js +5 -1
- package/src/describe.js +1 -1
- package/src/device.js +112 -14
- package/src/index.js +6 -1
- package/src/kit.js +1 -0
- package/src/particles.js +331 -0
- package/src/solution.js +42 -4
- package/src/solver.js +181 -50
- package/src/stack.js +6 -4
- package/src/types.js +2 -1
- package/types/channels.d.ts +14 -3
- package/types/checks.d.ts +2 -1
- package/types/kit.d.ts +1 -0
- package/types/particles.d.ts +42 -0
- package/types/solution.d.ts +1 -0
- package/types/solver.d.ts +8 -2
- package/types/stack.d.ts +6 -5
- package/types/types.d.ts +5 -0
package/package.json
CHANGED
package/src/channels.js
CHANGED
|
@@ -28,7 +28,7 @@ export function ghkCurrent(z, inside, outside, V, T) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Hodgkin and Huxley's squid-axon channels
|
|
31
|
+
* Hodgkin and Huxley's squid-axon channels: for a membrane face with the outside on its left
|
|
32
32
|
* (so the gates' voltage, φ_right − φ_left, is the membrane potential): the face's `gates`, and
|
|
33
33
|
* `species` links for Na⁺ (P·m³h), K⁺ (P·n⁴) and the leak (ungated), to spread into the face
|
|
34
34
|
* beside its φ law, `{ phi: { type: 'capacitive', C: 0.01 }, ...hh }`, adding links (or blocks)
|
|
@@ -40,13 +40,21 @@ export function ghkCurrent(z, inside, outside, V, T) {
|
|
|
40
40
|
* (`leak`, of charge `leakZ`: Cl⁻ by default, whose Nernst level in squid is near their leak
|
|
41
41
|
* reversal, −54 mV). The
|
|
42
42
|
* rates are theirs at 6.3 °C, scaled to `T` by Q₁₀ = 3.
|
|
43
|
+
*
|
|
44
|
+
* Or with `linear: true`, Hodgkin and Huxley's own linear channels on the face: conductance links,
|
|
45
|
+
* G = g, gated the same, whose currents are g(V − E) with E each ion's Nernst level at the
|
|
46
|
+
* concentrations on either side (so `inside`, `outside` and `at` aren't needed).
|
|
47
|
+
*
|
|
48
|
+
* Or, given `area` (membrane per volume of the region, 2/a for an axon of radius a), for a membrane
|
|
49
|
+
* port along a region: links that are their linear conductances, G = g × area per volume, gated the
|
|
50
|
+
* same, each ion's reversal potential following from the port's bath and the concentrations inside
|
|
51
|
+
* (so `inside`, `outside` and `at` aren't needed).
|
|
43
52
|
* @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
|
|
53
|
+
* leak?: string, leakZ?: number, at?: number, names?: { Na?: string, K?: string }, linear?: boolean, area?: number }} opts concentrations in
|
|
45
54
|
* mol/m³ by species name, K, S/m², V
|
|
46
55
|
* @returns {{ gates: object, species: object }}
|
|
47
56
|
*/
|
|
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');
|
|
57
|
+
export function hodgkinHuxley({ inside, outside, T = HH_T, g = {}, leak = 'Cl-', leakZ = -1, at = -65 * mV, names = {}, linear = false, area } = {}) {
|
|
50
58
|
need(num(T) && T > 0, `hodgkinHuxley: T must be a temperature in K, got ${T}`);
|
|
51
59
|
need(num(at), `hodgkinHuxley: at must be a voltage (V), got ${at}`);
|
|
52
60
|
const G = { Na: 1200, K: 360, leak: 3, ...g };
|
|
@@ -54,6 +62,21 @@ export function hodgkinHuxley({ inside, outside, T = HH_T, g = {}, leak = 'Cl-',
|
|
|
54
62
|
const q10 = Math.exp(Math.log(3) * ((T - HH_T) / 10)); // Q₁₀ = 3
|
|
55
63
|
const scaled = (r) => ({ ...r, rate: r.rate * q10 });
|
|
56
64
|
const gates = Object.fromEntries(Object.entries(HH_GATES).map(([k, gt]) => [k, { alpha: scaled(gt.alpha), beta: scaled(gt.beta) }]));
|
|
65
|
+
need(typeof linear === 'boolean', `hodgkinHuxley: linear must be true or false, got ${linear}`);
|
|
66
|
+
if (area !== undefined || linear) {
|
|
67
|
+
need(area === undefined || (num(area) && area > 0), `hodgkinHuxley: area must be the membrane per volume (m²/m³; 2/a for an axon of radius a), got ${area}`);
|
|
68
|
+
for (const [k, v] of Object.entries(G)) need(num(v) && v > 0, `hodgkinHuxley: g.${k} must be a conductance > 0 (S/m²), got ${v}`);
|
|
69
|
+
const per = area ?? 1; // per volume along a port, or per area on a face
|
|
70
|
+
return {
|
|
71
|
+
gates,
|
|
72
|
+
species: {
|
|
73
|
+
[Na]: { type: 'conductance', G: G.Na * per, gates: { m: 3, h: 1 } },
|
|
74
|
+
[K]: { type: 'conductance', G: G.K * per, gates: { n: 4 } },
|
|
75
|
+
[leak]: { type: 'conductance', G: G.leak * per },
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
need(inside && outside && typeof inside === 'object' && typeof outside === 'object', 'hodgkinHuxley: give inside and outside concentrations (mol/m³) by species name (or, for a membrane port, its area)');
|
|
57
80
|
const P = (name, z, gval, what) => {
|
|
58
81
|
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
82
|
need(num(gval) && gval > 0, `hodgkinHuxley: g.${what} must be a conductance > 0 (S/m²), got ${gval}`);
|
package/src/checks.js
CHANGED
|
@@ -29,7 +29,8 @@ const sig = (v, p = 3) => (v === 0 || !Number.isFinite(v) ? String(v) : Math.abs
|
|
|
29
29
|
* zero; under light it's J = F(G − R), and it says where every carrier went.
|
|
30
30
|
* - **conservation** (a transient): every closed, unreacting stretch keeps what it held plus
|
|
31
31
|
* what came in.
|
|
32
|
-
* - **grid** (
|
|
32
|
+
* - **grid** (with `refine`, the default; in a transient, `ok: null` and a reminder, since the
|
|
33
|
+
* answer depends on the whole run): in a steady state, the device solved again on a grid
|
|
33
34
|
* twice as fine everywhere, from this solution interpolated onto it. The current, each floating terminal's voltage and each
|
|
34
35
|
* region's charge must agree to `tol` (relative, default 1e-2, a plot's accuracy; a benchmark
|
|
35
36
|
* wants less). The discretisation is second order, so the change estimates this grid's error
|
|
@@ -67,6 +68,9 @@ export function check(device, sol, { refine = true, tol = 1e-2 } = {}) {
|
|
|
67
68
|
const ledger = steady ? balance(device, sol) : null;
|
|
68
69
|
items.push(ledger ?? conservation(sol, Math.max(...device.model.grid.area)));
|
|
69
70
|
if (steady && refine) items.push(gridCheck(device, sol, tol, ledger.details.gross));
|
|
71
|
+
// A transient's answer depends on its whole history (and on drives set along the way), so
|
|
72
|
+
// only a run of its own on a finer grid can check it.
|
|
73
|
+
else if (refine) items.push({ name: 'grid', ok: null, summary: 'not checked in a transient: run it again on a grid twice as fine (hmin and hmax halved) and compare' });
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
const ok = sol.converged && items.every((it) => it.ok !== false);
|
package/src/describe.js
CHANGED
|
@@ -202,7 +202,7 @@ export function describe(def) {
|
|
|
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
|
+
return l.type === 'permeability' ? [`${sp.name} ${num(l.P)} m/s${gated ? ` × ${gated}` : ''}`] : l.type === 'conductance' ? [`${sp.name} ${num(l.G)} S/m²${gated ? ` × ${gated}` : ''}`] : [];
|
|
206
206
|
});
|
|
207
207
|
const gates = (itf.gates ?? []).map((g) => g.name);
|
|
208
208
|
lines.push(
|
package/src/device.js
CHANGED
|
@@ -15,7 +15,7 @@ import { stoichiometry as equationStoichiometry, faceSides, parseEquation } from
|
|
|
15
15
|
export { DeviceError };
|
|
16
16
|
|
|
17
17
|
// Contacts use the same laws as internal faces: the outside is a phase with known levels.
|
|
18
|
-
const SPECIES_LINK_TYPES = new Set(['blocked', 'equilibrium', 'conductance', 'exchange']);
|
|
18
|
+
const SPECIES_LINK_TYPES = new Set(['blocked', 'equilibrium', 'conductance', 'exchange', 'velocity']);
|
|
19
19
|
const INTERFACE_LINK_TYPES = new Set(['equilibrium', 'blocked', 'conductance', 'permeability']);
|
|
20
20
|
const PHI_LINK_TYPES = new Set(['bulk', 'neutral', 'capacitive', 'pinned']);
|
|
21
21
|
const GRID_FIELDS = ['hmin', 'hmax', 'ratio', 'minCells'];
|
|
@@ -25,6 +25,7 @@ const LINK_FIELDS = {
|
|
|
25
25
|
equilibrium: ['type', 'offset', 'mu'],
|
|
26
26
|
conductance: ['type', 'G', 'offset'],
|
|
27
27
|
exchange: ['type', 'k', 'mu'],
|
|
28
|
+
velocity: ['type', 'v', 'offset', 'mu'],
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
@@ -55,6 +56,49 @@ function positive(v, path) {
|
|
|
55
56
|
return v;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
// Where a region's nodes miss a starting profile worst: the profile at each of its points
|
|
60
|
+
// against the line between the nodes either side, as a fraction of its range over the region
|
|
61
|
+
// (the start is the profile sampled at the nodes). Empty where they miss it by under 2%.
|
|
62
|
+
function unresolved(p, grid, r) {
|
|
63
|
+
const at = (t) => {
|
|
64
|
+
const { x, values: v } = p, last = x.length - 1;
|
|
65
|
+
if (t <= x[0]) return v[0];
|
|
66
|
+
if (t >= x[last]) return v[last];
|
|
67
|
+
let k = 0;
|
|
68
|
+
while (x[k + 1] < t) k++;
|
|
69
|
+
return v[k] + ((v[k + 1] - v[k]) * (t - x[k])) / (x[k + 1] - x[k]);
|
|
70
|
+
};
|
|
71
|
+
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r], a = grid.x[g0], b = grid.x[g1];
|
|
72
|
+
let lo = Math.min(at(a), at(b)), hi = Math.max(at(a), at(b));
|
|
73
|
+
p.x.forEach((t, k) => {
|
|
74
|
+
if (t > a && t < b) (lo = Math.min(lo, p.values[k])), (hi = Math.max(hi, p.values[k]));
|
|
75
|
+
});
|
|
76
|
+
if (!(hi > lo)) return {};
|
|
77
|
+
const missAt = (t, xl, xr) => {
|
|
78
|
+
const cl = at(xl), cr = at(xr);
|
|
79
|
+
return Math.abs(at(t) - (cl + ((cr - cl) * (t - xl)) / (xr - xl))) / (hi - lo);
|
|
80
|
+
};
|
|
81
|
+
let worst = {}, g = g0;
|
|
82
|
+
p.x.forEach((t) => {
|
|
83
|
+
if (!(t > a && t < b)) return;
|
|
84
|
+
while (grid.x[g + 1] < t) g++;
|
|
85
|
+
const off = missAt(t, grid.x[g], grid.x[g + 1]);
|
|
86
|
+
if (off > 0.02 && !(off <= worst.off)) worst = { x: t, h: grid.x[g + 1] - grid.x[g], off };
|
|
87
|
+
});
|
|
88
|
+
if (!worst.off) return worst;
|
|
89
|
+
// The cells that would resolve it there: uniform ones, finer until they miss it by under 2%
|
|
90
|
+
// however they fall on it. (Cells wider than a step miss it by about as much at any size, so
|
|
91
|
+
// the miss isn't extrapolated.)
|
|
92
|
+
const near = Array.from(p.x).filter((t) => Math.abs(t - worst.x) <= 5 * worst.h);
|
|
93
|
+
const misses = (h) => Math.max(...[0, 0.25, 0.5, 0.75].flatMap((f) => near.map((t) => {
|
|
94
|
+
const xl = worst.x + h * (Math.floor((t - worst.x) / h - f) + f);
|
|
95
|
+
return missAt(t, xl, xl + h);
|
|
96
|
+
})));
|
|
97
|
+
worst.want = worst.h / 2;
|
|
98
|
+
while (misses(worst.want) > 0.02 && worst.want > worst.h * 1e-3) worst.want /= Math.SQRT2;
|
|
99
|
+
return worst;
|
|
100
|
+
}
|
|
101
|
+
|
|
58
102
|
// A profile against the device's x: { x, values }, piecewise linear between its points and
|
|
59
103
|
// constant beyond its ends (read with profileAt).
|
|
60
104
|
function profile(v, path, what, ok, okText) {
|
|
@@ -325,6 +369,18 @@ export function normalizeDevice(def) {
|
|
|
325
369
|
} catch (err) {
|
|
326
370
|
throw new DeviceError(`grid: ${err.message}`);
|
|
327
371
|
}
|
|
372
|
+
// A starting profile steeper than the cells under it starts as something else, silently: a
|
|
373
|
+
// liquid junction's potential then looks as if it grows while the junction forms.
|
|
374
|
+
regions.forEach((reg, r) => {
|
|
375
|
+
const missed = reg.c0Profile.map((p, i) => p && { i, ...unresolved(p, grid, r) }).filter((m) => m && m.off);
|
|
376
|
+
if (missed.length === 0) return;
|
|
377
|
+
const miss = missed.reduce((a, m) => (m.off > a.off ? m : a));
|
|
378
|
+
warnings.push(
|
|
379
|
+
`regions[${r}].c0 (${missed.map((m) => species[m.i].name).join(', ')}): the grid doesn't resolve the starting profile ` +
|
|
380
|
+
`(at x = ${miss.x.toExponential(2)} m, nodes ${miss.h.toExponential(1)} m apart miss it by ${Math.round(100 * miss.off)}% of its range), ` +
|
|
381
|
+
`so the start isn't the one given; cells of about ${miss.want.toExponential(0)} m there would resolve it (hmax, or a region boundary there to grade toward)`,
|
|
382
|
+
);
|
|
383
|
+
});
|
|
328
384
|
const geometry = normalizeGeometry(def.geometry);
|
|
329
385
|
if (geometry.type !== 'planar') {
|
|
330
386
|
regions.forEach((reg, r) => need(reg.velocity === 0, `regions[${r}].velocity: flow is for a planar device (a uniform velocity through a varying cross-section wouldn't conserve the liquid)`));
|
|
@@ -669,7 +725,7 @@ function normalizeGeometry(g) {
|
|
|
669
725
|
// the window, `area` (m²/m³) of it per volume.
|
|
670
726
|
function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT) {
|
|
671
727
|
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', 'gates']);
|
|
728
|
+
fields(pdef, path, ['name', 'region', 'from', 'to', 'V', 'I', 'R', 'terminal', 'species', 'reactions', 'area', 'surface', 'capacitance', 'gates', 'bath']);
|
|
673
729
|
let r;
|
|
674
730
|
if (Number.isInteger(pdef.region)) r = pdef.region;
|
|
675
731
|
else r = regions.findIndex((reg) => reg.name === pdef.region);
|
|
@@ -691,9 +747,27 @@ function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT
|
|
|
691
747
|
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
748
|
const capacitance = portCapacitance(pdef, path, mat);
|
|
693
749
|
// Gates (a membrane's channels): they follow the voltage across the port's capacitance.
|
|
694
|
-
const gates = normalizeGates(pdef.gates, path, mat, mat);
|
|
750
|
+
const gates = normalizeGates(pdef.gates, path, mat, mat, "the port's capacitance");
|
|
695
751
|
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`);
|
|
696
752
|
need(pdef.species === undefined ? reactions.length > 0 || capacitance !== null : isObject(pdef.species), `${path}.species must map species names to port links`);
|
|
753
|
+
// A bath: the outside's composition. The port's V is then the outside's φ, and each linked
|
|
754
|
+
// species' level there, against it, follows from the composition through the window's
|
|
755
|
+
// statistics, (μ° + RT ζ)/(zF) (or μ for a neutral one), unless its link gives one. Only the
|
|
756
|
+
// linked species' levels are taken, so it needn't list (or balance) the rest.
|
|
757
|
+
let bathLevel = null;
|
|
758
|
+
if (pdef.bath !== undefined) {
|
|
759
|
+
need(isObject(pdef.bath) && isObject(pdef.bath.c), `${path}.bath must be { c: { species: concentration } }`);
|
|
760
|
+
fields(pdef.bath, `${path}.bath`, ['c']);
|
|
761
|
+
const cb = new Float64Array(species.length);
|
|
762
|
+
for (const [sname, v] of Object.entries(pdef.bath.c)) {
|
|
763
|
+
need(speciesIndex.has(sname), `${path}.bath.c.${sname}: unknown species '${sname}'${known(speciesIndex)}`);
|
|
764
|
+
const i = speciesIndex.get(sname);
|
|
765
|
+
need(mat.present[i], `${path}.bath.c.${sname}: '${sname}' is absent from ${reg.name} (material '${mat.name}'), so its level outside means nothing here`);
|
|
766
|
+
cb[i] = positive(v, `${path}.bath.c.${sname}`);
|
|
767
|
+
}
|
|
768
|
+
const zb = bathZeta(mat, cb, reg.background);
|
|
769
|
+
bathLevel = (i) => (cb[i] > 0 ? mat.mu0[i] + RT * zb[i] : undefined); // μ̄ − zFφ outside
|
|
770
|
+
}
|
|
697
771
|
const links = species.map(() => ({ type: 'blocked' }));
|
|
698
772
|
for (const [sname, raw] of Object.entries(pdef.species ?? {})) {
|
|
699
773
|
const lpath = `${path}.species.${sname}`;
|
|
@@ -708,14 +782,15 @@ function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT
|
|
|
708
782
|
const z = species[i].z;
|
|
709
783
|
// The outside level: an offset from V for charged species, an absolute μ for neutral ones.
|
|
710
784
|
let level;
|
|
785
|
+
const fromBath = bathLevel?.(i);
|
|
711
786
|
if (z === 0) {
|
|
712
787
|
need(link.type !== 'conductance', `${lpath}: a neutral species exchanges by { type: 'exchange', k, mu }`);
|
|
713
|
-
level = { mu: finite(link.mu, `${lpath}.mu (the outside μ, J/mol)`) };
|
|
788
|
+
level = { mu: finite(link.mu ?? fromBath, `${lpath}.mu (the outside μ, J/mol${bathLevel ? `; or give '${sname}' in the bath` : ''})`) };
|
|
714
789
|
} else {
|
|
715
790
|
need(link.type !== 'exchange', `${lpath}: a charged species exchanges by { type: 'conductance', G }`);
|
|
716
791
|
need(link.mu === undefined, `${lpath}: a charged species is held by an offset from the port voltage, not by mu`);
|
|
717
|
-
const offset = link.offset ?? (terminal === i ? 0 : undefined);
|
|
718
|
-
need(offset !== undefined, `${lpath}.offset: give V_i − V (V); only the terminal species defaults to 0`);
|
|
792
|
+
const offset = link.offset ?? (fromBath !== undefined ? fromBath / (z * FARADAY) : !bathLevel && terminal === i ? 0 : undefined);
|
|
793
|
+
need(offset !== undefined, bathLevel ? `${lpath}: give '${sname}' in the bath (or an offset, V_i − V)` : `${lpath}.offset: give V_i − V (V); only the terminal species defaults to 0 (or give the port a bath)`);
|
|
719
794
|
level = { offset: finite(offset, `${lpath}.offset`) };
|
|
720
795
|
}
|
|
721
796
|
// On a metal, G is a lumped conductance per area (S/m²), spread over its thickness.
|
|
@@ -968,10 +1043,10 @@ function gatedBy(raw, gates, lpath, where) {
|
|
|
968
1043
|
return [q, p];
|
|
969
1044
|
});
|
|
970
1045
|
}
|
|
971
|
-
function normalizeGates(raw, where, matL, matR) {
|
|
1046
|
+
function normalizeGates(raw, where, matL, matR, across = 'the face') {
|
|
972
1047
|
if (raw === undefined) return [];
|
|
973
1048
|
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
|
|
1049
|
+
need(!matL.phiFree && !matR.phiFree, `${where}.gates: a gate follows the voltage across ${across}, which needs φ there`);
|
|
975
1050
|
return Object.entries(raw).map(([name, gdef]) => {
|
|
976
1051
|
const gpath = `${where}.gates.${name}`;
|
|
977
1052
|
need(isObject(gdef), `${gpath} must be { alpha, beta }`);
|
|
@@ -1153,6 +1228,22 @@ function normalizeContact(cdef, side, region, materials, species, speciesIndex,
|
|
|
1153
1228
|
links[i] = { type: 'conductance', G: positive(link.G, `${lpath}.G`), offset: finite(offset, `${lpath}.offset`) };
|
|
1154
1229
|
continue;
|
|
1155
1230
|
}
|
|
1231
|
+
if (link.type === 'velocity') {
|
|
1232
|
+
// A surface velocity: N_in = v (c_eq − c), c_eq the edge's concentration in equilibrium
|
|
1233
|
+
// with the outside level (thermionic emission over a barrier, a contact's surface
|
|
1234
|
+
// recombination velocity), v c (e^((μ̄_out − μ̄)/RT) − 1) for any statistics.
|
|
1235
|
+
const v = positive(link.v, `${lpath}.v (m/s)`);
|
|
1236
|
+
if (species[i].z === 0) {
|
|
1237
|
+
need(link.offset === undefined, `${lpath}: a neutral species' outside level is its mu (J/mol), not an offset`);
|
|
1238
|
+
links[i] = { type: 'velocity', v, mu: finite(link.mu, `${lpath}.mu (the outside μ, J/mol)`) };
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
need(link.mu === undefined, `${lpath}: a charged species' outside level is an offset from the terminal voltage, not a mu`);
|
|
1242
|
+
const offset = link.offset ?? (terminal === i ? 0 : undefined);
|
|
1243
|
+
need(offset !== undefined, `${lpath}.offset: give V_i − V_terminal (V) of the outside reservoir; only the terminal species defaults to 0`);
|
|
1244
|
+
links[i] = { type: 'velocity', v, offset: finite(offset, `${lpath}.offset`) };
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1156
1247
|
if (link.type === 'exchange') {
|
|
1157
1248
|
// A neutral species exchanging with the outside at its μ: N_in = k (μ_out − μ)/RT.
|
|
1158
1249
|
need(species[i].z === 0, `${lpath}: a charged species exchanges by { type: 'conductance', G }`);
|
|
@@ -1184,12 +1275,19 @@ function normalizeContact(cdef, side, region, materials, species, speciesIndex,
|
|
|
1184
1275
|
const bath = cdef.bath;
|
|
1185
1276
|
need(isObject(bath) && isObject(bath.c), `${path}.bath must be { c: { species: concentration }, reference }`);
|
|
1186
1277
|
fields(bath, `${path}.bath`, ['c', 'reference', 'offset']);
|
|
1187
|
-
|
|
1188
|
-
|
|
1278
|
+
// Without a reference, V is the bath's φ (an ideal salt bridge: the membrane potential's
|
|
1279
|
+
// convention), and the terminal is the first charged species in it.
|
|
1280
|
+
const byPhi = bath.reference === undefined;
|
|
1281
|
+
if (byPhi) need(bath.offset === undefined, `${path}.bath.offset: an offset places the reference species, and this bath has none (its V is its φ)`);
|
|
1282
|
+
else need(speciesIndex.has(bath.reference), `${path}.bath.reference: unknown species ${JSON.stringify(bath.reference)}${known(speciesIndex)}`);
|
|
1283
|
+
const r = byPhi
|
|
1284
|
+
? species.findIndex((sp) => sp.z !== 0 && bath.c[sp.name] > 0)
|
|
1285
|
+
: speciesIndex.get(bath.reference);
|
|
1286
|
+
need(r >= 0, `${path}.bath: the bath needs a charged species`);
|
|
1189
1287
|
need(species[r].z !== 0, `${path}.bath.reference: the reference species must be charged`);
|
|
1190
|
-
need(bath.c[
|
|
1191
|
-
need(cdef.terminal === undefined || terminal === r, `${path}.terminal must be the bath's reference species`);
|
|
1192
|
-
terminal = r;
|
|
1288
|
+
need(bath.c[species[r].name] !== undefined, `${path}.bath.reference: '${bath.reference}' must be in the bath`);
|
|
1289
|
+
need(byPhi || cdef.terminal === undefined || terminal === r, `${path}.terminal must be the bath's reference species`);
|
|
1290
|
+
terminal = byPhi && terminal !== null ? terminal : r;
|
|
1193
1291
|
let charge = region.fixedCharge / FARADAY, scale = Math.abs(charge);
|
|
1194
1292
|
const cb = new Float64Array(species.length);
|
|
1195
1293
|
for (const [sname, v] of Object.entries(bath.c)) {
|
|
@@ -1205,7 +1303,7 @@ function normalizeContact(cdef, side, region, materials, species, speciesIndex,
|
|
|
1205
1303
|
const zb = bathZeta(mat, cb, region.background);
|
|
1206
1304
|
const level = (i) => mat.mu0[i] + RT * zb[i]; // μ̄ − zFφ_bath
|
|
1207
1305
|
const refOffset = bath.offset === undefined ? 0 : finite(bath.offset, `${path}.bath.offset`);
|
|
1208
|
-
const beta = refOffset - level(r) / (species[r].z * FARADAY); // φ_bath − V
|
|
1306
|
+
const beta = byPhi ? 0 : refOffset - level(r) / (species[r].z * FARADAY); // φ_bath − V
|
|
1209
1307
|
for (let i = 0; i < species.length; i++) {
|
|
1210
1308
|
if (!(cb[i] > 0)) continue;
|
|
1211
1309
|
links[i] =
|
package/src/index.js
CHANGED
|
@@ -166,7 +166,12 @@ export class Device {
|
|
|
166
166
|
* @returns {Solution}
|
|
167
167
|
*/
|
|
168
168
|
solve(opts) {
|
|
169
|
-
|
|
169
|
+
// A solve that fails reports the state it failed in, and leaves the device as it was: a
|
|
170
|
+
// device with no steady state (a host filling at a current) can still advance() from there.
|
|
171
|
+
const snap = this.solver._snapshot();
|
|
172
|
+
const sol = makeSolution(this.solver, this.solver.solveSteady(opts));
|
|
173
|
+
if (!sol.converged) this.solver._restore(snap);
|
|
174
|
+
return sol;
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
/**
|
package/src/kit.js
CHANGED
|
@@ -15,6 +15,7 @@ export { describe, unitWarnings } from './describe.js';
|
|
|
15
15
|
export { recorder } from './recorder.js';
|
|
16
16
|
export { henderson, planck } from './junction.js';
|
|
17
17
|
export { check } from './checks.js';
|
|
18
|
+
export { particles } from './particles.js';
|
|
18
19
|
export { polarization } from './polarization.js';
|
|
19
20
|
export { perovskiteCell, PEROVSKITE_SCANS, hysteresis } from './devices.js';
|
|
20
21
|
export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal } from './data.js';
|