driftlet 1.1.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/conventions.md +2 -2
- package/docs/data.md +4 -1
- package/docs/device.md +6 -5
- package/docs/internals.md +86 -0
- package/docs/numerics.md +24 -5
- package/docs/reliability.md +34 -0
- package/docs/statistics.md +1 -0
- package/llms.txt +6 -6
- package/package.json +2 -1
- package/src/channels.js +12 -7
- package/src/checks.js +6 -4
- package/src/data.js +1 -1
- package/src/describe.js +1 -1
- package/src/device.js +3 -4
- package/src/devices.js +9 -1
- package/src/equation.js +14 -3
- package/src/index.js +6 -3
- package/src/junction.js +9 -3
- package/src/kit.js +1 -1
- package/src/lawRows.js +562 -0
- package/src/laws.js +693 -0
- package/src/live.js +14 -9
- package/src/particles.js +4 -1
- package/src/plot.js +8 -4
- package/src/solution.js +1 -0
- package/src/solver.js +82 -902
- package/src/stack.js +5 -1
- package/src/statistics.js +31 -10
- package/src/traces.js +10 -7
- package/src/types.js +96 -29
- package/types/index.d.ts +14 -5
- package/types/kit.d.ts +1 -1
- package/types/lawRows.d.ts +5 -0
- package/types/laws.d.ts +86 -0
- package/types/plot.d.ts +2 -2
- package/types/solution.d.ts +1 -0
- package/types/solver.d.ts +80 -39
- package/types/types.d.ts +179 -26
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,16 +495,20 @@ 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).
|
|
503
506
|
this.termB = this.terms.map(() => new Float64Array(this.sys.size + 1));
|
|
504
507
|
this.termC = this.terms.map(() => new Float64Array(this.sys.size + 1));
|
|
505
508
|
this.termDI = new Float64Array(this.terms.length);
|
|
509
|
+
// (and a circuit row's derivatives by other terminals' voltages: a law in one's row that counts
|
|
510
|
+
// another electrode's charge)
|
|
511
|
+
this.termDX = this.terms.map(() => new Float64Array(this.terms.length));
|
|
506
512
|
this.termRes = new Float64Array(this.terms.length);
|
|
507
513
|
this._refreshSources();
|
|
508
514
|
// A floating terminal starts level with the first held one (no current).
|
|
@@ -733,7 +739,8 @@ export class Solver {
|
|
|
733
739
|
// a port). So Ag⁺ between silver electrodes is fed (Ag⁺ + e⁻ ⇌ Ag(s), the electrons fed by the
|
|
734
740
|
// contacts), while Fe³⁺ and Fe²⁺ between platinum electrodes aren't (Fe³⁺ + e⁻ ⇌ Fe²⁺
|
|
735
741
|
// conserves the iron, whatever the electrons do).
|
|
736
|
-
this.
|
|
742
|
+
this.rough = this.stretches.map((st) => this._roughAmount(st));
|
|
743
|
+
this.moieties = conservedMoieties(this._structure());
|
|
737
744
|
this.stretches.forEach((st, k) => {
|
|
738
745
|
st.connected = st.contactFed || this.moieties.every((w) => w[k] === 0);
|
|
739
746
|
});
|
|
@@ -750,7 +757,8 @@ export class Solver {
|
|
|
750
757
|
// inversion electrons, which reach the back contact only through a bulk with ~1e3 of them per
|
|
751
758
|
// cm³; blocked SO₄²⁻ driven from a zinc cathode's extended space charge, down to 1e-25 mol/m³
|
|
752
759
|
// there, whose chain of conductances lost the system 14 digits). So steady solves hold it
|
|
753
|
-
// flat (
|
|
760
|
+
// flat (flatRows in src/lawRows.js): at its contact's level, or reaching none, at the level
|
|
761
|
+
// its amount fixes.
|
|
754
762
|
// (Not through flow or mixing, nor a concentrated material's cross-diffusion, where carrying
|
|
755
763
|
// nothing isn't a level flat.)
|
|
756
764
|
const plain = (st) => {
|
|
@@ -783,7 +791,7 @@ export class Solver {
|
|
|
783
791
|
const ks = rx.part.map((p) => this.stretchOf[(f + p.side) * n + p.i]);
|
|
784
792
|
if (ks.some((k) => k < 0)) continue;
|
|
785
793
|
join(ks);
|
|
786
|
-
rxns.push([ks, (side) => rx.part.reduce((a, p) => a
|
|
794
|
+
rxns.push([ks, (side) => rx.part.reduce((a, p) => a - p.nu * this.contactEta(side, p.i), rx.fixedA)]); // (a face's ν: products +, so − Σ ν η)
|
|
787
795
|
}
|
|
788
796
|
});
|
|
789
797
|
const groups = new Map();
|
|
@@ -814,776 +822,39 @@ export class Solver {
|
|
|
814
822
|
return t;
|
|
815
823
|
}
|
|
816
824
|
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
_moves(st) {
|
|
820
|
-
const { regions, materials } = this.model;
|
|
821
|
-
for (let q = st.regions[0]; q <= st.regions[1]; q++) {
|
|
822
|
-
const mat = materials[regions[q].material];
|
|
823
|
-
if (!(mat.conductor || mat.D[st.species] > 0)) return false;
|
|
824
|
-
}
|
|
825
|
-
return true;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
_still(st) {
|
|
829
|
-
const { regions, materials } = this.model;
|
|
830
|
-
for (let q = st.regions[0]; q <= st.regions[1]; q++) {
|
|
831
|
-
const mat = materials[regions[q].material];
|
|
832
|
-
if (mat.conductor || mat.D[st.species] > 0) return false;
|
|
833
|
-
}
|
|
834
|
-
return true;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// Whether terminal k (a contact, or port k − 2) feeds stretch st: links its species there.
|
|
838
|
-
_feeds(k, st) {
|
|
839
|
-
return k === 0 ? st.leftOpen : k === 1 ? st.rightOpen : st.linked.includes(k - 2);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// What a steady solve keeps (under the terminals' present drives, so again after a change of
|
|
843
|
-
// drive): each conserved amount, solved directly in place of one (redundant) balance row, and
|
|
844
|
-
// 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.
|
|
845
827
|
_laws() {
|
|
846
|
-
const
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
this.
|
|
857
|
-
const conductor = (st) => materials[regions[st.regions[0]].material].conductor;
|
|
858
|
-
// The terminals driven by a current that feed a stretch: a combination through one keeps what
|
|
859
|
-
// the state holds when a solve begins (see _captureLaws), and has no steady state at any other
|
|
860
|
-
// current.
|
|
861
|
-
const driven = (st) => this.terms.flatMap((t, k) => (t.drive.kind === 'I' && this._feeds(k, st) ? [k] : []));
|
|
862
|
-
const add = (parts, rowStretch, surface = [], circuit = -1, carriers = []) => {
|
|
863
|
-
const st = this.stretches[rowStretch];
|
|
864
|
-
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
865
|
-
for (const p of surface) nNodes += model.ports[this.surfCols[p.col][0]].nodes.length * model.ports[this.surfCols[p.col][0]].surface.length;
|
|
866
|
-
for (const p of carriers) nNodes += model.ports[p.k - 2].nodes.length;
|
|
867
|
-
for (const p of parts) this.stretches[p.stretch].conserved = true;
|
|
868
|
-
this.constraints.push({
|
|
869
|
-
parts,
|
|
870
|
-
surface,
|
|
871
|
-
carriers, // (what a port's electrode holds, Q/(zF) at weight w: see _conservedMoieties)
|
|
872
|
-
terminals: [...new Set([...parts.flatMap((p) => driven(this.stretches[p.stretch])), ...carriers.map((p) => p.k)])],
|
|
873
|
-
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(),
|
|
874
|
-
circuit, // (the terminal whose circuit row it stands in for, or −1 for a balance row's, `row`)
|
|
875
|
-
// (and the terminal's weight in it, w/z on what that feeds: the amount changes by λ I/F)
|
|
876
|
-
lambda: circuit < 0 ? 0 : (carriers.find((p) => p.k === circuit)?.w / this.z[model.ports[circuit - 2]?.terminal] || parts.map((p) => (driven(this.stretches[p.stretch]).includes(circuit) ? p.w / this.z[this.stretches[p.stretch].species] : 0)).find((x) => x !== 0)),
|
|
877
|
-
row: circuit >= 0 ? -1 : this.blockOfNode[st.nodes[0]] * this.M + 1 + st.species,
|
|
878
|
-
idx: new Int32Array(nNodes * this.M),
|
|
879
|
-
w: new Float64Array(nNodes * this.M),
|
|
880
|
-
len: 0,
|
|
881
|
-
res: 0,
|
|
882
|
-
q: new Float64Array(this.sys.size + 1),
|
|
883
|
-
});
|
|
884
|
-
};
|
|
885
|
-
this.stretches.forEach((st, k) => {
|
|
886
|
-
if (!st.spectator || !this._moves(st)) return;
|
|
887
|
-
add([{ stretch: k, w: 1 }], k);
|
|
888
|
-
// Held flat (see _flatRows), it moves as a whole: the pin's response is the whole stretch's
|
|
889
|
-
// shift, its amount saying how far.
|
|
890
|
-
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);
|
|
891
|
-
});
|
|
892
|
-
const S = this.stretches.length, T = this.terms.length, X = S + this.surfCols.length;
|
|
893
|
-
const zc = (k) => this.z[model.ports[k - 2].terminal];
|
|
894
|
-
// The rows replaced: a moving stretch's balance (one of still stretches alone is kept node by
|
|
895
|
-
// node, below), or the circuit row of a terminal driven at no current, which says nothing the
|
|
896
|
-
// combination's balance rows don't (their weighted sum is that terminal's current, weighted
|
|
897
|
-
// by w/z). Together they must be independent, each combination's weights over the rows
|
|
898
|
-
// replaced a nonsingular matrix, or one balance is lost and another said twice (A + B = AB and
|
|
899
|
-
// 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
|
|
900
|
-
// A twice and C not at all, and the solve converged to a state that wasn't steady). So they're
|
|
901
|
-
// chosen by elimination, each combination's weights reduced by the rows already chosen: a
|
|
902
|
-
// circuit row where one is left, else the first stretch with weight 1, else the basis
|
|
903
|
-
// vector's own (its weight is 1 there, and no other vector's is), else the largest.
|
|
904
|
-
const rowOf = (k) => this.blockOfNode[this.stretches[k].nodes[0]] * this.M + 1 + this.stretches[k].species;
|
|
905
|
-
const chosen = []; // (a column and the reduced weights, per row replaced: S + t for terminal t's)
|
|
906
|
-
const spectators = new Set(this.constraints.map((cs) => cs.row));
|
|
907
|
-
// What an electrode holds goes only in its terminal's circuit row (its charge depends on the
|
|
908
|
-
// terminal's voltage, which a balance row's pin can't carry): the combinations through it come
|
|
909
|
-
// first, so that the first takes that row, and the rest have its multiple taken off.
|
|
910
|
-
const through = (w) => this.carriers.some((k, j) => w[X + j] !== 0);
|
|
911
|
-
const held = new Map(); // (terminal → the combination in place of its circuit row)
|
|
912
|
-
// Reactions' equilibria first. A face reaction that alone changes some combination (without
|
|
913
|
-
// it, w would be conserved: Ag⁺ closed in a cell where Ag⁺ + e⁻ = Ag(s) is its one source, or
|
|
914
|
-
// Ag⁺ + AgNO₃ where it also complexes) has that combination's summed balance rows equal to its
|
|
915
|
-
// rate, w·ν times it, which a steady state sets to zero. The rate is a poor row: k₀ times a
|
|
916
|
-
// function of the levels, beside diffusion's terms in the same balances. An Ag⁺/Ag couple
|
|
917
|
-
// 1e-20 as fast as Fe³⁺/Fe²⁺ on the same platinum was lost to their round-off, the
|
|
918
|
-
// open-circuit potential settling where silver wasn't (−0.4661 V, the joint equilibrium
|
|
919
|
-
// −0.5004 V), and 1e-15 to 1e-18 failed. Its affinity says the same at any k₀ (each kinetic
|
|
920
|
-
// form vanishes only there), so it stands in for one of those rows (see _applyAffinities),
|
|
921
|
-
// chosen by the same elimination, and a stretch's: never a circuit row.
|
|
922
|
-
// The slowest first, whose rate rows are the poorest, and each row picked by the reaction's
|
|
923
|
-
// own terms, never by where its species are listed: the same rows in any listing. (Picked by
|
|
924
|
-
// listing, an electrode's two couples took different rows with the species reversed, and
|
|
925
|
-
// Newton converged in one listing and crawled in the other.)
|
|
926
|
-
this.affinities = [];
|
|
927
|
-
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() })));
|
|
928
|
-
faceRx.sort((a, b) => a.rx.k0 - b.rx.k0 || (a.key < b.key ? -1 : a.key > b.key ? 1 : a.f - b.f));
|
|
929
|
-
for (const { f, rx } of faceRx) {
|
|
930
|
-
const nu = new Float64Array(S);
|
|
931
|
-
let inFlat = false;
|
|
932
|
-
for (const p of rx.part) {
|
|
933
|
-
const k = this.stretchOf[(f + p.side) * this.n + p.i];
|
|
934
|
-
if (k < 0 || this.flatStretches.includes(this.stretches[k])) inFlat = true;
|
|
935
|
-
else nu[k] += p.nu;
|
|
936
|
-
}
|
|
937
|
-
if (inFlat) continue; // (held flat at a contact's levels, in equilibrium already)
|
|
938
|
-
// (Not one fed by a terminal driven by a current: its sum is the rate plus what that
|
|
939
|
-
// passes. Silver's, e⁻ + Fe²⁺ with the electrons fed, or Ag⁺ + AgNO₃, which nothing is.
|
|
940
|
-
// A current held at zero passes nothing: an electrode at open circuit feeding a closed
|
|
941
|
-
// pool of Ag⁺ beside a slow Fe³⁺/Fe²⁺ couple settled 78 mV off, its iron not stationary.)
|
|
942
|
-
const passes = (k) => !this._noCurrent(this.terms[k]);
|
|
943
|
-
const fed = (w) => w.some((x, k) => x !== 0 && ((k >= X && passes(this.carriers[k - X])) || (k < S && driven(this.stretches[k]).some(passes))));
|
|
944
|
-
// Any of the combinations it alone changes will do, with any conserved amount added (whose
|
|
945
|
-
// summed balance is zero too): one of them needs a row of its own left to take. (Silver's,
|
|
946
|
-
// e⁻ − Fe³⁺ beside a closed iron couple that had Fe³⁺'s, is e⁻ + Fe²⁺ with the iron added.)
|
|
947
|
-
const news = this._conservedMoieties(true, [], rx).filter((w) => Math.abs(nu.reduce((t, x, k) => t + x * w[k], 0)) > 1e-9 && !fed(w));
|
|
948
|
-
const kept = laws.filter((l) => !fed(l));
|
|
949
|
-
const tries = [...news, ...news.flatMap((w) => kept.flatMap((l) => [1, -1].map((sg) => w.map((x, m) => x + sg * l[m]))))];
|
|
950
|
-
for (const w of tries) {
|
|
951
|
-
const v = new Float64Array(S + T);
|
|
952
|
-
for (let k = 0; k < S; k++) {
|
|
953
|
-
if (w[k] === 0) continue;
|
|
954
|
-
const st = this.stretches[k];
|
|
955
|
-
if (this._moves(st) && !spectators.has(rowOf(k))) v[k] = w[k];
|
|
956
|
-
if (this.z[st.species] !== 0) for (const t of driven(st)) v[S + t] = w[k] / this.z[st.species];
|
|
957
|
-
}
|
|
958
|
-
for (const [col, row] of chosen) {
|
|
959
|
-
const fc = v[col];
|
|
960
|
-
if (fc !== 0) for (let j = 0; j < S + T; j++) v[j] -= fc * row[j];
|
|
961
|
-
}
|
|
962
|
-
const scale = v.reduce((m, x) => Math.max(m, Math.abs(x)), 0);
|
|
963
|
-
if (!(scale > 1e-9)) continue;
|
|
964
|
-
// (Never a metal's carriers, nor a stretch a terminal feeds, even at no current: their rows
|
|
965
|
-
// carry the terminal's current, and Pt's electrons standing for an iron couple's balance
|
|
966
|
-
// put the electrode at 602 V.)
|
|
967
|
-
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]));
|
|
968
|
-
// (One of its own species first, then weight 1, the largest, and by name.)
|
|
969
|
-
const own = (k) => rx.part.some((p) => p.i === this.stretches[k].species && this.stretchOf[(f + p.side) * this.n + p.i] === k);
|
|
970
|
-
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]];
|
|
971
|
-
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; };
|
|
972
|
-
const col = [...Array(S).keys()].filter(ok).reduce((b, k) => (b < 0 || less(k, b) ? k : b), -1);
|
|
973
|
-
if (col < 0) continue;
|
|
974
|
-
chosen.push([col, v.map((x) => x / v[col])]);
|
|
975
|
-
this.affinities.push({ rx, f, row: rowOf(col), idx: new Int32Array(rx.part.length), w: new Float64Array(rx.part.length), len: 0, res: 0 });
|
|
976
|
-
break;
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
for (let w of [...laws.filter(through), ...laws.filter((w) => !through(w))]) {
|
|
980
|
-
for (const [j, k] of this.carriers.entries()) {
|
|
981
|
-
const c = held.get(k);
|
|
982
|
-
if (w[X + j] === 0 || !c) continue;
|
|
983
|
-
const f = w[X + j] / c[X + j], own = w.own;
|
|
984
|
-
w = w.map((x, m) => (Math.abs(x - f * c[m]) < 1e-9 ? 0 : x - f * c[m]));
|
|
985
|
-
w.own = own;
|
|
986
|
-
}
|
|
987
|
-
const parts = [...w.keys()].filter((k) => k < S && w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
988
|
-
const surface = [...w.keys()].filter((k) => k >= S && k < X && w[k] !== 0).map((k) => ({ col: k - S, w: w[k] }));
|
|
989
|
-
const carriers = this.carriers.flatMap((k, j) => (w[X + j] !== 0 ? [{ k, w: w[X + j] }] : []));
|
|
990
|
-
if (parts.length === 0) continue; // (a surface's own: node by node, below)
|
|
991
|
-
if (parts.length === 1 && surface.length === 0 && this.stretches[parts[0].stretch].spectator) continue; // (above)
|
|
992
|
-
if (parts.some((p) => !this._moves(this.stretches[p.stretch]) && !this._still(this.stretches[p.stretch]))) continue;
|
|
993
|
-
const v = new Float64Array(S + T);
|
|
994
|
-
for (const { stretch: k, w: wk } of parts) {
|
|
995
|
-
const st = this.stretches[k];
|
|
996
|
-
if (this._moves(st) && !spectators.has(rowOf(k))) v[k] = wk;
|
|
997
|
-
if (this.z[st.species] !== 0) for (const t of driven(st)) v[S + t] = wk / this.z[st.species];
|
|
998
|
-
}
|
|
999
|
-
for (const { k, w: wk } of carriers) v[S + k] = wk / zc(k);
|
|
1000
|
-
for (const [col, row] of chosen) {
|
|
1001
|
-
const f = v[col];
|
|
1002
|
-
if (f !== 0) for (let j = 0; j < S + T; j++) v[j] -= f * row[j];
|
|
1003
|
-
}
|
|
1004
|
-
const scale = v.reduce((m, x) => Math.max(m, Math.abs(x)), 0);
|
|
1005
|
-
if (!(scale > 1e-9)) continue;
|
|
1006
|
-
const ok = (j) => j !== undefined && Math.abs(v[j]) > 1e-9 * scale;
|
|
1007
|
-
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);
|
|
1008
|
-
if (carriers.some((p) => p.k !== col - S)) continue; // (an electrode's charge in a circuit row not its own: never, as they're chosen)
|
|
1009
|
-
chosen.push([col, v.map((x) => x / v[col])]);
|
|
1010
|
-
if (col >= S) held.set(col - S, w);
|
|
1011
|
-
add(parts, col < S ? col : parts[0].stretch, surface, col < S ? -1 : col - S, carriers);
|
|
1012
|
-
}
|
|
1013
|
-
// A combination that nothing carries anywhere is conserved node by node: of still stretches
|
|
1014
|
-
// (trap states: X⁰ + X⁻ under e⁻ + X⁰ = X⁻), or of an electrode's surface species (adsorbates
|
|
1015
|
-
// that only turn into each other). At each node its weighted sum stays what it was, a row local
|
|
1016
|
-
// to that node's block, in place of one of their balances.
|
|
1017
|
-
this.localConstraints = [];
|
|
1018
|
-
const span = ([g0, g1]) => Array.from({ length: g1 - g0 + 1 }, (_, j) => g0 + j);
|
|
1019
|
-
for (const w of laws) {
|
|
1020
|
-
const parts = [...w.keys()].filter((k) => w[k] !== 0);
|
|
1021
|
-
if (parts.some((k) => k >= X)) continue; // (through an electrode: above)
|
|
1022
|
-
const sets = parts.map((k) => (k >= S ? model.ports[this.surfCols[k - S][0]].nodes : this._still(this.stretches[k]) ? span(this.stretches[k].nodes) : null));
|
|
1023
|
-
if (sets.some((set) => set === null || set.length !== sets[0].length || set.some((g, j) => g !== sets[0][j]))) continue; // (side by side only)
|
|
1024
|
-
for (const k of parts) if (k < S) this.stretches[k].conserved = true;
|
|
1025
|
-
const slot = (k) => (k < S ? 1 + this.stretches[k].species : 1 + this.n + this.surfCols[k - S][1]);
|
|
1026
|
-
this.localConstraints.push({
|
|
1027
|
-
parts: parts.map((k) => (k < S ? { i: this.stretches[k].species, w: w[k] } : { q: this.surfCols[k - S][1], w: w[k] })),
|
|
1028
|
-
row: slot(parts.includes(w.own) ? w.own : parts[0]),
|
|
1029
|
-
nodes: sets[0],
|
|
1030
|
-
reference: new Float64Array(sets[0].length),
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
1033
|
-
// Islands: the pieces of a stretch between its faces. One that nothing else feeds (no
|
|
1034
|
-
// contact, port or reaction) holds its level only through its faces' fluxes, which can be
|
|
1035
|
-
// ~1e-24 of its own conduction (a tiny conductance at the face, or a neighbour that barely
|
|
1036
|
-
// conducts): eliminated, the level is lost to round-off, and Newton settles at a wrong one with
|
|
1037
|
-
// nothing to show for it. In steady solves the piece's balance summed over its nodes, where
|
|
1038
|
-
// the internal fluxes cancel exactly and the faces' alone are left, replaces its first node's
|
|
1039
|
-
// (see _applyIslands), once the plain solves converge (see newton).
|
|
1040
|
-
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)
|
|
1041
839
|
this.pinnedIslands = [];
|
|
1042
840
|
this.islandsOn = false; // (newton() turns them on, see there)
|
|
1043
841
|
this.pins = [];
|
|
1044
|
-
const used = new Set([...this.constraints.map((cs) => cs.row).filter((o) => o >= 0), ...this.affinities.map((af) => af.row)]);
|
|
1045
|
-
for (const st of this.stretches) {
|
|
1046
|
-
const i = st.species, [s0, s1] = st.regions;
|
|
1047
|
-
if (!st.mobile || conductor(st)) continue;
|
|
1048
|
-
for (let q0 = s0, q1 = s0; q1 <= s1; q1++) {
|
|
1049
|
-
const p0 = q0;
|
|
1050
|
-
q0 = q1 + 1;
|
|
1051
|
-
if (p0 === s0 && q1 === s1) continue; // (uncut)
|
|
1052
|
-
if ((p0 === s0 && st.leftOpen) || (q1 === s1 && st.rightOpen)) continue;
|
|
1053
|
-
const inside = (q) => q >= p0 && q <= q1;
|
|
1054
|
-
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;
|
|
1055
|
-
let reactive = false;
|
|
1056
|
-
for (let q = p0; q <= q1; q++) {
|
|
1057
|
-
for (const rx of model.reactions) if (rx.kf[regions[q].material] > 0 && [...rx.reactants, ...rx.products].some((p) => p.i === i)) reactive = true;
|
|
1058
|
-
}
|
|
1059
|
-
for (let f = Math.max(0, p0 - 1); f <= Math.min(model.interfaces.length - 1, q1); f++) {
|
|
1060
|
-
for (const rx of model.interfaces[f].reactions) if (rx.part.some((p) => p.i === i && inside(f + p.side))) reactive = true;
|
|
1061
|
-
}
|
|
1062
|
-
const row = this.blockOfNode[grid.regionStart[p0]] * this.M + 1 + i;
|
|
1063
|
-
if (reactive || used.has(row)) continue;
|
|
1064
|
-
used.add(row);
|
|
1065
|
-
// The faces' fluxes into the piece: −A_f u at its left face, +A_f u at its right. Across
|
|
1066
|
-
// a face that holds μ̄ level, u is set only by the edge balances, and eliminated, it can
|
|
1067
|
-
// come out of the piece's own (G Δη, its two η all but equal: a neighbour 1e20 times
|
|
1068
|
-
// less conductive, and the piece's level was seen through one face of two, and Newton
|
|
1069
|
-
// swung it back and forth across both). There the outside edge node's balance is added:
|
|
1070
|
-
// u cancels, and the outside's first segment carries the flux instead.
|
|
1071
|
-
const faces = [], outside = [];
|
|
1072
|
-
if (p0 > s0) faces.push([p0 - 1, -1, grid.regionEnd[p0 - 1]]);
|
|
1073
|
-
if (q1 < s1) faces.push([q1, 1, grid.regionStart[q1 + 1]]);
|
|
1074
|
-
for (const [f, , g] of faces) {
|
|
1075
|
-
if (model.interfaces[f].links[i].type !== 'equilibrium' || g === 0 || g === this.nNodes - 1) continue;
|
|
1076
|
-
if (model.ports.some((port) => port.nodes.includes(g))) continue;
|
|
1077
|
-
outside.push(this.blockOfNode[g] * this.M + 1 + i);
|
|
1078
|
-
}
|
|
1079
|
-
this.islands.push({
|
|
1080
|
-
stretch: st,
|
|
1081
|
-
row,
|
|
1082
|
-
flux: faces.map(([f]) => this.blockOfFace[f] * this.M + 1 + i),
|
|
1083
|
-
weight: faces.map(([f, sg]) => sg * grid.area[grid.regionEnd[f]]),
|
|
1084
|
-
outside,
|
|
1085
|
-
idx: new Int32Array(faces.length + 3 * this.M * outside.length),
|
|
1086
|
-
w: new Float64Array(faces.length + 3 * this.M * outside.length),
|
|
1087
|
-
len: 0,
|
|
1088
|
-
res: 0,
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
for (const isl of this.islands) isl.outside = isl.outside.filter((o) => !used.has(o));
|
|
1093
|
-
|
|
1094
|
-
// Time steps only: a terminal behind a resistance that alone feeds a combination reads its
|
|
1095
|
-
// current by that combination's change too (see _circuitLaws), its combinations found with it
|
|
1096
|
-
// counted as driven by a current. (Not in the steady system, where it holds its level.)
|
|
1097
|
-
this.timeLaws = [];
|
|
1098
|
-
const behind = this.floating.filter((k) => this.terms[k].drive.kind !== 'I');
|
|
1099
|
-
if (behind.length > 0) {
|
|
1100
|
-
const steadyCarriers = this.carriers, more = this._conservedMoieties(true, behind), X2 = S + this.surfCols.length;
|
|
1101
|
-
for (const w of more) {
|
|
1102
|
-
const parts = [...w.keys()].filter((k) => k < S && w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
1103
|
-
if (parts.length === 0 || parts.some((p) => !this._moves(this.stretches[p.stretch]) && !this._still(this.stretches[p.stretch]))) continue;
|
|
1104
|
-
const surface = [...w.keys()].filter((k) => k >= S && k < X2 && w[k] !== 0).map((k) => ({ col: k - S, w: w[k] }));
|
|
1105
|
-
const carriers = this.carriers.flatMap((k, j) => (w[X2 + j] !== 0 ? [{ k, w: w[X2 + j] }] : []));
|
|
1106
|
-
// Fed by one terminal behind a resistance and nothing else that passes current.
|
|
1107
|
-
const fedBy = new Set([...carriers.map((p) => p.k), ...parts.flatMap((p) => this.floating.filter((k) => this._feeds(k, this.stretches[p.stretch])))]);
|
|
1108
|
-
if (fedBy.size !== 1) continue;
|
|
1109
|
-
const [k] = fedBy;
|
|
1110
|
-
if (!behind.includes(k) || this.timeLaws.some((cs) => cs.circuit === k)) continue;
|
|
1111
|
-
const zc = k >= 2 ? this.z[model.ports[k - 2].terminal] : 0;
|
|
1112
|
-
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);
|
|
1113
|
-
if (!lambda) continue;
|
|
1114
|
-
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
1115
|
-
for (const p of surface) nNodes += model.ports[this.surfCols[p.col][0]].nodes.length * model.ports[this.surfCols[p.col][0]].surface.length;
|
|
1116
|
-
for (const p of carriers) nNodes += model.ports[p.k - 2].nodes.length;
|
|
1117
|
-
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 });
|
|
1118
|
-
}
|
|
1119
|
-
this.carriers = steadyCarriers;
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// What each combination fed by a terminal driven at no current holds now, which a steady solve
|
|
1124
|
-
// keeps: a closed battery at open circuit stays at the charge it had.
|
|
1125
|
-
_captureLaws() {
|
|
1126
|
-
// A terminal at no current that passes current only by charging a capacitance keeps its charge.
|
|
1127
|
-
this.chargeRefs.clear();
|
|
1128
|
-
const charged = this.floating.filter((k) => this.terms[k].drive.kind === 'I' && this._chargeOnly(k));
|
|
1129
|
-
if (charged.length > 0) {
|
|
1130
|
-
this._assembleBookkeeping(Infinity);
|
|
1131
|
-
for (const k of charged) this.chargeRefs.set(k, k >= 2 ? this.portQ[k - 2] : k === 0 ? this.contactD.left : -this.contactD.right);
|
|
1132
|
-
}
|
|
1133
|
-
this.lawRefs.clear();
|
|
1134
|
-
if (this.constraints.some((cs) => cs.carriers.some(({ k }) => this.model.ports[k - 2].capacitance)) && charged.length === 0) this._assembleBookkeeping(Infinity);
|
|
1135
|
-
for (const cs of this.constraints) {
|
|
1136
|
-
if (cs.terminals.length === 0) continue;
|
|
1137
|
-
let a = 0;
|
|
1138
|
-
for (const { stretch, w } of cs.parts) a += w * this.amount(this.stretches[stretch]);
|
|
1139
|
-
for (const { col, w } of cs.surface) a += w * this.surfaceAmount(col);
|
|
1140
|
-
for (const { k, w } of cs.carriers) if (this.model.ports[k - 2].capacitance) a += (w * this.portQ[k - 2]) / (this.z[this.model.ports[k - 2].terminal] * FARADAY);
|
|
1141
|
-
this.lawRefs.set(cs.key, a);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
// Each local constraint's weighted sum at each node, as the state holds it now: what a steady
|
|
1146
|
-
// solve keeps.
|
|
1147
|
-
_captureLocal() {
|
|
1148
|
-
const { n, c, th, nSurf } = this;
|
|
1149
|
-
for (const lc of this.localConstraints) {
|
|
1150
|
-
lc.nodes.forEach((g, j) => {
|
|
1151
|
-
let t = 0;
|
|
1152
|
-
for (const { i, q, w } of lc.parts) t += w * (i !== undefined ? c[g * n + i] : th[g * nSurf + q]);
|
|
1153
|
-
lc.reference[j] = t;
|
|
1154
|
-
});
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// The islands' summed balances (steady solves only): Σ A_f u over the faces into each, its
|
|
1159
|
-
// first node's row pinned in the factorised matrix and the sum bordered (_solveBordered) like a
|
|
1160
|
-
// conserved amount's. The pieces of a stretch held flat by its contact are left as they are.
|
|
1161
|
-
_applyIslands() {
|
|
1162
|
-
const { u, res } = this, R = this.rix, out = [];
|
|
1163
|
-
for (const isl of this.islands) {
|
|
1164
|
-
if (this.flatStretches.includes(isl.stretch)) continue;
|
|
1165
|
-
const b0 = Math.floor(isl.row / this.M), r0 = isl.row % this.M;
|
|
1166
|
-
if (this.loc[isl.row] < 0) continue;
|
|
1167
|
-
// Gathered by column (the faces' u, then each outside row's entries, which cancel those u
|
|
1168
|
-
// exactly where they meet), then the nonzeros kept.
|
|
1169
|
-
const acc = this.islandRow ?? (this.islandRow = new Float64Array(this.sys.size + 1));
|
|
1170
|
-
const cols = [];
|
|
1171
|
-
const add = (col, v) => {
|
|
1172
|
-
if (acc[col] === 0) cols.push(col);
|
|
1173
|
-
acc[col] += v;
|
|
1174
|
-
};
|
|
1175
|
-
let t = 0;
|
|
1176
|
-
isl.flux.forEach((o, j) => {
|
|
1177
|
-
if (this.loc[o] < 0) return;
|
|
1178
|
-
t += isl.weight[j] * u[o];
|
|
1179
|
-
add(R[o], isl.weight[j]);
|
|
1180
|
-
});
|
|
1181
|
-
for (const o of isl.outside) {
|
|
1182
|
-
if (this.loc[o] < 0) continue;
|
|
1183
|
-
t += res[R[o]];
|
|
1184
|
-
this._rowInto(o, add);
|
|
1185
|
-
}
|
|
1186
|
-
let len = 0;
|
|
1187
|
-
for (const col of cols) {
|
|
1188
|
-
if (col < this.sys.size && acc[col] !== 0) {
|
|
1189
|
-
isl.idx[len] = col;
|
|
1190
|
-
isl.w[len++] = acc[col];
|
|
1191
|
-
}
|
|
1192
|
-
acc[col] = 0;
|
|
1193
|
-
}
|
|
1194
|
-
isl.len = len;
|
|
1195
|
-
isl.res = t;
|
|
1196
|
-
this._replaceRow(b0, r0);
|
|
1197
|
-
for (const B of this.termB) B[R[isl.row]] = 0;
|
|
1198
|
-
this._j(b0, r0, b0, r0, 1);
|
|
1199
|
-
res[R[isl.row]] = 0;
|
|
1200
|
-
out.push(isl);
|
|
1201
|
-
}
|
|
1202
|
-
return out;
|
|
1203
842
|
}
|
|
1204
843
|
|
|
1205
|
-
//
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
const {
|
|
1209
|
-
|
|
1210
|
-
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]);
|
|
1211
|
-
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]);
|
|
1212
|
-
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]);
|
|
1213
|
-
if (this.lin) {
|
|
1214
|
-
const into = this.linRow ?? (this.linRow = new Float64Array(sys.size + 1));
|
|
1215
|
-
this.lin.capture(this.rix[o], 1, 0, into);
|
|
1216
|
-
for (let k = 0; k < sys.size; k++) {
|
|
1217
|
-
if (into[k] !== 0) add(k, into[k]);
|
|
1218
|
-
into[k] = 0;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
// The local constraints' rows (steady solves only): Σ w c_i − reference at each node.
|
|
1224
|
-
_applyLocalConstraints() {
|
|
1225
|
-
const { n, M, c, th, nSurf, res, dA: d } = this;
|
|
1226
|
-
for (const lc of this.localConstraints) {
|
|
1227
|
-
lc.nodes.forEach((g, j) => {
|
|
1228
|
-
const b = this.blockOfNode[g];
|
|
1229
|
-
if (this.loc[b * M + lc.row] < 0) return;
|
|
1230
|
-
this._replaceRow(b, lc.row);
|
|
1231
|
-
let t = 0;
|
|
1232
|
-
for (const { i, q, w } of lc.parts) {
|
|
1233
|
-
if (i !== undefined) {
|
|
1234
|
-
t += w * c[g * n + i];
|
|
1235
|
-
this._dc(g, i, d);
|
|
1236
|
-
for (let s = 0; s < M; s++) if (d[s] !== 0) this._j(b, lc.row, b, s, w * d[s]);
|
|
1237
|
-
continue;
|
|
1238
|
-
}
|
|
1239
|
-
// A coverage, with ∂θ_q/∂η_x = θ_q (δ_qx − θ_x) over its surface's species.
|
|
1240
|
-
const tq = th[g * nSurf + q], ns = this.model.ports[this.surfPort[g]].surface.length;
|
|
1241
|
-
t += w * tq;
|
|
1242
|
-
for (let x = 0; x < ns; x++) this._j(b, lc.row, b, 1 + n + x, w * tq * ((q === x ? 1 : 0) - th[g * nSurf + x]));
|
|
1243
|
-
}
|
|
1244
|
-
res[this.rix[b * M + lc.row]] = t - lc.reference[j];
|
|
1245
|
-
});
|
|
1246
|
-
}
|
|
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 };
|
|
1247
849
|
}
|
|
1248
850
|
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
// steady state) feeds each stretch it links freely: a unit row. With `drives`, one driven by a
|
|
1253
|
-
// current feeds its charged species only as that current allows, in proportion to their
|
|
1254
|
-
// charges, so a combination weighted by z there is conserved while it passes none: a battery's
|
|
1255
|
-
// cathode, its electrons behind an open circuit, keeps its charge. Exact integer data, so plain
|
|
1256
|
-
// elimination will do.
|
|
1257
|
-
_conservedMoieties(drives = false, alsoCurrent = [], skip = null) {
|
|
1258
|
-
const { model, n, z } = this, S = this.stretches.length;
|
|
1259
|
-
// With `drives`, an electrode port driven by a current whose reactions take its carrier from
|
|
1260
|
-
// the electrode has a column of its own, last: what the electrode holds, its charge Q/(zF),
|
|
1261
|
-
// which the current feeds and the reactions draw on (a port at no current with M⁺ + e⁻ = M(s)
|
|
1262
|
-
// keeps M⁺ + Q/F, or with no capacitance, Q = 0, the M⁺ alone).
|
|
1263
|
-
const byCurrent = (t, k) => t.drive.kind === 'I' || alsoCurrent.includes(k); // (with `alsoCurrent`, terminals counted as such)
|
|
1264
|
-
this.carriers = drives ? this.terms.flatMap((t, k) => (byCurrent(t, k) && k >= 2 && model.ports[k - 2].reactions.length > 0 ? [k] : [])) : [];
|
|
1265
|
-
const X = S + this.surfCols.length, C = X + this.carriers.length;
|
|
1266
|
-
// (With `skip`, a reaction left out: the combinations that it alone changes come out too, and
|
|
1267
|
-
// its own rows, over the columns, are handed back as `basis.skipped`.)
|
|
1268
|
-
const rows = [], skipped = [];
|
|
1269
|
-
model.interfaces.forEach((itf, f) => {
|
|
1270
|
-
for (const rx of itf.reactions) {
|
|
1271
|
-
const row = new Float64Array(C);
|
|
1272
|
-
for (const p of rx.part) row[this.stretchOf[(f + p.side) * n + p.i]] += p.nu;
|
|
1273
|
-
(rx === skip ? skipped : rows).push(row);
|
|
1274
|
-
}
|
|
1275
|
-
});
|
|
1276
|
-
// An electrode surface's species are columns too, after the stretches (S + j for surface
|
|
1277
|
-
// column j), so a combination that passes through them (A⁺ + e⁻ = S, S + e⁻ = B⁻) is conserved
|
|
1278
|
-
// with what the surface holds.
|
|
1279
|
-
model.ports.forEach((port, k) => {
|
|
1280
|
-
for (const rx of port.reactions) {
|
|
1281
|
-
const row = new Float64Array(C); // (the electrode's carrier is outside the device)
|
|
1282
|
-
if (rx === skip) skipped.push(row);
|
|
1283
|
-
for (const p of rx.part) {
|
|
1284
|
-
if (p.side === 0) row[this.stretchOf[port.region * n + p.i]] += p.nu;
|
|
1285
|
-
else if (p.side === 2) row[S + this.surfColOf[k] + p.s] += p.nu;
|
|
1286
|
-
else if (this.carriers.includes(k + 2)) row[X + this.carriers.indexOf(k + 2)] += p.nu;
|
|
1287
|
-
}
|
|
1288
|
-
if (rx !== skip) rows.push(row);
|
|
1289
|
-
}
|
|
1290
|
-
});
|
|
1291
|
-
model.regions.forEach((reg, q) => {
|
|
1292
|
-
for (const rx of model.reactions) {
|
|
1293
|
-
if (!(rx.kf[reg.material] > 0)) continue;
|
|
1294
|
-
const row = new Float64Array(C);
|
|
1295
|
-
for (const p of rx.reactants) row[this.stretchOf[q * n + p.i]] -= p.nu;
|
|
1296
|
-
for (const p of rx.products) row[this.stretchOf[q * n + p.i]] += p.nu;
|
|
1297
|
-
(rx === skip ? skipped : rows).push(row);
|
|
1298
|
-
}
|
|
1299
|
-
});
|
|
1300
|
-
const unit = (k) => {
|
|
1301
|
-
const row = new Float64Array(C);
|
|
1302
|
-
row[k] = 1;
|
|
1303
|
-
rows.push(row);
|
|
1304
|
-
};
|
|
1305
|
-
this.terms.forEach((t, k) => {
|
|
1306
|
-
const fed = this.stretches.flatMap((st, j) => (this._feeds(k, st) ? [j] : []));
|
|
1307
|
-
if (!(drives && byCurrent(t, k))) return fed.forEach(unit);
|
|
1308
|
-
const charged = fed.filter((j) => z[this.stretches[j].species] !== 0).map((j) => [j, z[this.stretches[j].species]]);
|
|
1309
|
-
if (this.carriers.includes(k)) charged.push([X + this.carriers.indexOf(k), z[model.ports[k - 2].terminal]]);
|
|
1310
|
-
fed.filter((j) => z[this.stretches[j].species] === 0).forEach(unit);
|
|
1311
|
-
for (let a = 1; a < charged.length; a++) {
|
|
1312
|
-
const row = new Float64Array(C);
|
|
1313
|
-
row[charged[0][0]] = 1 / charged[0][1];
|
|
1314
|
-
row[charged[a][0]] = -1 / charged[a][1];
|
|
1315
|
-
rows.push(row);
|
|
1316
|
-
}
|
|
1317
|
-
});
|
|
1318
|
-
// Reduced row echelon form; the free columns give the null space. Immobile stretches' columns
|
|
1319
|
-
// go first, so that a combination of them alone (trap states, X⁰ + X⁻) comes out on its own,
|
|
1320
|
-
// to be kept node by node, apart from any that moves (e⁻ − X⁰, the electrons the traps hold).
|
|
1321
|
-
// Then the stretches by their starting amounts, least first, and by name: a trace species
|
|
1322
|
-
// pivoted after an abundant one comes out added to it (A + B ⇌ C with B at 1 M and A at 1e-14
|
|
1323
|
-
// gave B + C and A − B, where A + C was lost to round-off), and the basis followed the listing.
|
|
1324
|
-
const still = (k) => (k < S && this._still(this.stretches[k]) ? 1 : 0);
|
|
1325
|
-
const rough = this.stretches.map((st) => this._roughAmount(st)), name = (k) => model.species[this.stretches[k].species].name;
|
|
1326
|
-
const order = [...Array(C).keys()].sort((a, b) => {
|
|
1327
|
-
if (still(a) !== still(b)) return still(b) - still(a);
|
|
1328
|
-
if (a >= S || b >= S) return a - b;
|
|
1329
|
-
if (rough[a] !== rough[b]) return rough[a] - rough[b];
|
|
1330
|
-
const na = name(a), nb = name(b);
|
|
1331
|
-
return na < nb ? -1 : na > nb ? 1 : a - b;
|
|
1332
|
-
});
|
|
1333
|
-
const pivots = [];
|
|
1334
|
-
let r = 0;
|
|
1335
|
-
for (const col of order) {
|
|
1336
|
-
if (r >= rows.length) break;
|
|
1337
|
-
let best = r;
|
|
1338
|
-
for (let i = r + 1; i < rows.length; i++) if (Math.abs(rows[i][col]) > Math.abs(rows[best][col])) best = i;
|
|
1339
|
-
if (Math.abs(rows[best][col]) < 1e-9) continue;
|
|
1340
|
-
[rows[r], rows[best]] = [rows[best], rows[r]];
|
|
1341
|
-
const pr = rows[r], pv = pr[col];
|
|
1342
|
-
for (let j = 0; j < C; j++) pr[j] /= pv;
|
|
1343
|
-
for (let i = 0; i < rows.length; i++) {
|
|
1344
|
-
if (i === r || rows[i][col] === 0) continue;
|
|
1345
|
-
const fct = rows[i][col];
|
|
1346
|
-
for (let j = 0; j < C; j++) rows[i][j] -= fct * pr[j];
|
|
1347
|
-
}
|
|
1348
|
-
pivots.push(col);
|
|
1349
|
-
r++;
|
|
1350
|
-
}
|
|
1351
|
-
const isPivot = new Uint8Array(C);
|
|
1352
|
-
for (const col of pivots) isPivot[col] = 1;
|
|
1353
|
-
const basis = [];
|
|
1354
|
-
for (let free = 0; free < C; free++) {
|
|
1355
|
-
if (isPivot[free]) continue;
|
|
1356
|
-
const w = new Float64Array(C);
|
|
1357
|
-
w[free] = 1;
|
|
1358
|
-
pivots.forEach((col, i) => {
|
|
1359
|
-
const v = -rows[i][free];
|
|
1360
|
-
w[col] = Math.abs(v) < 1e-9 ? 0 : v;
|
|
1361
|
-
});
|
|
1362
|
-
w.own = free; // (its weight is 1 there, and no other vector's is: the row it replaces)
|
|
1363
|
-
basis.push(w);
|
|
1364
|
-
}
|
|
1365
|
-
basis.skipped = skipped;
|
|
1366
|
-
return basis;
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
// Every stretch either reaches a contact or is a mobile spectator: the steady equations can
|
|
1370
|
-
// be solved directly (dt = ∞), with the spectators' amounts as constraints.
|
|
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.
|
|
1371
854
|
_directSteady() {
|
|
1372
|
-
return this.stretches.every((st) => st.connected || st.conserved);
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
// Conservation rows for the spectators (steady solves only): Σ v c_i = amount over the
|
|
1376
|
-
// stretch replaces the balance row of its first node, which in steady state is the negative
|
|
1377
|
-
// sum of the others. That row is dense, so it's kept aside: the factorised matrix gets a pin
|
|
1378
|
-
// (identity row) there instead, and _solveBordered restores the constraint.
|
|
1379
|
-
_applyConstraints() {
|
|
1380
|
-
const { M, res } = this;
|
|
1381
|
-
for (const cs of this.constraints) {
|
|
1382
|
-
const { amount, reference, dV } = this._lawAmount(cs);
|
|
1383
|
-
cs.res = amount - (cs.terminals.length > 0 ? (this.lawRefs.get(cs.key) ?? amount) : reference);
|
|
1384
|
-
if (cs.circuit >= 0) {
|
|
1385
|
-
// In place of the terminal's circuit row (its I = 0).
|
|
1386
|
-
const C = this.termC[cs.circuit];
|
|
1387
|
-
C.fill(0);
|
|
1388
|
-
for (let j = 0; j < cs.len; j++) C[cs.idx[j]] += cs.w[j];
|
|
1389
|
-
this.termRes[cs.circuit] = cs.res;
|
|
1390
|
-
this.termDI[cs.circuit] = dV;
|
|
1391
|
-
continue;
|
|
1392
|
-
}
|
|
1393
|
-
const b0 = Math.floor(cs.row / M), r0 = cs.row % M;
|
|
1394
|
-
this._replaceRow(b0, r0);
|
|
1395
|
-
this._j(b0, r0, b0, r0, 1);
|
|
1396
|
-
res[this.rix[cs.row]] = 0;
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// A conserved combination's amount as the state holds it (and its reference, closed), with its
|
|
1401
|
-
// derivatives in cs.idx/w (and dV, by the voltage of a terminal whose electrode's charge it
|
|
1402
|
-
// counts).
|
|
1403
|
-
_lawAmount(cs) {
|
|
1404
|
-
const { n, M, c, dA: d } = this;
|
|
1405
|
-
const vol = this.model.grid.vol;
|
|
1406
|
-
let amount = 0, reference = 0, len = 0, gross = 0;
|
|
1407
|
-
for (const { stretch, w } of cs.parts) {
|
|
1408
|
-
const st = this.stretches[stretch], i = st.species;
|
|
1409
|
-
if (cs.terminals.length === 0) reference += w * this.referenceAmounts[stretch];
|
|
1410
|
-
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
1411
|
-
amount += w * vol[g] * c[g * n + i];
|
|
1412
|
-
gross += Math.abs(w * vol[g] * c[g * n + i]);
|
|
1413
|
-
if (this.nodeConductor[g] >= 0) {
|
|
1414
|
-
// A metal's carriers: the sheet at a charged face, vol·c = ±D_f·A/(zF), held by the
|
|
1415
|
-
// face's displacement.
|
|
1416
|
-
const f = this.sheetFace[g];
|
|
1417
|
-
if (f >= 0 && this.loc[this.blockOfFace[f] * M] >= 0) {
|
|
1418
|
-
cs.idx[len] = this.rix[this.blockOfFace[f] * M];
|
|
1419
|
-
cs.w[len++] = (w * this.sheetSign[g] * this.model.grid.area[g]) / (this.z[i] * FARADAY);
|
|
1420
|
-
}
|
|
1421
|
-
continue;
|
|
1422
|
-
}
|
|
1423
|
-
this._dc(g, i, d);
|
|
1424
|
-
const b = this.blockOfNode[g];
|
|
1425
|
-
for (let r = 0; r < M; r++) {
|
|
1426
|
-
if (d[r] === 0 || this.loc[b * M + r] < 0) continue;
|
|
1427
|
-
cs.idx[len] = this.rix[b * M + r];
|
|
1428
|
-
cs.w[len++] = w * vol[g] * d[r];
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
// What the electrodes' surfaces hold of it: Γ Σ v·a·θ, with ∂θ_s/∂η_x = θ_s (δ_sx − θ_x).
|
|
1433
|
-
for (const { col, w } of cs.surface) {
|
|
1434
|
-
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity, ns = port.surface.length;
|
|
1435
|
-
if (cs.terminals.length === 0) reference += w * this.surfaceRef[col];
|
|
1436
|
-
port.nodes.forEach((g, j) => {
|
|
1437
|
-
const f = w * G * vol[g] * this.portArea[k][j], b = this.blockOfNode[g], tq = this.th[g * this.nSurf + q];
|
|
1438
|
-
amount += f * tq;
|
|
1439
|
-
gross += Math.abs(f * tq);
|
|
1440
|
-
for (let x = 0; x < ns; x++) {
|
|
1441
|
-
cs.idx[len] = this.rix[b * M + 1 + n + x];
|
|
1442
|
-
cs.w[len++] = f * tq * ((q === x ? 1 : 0) - this.th[g * this.nSurf + x]);
|
|
1443
|
-
}
|
|
1444
|
-
});
|
|
1445
|
-
}
|
|
1446
|
-
// What an electrode holds, its charge Q/(zF) on the port's capacitance (none without one),
|
|
1447
|
-
// Q = Σ vol·a·C (V − zeroCharge − φ) over its nodes (as _portCapacitance has it).
|
|
1448
|
-
let dV = 0;
|
|
1449
|
-
for (const { k, w } of cs.carriers) {
|
|
1450
|
-
const port = this.model.ports[k - 2];
|
|
1451
|
-
if (!port.capacitance) continue;
|
|
1452
|
-
const f = w / (this.z[port.terminal] * FARADAY), { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k];
|
|
1453
|
-
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
1454
|
-
port.nodes.forEach((g, j) => {
|
|
1455
|
-
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
1456
|
-
const b = this.blockOfNode[g], s = vol[g] * this.portArea[k - 2][j] * Cs;
|
|
1457
|
-
amount += f * s * (V - zeroCharge - this.VT * (this.u[b * M] + this.uLo[b * M]));
|
|
1458
|
-
gross += Math.abs(f * s * (V - zeroCharge - this.VT * (this.u[b * M] + this.uLo[b * M])));
|
|
1459
|
-
dV += f * s;
|
|
1460
|
-
cs.idx[len] = this.rix[b * M];
|
|
1461
|
-
cs.w[len++] = -f * s * this.VT;
|
|
1462
|
-
});
|
|
1463
|
-
}
|
|
1464
|
-
cs.len = len;
|
|
1465
|
-
return { amount, reference, dV, gross };
|
|
855
|
+
return this.unkept.length === 0 && this.stretches.every((st) => st.connected || st.conserved);
|
|
1466
856
|
}
|
|
1467
857
|
|
|
1468
|
-
// The same amount at the step's start, as the storage terms have it (BDF2's c* included).
|
|
1469
|
-
_lawStart(cs) {
|
|
1470
|
-
const { n, nSurf } = this, vol = this.model.grid.vol;
|
|
1471
|
-
let a = 0;
|
|
1472
|
-
for (const { stretch, w } of cs.parts) {
|
|
1473
|
-
const st = this.stretches[stretch], i = st.species;
|
|
1474
|
-
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) a += w * vol[g] * this.cOld[g * n + i];
|
|
1475
|
-
}
|
|
1476
|
-
for (const { col, w } of cs.surface) {
|
|
1477
|
-
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity;
|
|
1478
|
-
port.nodes.forEach((g, j) => (a += w * G * vol[g] * this.portArea[k][j] * this.thOld[g * nSurf + q]));
|
|
1479
|
-
}
|
|
1480
|
-
for (const { k, w } of cs.carriers) if (this.model.ports[k - 2].capacitance) a += (w * this.portQStart[k - 2]) / (this.z[this.model.ports[k - 2].terminal] * FARADAY);
|
|
1481
|
-
return a;
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
// Time steps: a terminal driven by a current that alone feeds a conserved combination can read
|
|
1485
|
-
// its current as the combination's change, F (S − S*)/(λ dt), which the step's balance rows sum
|
|
1486
|
-
// to exactly, rather than as the flux through it. Both are exact; they differ in round-off and
|
|
1487
|
-
// in what Newton can see. Through a metal, the flux is G Δη, its two η all but equal: a floating
|
|
1488
|
-
// gate's charging current fell below that at steps of ~0.03 s, and the step came out singular.
|
|
1489
|
-
// Over long steps the change is the better kept (a closed host at open circuit, read by its
|
|
1490
|
-
// flux, drifted from its charge by 3 mV over 5e11 s, and its steps failed by 4e12 s). Over
|
|
1491
|
-
// short ones it's the amount's round-off over dt (a battery's cathode holds 0.04 mol/m² of
|
|
1492
|
-
// electrons: 1e-6 A/m² at steps of 1e-6 s, enough to reject steps at tol 1e-8), where the flux,
|
|
1493
|
-
// its levels carried in two words (u + uLo), is far cleaner than ε G. So a step reads the change
|
|
1494
|
-
// where that's ε·F·S/(λ dt) below 1e-6 of the flux's ε G, as its start has them, keeps to it
|
|
1495
|
-
// throughout, and where a step read by the flux fails, it's tried once more read by the change
|
|
1496
|
-
// (see step).
|
|
1497
|
-
_circuitLaws(dt) {
|
|
1498
|
-
for (const cs of [...this.constraints, ...this.timeLaws]) {
|
|
1499
|
-
const k = cs.circuit;
|
|
1500
|
-
if (k < 0 || !this.floating.includes(k)) continue;
|
|
1501
|
-
const { amount, dV, gross } = this._lawAmount(cs), f = FARADAY / (cs.lambda * dt), C = this.termC[k];
|
|
1502
|
-
if (!this.lawForm.has(cs)) {
|
|
1503
|
-
let G = 0;
|
|
1504
|
-
for (let j = 0; j < C.length; j++) G = Math.max(G, Math.abs(C[j]));
|
|
1505
|
-
this.lawForm.set(cs, Math.abs(f) * gross < 1e-6 * G);
|
|
1506
|
-
}
|
|
1507
|
-
if (!this.lawForm.get(cs)) continue;
|
|
1508
|
-
C.fill(0);
|
|
1509
|
-
for (let j = 0; j < cs.len; j++) C[cs.idx[j]] += f * cs.w[j];
|
|
1510
|
-
const d = this.terms[k].drive, src = sourceAt(d.src, this.sourceTime, this.sourceBefore);
|
|
1511
|
-
this.termRes[k] = f * (amount - this._lawStart(cs)) - (d.kind === 'I' ? src : (src - this.termV[k]) / d.R);
|
|
1512
|
-
this.termDI[k] = f * dV + (d.kind === 'I' ? 0 : 1 / d.R);
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
// Solve the full Newton system, J δ = rhs, with the extra unknowns and rows that don't fit the
|
|
1517
|
-
// block-tridiagonal matrix T: the floating terminals' voltages (with their circuit rows), and
|
|
1518
|
-
// the rows pinned in T: the spectators' conserved amounts (see _applyConstraints) and the
|
|
1519
|
-
// islands' summed balances (see _applyIslands). By low-rank
|
|
1520
|
-
// updates of T (Woodbury): δ = y + Σ_q Q_q μ_q − Σ_k X_k δV_k with y = T⁻¹ rhs, Q_q = T⁻¹ e_q
|
|
1521
|
-
// (the response to a unit pin) and X_k = T⁻¹ B_k (to a unit change of V_k), then a small dense
|
|
1522
|
-
// system for the μ (pins) and δV (terminals):
|
|
1523
|
-
// pin q: Σ (W_q·Q_q′) μ_q′ − Σ (W_q·X_k) δV_k = res_q − W_q·y
|
|
1524
|
-
// terminal k: Σ (C_k·Q_q) μ_q + Σ (δ_kk′ ∂I_k/∂V_k − C_k·X_k′) δV_k′ = res_k − C_k·y
|
|
1525
|
-
_solveBordered(rhs, delta, deltaV, pins) {
|
|
1526
|
-
const N = this.sys.size, fl = this.floating, P = pins.length, K = fl.length, S = P + K;
|
|
1527
|
-
this._solveLinear(rhs, delta);
|
|
1528
|
-
if (S === 0) return;
|
|
1529
|
-
const e = this.dWork ?? (this.dWork = new Float64Array(N + 1));
|
|
1530
|
-
const cols = this.borderCols ?? (this.borderCols = []);
|
|
1531
|
-
for (let a = 0; a < S; a++) {
|
|
1532
|
-
if (!cols[a]) cols[a] = new Float64Array(N + 1);
|
|
1533
|
-
if (a < P) {
|
|
1534
|
-
// The response to a unit pin, or where the pin moves a flat stretch, to its shift.
|
|
1535
|
-
const col = pins[a].col ?? [pins[a].row];
|
|
1536
|
-
for (const o of col) e[this.rix[o]] = 1;
|
|
1537
|
-
this._solveLinear(e, cols[a]);
|
|
1538
|
-
for (const o of col) e[this.rix[o]] = 0;
|
|
1539
|
-
} else this._solveLinear(this.termB[fl[a - P]], cols[a]);
|
|
1540
|
-
}
|
|
1541
|
-
// Row a of the small system, as a dot product with a compact vector (sparse for pins).
|
|
1542
|
-
const dot = (a, v) => {
|
|
1543
|
-
let t = 0;
|
|
1544
|
-
if (a < P) {
|
|
1545
|
-
const p = pins[a];
|
|
1546
|
-
for (let j = 0; j < p.len; j++) t += p.w[j] * v[p.idx[j]];
|
|
1547
|
-
} else {
|
|
1548
|
-
const C = this.termC[fl[a - P]];
|
|
1549
|
-
for (let j = 0; j < N; j++) t += C[j] * v[j];
|
|
1550
|
-
}
|
|
1551
|
-
return t;
|
|
1552
|
-
};
|
|
1553
|
-
const A = Array.from({ length: S }, () => new Float64Array(S + 1));
|
|
1554
|
-
for (let a = 0; a < S; a++) {
|
|
1555
|
-
for (let b = 0; b < S; b++) A[a][b] = (b < P ? 1 : -1) * dot(a, cols[b]);
|
|
1556
|
-
if (a >= P) A[a][a] += this.termDI[fl[a - P]];
|
|
1557
|
-
A[a][S] = (a < P ? pins[a].res : this.termRes[fl[a - P]]) - dot(a, delta);
|
|
1558
|
-
let mx = 0;
|
|
1559
|
-
for (let b = 0; b < S; b++) mx = Math.max(mx, Math.abs(A[a][b]));
|
|
1560
|
-
if (mx > 0) for (let b = 0; b <= S; b++) A[a][b] /= mx;
|
|
1561
|
-
}
|
|
1562
|
-
// Small dense solve with partial pivoting.
|
|
1563
|
-
for (let col = 0; col < S; col++) {
|
|
1564
|
-
let p = col;
|
|
1565
|
-
for (let r = col + 1; r < S; r++) if (Math.abs(A[r][col]) > Math.abs(A[p][col])) p = r;
|
|
1566
|
-
[A[col], A[p]] = [A[p], A[col]];
|
|
1567
|
-
if (A[col][col] === 0) throw new SolverError('the bordered system is singular: a conserved amount or a floating terminal is not determined');
|
|
1568
|
-
for (let r = col + 1; r < S; r++) {
|
|
1569
|
-
const f = A[r][col] / A[col][col];
|
|
1570
|
-
for (let k = col; k <= S; k++) A[r][k] -= f * A[col][k];
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
const x = new Float64Array(S);
|
|
1574
|
-
for (let r = S - 1; r >= 0; r--) {
|
|
1575
|
-
let v = A[r][S];
|
|
1576
|
-
for (let k = r + 1; k < S; k++) v -= A[r][k] * x[k];
|
|
1577
|
-
x[r] = v / A[r][r];
|
|
1578
|
-
}
|
|
1579
|
-
for (let a = 0; a < S; a++) {
|
|
1580
|
-
const sa = a < P ? x[a] : -x[a], col = cols[a];
|
|
1581
|
-
for (let j = 0; j < N; j++) delta[j] += sa * col[j];
|
|
1582
|
-
if (a >= P) deltaV[a - P] = x[a];
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
858
|
// An electrode surface at its starting coverages at node g: η = μ°/RT + ln(θ/θ₀).
|
|
1588
859
|
_surfaceStart(g) {
|
|
1589
860
|
const { n, M, u, model } = this, surf = model.ports[this.surfPort[g]].surface, b = this.blockOfNode[g];
|
|
@@ -1616,7 +887,7 @@ export class Solver {
|
|
|
1616
887
|
this.floating = this.terms.flatMap((t, k) => (t.drive.kind === 'I' || t.drive.R > 0 ? [k] : []));
|
|
1617
888
|
if (this.floating.length !== before.length) this.deltaV = null;
|
|
1618
889
|
// A terminal newly driven by a current (or no longer) can close a combination behind it.
|
|
1619
|
-
// (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)
|
|
1620
891
|
this.drivenKey = this._drivenKey();
|
|
1621
892
|
if (this.drivenKey !== driven) this._laws();
|
|
1622
893
|
this._refreshSources();
|
|
@@ -1624,17 +895,17 @@ export class Solver {
|
|
|
1624
895
|
}
|
|
1625
896
|
|
|
1626
897
|
// After a steady solve: each reaction that alone changes some combination nothing else feeds
|
|
1627
|
-
// (see
|
|
898
|
+
// (see src/laws.js), whose total rate a steady state therefore sets to zero, but whose affinity has one
|
|
1628
899
|
// sign wherever it acts, so that its total can't be zero. Too slow to show, its rate lost to
|
|
1629
900
|
// round-off, it was left where the solve began: a slow complexation 43 mV off, an electrode
|
|
1630
901
|
// port's slow couple at its starting composition, each converged without a word. (A face
|
|
1631
902
|
// reaction stands on its affinity, so it's only caught here if that couldn't be arranged.)
|
|
1632
903
|
stuckReactions() {
|
|
1633
904
|
const { n, M, u, uLo, model } = this, grid = model.grid, S = this.stretches.length, X = S + this.surfCols.length, out = [];
|
|
1634
|
-
const passes = (k) => !
|
|
1635
|
-
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])))));
|
|
1636
907
|
const needed = (rx) => {
|
|
1637
|
-
const basis = this.
|
|
908
|
+
const basis = conservedMoieties(this._structure(), { drives: true, skip: rx });
|
|
1638
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));
|
|
1639
910
|
};
|
|
1640
911
|
const eta = (g, slot) => u[this.blockOfNode[g] * M + slot] + uLo[this.blockOfNode[g] * M + slot];
|
|
@@ -1667,15 +938,11 @@ export class Solver {
|
|
|
1667
938
|
return out;
|
|
1668
939
|
}
|
|
1669
940
|
|
|
1670
|
-
// 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,
|
|
1671
942
|
// behind a resistance or not, and a current that's held at zero throughout (which feeds a
|
|
1672
943
|
// combination nothing, so a reaction's affinity may stand in for its summed balance).
|
|
1673
944
|
_drivenKey() {
|
|
1674
|
-
return this.terms.map((t) => `${t.drive.kind}${t.drive.R > 0 ? 'R' : ''}${
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
_noCurrent(t) {
|
|
1678
|
-
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();
|
|
1679
946
|
}
|
|
1680
947
|
|
|
1681
948
|
// Held terminals take their sources' values at sourceTime (a floating one keeps its voltage,
|
|
@@ -2182,7 +1449,7 @@ export class Solver {
|
|
|
2182
1449
|
res.fill(0);
|
|
2183
1450
|
this.dtNow = dt;
|
|
2184
1451
|
if (this.lin) this.lin.reset(1 / dt);
|
|
2185
|
-
if (this.steady && dt === Infinity) this
|
|
1452
|
+
if (this.steady && dt === Infinity) snapFlat(this);
|
|
2186
1453
|
this.computeConcentrations();
|
|
2187
1454
|
|
|
2188
1455
|
// Regions, each assembled by the kernel for its kind: a conductor (its carrier only), a
|
|
@@ -2214,16 +1481,9 @@ export class Solver {
|
|
|
2214
1481
|
// Terminals: ports (after every other term at their nodes, so a held level can read its
|
|
2215
1482
|
// flux), then contacts, then the circuit rows of the floating ones.
|
|
2216
1483
|
this._terminals(dt);
|
|
2217
|
-
|
|
2218
|
-
//
|
|
2219
|
-
|
|
2220
|
-
const steady = this.steady && dt === Infinity;
|
|
2221
|
-
if (steady) this._flatRows();
|
|
2222
|
-
if (steady && this.affinities.length > 0) this._applyAffinities();
|
|
2223
|
-
if (steady && this.constraints.length > 0) this._applyConstraints();
|
|
2224
|
-
if (steady && this.localConstraints.length > 0) this._applyLocalConstraints();
|
|
2225
|
-
this.pinnedIslands = steady && this.islandsOn ? this._applyIslands() : [];
|
|
2226
|
-
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);
|
|
2227
1487
|
this.transformed = this.combining && dt !== Infinity;
|
|
2228
1488
|
if (this.transformed) this._chargeRows(dt);
|
|
2229
1489
|
}
|
|
@@ -3082,6 +2342,7 @@ export class Solver {
|
|
|
3082
2342
|
this.termC[k].fill(0);
|
|
3083
2343
|
}
|
|
3084
2344
|
this.termDI.fill(0);
|
|
2345
|
+
for (const x of this.termDX) x.fill(0);
|
|
3085
2346
|
this.termI.fill(0);
|
|
3086
2347
|
this.qg.fill(0);
|
|
3087
2348
|
this.qgSlope.fill(0);
|
|
@@ -3281,7 +2542,7 @@ export class Solver {
|
|
|
3281
2542
|
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
3282
2543
|
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
3283
2544
|
// Its current, the charging (Q − Q at the step's start)/dt; or, steady at no current, the
|
|
3284
|
-
// charge it keeps in place of that (see
|
|
2545
|
+
// charge it keeps in place of that (see captureLaws in src/lawRows.js).
|
|
3285
2546
|
const rate = Number.isFinite(dt) ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
3286
2547
|
let Q = 0, dQdV = 0;
|
|
3287
2548
|
port.nodes.forEach((g, w) => {
|
|
@@ -3413,6 +2674,14 @@ export class Solver {
|
|
|
3413
2674
|
// φ law (none where φ is undefined: nothing at the end node responds to it). The displacement
|
|
3414
2675
|
// into the device, D_in = sgn·D, adds (D_in − D_in at the step's start)/dt to the current.
|
|
3415
2676
|
const link = ct.phi, Dstart = sgn * this.contactDStart[side];
|
|
2677
|
+
// (In a steady solve, what a law in this circuit row counts of D_in: see applyConstraints in
|
|
2678
|
+
// src/lawRows.js.)
|
|
2679
|
+
const q = !dyn && this.steady && this.contactLaw[k] ? (this.contactQ[k] ??= { row: new Float64Array(this.sys.size + 1), D: 0, dV: 0 }) : null;
|
|
2680
|
+
if (q) {
|
|
2681
|
+
q.row.fill(0);
|
|
2682
|
+
q.D = 0;
|
|
2683
|
+
q.dV = 0;
|
|
2684
|
+
}
|
|
3416
2685
|
if (this.phiUndefined[g]) {
|
|
3417
2686
|
this.contactD[side] = 0;
|
|
3418
2687
|
} else if (link.type === 'capacitive') {
|
|
@@ -3422,8 +2691,9 @@ export class Solver {
|
|
|
3422
2691
|
this._j(b, 0, b, 0, Ac * link.C * VT);
|
|
3423
2692
|
B[R[b * M]] += -Ac * link.C;
|
|
3424
2693
|
this.contactD[side] = sgn * Din;
|
|
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];
|
|
3425
2695
|
// Its current, the charging (D_in − D_in at the step's start)/dt; or, steady at no current,
|
|
3426
|
-
// the charge it keeps in place of that (see
|
|
2696
|
+
// the charge it keeps in place of that (see captureLaws in src/lawRows.js).
|
|
3427
2697
|
const rate = dyn ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
3428
2698
|
if (rate > 0) {
|
|
3429
2699
|
I += (Din - (dyn ? Dstart : this.chargeRefs.get(k))) * rate;
|
|
@@ -3434,6 +2704,10 @@ export class Solver {
|
|
|
3434
2704
|
// The Poisson residual is the outside's charge, D_in.
|
|
3435
2705
|
const Din = res[R[b * M]];
|
|
3436
2706
|
this.contactD[side] = sgn * Din;
|
|
2707
|
+
if (q) {
|
|
2708
|
+
q.D = Din;
|
|
2709
|
+
this._captureRow(b, 0, 1, q.row);
|
|
2710
|
+
}
|
|
3437
2711
|
if (dyn && !(this.combining && this.lateStorage[g] === 1)) {
|
|
3438
2712
|
I += (Din - Dstart) / dt;
|
|
3439
2713
|
this._captureRow(b, 0, 1 / dt, C);
|
|
@@ -3470,92 +2744,6 @@ export class Solver {
|
|
|
3470
2744
|
|
|
3471
2745
|
|
|
3472
2746
|
|
|
3473
|
-
// Steady solves: each flat stretch's level held at its contact's throughout (see
|
|
3474
|
-
// flatStretches), in place of its balances, as an equilibrium link holds it at the end node.
|
|
3475
|
-
// A flat stretch's level: its contact's, through the terminal k that holds it, or (reaching
|
|
3476
|
-
// none) its first node's, which its amount fixes (that row is the amount's, a pin whose
|
|
3477
|
-
// response moves the whole stretch: see _applyConstraints).
|
|
3478
|
-
_flatLevel(st) {
|
|
3479
|
-
const { M, u, uLo, z, VT, model } = this, i = st.species;
|
|
3480
|
-
if (st.leftOpen || st.rightOpen) {
|
|
3481
|
-
const side = st.leftOpen ? 'left' : 'right', link = model.contacts[side].species[i], k = side === 'left' ? 0 : 1;
|
|
3482
|
-
return { k, hi: z[i] === 0 ? link.mu / model.RT : (z[i] * (this.termV[k] + link.offset)) / VT, lo: 0 };
|
|
3483
|
-
}
|
|
3484
|
-
const o = this.blockOfNode[st.nodes[0]] * M + 1 + i;
|
|
3485
|
-
return { k: -1, hi: u[o], lo: uLo[o] };
|
|
3486
|
-
}
|
|
3487
|
-
|
|
3488
|
-
// Each flat stretch's nodes set to its first node's level exactly (where Newton's damped
|
|
3489
|
-
// updates have taken it, on the way to its contact's), so that it carries exactly no flux (the
|
|
3490
|
-
// expm1 form of a zero difference). Flat only to round-off, a level moved by its contact had
|
|
3491
|
-
// read as a current: a MOS capacitor's holes, their contact jumped to −6 V, as
|
|
3492
|
-
// 3e-4 A/m² through a bulk that conducts ~1e10 S/m² (the true current was 3.5e-7).
|
|
3493
|
-
_snapFlat() {
|
|
3494
|
-
const { M, u, uLo } = this;
|
|
3495
|
-
for (const st of this.flatStretches) {
|
|
3496
|
-
const f = this.blockOfNode[st.nodes[0]] * M + 1 + st.species, hi = u[f], lo = uLo[f];
|
|
3497
|
-
for (let g = st.nodes[0] + 1; g <= st.nodes[1]; g++) {
|
|
3498
|
-
const o = this.blockOfNode[g] * M + 1 + st.species;
|
|
3499
|
-
if (this.loc[o] < 0) continue;
|
|
3500
|
-
u[o] = hi;
|
|
3501
|
-
uLo[o] = lo;
|
|
3502
|
-
}
|
|
3503
|
-
}
|
|
3504
|
-
}
|
|
3505
|
-
|
|
3506
|
-
_flatRows() {
|
|
3507
|
-
const { M, u, uLo, res, z, VT, model } = this, R = this.rix;
|
|
3508
|
-
for (const st of this.flatStretches) {
|
|
3509
|
-
const i = st.species, open = st.leftOpen || st.rightOpen;
|
|
3510
|
-
const { k, hi, lo } = this._flatLevel(st);
|
|
3511
|
-
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
3512
|
-
const b = this.blockOfNode[g], o = b * M + 1 + i;
|
|
3513
|
-
if (this.loc[o] < 0) continue;
|
|
3514
|
-
this._replaceRow(b, 1 + i);
|
|
3515
|
-
for (const B of this.termB) B[R[o]] = 0; // (the row's old terminal terms: the end node's own link)
|
|
3516
|
-
this._j(b, 1 + i, b, 1 + i, 1);
|
|
3517
|
-
res[R[o]] = u[o] - hi + (uLo[o] - lo); // (the big terms first: hi + lo rounds to hi)
|
|
3518
|
-
if (open && z[i] !== 0) this.termB[k][R[o]] += -z[i] / VT;
|
|
3519
|
-
}
|
|
3520
|
-
// A face inside the stretch whose link holds the level continuous carries the species' flux
|
|
3521
|
-
// as an unknown that only the edge balances, now replaced, determined: it's zero.
|
|
3522
|
-
for (let f = st.regions[0]; f < st.regions[1]; f++) {
|
|
3523
|
-
if (model.interfaces[f].links[i].type !== 'equilibrium') continue;
|
|
3524
|
-
const b = this.blockOfFace[f], o = b * M + 1 + i;
|
|
3525
|
-
if (this.loc[o] < 0) continue;
|
|
3526
|
-
this._replaceRow(b, 1 + i);
|
|
3527
|
-
for (const B of this.termB) B[R[o]] = 0;
|
|
3528
|
-
this._j(b, 1 + i, b, 1 + i, 1);
|
|
3529
|
-
res[R[o]] = u[o] + uLo[o];
|
|
3530
|
-
}
|
|
3531
|
-
}
|
|
3532
|
-
}
|
|
3533
|
-
|
|
3534
|
-
// Steady solves: each reaction that alone changes a combination (see _laws) held at its
|
|
3535
|
-
// equilibrium, a = fixedA − Σ ν η = 0, in place of one of that combination's balance rows (a pin,
|
|
3536
|
-
// like an amount's).
|
|
3537
|
-
_applyAffinities() {
|
|
3538
|
-
const { M, u, uLo, res } = this, grid = this.model.grid;
|
|
3539
|
-
for (const af of this.affinities) {
|
|
3540
|
-
const b0 = Math.floor(af.row / M), r0 = af.row % M;
|
|
3541
|
-
this._replaceRow(b0, r0);
|
|
3542
|
-
this._j(b0, r0, b0, r0, 1);
|
|
3543
|
-
res[this.rix[af.row]] = 0;
|
|
3544
|
-
const gL = grid.regionEnd[af.f], gR = grid.regionStart[af.f + 1];
|
|
3545
|
-
let hi = af.rx.fixedA, lo = 0, len = 0;
|
|
3546
|
-
for (const p of af.rx.part) {
|
|
3547
|
-
const o = this.blockOfNode[p.side ? gR : gL] * M + 1 + p.i;
|
|
3548
|
-
hi -= p.nu * u[o];
|
|
3549
|
-
lo -= p.nu * uLo[o];
|
|
3550
|
-
if (this.loc[o] < 0) continue;
|
|
3551
|
-
af.idx[len] = this.rix[o];
|
|
3552
|
-
af.w[len++] = -p.nu;
|
|
3553
|
-
}
|
|
3554
|
-
af.len = len;
|
|
3555
|
-
af.res = hi + lo;
|
|
3556
|
-
}
|
|
3557
|
-
}
|
|
3558
|
-
|
|
3559
2747
|
// Zero one row of the Jacobian (all three blocks), ready to be replaced.
|
|
3560
2748
|
_replaceRow(b, rs) {
|
|
3561
2749
|
const l = this.loc[b * this.M + rs];
|
|
@@ -3620,18 +2808,19 @@ export class Solver {
|
|
|
3620
2808
|
// entries; where they're huge (an inversion layer's 0.1 nm cells, a conductance ~1e11 against
|
|
3621
2809
|
// the layer's own storage and the trickle from the bulk), eliminating them loses the layer's
|
|
3622
2810
|
// overall level to round-off, and Newton rattles there. delta and deltaV come in as the plain
|
|
3623
|
-
// 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
|
|
3624
2812
|
// if refining didn't help, leaving them as they came.
|
|
3625
2813
|
_refine(dt, delta, deltaV) {
|
|
3626
2814
|
const { res, termRes, termB, termC, termDI, rowDiv, floating: fl } = this;
|
|
3627
2815
|
const N = this.sys.size, K = fl.length, pins = this.pins, R = this.rix;
|
|
3628
|
-
const save = { res: Float64Array.from(res), termRes: Float64Array.from(termRes), termB: termB.map((b) => Float64Array.from(b)), termC: termC.map((c) => Float64Array.from(c)), termDI: Float64Array.from(termDI), transformed: this.transformed, pinRes: this.pins.map((p) => p.res) };
|
|
2816
|
+
const save = { res: Float64Array.from(res), termRes: Float64Array.from(termRes), termB: termB.map((b) => Float64Array.from(b)), termC: termC.map((c) => Float64Array.from(c)), termDI: Float64Array.from(termDI), termDX: this.termDX.map((x) => Float64Array.from(x)), transformed: this.transformed, pinRes: this.pins.map((p) => p.res) };
|
|
3629
2817
|
const restoreArrays = () => {
|
|
3630
2818
|
res.set(save.res);
|
|
3631
2819
|
termRes.set(save.termRes);
|
|
3632
2820
|
save.termB.forEach((b, k) => termB[k].set(b));
|
|
3633
2821
|
save.termC.forEach((c, k) => termC[k].set(c));
|
|
3634
2822
|
termDI.set(save.termDI);
|
|
2823
|
+
save.termDX.forEach((x, k) => this.termDX[k].set(x));
|
|
3635
2824
|
this.transformed = save.transformed;
|
|
3636
2825
|
this.pins.forEach((p, a) => (p.res = save.pinRes[a]));
|
|
3637
2826
|
};
|
|
@@ -3646,7 +2835,7 @@ export class Solver {
|
|
|
3646
2835
|
rest.multiply(v, jv);
|
|
3647
2836
|
if (save.transformed) this._untransform(v); // (the dilute terms in the plain unknowns)
|
|
3648
2837
|
lin.apply(v, jv, 1, 1 / dt);
|
|
3649
|
-
// 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).
|
|
3650
2839
|
for (const p of pins) {
|
|
3651
2840
|
if (!p.col) continue;
|
|
3652
2841
|
for (const o of p.col) if (o !== p.row && this.loc[o] >= 0) jv[R[o]] -= x[R[p.row]];
|
|
@@ -3659,6 +2848,7 @@ export class Solver {
|
|
|
3659
2848
|
}
|
|
3660
2849
|
for (let a = 0; a < K; a++) {
|
|
3661
2850
|
let t = Dk[a] * x[N + a];
|
|
2851
|
+
for (let b = 0; b < K; b++) if (b !== a) t += this.termDX[fl[a]][fl[b]] * x[N + b];
|
|
3662
2852
|
for (let j = 0; j < N; j++) t += Ck[a][j] * x[j];
|
|
3663
2853
|
out[N + a] = t;
|
|
3664
2854
|
}
|
|
@@ -3678,7 +2868,7 @@ export class Solver {
|
|
|
3678
2868
|
p.res = rhs[R[p.row]];
|
|
3679
2869
|
rhs[R[p.row]] = 0;
|
|
3680
2870
|
}
|
|
3681
|
-
this
|
|
2871
|
+
solveBordered(this, rhs, out, outV, pins);
|
|
3682
2872
|
const z = new Float64Array(N + K);
|
|
3683
2873
|
for (let j = 0; j < N; j++) z[j] = out[j];
|
|
3684
2874
|
for (let a = 0; a < K; a++) z[N + a] = outV[a];
|
|
@@ -3740,7 +2930,8 @@ export class Solver {
|
|
|
3740
2930
|
const history = [], ownHistory = [];
|
|
3741
2931
|
// Refined solves (see _refine), once the plain ones stall.
|
|
3742
2932
|
let refine = false, refined = 0, noRefine = false;
|
|
3743
|
-
// Islands (see
|
|
2933
|
+
// Islands (see applyIslands in src/lawRows.js): their summed rows go in only once the plain
|
|
2934
|
+
// solves converge.
|
|
3744
2935
|
// Far from the solution, Newton exact along a weakly held level takes it by 1e5 thermal
|
|
3745
2936
|
// units (a floating base, cold at bias), where the plain solves, which barely see that level,
|
|
3746
2937
|
// converge. Converged, an island at the wrong level shows: it takes in a different current
|
|
@@ -3820,7 +3011,7 @@ export class Solver {
|
|
|
3820
3011
|
try {
|
|
3821
3012
|
const pins = this.pins;
|
|
3822
3013
|
deltaV.fill(0);
|
|
3823
|
-
this
|
|
3014
|
+
solveBordered(this, res, delta, deltaV, pins);
|
|
3824
3015
|
if (refine && !this._refine(dt, delta, deltaV)) {
|
|
3825
3016
|
refine = false; // (GMRES didn't converge, or strayed: the plain solves it is)
|
|
3826
3017
|
noRefine = true;
|
|
@@ -4012,7 +3203,7 @@ export class Solver {
|
|
|
4012
3203
|
this.sourceTime = this.steady ? this.steadyAt : tEnd;
|
|
4013
3204
|
this.sourceBefore = !this.steady;
|
|
4014
3205
|
this.combining = true;
|
|
4015
|
-
this.lawForm = new Map(); // (see
|
|
3206
|
+
this.lawForm = new Map(); // (see circuitLaws in src/lawRows.js)
|
|
4016
3207
|
let result;
|
|
4017
3208
|
try {
|
|
4018
3209
|
result = this.newton(dtEff, opts);
|
|
@@ -4029,7 +3220,8 @@ export class Solver {
|
|
|
4029
3220
|
}
|
|
4030
3221
|
if (!result.converged && Number.isFinite(dtEff) && [...this.lawForm.values()].some((v) => !v)) {
|
|
4031
3222
|
// A terminal read by its flux whose response is lost to round-off (a floating gate's
|
|
4032
|
-
// 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).
|
|
4033
3225
|
this.u.set(this.uPrev);
|
|
4034
3226
|
this.uLo.set(this.uPrevLo);
|
|
4035
3227
|
this.termV.set(this.termVPrev);
|
|
@@ -5057,7 +4249,8 @@ export class Solver {
|
|
|
5057
4249
|
const last = this.model.regions.length - 1;
|
|
5058
4250
|
const { n, cOld } = this, vol = this.model.grid.vol;
|
|
5059
4251
|
// A stretch alone in what a terminal driven by a current feeds takes in what that current
|
|
5060
|
-
// 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.
|
|
5061
4254
|
const driven = new Map();
|
|
5062
4255
|
for (const cs of this.constraints) {
|
|
5063
4256
|
if (cs.circuit < 0 || this.terms[cs.circuit].drive.kind !== 'I' || cs.parts.length !== 1 || cs.surface.length || cs.carriers.length) continue;
|
|
@@ -5102,7 +4295,7 @@ export class Solver {
|
|
|
5102
4295
|
* Steady state: where a transient from the present state ends, found as the solution of the
|
|
5103
4296
|
* steady equations (dt = ∞, no storage term), with no slow modes to wait out, however slow the
|
|
5104
4297
|
* physics (e.g. exponentially scarce minority carriers). What the storage term held is held by
|
|
5105
|
-
* 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
|
|
5106
4299
|
* row that says nothing new, levels that carry nothing held flat, islands' summed balances.
|
|
5107
4300
|
* Where Newton from here fails: continuation along the drives, then the pseudo-transient ramp.
|
|
5108
4301
|
* The clock is not advanced, and open-system conservation bookkeeping restarts here.
|
|
@@ -5112,7 +4305,7 @@ export class Solver {
|
|
|
5112
4305
|
solveSteady(opts = {}) {
|
|
5113
4306
|
this.unreached = null; // (a driven current no held voltage reaches: see _floatingContinuation)
|
|
5114
4307
|
this.charging = null; // (a driven current that only charges: see _solveSteadyAll)
|
|
5115
|
-
this
|
|
4308
|
+
captureLaws(this);
|
|
5116
4309
|
this.steady = true; // (the steady system's rows; sources at the present time)
|
|
5117
4310
|
this.steadyAt = this.time; // (which a pseudo-transient's steps don't move on)
|
|
5118
4311
|
try {
|
|
@@ -5131,7 +4324,7 @@ export class Solver {
|
|
|
5131
4324
|
const I = sourceAt(this.terms[k].drive.src, this.time);
|
|
5132
4325
|
if (this.terms[k].drive.kind !== 'I' || I === 0) continue;
|
|
5133
4326
|
const laws = this.constraints.filter((cs) => cs.terminals.includes(k));
|
|
5134
|
-
if (laws.length === 0 && !this
|
|
4327
|
+
if (laws.length === 0 && !chargeOnly(this, k)) continue;
|
|
5135
4328
|
const what = laws.flatMap((cs) => cs.parts.map(({ stretch }) => {
|
|
5136
4329
|
const st = this.stretches[stretch], [r0, r1] = st.regions;
|
|
5137
4330
|
return `${species[st.species].name} in ${regions[r0].name}${r1 > r0 ? `…${regions[r1].name}` : ''}`;
|
|
@@ -5151,7 +4344,7 @@ export class Solver {
|
|
|
5151
4344
|
_steadyFromHere(opts) {
|
|
5152
4345
|
this.conditioning = null;
|
|
5153
4346
|
this.computeConcentrations();
|
|
5154
|
-
this
|
|
4347
|
+
captureLocal(this); // immobile combinations keep, node by node, what they hold now
|
|
5155
4348
|
this.sourceTime = this.time;
|
|
5156
4349
|
// Continuation applies where the right terminal is held at a voltage, against a held left one.
|
|
5157
4350
|
const [dl, dr] = [this.terms[0].drive, this.terms[1].drive];
|
|
@@ -5247,20 +4440,7 @@ export class Solver {
|
|
|
5247
4440
|
// only charges a capacitance (so that its steady states are one per amount or charge, not one
|
|
5248
4441
|
// per voltage).
|
|
5249
4442
|
_bound(k) {
|
|
5250
|
-
return this.constraints.some((cs) => cs.terminals.includes(k)) || this
|
|
5251
|
-
}
|
|
5252
|
-
|
|
5253
|
-
// Whether terminal k passes current only by charging a capacitance: no charged species through
|
|
5254
|
-
// it (nor, a port, reactions), and a capacitance to charge (a gate's φ law, a port's own).
|
|
5255
|
-
_chargeOnly(k) {
|
|
5256
|
-
const t = this.terms[k], { species } = this.model;
|
|
5257
|
-
const ions = (links) => links.some((l, i) => l.type !== 'blocked' && species[i].z !== 0);
|
|
5258
|
-
if (t.kind === 'port') {
|
|
5259
|
-
const p = this.model.ports[t.index];
|
|
5260
|
-
return p.reactions.length === 0 && !ions(p.species) && !!p.capacitance;
|
|
5261
|
-
}
|
|
5262
|
-
const c = this.model.contacts[t.side];
|
|
5263
|
-
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);
|
|
5264
4444
|
}
|
|
5265
4445
|
|
|
5266
4446
|
// A cold steady solve with a terminal driven by a current, which can fail where the same state
|