driftlet 0.16.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/docs/device.md +29 -11
- package/docs/kit.md +10 -1
- package/docs/numerics.md +68 -12
- package/docs/statistics.md +2 -1
- package/docs/templates.md +260 -0
- package/llms.txt +107 -299
- package/package.json +15 -3
- package/src/blockTridiagonal.js +73 -1
- package/src/channels.js +6 -4
- package/src/checks.js +78 -3
- package/src/data.js +5 -1
- package/src/device.js +1 -1
- package/src/devices.js +2 -0
- package/src/equation.js +2 -1
- package/src/errors.js +33 -0
- package/src/grid.js +12 -0
- package/src/index.js +21 -10
- package/src/junction.js +3 -1
- package/src/kit.js +1 -1
- package/src/live.js +2 -0
- package/src/particles.js +2 -0
- package/src/plot.js +4 -0
- package/src/polarization.js +2 -1
- package/src/recorder.js +2 -1
- package/src/solution.js +39 -5
- package/src/solver.js +248 -30
- package/src/sources.js +8 -1
- package/src/stack.js +14 -1
- package/src/statistics.js +1 -1
- package/src/traces.js +3 -0
- package/src/types.js +3 -1
- package/types/blockTridiagonal.d.ts +1 -0
- package/types/channels.d.ts +3 -3
- package/types/checks.d.ts +9 -3
- package/types/data.d.ts +3 -3
- package/types/devices.d.ts +1 -1
- package/types/equation.d.ts +1 -1
- package/types/errors.d.ts +8 -0
- package/types/index.d.ts +4 -7
- package/types/junction.d.ts +2 -2
- package/types/kit.d.ts +1 -1
- package/types/live.d.ts +1 -1
- package/types/particles.d.ts +1 -1
- package/types/plot.d.ts +3 -3
- package/types/polarization.d.ts +1 -1
- package/types/recorder.d.ts +1 -1
- package/types/solver.d.ts +8 -1
- package/types/sources.d.ts +7 -7
- package/types/traces.d.ts +1 -1
- package/types/types.d.ts +9 -1
package/src/solution.js
CHANGED
|
@@ -87,7 +87,7 @@ export function makeSolution(solver, result = {}) {
|
|
|
87
87
|
}
|
|
88
88
|
solver._assembleBookkeeping(solver.lastDt);
|
|
89
89
|
sol.contacts = {};
|
|
90
|
-
sol.
|
|
90
|
+
sol.plates = {};
|
|
91
91
|
for (const side of ['left', 'right']) {
|
|
92
92
|
const ct = contacts[side];
|
|
93
93
|
const flux = {};
|
|
@@ -107,7 +107,7 @@ export function makeSolution(solver, result = {}) {
|
|
|
107
107
|
if (ct.phi.type === 'capacitive' || ct.phi.type === 'pinned') {
|
|
108
108
|
// Charge on the gate (or metal) plate, per area if the device is planar: +D at the left, −D
|
|
109
109
|
// at the right.
|
|
110
|
-
sol.
|
|
110
|
+
sol.plates[side] = { V, D, charge: side === 'left' ? D : -D };
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
sol.current = sol.contacts.right.current;
|
|
@@ -152,6 +152,16 @@ export function makeSolution(solver, result = {}) {
|
|
|
152
152
|
}
|
|
153
153
|
return out;
|
|
154
154
|
});
|
|
155
|
+
// A floating terminal passes what its circuit says, exactly (see passedCurrent).
|
|
156
|
+
if (sol.converged) {
|
|
157
|
+
for (const k of solver.floating) {
|
|
158
|
+
const I = solver.passedCurrent(k);
|
|
159
|
+
if (k === 0) sol.contacts.left.current = I;
|
|
160
|
+
else if (k === 1) sol.contacts.right.current = -I;
|
|
161
|
+
else sol.ports[k - 2].current = I;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
sol.current = sol.contacts.right.current;
|
|
155
165
|
sol.terminalVoltage = sol.contacts.right.V - sol.contacts.left.V;
|
|
156
166
|
// Every terminal (the contacts, then the ports by name): its voltage and its current into the
|
|
157
167
|
// device, conduction plus displacement. They sum to zero in steady state.
|
|
@@ -303,11 +313,24 @@ export function makeSolution(solver, result = {}) {
|
|
|
303
313
|
// refined. Flag it where it lowers that capacitance by over 2%. (Where the box is the smaller
|
|
304
314
|
// part, as an ε = 0 metal's against a semiconductor's depletion, it's a fine stand-in for the
|
|
305
315
|
// metal's own screening.)
|
|
306
|
-
|
|
316
|
+
// And a reaction at the face sees that box's composition, which holding the charge σ shifts
|
|
317
|
+
// by about z·σ/(Cbox·V_T) for an ion of charge z: a Frumkin effect set by the mesh, which grows
|
|
318
|
+
// without limit as the box shrinks. Flag it past 1%.
|
|
319
|
+
const boxed = (g, h, phi, other, where, sigma = 0, reacting = []) => {
|
|
307
320
|
const mat = model.materials[model.regions[grid.nodeRegion[g]].material];
|
|
308
321
|
if (mat.conductor || mat.epsr !== 0 || solver.phiUndefined?.[g]) return;
|
|
309
322
|
const s2 = solver.screening(g), Cbox = (F * F * s2 * h) / (2 * RT);
|
|
310
323
|
if (!(s2 > 0)) return;
|
|
324
|
+
const ion = reacting.filter((i) => species[i].z !== 0).reduce((a, i) => (a < 0 || Math.abs(species[i].z) > Math.abs(species[a].z) ? i : a), -1);
|
|
325
|
+
const shift = ion < 0 ? 0 : (Math.abs(species[ion].z * sigma) / Cbox) * (F / RT);
|
|
326
|
+
if (shift > 0.01) {
|
|
327
|
+
sol.warnings.push(
|
|
328
|
+
`${where}: the reaction here sees the strictly neutral cell beside the face (${h.toExponential(2)} m), which holds the face's charge as a diffuse layer that wide: ` +
|
|
329
|
+
`${species[ion].name} there is shifted by about ${(100 * shift).toPrecision(2)}%, a Frumkin effect set by the mesh that grows as the cell shrinks. ` +
|
|
330
|
+
`Set the face's zeroCharge near its rest potential (so it holds little charge), coarsen that cell to ${((200 * Math.abs(species[ion].z * sigma)) / (F * s2)).toExponential(1)} m or more, ` +
|
|
331
|
+
'or resolve the double layer (epsr > 0, hmin below the Debye length).',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
311
334
|
let C = phi.C;
|
|
312
335
|
if (phi.type === 'pinned') {
|
|
313
336
|
const om = other < 0 ? null : model.materials[model.regions[grid.nodeRegion[other]].material];
|
|
@@ -328,8 +351,10 @@ export function makeSolution(solver, result = {}) {
|
|
|
328
351
|
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1];
|
|
329
352
|
check(gL, grid.segLength[gL - 1], `interfaces[${f}] (left side)`);
|
|
330
353
|
check(gR, grid.segLength[gR], `interfaces[${f}] (right side)`);
|
|
331
|
-
|
|
332
|
-
|
|
354
|
+
const reacting = (side) => itf.reactions.flatMap((rx) => rx.part.filter((p) => p.side === side).map((p) => p.i));
|
|
355
|
+
const sigma = sol.interfaces[f].D;
|
|
356
|
+
boxed(gL, grid.segLength[gL - 1], itf.phi, gR, `interfaces[${f}] (left side)`, sigma, reacting(0));
|
|
357
|
+
boxed(gR, grid.segLength[gR], itf.phi, gL, `interfaces[${f}] (right side)`, sigma, reacting(1));
|
|
333
358
|
});
|
|
334
359
|
const resolves = (ct) => ct.phi.type === 'capacitive' || ct.phi.type === 'pinned';
|
|
335
360
|
if (resolves(contacts.left)) {
|
|
@@ -361,6 +386,15 @@ export function makeSolution(solver, result = {}) {
|
|
|
361
386
|
}
|
|
362
387
|
});
|
|
363
388
|
});
|
|
389
|
+
// A reaction a steady state needs at equilibrium, left out of it (see stuckReactions).
|
|
390
|
+
if (sol.steady) {
|
|
391
|
+
for (const { where, affinity } of solver.stuckReactions()) {
|
|
392
|
+
sol.warnings.push(
|
|
393
|
+
`${where}: out of equilibrium by ${(affinity * solver.VT * 1e3).toPrecision(3)} mV of affinity wherever it acts, yet nothing else makes or uses what it changes, so a steady state needs it at equilibrium. ` +
|
|
394
|
+
'Its rate is too small for the solve to resolve, and it was left where the solve began: this is not a steady state. Raise its rate constant (the steady state does not depend on it), or advance() in time to see how slowly it gets there.',
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
364
398
|
// Any other species denser than any material can be (1e6 mol/m³: water is 5.5e4, a metal's
|
|
365
399
|
// atoms ~1.5e5): a level set where the device can't follow it, such as a contact's for one ion
|
|
366
400
|
// far from its neighbours' (Na⁺ held at 1.2e37 mol/m³, whose currents then read 1e28 A/m²).
|
package/src/solver.js
CHANGED
|
@@ -491,7 +491,7 @@ export class Solver {
|
|
|
491
491
|
this.steady = false; // inside a steady solve: sources at the present time, not a step's end
|
|
492
492
|
// The floating ones are extra unknowns, solved with the grid's by bordering (_solveBordered).
|
|
493
493
|
this.floating = this.terms.flatMap((t, k) => (t.drive.kind === 'I' || t.drive.R > 0 ? [k] : []));
|
|
494
|
-
this.drivenKey = this.
|
|
494
|
+
this.drivenKey = this._drivenKey();
|
|
495
495
|
this.lawRefs = new Map(); // what a steady solve keeps behind a terminal at no current (_captureLaws)
|
|
496
496
|
this.chargeRefs = new Map(); // …and the charge a terminal at no current that only charges keeps
|
|
497
497
|
// Per terminal: B = ∂res/∂V (compact column), C = ∂I/∂x (compact row), ∂I/∂V, and for a
|
|
@@ -882,6 +882,73 @@ export class Solver {
|
|
|
882
882
|
// first, so that the first takes that row, and the rest have its multiple taken off.
|
|
883
883
|
const through = (w) => this.carriers.some((k, j) => w[X + j] !== 0);
|
|
884
884
|
const held = new Map(); // (terminal → the combination in place of its circuit row)
|
|
885
|
+
// Reactions' equilibria first. A face reaction that alone changes some combination (without
|
|
886
|
+
// it, w would be conserved: Ag⁺ closed in a cell where Ag⁺ + e⁻ = Ag(s) is its one source, or
|
|
887
|
+
// Ag⁺ + AgNO₃ where it also complexes) has that combination's summed balance rows equal to its
|
|
888
|
+
// rate, w·ν times it, which a steady state sets to zero. The rate is a poor row: k₀ times a
|
|
889
|
+
// function of the levels, beside diffusion's terms in the same balances. An Ag⁺/Ag couple
|
|
890
|
+
// 1e-20 as fast as Fe³⁺/Fe²⁺ on the same platinum was lost to their round-off, the
|
|
891
|
+
// open-circuit potential settling where silver wasn't (−0.4661 V, the joint equilibrium
|
|
892
|
+
// −0.5004 V), and 1e-15 to 1e-18 failed. Its affinity says the same at any k₀ (each kinetic
|
|
893
|
+
// form vanishes only there), so it stands in for one of those rows (see _applyAffinities),
|
|
894
|
+
// chosen by the same elimination, and a stretch's: never a circuit row.
|
|
895
|
+
// The slowest first, whose rate rows are the poorest, and each row picked by the reaction's
|
|
896
|
+
// own terms, never by where its species are listed: the same rows in any listing. (Picked by
|
|
897
|
+
// listing, an electrode's two couples took different rows with the species reversed, and
|
|
898
|
+
// Newton converged in one listing and crawled in the other.)
|
|
899
|
+
this.affinities = [];
|
|
900
|
+
const faceRx = model.interfaces.flatMap((itf, f) => itf.reactions.map((rx) => ({ f, rx, key: rx.part.map((p) => `${model.species[p.i].name}:${p.side}:${p.nu}`).sort().join() })));
|
|
901
|
+
faceRx.sort((a, b) => a.rx.k0 - b.rx.k0 || (a.key < b.key ? -1 : a.key > b.key ? 1 : a.f - b.f));
|
|
902
|
+
for (const { f, rx } of faceRx) {
|
|
903
|
+
const nu = new Float64Array(S);
|
|
904
|
+
let inFlat = false;
|
|
905
|
+
for (const p of rx.part) {
|
|
906
|
+
const k = this.stretchOf[(f + p.side) * this.n + p.i];
|
|
907
|
+
if (k < 0 || this.flatStretches.includes(this.stretches[k])) inFlat = true;
|
|
908
|
+
else nu[k] += p.nu;
|
|
909
|
+
}
|
|
910
|
+
if (inFlat) continue; // (held flat at a contact's levels, in equilibrium already)
|
|
911
|
+
// (Not one fed by a terminal driven by a current: its sum is the rate plus what that
|
|
912
|
+
// passes. Silver's, e⁻ + Fe²⁺ with the electrons fed, or Ag⁺ + AgNO₃, which nothing is.
|
|
913
|
+
// A current held at zero passes nothing: an electrode at open circuit feeding a closed
|
|
914
|
+
// pool of Ag⁺ beside a slow Fe³⁺/Fe²⁺ couple settled 78 mV off, its iron not stationary.)
|
|
915
|
+
const passes = (k) => !this._noCurrent(this.terms[k]);
|
|
916
|
+
const fed = (w) => w.some((x, k) => x !== 0 && ((k >= X && passes(this.carriers[k - X])) || (k < S && driven(this.stretches[k]).some(passes))));
|
|
917
|
+
// Any of the combinations it alone changes will do, with any conserved amount added (whose
|
|
918
|
+
// summed balance is zero too): one of them needs a row of its own left to take. (Silver's,
|
|
919
|
+
// e⁻ − Fe³⁺ beside a closed iron couple that had Fe³⁺'s, is e⁻ + Fe²⁺ with the iron added.)
|
|
920
|
+
const news = this._conservedMoieties(true, [], rx).filter((w) => Math.abs(nu.reduce((t, x, k) => t + x * w[k], 0)) > 1e-9 && !fed(w));
|
|
921
|
+
const kept = laws.filter((l) => !fed(l));
|
|
922
|
+
const tries = [...news, ...news.flatMap((w) => kept.flatMap((l) => [1, -1].map((sg) => w.map((x, m) => x + sg * l[m]))))];
|
|
923
|
+
for (const w of tries) {
|
|
924
|
+
const v = new Float64Array(S + T);
|
|
925
|
+
for (let k = 0; k < S; k++) {
|
|
926
|
+
if (w[k] === 0) continue;
|
|
927
|
+
const st = this.stretches[k];
|
|
928
|
+
if (this._moves(st) && !spectators.has(rowOf(k))) v[k] = w[k];
|
|
929
|
+
if (this.z[st.species] !== 0) for (const t of driven(st)) v[S + t] = w[k] / this.z[st.species];
|
|
930
|
+
}
|
|
931
|
+
for (const [col, row] of chosen) {
|
|
932
|
+
const fc = v[col];
|
|
933
|
+
if (fc !== 0) for (let j = 0; j < S + T; j++) v[j] -= fc * row[j];
|
|
934
|
+
}
|
|
935
|
+
const scale = v.reduce((m, x) => Math.max(m, Math.abs(x)), 0);
|
|
936
|
+
if (!(scale > 1e-9)) continue;
|
|
937
|
+
// (Never a metal's carriers, nor a stretch a terminal feeds, even at no current: their rows
|
|
938
|
+
// carry the terminal's current, and Pt's electrons standing for an iron couple's balance
|
|
939
|
+
// put the electrode at 602 V.)
|
|
940
|
+
const ok = (j) => j !== undefined && j >= 0 && j < S && Math.abs(v[j]) > 1e-9 * scale && !conductor(this.stretches[j]) && this.terms.every((_, t) => !this._feeds(t, this.stretches[j]));
|
|
941
|
+
// (One of its own species first, then weight 1, the largest, and by name.)
|
|
942
|
+
const own = (k) => rx.part.some((p) => p.i === this.stretches[k].species && this.stretchOf[(f + p.side) * this.n + p.i] === k);
|
|
943
|
+
const key = (k) => [own(k) ? 0 : 1, Math.abs(v[k]) === 1 ? 0 : 1, -Math.abs(v[k]), model.species[this.stretches[k].species].name, this.stretches[k].regions[0]];
|
|
944
|
+
const less = (a, b) => { const ka = key(a), kb = key(b); for (let j = 0; j < ka.length; j++) if (ka[j] !== kb[j]) return ka[j] < kb[j]; return false; };
|
|
945
|
+
const col = [...Array(S).keys()].filter(ok).reduce((b, k) => (b < 0 || less(k, b) ? k : b), -1);
|
|
946
|
+
if (col < 0) continue;
|
|
947
|
+
chosen.push([col, v.map((x) => x / v[col])]);
|
|
948
|
+
this.affinities.push({ rx, f, row: rowOf(col), idx: new Int32Array(rx.part.length), w: new Float64Array(rx.part.length), len: 0, res: 0 });
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
885
952
|
for (let w of [...laws.filter(through), ...laws.filter((w) => !through(w))]) {
|
|
886
953
|
for (const [j, k] of this.carriers.entries()) {
|
|
887
954
|
const c = held.get(k);
|
|
@@ -947,7 +1014,7 @@ export class Solver {
|
|
|
947
1014
|
this.pinnedIslands = [];
|
|
948
1015
|
this.islandsOn = false; // (newton() turns them on, see there)
|
|
949
1016
|
this.pins = [];
|
|
950
|
-
const used = new Set(this.constraints.map((cs) => cs.row).filter((o) => o >= 0));
|
|
1017
|
+
const used = new Set([...this.constraints.map((cs) => cs.row).filter((o) => o >= 0), ...this.affinities.map((af) => af.row)]);
|
|
951
1018
|
for (const st of this.stretches) {
|
|
952
1019
|
const i = st.species, [s0, s1] = st.regions;
|
|
953
1020
|
if (!st.mobile || conductor(st)) continue;
|
|
@@ -1160,7 +1227,7 @@ export class Solver {
|
|
|
1160
1227
|
// charges, so a combination weighted by z there is conserved while it passes none: a battery's
|
|
1161
1228
|
// cathode, its electrons behind an open circuit, keeps its charge. Exact integer data, so plain
|
|
1162
1229
|
// elimination will do.
|
|
1163
|
-
_conservedMoieties(drives = false, alsoCurrent = []) {
|
|
1230
|
+
_conservedMoieties(drives = false, alsoCurrent = [], skip = null) {
|
|
1164
1231
|
const { model, n, z } = this, S = this.stretches.length;
|
|
1165
1232
|
// With `drives`, an electrode port driven by a current whose reactions take its carrier from
|
|
1166
1233
|
// the electrode has a column of its own, last: what the electrode holds, its charge Q/(zF),
|
|
@@ -1169,12 +1236,14 @@ export class Solver {
|
|
|
1169
1236
|
const byCurrent = (t, k) => t.drive.kind === 'I' || alsoCurrent.includes(k); // (with `alsoCurrent`, terminals counted as such)
|
|
1170
1237
|
this.carriers = drives ? this.terms.flatMap((t, k) => (byCurrent(t, k) && k >= 2 && model.ports[k - 2].reactions.length > 0 ? [k] : [])) : [];
|
|
1171
1238
|
const X = S + this.surfCols.length, C = X + this.carriers.length;
|
|
1172
|
-
|
|
1239
|
+
// (With `skip`, a reaction left out: the combinations that it alone changes come out too, and
|
|
1240
|
+
// its own rows, over the columns, are handed back as `basis.skipped`.)
|
|
1241
|
+
const rows = [], skipped = [];
|
|
1173
1242
|
model.interfaces.forEach((itf, f) => {
|
|
1174
1243
|
for (const rx of itf.reactions) {
|
|
1175
1244
|
const row = new Float64Array(C);
|
|
1176
1245
|
for (const p of rx.part) row[this.stretchOf[(f + p.side) * n + p.i]] += p.nu;
|
|
1177
|
-
rows.push(row);
|
|
1246
|
+
(rx === skip ? skipped : rows).push(row);
|
|
1178
1247
|
}
|
|
1179
1248
|
});
|
|
1180
1249
|
// An electrode surface's species are columns too, after the stretches (S + j for surface
|
|
@@ -1183,12 +1252,13 @@ export class Solver {
|
|
|
1183
1252
|
model.ports.forEach((port, k) => {
|
|
1184
1253
|
for (const rx of port.reactions) {
|
|
1185
1254
|
const row = new Float64Array(C); // (the electrode's carrier is outside the device)
|
|
1255
|
+
if (rx === skip) skipped.push(row);
|
|
1186
1256
|
for (const p of rx.part) {
|
|
1187
1257
|
if (p.side === 0) row[this.stretchOf[port.region * n + p.i]] += p.nu;
|
|
1188
1258
|
else if (p.side === 2) row[S + this.surfColOf[k] + p.s] += p.nu;
|
|
1189
1259
|
else if (this.carriers.includes(k + 2)) row[X + this.carriers.indexOf(k + 2)] += p.nu;
|
|
1190
1260
|
}
|
|
1191
|
-
rows.push(row);
|
|
1261
|
+
if (rx !== skip) rows.push(row);
|
|
1192
1262
|
}
|
|
1193
1263
|
});
|
|
1194
1264
|
model.regions.forEach((reg, q) => {
|
|
@@ -1197,7 +1267,7 @@ export class Solver {
|
|
|
1197
1267
|
const row = new Float64Array(C);
|
|
1198
1268
|
for (const p of rx.reactants) row[this.stretchOf[q * n + p.i]] -= p.nu;
|
|
1199
1269
|
for (const p of rx.products) row[this.stretchOf[q * n + p.i]] += p.nu;
|
|
1200
|
-
rows.push(row);
|
|
1270
|
+
(rx === skip ? skipped : rows).push(row);
|
|
1201
1271
|
}
|
|
1202
1272
|
});
|
|
1203
1273
|
const unit = (k) => {
|
|
@@ -1254,6 +1324,7 @@ export class Solver {
|
|
|
1254
1324
|
w.own = free; // (its weight is 1 there, and no other vector's is: the row it replaces)
|
|
1255
1325
|
basis.push(w);
|
|
1256
1326
|
}
|
|
1327
|
+
basis.skipped = skipped;
|
|
1257
1328
|
return basis;
|
|
1258
1329
|
}
|
|
1259
1330
|
|
|
@@ -1508,12 +1579,67 @@ export class Solver {
|
|
|
1508
1579
|
if (this.floating.length !== before.length) this.deltaV = null;
|
|
1509
1580
|
// A terminal newly driven by a current (or no longer) can close a combination behind it.
|
|
1510
1581
|
// (and one put behind a resistance, or taken from it, its time steps' reading: see _laws)
|
|
1511
|
-
this.drivenKey = this.
|
|
1582
|
+
this.drivenKey = this._drivenKey();
|
|
1512
1583
|
if (this.drivenKey !== driven) this._laws();
|
|
1513
1584
|
this._refreshSources();
|
|
1514
1585
|
this.history = [];
|
|
1515
1586
|
}
|
|
1516
1587
|
|
|
1588
|
+
// After a steady solve: each reaction that alone changes some combination nothing else feeds
|
|
1589
|
+
// (see _laws), whose total rate a steady state therefore sets to zero, but whose affinity has one
|
|
1590
|
+
// sign wherever it acts, so that its total can't be zero. Too slow to show, its rate lost to
|
|
1591
|
+
// round-off, it was left where the solve began: a slow complexation 43 mV off, an electrode
|
|
1592
|
+
// port's slow couple at its starting composition, each converged without a word. (A face
|
|
1593
|
+
// reaction stands on its affinity, so it's only caught here if that couldn't be arranged.)
|
|
1594
|
+
stuckReactions() {
|
|
1595
|
+
const { n, M, u, uLo, model } = this, grid = model.grid, S = this.stretches.length, X = S + this.surfCols.length, out = [];
|
|
1596
|
+
const passes = (k) => !this._noCurrent(this.terms[k]);
|
|
1597
|
+
const fed = (w) => w.some((x, k) => x !== 0 && ((k >= X && passes(this.carriers[k - X])) || (k < S && this.terms.some((t, j) => t.drive.kind === 'I' && passes(j) && this._feeds(j, this.stretches[k])))));
|
|
1598
|
+
const needed = (rx) => {
|
|
1599
|
+
const basis = this._conservedMoieties(true, [], rx);
|
|
1600
|
+
return basis.some((w) => !fed(w) && basis.skipped.some((row) => Math.abs(row.reduce((t, x, k) => t + x * w[k], 0)) > 1e-9));
|
|
1601
|
+
};
|
|
1602
|
+
const eta = (g, slot) => u[this.blockOfNode[g] * M + slot] + uLo[this.blockOfNode[g] * M + slot];
|
|
1603
|
+
const report = (where, as) => {
|
|
1604
|
+
if (as.length === 0 || !(as.every((a) => a > 1e-6) || as.every((a) => a < -1e-6))) return;
|
|
1605
|
+
out.push({ where, affinity: as.reduce((m, a) => (Math.abs(a) < Math.abs(m) ? a : m)) });
|
|
1606
|
+
};
|
|
1607
|
+
model.interfaces.forEach((itf, f) => {
|
|
1608
|
+
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1];
|
|
1609
|
+
itf.reactions.forEach((rx, q) => {
|
|
1610
|
+
if (this.affinities.some((af) => af.rx === rx) || !needed(rx)) return;
|
|
1611
|
+
report(`interfaces[${f}].reactions[${q}]`, [rx.part.reduce((a, p) => a - p.nu * eta(p.side ? gR : gL, 1 + p.i), rx.fixedA)]);
|
|
1612
|
+
});
|
|
1613
|
+
});
|
|
1614
|
+
model.reactions.forEach((mrx, x) => {
|
|
1615
|
+
const rx = this.rxs[x];
|
|
1616
|
+
if (rx.generation || !needed(mrx)) return;
|
|
1617
|
+
const as = [];
|
|
1618
|
+
for (let g = 0; g < this.nNodes; g++) if (rx.kfNode[g] > 0 || rx.srh?.[this.nodeMaterial[g]]) as.push(rx.sp.reduce((a, i, p) => a + rx.nu[p] * eta(g, 1 + i), rx.fixedA));
|
|
1619
|
+
report(`bulkReactions[${x}]`, as);
|
|
1620
|
+
});
|
|
1621
|
+
model.ports.forEach((port, k) => {
|
|
1622
|
+
port.reactions.forEach((rx, q) => {
|
|
1623
|
+
if (!needed(rx)) return;
|
|
1624
|
+
let aV = rx.fixedA;
|
|
1625
|
+
for (const p of rx.part) if (p.side === 1) aV -= p.nu * this.portEta(port, p.i);
|
|
1626
|
+
report(`ports[${k}].reactions[${q}]`, port.nodes.map((g) => rx.part.reduce((a, p) => (p.side === 1 ? a : a - p.nu * eta(g, p.side === 2 ? 1 + n + p.s : 1 + p.i)), aV)));
|
|
1627
|
+
});
|
|
1628
|
+
});
|
|
1629
|
+
return out;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// How each terminal is driven, as far as the laws care (see _laws): by a voltage or a current,
|
|
1633
|
+
// behind a resistance or not, and a current that's held at zero throughout (which feeds a
|
|
1634
|
+
// combination nothing, so a reaction's affinity may stand in for its summed balance).
|
|
1635
|
+
_drivenKey() {
|
|
1636
|
+
return this.terms.map((t) => `${t.drive.kind}${t.drive.R > 0 ? 'R' : ''}${this._noCurrent(t) ? '0' : ''}`).join();
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
_noCurrent(t) {
|
|
1640
|
+
return t.drive.kind === 'I' && !(t.drive.R > 0) && t.drive.src.value === 0;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1517
1643
|
// Held terminals take their sources' values at sourceTime (a floating one keeps its voltage,
|
|
1518
1644
|
// an unknown; one behind a resistance reads its source voltage in _circuit).
|
|
1519
1645
|
_refreshSources() {
|
|
@@ -1779,6 +1905,22 @@ export class Solver {
|
|
|
1779
1905
|
});
|
|
1780
1906
|
if (w > 0) this.termV[k] = port.capacitance.zeroCharge + sum / w;
|
|
1781
1907
|
}
|
|
1908
|
+
// A floating port linked to its carrier (no reactions, no double layer: a wire into an
|
|
1909
|
+
// insertion host) starts at that carrier's level across its window, not at 0 V.
|
|
1910
|
+
for (const k of this.floating) {
|
|
1911
|
+
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1912
|
+
const i = port?.terminal, link = i === null || i === undefined ? null : port.species[i];
|
|
1913
|
+
if (!port || port.reactions.length > 0 || port.capacitance || !link || link.type === 'blocked' || z[i] === 0) continue;
|
|
1914
|
+
let w = 0, sum = 0;
|
|
1915
|
+
port.nodes.forEach((g, j) => {
|
|
1916
|
+
const a = this.model.grid.vol[g] * (this.portArea[t.index]?.[j] ?? 1); // (a window with no area: by volume)
|
|
1917
|
+
w += a;
|
|
1918
|
+
sum += (a * this.VT * u[this.blockOfNode[g] * M + 1 + i]) / z[i];
|
|
1919
|
+
});
|
|
1920
|
+
// (Levels at the start mean something only against a held terminal's, as for a contact.)
|
|
1921
|
+
const held = [0, 1].find((h) => !this.floating.includes(h) && Number.isFinite(level(this.terms[h].side)));
|
|
1922
|
+
if (w > 0 && held !== undefined) this.termV[k] = this.termV[held] + sum / w - (link.offset ?? 0) - level(this.terms[held].side);
|
|
1923
|
+
}
|
|
1782
1924
|
// Gates start open as far as the starting voltage across their face holds them, α/(α + β).
|
|
1783
1925
|
for (const gt of this.gateList) {
|
|
1784
1926
|
const { gate, o } = gt, V = this._gateVoltage(gt), a = gateRate(gate.alpha, V)[0], b = gateRate(gate.beta, V)[0];
|
|
@@ -2031,10 +2173,11 @@ export class Solver {
|
|
|
2031
2173
|
// balances, the last two kept aside for _solveBordered.
|
|
2032
2174
|
const steady = this.steady && dt === Infinity;
|
|
2033
2175
|
if (steady) this._flatRows();
|
|
2176
|
+
if (steady && this.affinities.length > 0) this._applyAffinities();
|
|
2034
2177
|
if (steady && this.constraints.length > 0) this._applyConstraints();
|
|
2035
2178
|
if (steady && this.localConstraints.length > 0) this._applyLocalConstraints();
|
|
2036
2179
|
this.pinnedIslands = steady && this.islandsOn ? this._applyIslands() : [];
|
|
2037
|
-
this.pins = steady ? [...this.constraints.filter((cs) => cs.circuit < 0), ...this.pinnedIslands] : [];
|
|
2180
|
+
this.pins = steady ? [...this.constraints.filter((cs) => cs.circuit < 0), ...this.affinities, ...this.pinnedIslands] : [];
|
|
2038
2181
|
this.transformed = this.combining && dt !== Infinity;
|
|
2039
2182
|
if (this.transformed) this._chargeRows(dt);
|
|
2040
2183
|
}
|
|
@@ -3274,6 +3417,31 @@ export class Solver {
|
|
|
3274
3417
|
}
|
|
3275
3418
|
}
|
|
3276
3419
|
|
|
3420
|
+
// Steady solves: each reaction that alone changes a combination (see _laws) held at its
|
|
3421
|
+
// equilibrium, a = fixedA − Σ ν η = 0, in place of one of that combination's balance rows (a pin,
|
|
3422
|
+
// like an amount's).
|
|
3423
|
+
_applyAffinities() {
|
|
3424
|
+
const { M, u, uLo, res } = this, grid = this.model.grid;
|
|
3425
|
+
for (const af of this.affinities) {
|
|
3426
|
+
const b0 = Math.floor(af.row / M), r0 = af.row % M;
|
|
3427
|
+
this._replaceRow(b0, r0);
|
|
3428
|
+
this._j(b0, r0, b0, r0, 1);
|
|
3429
|
+
res[this.rix[af.row]] = 0;
|
|
3430
|
+
const gL = grid.regionEnd[af.f], gR = grid.regionStart[af.f + 1];
|
|
3431
|
+
let hi = af.rx.fixedA, lo = 0, len = 0;
|
|
3432
|
+
for (const p of af.rx.part) {
|
|
3433
|
+
const o = this.blockOfNode[p.side ? gR : gL] * M + 1 + p.i;
|
|
3434
|
+
hi -= p.nu * u[o];
|
|
3435
|
+
lo -= p.nu * uLo[o];
|
|
3436
|
+
if (this.loc[o] < 0) continue;
|
|
3437
|
+
af.idx[len] = this.rix[o];
|
|
3438
|
+
af.w[len++] = -p.nu;
|
|
3439
|
+
}
|
|
3440
|
+
af.len = len;
|
|
3441
|
+
af.res = hi + lo;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3277
3445
|
// Zero one row of the Jacobian (all three blocks), ready to be replaced.
|
|
3278
3446
|
_replaceRow(b, rs) {
|
|
3279
3447
|
const l = this.loc[b * this.M + rs];
|
|
@@ -3411,8 +3579,13 @@ export class Solver {
|
|
|
3411
3579
|
// stops refining.
|
|
3412
3580
|
const x0 = Float64Array.from(x);
|
|
3413
3581
|
const ok = gmresReal(op, pb, x, { m: 8, restarts: 1, tol: 1e-8 });
|
|
3414
|
-
|
|
3415
|
-
|
|
3582
|
+
// (Measured on the potentials, as Newton's own step is: a rate the plain solve left
|
|
3583
|
+
// unsolved, at a perturbed pivot, can need a correction of 41.7 mol/(m²·s).)
|
|
3584
|
+
const dx = new Float64Array(N + 1), dV = new Float64Array(K);
|
|
3585
|
+
for (let j = 0; j < N; j++) dx[j] = x[j] - x0[j];
|
|
3586
|
+
for (let a = 0; a < K; a++) dV[a] = x[N + a] - x0[N + a];
|
|
3587
|
+
if (this.transformed) this._untransform(dx);
|
|
3588
|
+
const c = this._maxPotentialStep(dx, dV);
|
|
3416
3589
|
if (!ok || !(c < 1)) return false;
|
|
3417
3590
|
for (let j = 0; j < N; j++) delta[j] = x[j];
|
|
3418
3591
|
for (let a = 0; a < K; a++) deltaV[a] = x[N + a];
|
|
@@ -3464,7 +3637,19 @@ export class Solver {
|
|
|
3464
3637
|
// A/m² of noise), their rows can throw Newton off instead: if it doesn't converge again
|
|
3465
3638
|
// within a dozen iterations, the plain solution stands.
|
|
3466
3639
|
this.islandsOn = islands;
|
|
3467
|
-
let rounds = 0, kept = null;
|
|
3640
|
+
let rounds = 0, kept = null, perturbed = false;
|
|
3641
|
+
// Converged by a small update, but solved at a perturbed pivot, or in a steady solve (where a
|
|
3642
|
+
// factorisation that lost a row's digits left a reaction's rate 3.6e7 off it, the update
|
|
3643
|
+
// small, an I = 0 electrode at a voltage 0.4 V from its steady one): the residual itself,
|
|
3644
|
+
// assembled at the state, must be small too (each row scaled by its largest Jacobian entry).
|
|
3645
|
+
const unsolved = () => {
|
|
3646
|
+
if (!perturbed && dt !== Infinity) return false;
|
|
3647
|
+
this.assemble(dt);
|
|
3648
|
+
this._equilibrate();
|
|
3649
|
+
let r = 0;
|
|
3650
|
+
for (let k = 0; k < this.SINK; k++) r = Math.max(r, Math.abs(this.res[k]));
|
|
3651
|
+
return !(r < 1e-6);
|
|
3652
|
+
};
|
|
3468
3653
|
const islandsOff = (it, result) => {
|
|
3469
3654
|
if (dt !== Infinity || !this.steady || this.islands.length === 0 || rounds >= 2 || !(this._kirchhoff() > 1e-9)) return false;
|
|
3470
3655
|
this.islandsOn = true;
|
|
@@ -3509,6 +3694,13 @@ export class Solver {
|
|
|
3509
3694
|
} catch (err) {
|
|
3510
3695
|
return fail({ converged: false, iterations: it, history, error: err.message });
|
|
3511
3696
|
}
|
|
3697
|
+
// A pivot perturbed (one that cancelled to exactly nothing) can leave a row unsolved, its
|
|
3698
|
+
// update small while its residual isn't: the same couple written twice on an electrode at
|
|
3699
|
+
// open circuit converged with one spelling's rate row 41.7 mol/(m²·s) off, the couple
|
|
3700
|
+
// 1.5 RT from equilibrium. Refine the solves (exact J·v) wherever one was, and before
|
|
3701
|
+
// taking a small update for convergence there, look at the residual itself (see unsolved).
|
|
3702
|
+
perturbed = this.sys.perturbedPivots > 0;
|
|
3703
|
+
if (perturbed && !noRefine) refine = true;
|
|
3512
3704
|
try {
|
|
3513
3705
|
const pins = this.pins;
|
|
3514
3706
|
deltaV.fill(0);
|
|
@@ -3547,6 +3739,7 @@ export class Solver {
|
|
|
3547
3739
|
const last = history[history.length - 2], theta = step / last;
|
|
3548
3740
|
const contracted = dt !== Infinity && it >= 2 && step < 1e-6 && theta < 0.1 && (theta / (1 - theta)) * step < tol;
|
|
3549
3741
|
if (alpha === 1 && (step < tol || contracted)) {
|
|
3742
|
+
if (unsolved()) return fail({ converged: false, iterations: it, history, error: 'a row left unsolved at a pivot that cancelled exactly' });
|
|
3550
3743
|
const done = { converged: true, iterations: it, history, residual: rmax };
|
|
3551
3744
|
if (islandsOff(it, done)) continue;
|
|
3552
3745
|
this.computeConcentrations();
|
|
@@ -3567,6 +3760,7 @@ export class Solver {
|
|
|
3567
3760
|
if (refine) refined++;
|
|
3568
3761
|
else if (!noRefine && alpha === 1 && it >= 3 && step > 0.25 * p1 && (step < 1e-4 || (step > p1 && p1 > p2))) refine = true;
|
|
3569
3762
|
if (alpha === 1 && it >= 4 && (!refine || refined >= 1 || noRefine) && step < floor && p1 < floor && step > 0.25 * p1 && p1 > 0.25 * p2) {
|
|
3763
|
+
if (unsolved()) return fail({ converged: false, iterations: it, history, error: 'a row left unsolved at a pivot that cancelled exactly' });
|
|
3570
3764
|
const done = { converged: true, iterations: it, history, residual: rmax, roundoff: true };
|
|
3571
3765
|
if (islandsOff(it, done)) continue;
|
|
3572
3766
|
this.computeConcentrations();
|
|
@@ -3676,11 +3870,7 @@ export class Solver {
|
|
|
3676
3870
|
this.portQEnd = Float64Array.from(this.portQ);
|
|
3677
3871
|
}
|
|
3678
3872
|
const DN = { ...this.contactDEnd }, QN = Float64Array.from(this.portQEnd);
|
|
3679
|
-
if (opts.guess)
|
|
3680
|
-
// Start Newton from a predicted state (e.g. extrapolated from the history).
|
|
3681
|
-
this.u.set(opts.guess);
|
|
3682
|
-
this.uLo.fill(0);
|
|
3683
|
-
}
|
|
3873
|
+
if (opts.guess) this._guess(opts.guess); // start Newton from a predicted state
|
|
3684
3874
|
let dtEff = dt;
|
|
3685
3875
|
if (bdf) {
|
|
3686
3876
|
const a0 = (1 + 2 * w) / (1 + w), b1 = (1 + w) / a0, b2 = (w * w) / (1 + w) / a0;
|
|
@@ -3717,10 +3907,7 @@ export class Solver {
|
|
|
3717
3907
|
this.u.set(this.uPrev);
|
|
3718
3908
|
this.uLo.set(this.uPrevLo);
|
|
3719
3909
|
this.termV.set(this.termVPrev);
|
|
3720
|
-
if (opts.guess)
|
|
3721
|
-
this.u.set(opts.guess);
|
|
3722
|
-
this.uLo.fill(0);
|
|
3723
|
-
}
|
|
3910
|
+
if (opts.guess) this._guess(opts.guess);
|
|
3724
3911
|
const again = this.newton(dtEff, { ...opts, islands: true });
|
|
3725
3912
|
result = { ...again, iterations: result.iterations + again.iterations };
|
|
3726
3913
|
}
|
|
@@ -3730,10 +3917,7 @@ export class Solver {
|
|
|
3730
3917
|
this.u.set(this.uPrev);
|
|
3731
3918
|
this.uLo.set(this.uPrevLo);
|
|
3732
3919
|
this.termV.set(this.termVPrev);
|
|
3733
|
-
if (opts.guess)
|
|
3734
|
-
this.u.set(opts.guess);
|
|
3735
|
-
this.uLo.fill(0);
|
|
3736
|
-
}
|
|
3920
|
+
if (opts.guess) this._guess(opts.guess);
|
|
3737
3921
|
for (const cs of this.lawForm.keys()) this.lawForm.set(cs, true);
|
|
3738
3922
|
const again = this.newton(dtEff, opts);
|
|
3739
3923
|
result = { ...again, iterations: result.iterations + again.iterations };
|
|
@@ -3766,6 +3950,19 @@ export class Solver {
|
|
|
3766
3950
|
return result;
|
|
3767
3951
|
}
|
|
3768
3952
|
|
|
3953
|
+
// Newton's unknowns set to a guess (e.g. extrapolated from the history). Only its unknowns: the
|
|
3954
|
+
// other slots never change (see _activeSlots), and among them is a gauge, an insertion host's
|
|
3955
|
+
// φ̂, that nothing pulls back. Extrapolated through steps that grow, it wandered off
|
|
3956
|
+
// geometrically, 147 to 8000 thermal units in a battery's first 100 s and 1e11 by 1e5 s, where
|
|
3957
|
+
// its species' levels (η − zφ̂, summed back by the host's statistics) lost their digits to it.
|
|
3958
|
+
_guess(guess) {
|
|
3959
|
+
const { u, uLo } = this;
|
|
3960
|
+
for (const k of this.fullOf) {
|
|
3961
|
+
u[k] = guess[k];
|
|
3962
|
+
uLo[k] = 0;
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3769
3966
|
/**
|
|
3770
3967
|
* Adaptive time stepping to tEnd: variable-step BDF2 (or backward Euler), with the local error
|
|
3771
3968
|
* estimated against an explicit predictor through the previous states, and controlled to
|
|
@@ -3786,8 +3983,13 @@ export class Solver {
|
|
|
3786
3983
|
const trace = { t: [], current: [], voltage: [], ...(sample ? { probes: sample.out } : {}) };
|
|
3787
3984
|
let steps = 0, rejected = 0, iterations = 0, failed = false;
|
|
3788
3985
|
// Already there (an animation frame with no time to add): nothing to do, and the step size
|
|
3789
|
-
// carried to the next call stays as it was.
|
|
3790
|
-
|
|
3986
|
+
// carried to the next call stays as it was. A time already passed, or reached again outside
|
|
3987
|
+
// an animation, is most likely advance(dt) read as a duration: say so.
|
|
3988
|
+
if (!(tEnd > this.time)) {
|
|
3989
|
+
const relative = tEnd < this.time || (this.time > 0 && opts.budgetMs === undefined);
|
|
3990
|
+
return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace, ...(relative ? { stopped: `advance(${tEnd}) did nothing: ${absolute}. To run for a further dt, advance(sol.time + dt)` } : {}) };
|
|
3991
|
+
}
|
|
3992
|
+
const t0 = this.time;
|
|
3791
3993
|
let dt = this.dtNext ?? opts.dt0 ?? (tEnd - this.time) * 1e-4;
|
|
3792
3994
|
if (!(dt > 0)) dt = (tEnd - this.time) * 1e-4;
|
|
3793
3995
|
// The shortest step worth trying: round-off relative to the time, or the device's fastest
|
|
@@ -3800,8 +4002,10 @@ export class Solver {
|
|
|
3800
4002
|
if (steps + rejected >= maxSteps || clock() - start > budgetMs) break;
|
|
3801
4003
|
// Within round-off of the end (a target a hair past a breakpoint just landed on, say):
|
|
3802
4004
|
// snap to it rather than attempt a step of ~1e-13 s, which can't be resolved and, in a
|
|
3803
|
-
// strictly neutral device, fails outright.
|
|
3804
|
-
|
|
4005
|
+
// strictly neutral device, fails outright. Round-off of the time and negligible beside the
|
|
4006
|
+
// interval asked for: by the time alone, a GITT pulse of 0.01 s at t = 1e9 s was skipped
|
|
4007
|
+
// whole (the time moved, nothing else did), and from 1e10 s 17% of a pulse train's charge.
|
|
4008
|
+
if (tEnd - this.time <= 1e-10 * Math.abs(tEnd) && tEnd - this.time <= 1e-9 * (tEnd - t0)) {
|
|
3805
4009
|
this.time = tEnd;
|
|
3806
4010
|
break;
|
|
3807
4011
|
}
|
|
@@ -3895,7 +4099,10 @@ export class Solver {
|
|
|
3895
4099
|
// Stopped short, other than on the caller's wall-time budget: say where, and why.
|
|
3896
4100
|
let stopped;
|
|
3897
4101
|
const at = `advance stopped at t = ${this.time.toPrecision(6)} s of ${tEnd.toPrecision(6)}`;
|
|
3898
|
-
|
|
4102
|
+
// (What usually does it, most likely first. Round 11's agents read the old list, which led
|
|
4103
|
+
// with a drive's jump, as blaming a modest current pulse: a host emptied at its surface, and
|
|
4104
|
+
// an electrode at open circuit with no double layer, held only by a slow reaction.)
|
|
4105
|
+
if (failed) stopped = `${at}: a step did not converge even when shortened to ${dt.toExponential(1)} s. Look for: a host or surface driven to its limit (emptied or filled at its surface by a current it can no longer pass); an electrode at open circuit held only by a slow reaction, with no double-layer capacitance to keep its level (give its face one: phi capacitive); a drive's jump too big for one step (ramp it); a driven current the device cannot keep passing (a diode's reverse recovery past its storage time: stop there); or a device the steady solves also find hard.`;
|
|
3899
4106
|
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.`;
|
|
3900
4107
|
return { converged: !failed, done: this.time >= tEnd, steps, rejected, iterations, trace, ...(stopped ? { stopped } : {}) };
|
|
3901
4108
|
}
|
|
@@ -4113,8 +4320,19 @@ export class Solver {
|
|
|
4113
4320
|
this.computeConcentrations();
|
|
4114
4321
|
}
|
|
4115
4322
|
|
|
4323
|
+
// What a floating terminal k passes into the device, which its circuit row holds exactly once
|
|
4324
|
+
// converged: the current it's driven by, or (V_source − V)/R behind a resistance (the source
|
|
4325
|
+
// as the last step saw it, before any jump at its end). Read as a flux through a metal instead,
|
|
4326
|
+
// it carried that flux's round-off, G Δη with G huge: a gate behind R read 1.5e-4 off wherever
|
|
4327
|
+
// Newton stopped an update early, and below ~1e-16 A/m², nothing but noise.
|
|
4328
|
+
passedCurrent(k) {
|
|
4329
|
+
const d = this.terms[k].drive, src = sourceAt(d.src, this.time, !this.atSteady);
|
|
4330
|
+
return d.kind === 'I' ? src : (src - this.termV[k]) / d.R;
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4116
4333
|
// Terminal current toward +x at the right contact, from the last assembly at this state.
|
|
4117
4334
|
_terminalCurrent() {
|
|
4335
|
+
if (this.floating.includes(1)) return -this.passedCurrent(1);
|
|
4118
4336
|
let I = 0;
|
|
4119
4337
|
for (let i = 0; i < this.n; i++) I += FARADAY * this.z[i] * this.contactFlux.right[i];
|
|
4120
4338
|
const dt = this.lastDt;
|
package/src/sources.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// driftlet/kit: sources as plain definition data. Waveforms for a terminal's V or I, an injector
|
|
2
2
|
// port, and band-to-band recombination from a lifetime. Each returns an ordinary spec value.
|
|
3
3
|
|
|
4
|
-
import { DeviceError } from './errors.js';
|
|
4
|
+
import { DeviceError, checkOptions } from './errors.js';
|
|
5
5
|
|
|
6
6
|
const fail = (message) => {
|
|
7
7
|
throw new DeviceError(message);
|
|
@@ -19,6 +19,7 @@ const num = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
|
19
19
|
* @returns {{ t: number[], values: number[] }}
|
|
20
20
|
*/
|
|
21
21
|
export function pulse({ start = 0, width, amplitude, rise = 0, base = 0 } = {}) {
|
|
22
|
+
checkOptions('pulse', arguments[0], ['start', 'width', 'amplitude', 'rise', 'base']);
|
|
22
23
|
need(num(width) && width > 0, `pulse: width must be > 0 (s), got ${width}`);
|
|
23
24
|
need(num(amplitude), `pulse: amplitude must be a finite number (V or A/m²), got ${amplitude}`);
|
|
24
25
|
need(num(start) && start >= 0, `pulse: start must be ≥ 0 (s), got ${start}`);
|
|
@@ -35,6 +36,7 @@ export function pulse({ start = 0, width, amplitude, rise = 0, base = 0 } = {})
|
|
|
35
36
|
* @returns {{ t: number[], values: number[], repeat: true }}
|
|
36
37
|
*/
|
|
37
38
|
export function square({ high, low, period, duty = 0.5 } = {}) {
|
|
39
|
+
checkOptions('square', arguments[0], ['high', 'low', 'period', 'duty']);
|
|
38
40
|
need(num(high) && num(low), `square: high and low must be finite numbers, got ${high} and ${low}`);
|
|
39
41
|
need(num(period) && period > 0, `square: period must be > 0 (s), got ${period}`);
|
|
40
42
|
need(num(duty) && duty > 0 && duty < 1, `square: duty must be between 0 and 1, got ${duty}`);
|
|
@@ -49,6 +51,7 @@ export function square({ high, low, period, duty = 0.5 } = {}) {
|
|
|
49
51
|
* @returns {{ t: number[], values: number[], repeat: true }}
|
|
50
52
|
*/
|
|
51
53
|
export function triangle({ from, to, period } = {}) {
|
|
54
|
+
checkOptions('triangle', arguments[0], ['from', 'to', 'period']);
|
|
52
55
|
need(num(from) && num(to), `triangle: from and to must be finite numbers, got ${from} and ${to}`);
|
|
53
56
|
need(num(period) && period > 0, `triangle: period must be > 0 (s), got ${period}`);
|
|
54
57
|
return { t: [0, period / 2, period], values: [from, to, from], repeat: true };
|
|
@@ -60,6 +63,7 @@ export function triangle({ from, to, period } = {}) {
|
|
|
60
63
|
* @returns {{ t: number[], values: number[] }}
|
|
61
64
|
*/
|
|
62
65
|
export function ramp({ from, to, duration, start = 0 } = {}) {
|
|
66
|
+
checkOptions('ramp', arguments[0], ['from', 'to', 'duration', 'start']);
|
|
63
67
|
need(num(from) && num(to), `ramp: from and to must be finite numbers, got ${from} and ${to}`);
|
|
64
68
|
need(num(duration) && duration > 0, `ramp: duration must be > 0 (s), got ${duration}`);
|
|
65
69
|
need(num(start) && start >= 0, `ramp: start must be ≥ 0 (s), got ${start}`);
|
|
@@ -78,6 +82,7 @@ export function ramp({ from, to, duration, start = 0 } = {}) {
|
|
|
78
82
|
* @returns {object} a port, for the definition's `ports`
|
|
79
83
|
*/
|
|
80
84
|
export function injector({ name = 'injector', region, from, to, species, I, headroom = 2.5 } = {}) {
|
|
85
|
+
checkOptions('injector', arguments[0], ['name', 'region', 'from', 'to', 'species', 'I', 'headroom']);
|
|
81
86
|
need(region !== undefined, 'injector: give the region (its name or index)');
|
|
82
87
|
need(num(from) && num(to) && to > from, `injector: from and to must bound the window, in m from the region's left end (got ${from}, ${to})`);
|
|
83
88
|
need(typeof species === 'string', 'injector: give the species it injects, e.g. species: \'h+\'');
|
|
@@ -96,6 +101,7 @@ export function injector({ name = 'injector', region, from, to, species, I, head
|
|
|
96
101
|
* @returns {{ equation: string, kf: Record<string, number> }} a bulk reaction
|
|
97
102
|
*/
|
|
98
103
|
export function recombination({ material, tau, majority } = {}) {
|
|
104
|
+
checkOptions('recombination', arguments[0], ['material', 'tau', 'majority']);
|
|
99
105
|
need(typeof material === 'string', 'recombination: give the material, by name');
|
|
100
106
|
need(num(tau) && tau > 0, `recombination: tau must be > 0 (s), got ${tau}`);
|
|
101
107
|
need(num(majority) && majority > 0, `recombination: majority must be the majority carrier density > 0 (mol/m³; units.perCm3 converts), got ${majority}`);
|
|
@@ -117,6 +123,7 @@ export function recombination({ material, tau, majority } = {}) {
|
|
|
117
123
|
* @returns {{ equation: string, fixed: Record<string, number>, kf: Record<string, { x: number[], values: number[] }> }} a bulk reaction
|
|
118
124
|
*/
|
|
119
125
|
export function photogeneration({ material, flux, alpha, mu, from = 0, to, makes = 'e- + h+' } = {}) {
|
|
126
|
+
checkOptions('photogeneration', arguments[0], ['material', 'flux', 'alpha', 'mu', 'from', 'to', 'makes']);
|
|
120
127
|
need(typeof material === 'string', 'photogeneration: give the material, by name');
|
|
121
128
|
need(num(flux) && flux >= 0, `photogeneration: flux must be the photon flux ≥ 0 entering the material (mol/(m²·s)), got ${flux}`);
|
|
122
129
|
need(num(alpha) && alpha > 0, `photogeneration: alpha must be the absorption coefficient > 0 (1/m; 1/cm × 100), got ${alpha}`);
|