driftlet 0.12.0 → 0.13.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 +51 -21
- package/docs/kit.md +14 -3
- package/docs/reliability.md +60 -3
- package/docs/statistics.md +10 -0
- package/llms.txt +566 -295
- package/package.json +1 -1
- package/src/channels.js +27 -4
- package/src/describe.js +1 -1
- package/src/device.js +57 -14
- package/src/index.js +6 -1
- package/src/solution.js +16 -2
- package/src/solver.js +92 -48
- package/src/stack.js +6 -4
- package/src/types.js +2 -1
- package/types/channels.d.ts +14 -3
- package/types/solver.d.ts +4 -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/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);
|
|
@@ -669,7 +670,7 @@ function normalizeGeometry(g) {
|
|
|
669
670
|
// the window, `area` (m²/m³) of it per volume.
|
|
670
671
|
function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT) {
|
|
671
672
|
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']);
|
|
673
|
+
fields(pdef, path, ['name', 'region', 'from', 'to', 'V', 'I', 'R', 'terminal', 'species', 'reactions', 'area', 'surface', 'capacitance', 'gates', 'bath']);
|
|
673
674
|
let r;
|
|
674
675
|
if (Number.isInteger(pdef.region)) r = pdef.region;
|
|
675
676
|
else r = regions.findIndex((reg) => reg.name === pdef.region);
|
|
@@ -691,9 +692,27 @@ function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT
|
|
|
691
692
|
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
693
|
const capacitance = portCapacitance(pdef, path, mat);
|
|
693
694
|
// Gates (a membrane's channels): they follow the voltage across the port's capacitance.
|
|
694
|
-
const gates = normalizeGates(pdef.gates, path, mat, mat);
|
|
695
|
+
const gates = normalizeGates(pdef.gates, path, mat, mat, "the port's capacitance");
|
|
695
696
|
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
697
|
need(pdef.species === undefined ? reactions.length > 0 || capacitance !== null : isObject(pdef.species), `${path}.species must map species names to port links`);
|
|
698
|
+
// A bath: the outside's composition. The port's V is then the outside's φ, and each linked
|
|
699
|
+
// species' level there, against it, follows from the composition through the window's
|
|
700
|
+
// statistics, (μ° + RT ζ)/(zF) (or μ for a neutral one), unless its link gives one. Only the
|
|
701
|
+
// linked species' levels are taken, so it needn't list (or balance) the rest.
|
|
702
|
+
let bathLevel = null;
|
|
703
|
+
if (pdef.bath !== undefined) {
|
|
704
|
+
need(isObject(pdef.bath) && isObject(pdef.bath.c), `${path}.bath must be { c: { species: concentration } }`);
|
|
705
|
+
fields(pdef.bath, `${path}.bath`, ['c']);
|
|
706
|
+
const cb = new Float64Array(species.length);
|
|
707
|
+
for (const [sname, v] of Object.entries(pdef.bath.c)) {
|
|
708
|
+
need(speciesIndex.has(sname), `${path}.bath.c.${sname}: unknown species '${sname}'${known(speciesIndex)}`);
|
|
709
|
+
const i = speciesIndex.get(sname);
|
|
710
|
+
need(mat.present[i], `${path}.bath.c.${sname}: '${sname}' is absent from ${reg.name} (material '${mat.name}'), so its level outside means nothing here`);
|
|
711
|
+
cb[i] = positive(v, `${path}.bath.c.${sname}`);
|
|
712
|
+
}
|
|
713
|
+
const zb = bathZeta(mat, cb, reg.background);
|
|
714
|
+
bathLevel = (i) => (cb[i] > 0 ? mat.mu0[i] + RT * zb[i] : undefined); // μ̄ − zFφ outside
|
|
715
|
+
}
|
|
697
716
|
const links = species.map(() => ({ type: 'blocked' }));
|
|
698
717
|
for (const [sname, raw] of Object.entries(pdef.species ?? {})) {
|
|
699
718
|
const lpath = `${path}.species.${sname}`;
|
|
@@ -708,14 +727,15 @@ function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT
|
|
|
708
727
|
const z = species[i].z;
|
|
709
728
|
// The outside level: an offset from V for charged species, an absolute μ for neutral ones.
|
|
710
729
|
let level;
|
|
730
|
+
const fromBath = bathLevel?.(i);
|
|
711
731
|
if (z === 0) {
|
|
712
732
|
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)`) };
|
|
733
|
+
level = { mu: finite(link.mu ?? fromBath, `${lpath}.mu (the outside μ, J/mol${bathLevel ? `; or give '${sname}' in the bath` : ''})`) };
|
|
714
734
|
} else {
|
|
715
735
|
need(link.type !== 'exchange', `${lpath}: a charged species exchanges by { type: 'conductance', G }`);
|
|
716
736
|
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`);
|
|
737
|
+
const offset = link.offset ?? (fromBath !== undefined ? fromBath / (z * FARADAY) : !bathLevel && terminal === i ? 0 : undefined);
|
|
738
|
+
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
739
|
level = { offset: finite(offset, `${lpath}.offset`) };
|
|
720
740
|
}
|
|
721
741
|
// On a metal, G is a lumped conductance per area (S/m²), spread over its thickness.
|
|
@@ -968,10 +988,10 @@ function gatedBy(raw, gates, lpath, where) {
|
|
|
968
988
|
return [q, p];
|
|
969
989
|
});
|
|
970
990
|
}
|
|
971
|
-
function normalizeGates(raw, where, matL, matR) {
|
|
991
|
+
function normalizeGates(raw, where, matL, matR, across = 'the face') {
|
|
972
992
|
if (raw === undefined) return [];
|
|
973
993
|
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
|
|
994
|
+
need(!matL.phiFree && !matR.phiFree, `${where}.gates: a gate follows the voltage across ${across}, which needs φ there`);
|
|
975
995
|
return Object.entries(raw).map(([name, gdef]) => {
|
|
976
996
|
const gpath = `${where}.gates.${name}`;
|
|
977
997
|
need(isObject(gdef), `${gpath} must be { alpha, beta }`);
|
|
@@ -1153,6 +1173,22 @@ function normalizeContact(cdef, side, region, materials, species, speciesIndex,
|
|
|
1153
1173
|
links[i] = { type: 'conductance', G: positive(link.G, `${lpath}.G`), offset: finite(offset, `${lpath}.offset`) };
|
|
1154
1174
|
continue;
|
|
1155
1175
|
}
|
|
1176
|
+
if (link.type === 'velocity') {
|
|
1177
|
+
// A surface velocity: N_in = v (c_eq − c), c_eq the edge's concentration in equilibrium
|
|
1178
|
+
// with the outside level (thermionic emission over a barrier, a contact's surface
|
|
1179
|
+
// recombination velocity), v c (e^((μ̄_out − μ̄)/RT) − 1) for any statistics.
|
|
1180
|
+
const v = positive(link.v, `${lpath}.v (m/s)`);
|
|
1181
|
+
if (species[i].z === 0) {
|
|
1182
|
+
need(link.offset === undefined, `${lpath}: a neutral species' outside level is its mu (J/mol), not an offset`);
|
|
1183
|
+
links[i] = { type: 'velocity', v, mu: finite(link.mu, `${lpath}.mu (the outside μ, J/mol)`) };
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
need(link.mu === undefined, `${lpath}: a charged species' outside level is an offset from the terminal voltage, not a mu`);
|
|
1187
|
+
const offset = link.offset ?? (terminal === i ? 0 : undefined);
|
|
1188
|
+
need(offset !== undefined, `${lpath}.offset: give V_i − V_terminal (V) of the outside reservoir; only the terminal species defaults to 0`);
|
|
1189
|
+
links[i] = { type: 'velocity', v, offset: finite(offset, `${lpath}.offset`) };
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1156
1192
|
if (link.type === 'exchange') {
|
|
1157
1193
|
// A neutral species exchanging with the outside at its μ: N_in = k (μ_out − μ)/RT.
|
|
1158
1194
|
need(species[i].z === 0, `${lpath}: a charged species exchanges by { type: 'conductance', G }`);
|
|
@@ -1184,12 +1220,19 @@ function normalizeContact(cdef, side, region, materials, species, speciesIndex,
|
|
|
1184
1220
|
const bath = cdef.bath;
|
|
1185
1221
|
need(isObject(bath) && isObject(bath.c), `${path}.bath must be { c: { species: concentration }, reference }`);
|
|
1186
1222
|
fields(bath, `${path}.bath`, ['c', 'reference', 'offset']);
|
|
1187
|
-
|
|
1188
|
-
|
|
1223
|
+
// Without a reference, V is the bath's φ (an ideal salt bridge: the membrane potential's
|
|
1224
|
+
// convention), and the terminal is the first charged species in it.
|
|
1225
|
+
const byPhi = bath.reference === undefined;
|
|
1226
|
+
if (byPhi) need(bath.offset === undefined, `${path}.bath.offset: an offset places the reference species, and this bath has none (its V is its φ)`);
|
|
1227
|
+
else need(speciesIndex.has(bath.reference), `${path}.bath.reference: unknown species ${JSON.stringify(bath.reference)}${known(speciesIndex)}`);
|
|
1228
|
+
const r = byPhi
|
|
1229
|
+
? species.findIndex((sp) => sp.z !== 0 && bath.c[sp.name] > 0)
|
|
1230
|
+
: speciesIndex.get(bath.reference);
|
|
1231
|
+
need(r >= 0, `${path}.bath: the bath needs a charged species`);
|
|
1189
1232
|
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;
|
|
1233
|
+
need(bath.c[species[r].name] !== undefined, `${path}.bath.reference: '${bath.reference}' must be in the bath`);
|
|
1234
|
+
need(byPhi || cdef.terminal === undefined || terminal === r, `${path}.terminal must be the bath's reference species`);
|
|
1235
|
+
terminal = byPhi && terminal !== null ? terminal : r;
|
|
1193
1236
|
let charge = region.fixedCharge / FARADAY, scale = Math.abs(charge);
|
|
1194
1237
|
const cb = new Float64Array(species.length);
|
|
1195
1238
|
for (const [sname, v] of Object.entries(bath.c)) {
|
|
@@ -1205,7 +1248,7 @@ function normalizeContact(cdef, side, region, materials, species, speciesIndex,
|
|
|
1205
1248
|
const zb = bathZeta(mat, cb, region.background);
|
|
1206
1249
|
const level = (i) => mat.mu0[i] + RT * zb[i]; // μ̄ − zFφ_bath
|
|
1207
1250
|
const refOffset = bath.offset === undefined ? 0 : finite(bath.offset, `${path}.bath.offset`);
|
|
1208
|
-
const beta = refOffset - level(r) / (species[r].z * FARADAY); // φ_bath − V
|
|
1251
|
+
const beta = byPhi ? 0 : refOffset - level(r) / (species[r].z * FARADAY); // φ_bath − V
|
|
1209
1252
|
for (let i = 0; i < species.length; i++) {
|
|
1210
1253
|
if (!(cb[i] > 0)) continue;
|
|
1211
1254
|
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/solution.js
CHANGED
|
@@ -191,7 +191,7 @@ export function makeSolution(solver, result = {}) {
|
|
|
191
191
|
|
|
192
192
|
// Resolution warnings: where the model resolves a double layer (dipole or capacitive faces,
|
|
193
193
|
// gate contacts), check the local Debye length against the adjacent cell.
|
|
194
|
-
sol.warnings = [...(model.warnings ?? [])];
|
|
194
|
+
sol.warnings = [...(model.warnings ?? []), ...(result.stopped ? [result.stopped] : [])];
|
|
195
195
|
// A surface covered almost completely: its coverages' η barely move anything below ~1e-10 bare,
|
|
196
196
|
// and Newton loses them (a transient can stall there).
|
|
197
197
|
sol.ports.forEach((p) => {
|
|
@@ -207,9 +207,17 @@ export function makeSolution(solver, result = {}) {
|
|
|
207
207
|
const cond = solver.conditioning, un = solver.unreached;
|
|
208
208
|
if (result.converged === false && un) {
|
|
209
209
|
const [[V0, I0], [V1, I1]] = [un.low, un.high], g = (x) => x.toPrecision(3), A = model.geometry.type === 'planar' ? 'A/m²' : 'A';
|
|
210
|
+
// The opposite sign within reach: likely a sign slip (a drive is the current into the device).
|
|
211
|
+
const flipped = Math.min(I0, I1) <= -un.target && -un.target <= Math.max(I0, I1);
|
|
210
212
|
sol.warnings.push(
|
|
211
213
|
`${un.terminal}: no steady state passes the driven current ${g(un.target)} ${A}; held from ${g(V0)} V to ${g(V1)} V ` +
|
|
212
|
-
`it passed ${g(I0)} to ${g(I1)} ${A} (a limiting current, or kinetics too slow). Drive less, or hold a voltage
|
|
214
|
+
`it passed ${g(I0)} to ${g(I1)} ${A} (a limiting current, or kinetics too slow). Drive less, or hold a voltage.` +
|
|
215
|
+
(flipped ? ` (It passes ${g(-un.target)} ${A}: if that was meant, a terminal's current is into the device.)` : '') +
|
|
216
|
+
// Nothing passed at any voltage: the device only stores what comes in (a host filling, a
|
|
217
|
+
// capacitor charging), so it has no steady state at a current.
|
|
218
|
+
(Math.max(Math.abs(I0), Math.abs(I1)) < 1e-6 * Math.abs(un.target)
|
|
219
|
+
? ' It passes almost nothing steadily at any voltage, so it may only store what comes in (a host filling, a capacitor charging), which has no steady state at a current: advance() in time instead.'
|
|
220
|
+
: ''),
|
|
213
221
|
);
|
|
214
222
|
} else if (result.converged === false && cond && cond.digits > 12) {
|
|
215
223
|
const digits = Number.isFinite(cond.digits) ? `lost ${cond.digits.toFixed(0)} of its ~16 digits` : 'was exactly singular';
|
|
@@ -219,6 +227,12 @@ export function makeSolution(solver, result = {}) {
|
|
|
219
227
|
'chain whose level nothing pins. Strengthen that coupling, or anchor the region (a port, a contact).',
|
|
220
228
|
);
|
|
221
229
|
}
|
|
230
|
+
if (result.converged === false && !result.stopped && !un && !(cond && cond.digits > 12)) {
|
|
231
|
+
sol.warnings.push(
|
|
232
|
+
'the solve did not converge, and nothing specific showed why. ' +
|
|
233
|
+
'Check the definition with describe() (from driftlet/kit); approach this state in smaller steps (a sweep, or advance() in time); or refine the grid.',
|
|
234
|
+
);
|
|
235
|
+
}
|
|
222
236
|
const debye = (g) => {
|
|
223
237
|
const mat = model.materials[model.regions[grid.nodeRegion[g]].material];
|
|
224
238
|
const s2 = solver.screening(g); // zᵀKz: Σ z² c for ideal statistics
|
package/src/solver.js
CHANGED
|
@@ -261,12 +261,18 @@ export class SolverError extends Error {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// Lagrange weights at x through the points s0, s1, s2.
|
|
265
|
+
function lagrange3(x, s0, s1, s2) {
|
|
266
|
+
return [((x - s1) * (x - s2)) / ((s0 - s1) * (s0 - s2)), ((x - s0) * (x - s2)) / ((s1 - s0) * (s1 - s2)), ((x - s0) * (x - s1)) / ((s2 - s0) * (s2 - s1))];
|
|
267
|
+
}
|
|
268
|
+
|
|
264
269
|
// A gate's rate law (see normalizeGates in device.js) at voltage V: [rate, ∂rate/∂V], 1/s and 1/(s·V).
|
|
265
270
|
function gateRate(law, V) {
|
|
266
271
|
const { rate, midpoint, scale } = law;
|
|
267
272
|
if (law.type === 'exp') {
|
|
268
|
-
|
|
269
|
-
|
|
273
|
+
// (held finite where Newton's iterates stray to absurd voltages: β_m at −13 V would overflow)
|
|
274
|
+
const y = (V - midpoint) / scale, v = rate * Math.exp(Math.min(y, 700));
|
|
275
|
+
return [v, y > 700 ? 0 : v / scale];
|
|
270
276
|
}
|
|
271
277
|
if (law.type === 'sigmoid') {
|
|
272
278
|
const e = Math.exp((midpoint - V) / scale), d = 1 + e;
|
|
@@ -1214,7 +1220,7 @@ export class Solver {
|
|
|
1214
1220
|
/** η_i/RT that a fixed contact link imposes (or NaN if the link isn't fixed). */
|
|
1215
1221
|
contactEta(side, i) {
|
|
1216
1222
|
const link = this.model.contacts[side].species[i];
|
|
1217
|
-
if (link.type !== 'equilibrium') return NaN;
|
|
1223
|
+
if (link.type !== 'equilibrium' && link.type !== 'velocity') return NaN;
|
|
1218
1224
|
const z = this.z[i];
|
|
1219
1225
|
return z === 0 ? link.mu / this.model.RT : (z * (this.termV[side === 'left' ? 0 : 1] + link.offset)) / this.VT;
|
|
1220
1226
|
}
|
|
@@ -2519,6 +2525,7 @@ export class Solver {
|
|
|
2519
2525
|
flux[i] += res[R[o]];
|
|
2520
2526
|
this._captureRow(b, r, zF, C);
|
|
2521
2527
|
this._replaceRow(b, r);
|
|
2528
|
+
for (const Bk of this.termB) Bk[R[o]] = 0; // (other ports' terms in the row replaced)
|
|
2522
2529
|
this._j(b, r, b, r, 1);
|
|
2523
2530
|
res[R[o]] = -deta;
|
|
2524
2531
|
B[R[o]] += -z[i] / VT;
|
|
@@ -2746,7 +2753,7 @@ export class Solver {
|
|
|
2746
2753
|
}
|
|
2747
2754
|
|
|
2748
2755
|
// Conductance links: J (toward the device) = G (V_out − V_i), V_out = V_t + offset; exchange
|
|
2749
|
-
// links (neutral species): N_in = k (μ_out − μ)/RT.
|
|
2756
|
+
// links (neutral species): N_in = k (μ_out − μ)/RT; velocity links, v (c_eq − c).
|
|
2750
2757
|
for (let i = 0; i < n; i++) {
|
|
2751
2758
|
const link = ct.species[i];
|
|
2752
2759
|
const o = b * M + 1 + i;
|
|
@@ -2758,6 +2765,16 @@ export class Solver {
|
|
|
2758
2765
|
res[R[o]] -= (Ac * link.G * (Vt + link.offset - Vi)) / (z[i] * F);
|
|
2759
2766
|
this._j(b, 1 + i, b, 1 + i, (Ac * link.G * VT) / (z[i] * z[i] * F));
|
|
2760
2767
|
B[R[o]] += (-Ac * link.G) / (z[i] * F);
|
|
2768
|
+
} else if (link.type === 'velocity') {
|
|
2769
|
+
// N_in = v c (e^(η_out − η) − 1): v (c_eq − c), c_eq in equilibrium with the outside.
|
|
2770
|
+
const ci = c[g * n + i], E = Math.exp(Math.min(this.contactEta(side, i) - (u[o] + uLo[o]), 700));
|
|
2771
|
+
const N = Ac * link.v * ci * (E - 1), d = this.dA;
|
|
2772
|
+
res[R[o]] -= N;
|
|
2773
|
+
d.fill(0);
|
|
2774
|
+
this._dlnc(g, i, 1, d);
|
|
2775
|
+
for (let s = 0; s <= n; s++) if (d[s] !== 0) this._j(b, 1 + i, b, s, -N * d[s]);
|
|
2776
|
+
this._j(b, 1 + i, b, 1 + i, Ac * link.v * ci * E);
|
|
2777
|
+
if (z[i] !== 0) B[R[o]] += (-Ac * link.v * ci * E * z[i]) / VT;
|
|
2761
2778
|
}
|
|
2762
2779
|
}
|
|
2763
2780
|
|
|
@@ -3408,7 +3425,7 @@ export class Solver {
|
|
|
3408
3425
|
}
|
|
3409
3426
|
} else {
|
|
3410
3427
|
// (Order 2 once three consistent states can check it.)
|
|
3411
|
-
const r = this.step(h, { method: this.history.length >= 2 ? method : 'be', guess: this._extrapolate(guess,
|
|
3428
|
+
const r = this.step(h, { method: this.history.length >= 2 ? method : 'be', guess: this._extrapolate(guess, h) });
|
|
3412
3429
|
iterations += r.iterations;
|
|
3413
3430
|
if (!r.converged) {
|
|
3414
3431
|
rejected++;
|
|
@@ -3454,24 +3471,28 @@ export class Solver {
|
|
|
3454
3471
|
}
|
|
3455
3472
|
this.dtNext = dt;
|
|
3456
3473
|
this.landing = undefined;
|
|
3457
|
-
|
|
3474
|
+
// Stopped short, other than on the caller's wall-time budget: say where, and why.
|
|
3475
|
+
let stopped;
|
|
3476
|
+
const at = `advance stopped at t = ${this.time.toPrecision(6)} s of ${tEnd.toPrecision(6)}`;
|
|
3477
|
+
if (failed) stopped = `${at}: a step didn't converge even when shortened to ${dt.toExponential(1)} s. A drive's jump too big for one step (ramp it), a driven current the device can't keep passing (a diode's reverse recovery past its storage time: stop there), or a device the steady solves also find hard.`;
|
|
3478
|
+
else if (this.time < tEnd && steps + rejected >= maxSteps) stopped = `${at}: maxSteps (${maxSteps}) used up (${rejected} of them rejected). Call again to go on, or loosen tol.`;
|
|
3479
|
+
return { converged: !failed, done: this.time >= tEnd, steps, rejected, iterations, trace, ...(stopped ? { stopped } : {}) };
|
|
3458
3480
|
}
|
|
3459
3481
|
|
|
3460
|
-
// Quadratic (or linear) extrapolation of the state
|
|
3461
|
-
// two most recent history entries: a starting guess for Newton.
|
|
3462
|
-
|
|
3482
|
+
// Quadratic (or linear) extrapolation of the state a time h ahead, from the current state and
|
|
3483
|
+
// the two most recent history entries: a starting guess for Newton. (In offsets built from the
|
|
3484
|
+
// steps' own lengths, never differences of absolute times: a second into a run, after a jump
|
|
3485
|
+
// that a fine grid resolves in steps of 1e-13 s, those differences keep only 3 digits.)
|
|
3486
|
+
_extrapolate(out, h) {
|
|
3463
3487
|
const [e0, e1] = this.history;
|
|
3464
|
-
const
|
|
3488
|
+
const u = this.u;
|
|
3465
3489
|
if (!e0) return undefined;
|
|
3466
3490
|
if (!e1) {
|
|
3467
|
-
const l =
|
|
3491
|
+
const l = h / e0.dt;
|
|
3468
3492
|
for (let k = 0; k < out.length; k++) out[k] = u[k] + l * (u[k] - e0.u[k]);
|
|
3469
3493
|
return out;
|
|
3470
3494
|
}
|
|
3471
|
-
const
|
|
3472
|
-
const l0 = ((t - t1) * (t - t2)) / ((t0 - t1) * (t0 - t2));
|
|
3473
|
-
const l1 = ((t - t0) * (t - t2)) / ((t1 - t0) * (t1 - t2));
|
|
3474
|
-
const l2 = ((t - t0) * (t - t1)) / ((t2 - t0) * (t2 - t1));
|
|
3495
|
+
const [l0, l1, l2] = lagrange3(h, 0, -e0.dt, -(e0.dt + e1.dt));
|
|
3475
3496
|
for (let k = 0; k < out.length; k++) out[k] = l0 * u[k] + l1 * e0.u[k] + l2 * e1.u[k];
|
|
3476
3497
|
return out;
|
|
3477
3498
|
}
|
|
@@ -3480,7 +3501,7 @@ export class Solver {
|
|
|
3480
3501
|
// linear after backward Euler), and the factor turning |u − pred| into the local error.
|
|
3481
3502
|
_predict(pred, bdf) {
|
|
3482
3503
|
const [e0, e1, e2] = this.history;
|
|
3483
|
-
const
|
|
3504
|
+
const h = e0.dt;
|
|
3484
3505
|
if (!e1) {
|
|
3485
3506
|
pred.set(e0.u);
|
|
3486
3507
|
return { scale: 1, order: 1 };
|
|
@@ -3488,18 +3509,15 @@ export class Solver {
|
|
|
3488
3509
|
const hp = e1.dt;
|
|
3489
3510
|
if (bdf && e2) {
|
|
3490
3511
|
const hpp = e2.dt;
|
|
3491
|
-
// Lagrange through
|
|
3492
|
-
const
|
|
3493
|
-
const l0 = ((t - t1) * (t - t2)) / ((t0 - t1) * (t0 - t2));
|
|
3494
|
-
const l1 = ((t - t0) * (t - t2)) / ((t1 - t0) * (t1 - t2));
|
|
3495
|
-
const l2 = ((t - t0) * (t - t1)) / ((t2 - t0) * (t2 - t1));
|
|
3512
|
+
// Lagrange through the three states before at the new time (offsets as in _extrapolate).
|
|
3513
|
+
const [l0, l1, l2] = lagrange3(0, -h, -(h + hp), -(h + hp + hpp));
|
|
3496
3514
|
for (let k = 0; k < pred.length; k++) pred[k] = l0 * e0.u[k] + l1 * e1.u[k] + l2 * e2.u[k];
|
|
3497
3515
|
const w = h / hp;
|
|
3498
3516
|
const Cc = (h * h * h * (1 + w) * (1 + w)) / (w * (1 + 2 * w));
|
|
3499
3517
|
const Cp = h * (h + hp) * (h + hp + hpp);
|
|
3500
3518
|
return { scale: Cc / (Cc + Cp), order: 2 };
|
|
3501
3519
|
}
|
|
3502
|
-
const l0 = (
|
|
3520
|
+
const l0 = (h + hp) / hp, l1 = 1 - l0;
|
|
3503
3521
|
for (let k = 0; k < pred.length; k++) pred[k] = l0 * e0.u[k] + l1 * e1.u[k];
|
|
3504
3522
|
// Backward Euler's error against a linear predictor; after a BDF2 step without enough
|
|
3505
3523
|
// history this overestimates, which is safe.
|
|
@@ -3720,7 +3738,15 @@ export class Solver {
|
|
|
3720
3738
|
for (const X of ['A', 'B', 'C']) S[X] = sys[X].map((v, k) => (v - J[X][k]) * dts);
|
|
3721
3739
|
const Cs = this.termC.map((v, k) => v.map((x, j) => (x - Ct[k][j]) * dts));
|
|
3722
3740
|
const DIs = this.termDI.map((x, k) => (x - DI[k]) * dts);
|
|
3723
|
-
|
|
3741
|
+
// Where the measured contact's current may be read across a segment: anywhere without
|
|
3742
|
+
// ports; with them, only between it and the nearest window (a port's exchange takes current
|
|
3743
|
+
// in or out along it). Never for a port's own terminal.
|
|
3744
|
+
let cuts = kT < 2 ? this._currentCuts() : null;
|
|
3745
|
+
if (cuts && this.model.ports.length > 0) {
|
|
3746
|
+
const windows = this.model.ports.flatMap((p) => Array.from(p.nodes));
|
|
3747
|
+
const lo = Math.min(...windows), hi = Math.max(...windows);
|
|
3748
|
+
cuts = cuts.filter((cut) => (kT === 0 ? cut.s + 1 <= lo : cut.s >= hi));
|
|
3749
|
+
}
|
|
3724
3750
|
// J·v for GMRES, exact: the assembled J holds a flux's dependence on η_L and η_R as two
|
|
3725
3751
|
// entries, and where both are huge (an inversion layer) and v nearly uniform, J·v loses the
|
|
3726
3752
|
// flux to round-off. So the dilute kernels' terms are kept in difference form (see
|
|
@@ -3889,7 +3915,7 @@ export class Solver {
|
|
|
3889
3915
|
Ii += im;
|
|
3890
3916
|
for (let j = 0; j < Nc; j++) err += (Math.abs(c[j]) + w * Math.abs(cs[j])) * (eps * Math.hypot(p.r[j], p.i[j]) + p.floor);
|
|
3891
3917
|
}
|
|
3892
|
-
if (!cuts
|
|
3918
|
+
if (!cuts) return { Ir, Ii, err };
|
|
3893
3919
|
const sgn = kT === 0 ? 1 : -1;
|
|
3894
3920
|
for (const cut of cuts) {
|
|
3895
3921
|
let re = 0, im = 0, e = 0;
|
|
@@ -3929,7 +3955,8 @@ export class Solver {
|
|
|
3929
3955
|
// taken at its edges; and what's left, small, solved for to GMRES's floor, its error now
|
|
3930
3956
|
// relative to itself. An electrolyte at its open circuit needs it: the response to its
|
|
3931
3957
|
// terminal is nearly such a shift, and its current the slope of levels uniform to 1e-14,
|
|
3932
|
-
// more than a double can hold. (
|
|
3958
|
+
// more than a double can hold. (A port's exchange sees the shift: its window's rows are
|
|
3959
|
+
// kept with the edges'.) So does a MOS capacitor's back contact, its channel held by a port.
|
|
3933
3960
|
if (K === 0 && cuts && err > 1e-2 * Math.hypot(Ir, Ii)) {
|
|
3934
3961
|
const [er, ei] = this._regionShift(yr, yi);
|
|
3935
3962
|
const tr = new Float64Array(Nc), ti = new Float64Array(Nc);
|
|
@@ -4019,8 +4046,9 @@ export class Solver {
|
|
|
4019
4046
|
return [er, ei];
|
|
4020
4047
|
}
|
|
4021
4048
|
|
|
4022
|
-
// Compact rows a region's uniform shift can touch: faces, the nodes beside them,
|
|
4023
|
-
//
|
|
4049
|
+
// Compact rows a region's uniform shift can touch: faces, the nodes beside them, the two ends
|
|
4050
|
+
// and the ports' windows (whatever holds a level from outside). Elsewhere inside a region,
|
|
4051
|
+
// every row is unchanged by it.
|
|
4024
4052
|
_regionEdgeRows() {
|
|
4025
4053
|
const { M, loc, nB } = this, R = this.rix;
|
|
4026
4054
|
const edge = new Uint8Array(this.sys.size);
|
|
@@ -4036,16 +4064,17 @@ export class Solver {
|
|
|
4036
4064
|
mark(b);
|
|
4037
4065
|
mark(b + 1);
|
|
4038
4066
|
}
|
|
4067
|
+
for (const port of this.model.ports) for (const g of port.nodes) mark(this.blockOfNode[g]);
|
|
4039
4068
|
return edge;
|
|
4040
4069
|
}
|
|
4041
4070
|
|
|
4042
|
-
// The cuts where the impedance may read a small-signal current
|
|
4043
|
-
//
|
|
4044
|
-
//
|
|
4045
|
-
//
|
|
4071
|
+
// The cuts where the impedance may read a small-signal current. Each is the linearised total
|
|
4072
|
+
// current toward +x through one segment s of an ideal region, Σ (g + iω d)·δu over the
|
|
4073
|
+
// unknowns idx: the Scharfetter–Gummel fluxes' derivatives (as in _segmentsDilute) and the
|
|
4074
|
+
// displacement's. Without ports it's the same through every cut; with them, only between a
|
|
4075
|
+
// contact and the nearest port's window (see _impedance).
|
|
4046
4076
|
_currentCuts() {
|
|
4047
4077
|
const { model, n, M, u, uLo, c, z } = this, F = FARADAY;
|
|
4048
|
-
if (model.ports.length > 0) return null;
|
|
4049
4078
|
const { grid, materials, regions } = model;
|
|
4050
4079
|
const cuts = [];
|
|
4051
4080
|
for (let r = 0; r < regions.length; r++) {
|
|
@@ -4085,7 +4114,7 @@ export class Solver {
|
|
|
4085
4114
|
}
|
|
4086
4115
|
if (terms.size === 0) continue;
|
|
4087
4116
|
const idx = [...terms.keys()];
|
|
4088
|
-
cuts.push({ idx, g: idx.map((x) => terms.get(x)[0]), d: idx.map((x) => terms.get(x)[1]) });
|
|
4117
|
+
cuts.push({ s, idx, g: idx.map((x) => terms.get(x)[0]), d: idx.map((x) => terms.get(x)[1]) });
|
|
4089
4118
|
}
|
|
4090
4119
|
}
|
|
4091
4120
|
return cuts;
|
|
@@ -4264,7 +4293,7 @@ export class Solver {
|
|
|
4264
4293
|
// pseudo-transient ramp: one direct attempt first.
|
|
4265
4294
|
const quick = (canContinue || this.hasGeneration) && direct && opts.continuation !== false;
|
|
4266
4295
|
let r = this._solveSteady(quick ? { ...opts, maxSteps: 1 } : opts);
|
|
4267
|
-
if (r.converged) this.solvedV = target;
|
|
4296
|
+
if (r.converged) this.solvedV = [level, target];
|
|
4268
4297
|
if (r.converged) return r;
|
|
4269
4298
|
// Generation (e.g. light) holding the device far from equilibrium: ramp it up from nearly
|
|
4270
4299
|
// nothing, each solve warm from the last.
|
|
@@ -4278,7 +4307,7 @@ export class Solver {
|
|
|
4278
4307
|
const g = this._generationContinuation(opts, r);
|
|
4279
4308
|
dimmer = this.dimmer;
|
|
4280
4309
|
if (g.converged) {
|
|
4281
|
-
this.solvedV = target;
|
|
4310
|
+
this.solvedV = [level, target];
|
|
4282
4311
|
return g;
|
|
4283
4312
|
}
|
|
4284
4313
|
r = g;
|
|
@@ -4319,17 +4348,23 @@ export class Solver {
|
|
|
4319
4348
|
this.computeConcentrations();
|
|
4320
4349
|
};
|
|
4321
4350
|
restart();
|
|
4322
|
-
// Ramp from the last converged
|
|
4323
|
-
//
|
|
4324
|
-
|
|
4325
|
-
const
|
|
4351
|
+
// Ramp from the contacts' voltages at the last converged solve when the state is that
|
|
4352
|
+
// solution (a warm start): the left one first where it moved, the right held where it was,
|
|
4353
|
+
// then the right. Otherwise from level terminals.
|
|
4354
|
+
const solved = this.solvedV;
|
|
4355
|
+
let c;
|
|
4356
|
+
if (solved) {
|
|
4357
|
+
c = solved[0] === level ? { ...r, converged: true, ramped: false } : this._continuation(opts, 0, solved[0], level, r, true, solved[1]);
|
|
4358
|
+
// (Ramping the right from level terminals, solve there first: a sweep's start.)
|
|
4359
|
+
if (c.converged) c = this._continuation(opts, 1, solved[1], target, c, c.ramped || solved[1] !== level);
|
|
4360
|
+
} else c = this._continuation(opts, 1, level, target, r, false);
|
|
4326
4361
|
if (c.converged) {
|
|
4327
|
-
this.solvedV = target;
|
|
4362
|
+
this.solvedV = [level, target];
|
|
4328
4363
|
return c;
|
|
4329
4364
|
}
|
|
4330
4365
|
restart();
|
|
4331
4366
|
r = this._solveSteady(opts); // the full pseudo-transient ramp
|
|
4332
|
-
if (r.converged) this.solvedV = target;
|
|
4367
|
+
if (r.converged) this.solvedV = [level, target];
|
|
4333
4368
|
return { ...r, steps: r.steps + c.steps, iterations: r.iterations + c.iterations };
|
|
4334
4369
|
}
|
|
4335
4370
|
|
|
@@ -4410,13 +4445,15 @@ export class Solver {
|
|
|
4410
4445
|
// Floated, the system can lose what held it doesn't (a closed redox cell driven near its
|
|
4411
4446
|
// limit: its bordered row 33 digits short), but a current-driven steady state is the held
|
|
4412
4447
|
// one at the voltage that passes the target: found by held solves, by regula falsi
|
|
4413
|
-
// (Illinois) within the bracket, to 1e-10 of the target
|
|
4448
|
+
// (Illinois) within the bracket, to 1e-10 of the target, or at open circuit of the largest
|
|
4449
|
+
// current the march saw (a target of 0 is never met exactly).
|
|
4450
|
+
const scale = Math.max(Math.abs(target), ...seen.map(([, I]) => Math.abs(I)));
|
|
4414
4451
|
try {
|
|
4415
4452
|
let side = 0;
|
|
4416
4453
|
for (let it = 0; it < 60; it++) {
|
|
4417
4454
|
const Vm = Va - (fa * (Vb - Va)) / (fb - fa), fm = hold(Vm);
|
|
4418
4455
|
if (!Number.isFinite(fm)) return fail();
|
|
4419
|
-
if (Math.abs(fm) <= 1e-10 *
|
|
4456
|
+
if (Math.abs(fm) <= 1e-10 * scale) return { converged: true, steps, iterations, history, residual: Math.abs(fm) };
|
|
4420
4457
|
if (Math.sign(fm) === Math.sign(fb)) {
|
|
4421
4458
|
[Vb, fb] = [Vm, fm];
|
|
4422
4459
|
if (side === -1) fa /= 2;
|
|
@@ -4474,12 +4511,18 @@ export class Solver {
|
|
|
4474
4511
|
}
|
|
4475
4512
|
}
|
|
4476
4513
|
|
|
4477
|
-
|
|
4514
|
+
// Terminal k (a contact) ramped from `level` to `target` through steady solves, each warm from
|
|
4515
|
+
// the last (`hold`: the other contact's voltage meanwhile, if not its own). A warm ramp that has
|
|
4516
|
+
// nowhere to go converges only if what it started from did (`ramped`): the state it carries
|
|
4517
|
+
// must be some ramp's solution, never just the one a failed solve left.
|
|
4518
|
+
_continuation(opts, k, level, target, r, warm, hold) {
|
|
4478
4519
|
const sub = this._directSteady() ? { ...opts, maxSteps: 1 } : opts;
|
|
4479
4520
|
let V = level, dV = (target - level) / 8, steps = r.steps, iterations = r.iterations;
|
|
4480
4521
|
const history = r.history.slice();
|
|
4522
|
+
if (warm && V === target) return { ...r, converged: r.ramped === true, steps, iterations, history, ramped: r.ramped };
|
|
4481
4523
|
try {
|
|
4482
|
-
this.sourceOverride.set(
|
|
4524
|
+
this.sourceOverride.set(k, V);
|
|
4525
|
+
if (hold !== undefined) this.sourceOverride.set(1 - k, hold);
|
|
4483
4526
|
let q;
|
|
4484
4527
|
if (!warm) {
|
|
4485
4528
|
q = this._solveSteady(opts);
|
|
@@ -4501,7 +4544,7 @@ export class Solver {
|
|
|
4501
4544
|
while (V !== target) {
|
|
4502
4545
|
const next = Math.abs(target - V) <= Math.abs(dV) ? target : V + dV;
|
|
4503
4546
|
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
4504
|
-
this.sourceOverride.set(
|
|
4547
|
+
this.sourceOverride.set(k, next);
|
|
4505
4548
|
q = this._solveSteady(sub);
|
|
4506
4549
|
steps += q.steps;
|
|
4507
4550
|
iterations += q.iterations;
|
|
@@ -4518,9 +4561,10 @@ export class Solver {
|
|
|
4518
4561
|
if (Math.abs(dV) < 1e-6 * Math.abs(target - level)) return { converged: false, steps, iterations, history };
|
|
4519
4562
|
}
|
|
4520
4563
|
}
|
|
4521
|
-
return { converged: true, steps, iterations, history };
|
|
4564
|
+
return { converged: true, steps, iterations, history, ramped: true };
|
|
4522
4565
|
} finally {
|
|
4523
|
-
this.sourceOverride.delete(
|
|
4566
|
+
this.sourceOverride.delete(k);
|
|
4567
|
+
if (hold !== undefined) this.sourceOverride.delete(1 - k);
|
|
4524
4568
|
this._refreshSources();
|
|
4525
4569
|
}
|
|
4526
4570
|
}
|
package/src/stack.js
CHANGED
|
@@ -87,16 +87,18 @@ export function ohmic(d = 0, carriers = ['e-', 'h+']) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* A bath: a neutral composition `c` (mol/m³) held at the contact
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* A bath: a neutral composition `c` (mol/m³) held at the contact. With a `reference`, the charged
|
|
91
|
+
* species a reference electrode would sense (Cl⁻ for Ag/AgCl), its levels are referred to that
|
|
92
|
+
* species, which is the terminal. Without one, `bath(c, drive)`, the terminal voltage is the
|
|
93
|
+
* bath's φ: an ideal salt bridge, the convention of membrane potentials.
|
|
93
94
|
* @param {Record<string, number>} c
|
|
94
|
-
* @param {string} reference
|
|
95
|
+
* @param {string | number | object} [reference] the reference species, or the drive
|
|
95
96
|
* @param {number | object} [drive]
|
|
96
97
|
* @param {{ offset?: number }} [opts] where the reference species sits relative to the terminal
|
|
97
98
|
* voltage, V_ref = V + offset (V)
|
|
98
99
|
*/
|
|
99
100
|
export function bath(c, reference, d = 0, { offset } = {}) {
|
|
101
|
+
if (typeof reference !== 'string') return { ...drive(reference ?? 0, 'bath'), bath: { c: { ...c } } };
|
|
100
102
|
return { ...drive(d, 'bath'), bath: { c: { ...c }, reference, ...(offset === undefined ? {} : { offset }) } };
|
|
101
103
|
}
|
|
102
104
|
|