driftlet 1.2.1 → 1.2.3
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/device.md +5 -0
- package/docs/internals.md +93 -0
- package/docs/numerics.md +12 -1
- package/docs/reliability.md +53 -4
- package/llms.txt +6 -6
- package/package.json +2 -1
- package/src/lawRows.js +564 -0
- package/src/laws.js +693 -0
- package/src/solver.js +110 -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
842
|
}
|
|
1198
843
|
|
|
1199
|
-
// What
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
}
|
|
1284
|
-
|
|
1285
|
-
// Row o's Jacobian entries, as add(compact column, value): the assembled ones, and the dilute
|
|
1286
|
-
// kernels' kept aside in difference form (see _assembleDifference) when they are.
|
|
1287
|
-
_rowInto(o, add) {
|
|
1288
|
-
const { sys, M, nB } = this, b = Math.floor(o / M), l = this.loc[o];
|
|
1289
|
-
const { A, B, C, sizes, offA, offB, offC, offX } = sys, m = sizes[b];
|
|
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,
|
|
@@ -1993,6 +1152,53 @@ export class Solver {
|
|
|
1993
1152
|
}
|
|
1994
1153
|
}
|
|
1995
1154
|
}
|
|
1155
|
+
// Two strictly neutral regions meeting at a face whose links hold ions in equilibrium: where
|
|
1156
|
+
// c0 leaves the two sides out of it (a different fixed charge or immobile ion on each, so
|
|
1157
|
+
// c₊c₋ differs across it), the face reaches it at t = 0⁺, before anything diffuses, and a
|
|
1158
|
+
// first step made to take that jump met an exactly singular pivot. Start there: the ions the
|
|
1159
|
+
// face passes shared between its two nodes in equilibrium (μ̄ equal across it), each one's
|
|
1160
|
+
// amount over the two nodes kept, everything else as c0 has it. The one unknown is the jump
|
|
1161
|
+
// in φ̂ across it (a Donnan potential), set by neutrality on one side; the other's follows,
|
|
1162
|
+
// the two nodes' charge summing to what it was, zero.
|
|
1163
|
+
for (let f = 0; f < interfaces.length; f++) {
|
|
1164
|
+
const itf = interfaces[f], mL = materials[regions[f].material], mR = materials[regions[f + 1].material];
|
|
1165
|
+
const strict = (m) => m.epsr === 0 && !m.conductor && !m.phiFree && m.ideal;
|
|
1166
|
+
if (itf.phi.type !== 'neutral' || !strict(mL) || !strict(mR)) continue;
|
|
1167
|
+
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1], bL = this.blockOfNode[gL] * M, bR = this.blockOfNode[gR] * M;
|
|
1168
|
+
const pass = [];
|
|
1169
|
+
for (let i = 0; i < n; i++) if (itf.links[i].type === 'equilibrium' && mL.present[i] && mR.present[i]) pass.push(i);
|
|
1170
|
+
if (!pass.some((i) => z[i] !== 0)) continue;
|
|
1171
|
+
const RT = model.RT, vL = grid.vol[gL], vR = grid.vol[gR];
|
|
1172
|
+
const conc = (m, b, i) => m.cRef[i] * Math.exp(u[b + 1 + i] - m.mu0[i] / RT - z[i] * u[b]);
|
|
1173
|
+
const amount = pass.map((i) => vL * conc(mL, bL, i) + vR * conc(mR, bR, i));
|
|
1174
|
+
let qOther = regions[f].fixedCharge / FARADAY; // (the left node's charge from all it doesn't share)
|
|
1175
|
+
for (let i = 0; i < n; i++) if (mL.present[i] && !pass.includes(i) && z[i] !== 0) qOther += z[i] * conc(mL, bL, i);
|
|
1176
|
+
// At a jump D = φ̂_R − φ̂_L: c_L/c_R = (cRef_L/cRef_R)·exp((μ°_R − μ°_L)/RT + z D), the amount fixing both.
|
|
1177
|
+
const split = (D) =>
|
|
1178
|
+
pass.map((i, j) => {
|
|
1179
|
+
const K = (mL.cRef[i] / mR.cRef[i]) * Math.exp((mR.mu0[i] - mL.mu0[i]) / RT + z[i] * D), cR = amount[j] / (vL * K + vR);
|
|
1180
|
+
return [K * cR, cR];
|
|
1181
|
+
});
|
|
1182
|
+
const qL = (D) => split(D).reduce((q, [cL], j) => q + z[pass[j]] * cL, qOther); // (increasing in D)
|
|
1183
|
+
const D0 = u[bR] - u[bL];
|
|
1184
|
+
let lo = D0 - 1, hi = D0 + 1;
|
|
1185
|
+
while (qL(lo) > 0 && lo > D0 - 1e3) lo -= 2 * (hi - lo);
|
|
1186
|
+
while (qL(hi) < 0 && hi < D0 + 1e3) hi += 2 * (hi - lo);
|
|
1187
|
+
if (!(qL(lo) <= 0 && qL(hi) >= 0)) continue; // (nothing the face shares can balance it: leave c0)
|
|
1188
|
+
for (let it = 0; it < 200 && hi - lo > 1e-13 * Math.max(1, Math.abs(D0)); it++) {
|
|
1189
|
+
const m = 0.5 * (lo + hi);
|
|
1190
|
+
if (qL(m) < 0) lo = m;
|
|
1191
|
+
else hi = m;
|
|
1192
|
+
}
|
|
1193
|
+
const D = 0.5 * (lo + hi), cs = split(D);
|
|
1194
|
+
const keep = Float64Array.from({ length: n }, (_, i) => (mR.present[i] ? conc(mR, bR, i) : 0)); // (the right node's, before φ̂ moves)
|
|
1195
|
+
u[bR] = u[bL] + D;
|
|
1196
|
+
for (let i = 0; i < n; i++) if (mR.present[i] && !pass.includes(i)) u[bR + 1 + i] = Math.log(keep[i] / mR.cRef[i]) + mR.mu0[i] / RT + z[i] * u[bR];
|
|
1197
|
+
pass.forEach((i, j) => {
|
|
1198
|
+
u[bL + 1 + i] = Math.log(cs[j][0] / mL.cRef[i]) + mL.mu0[i] / RT + z[i] * u[bL];
|
|
1199
|
+
u[bR + 1 + i] = Math.log(cs[j][1] / mR.cRef[i]) + mR.mu0[i] / RT + z[i] * u[bR];
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1996
1202
|
// An insertion host's φ is a gauge: c0 sets only its neutral combination's level, not how that
|
|
1997
1203
|
// splits between its ion and its electrons. Start its ion level with the same ion's beside it
|
|
1998
1204
|
// across a face (the equilibrium a link or a transfer reaction heads for), so its electrons,
|
|
@@ -2290,7 +1496,7 @@ export class Solver {
|
|
|
2290
1496
|
res.fill(0);
|
|
2291
1497
|
this.dtNow = dt;
|
|
2292
1498
|
if (this.lin) this.lin.reset(1 / dt);
|
|
2293
|
-
if (this.steady && dt === Infinity) this
|
|
1499
|
+
if (this.steady && dt === Infinity) snapFlat(this);
|
|
2294
1500
|
this.computeConcentrations();
|
|
2295
1501
|
|
|
2296
1502
|
// Regions, each assembled by the kernel for its kind: a conductor (its carrier only), a
|
|
@@ -2322,16 +1528,9 @@ export class Solver {
|
|
|
2322
1528
|
// Terminals: ports (after every other term at their nodes, so a held level can read its
|
|
2323
1529
|
// flux), then contacts, then the circuit rows of the floating ones.
|
|
2324
1530
|
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] : [];
|
|
1531
|
+
// The laws' rows (src/lawRows.js): a time step's circuit laws; a steady system's levels held
|
|
1532
|
+
// flat, affinities, conserved amounts, islands' balances, and the pins held aside of them.
|
|
1533
|
+
applyLaws(this, dt);
|
|
2335
1534
|
this.transformed = this.combining && dt !== Infinity;
|
|
2336
1535
|
if (this.transformed) this._chargeRows(dt);
|
|
2337
1536
|
}
|
|
@@ -3390,7 +2589,7 @@ export class Solver {
|
|
|
3390
2589
|
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
3391
2590
|
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
3392
2591
|
// 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
|
|
2592
|
+
// charge it keeps in place of that (see captureLaws in src/lawRows.js).
|
|
3394
2593
|
const rate = Number.isFinite(dt) ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
3395
2594
|
let Q = 0, dQdV = 0;
|
|
3396
2595
|
port.nodes.forEach((g, w) => {
|
|
@@ -3522,7 +2721,8 @@ export class Solver {
|
|
|
3522
2721
|
// φ law (none where φ is undefined: nothing at the end node responds to it). The displacement
|
|
3523
2722
|
// into the device, D_in = sgn·D, adds (D_in − D_in at the step's start)/dt to the current.
|
|
3524
2723
|
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
|
|
2724
|
+
// (In a steady solve, what a law in this circuit row counts of D_in: see applyConstraints in
|
|
2725
|
+
// src/lawRows.js.)
|
|
3526
2726
|
const q = !dyn && this.steady && this.contactLaw[k] ? (this.contactQ[k] ??= { row: new Float64Array(this.sys.size + 1), D: 0, dV: 0 }) : null;
|
|
3527
2727
|
if (q) {
|
|
3528
2728
|
q.row.fill(0);
|
|
@@ -3540,7 +2740,7 @@ export class Solver {
|
|
|
3540
2740
|
this.contactD[side] = sgn * Din;
|
|
3541
2741
|
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
2742
|
// 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
|
|
2743
|
+
// the charge it keeps in place of that (see captureLaws in src/lawRows.js).
|
|
3544
2744
|
const rate = dyn ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
3545
2745
|
if (rate > 0) {
|
|
3546
2746
|
I += (Din - (dyn ? Dstart : this.chargeRefs.get(k))) * rate;
|
|
@@ -3591,92 +2791,6 @@ export class Solver {
|
|
|
3591
2791
|
|
|
3592
2792
|
|
|
3593
2793
|
|
|
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
2794
|
// Zero one row of the Jacobian (all three blocks), ready to be replaced.
|
|
3681
2795
|
_replaceRow(b, rs) {
|
|
3682
2796
|
const l = this.loc[b * this.M + rs];
|
|
@@ -3741,7 +2855,7 @@ export class Solver {
|
|
|
3741
2855
|
// entries; where they're huge (an inversion layer's 0.1 nm cells, a conductance ~1e11 against
|
|
3742
2856
|
// the layer's own storage and the trickle from the bulk), eliminating them loses the layer's
|
|
3743
2857
|
// 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
|
|
2858
|
+
// solve's and leave refined (in the transformed unknowns, as solveBordered gives them); false
|
|
3745
2859
|
// if refining didn't help, leaving them as they came.
|
|
3746
2860
|
_refine(dt, delta, deltaV) {
|
|
3747
2861
|
const { res, termRes, termB, termC, termDI, rowDiv, floating: fl } = this;
|
|
@@ -3768,7 +2882,7 @@ export class Solver {
|
|
|
3768
2882
|
rest.multiply(v, jv);
|
|
3769
2883
|
if (save.transformed) this._untransform(v); // (the dilute terms in the plain unknowns)
|
|
3770
2884
|
lin.apply(v, jv, 1, 1 / dt);
|
|
3771
|
-
// A flat stretch's rows hold each level to its first node's (see
|
|
2885
|
+
// A flat stretch's rows hold each level to its first node's (see flatRows in src/lawRows.js).
|
|
3772
2886
|
for (const p of pins) {
|
|
3773
2887
|
if (!p.col) continue;
|
|
3774
2888
|
for (const o of p.col) if (o !== p.row && this.loc[o] >= 0) jv[R[o]] -= x[R[p.row]];
|
|
@@ -3801,7 +2915,7 @@ export class Solver {
|
|
|
3801
2915
|
p.res = rhs[R[p.row]];
|
|
3802
2916
|
rhs[R[p.row]] = 0;
|
|
3803
2917
|
}
|
|
3804
|
-
this
|
|
2918
|
+
solveBordered(this, rhs, out, outV, pins);
|
|
3805
2919
|
const z = new Float64Array(N + K);
|
|
3806
2920
|
for (let j = 0; j < N; j++) z[j] = out[j];
|
|
3807
2921
|
for (let a = 0; a < K; a++) z[N + a] = outV[a];
|
|
@@ -3863,7 +2977,8 @@ export class Solver {
|
|
|
3863
2977
|
const history = [], ownHistory = [];
|
|
3864
2978
|
// Refined solves (see _refine), once the plain ones stall.
|
|
3865
2979
|
let refine = false, refined = 0, noRefine = false;
|
|
3866
|
-
// Islands (see
|
|
2980
|
+
// Islands (see applyIslands in src/lawRows.js): their summed rows go in only once the plain
|
|
2981
|
+
// solves converge.
|
|
3867
2982
|
// Far from the solution, Newton exact along a weakly held level takes it by 1e5 thermal
|
|
3868
2983
|
// units (a floating base, cold at bias), where the plain solves, which barely see that level,
|
|
3869
2984
|
// converge. Converged, an island at the wrong level shows: it takes in a different current
|
|
@@ -3943,7 +3058,7 @@ export class Solver {
|
|
|
3943
3058
|
try {
|
|
3944
3059
|
const pins = this.pins;
|
|
3945
3060
|
deltaV.fill(0);
|
|
3946
|
-
this
|
|
3061
|
+
solveBordered(this, res, delta, deltaV, pins);
|
|
3947
3062
|
if (refine && !this._refine(dt, delta, deltaV)) {
|
|
3948
3063
|
refine = false; // (GMRES didn't converge, or strayed: the plain solves it is)
|
|
3949
3064
|
noRefine = true;
|
|
@@ -4135,7 +3250,7 @@ export class Solver {
|
|
|
4135
3250
|
this.sourceTime = this.steady ? this.steadyAt : tEnd;
|
|
4136
3251
|
this.sourceBefore = !this.steady;
|
|
4137
3252
|
this.combining = true;
|
|
4138
|
-
this.lawForm = new Map(); // (see
|
|
3253
|
+
this.lawForm = new Map(); // (see circuitLaws in src/lawRows.js)
|
|
4139
3254
|
let result;
|
|
4140
3255
|
try {
|
|
4141
3256
|
result = this.newton(dtEff, opts);
|
|
@@ -4152,7 +3267,8 @@ export class Solver {
|
|
|
4152
3267
|
}
|
|
4153
3268
|
if (!result.converged && Number.isFinite(dtEff) && [...this.lawForm.values()].some((v) => !v)) {
|
|
4154
3269
|
// 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
|
|
3270
|
+
// charging, below its metal's G Δη): once more, read by the change (see circuitLaws in
|
|
3271
|
+
// src/lawRows.js).
|
|
4156
3272
|
this.u.set(this.uPrev);
|
|
4157
3273
|
this.uLo.set(this.uPrevLo);
|
|
4158
3274
|
this.termV.set(this.termVPrev);
|
|
@@ -5180,7 +4296,8 @@ export class Solver {
|
|
|
5180
4296
|
const last = this.model.regions.length - 1;
|
|
5181
4297
|
const { n, cOld } = this, vol = this.model.grid.vol;
|
|
5182
4298
|
// 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
|
|
4299
|
+
// says, which the step kept exactly (see circuitLaws in src/lawRows.js); the flux through a
|
|
4300
|
+
// metal is round-off.
|
|
5184
4301
|
const driven = new Map();
|
|
5185
4302
|
for (const cs of this.constraints) {
|
|
5186
4303
|
if (cs.circuit < 0 || this.terms[cs.circuit].drive.kind !== 'I' || cs.parts.length !== 1 || cs.surface.length || cs.carriers.length) continue;
|
|
@@ -5225,7 +4342,7 @@ export class Solver {
|
|
|
5225
4342
|
* Steady state: where a transient from the present state ends, found as the solution of the
|
|
5226
4343
|
* steady equations (dt = ∞, no storage term), with no slow modes to wait out, however slow the
|
|
5227
4344
|
* physics (e.g. exponentially scarce minority carriers). What the storage term held is held by
|
|
5228
|
-
* the steady system's own rows instead (see
|
|
4345
|
+
* the steady system's own rows instead (see src/laws.js): each conserved amount in place of a balance
|
|
5229
4346
|
* row that says nothing new, levels that carry nothing held flat, islands' summed balances.
|
|
5230
4347
|
* Where Newton from here fails: continuation along the drives, then the pseudo-transient ramp.
|
|
5231
4348
|
* The clock is not advanced, and open-system conservation bookkeeping restarts here.
|
|
@@ -5235,7 +4352,7 @@ export class Solver {
|
|
|
5235
4352
|
solveSteady(opts = {}) {
|
|
5236
4353
|
this.unreached = null; // (a driven current no held voltage reaches: see _floatingContinuation)
|
|
5237
4354
|
this.charging = null; // (a driven current that only charges: see _solveSteadyAll)
|
|
5238
|
-
this
|
|
4355
|
+
captureLaws(this);
|
|
5239
4356
|
this.steady = true; // (the steady system's rows; sources at the present time)
|
|
5240
4357
|
this.steadyAt = this.time; // (which a pseudo-transient's steps don't move on)
|
|
5241
4358
|
try {
|
|
@@ -5254,7 +4371,7 @@ export class Solver {
|
|
|
5254
4371
|
const I = sourceAt(this.terms[k].drive.src, this.time);
|
|
5255
4372
|
if (this.terms[k].drive.kind !== 'I' || I === 0) continue;
|
|
5256
4373
|
const laws = this.constraints.filter((cs) => cs.terminals.includes(k));
|
|
5257
|
-
if (laws.length === 0 && !this
|
|
4374
|
+
if (laws.length === 0 && !chargeOnly(this, k)) continue;
|
|
5258
4375
|
const what = laws.flatMap((cs) => cs.parts.map(({ stretch }) => {
|
|
5259
4376
|
const st = this.stretches[stretch], [r0, r1] = st.regions;
|
|
5260
4377
|
return `${species[st.species].name} in ${regions[r0].name}${r1 > r0 ? `…${regions[r1].name}` : ''}`;
|
|
@@ -5274,7 +4391,7 @@ export class Solver {
|
|
|
5274
4391
|
_steadyFromHere(opts) {
|
|
5275
4392
|
this.conditioning = null;
|
|
5276
4393
|
this.computeConcentrations();
|
|
5277
|
-
this
|
|
4394
|
+
captureLocal(this); // immobile combinations keep, node by node, what they hold now
|
|
5278
4395
|
this.sourceTime = this.time;
|
|
5279
4396
|
// Continuation applies where the right terminal is held at a voltage, against a held left one.
|
|
5280
4397
|
const [dl, dr] = [this.terms[0].drive, this.terms[1].drive];
|
|
@@ -5370,20 +4487,7 @@ export class Solver {
|
|
|
5370
4487
|
// only charges a capacitance (so that its steady states are one per amount or charge, not one
|
|
5371
4488
|
// per voltage).
|
|
5372
4489
|
_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');
|
|
4490
|
+
return this.constraints.some((cs) => cs.terminals.includes(k)) || chargeOnly(this, k);
|
|
5387
4491
|
}
|
|
5388
4492
|
|
|
5389
4493
|
// A cold steady solve with a terminal driven by a current, which can fail where the same state
|