driftlet 1.2.1 → 1.2.2
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 +2 -2
- package/docs/internals.md +86 -0
- package/docs/numerics.md +12 -1
- package/docs/reliability.md +15 -4
- package/llms.txt +6 -6
- package/package.json +1 -1
- package/src/lawRows.js +562 -0
- package/src/laws.js +693 -0
- package/src/solver.js +63 -1006
- package/types/lawRows.d.ts +5 -0
- package/types/laws.d.ts +86 -0
- package/types/solver.d.ts +78 -43
package/src/solver.js
CHANGED
|
@@ -220,6 +220,8 @@ import { EPS0, FARADAY } from './constants.js';
|
|
|
220
220
|
import { nextBreakpoint, sourceAt } from './device.js';
|
|
221
221
|
import { DifferenceTerms } from './difference.js';
|
|
222
222
|
import { DeviceError } from './errors.js';
|
|
223
|
+
import { chargeOnly, conservedMoieties, feeds, findLaws, noCurrent } from './laws.js';
|
|
224
|
+
import { applyLaws, captureLaws, captureLocal, snapFlat, solveBordered } from './lawRows.js';
|
|
223
225
|
import { powi, powr } from './pow.js';
|
|
224
226
|
|
|
225
227
|
// A profile's value at x (c0, kf): piecewise linear, constant beyond its ends.
|
|
@@ -482,7 +484,7 @@ export class Solver {
|
|
|
482
484
|
model.ports.forEach((port, k) => port.capacitance && port.nodes.forEach((g, w) => this.qgTerms[g].push([2 + k, w])));
|
|
483
485
|
this.contactDOld = { left: 0, right: 0 };
|
|
484
486
|
this.contactDStart = { left: 0, right: 0 };
|
|
485
|
-
this.lawForm = new Map(); // (each circuit law: read as its change this step, see
|
|
487
|
+
this.lawForm = new Map(); // (each circuit law: read as its change this step, see src/lawRows.js)
|
|
486
488
|
// Terminals (the two contacts, then the ports): each one's voltage (V), held by its source or
|
|
487
489
|
// floating (driven by a current, or behind a resistance), and its current into the device.
|
|
488
490
|
// Sources are read at sourceTime: a step's end, or now for a steady solve.
|
|
@@ -493,10 +495,11 @@ export class Solver {
|
|
|
493
495
|
this.sourceTime = 0;
|
|
494
496
|
this.sourceOverride = new Map(); // terminal → voltage, while a steady solve ramps it
|
|
495
497
|
this.steady = false; // inside a steady solve: sources at the present time, not a step's end
|
|
496
|
-
// The floating ones are extra unknowns, solved with the grid's by bordering (
|
|
498
|
+
// The floating ones are extra unknowns, solved with the grid's by bordering (solveBordered in
|
|
499
|
+
// src/lawRows.js).
|
|
497
500
|
this.floating = this.terms.flatMap((t, k) => (t.drive.kind === 'I' || t.drive.R > 0 ? [k] : []));
|
|
498
501
|
this.drivenKey = this._drivenKey();
|
|
499
|
-
this.lawRefs = new Map(); // what a steady solve keeps behind a terminal at no current (
|
|
502
|
+
this.lawRefs = new Map(); // what a steady solve keeps behind a terminal at no current (src/lawRows.js)
|
|
500
503
|
this.chargeRefs = new Map(); // …and the charge a terminal at no current that only charges keeps
|
|
501
504
|
// Per terminal: B = ∂res/∂V (compact column), C = ∂I/∂x (compact row), ∂I/∂V, and for a
|
|
502
505
|
// floating one its circuit residual (I − I_set, or I − (V_src − V)/R).
|
|
@@ -736,7 +739,8 @@ export class Solver {
|
|
|
736
739
|
// a port). So Ag⁺ between silver electrodes is fed (Ag⁺ + e⁻ ⇌ Ag(s), the electrons fed by the
|
|
737
740
|
// contacts), while Fe³⁺ and Fe²⁺ between platinum electrodes aren't (Fe³⁺ + e⁻ ⇌ Fe²⁺
|
|
738
741
|
// conserves the iron, whatever the electrons do).
|
|
739
|
-
this.
|
|
742
|
+
this.rough = this.stretches.map((st) => this._roughAmount(st));
|
|
743
|
+
this.moieties = conservedMoieties(this._structure());
|
|
740
744
|
this.stretches.forEach((st, k) => {
|
|
741
745
|
st.connected = st.contactFed || this.moieties.every((w) => w[k] === 0);
|
|
742
746
|
});
|
|
@@ -753,7 +757,8 @@ export class Solver {
|
|
|
753
757
|
// inversion electrons, which reach the back contact only through a bulk with ~1e3 of them per
|
|
754
758
|
// cm³; blocked SO₄²⁻ driven from a zinc cathode's extended space charge, down to 1e-25 mol/m³
|
|
755
759
|
// there, whose chain of conductances lost the system 14 digits). So steady solves hold it
|
|
756
|
-
// flat (
|
|
760
|
+
// flat (flatRows in src/lawRows.js): at its contact's level, or reaching none, at the level
|
|
761
|
+
// its amount fixes.
|
|
757
762
|
// (Not through flow or mixing, nor a concentrated material's cross-diffusion, where carrying
|
|
758
763
|
// nothing isn't a level flat.)
|
|
759
764
|
const plain = (st) => {
|
|
@@ -817,881 +822,39 @@ export class Solver {
|
|
|
817
822
|
return t;
|
|
818
823
|
}
|
|
819
824
|
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
_moves(st) {
|
|
823
|
-
const { regions, materials } = this.model;
|
|
824
|
-
for (let q = st.regions[0]; q <= st.regions[1]; q++) {
|
|
825
|
-
const mat = materials[regions[q].material];
|
|
826
|
-
if (!(mat.conductor || mat.D[st.species] > 0)) return false;
|
|
827
|
-
}
|
|
828
|
-
return true;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
_still(st) {
|
|
832
|
-
const { regions, materials } = this.model;
|
|
833
|
-
for (let q = st.regions[0]; q <= st.regions[1]; q++) {
|
|
834
|
-
const mat = materials[regions[q].material];
|
|
835
|
-
if (mat.conductor || mat.D[st.species] > 0) return false;
|
|
836
|
-
}
|
|
837
|
-
return true;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// The charge number an electrode's carrier column counts its charge in, Q/(zF): its terminal
|
|
841
|
-
// species', or 1 for a port with none (a membrane's capacitance: Q/F).
|
|
842
|
-
_carrierZ(k) {
|
|
843
|
-
const t = this.model.ports[k - 2].terminal;
|
|
844
|
-
return t === null || t === undefined ? 1 : this.z[t];
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// Whether contact k (0, 1) holds a charge its current counts, D_in on its φ law (capacitive,
|
|
848
|
-
// pinned or bulk, where φ is defined at its end node).
|
|
849
|
-
_contactCharges(k) {
|
|
850
|
-
const g = k === 0 ? 0 : this.nNodes - 1;
|
|
851
|
-
return ['capacitive', 'pinned', 'bulk'].includes(this.model.contacts[k === 0 ? 'left' : 'right'].phi.type) && !this.phiUndefined[g];
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// Whether terminal k (a contact, or port k − 2) feeds stretch st: links its species there.
|
|
855
|
-
_feeds(k, st) {
|
|
856
|
-
return k === 0 ? st.leftOpen : k === 1 ? st.rightOpen : st.linked.includes(k - 2);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
// What a steady solve keeps (under the terminals' present drives, so again after a change of
|
|
860
|
-
// drive): each conserved amount, solved directly in place of one (redundant) balance row, and
|
|
861
|
-
// the islands' summed balances.
|
|
825
|
+
// The laws a steady solve keeps (src/laws.js finds them, from the structure alone), set on the
|
|
826
|
+
// solver with the state that goes with them; again after a change of drive.
|
|
862
827
|
_laws() {
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
this.
|
|
874
|
-
const conductor = (st) => materials[regions[st.regions[0]].material].conductor;
|
|
875
|
-
// The terminals driven by a current that feed a stretch: a combination through one keeps what
|
|
876
|
-
// the state holds when a solve begins (see _captureLaws), and has no steady state at any other
|
|
877
|
-
// current.
|
|
878
|
-
const driven = (st) => this.terms.flatMap((t, k) => (t.drive.kind === 'I' && this._feeds(k, st) ? [k] : []));
|
|
879
|
-
const add = (parts, rowStretch, surface = [], circuit = -1, carriers = []) => {
|
|
880
|
-
const st = this.stretches[rowStretch];
|
|
881
|
-
// (the terminal's weight in it, w/z on what that feeds: the amount changes by λ I/F)
|
|
882
|
-
const own = carriers.find((p) => p.k === circuit);
|
|
883
|
-
const lambda = circuit < 0 ? 0 : ((own && own.w / this._carrierZ(circuit)) || parts.map((p) => (driven(this.stretches[p.stretch]).includes(circuit) ? p.w / this.z[this.stretches[p.stretch].species] : 0)).find((x) => x !== 0));
|
|
884
|
-
// In a circuit row, what passes through that terminal's capacitance is kept too, λ Q/F, as
|
|
885
|
-
// its current counts dQ/dt: a port or contact at no current that passes K⁺ and charges keeps
|
|
886
|
-
// n_K + Q/F. (Its K⁺ alone held, a membrane port's voltage settled 7 mV from the transient's.)
|
|
887
|
-
// A port's charge, as an electrode's carrier; a contact's, its displacement D_in, below.
|
|
888
|
-
if (circuit >= 2 && model.ports[circuit - 2].capacitance && !carriers.some((p) => p.k === circuit)) carriers = [...carriers, { k: circuit, w: lambda * this._carrierZ(circuit) }];
|
|
889
|
-
// Every contact driven by a current that feeds it and holds a charge, D_in, kept beside the
|
|
890
|
-
// ions it passes at its own λ (two contacts at no current passing K⁺, one a capacitor: its
|
|
891
|
-
// charge was left out where the law took the other's row).
|
|
892
|
-
const lam = (t) => parts.map((p) => (driven(this.stretches[p.stretch]).includes(t) && this.z[this.stretches[p.stretch].species] !== 0 ? p.w / this.z[this.stretches[p.stretch].species] : 0)).find((x) => x !== 0);
|
|
893
|
-
const contacts = circuit < 0 ? [] : [0, 1].filter((t) => lam(t) && this._contactCharges(t)).map((t) => ({ t, lambda: lam(t) }));
|
|
894
|
-
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
895
|
-
for (const p of surface) nNodes += model.ports[this.surfCols[p.col][0]].nodes.length * model.ports[this.surfCols[p.col][0]].surface.length;
|
|
896
|
-
for (const p of carriers) nNodes += model.ports[p.k - 2].nodes.length;
|
|
897
|
-
for (const p of parts) this.stretches[p.stretch].conserved = true;
|
|
898
|
-
this.constraints.push({
|
|
899
|
-
parts,
|
|
900
|
-
surface,
|
|
901
|
-
carriers, // (what a port's electrode holds, Q/(zF) at weight w: see _conservedMoieties)
|
|
902
|
-
contacts, // (contacts whose charges it keeps, λ D_in/F each)
|
|
903
|
-
terminals: [...new Set([...parts.flatMap((p) => driven(this.stretches[p.stretch])), ...carriers.map((p) => p.k)])],
|
|
904
|
-
key: parts.map((p) => `${p.stretch}:${p.w}`).join() + surface.map((p) => `|${p.col}:${p.w}`).join() + carriers.map((p) => `|T${p.k}:${p.w}`).join(),
|
|
905
|
-
circuit, // (the terminal whose circuit row it stands in for, or −1 for a balance row's, `row`)
|
|
906
|
-
lambda,
|
|
907
|
-
row: circuit >= 0 ? -1 : this.blockOfNode[st.nodes[0]] * this.M + 1 + st.species,
|
|
908
|
-
idx: new Int32Array(nNodes * this.M),
|
|
909
|
-
w: new Float64Array(nNodes * this.M),
|
|
910
|
-
len: 0,
|
|
911
|
-
res: 0,
|
|
912
|
-
q: new Float64Array(this.sys.size + 1),
|
|
913
|
-
});
|
|
914
|
-
};
|
|
915
|
-
this.stretches.forEach((st, k) => {
|
|
916
|
-
if (!st.spectator || !this._moves(st)) return;
|
|
917
|
-
add([{ stretch: k, w: 1 }], k);
|
|
918
|
-
// Held flat (see _flatRows), it moves as a whole: the pin's response is the whole stretch's
|
|
919
|
-
// shift, its amount saying how far.
|
|
920
|
-
if (this.flatStretches.includes(st)) this.constraints.at(-1).col = Array.from({ length: st.nodes[1] - st.nodes[0] + 1 }, (_, j) => this.blockOfNode[st.nodes[0] + j] * this.M + 1 + st.species);
|
|
921
|
-
});
|
|
922
|
-
const S = this.stretches.length, T = this.terms.length, X = S + this.surfCols.length;
|
|
923
|
-
// Two column spaces. A law's weights w: stretches [0, S), electrode surfaces' species [S, X)
|
|
924
|
-
// (surface column k − S), electrodes' charges [X, …) (carrier k − X). The rows a law can take,
|
|
925
|
-
// v: stretches' balance rows [0, S), then terminals' circuit rows (terminal j − S).
|
|
926
|
-
const isStretch = (k) => k < S, isSurface = (k) => k >= S && k < X, isCarrier = (k) => k >= X;
|
|
927
|
-
const circuitOf = (j) => (j >= S ? j - S : -1);
|
|
928
|
-
const zc = (k) => this._carrierZ(k);
|
|
929
|
-
// The rows replaced: a moving stretch's balance (one of still stretches alone is kept node by
|
|
930
|
-
// node, below), or the circuit row of a terminal driven at no current, which says nothing the
|
|
931
|
-
// combination's balance rows don't (their weighted sum is that terminal's current, weighted
|
|
932
|
-
// by w/z). Together they must be independent, each combination's weights over the rows
|
|
933
|
-
// replaced a nonsingular matrix, or one balance is lost and another said twice (A + B = AB and
|
|
934
|
-
// A + C = AC: −A + B + C in place of B's row, B + AB of AB's and A − B + AC of A's said B and
|
|
935
|
-
// A twice and C not at all, and the solve converged to a state that wasn't steady). So they're
|
|
936
|
-
// chosen by elimination, each combination's weights reduced by the rows already chosen: a
|
|
937
|
-
// circuit row where one is left, else the first stretch with weight 1, else the basis
|
|
938
|
-
// vector's own (its weight is 1 there, and no other vector's is), else the largest.
|
|
939
|
-
const rowOf = (k) => this.blockOfNode[this.stretches[k].nodes[0]] * this.M + 1 + this.stretches[k].species;
|
|
940
|
-
const chosen = []; // (a column and the reduced weights, per row replaced: S + t for terminal t's)
|
|
941
|
-
const spectators = new Set(this.constraints.map((cs) => cs.row));
|
|
942
|
-
// What an electrode holds goes only in its terminal's circuit row (its charge depends on the
|
|
943
|
-
// terminal's voltage, which a balance row's pin can't carry): the combinations through it come
|
|
944
|
-
// first, so that the first takes that row, and the rest have its multiple taken off.
|
|
945
|
-
const through = (w) => this.carriers.some((k, j) => w[X + j] !== 0);
|
|
946
|
-
const held = new Map(); // (terminal → the combination in place of its circuit row)
|
|
947
|
-
// Reactions' equilibria first. A face reaction that alone changes some combination (without
|
|
948
|
-
// it, w would be conserved: Ag⁺ closed in a cell where Ag⁺ + e⁻ = Ag(s) is its one source, or
|
|
949
|
-
// Ag⁺ + AgNO₃ where it also complexes) has that combination's summed balance rows equal to its
|
|
950
|
-
// rate, w·ν times it, which a steady state sets to zero. The rate is a poor row: k₀ times a
|
|
951
|
-
// function of the levels, beside diffusion's terms in the same balances. An Ag⁺/Ag couple
|
|
952
|
-
// 1e-20 as fast as Fe³⁺/Fe²⁺ on the same platinum was lost to their round-off, the
|
|
953
|
-
// open-circuit potential settling where silver wasn't (−0.4661 V, the joint equilibrium
|
|
954
|
-
// −0.5004 V), and 1e-15 to 1e-18 failed. Its affinity says the same at any k₀ (each kinetic
|
|
955
|
-
// form vanishes only there), so it stands in for one of those rows (see _applyAffinities),
|
|
956
|
-
// chosen by the same elimination, and a stretch's: never a circuit row.
|
|
957
|
-
// The slowest first, whose rate rows are the poorest, and each row picked by the reaction's
|
|
958
|
-
// own terms, never by where its species are listed: the same rows in any listing. (Picked by
|
|
959
|
-
// listing, an electrode's two couples took different rows with the species reversed, and
|
|
960
|
-
// Newton converged in one listing and crawled in the other.)
|
|
961
|
-
this.affinities = [];
|
|
962
|
-
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() })));
|
|
963
|
-
faceRx.sort((a, b) => a.rx.k0 - b.rx.k0 || (a.key < b.key ? -1 : a.key > b.key ? 1 : a.f - b.f));
|
|
964
|
-
for (const { f, rx } of faceRx) {
|
|
965
|
-
const nu = new Float64Array(S);
|
|
966
|
-
let inFlat = false;
|
|
967
|
-
for (const p of rx.part) {
|
|
968
|
-
const k = this.stretchOf[(f + p.side) * this.n + p.i];
|
|
969
|
-
if (k < 0 || this.flatStretches.includes(this.stretches[k])) inFlat = true;
|
|
970
|
-
else nu[k] += p.nu;
|
|
971
|
-
}
|
|
972
|
-
if (inFlat) continue; // (held flat at a contact's levels, in equilibrium already)
|
|
973
|
-
// (Not one fed by a terminal driven by a current: its sum is the rate plus what that
|
|
974
|
-
// passes. Silver's, e⁻ + Fe²⁺ with the electrons fed, or Ag⁺ + AgNO₃, which nothing is.
|
|
975
|
-
// A current held at zero passes nothing: an electrode at open circuit feeding a closed
|
|
976
|
-
// pool of Ag⁺ beside a slow Fe³⁺/Fe²⁺ couple settled 78 mV off, its iron not stationary.)
|
|
977
|
-
const passes = (k) => !this._noCurrent(this.terms[k]);
|
|
978
|
-
const fed = (w) => w.some((x, k) => x !== 0 && ((isCarrier(k) && passes(this.carriers[k - X])) || (isStretch(k) && driven(this.stretches[k]).some(passes))));
|
|
979
|
-
// Any of the combinations it alone changes will do, with any conserved amount added (whose
|
|
980
|
-
// summed balance is zero too): one of them needs a row of its own left to take. (Silver's,
|
|
981
|
-
// e⁻ − Fe³⁺ beside a closed iron couple that had Fe³⁺'s, is e⁻ + Fe²⁺ with the iron added.)
|
|
982
|
-
const news = this._conservedMoieties(true, [], rx).filter((w) => Math.abs(nu.reduce((t, x, k) => t + x * w[k], 0)) > 1e-9 && !fed(w));
|
|
983
|
-
// (One it changes only through immobile species has its rate alone in their balance at the
|
|
984
|
-
// face's node already, a row with no diffusion beside it to lose it to: left as it is. Stood
|
|
985
|
-
// in for by a mobile partner, Ag⁺ + Cl⁻ = S(immobile) beside a floating Ag electrode said its
|
|
986
|
-
// rate twice and lost the electrode's I = 0.)
|
|
987
|
-
if (news.some((w) => [...w.keys()].every((k) => !isStretch(k) || w[k] === 0 || this._still(this.stretches[k])))) continue;
|
|
988
|
-
const kept = laws.filter((l) => !fed(l));
|
|
989
|
-
const tries = [...news, ...news.flatMap((w) => kept.flatMap((l) => [1, -1].map((sg) => w.map((x, m) => x + sg * l[m]))))];
|
|
990
|
-
for (const w of tries) {
|
|
991
|
-
const v = new Float64Array(S + T);
|
|
992
|
-
for (let k = 0; k < S; k++) {
|
|
993
|
-
if (w[k] === 0) continue;
|
|
994
|
-
const st = this.stretches[k];
|
|
995
|
-
if (this._moves(st) && !spectators.has(rowOf(k))) v[k] = w[k];
|
|
996
|
-
if (this.z[st.species] !== 0) for (const t of driven(st)) v[S + t] = w[k] / this.z[st.species];
|
|
997
|
-
}
|
|
998
|
-
for (const [col, row] of chosen) {
|
|
999
|
-
const fc = v[col];
|
|
1000
|
-
if (fc !== 0) for (let j = 0; j < S + T; j++) v[j] -= fc * row[j];
|
|
1001
|
-
}
|
|
1002
|
-
const scale = v.reduce((m, x) => Math.max(m, Math.abs(x)), 0);
|
|
1003
|
-
if (!(scale > 1e-9)) continue;
|
|
1004
|
-
// (Never a metal's carriers, nor a stretch a terminal feeds, even at no current: their rows
|
|
1005
|
-
// carry the terminal's current, and Pt's electrons standing for an iron couple's balance
|
|
1006
|
-
// put the electrode at 602 V.)
|
|
1007
|
-
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]));
|
|
1008
|
-
// (One of its own species first, then weight 1, the largest, and by name.)
|
|
1009
|
-
const own = (k) => rx.part.some((p) => p.i === this.stretches[k].species && this.stretchOf[(f + p.side) * this.n + p.i] === k);
|
|
1010
|
-
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]];
|
|
1011
|
-
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; };
|
|
1012
|
-
const col = [...Array(S).keys()].filter(ok).reduce((b, k) => (b < 0 || less(k, b) ? k : b), -1);
|
|
1013
|
-
if (col < 0) continue;
|
|
1014
|
-
chosen.push([col, v.map((x) => x / v[col])]);
|
|
1015
|
-
this.affinities.push({ rx, f, row: rowOf(col), idx: new Int32Array(rx.part.length), w: new Float64Array(rx.part.length), len: 0, res: 0 });
|
|
1016
|
-
break;
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
this.contactQ = [null, null];
|
|
1020
|
-
for (let w of [...laws.filter(through), ...laws.filter((w) => !through(w))]) {
|
|
1021
|
-
for (const [j, k] of this.carriers.entries()) {
|
|
1022
|
-
const c = held.get(k);
|
|
1023
|
-
if (w[X + j] === 0 || !c) continue;
|
|
1024
|
-
const f = w[X + j] / c[X + j], own = w.own;
|
|
1025
|
-
w = w.map((x, m) => (Math.abs(x - f * c[m]) < 1e-9 ? 0 : x - f * c[m]));
|
|
1026
|
-
w.own = own;
|
|
1027
|
-
}
|
|
1028
|
-
const parts = [...w.keys()].filter((k) => isStretch(k) && w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
1029
|
-
const surface = [...w.keys()].filter((k) => isSurface(k) && w[k] !== 0).map((k) => ({ col: k - S, w: w[k] }));
|
|
1030
|
-
const carriers = this.carriers.flatMap((k, j) => (w[X + j] !== 0 ? [{ k, w: w[X + j] }] : []));
|
|
1031
|
-
// (A surface's own is kept node by node, below; one with an electrode's charge in it too is
|
|
1032
|
-
// the circuit's, here: below skips it, and a floating electrode whose adsorbates gave it
|
|
1033
|
-
// electrons settled 15 mV from where the transient ends.)
|
|
1034
|
-
if (parts.length === 0 && carriers.length === 0) continue;
|
|
1035
|
-
if (parts.length === 1 && surface.length === 0 && this.stretches[parts[0].stretch].spectator) continue; // (above)
|
|
1036
|
-
if (parts.some((p) => !this._moves(this.stretches[p.stretch]) && !this._still(this.stretches[p.stretch]))) continue;
|
|
1037
|
-
const v = new Float64Array(S + T);
|
|
1038
|
-
for (const { stretch: k, w: wk } of parts) {
|
|
1039
|
-
const st = this.stretches[k];
|
|
1040
|
-
if (this._moves(st) && !spectators.has(rowOf(k))) v[k] = wk;
|
|
1041
|
-
if (this.z[st.species] !== 0) for (const t of driven(st)) v[S + t] = wk / this.z[st.species];
|
|
1042
|
-
}
|
|
1043
|
-
for (const { k, w: wk } of carriers) v[S + k] = wk / zc(k);
|
|
1044
|
-
for (const [col, row] of chosen) {
|
|
1045
|
-
const f = v[col];
|
|
1046
|
-
if (f !== 0) for (let j = 0; j < S + T; j++) v[j] -= f * row[j];
|
|
1047
|
-
}
|
|
1048
|
-
const scale = v.reduce((m, x) => Math.max(m, Math.abs(x)), 0);
|
|
1049
|
-
if (!(scale > 1e-9)) continue;
|
|
1050
|
-
const ok = (j) => j !== undefined && Math.abs(v[j]) > 1e-9 * scale;
|
|
1051
|
-
const col = [...Array(T).keys()].map((t) => S + t).find(ok) ?? [parts.find((p) => w[p.stretch] === 1)?.stretch, w.own].find(ok) ?? v.findIndex((x) => Math.abs(x) === scale);
|
|
1052
|
-
// (Other electrodes' charges in a circuit row not their own: their voltages enter it through
|
|
1053
|
-
// termDX. Two floating electrodes sharing a closed Fe³⁺, their law dropped, settled at 1.3 V
|
|
1054
|
-
// for −0.65.)
|
|
1055
|
-
chosen.push([col, v.map((x) => x / v[col])]);
|
|
1056
|
-
if (circuitOf(col) >= 0) held.set(circuitOf(col), w);
|
|
1057
|
-
add(parts, circuitOf(col) < 0 ? col : parts[0]?.stretch, surface, circuitOf(col), carriers);
|
|
1058
|
-
}
|
|
1059
|
-
this.contactLaw = [0, 1].map((k) => this.constraints.some((cs) => cs.contacts.some((c) => c.t === k)));
|
|
1060
|
-
// A combination that nothing carries anywhere is conserved node by node: of still stretches
|
|
1061
|
-
// (trap states: X⁰ + X⁻ under e⁻ + X⁰ = X⁻), or of an electrode's surface species (adsorbates
|
|
1062
|
-
// that only turn into each other). At each node its weighted sum stays what it was, a row local
|
|
1063
|
-
// to that node's block, in place of one of their balances.
|
|
1064
|
-
this.localConstraints = [];
|
|
1065
|
-
const span = ([g0, g1]) => Array.from({ length: g1 - g0 + 1 }, (_, j) => g0 + j);
|
|
1066
|
-
for (const w of laws) {
|
|
1067
|
-
const parts = [...w.keys()].filter((k) => w[k] !== 0);
|
|
1068
|
-
if (parts.some(isCarrier)) continue; // (through an electrode: above)
|
|
1069
|
-
const sets = parts.map((k) => (isSurface(k) ? model.ports[this.surfCols[k - S][0]].nodes : this._still(this.stretches[k]) ? span(this.stretches[k].nodes) : null));
|
|
1070
|
-
if (sets.includes(null)) continue;
|
|
1071
|
-
// Its nodes: the immobile stretches', side by side, with any surface's window among them (a
|
|
1072
|
-
// surface's term only where it is: an immobile S that adsorbs on an electrode spread over
|
|
1073
|
-
// part of its region keeps S + Γaθ there and S alone elsewhere; required to match, that law
|
|
1074
|
-
// was lost, and a floating electrode's coverage drifted). Or a surface's alone.
|
|
1075
|
-
const own = parts.filter(isStretch), base = own.length > 0 ? sets[parts.indexOf(own[0])] : sets[0];
|
|
1076
|
-
const same = (set) => set.length === base.length && set.every((g, j) => g === base[j]);
|
|
1077
|
-
if (!parts.every((k, j) => (isStretch(k) ? same(sets[j]) : sets[j].every((g) => base.includes(g))))) continue;
|
|
1078
|
-
for (const k of own) this.stretches[k].conserved = true;
|
|
1079
|
-
const slot = (k) => (isStretch(k) ? 1 + this.stretches[k].species : 1 + this.n + this.surfCols[k - S][1]);
|
|
1080
|
-
// (its row a stretch's where it has one, present at every node; else the surface's)
|
|
1081
|
-
const rowPart = own.length > 0 ? (own.includes(w.own) ? w.own : own[0]) : parts.includes(w.own) ? w.own : parts[0];
|
|
1082
|
-
this.localConstraints.push({
|
|
1083
|
-
parts: parts.map((k) => (isStretch(k) ? { i: this.stretches[k].species, w: w[k] } : { q: this.surfCols[k - S][1], k: this.surfCols[k - S][0], w: w[k] })),
|
|
1084
|
-
row: slot(rowPart),
|
|
1085
|
-
nodes: base,
|
|
1086
|
-
reference: new Float64Array(sets[0].length),
|
|
1087
|
-
});
|
|
1088
|
-
}
|
|
1089
|
-
// An immobile stretch at a node where nothing touches it (no reaction there, no port or contact
|
|
1090
|
-
// linking it) keeps its concentration: its balance there is 0 = 0 in a steady state, which
|
|
1091
|
-
// holds nothing, and a charged one's level stayed where it was while φ moved (S⁺ beside a face
|
|
1092
|
-
// that makes it, the bath moved 0.2 V: S⁺ fell to e^{−0.2/V_T} of itself away from the face).
|
|
1093
|
-
for (const st of this.stretches) {
|
|
1094
|
-
if (!this._still(st)) continue;
|
|
1095
|
-
const i = st.species, touched = new Set();
|
|
1096
|
-
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) if (this.rxsIn[this.nodeMaterial[g]].some((rx) => rx.sp.includes(i))) touched.add(g);
|
|
1097
|
-
model.interfaces.forEach((itf, f) => {
|
|
1098
|
-
for (const rx of itf.reactions) for (const p of rx.part) if (p.i === i) touched.add(p.side === 0 ? grid.regionEnd[f] : grid.regionStart[f + 1]);
|
|
1099
|
-
});
|
|
1100
|
-
for (const port of model.ports) {
|
|
1101
|
-
if (port.species[i].type !== 'blocked' || port.reactions.some((rx) => rx.part.some((p) => p.side === 0 && p.i === i))) for (const g of port.nodes) touched.add(g);
|
|
1102
|
-
}
|
|
1103
|
-
if (st.leftOpen) touched.add(0);
|
|
1104
|
-
if (st.rightOpen) touched.add(this.nNodes - 1);
|
|
1105
|
-
for (const lc of this.localConstraints) if (lc.row === 1 + i) for (const g of lc.nodes) touched.add(g);
|
|
1106
|
-
const free = span(st.nodes).filter((g) => !touched.has(g));
|
|
1107
|
-
if (free.length > 0) this.localConstraints.push({ parts: [{ i, w: 1 }], row: 1 + i, nodes: free, reference: new Float64Array(free.length) });
|
|
1108
|
-
}
|
|
1109
|
-
// Islands: the pieces of a stretch between its faces. One that nothing else feeds (no
|
|
1110
|
-
// contact, port or reaction) holds its level only through its faces' fluxes, which can be
|
|
1111
|
-
// ~1e-24 of its own conduction (a tiny conductance at the face, or a neighbour that barely
|
|
1112
|
-
// conducts): eliminated, the level is lost to round-off, and Newton settles at a wrong one with
|
|
1113
|
-
// nothing to show for it. In steady solves the piece's balance summed over its nodes, where
|
|
1114
|
-
// the internal fluxes cancel exactly and the faces' alone are left, replaces its first node's
|
|
1115
|
-
// (see _applyIslands), once the plain solves converge (see newton).
|
|
1116
|
-
this.islands = [];
|
|
828
|
+
const found = findLaws(this._structure());
|
|
829
|
+
for (const [k, st] of this.stretches.entries()) st.conserved = found.conserved.has(k);
|
|
830
|
+
this.constraints = found.constraints;
|
|
831
|
+
this.affinities = found.affinities;
|
|
832
|
+
this.localConstraints = found.localConstraints;
|
|
833
|
+
this.islands = found.islands;
|
|
834
|
+
this.timeLaws = found.timeLaws;
|
|
835
|
+
this.contactLaw = found.contactLaw;
|
|
836
|
+
this.carriers = found.carriers;
|
|
837
|
+
this.unkept = found.unkept; // (laws no row here can keep: see _directSteady)
|
|
838
|
+
this.contactQ = [null, null]; // (each contact's charge as _contact captures it, for contactLaw)
|
|
1117
839
|
this.pinnedIslands = [];
|
|
1118
840
|
this.islandsOn = false; // (newton() turns them on, see there)
|
|
1119
841
|
this.pins = [];
|
|
1120
|
-
const used = new Set([...this.constraints.map((cs) => cs.row).filter((o) => o >= 0), ...this.affinities.map((af) => af.row)]);
|
|
1121
|
-
for (const st of this.stretches) {
|
|
1122
|
-
const i = st.species, [s0, s1] = st.regions;
|
|
1123
|
-
if (!st.mobile || conductor(st)) continue;
|
|
1124
|
-
for (let q0 = s0, q1 = s0; q1 <= s1; q1++) {
|
|
1125
|
-
const p0 = q0;
|
|
1126
|
-
q0 = q1 + 1;
|
|
1127
|
-
if (p0 === s0 && q1 === s1) continue; // (uncut)
|
|
1128
|
-
if ((p0 === s0 && st.leftOpen) || (q1 === s1 && st.rightOpen)) continue;
|
|
1129
|
-
const inside = (q) => q >= p0 && q <= q1;
|
|
1130
|
-
if (model.ports.some((port) => inside(port.region) && (port.species[i].type !== 'blocked' || port.reactions.some((rx) => rx.part.some((p) => p.side === 0 && p.i === i))))) continue;
|
|
1131
|
-
let reactive = false;
|
|
1132
|
-
for (let q = p0; q <= q1; q++) {
|
|
1133
|
-
for (const rx of model.reactions) if (rx.kf[regions[q].material] > 0 && [...rx.reactants, ...rx.products].some((p) => p.i === i)) reactive = true;
|
|
1134
|
-
}
|
|
1135
|
-
for (let f = Math.max(0, p0 - 1); f <= Math.min(model.interfaces.length - 1, q1); f++) {
|
|
1136
|
-
for (const rx of model.interfaces[f].reactions) if (rx.part.some((p) => p.i === i && inside(f + p.side))) reactive = true;
|
|
1137
|
-
}
|
|
1138
|
-
const row = this.blockOfNode[grid.regionStart[p0]] * this.M + 1 + i;
|
|
1139
|
-
if (reactive || used.has(row)) continue;
|
|
1140
|
-
used.add(row);
|
|
1141
|
-
// The faces' fluxes into the piece: −A_f u at its left face, +A_f u at its right. Across
|
|
1142
|
-
// a face that holds μ̄ level, u is set only by the edge balances, and eliminated, it can
|
|
1143
|
-
// come out of the piece's own (G Δη, its two η all but equal: a neighbour 1e20 times
|
|
1144
|
-
// less conductive, and the piece's level was seen through one face of two, and Newton
|
|
1145
|
-
// swung it back and forth across both). There the outside edge node's balance is added:
|
|
1146
|
-
// u cancels, and the outside's first segment carries the flux instead.
|
|
1147
|
-
const faces = [], outside = [];
|
|
1148
|
-
if (p0 > s0) faces.push([p0 - 1, -1, grid.regionEnd[p0 - 1]]);
|
|
1149
|
-
if (q1 < s1) faces.push([q1, 1, grid.regionStart[q1 + 1]]);
|
|
1150
|
-
for (const [f, , g] of faces) {
|
|
1151
|
-
if (model.interfaces[f].links[i].type !== 'equilibrium' || g === 0 || g === this.nNodes - 1) continue;
|
|
1152
|
-
if (model.ports.some((port) => port.nodes.includes(g))) continue;
|
|
1153
|
-
outside.push(this.blockOfNode[g] * this.M + 1 + i);
|
|
1154
|
-
}
|
|
1155
|
-
this.islands.push({
|
|
1156
|
-
stretch: st,
|
|
1157
|
-
row,
|
|
1158
|
-
flux: faces.map(([f]) => this.blockOfFace[f] * this.M + 1 + i),
|
|
1159
|
-
weight: faces.map(([f, sg]) => sg * grid.area[grid.regionEnd[f]]),
|
|
1160
|
-
outside,
|
|
1161
|
-
idx: new Int32Array(faces.length + 3 * this.M * outside.length),
|
|
1162
|
-
w: new Float64Array(faces.length + 3 * this.M * outside.length),
|
|
1163
|
-
len: 0,
|
|
1164
|
-
res: 0,
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
for (const isl of this.islands) isl.outside = isl.outside.filter((o) => !used.has(o));
|
|
1169
|
-
|
|
1170
|
-
// Time steps only: a terminal behind a resistance that alone feeds a combination reads its
|
|
1171
|
-
// current by that combination's change too (see _circuitLaws), its combinations found with it
|
|
1172
|
-
// counted as driven by a current. (Not in the steady system, where it holds its level.)
|
|
1173
|
-
this.timeLaws = [];
|
|
1174
|
-
const behind = this.floating.filter((k) => this.terms[k].drive.kind !== 'I');
|
|
1175
|
-
if (behind.length > 0) {
|
|
1176
|
-
const steadyCarriers = this.carriers, more = this._conservedMoieties(true, behind), X2 = S + this.surfCols.length;
|
|
1177
|
-
for (const w of more) {
|
|
1178
|
-
const parts = [...w.keys()].filter((k) => k < S && w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
1179
|
-
if (parts.length === 0 || parts.some((p) => !this._moves(this.stretches[p.stretch]) && !this._still(this.stretches[p.stretch]))) continue;
|
|
1180
|
-
const surface = [...w.keys()].filter((k) => k >= S && k < X2 && w[k] !== 0).map((k) => ({ col: k - S, w: w[k] }));
|
|
1181
|
-
const carriers = this.carriers.flatMap((k, j) => (w[X2 + j] !== 0 ? [{ k, w: w[X2 + j] }] : []));
|
|
1182
|
-
// Fed by one terminal behind a resistance and nothing else that passes current.
|
|
1183
|
-
const fedBy = new Set([...carriers.map((p) => p.k), ...parts.flatMap((p) => this.floating.filter((k) => this._feeds(k, this.stretches[p.stretch])))]);
|
|
1184
|
-
if (fedBy.size !== 1) continue;
|
|
1185
|
-
const [k] = fedBy;
|
|
1186
|
-
if (!behind.includes(k) || this.timeLaws.some((cs) => cs.circuit === k)) continue;
|
|
1187
|
-
const zc = k >= 2 ? this._carrierZ(k) : 0;
|
|
1188
|
-
const lambda = carriers.find((p) => p.k === k)?.w / zc || parts.map((p) => (this._feeds(k, this.stretches[p.stretch]) && this.z[this.stretches[p.stretch].species] !== 0 ? p.w / this.z[this.stretches[p.stretch].species] : 0)).find((x) => x !== 0);
|
|
1189
|
-
if (!lambda) continue;
|
|
1190
|
-
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
1191
|
-
for (const p of surface) nNodes += model.ports[this.surfCols[p.col][0]].nodes.length * model.ports[this.surfCols[p.col][0]].surface.length;
|
|
1192
|
-
for (const p of carriers) nNodes += model.ports[p.k - 2].nodes.length;
|
|
1193
|
-
this.timeLaws.push({ parts, surface, carriers, terminals: [k], circuit: k, lambda, idx: new Int32Array(nNodes * this.M), w: new Float64Array(nNodes * this.M), len: 0 });
|
|
1194
|
-
}
|
|
1195
|
-
this.carriers = steadyCarriers;
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
// What each combination fed by a terminal driven at no current holds now, which a steady solve
|
|
1200
|
-
// keeps: a closed battery at open circuit stays at the charge it had.
|
|
1201
|
-
_captureLaws() {
|
|
1202
|
-
// A terminal at no current that passes current only by charging a capacitance keeps its charge.
|
|
1203
|
-
this.chargeRefs.clear();
|
|
1204
|
-
const charged = this.floating.filter((k) => this.terms[k].drive.kind === 'I' && this._chargeOnly(k));
|
|
1205
|
-
if (charged.length > 0) {
|
|
1206
|
-
this._assembleBookkeeping(Infinity);
|
|
1207
|
-
for (const k of charged) this.chargeRefs.set(k, k >= 2 ? this.portQ[k - 2] : k === 0 ? this.contactD.left : -this.contactD.right);
|
|
1208
|
-
}
|
|
1209
|
-
this.lawRefs.clear();
|
|
1210
|
-
if (this.constraints.some((cs) => cs.contacts.length > 0 || cs.carriers.some(({ k }) => this.model.ports[k - 2].capacitance)) && charged.length === 0) this._assembleBookkeeping(Infinity);
|
|
1211
|
-
for (const cs of this.constraints) {
|
|
1212
|
-
if (cs.terminals.length === 0) continue;
|
|
1213
|
-
let a = 0;
|
|
1214
|
-
for (const { stretch, w } of cs.parts) a += w * this.amount(this.stretches[stretch]);
|
|
1215
|
-
for (const { col, w } of cs.surface) a += w * this.surfaceAmount(col);
|
|
1216
|
-
for (const { k, w } of cs.carriers) if (this.model.ports[k - 2].capacitance) a += (w * this.portQ[k - 2]) / (this._carrierZ(k) * FARADAY);
|
|
1217
|
-
for (const { t, lambda } of cs.contacts) a += (lambda * (t === 0 ? this.contactD.left : -this.contactD.right)) / FARADAY;
|
|
1218
|
-
this.lawRefs.set(cs.key, a);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// Each local constraint's weighted sum at each node, as the state holds it now: what a steady
|
|
1223
|
-
// solve keeps.
|
|
1224
|
-
_captureLocal() {
|
|
1225
|
-
const { n, c, th, nSurf } = this;
|
|
1226
|
-
for (const lc of this.localConstraints) {
|
|
1227
|
-
lc.nodes.forEach((g, j) => {
|
|
1228
|
-
let t = 0;
|
|
1229
|
-
for (const { i, q, k, w } of lc.parts) {
|
|
1230
|
-
if (i !== undefined) t += w * c[g * n + i];
|
|
1231
|
-
else if (this.surfPort[g] === k) t += w * this._sites(g) * th[g * nSurf + q];
|
|
1232
|
-
}
|
|
1233
|
-
lc.reference[j] = t;
|
|
1234
|
-
});
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
// The islands' summed balances (steady solves only): Σ A_f u over the faces into each, its
|
|
1239
|
-
// first node's row pinned in the factorised matrix and the sum bordered (_solveBordered) like a
|
|
1240
|
-
// conserved amount's. The pieces of a stretch held flat by its contact are left as they are.
|
|
1241
|
-
_applyIslands() {
|
|
1242
|
-
const { u, res } = this, R = this.rix, out = [];
|
|
1243
|
-
for (const isl of this.islands) {
|
|
1244
|
-
if (this.flatStretches.includes(isl.stretch)) continue;
|
|
1245
|
-
const b0 = Math.floor(isl.row / this.M), r0 = isl.row % this.M;
|
|
1246
|
-
if (this.loc[isl.row] < 0) continue;
|
|
1247
|
-
// Gathered by column (the faces' u, then each outside row's entries, which cancel those u
|
|
1248
|
-
// exactly where they meet), then the nonzeros kept.
|
|
1249
|
-
const acc = this.islandRow ?? (this.islandRow = new Float64Array(this.sys.size + 1));
|
|
1250
|
-
const cols = [];
|
|
1251
|
-
const add = (col, v) => {
|
|
1252
|
-
if (acc[col] === 0) cols.push(col);
|
|
1253
|
-
acc[col] += v;
|
|
1254
|
-
};
|
|
1255
|
-
let t = 0;
|
|
1256
|
-
isl.flux.forEach((o, j) => {
|
|
1257
|
-
if (this.loc[o] < 0) return;
|
|
1258
|
-
t += isl.weight[j] * u[o];
|
|
1259
|
-
add(R[o], isl.weight[j]);
|
|
1260
|
-
});
|
|
1261
|
-
for (const o of isl.outside) {
|
|
1262
|
-
if (this.loc[o] < 0) continue;
|
|
1263
|
-
t += res[R[o]];
|
|
1264
|
-
this._rowInto(o, add);
|
|
1265
|
-
}
|
|
1266
|
-
let len = 0;
|
|
1267
|
-
for (const col of cols) {
|
|
1268
|
-
if (col < this.sys.size && acc[col] !== 0) {
|
|
1269
|
-
isl.idx[len] = col;
|
|
1270
|
-
isl.w[len++] = acc[col];
|
|
1271
|
-
}
|
|
1272
|
-
acc[col] = 0;
|
|
1273
|
-
}
|
|
1274
|
-
isl.len = len;
|
|
1275
|
-
isl.res = t;
|
|
1276
|
-
this._replaceRow(b0, r0);
|
|
1277
|
-
for (const B of this.termB) B[R[isl.row]] = 0;
|
|
1278
|
-
this._j(b0, r0, b0, r0, 1);
|
|
1279
|
-
res[R[isl.row]] = 0;
|
|
1280
|
-
out.push(isl);
|
|
1281
|
-
}
|
|
1282
|
-
return out;
|
|
1283
842
|
}
|
|
1284
843
|
|
|
1285
|
-
//
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
const {
|
|
1289
|
-
|
|
1290
|
-
for (let k = 0; k < m; k++) if (B[offB[b] + l * m + k] !== 0) add(offX[b] + k, B[offB[b] + l * m + k]);
|
|
1291
|
-
if (b > 0) for (let k = 0, mp = sizes[b - 1]; k < mp; k++) if (A[offA[b] + l * mp + k] !== 0) add(offX[b - 1] + k, A[offA[b] + l * mp + k]);
|
|
1292
|
-
if (b < nB - 1) for (let k = 0, mn = sizes[b + 1]; k < mn; k++) if (C[offC[b] + l * mn + k] !== 0) add(offX[b + 1] + k, C[offC[b] + l * mn + k]);
|
|
1293
|
-
if (this.lin) {
|
|
1294
|
-
const into = this.linRow ?? (this.linRow = new Float64Array(sys.size + 1));
|
|
1295
|
-
this.lin.capture(this.rix[o], 1, 0, into);
|
|
1296
|
-
for (let k = 0; k < sys.size; k++) {
|
|
1297
|
-
if (into[k] !== 0) add(k, into[k]);
|
|
1298
|
-
into[k] = 0;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
// An electrode surface's sites per volume at node g, Γ a (mol/m³): what a coverage of 1 holds,
|
|
1304
|
-
// beside the concentrations of a local constraint. (Summed as θ alone, an immobile S that adsorbs
|
|
1305
|
-
// as A(ads) kept S + θ_A, mol/m³ and a fraction.)
|
|
1306
|
-
_sites(g) {
|
|
1307
|
-
const k = this.surfPort[g];
|
|
1308
|
-
return this.model.ports[k].surface[0].capacity * this.portArea[k][this.surfW[g]];
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
// The local constraints' rows (steady solves only): Σ w c_i − reference at each node.
|
|
1312
|
-
_applyLocalConstraints() {
|
|
1313
|
-
const { n, M, c, th, nSurf, res, dA: d } = this;
|
|
1314
|
-
for (const lc of this.localConstraints) {
|
|
1315
|
-
lc.nodes.forEach((g, j) => {
|
|
1316
|
-
const b = this.blockOfNode[g];
|
|
1317
|
-
if (this.loc[b * M + lc.row] < 0) return;
|
|
1318
|
-
this._replaceRow(b, lc.row);
|
|
1319
|
-
let t = 0;
|
|
1320
|
-
for (const { i, q, k, w } of lc.parts) {
|
|
1321
|
-
if (i !== undefined) {
|
|
1322
|
-
t += w * c[g * n + i];
|
|
1323
|
-
this._dc(g, i, d);
|
|
1324
|
-
for (let s = 0; s < M; s++) if (d[s] !== 0) this._j(b, lc.row, b, s, w * d[s]);
|
|
1325
|
-
continue;
|
|
1326
|
-
}
|
|
1327
|
-
// A coverage, with ∂θ_q/∂η_x = θ_q (δ_qx − θ_x) over its surface's species (where it is).
|
|
1328
|
-
if (this.surfPort[g] !== k) continue;
|
|
1329
|
-
const tq = th[g * nSurf + q], ns = this.model.ports[this.surfPort[g]].surface.length, ws = w * this._sites(g);
|
|
1330
|
-
t += ws * tq;
|
|
1331
|
-
for (let x = 0; x < ns; x++) this._j(b, lc.row, b, 1 + n + x, ws * tq * ((q === x ? 1 : 0) - th[g * nSurf + x]));
|
|
1332
|
-
}
|
|
1333
|
-
res[this.rix[b * M + lc.row]] = t - lc.reference[j];
|
|
1334
|
-
});
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// A basis of the conserved combinations of stretch amounts: the null space of the
|
|
1339
|
-
// stoichiometry (a row per reaction, over the stretches it touches) together with what the
|
|
1340
|
-
// terminals feed. A terminal held at a voltage (or behind a resistance, which holds it at
|
|
1341
|
-
// steady state) feeds each stretch it links freely: a unit row. With `drives`, one driven by a
|
|
1342
|
-
// current feeds its charged species only as that current allows, in proportion to their
|
|
1343
|
-
// charges, so a combination weighted by z there is conserved while it passes none: a battery's
|
|
1344
|
-
// cathode, its electrons behind an open circuit, keeps its charge. Exact integer data, so plain
|
|
1345
|
-
// elimination will do.
|
|
1346
|
-
_conservedMoieties(drives = false, alsoCurrent = [], skip = null) {
|
|
1347
|
-
const { model, n, z } = this, S = this.stretches.length;
|
|
1348
|
-
// With `drives`, an electrode port driven by a current whose reactions take its carrier from
|
|
1349
|
-
// the electrode has a column of its own, last: what the electrode holds, its charge Q/(zF),
|
|
1350
|
-
// which the current feeds and the reactions draw on (a port at no current with M⁺ + e⁻ = M(s)
|
|
1351
|
-
// keeps M⁺ + Q/F, or with no capacitance, Q = 0, the M⁺ alone).
|
|
1352
|
-
const byCurrent = (t, k) => t.drive.kind === 'I' || alsoCurrent.includes(k); // (with `alsoCurrent`, terminals counted as such)
|
|
1353
|
-
// (Or a port with a capacitance that passes ions, a membrane's: what it holds is Q/F. One that
|
|
1354
|
-
// only charges keeps its charge alone, see _captureLaws.)
|
|
1355
|
-
this.carriers = drives ? this.terms.flatMap((t, k) => (byCurrent(t, k) && k >= 2 && (model.ports[k - 2].reactions.length > 0 || (model.ports[k - 2].capacitance && !this._chargeOnly(k))) ? [k] : [])) : [];
|
|
1356
|
-
const X = S + this.surfCols.length, C = X + this.carriers.length;
|
|
1357
|
-
// (columns: stretches [0, S), surfaces' species [S, X), electrodes' charges [X, C), as in _laws)
|
|
1358
|
-
const isStretch = (k) => k < S, isSurface = (k) => k >= S && k < X;
|
|
1359
|
-
// (With `skip`, a reaction left out: the combinations that it alone changes come out too, and
|
|
1360
|
-
// its own rows, over the columns, are handed back as `basis.skipped`.)
|
|
1361
|
-
const rows = [], skipped = [];
|
|
1362
|
-
model.interfaces.forEach((itf, f) => {
|
|
1363
|
-
for (const rx of itf.reactions) {
|
|
1364
|
-
const row = new Float64Array(C);
|
|
1365
|
-
for (const p of rx.part) row[this.stretchOf[(f + p.side) * n + p.i]] += p.nu;
|
|
1366
|
-
(rx === skip ? skipped : rows).push(row);
|
|
1367
|
-
}
|
|
1368
|
-
});
|
|
1369
|
-
// An electrode surface's species are columns too, after the stretches (S + j for surface
|
|
1370
|
-
// column j), so a combination that passes through them (A⁺ + e⁻ = S, S + e⁻ = B⁻) is conserved
|
|
1371
|
-
// with what the surface holds.
|
|
1372
|
-
model.ports.forEach((port, k) => {
|
|
1373
|
-
for (const rx of port.reactions) {
|
|
1374
|
-
const row = new Float64Array(C); // (the electrode's carrier is outside the device)
|
|
1375
|
-
if (rx === skip) skipped.push(row);
|
|
1376
|
-
for (const p of rx.part) {
|
|
1377
|
-
if (p.side === 0) row[this.stretchOf[port.region * n + p.i]] += p.nu;
|
|
1378
|
-
else if (p.side === 2) row[S + this.surfColOf[k] + p.s] += p.nu;
|
|
1379
|
-
else if (this.carriers.includes(k + 2)) row[X + this.carriers.indexOf(k + 2)] += p.nu;
|
|
1380
|
-
}
|
|
1381
|
-
if (rx !== skip) rows.push(row);
|
|
1382
|
-
}
|
|
1383
|
-
});
|
|
1384
|
-
model.regions.forEach((reg, q) => {
|
|
1385
|
-
for (const rx of model.reactions) {
|
|
1386
|
-
if (!(rx.kf[reg.material] > 0)) continue;
|
|
1387
|
-
const row = new Float64Array(C);
|
|
1388
|
-
for (const p of rx.reactants) row[this.stretchOf[q * n + p.i]] -= p.nu;
|
|
1389
|
-
for (const p of rx.products) row[this.stretchOf[q * n + p.i]] += p.nu;
|
|
1390
|
-
(rx === skip ? skipped : rows).push(row);
|
|
1391
|
-
}
|
|
1392
|
-
});
|
|
1393
|
-
const unit = (k) => {
|
|
1394
|
-
const row = new Float64Array(C);
|
|
1395
|
-
row[k] = 1;
|
|
1396
|
-
rows.push(row);
|
|
1397
|
-
};
|
|
1398
|
-
this.terms.forEach((t, k) => {
|
|
1399
|
-
const fed = this.stretches.flatMap((st, j) => (this._feeds(k, st) ? [j] : []));
|
|
1400
|
-
if (!(drives && byCurrent(t, k))) return fed.forEach(unit);
|
|
1401
|
-
const charged = fed.filter((j) => z[this.stretches[j].species] !== 0).map((j) => [j, z[this.stretches[j].species]]);
|
|
1402
|
-
if (this.carriers.includes(k)) charged.push([X + this.carriers.indexOf(k), this._carrierZ(k)]);
|
|
1403
|
-
fed.filter((j) => z[this.stretches[j].species] === 0).forEach(unit);
|
|
1404
|
-
for (let a = 1; a < charged.length; a++) {
|
|
1405
|
-
const row = new Float64Array(C);
|
|
1406
|
-
row[charged[0][0]] = 1 / charged[0][1];
|
|
1407
|
-
row[charged[a][0]] = -1 / charged[a][1];
|
|
1408
|
-
rows.push(row);
|
|
1409
|
-
}
|
|
1410
|
-
});
|
|
1411
|
-
// Reduced row echelon form; the free columns give the null space. Immobile stretches' columns
|
|
1412
|
-
// go first, and electrode surfaces' (as local), so that a combination of them alone (trap
|
|
1413
|
-
// states, X⁰ + X⁻; adsorbates, A(ads) + B(ads)) comes out on its own, to be kept node by node,
|
|
1414
|
-
// apart from any that moves (e⁻ − X⁰, the electrons the traps hold). (Pivoted after a closed
|
|
1415
|
-
// Cl⁻ in A(ads) + Cl⁻ = B(ads) + e⁻, the surface's own came out mixed with it, and was lost.)
|
|
1416
|
-
// Then the stretches by their starting amounts, least first, and by name: a trace species
|
|
1417
|
-
// pivoted after an abundant one comes out added to it (A + B ⇌ C with B at 1 M and A at 1e-14
|
|
1418
|
-
// gave B + C and A − B, where A + C was lost to round-off), and the basis followed the listing.
|
|
1419
|
-
const still = (k) => ((isStretch(k) && this._still(this.stretches[k])) || isSurface(k) ? 1 : 0);
|
|
1420
|
-
const rough = this.stretches.map((st) => this._roughAmount(st)), name = (k) => model.species[this.stretches[k].species].name;
|
|
1421
|
-
const order = [...Array(C).keys()].sort((a, b) => {
|
|
1422
|
-
if (still(a) !== still(b)) return still(b) - still(a);
|
|
1423
|
-
if (!isStretch(a) || !isStretch(b)) return a - b;
|
|
1424
|
-
if (rough[a] !== rough[b]) return rough[a] - rough[b];
|
|
1425
|
-
const na = name(a), nb = name(b);
|
|
1426
|
-
return na < nb ? -1 : na > nb ? 1 : a - b;
|
|
1427
|
-
});
|
|
1428
|
-
const pivots = [];
|
|
1429
|
-
let r = 0;
|
|
1430
|
-
for (const col of order) {
|
|
1431
|
-
if (r >= rows.length) break;
|
|
1432
|
-
let best = r;
|
|
1433
|
-
for (let i = r + 1; i < rows.length; i++) if (Math.abs(rows[i][col]) > Math.abs(rows[best][col])) best = i;
|
|
1434
|
-
if (Math.abs(rows[best][col]) < 1e-9) continue;
|
|
1435
|
-
[rows[r], rows[best]] = [rows[best], rows[r]];
|
|
1436
|
-
const pr = rows[r], pv = pr[col];
|
|
1437
|
-
for (let j = 0; j < C; j++) pr[j] /= pv;
|
|
1438
|
-
for (let i = 0; i < rows.length; i++) {
|
|
1439
|
-
if (i === r || rows[i][col] === 0) continue;
|
|
1440
|
-
const fct = rows[i][col];
|
|
1441
|
-
for (let j = 0; j < C; j++) rows[i][j] -= fct * pr[j];
|
|
1442
|
-
}
|
|
1443
|
-
pivots.push(col);
|
|
1444
|
-
r++;
|
|
1445
|
-
}
|
|
1446
|
-
const isPivot = new Uint8Array(C);
|
|
1447
|
-
for (const col of pivots) isPivot[col] = 1;
|
|
1448
|
-
const basis = [];
|
|
1449
|
-
for (let free = 0; free < C; free++) {
|
|
1450
|
-
if (isPivot[free]) continue;
|
|
1451
|
-
const w = new Float64Array(C);
|
|
1452
|
-
w[free] = 1;
|
|
1453
|
-
pivots.forEach((col, i) => {
|
|
1454
|
-
const v = -rows[i][free];
|
|
1455
|
-
w[col] = Math.abs(v) < 1e-9 ? 0 : v;
|
|
1456
|
-
});
|
|
1457
|
-
w.own = free; // (its weight is 1 there, and no other vector's is: the row it replaces)
|
|
1458
|
-
basis.push(w);
|
|
1459
|
-
}
|
|
1460
|
-
basis.skipped = skipped;
|
|
1461
|
-
return basis;
|
|
844
|
+
// What src/laws.js reads (see its contract): the device's structure, never the state.
|
|
845
|
+
_structure() {
|
|
846
|
+
const { model, n, M, z, nNodes, stretches, stretchOf, flatStretches, rough, surfCols, surfColOf, terms, floating } = this;
|
|
847
|
+
const { blockOfNode, blockOfFace, rxsIn, nodeMaterial, phiUndefined } = this;
|
|
848
|
+
return { model, n, M, z, nNodes, stretches, stretchOf, flatStretches, rough, surfCols, surfColOf, terms, floating, blockOfNode, blockOfFace, rxsIn, nodeMaterial, phiUndefined };
|
|
1462
849
|
}
|
|
1463
850
|
|
|
1464
|
-
// Every stretch either reaches a contact or is a
|
|
1465
|
-
// be solved directly (dt = ∞), with the
|
|
851
|
+
// Every stretch either reaches a contact or is kept by a law, and no law is left unkept: the
|
|
852
|
+
// steady equations can be solved directly (dt = ∞), with the laws in place of their rows. Else
|
|
853
|
+
// the steady solve goes by time steps, which keep every amount themselves.
|
|
1466
854
|
_directSteady() {
|
|
1467
|
-
return this.stretches.every((st) => st.connected || st.conserved);
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
// Conservation rows for the spectators (steady solves only): Σ v c_i = amount over the
|
|
1471
|
-
// stretch replaces the balance row of its first node, which in steady state is the negative
|
|
1472
|
-
// sum of the others. That row is dense, so it's kept aside: the factorised matrix gets a pin
|
|
1473
|
-
// (identity row) there instead, and _solveBordered restores the constraint.
|
|
1474
|
-
_applyConstraints() {
|
|
1475
|
-
const { M, res } = this;
|
|
1476
|
-
for (const cs of this.constraints) {
|
|
1477
|
-
const { amount, reference, dV } = this._lawAmount(cs);
|
|
1478
|
-
cs.res = amount - (cs.terminals.length > 0 ? (this.lawRefs.get(cs.key) ?? amount) : reference);
|
|
1479
|
-
if (cs.circuit >= 0) {
|
|
1480
|
-
// In place of the terminal's circuit row (its I = 0).
|
|
1481
|
-
const C = this.termC[cs.circuit];
|
|
1482
|
-
C.fill(0);
|
|
1483
|
-
for (let j = 0; j < cs.len; j++) C[cs.idx[j]] += cs.w[j];
|
|
1484
|
-
for (const { t, lambda } of cs.contacts) {
|
|
1485
|
-
// A contact's charge, D_in, as _contact had it, with its derivatives.
|
|
1486
|
-
const q = this.contactQ[t], f = lambda / FARADAY;
|
|
1487
|
-
cs.res += f * q.D;
|
|
1488
|
-
for (let j = 0; j < C.length; j++) C[j] += f * q.row[j];
|
|
1489
|
-
dV[t] += f * q.dV;
|
|
1490
|
-
}
|
|
1491
|
-
this.termRes[cs.circuit] = cs.res;
|
|
1492
|
-
this.termDI[cs.circuit] = dV[cs.circuit];
|
|
1493
|
-
dV.forEach((x, k) => k !== cs.circuit && (this.termDX[cs.circuit][k] = x));
|
|
1494
|
-
continue;
|
|
1495
|
-
}
|
|
1496
|
-
const b0 = Math.floor(cs.row / M), r0 = cs.row % M;
|
|
1497
|
-
this._replaceRow(b0, r0);
|
|
1498
|
-
this._j(b0, r0, b0, r0, 1);
|
|
1499
|
-
res[this.rix[cs.row]] = 0;
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
// A conserved combination's amount as the state holds it (and its reference, closed), with its
|
|
1504
|
-
// derivatives in cs.idx/w (and dV, by the voltage of a terminal whose electrode's charge it
|
|
1505
|
-
// counts).
|
|
1506
|
-
_lawAmount(cs) {
|
|
1507
|
-
const { n, M, c, dA: d } = this;
|
|
1508
|
-
const vol = this.model.grid.vol;
|
|
1509
|
-
let amount = 0, reference = 0, len = 0, gross = 0;
|
|
1510
|
-
for (const { stretch, w } of cs.parts) {
|
|
1511
|
-
const st = this.stretches[stretch], i = st.species;
|
|
1512
|
-
if (cs.terminals.length === 0) reference += w * this.referenceAmounts[stretch];
|
|
1513
|
-
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
1514
|
-
amount += w * vol[g] * c[g * n + i];
|
|
1515
|
-
gross += Math.abs(w * vol[g] * c[g * n + i]);
|
|
1516
|
-
if (this.nodeConductor[g] >= 0) {
|
|
1517
|
-
// A metal's carriers: the sheet at a charged face, vol·c = ±D_f·A/(zF), held by the
|
|
1518
|
-
// face's displacement.
|
|
1519
|
-
const f = this.sheetFace[g];
|
|
1520
|
-
if (f >= 0 && this.loc[this.blockOfFace[f] * M] >= 0) {
|
|
1521
|
-
cs.idx[len] = this.rix[this.blockOfFace[f] * M];
|
|
1522
|
-
cs.w[len++] = (w * this.sheetSign[g] * this.model.grid.area[g]) / (this.z[i] * FARADAY);
|
|
1523
|
-
}
|
|
1524
|
-
continue;
|
|
1525
|
-
}
|
|
1526
|
-
this._dc(g, i, d);
|
|
1527
|
-
const b = this.blockOfNode[g];
|
|
1528
|
-
for (let r = 0; r < M; r++) {
|
|
1529
|
-
if (d[r] === 0 || this.loc[b * M + r] < 0) continue;
|
|
1530
|
-
cs.idx[len] = this.rix[b * M + r];
|
|
1531
|
-
cs.w[len++] = w * vol[g] * d[r];
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
// What the electrodes' surfaces hold of it: Γ Σ v·a·θ, with ∂θ_s/∂η_x = θ_s (δ_sx − θ_x).
|
|
1536
|
-
for (const { col, w } of cs.surface) {
|
|
1537
|
-
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity, ns = port.surface.length;
|
|
1538
|
-
if (cs.terminals.length === 0) reference += w * this.surfaceRef[col];
|
|
1539
|
-
port.nodes.forEach((g, j) => {
|
|
1540
|
-
const f = w * G * vol[g] * this.portArea[k][j], b = this.blockOfNode[g], tq = this.th[g * this.nSurf + q];
|
|
1541
|
-
amount += f * tq;
|
|
1542
|
-
gross += Math.abs(f * tq);
|
|
1543
|
-
for (let x = 0; x < ns; x++) {
|
|
1544
|
-
cs.idx[len] = this.rix[b * M + 1 + n + x];
|
|
1545
|
-
cs.w[len++] = f * tq * ((q === x ? 1 : 0) - this.th[g * this.nSurf + x]);
|
|
1546
|
-
}
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
|
-
// What an electrode holds, its charge Q/(zF) on the port's capacitance (none without one),
|
|
1550
|
-
// Q = Σ vol·a·C (V − zeroCharge − φ) over its nodes (as _portCapacitance has it).
|
|
1551
|
-
const dV = cs.dV ?? (cs.dV = new Float64Array(this.terms.length));
|
|
1552
|
-
dV.fill(0);
|
|
1553
|
-
for (const { k, w } of cs.carriers) {
|
|
1554
|
-
const port = this.model.ports[k - 2];
|
|
1555
|
-
if (!port.capacitance) continue;
|
|
1556
|
-
const f = w / (this._carrierZ(k) * FARADAY), { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k];
|
|
1557
|
-
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
1558
|
-
port.nodes.forEach((g, j) => {
|
|
1559
|
-
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
1560
|
-
const b = this.blockOfNode[g], s = vol[g] * this.portArea[k - 2][j] * Cs;
|
|
1561
|
-
amount += f * s * (V - zeroCharge - this.VT * (this.u[b * M] + this.uLo[b * M]));
|
|
1562
|
-
gross += Math.abs(f * s * (V - zeroCharge - this.VT * (this.u[b * M] + this.uLo[b * M])));
|
|
1563
|
-
dV[k] += f * s;
|
|
1564
|
-
cs.idx[len] = this.rix[b * M];
|
|
1565
|
-
cs.w[len++] = -f * s * this.VT;
|
|
1566
|
-
});
|
|
1567
|
-
}
|
|
1568
|
-
cs.len = len;
|
|
1569
|
-
return { amount, reference, dV, gross }; // (dV by terminal)
|
|
855
|
+
return this.unkept.length === 0 && this.stretches.every((st) => st.connected || st.conserved);
|
|
1570
856
|
}
|
|
1571
857
|
|
|
1572
|
-
// The same amount at the step's start, as the storage terms have it (BDF2's c* included).
|
|
1573
|
-
_lawStart(cs) {
|
|
1574
|
-
const { n, nSurf } = this, vol = this.model.grid.vol;
|
|
1575
|
-
let a = 0;
|
|
1576
|
-
for (const { stretch, w } of cs.parts) {
|
|
1577
|
-
const st = this.stretches[stretch], i = st.species;
|
|
1578
|
-
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) a += w * vol[g] * this.cOld[g * n + i];
|
|
1579
|
-
}
|
|
1580
|
-
for (const { col, w } of cs.surface) {
|
|
1581
|
-
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity;
|
|
1582
|
-
port.nodes.forEach((g, j) => (a += w * G * vol[g] * this.portArea[k][j] * this.thOld[g * nSurf + q]));
|
|
1583
|
-
}
|
|
1584
|
-
for (const { k, w } of cs.carriers) if (this.model.ports[k - 2].capacitance) a += (w * this.portQStart[k - 2]) / (this._carrierZ(k) * FARADAY);
|
|
1585
|
-
return a;
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
// Time steps: a terminal driven by a current that alone feeds a conserved combination can read
|
|
1589
|
-
// its current as the combination's change, F (S − S*)/(λ dt), which the step's balance rows sum
|
|
1590
|
-
// to exactly, rather than as the flux through it. Both are exact; they differ in round-off and
|
|
1591
|
-
// in what Newton can see. Through a metal, the flux is G Δη, its two η all but equal: a floating
|
|
1592
|
-
// gate's charging current fell below that at steps of ~0.03 s, and the step came out singular.
|
|
1593
|
-
// Over long steps the change is the better kept (a closed host at open circuit, read by its
|
|
1594
|
-
// flux, drifted from its charge by 3 mV over 5e11 s, and its steps failed by 4e12 s). Over
|
|
1595
|
-
// short ones it's the amount's round-off over dt (a battery's cathode holds 0.04 mol/m² of
|
|
1596
|
-
// electrons: 1e-6 A/m² at steps of 1e-6 s, enough to reject steps at tol 1e-8), where the flux,
|
|
1597
|
-
// its levels carried in two words (u + uLo), is far cleaner than ε G. So a step reads the change
|
|
1598
|
-
// where that's ε·F·S/(λ dt) below 1e-6 of the flux's ε G, as its start has them, keeps to it
|
|
1599
|
-
// throughout, and where a step read by the flux fails, it's tried once more read by the change
|
|
1600
|
-
// (see step).
|
|
1601
|
-
_circuitLaws(dt) {
|
|
1602
|
-
for (const cs of [...this.constraints, ...this.timeLaws]) {
|
|
1603
|
-
const k = cs.circuit;
|
|
1604
|
-
if (k < 0 || !this.floating.includes(k) || cs.contacts?.length > 0) continue; // (a contact's charge: its own rows)
|
|
1605
|
-
const { amount, dV, gross } = this._lawAmount(cs), f = FARADAY / (cs.lambda * dt), C = this.termC[k];
|
|
1606
|
-
if (!this.lawForm.has(cs)) {
|
|
1607
|
-
let G = 0;
|
|
1608
|
-
for (let j = 0; j < C.length; j++) G = Math.max(G, Math.abs(C[j]));
|
|
1609
|
-
this.lawForm.set(cs, Math.abs(f) * gross < 1e-6 * G);
|
|
1610
|
-
}
|
|
1611
|
-
if (!this.lawForm.get(cs)) continue;
|
|
1612
|
-
C.fill(0);
|
|
1613
|
-
for (let j = 0; j < cs.len; j++) C[cs.idx[j]] += f * cs.w[j];
|
|
1614
|
-
const d = this.terms[k].drive, src = sourceAt(d.src, this.sourceTime, this.sourceBefore);
|
|
1615
|
-
this.termRes[k] = f * (amount - this._lawStart(cs)) - (d.kind === 'I' ? src : (src - this.termV[k]) / d.R);
|
|
1616
|
-
this.termDI[k] = f * dV[k] + (d.kind === 'I' ? 0 : 1 / d.R);
|
|
1617
|
-
dV.forEach((x, j) => j !== k && (this.termDX[k][j] = f * x));
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
// Solve the full Newton system, J δ = rhs, with the extra unknowns and rows that don't fit the
|
|
1622
|
-
// block-tridiagonal matrix T: the floating terminals' voltages (with their circuit rows), and
|
|
1623
|
-
// the rows pinned in T: the spectators' conserved amounts (see _applyConstraints) and the
|
|
1624
|
-
// islands' summed balances (see _applyIslands). By low-rank
|
|
1625
|
-
// updates of T (Woodbury): δ = y + Σ_q Q_q μ_q − Σ_k X_k δV_k with y = T⁻¹ rhs, Q_q = T⁻¹ e_q
|
|
1626
|
-
// (the response to a unit pin) and X_k = T⁻¹ B_k (to a unit change of V_k), then a small dense
|
|
1627
|
-
// system for the μ (pins) and δV (terminals):
|
|
1628
|
-
// pin q: Σ (W_q·Q_q′) μ_q′ − Σ (W_q·X_k) δV_k = res_q − W_q·y
|
|
1629
|
-
// terminal k: Σ (C_k·Q_q) μ_q + Σ (δ_kk′ ∂I_k/∂V_k − C_k·X_k′) δV_k′ = res_k − C_k·y
|
|
1630
|
-
_solveBordered(rhs, delta, deltaV, pins) {
|
|
1631
|
-
const N = this.sys.size, fl = this.floating, P = pins.length, K = fl.length, S = P + K;
|
|
1632
|
-
this._solveLinear(rhs, delta);
|
|
1633
|
-
if (S === 0) return;
|
|
1634
|
-
const e = this.dWork ?? (this.dWork = new Float64Array(N + 1));
|
|
1635
|
-
const cols = this.borderCols ?? (this.borderCols = []);
|
|
1636
|
-
for (let a = 0; a < S; a++) {
|
|
1637
|
-
if (!cols[a]) cols[a] = new Float64Array(N + 1);
|
|
1638
|
-
if (a < P) {
|
|
1639
|
-
// The response to a unit pin, or where the pin moves a flat stretch, to its shift.
|
|
1640
|
-
const col = pins[a].col ?? [pins[a].row];
|
|
1641
|
-
for (const o of col) e[this.rix[o]] = 1;
|
|
1642
|
-
this._solveLinear(e, cols[a]);
|
|
1643
|
-
for (const o of col) e[this.rix[o]] = 0;
|
|
1644
|
-
} else this._solveLinear(this.termB[fl[a - P]], cols[a]);
|
|
1645
|
-
}
|
|
1646
|
-
// Row a of the small system, as a dot product with a compact vector (sparse for pins).
|
|
1647
|
-
const dot = (a, v) => {
|
|
1648
|
-
let t = 0;
|
|
1649
|
-
if (a < P) {
|
|
1650
|
-
const p = pins[a];
|
|
1651
|
-
for (let j = 0; j < p.len; j++) t += p.w[j] * v[p.idx[j]];
|
|
1652
|
-
} else {
|
|
1653
|
-
const C = this.termC[fl[a - P]];
|
|
1654
|
-
for (let j = 0; j < N; j++) t += C[j] * v[j];
|
|
1655
|
-
}
|
|
1656
|
-
return t;
|
|
1657
|
-
};
|
|
1658
|
-
const A = Array.from({ length: S }, () => new Float64Array(S + 1));
|
|
1659
|
-
for (let a = 0; a < S; a++) {
|
|
1660
|
-
for (let b = 0; b < S; b++) A[a][b] = (b < P ? 1 : -1) * dot(a, cols[b]);
|
|
1661
|
-
if (a >= P) {
|
|
1662
|
-
A[a][a] += this.termDI[fl[a - P]];
|
|
1663
|
-
for (let b = P; b < S; b++) if (b !== a) A[a][b] += this.termDX[fl[a - P]][fl[b - P]];
|
|
1664
|
-
}
|
|
1665
|
-
A[a][S] = (a < P ? pins[a].res : this.termRes[fl[a - P]]) - dot(a, delta);
|
|
1666
|
-
let mx = 0;
|
|
1667
|
-
for (let b = 0; b < S; b++) mx = Math.max(mx, Math.abs(A[a][b]));
|
|
1668
|
-
if (mx > 0) for (let b = 0; b <= S; b++) A[a][b] /= mx;
|
|
1669
|
-
}
|
|
1670
|
-
// Small dense solve with partial pivoting.
|
|
1671
|
-
for (let col = 0; col < S; col++) {
|
|
1672
|
-
let p = col;
|
|
1673
|
-
for (let r = col + 1; r < S; r++) if (Math.abs(A[r][col]) > Math.abs(A[p][col])) p = r;
|
|
1674
|
-
[A[col], A[p]] = [A[p], A[col]];
|
|
1675
|
-
if (A[col][col] === 0) throw new SolverError('the bordered system is singular: a conserved amount or a floating terminal is not determined');
|
|
1676
|
-
for (let r = col + 1; r < S; r++) {
|
|
1677
|
-
const f = A[r][col] / A[col][col];
|
|
1678
|
-
for (let k = col; k <= S; k++) A[r][k] -= f * A[col][k];
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
const x = new Float64Array(S);
|
|
1682
|
-
for (let r = S - 1; r >= 0; r--) {
|
|
1683
|
-
let v = A[r][S];
|
|
1684
|
-
for (let k = r + 1; k < S; k++) v -= A[r][k] * x[k];
|
|
1685
|
-
x[r] = v / A[r][r];
|
|
1686
|
-
}
|
|
1687
|
-
for (let a = 0; a < S; a++) {
|
|
1688
|
-
const sa = a < P ? x[a] : -x[a], col = cols[a];
|
|
1689
|
-
for (let j = 0; j < N; j++) delta[j] += sa * col[j];
|
|
1690
|
-
if (a >= P) deltaV[a - P] = x[a];
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
858
|
// An electrode surface at its starting coverages at node g: η = μ°/RT + ln(θ/θ₀).
|
|
1696
859
|
_surfaceStart(g) {
|
|
1697
860
|
const { n, M, u, model } = this, surf = model.ports[this.surfPort[g]].surface, b = this.blockOfNode[g];
|
|
@@ -1724,7 +887,7 @@ export class Solver {
|
|
|
1724
887
|
this.floating = this.terms.flatMap((t, k) => (t.drive.kind === 'I' || t.drive.R > 0 ? [k] : []));
|
|
1725
888
|
if (this.floating.length !== before.length) this.deltaV = null;
|
|
1726
889
|
// A terminal newly driven by a current (or no longer) can close a combination behind it.
|
|
1727
|
-
// (and one put behind a resistance, or taken from it, its time steps' reading: see
|
|
890
|
+
// (and one put behind a resistance, or taken from it, its time steps' reading: see src/laws.js)
|
|
1728
891
|
this.drivenKey = this._drivenKey();
|
|
1729
892
|
if (this.drivenKey !== driven) this._laws();
|
|
1730
893
|
this._refreshSources();
|
|
@@ -1732,17 +895,17 @@ export class Solver {
|
|
|
1732
895
|
}
|
|
1733
896
|
|
|
1734
897
|
// After a steady solve: each reaction that alone changes some combination nothing else feeds
|
|
1735
|
-
// (see
|
|
898
|
+
// (see src/laws.js), whose total rate a steady state therefore sets to zero, but whose affinity has one
|
|
1736
899
|
// sign wherever it acts, so that its total can't be zero. Too slow to show, its rate lost to
|
|
1737
900
|
// round-off, it was left where the solve began: a slow complexation 43 mV off, an electrode
|
|
1738
901
|
// port's slow couple at its starting composition, each converged without a word. (A face
|
|
1739
902
|
// reaction stands on its affinity, so it's only caught here if that couldn't be arranged.)
|
|
1740
903
|
stuckReactions() {
|
|
1741
904
|
const { n, M, u, uLo, model } = this, grid = model.grid, S = this.stretches.length, X = S + this.surfCols.length, out = [];
|
|
1742
|
-
const passes = (k) => !
|
|
1743
|
-
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) &&
|
|
905
|
+
const passes = (k) => !noCurrent(this.terms[k]);
|
|
906
|
+
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) && feeds(j, this.stretches[k])))));
|
|
1744
907
|
const needed = (rx) => {
|
|
1745
|
-
const basis = this.
|
|
908
|
+
const basis = conservedMoieties(this._structure(), { drives: true, skip: rx });
|
|
1746
909
|
return basis.some((w) => !fed(w) && basis.skipped.some((row) => Math.abs(row.reduce((t, x, k) => t + x * w[k], 0)) > 1e-9));
|
|
1747
910
|
};
|
|
1748
911
|
const eta = (g, slot) => u[this.blockOfNode[g] * M + slot] + uLo[this.blockOfNode[g] * M + slot];
|
|
@@ -1775,15 +938,11 @@ export class Solver {
|
|
|
1775
938
|
return out;
|
|
1776
939
|
}
|
|
1777
940
|
|
|
1778
|
-
// How each terminal is driven, as far as the laws care (see
|
|
941
|
+
// How each terminal is driven, as far as the laws care (see src/laws.js): by a voltage or a current,
|
|
1779
942
|
// behind a resistance or not, and a current that's held at zero throughout (which feeds a
|
|
1780
943
|
// combination nothing, so a reaction's affinity may stand in for its summed balance).
|
|
1781
944
|
_drivenKey() {
|
|
1782
|
-
return this.terms.map((t) => `${t.drive.kind}${t.drive.R > 0 ? 'R' : ''}${
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
_noCurrent(t) {
|
|
1786
|
-
return t.drive.kind === 'I' && !(t.drive.R > 0) && t.drive.src.value === 0;
|
|
945
|
+
return this.terms.map((t) => `${t.drive.kind}${t.drive.R > 0 ? 'R' : ''}${noCurrent(t) ? '0' : ''}`).join();
|
|
1787
946
|
}
|
|
1788
947
|
|
|
1789
948
|
// Held terminals take their sources' values at sourceTime (a floating one keeps its voltage,
|
|
@@ -2290,7 +1449,7 @@ export class Solver {
|
|
|
2290
1449
|
res.fill(0);
|
|
2291
1450
|
this.dtNow = dt;
|
|
2292
1451
|
if (this.lin) this.lin.reset(1 / dt);
|
|
2293
|
-
if (this.steady && dt === Infinity) this
|
|
1452
|
+
if (this.steady && dt === Infinity) snapFlat(this);
|
|
2294
1453
|
this.computeConcentrations();
|
|
2295
1454
|
|
|
2296
1455
|
// Regions, each assembled by the kernel for its kind: a conductor (its carrier only), a
|
|
@@ -2322,16 +1481,9 @@ export class Solver {
|
|
|
2322
1481
|
// Terminals: ports (after every other term at their nodes, so a held level can read its
|
|
2323
1482
|
// flux), then contacts, then the circuit rows of the floating ones.
|
|
2324
1483
|
this._terminals(dt);
|
|
2325
|
-
|
|
2326
|
-
//
|
|
2327
|
-
|
|
2328
|
-
const steady = this.steady && dt === Infinity;
|
|
2329
|
-
if (steady) this._flatRows();
|
|
2330
|
-
if (steady && this.affinities.length > 0) this._applyAffinities();
|
|
2331
|
-
if (steady && this.constraints.length > 0) this._applyConstraints();
|
|
2332
|
-
if (steady && this.localConstraints.length > 0) this._applyLocalConstraints();
|
|
2333
|
-
this.pinnedIslands = steady && this.islandsOn ? this._applyIslands() : [];
|
|
2334
|
-
this.pins = steady ? [...this.constraints.filter((cs) => cs.circuit < 0), ...this.affinities, ...this.pinnedIslands] : [];
|
|
1484
|
+
// The laws' rows (src/lawRows.js): a time step's circuit laws; a steady system's levels held
|
|
1485
|
+
// flat, affinities, conserved amounts, islands' balances, and the pins held aside of them.
|
|
1486
|
+
applyLaws(this, dt);
|
|
2335
1487
|
this.transformed = this.combining && dt !== Infinity;
|
|
2336
1488
|
if (this.transformed) this._chargeRows(dt);
|
|
2337
1489
|
}
|
|
@@ -3390,7 +2542,7 @@ export class Solver {
|
|
|
3390
2542
|
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
3391
2543
|
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
3392
2544
|
// Its current, the charging (Q − Q at the step's start)/dt; or, steady at no current, the
|
|
3393
|
-
// charge it keeps in place of that (see
|
|
2545
|
+
// charge it keeps in place of that (see captureLaws in src/lawRows.js).
|
|
3394
2546
|
const rate = Number.isFinite(dt) ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
3395
2547
|
let Q = 0, dQdV = 0;
|
|
3396
2548
|
port.nodes.forEach((g, w) => {
|
|
@@ -3522,7 +2674,8 @@ export class Solver {
|
|
|
3522
2674
|
// φ law (none where φ is undefined: nothing at the end node responds to it). The displacement
|
|
3523
2675
|
// into the device, D_in = sgn·D, adds (D_in − D_in at the step's start)/dt to the current.
|
|
3524
2676
|
const link = ct.phi, Dstart = sgn * this.contactDStart[side];
|
|
3525
|
-
// (In a steady solve, what a law in this circuit row counts of D_in: see
|
|
2677
|
+
// (In a steady solve, what a law in this circuit row counts of D_in: see applyConstraints in
|
|
2678
|
+
// src/lawRows.js.)
|
|
3526
2679
|
const q = !dyn && this.steady && this.contactLaw[k] ? (this.contactQ[k] ??= { row: new Float64Array(this.sys.size + 1), D: 0, dV: 0 }) : null;
|
|
3527
2680
|
if (q) {
|
|
3528
2681
|
q.row.fill(0);
|
|
@@ -3540,7 +2693,7 @@ export class Solver {
|
|
|
3540
2693
|
this.contactD[side] = sgn * Din;
|
|
3541
2694
|
if (q && this.loc[b * M] >= 0) [q.D, q.row[R[b * M]], q.dV] = [Din, -Ac * link.C * VT, Ac * link.C];
|
|
3542
2695
|
// Its current, the charging (D_in − D_in at the step's start)/dt; or, steady at no current,
|
|
3543
|
-
// the charge it keeps in place of that (see
|
|
2696
|
+
// the charge it keeps in place of that (see captureLaws in src/lawRows.js).
|
|
3544
2697
|
const rate = dyn ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
3545
2698
|
if (rate > 0) {
|
|
3546
2699
|
I += (Din - (dyn ? Dstart : this.chargeRefs.get(k))) * rate;
|
|
@@ -3591,92 +2744,6 @@ export class Solver {
|
|
|
3591
2744
|
|
|
3592
2745
|
|
|
3593
2746
|
|
|
3594
|
-
// Steady solves: each flat stretch's level held at its contact's throughout (see
|
|
3595
|
-
// flatStretches), in place of its balances, as an equilibrium link holds it at the end node.
|
|
3596
|
-
// A flat stretch's level: its contact's, through the terminal k that holds it, or (reaching
|
|
3597
|
-
// none) its first node's, which its amount fixes (that row is the amount's, a pin whose
|
|
3598
|
-
// response moves the whole stretch: see _applyConstraints).
|
|
3599
|
-
_flatLevel(st) {
|
|
3600
|
-
const { M, u, uLo, z, VT, model } = this, i = st.species;
|
|
3601
|
-
if (st.leftOpen || st.rightOpen) {
|
|
3602
|
-
const side = st.leftOpen ? 'left' : 'right', link = model.contacts[side].species[i], k = side === 'left' ? 0 : 1;
|
|
3603
|
-
return { k, hi: z[i] === 0 ? link.mu / model.RT : (z[i] * (this.termV[k] + link.offset)) / VT, lo: 0 };
|
|
3604
|
-
}
|
|
3605
|
-
const o = this.blockOfNode[st.nodes[0]] * M + 1 + i;
|
|
3606
|
-
return { k: -1, hi: u[o], lo: uLo[o] };
|
|
3607
|
-
}
|
|
3608
|
-
|
|
3609
|
-
// Each flat stretch's nodes set to its first node's level exactly (where Newton's damped
|
|
3610
|
-
// updates have taken it, on the way to its contact's), so that it carries exactly no flux (the
|
|
3611
|
-
// expm1 form of a zero difference). Flat only to round-off, a level moved by its contact had
|
|
3612
|
-
// read as a current: a MOS capacitor's holes, their contact jumped to −6 V, as
|
|
3613
|
-
// 3e-4 A/m² through a bulk that conducts ~1e10 S/m² (the true current was 3.5e-7).
|
|
3614
|
-
_snapFlat() {
|
|
3615
|
-
const { M, u, uLo } = this;
|
|
3616
|
-
for (const st of this.flatStretches) {
|
|
3617
|
-
const f = this.blockOfNode[st.nodes[0]] * M + 1 + st.species, hi = u[f], lo = uLo[f];
|
|
3618
|
-
for (let g = st.nodes[0] + 1; g <= st.nodes[1]; g++) {
|
|
3619
|
-
const o = this.blockOfNode[g] * M + 1 + st.species;
|
|
3620
|
-
if (this.loc[o] < 0) continue;
|
|
3621
|
-
u[o] = hi;
|
|
3622
|
-
uLo[o] = lo;
|
|
3623
|
-
}
|
|
3624
|
-
}
|
|
3625
|
-
}
|
|
3626
|
-
|
|
3627
|
-
_flatRows() {
|
|
3628
|
-
const { M, u, uLo, res, z, VT, model } = this, R = this.rix;
|
|
3629
|
-
for (const st of this.flatStretches) {
|
|
3630
|
-
const i = st.species, open = st.leftOpen || st.rightOpen;
|
|
3631
|
-
const { k, hi, lo } = this._flatLevel(st);
|
|
3632
|
-
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
3633
|
-
const b = this.blockOfNode[g], o = b * M + 1 + i;
|
|
3634
|
-
if (this.loc[o] < 0) continue;
|
|
3635
|
-
this._replaceRow(b, 1 + i);
|
|
3636
|
-
for (const B of this.termB) B[R[o]] = 0; // (the row's old terminal terms: the end node's own link)
|
|
3637
|
-
this._j(b, 1 + i, b, 1 + i, 1);
|
|
3638
|
-
res[R[o]] = u[o] - hi + (uLo[o] - lo); // (the big terms first: hi + lo rounds to hi)
|
|
3639
|
-
if (open && z[i] !== 0) this.termB[k][R[o]] += -z[i] / VT;
|
|
3640
|
-
}
|
|
3641
|
-
// A face inside the stretch whose link holds the level continuous carries the species' flux
|
|
3642
|
-
// as an unknown that only the edge balances, now replaced, determined: it's zero.
|
|
3643
|
-
for (let f = st.regions[0]; f < st.regions[1]; f++) {
|
|
3644
|
-
if (model.interfaces[f].links[i].type !== 'equilibrium') continue;
|
|
3645
|
-
const b = this.blockOfFace[f], o = b * M + 1 + i;
|
|
3646
|
-
if (this.loc[o] < 0) continue;
|
|
3647
|
-
this._replaceRow(b, 1 + i);
|
|
3648
|
-
for (const B of this.termB) B[R[o]] = 0;
|
|
3649
|
-
this._j(b, 1 + i, b, 1 + i, 1);
|
|
3650
|
-
res[R[o]] = u[o] + uLo[o];
|
|
3651
|
-
}
|
|
3652
|
-
}
|
|
3653
|
-
}
|
|
3654
|
-
|
|
3655
|
-
// Steady solves: each reaction that alone changes a combination (see _laws) held at its
|
|
3656
|
-
// equilibrium, a = fixedA − Σ ν η = 0, in place of one of that combination's balance rows (a pin,
|
|
3657
|
-
// like an amount's).
|
|
3658
|
-
_applyAffinities() {
|
|
3659
|
-
const { M, u, uLo, res } = this, grid = this.model.grid;
|
|
3660
|
-
for (const af of this.affinities) {
|
|
3661
|
-
const b0 = Math.floor(af.row / M), r0 = af.row % M;
|
|
3662
|
-
this._replaceRow(b0, r0);
|
|
3663
|
-
this._j(b0, r0, b0, r0, 1);
|
|
3664
|
-
res[this.rix[af.row]] = 0;
|
|
3665
|
-
const gL = grid.regionEnd[af.f], gR = grid.regionStart[af.f + 1];
|
|
3666
|
-
let hi = af.rx.fixedA, lo = 0, len = 0;
|
|
3667
|
-
for (const p of af.rx.part) {
|
|
3668
|
-
const o = this.blockOfNode[p.side ? gR : gL] * M + 1 + p.i;
|
|
3669
|
-
hi -= p.nu * u[o];
|
|
3670
|
-
lo -= p.nu * uLo[o];
|
|
3671
|
-
if (this.loc[o] < 0) continue;
|
|
3672
|
-
af.idx[len] = this.rix[o];
|
|
3673
|
-
af.w[len++] = -p.nu;
|
|
3674
|
-
}
|
|
3675
|
-
af.len = len;
|
|
3676
|
-
af.res = hi + lo;
|
|
3677
|
-
}
|
|
3678
|
-
}
|
|
3679
|
-
|
|
3680
2747
|
// Zero one row of the Jacobian (all three blocks), ready to be replaced.
|
|
3681
2748
|
_replaceRow(b, rs) {
|
|
3682
2749
|
const l = this.loc[b * this.M + rs];
|
|
@@ -3741,7 +2808,7 @@ export class Solver {
|
|
|
3741
2808
|
// entries; where they're huge (an inversion layer's 0.1 nm cells, a conductance ~1e11 against
|
|
3742
2809
|
// the layer's own storage and the trickle from the bulk), eliminating them loses the layer's
|
|
3743
2810
|
// overall level to round-off, and Newton rattles there. delta and deltaV come in as the plain
|
|
3744
|
-
// solve's and leave refined (in the transformed unknowns, as
|
|
2811
|
+
// solve's and leave refined (in the transformed unknowns, as solveBordered gives them); false
|
|
3745
2812
|
// if refining didn't help, leaving them as they came.
|
|
3746
2813
|
_refine(dt, delta, deltaV) {
|
|
3747
2814
|
const { res, termRes, termB, termC, termDI, rowDiv, floating: fl } = this;
|
|
@@ -3768,7 +2835,7 @@ export class Solver {
|
|
|
3768
2835
|
rest.multiply(v, jv);
|
|
3769
2836
|
if (save.transformed) this._untransform(v); // (the dilute terms in the plain unknowns)
|
|
3770
2837
|
lin.apply(v, jv, 1, 1 / dt);
|
|
3771
|
-
// A flat stretch's rows hold each level to its first node's (see
|
|
2838
|
+
// A flat stretch's rows hold each level to its first node's (see flatRows in src/lawRows.js).
|
|
3772
2839
|
for (const p of pins) {
|
|
3773
2840
|
if (!p.col) continue;
|
|
3774
2841
|
for (const o of p.col) if (o !== p.row && this.loc[o] >= 0) jv[R[o]] -= x[R[p.row]];
|
|
@@ -3801,7 +2868,7 @@ export class Solver {
|
|
|
3801
2868
|
p.res = rhs[R[p.row]];
|
|
3802
2869
|
rhs[R[p.row]] = 0;
|
|
3803
2870
|
}
|
|
3804
|
-
this
|
|
2871
|
+
solveBordered(this, rhs, out, outV, pins);
|
|
3805
2872
|
const z = new Float64Array(N + K);
|
|
3806
2873
|
for (let j = 0; j < N; j++) z[j] = out[j];
|
|
3807
2874
|
for (let a = 0; a < K; a++) z[N + a] = outV[a];
|
|
@@ -3863,7 +2930,8 @@ export class Solver {
|
|
|
3863
2930
|
const history = [], ownHistory = [];
|
|
3864
2931
|
// Refined solves (see _refine), once the plain ones stall.
|
|
3865
2932
|
let refine = false, refined = 0, noRefine = false;
|
|
3866
|
-
// Islands (see
|
|
2933
|
+
// Islands (see applyIslands in src/lawRows.js): their summed rows go in only once the plain
|
|
2934
|
+
// solves converge.
|
|
3867
2935
|
// Far from the solution, Newton exact along a weakly held level takes it by 1e5 thermal
|
|
3868
2936
|
// units (a floating base, cold at bias), where the plain solves, which barely see that level,
|
|
3869
2937
|
// converge. Converged, an island at the wrong level shows: it takes in a different current
|
|
@@ -3943,7 +3011,7 @@ export class Solver {
|
|
|
3943
3011
|
try {
|
|
3944
3012
|
const pins = this.pins;
|
|
3945
3013
|
deltaV.fill(0);
|
|
3946
|
-
this
|
|
3014
|
+
solveBordered(this, res, delta, deltaV, pins);
|
|
3947
3015
|
if (refine && !this._refine(dt, delta, deltaV)) {
|
|
3948
3016
|
refine = false; // (GMRES didn't converge, or strayed: the plain solves it is)
|
|
3949
3017
|
noRefine = true;
|
|
@@ -4135,7 +3203,7 @@ export class Solver {
|
|
|
4135
3203
|
this.sourceTime = this.steady ? this.steadyAt : tEnd;
|
|
4136
3204
|
this.sourceBefore = !this.steady;
|
|
4137
3205
|
this.combining = true;
|
|
4138
|
-
this.lawForm = new Map(); // (see
|
|
3206
|
+
this.lawForm = new Map(); // (see circuitLaws in src/lawRows.js)
|
|
4139
3207
|
let result;
|
|
4140
3208
|
try {
|
|
4141
3209
|
result = this.newton(dtEff, opts);
|
|
@@ -4152,7 +3220,8 @@ export class Solver {
|
|
|
4152
3220
|
}
|
|
4153
3221
|
if (!result.converged && Number.isFinite(dtEff) && [...this.lawForm.values()].some((v) => !v)) {
|
|
4154
3222
|
// A terminal read by its flux whose response is lost to round-off (a floating gate's
|
|
4155
|
-
// charging, below its metal's G Δη): once more, read by the change (see
|
|
3223
|
+
// charging, below its metal's G Δη): once more, read by the change (see circuitLaws in
|
|
3224
|
+
// src/lawRows.js).
|
|
4156
3225
|
this.u.set(this.uPrev);
|
|
4157
3226
|
this.uLo.set(this.uPrevLo);
|
|
4158
3227
|
this.termV.set(this.termVPrev);
|
|
@@ -5180,7 +4249,8 @@ export class Solver {
|
|
|
5180
4249
|
const last = this.model.regions.length - 1;
|
|
5181
4250
|
const { n, cOld } = this, vol = this.model.grid.vol;
|
|
5182
4251
|
// A stretch alone in what a terminal driven by a current feeds takes in what that current
|
|
5183
|
-
// says, which the step kept exactly (see
|
|
4252
|
+
// says, which the step kept exactly (see circuitLaws in src/lawRows.js); the flux through a
|
|
4253
|
+
// metal is round-off.
|
|
5184
4254
|
const driven = new Map();
|
|
5185
4255
|
for (const cs of this.constraints) {
|
|
5186
4256
|
if (cs.circuit < 0 || this.terms[cs.circuit].drive.kind !== 'I' || cs.parts.length !== 1 || cs.surface.length || cs.carriers.length) continue;
|
|
@@ -5225,7 +4295,7 @@ export class Solver {
|
|
|
5225
4295
|
* Steady state: where a transient from the present state ends, found as the solution of the
|
|
5226
4296
|
* steady equations (dt = ∞, no storage term), with no slow modes to wait out, however slow the
|
|
5227
4297
|
* physics (e.g. exponentially scarce minority carriers). What the storage term held is held by
|
|
5228
|
-
* the steady system's own rows instead (see
|
|
4298
|
+
* the steady system's own rows instead (see src/laws.js): each conserved amount in place of a balance
|
|
5229
4299
|
* row that says nothing new, levels that carry nothing held flat, islands' summed balances.
|
|
5230
4300
|
* Where Newton from here fails: continuation along the drives, then the pseudo-transient ramp.
|
|
5231
4301
|
* The clock is not advanced, and open-system conservation bookkeeping restarts here.
|
|
@@ -5235,7 +4305,7 @@ export class Solver {
|
|
|
5235
4305
|
solveSteady(opts = {}) {
|
|
5236
4306
|
this.unreached = null; // (a driven current no held voltage reaches: see _floatingContinuation)
|
|
5237
4307
|
this.charging = null; // (a driven current that only charges: see _solveSteadyAll)
|
|
5238
|
-
this
|
|
4308
|
+
captureLaws(this);
|
|
5239
4309
|
this.steady = true; // (the steady system's rows; sources at the present time)
|
|
5240
4310
|
this.steadyAt = this.time; // (which a pseudo-transient's steps don't move on)
|
|
5241
4311
|
try {
|
|
@@ -5254,7 +4324,7 @@ export class Solver {
|
|
|
5254
4324
|
const I = sourceAt(this.terms[k].drive.src, this.time);
|
|
5255
4325
|
if (this.terms[k].drive.kind !== 'I' || I === 0) continue;
|
|
5256
4326
|
const laws = this.constraints.filter((cs) => cs.terminals.includes(k));
|
|
5257
|
-
if (laws.length === 0 && !this
|
|
4327
|
+
if (laws.length === 0 && !chargeOnly(this, k)) continue;
|
|
5258
4328
|
const what = laws.flatMap((cs) => cs.parts.map(({ stretch }) => {
|
|
5259
4329
|
const st = this.stretches[stretch], [r0, r1] = st.regions;
|
|
5260
4330
|
return `${species[st.species].name} in ${regions[r0].name}${r1 > r0 ? `…${regions[r1].name}` : ''}`;
|
|
@@ -5274,7 +4344,7 @@ export class Solver {
|
|
|
5274
4344
|
_steadyFromHere(opts) {
|
|
5275
4345
|
this.conditioning = null;
|
|
5276
4346
|
this.computeConcentrations();
|
|
5277
|
-
this
|
|
4347
|
+
captureLocal(this); // immobile combinations keep, node by node, what they hold now
|
|
5278
4348
|
this.sourceTime = this.time;
|
|
5279
4349
|
// Continuation applies where the right terminal is held at a voltage, against a held left one.
|
|
5280
4350
|
const [dl, dr] = [this.terms[0].drive, this.terms[1].drive];
|
|
@@ -5370,20 +4440,7 @@ export class Solver {
|
|
|
5370
4440
|
// only charges a capacitance (so that its steady states are one per amount or charge, not one
|
|
5371
4441
|
// per voltage).
|
|
5372
4442
|
_bound(k) {
|
|
5373
|
-
return this.constraints.some((cs) => cs.terminals.includes(k)) || this
|
|
5374
|
-
}
|
|
5375
|
-
|
|
5376
|
-
// Whether terminal k passes current only by charging a capacitance: no charged species through
|
|
5377
|
-
// it (nor, a port, reactions), and a capacitance to charge (a gate's φ law, a port's own).
|
|
5378
|
-
_chargeOnly(k) {
|
|
5379
|
-
const t = this.terms[k], { species } = this.model;
|
|
5380
|
-
const ions = (links) => links.some((l, i) => l.type !== 'blocked' && species[i].z !== 0);
|
|
5381
|
-
if (t.kind === 'port') {
|
|
5382
|
-
const p = this.model.ports[t.index];
|
|
5383
|
-
return p.reactions.length === 0 && !ions(p.species) && !!p.capacitance;
|
|
5384
|
-
}
|
|
5385
|
-
const c = this.model.contacts[t.side];
|
|
5386
|
-
return !ions(c.species) && (c.phi.type === 'capacitive' || c.phi.type === 'pinned');
|
|
4443
|
+
return this.constraints.some((cs) => cs.terminals.includes(k)) || chargeOnly(this, k);
|
|
5387
4444
|
}
|
|
5388
4445
|
|
|
5389
4446
|
// A cold steady solve with a terminal driven by a current, which can fail where the same state
|