driftlet 0.14.0 → 0.16.4
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 +12 -1
- package/docs/alignment.md +6 -3
- package/docs/data.md +3 -1
- package/docs/device.md +102 -33
- package/docs/kit.md +55 -17
- package/docs/numerics.md +244 -121
- package/docs/reliability.md +15 -9
- package/docs/statistics.md +28 -4
- package/llms.txt +73 -41
- package/package.json +1 -1
- package/src/blockTridiagonal.js +85 -7
- package/src/checks.js +83 -6
- package/src/describe.js +46 -5
- package/src/device.js +33 -10
- package/src/index.js +23 -10
- package/src/particles.js +131 -55
- package/src/solution.js +164 -8
- package/src/solver.js +923 -277
- package/src/sources.js +10 -2
- package/src/stack.js +16 -6
- package/src/statistics.js +7 -3
- package/types/blockTridiagonal.d.ts +1 -0
- package/types/checks.d.ts +8 -2
- package/types/index.d.ts +2 -2
- package/types/particles.d.ts +3 -6
- package/types/solver.d.ts +50 -29
- package/types/stack.d.ts +6 -4
package/src/solver.js
CHANGED
|
@@ -478,6 +478,7 @@ export class Solver {
|
|
|
478
478
|
model.ports.forEach((port, k) => port.capacitance && port.nodes.forEach((g, w) => this.qgTerms[g].push([2 + k, w])));
|
|
479
479
|
this.contactDOld = { left: 0, right: 0 };
|
|
480
480
|
this.contactDStart = { left: 0, right: 0 };
|
|
481
|
+
this.lawForm = new Map(); // (each circuit law: read as its change this step, see _circuitLaws)
|
|
481
482
|
// Terminals (the two contacts, then the ports): each one's voltage (V), held by its source or
|
|
482
483
|
// floating (driven by a current, or behind a resistance), and its current into the device.
|
|
483
484
|
// Sources are read at sourceTime: a step's end, or now for a steady solve.
|
|
@@ -490,6 +491,9 @@ export class Solver {
|
|
|
490
491
|
this.steady = false; // inside a steady solve: sources at the present time, not a step's end
|
|
491
492
|
// The floating ones are extra unknowns, solved with the grid's by bordering (_solveBordered).
|
|
492
493
|
this.floating = this.terms.flatMap((t, k) => (t.drive.kind === 'I' || t.drive.R > 0 ? [k] : []));
|
|
494
|
+
this.drivenKey = this._drivenKey();
|
|
495
|
+
this.lawRefs = new Map(); // what a steady solve keeps behind a terminal at no current (_captureLaws)
|
|
496
|
+
this.chargeRefs = new Map(); // …and the charge a terminal at no current that only charges keeps
|
|
493
497
|
// Per terminal: B = ∂res/∂V (compact column), C = ∂I/∂x (compact row), ∂I/∂V, and for a
|
|
494
498
|
// floating one its circuit residual (I − I_set, or I − (V_src − V)/R).
|
|
495
499
|
this.termB = this.terms.map(() => new Float64Array(this.sys.size + 1));
|
|
@@ -725,49 +729,125 @@ export class Solver {
|
|
|
725
729
|
st.mobile = true;
|
|
726
730
|
for (let q = st.regions[0]; q <= st.regions[1]; q++) if (!(materials[regions[q].material].D[st.species] > 0)) st.mobile = false;
|
|
727
731
|
}
|
|
728
|
-
// Stretches
|
|
729
|
-
// reached by one contact
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
// electrons reach the back contact only through a bulk with ~1e3 of them per
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
|
|
736
|
-
|
|
732
|
+
// Stretches that carry nothing in a steady state, so whose level is flat there: a species that
|
|
733
|
+
// no reaction or port touches, reached by one contact at most, moving through ideal regions
|
|
734
|
+
// without flow or mixing (where carrying nothing is a level flat). Found through their own
|
|
735
|
+
// conduction, which can be all but nothing, such a level can't be moved (a MOS capacitor's
|
|
736
|
+
// inversion electrons, which reach the back contact only through a bulk with ~1e3 of them per
|
|
737
|
+
// cm³; blocked SO₄²⁻ driven from a zinc cathode's extended space charge, down to 1e-25 mol/m³
|
|
738
|
+
// there, whose chain of conductances lost the system 14 digits). So steady solves hold it
|
|
739
|
+
// flat (_flatRows): at its contact's level, or reaching none, at the level its amount fixes.
|
|
740
|
+
// (Not through flow or mixing, nor a concentrated material's cross-diffusion, where carrying
|
|
741
|
+
// nothing isn't a level flat.)
|
|
742
|
+
const plain = (st) => {
|
|
743
|
+
if (st.ports.length > 0 || !st.mobile || (st.leftOpen && st.rightOpen)) return false;
|
|
737
744
|
for (let q = st.regions[0]; q <= st.regions[1]; q++) {
|
|
738
745
|
const reg = regions[q], mat = materials[reg.material];
|
|
739
746
|
if (mat.conductor || !mat.ideal || reg.velocity !== 0 || reg.mixing > 0) return false;
|
|
740
747
|
}
|
|
741
748
|
return true;
|
|
749
|
+
};
|
|
750
|
+
this.flatStretches = this.stretches.filter((st) => !st.reactive && plain(st));
|
|
751
|
+
// Likewise a group of stretches that react only among themselves, each reaching the same one
|
|
752
|
+
// contact and nothing else, their reactions balanced at that contact's levels (no light, no
|
|
753
|
+
// other reservoir): in a steady state, one reservoir drives nothing, so the group is at
|
|
754
|
+
// equilibrium, each level flat at the contact's. A MOS capacitor's electrons and the holes
|
|
755
|
+
// they recombine with reach only its back contact: moved, slow generation through a bulk of
|
|
756
|
+
// GaAs with ~1e-4 electrons per cm³ couldn't carry the inversion layer's level with it.
|
|
757
|
+
const group = this.stretches.map((_, k) => k), root = (k) => (group[k] === k ? k : (group[k] = root(group[k])));
|
|
758
|
+
const join = (ks) => ks.forEach((k) => (group[root(k)] = root(ks[0])));
|
|
759
|
+
const rxns = []; // [stretches, A/RT at the contact's levels, as a function of the side]
|
|
760
|
+
regions.forEach((reg, q) => {
|
|
761
|
+
for (const rx of this.rxsIn[reg.material]) {
|
|
762
|
+
const ks = rx.sp.map((i) => this.stretchOf[q * n + i]);
|
|
763
|
+
join(ks);
|
|
764
|
+
rxns.push([ks, (side) => rx.sp.reduce((a, i, p) => a + rx.nu[p] * this.contactEta(side, i), rx.fixedA)]);
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
model.interfaces.forEach((itf, f) => {
|
|
768
|
+
for (const rx of itf.reactions) {
|
|
769
|
+
const ks = rx.part.map((p) => this.stretchOf[(f + p.side) * n + p.i]);
|
|
770
|
+
if (ks.some((k) => k < 0)) continue;
|
|
771
|
+
join(ks);
|
|
772
|
+
rxns.push([ks, (side) => rx.part.reduce((a, p) => a + p.nu * this.contactEta(side, p.i), rx.fixedA)]);
|
|
773
|
+
}
|
|
742
774
|
});
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
// the level and pin the face's flux leave a diagonal block singular.)
|
|
749
|
-
this.levelStretches = this.stretches.filter((st) => {
|
|
750
|
-
if (!st.spectator || !st.mobile || st.ports.length > 0 || st.regions[0] !== st.regions[1]) return false;
|
|
751
|
-
const reg = regions[st.regions[0]], mat = materials[reg.material];
|
|
752
|
-
return !mat.conductor && mat.ideal && reg.velocity === 0 && !(reg.mixing > 0);
|
|
775
|
+
const groups = new Map();
|
|
776
|
+
this.stretches.forEach((st, k) => {
|
|
777
|
+
if (!st.reactive) return;
|
|
778
|
+
if (!groups.has(root(k))) groups.set(root(k), []);
|
|
779
|
+
groups.get(root(k)).push(k);
|
|
753
780
|
});
|
|
754
|
-
|
|
781
|
+
for (const ks of groups.values()) {
|
|
782
|
+
const sts = ks.map((k) => this.stretches[k]), side = sts[0].leftOpen ? 'left' : 'right';
|
|
783
|
+
if (!sts.every((st) => plain(st) && st.leftOpen !== st.rightOpen && (side === 'left' ? st.leftOpen : st.rightOpen))) continue;
|
|
784
|
+
if (!rxns.every(([rk, A]) => !rk.some((k) => ks.includes(k)) || Math.abs(A(side)) < 1e-9)) continue;
|
|
785
|
+
this.flatStretches.push(...sts);
|
|
786
|
+
}
|
|
787
|
+
this._laws();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Whether a stretch's species moves through it (mobile in every region, or a metal's carrier),
|
|
791
|
+
// or stays still in every region (D = 0: trap states, conserved node by node).
|
|
792
|
+
_moves(st) {
|
|
793
|
+
const { regions, materials } = this.model;
|
|
794
|
+
for (let q = st.regions[0]; q <= st.regions[1]; q++) {
|
|
795
|
+
const mat = materials[regions[q].material];
|
|
796
|
+
if (!(mat.conductor || mat.D[st.species] > 0)) return false;
|
|
797
|
+
}
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
_still(st) {
|
|
802
|
+
const { regions, materials } = this.model;
|
|
803
|
+
for (let q = st.regions[0]; q <= st.regions[1]; q++) {
|
|
804
|
+
const mat = materials[regions[q].material];
|
|
805
|
+
if (mat.conductor || mat.D[st.species] > 0) return false;
|
|
806
|
+
}
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Whether terminal k (a contact, or port k − 2) feeds stretch st: links its species there.
|
|
811
|
+
_feeds(k, st) {
|
|
812
|
+
return k === 0 ? st.leftOpen : k === 1 ? st.rightOpen : st.linked.includes(k - 2);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// What a steady solve keeps (under the terminals' present drives, so again after a change of
|
|
816
|
+
// drive): each conserved amount, solved directly in place of one (redundant) balance row, and
|
|
817
|
+
// the islands' summed balances.
|
|
818
|
+
_laws() {
|
|
819
|
+
const { model } = this, { regions, materials, grid } = model;
|
|
820
|
+
const laws = this._conservedMoieties(true);
|
|
821
|
+
for (const st of this.stretches) st.conserved = false;
|
|
755
822
|
// Conserved amounts solved directly, each in place of one (redundant) balance row: a
|
|
756
|
-
// spectator's own amount
|
|
757
|
-
// iron of Fe³⁺, Fe²⁺ and FeCl²⁺, say), whose
|
|
758
|
-
//
|
|
759
|
-
//
|
|
823
|
+
// spectator's own amount (a floating metal's: the charge on its faces), each conserved
|
|
824
|
+
// combination of reacting stretches (the total iron of Fe³⁺, Fe²⁺ and FeCl²⁺, say), whose
|
|
825
|
+
// weighted balances sum to zero at steady state, and what a terminal driven at no current
|
|
826
|
+
// keeps behind it. A combination may take in still stretches (the electrons trap states hold),
|
|
827
|
+
// their amounts summed like any other; one of still stretches alone conserves node by node
|
|
828
|
+
// instead (below), and one of a stretch still in part only is left to giant time steps.
|
|
760
829
|
this.constraints = [];
|
|
761
830
|
const conductor = (st) => materials[regions[st.regions[0]].material].conductor;
|
|
762
|
-
|
|
831
|
+
// The terminals driven by a current that feed a stretch: a combination through one keeps what
|
|
832
|
+
// the state holds when a solve begins (see _captureLaws), and has no steady state at any other
|
|
833
|
+
// current.
|
|
834
|
+
const driven = (st) => this.terms.flatMap((t, k) => (t.drive.kind === 'I' && this._feeds(k, st) ? [k] : []));
|
|
835
|
+
const add = (parts, rowStretch, surface = [], circuit = -1, carriers = []) => {
|
|
763
836
|
const st = this.stretches[rowStretch];
|
|
764
837
|
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
765
838
|
for (const p of surface) nNodes += model.ports[this.surfCols[p.col][0]].nodes.length * model.ports[this.surfCols[p.col][0]].surface.length;
|
|
839
|
+
for (const p of carriers) nNodes += model.ports[p.k - 2].nodes.length;
|
|
766
840
|
for (const p of parts) this.stretches[p.stretch].conserved = true;
|
|
767
841
|
this.constraints.push({
|
|
768
842
|
parts,
|
|
769
843
|
surface,
|
|
770
|
-
|
|
844
|
+
carriers, // (what a port's electrode holds, Q/(zF) at weight w: see _conservedMoieties)
|
|
845
|
+
terminals: [...new Set([...parts.flatMap((p) => driven(this.stretches[p.stretch])), ...carriers.map((p) => p.k)])],
|
|
846
|
+
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(),
|
|
847
|
+
circuit, // (the terminal whose circuit row it stands in for, or −1 for a balance row's, `row`)
|
|
848
|
+
// (and the terminal's weight in it, w/z on what that feeds: the amount changes by λ I/F)
|
|
849
|
+
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)),
|
|
850
|
+
row: circuit >= 0 ? -1 : this.blockOfNode[st.nodes[0]] * this.M + 1 + st.species,
|
|
771
851
|
idx: new Int32Array(nNodes * this.M),
|
|
772
852
|
w: new Float64Array(nNodes * this.M),
|
|
773
853
|
len: 0,
|
|
@@ -776,38 +856,151 @@ export class Solver {
|
|
|
776
856
|
});
|
|
777
857
|
};
|
|
778
858
|
this.stretches.forEach((st, k) => {
|
|
779
|
-
if (st.spectator
|
|
859
|
+
if (!st.spectator || !this._moves(st)) return;
|
|
860
|
+
add([{ stretch: k, w: 1 }], k);
|
|
861
|
+
// Held flat (see _flatRows), it moves as a whole: the pin's response is the whole stretch's
|
|
862
|
+
// shift, its amount saying how far.
|
|
863
|
+
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);
|
|
780
864
|
});
|
|
781
|
-
const S = this.stretches.length;
|
|
782
|
-
|
|
865
|
+
const S = this.stretches.length, T = this.terms.length, X = S + this.surfCols.length;
|
|
866
|
+
const zc = (k) => this.z[model.ports[k - 2].terminal];
|
|
867
|
+
// The rows replaced: a moving stretch's balance (one of still stretches alone is kept node by
|
|
868
|
+
// node, below), or the circuit row of a terminal driven at no current, which says nothing the
|
|
869
|
+
// combination's balance rows don't (their weighted sum is that terminal's current, weighted
|
|
870
|
+
// by w/z). Together they must be independent, each combination's weights over the rows
|
|
871
|
+
// replaced a nonsingular matrix, or one balance is lost and another said twice (A + B = AB and
|
|
872
|
+
// 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
|
|
873
|
+
// A twice and C not at all, and the solve converged to a state that wasn't steady). So they're
|
|
874
|
+
// chosen by elimination, each combination's weights reduced by the rows already chosen: a
|
|
875
|
+
// circuit row where one is left, else the first stretch with weight 1, else the basis
|
|
876
|
+
// vector's own (its weight is 1 there, and no other vector's is), else the largest.
|
|
877
|
+
const rowOf = (k) => this.blockOfNode[this.stretches[k].nodes[0]] * this.M + 1 + this.stretches[k].species;
|
|
878
|
+
const chosen = []; // (a column and the reduced weights, per row replaced: S + t for terminal t's)
|
|
879
|
+
const spectators = new Set(this.constraints.map((cs) => cs.row));
|
|
880
|
+
// What an electrode holds goes only in its terminal's circuit row (its charge depends on the
|
|
881
|
+
// terminal's voltage, which a balance row's pin can't carry): the combinations through it come
|
|
882
|
+
// first, so that the first takes that row, and the rest have its multiple taken off.
|
|
883
|
+
const through = (w) => this.carriers.some((k, j) => w[X + j] !== 0);
|
|
884
|
+
const held = new Map(); // (terminal → the combination in place of its circuit row)
|
|
885
|
+
// Reactions' equilibria first. A face reaction that alone changes some combination (without
|
|
886
|
+
// it, w would be conserved: Ag⁺ closed in a cell where Ag⁺ + e⁻ = Ag(s) is its one source, or
|
|
887
|
+
// Ag⁺ + AgNO₃ where it also complexes) has that combination's summed balance rows equal to its
|
|
888
|
+
// rate, w·ν times it, which a steady state sets to zero. The rate is a poor row: k₀ times a
|
|
889
|
+
// function of the levels, beside diffusion's terms in the same balances. An Ag⁺/Ag couple
|
|
890
|
+
// 1e-20 as fast as Fe³⁺/Fe²⁺ on the same platinum was lost to their round-off, the
|
|
891
|
+
// open-circuit potential settling where silver wasn't (−0.4661 V, the joint equilibrium
|
|
892
|
+
// −0.5004 V), and 1e-15 to 1e-18 failed. Its affinity says the same at any k₀ (each kinetic
|
|
893
|
+
// form vanishes only there), so it stands in for one of those rows (see _applyAffinities),
|
|
894
|
+
// chosen by the same elimination, and a stretch's: never a circuit row.
|
|
895
|
+
// The slowest first, whose rate rows are the poorest, and each row picked by the reaction's
|
|
896
|
+
// own terms, never by where its species are listed: the same rows in any listing. (Picked by
|
|
897
|
+
// listing, an electrode's two couples took different rows with the species reversed, and
|
|
898
|
+
// Newton converged in one listing and crawled in the other.)
|
|
899
|
+
this.affinities = [];
|
|
900
|
+
const faceRx = model.interfaces.flatMap((itf, f) => itf.reactions.map((rx) => ({ f, rx, key: rx.part.map((p) => `${model.species[p.i].name}:${p.side}:${p.nu}`).sort().join() })));
|
|
901
|
+
faceRx.sort((a, b) => a.rx.k0 - b.rx.k0 || (a.key < b.key ? -1 : a.key > b.key ? 1 : a.f - b.f));
|
|
902
|
+
for (const { f, rx } of faceRx) {
|
|
903
|
+
const nu = new Float64Array(S);
|
|
904
|
+
let inFlat = false;
|
|
905
|
+
for (const p of rx.part) {
|
|
906
|
+
const k = this.stretchOf[(f + p.side) * this.n + p.i];
|
|
907
|
+
if (k < 0 || this.flatStretches.includes(this.stretches[k])) inFlat = true;
|
|
908
|
+
else nu[k] += p.nu;
|
|
909
|
+
}
|
|
910
|
+
if (inFlat) continue; // (held flat at a contact's levels, in equilibrium already)
|
|
911
|
+
// (Not one fed by a terminal driven by a current: its sum is the rate plus what that
|
|
912
|
+
// passes. Silver's, e⁻ + Fe²⁺ with the electrons fed, or Ag⁺ + AgNO₃, which nothing is.
|
|
913
|
+
// A current held at zero passes nothing: an electrode at open circuit feeding a closed
|
|
914
|
+
// pool of Ag⁺ beside a slow Fe³⁺/Fe²⁺ couple settled 78 mV off, its iron not stationary.)
|
|
915
|
+
const passes = (k) => !this._noCurrent(this.terms[k]);
|
|
916
|
+
const fed = (w) => w.some((x, k) => x !== 0 && ((k >= X && passes(this.carriers[k - X])) || (k < S && driven(this.stretches[k]).some(passes))));
|
|
917
|
+
// Any of the combinations it alone changes will do, with any conserved amount added (whose
|
|
918
|
+
// summed balance is zero too): one of them needs a row of its own left to take. (Silver's,
|
|
919
|
+
// e⁻ − Fe³⁺ beside a closed iron couple that had Fe³⁺'s, is e⁻ + Fe²⁺ with the iron added.)
|
|
920
|
+
const news = this._conservedMoieties(true, [], rx).filter((w) => Math.abs(nu.reduce((t, x, k) => t + x * w[k], 0)) > 1e-9 && !fed(w));
|
|
921
|
+
const kept = laws.filter((l) => !fed(l));
|
|
922
|
+
const tries = [...news, ...news.flatMap((w) => kept.flatMap((l) => [1, -1].map((sg) => w.map((x, m) => x + sg * l[m]))))];
|
|
923
|
+
for (const w of tries) {
|
|
924
|
+
const v = new Float64Array(S + T);
|
|
925
|
+
for (let k = 0; k < S; k++) {
|
|
926
|
+
if (w[k] === 0) continue;
|
|
927
|
+
const st = this.stretches[k];
|
|
928
|
+
if (this._moves(st) && !spectators.has(rowOf(k))) v[k] = w[k];
|
|
929
|
+
if (this.z[st.species] !== 0) for (const t of driven(st)) v[S + t] = w[k] / this.z[st.species];
|
|
930
|
+
}
|
|
931
|
+
for (const [col, row] of chosen) {
|
|
932
|
+
const fc = v[col];
|
|
933
|
+
if (fc !== 0) for (let j = 0; j < S + T; j++) v[j] -= fc * row[j];
|
|
934
|
+
}
|
|
935
|
+
const scale = v.reduce((m, x) => Math.max(m, Math.abs(x)), 0);
|
|
936
|
+
if (!(scale > 1e-9)) continue;
|
|
937
|
+
// (Never a metal's carriers, nor a stretch a terminal feeds, even at no current: their rows
|
|
938
|
+
// carry the terminal's current, and Pt's electrons standing for an iron couple's balance
|
|
939
|
+
// put the electrode at 602 V.)
|
|
940
|
+
const ok = (j) => j !== undefined && j >= 0 && j < S && Math.abs(v[j]) > 1e-9 * scale && !conductor(this.stretches[j]) && this.terms.every((_, t) => !this._feeds(t, this.stretches[j]));
|
|
941
|
+
// (One of its own species first, then weight 1, the largest, and by name.)
|
|
942
|
+
const own = (k) => rx.part.some((p) => p.i === this.stretches[k].species && this.stretchOf[(f + p.side) * this.n + p.i] === k);
|
|
943
|
+
const key = (k) => [own(k) ? 0 : 1, Math.abs(v[k]) === 1 ? 0 : 1, -Math.abs(v[k]), model.species[this.stretches[k].species].name, this.stretches[k].regions[0]];
|
|
944
|
+
const less = (a, b) => { const ka = key(a), kb = key(b); for (let j = 0; j < ka.length; j++) if (ka[j] !== kb[j]) return ka[j] < kb[j]; return false; };
|
|
945
|
+
const col = [...Array(S).keys()].filter(ok).reduce((b, k) => (b < 0 || less(k, b) ? k : b), -1);
|
|
946
|
+
if (col < 0) continue;
|
|
947
|
+
chosen.push([col, v.map((x) => x / v[col])]);
|
|
948
|
+
this.affinities.push({ rx, f, row: rowOf(col), idx: new Int32Array(rx.part.length), w: new Float64Array(rx.part.length), len: 0, res: 0 });
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
for (let w of [...laws.filter(through), ...laws.filter((w) => !through(w))]) {
|
|
953
|
+
for (const [j, k] of this.carriers.entries()) {
|
|
954
|
+
const c = held.get(k);
|
|
955
|
+
if (w[X + j] === 0 || !c) continue;
|
|
956
|
+
const f = w[X + j] / c[X + j], own = w.own;
|
|
957
|
+
w = w.map((x, m) => (Math.abs(x - f * c[m]) < 1e-9 ? 0 : x - f * c[m]));
|
|
958
|
+
w.own = own;
|
|
959
|
+
}
|
|
783
960
|
const parts = [...w.keys()].filter((k) => k < S && w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
784
|
-
const surface = [...w.keys()].filter((k) => k >= S && w[k] !== 0).map((k) => ({ col: k - S, w: w[k] }));
|
|
785
|
-
|
|
961
|
+
const surface = [...w.keys()].filter((k) => k >= S && k < X && w[k] !== 0).map((k) => ({ col: k - S, w: w[k] }));
|
|
962
|
+
const carriers = this.carriers.flatMap((k, j) => (w[X + j] !== 0 ? [{ k, w: w[X + j] }] : []));
|
|
963
|
+
if (parts.length === 0) continue; // (a surface's own: node by node, below)
|
|
786
964
|
if (parts.length === 1 && surface.length === 0 && this.stretches[parts[0].stretch].spectator) continue; // (above)
|
|
787
|
-
if (parts.some((p) => !this.stretches[p.stretch]
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
965
|
+
if (parts.some((p) => !this._moves(this.stretches[p.stretch]) && !this._still(this.stretches[p.stretch]))) continue;
|
|
966
|
+
const v = new Float64Array(S + T);
|
|
967
|
+
for (const { stretch: k, w: wk } of parts) {
|
|
968
|
+
const st = this.stretches[k];
|
|
969
|
+
if (this._moves(st) && !spectators.has(rowOf(k))) v[k] = wk;
|
|
970
|
+
if (this.z[st.species] !== 0) for (const t of driven(st)) v[S + t] = wk / this.z[st.species];
|
|
971
|
+
}
|
|
972
|
+
for (const { k, w: wk } of carriers) v[S + k] = wk / zc(k);
|
|
973
|
+
for (const [col, row] of chosen) {
|
|
974
|
+
const f = v[col];
|
|
975
|
+
if (f !== 0) for (let j = 0; j < S + T; j++) v[j] -= f * row[j];
|
|
976
|
+
}
|
|
977
|
+
const scale = v.reduce((m, x) => Math.max(m, Math.abs(x)), 0);
|
|
978
|
+
if (!(scale > 1e-9)) continue;
|
|
979
|
+
const ok = (j) => j !== undefined && Math.abs(v[j]) > 1e-9 * scale;
|
|
980
|
+
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);
|
|
981
|
+
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)
|
|
982
|
+
chosen.push([col, v.map((x) => x / v[col])]);
|
|
983
|
+
if (col >= S) held.set(col - S, w);
|
|
984
|
+
add(parts, col < S ? col : parts[0].stretch, surface, col < S ? -1 : col - S, carriers);
|
|
985
|
+
}
|
|
986
|
+
// A combination that nothing carries anywhere is conserved node by node: of still stretches
|
|
987
|
+
// (trap states: X⁰ + X⁻ under e⁻ + X⁰ = X⁻), or of an electrode's surface species (adsorbates
|
|
988
|
+
// that only turn into each other). At each node its weighted sum stays what it was, a row local
|
|
989
|
+
// to that node's block, in place of one of their balances.
|
|
794
990
|
this.localConstraints = [];
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
if (sts.some((st) => st.nodes[0] !== sts[0].nodes[0] || st.nodes[1] !== sts[0].nodes[1])) continue; // (side by side only)
|
|
804
|
-
for (const st of sts) st.conserved = true;
|
|
805
|
-
const row = parts.find((p) => p.w === 1) ?? parts[0];
|
|
991
|
+
const span = ([g0, g1]) => Array.from({ length: g1 - g0 + 1 }, (_, j) => g0 + j);
|
|
992
|
+
for (const w of laws) {
|
|
993
|
+
const parts = [...w.keys()].filter((k) => w[k] !== 0);
|
|
994
|
+
if (parts.some((k) => k >= X)) continue; // (through an electrode: above)
|
|
995
|
+
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));
|
|
996
|
+
if (sets.some((set) => set === null || set.length !== sets[0].length || set.some((g, j) => g !== sets[0][j]))) continue; // (side by side only)
|
|
997
|
+
for (const k of parts) if (k < S) this.stretches[k].conserved = true;
|
|
998
|
+
const slot = (k) => (k < S ? 1 + this.stretches[k].species : 1 + this.n + this.surfCols[k - S][1]);
|
|
806
999
|
this.localConstraints.push({
|
|
807
|
-
parts: parts.map((
|
|
808
|
-
row:
|
|
809
|
-
nodes:
|
|
810
|
-
reference: new Float64Array(
|
|
1000
|
+
parts: parts.map((k) => (k < S ? { i: this.stretches[k].species, w: w[k] } : { q: this.surfCols[k - S][1], w: w[k] })),
|
|
1001
|
+
row: slot(parts.includes(w.own) ? w.own : parts[0]),
|
|
1002
|
+
nodes: sets[0],
|
|
1003
|
+
reference: new Float64Array(sets[0].length),
|
|
811
1004
|
});
|
|
812
1005
|
}
|
|
813
1006
|
// Islands: the pieces of a stretch between its faces. One that nothing else feeds (no
|
|
@@ -821,7 +1014,7 @@ export class Solver {
|
|
|
821
1014
|
this.pinnedIslands = [];
|
|
822
1015
|
this.islandsOn = false; // (newton() turns them on, see there)
|
|
823
1016
|
this.pins = [];
|
|
824
|
-
const used = new Set(this.constraints.map((cs) => cs.row));
|
|
1017
|
+
const used = new Set([...this.constraints.map((cs) => cs.row).filter((o) => o >= 0), ...this.affinities.map((af) => af.row)]);
|
|
825
1018
|
for (const st of this.stretches) {
|
|
826
1019
|
const i = st.species, [s0, s1] = st.regions;
|
|
827
1020
|
if (!st.mobile || conductor(st)) continue;
|
|
@@ -870,19 +1063,68 @@ export class Solver {
|
|
|
870
1063
|
}
|
|
871
1064
|
}
|
|
872
1065
|
for (const isl of this.islands) isl.outside = isl.outside.filter((o) => !used.has(o));
|
|
873
|
-
|
|
1066
|
+
|
|
1067
|
+
// Time steps only: a terminal behind a resistance that alone feeds a combination reads its
|
|
1068
|
+
// current by that combination's change too (see _circuitLaws), its combinations found with it
|
|
1069
|
+
// counted as driven by a current. (Not in the steady system, where it holds its level.)
|
|
1070
|
+
this.timeLaws = [];
|
|
1071
|
+
const behind = this.floating.filter((k) => this.terms[k].drive.kind !== 'I');
|
|
1072
|
+
if (behind.length > 0) {
|
|
1073
|
+
const steadyCarriers = this.carriers, more = this._conservedMoieties(true, behind), X2 = S + this.surfCols.length;
|
|
1074
|
+
for (const w of more) {
|
|
1075
|
+
const parts = [...w.keys()].filter((k) => k < S && w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
1076
|
+
if (parts.length === 0 || parts.some((p) => !this._moves(this.stretches[p.stretch]) && !this._still(this.stretches[p.stretch]))) continue;
|
|
1077
|
+
const surface = [...w.keys()].filter((k) => k >= S && k < X2 && w[k] !== 0).map((k) => ({ col: k - S, w: w[k] }));
|
|
1078
|
+
const carriers = this.carriers.flatMap((k, j) => (w[X2 + j] !== 0 ? [{ k, w: w[X2 + j] }] : []));
|
|
1079
|
+
// Fed by one terminal behind a resistance and nothing else that passes current.
|
|
1080
|
+
const fedBy = new Set([...carriers.map((p) => p.k), ...parts.flatMap((p) => this.floating.filter((k) => this._feeds(k, this.stretches[p.stretch])))]);
|
|
1081
|
+
if (fedBy.size !== 1) continue;
|
|
1082
|
+
const [k] = fedBy;
|
|
1083
|
+
if (!behind.includes(k) || this.timeLaws.some((cs) => cs.circuit === k)) continue;
|
|
1084
|
+
const zc = k >= 2 ? this.z[model.ports[k - 2].terminal] : 0;
|
|
1085
|
+
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);
|
|
1086
|
+
if (!lambda) continue;
|
|
1087
|
+
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
1088
|
+
for (const p of surface) nNodes += model.ports[this.surfCols[p.col][0]].nodes.length * model.ports[this.surfCols[p.col][0]].surface.length;
|
|
1089
|
+
for (const p of carriers) nNodes += model.ports[p.k - 2].nodes.length;
|
|
1090
|
+
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 });
|
|
1091
|
+
}
|
|
1092
|
+
this.carriers = steadyCarriers;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// What each combination fed by a terminal driven at no current holds now, which a steady solve
|
|
1097
|
+
// keeps: a closed battery at open circuit stays at the charge it had.
|
|
1098
|
+
_captureLaws() {
|
|
1099
|
+
// A terminal at no current that passes current only by charging a capacitance keeps its charge.
|
|
1100
|
+
this.chargeRefs.clear();
|
|
1101
|
+
const charged = this.floating.filter((k) => this.terms[k].drive.kind === 'I' && this._chargeOnly(k));
|
|
1102
|
+
if (charged.length > 0) {
|
|
1103
|
+
this._assembleBookkeeping(Infinity);
|
|
1104
|
+
for (const k of charged) this.chargeRefs.set(k, k >= 2 ? this.portQ[k - 2] : k === 0 ? this.contactD.left : -this.contactD.right);
|
|
1105
|
+
}
|
|
1106
|
+
this.lawRefs.clear();
|
|
1107
|
+
if (this.constraints.some((cs) => cs.carriers.some(({ k }) => this.model.ports[k - 2].capacitance)) && charged.length === 0) this._assembleBookkeeping(Infinity);
|
|
1108
|
+
for (const cs of this.constraints) {
|
|
1109
|
+
if (cs.terminals.length === 0) continue;
|
|
1110
|
+
let a = 0;
|
|
1111
|
+
for (const { stretch, w } of cs.parts) a += w * this.amount(this.stretches[stretch]);
|
|
1112
|
+
for (const { col, w } of cs.surface) a += w * this.surfaceAmount(col);
|
|
1113
|
+
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);
|
|
1114
|
+
this.lawRefs.set(cs.key, a);
|
|
1115
|
+
}
|
|
874
1116
|
}
|
|
875
1117
|
|
|
876
1118
|
// Each local constraint's weighted sum at each node, as the state holds it now: what a steady
|
|
877
1119
|
// solve keeps.
|
|
878
1120
|
_captureLocal() {
|
|
879
|
-
const { n, c } = this;
|
|
1121
|
+
const { n, c, th, nSurf } = this;
|
|
880
1122
|
for (const lc of this.localConstraints) {
|
|
881
|
-
|
|
1123
|
+
lc.nodes.forEach((g, j) => {
|
|
882
1124
|
let t = 0;
|
|
883
|
-
for (const { i, w } of lc.parts) t += w * c[g * n + i];
|
|
884
|
-
lc.reference[
|
|
885
|
-
}
|
|
1125
|
+
for (const { i, q, w } of lc.parts) t += w * (i !== undefined ? c[g * n + i] : th[g * nSurf + q]);
|
|
1126
|
+
lc.reference[j] = t;
|
|
1127
|
+
});
|
|
886
1128
|
}
|
|
887
1129
|
}
|
|
888
1130
|
|
|
@@ -892,7 +1134,7 @@ export class Solver {
|
|
|
892
1134
|
_applyIslands() {
|
|
893
1135
|
const { u, res } = this, R = this.rix, out = [];
|
|
894
1136
|
for (const isl of this.islands) {
|
|
895
|
-
if (this.
|
|
1137
|
+
if (this.flatStretches.includes(isl.stretch)) continue;
|
|
896
1138
|
const b0 = Math.floor(isl.row / this.M), r0 = isl.row % this.M;
|
|
897
1139
|
if (this.loc[isl.row] < 0) continue;
|
|
898
1140
|
// Gathered by column (the faces' u, then each outside row's entries, which cancel those u
|
|
@@ -953,34 +1195,55 @@ export class Solver {
|
|
|
953
1195
|
|
|
954
1196
|
// The local constraints' rows (steady solves only): Σ w c_i − reference at each node.
|
|
955
1197
|
_applyLocalConstraints() {
|
|
956
|
-
const { n, M, c, res, dA: d } = this;
|
|
1198
|
+
const { n, M, c, th, nSurf, res, dA: d } = this;
|
|
957
1199
|
for (const lc of this.localConstraints) {
|
|
958
|
-
|
|
1200
|
+
lc.nodes.forEach((g, j) => {
|
|
959
1201
|
const b = this.blockOfNode[g];
|
|
960
|
-
if (this.loc[b * M + lc.row] < 0)
|
|
1202
|
+
if (this.loc[b * M + lc.row] < 0) return;
|
|
961
1203
|
this._replaceRow(b, lc.row);
|
|
962
1204
|
let t = 0;
|
|
963
|
-
for (const { i, w } of lc.parts) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1205
|
+
for (const { i, q, w } of lc.parts) {
|
|
1206
|
+
if (i !== undefined) {
|
|
1207
|
+
t += w * c[g * n + i];
|
|
1208
|
+
this._dc(g, i, d);
|
|
1209
|
+
for (let s = 0; s < M; s++) if (d[s] !== 0) this._j(b, lc.row, b, s, w * d[s]);
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
// A coverage, with ∂θ_q/∂η_x = θ_q (δ_qx − θ_x) over its surface's species.
|
|
1213
|
+
const tq = th[g * nSurf + q], ns = this.model.ports[this.surfPort[g]].surface.length;
|
|
1214
|
+
t += w * tq;
|
|
1215
|
+
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]));
|
|
967
1216
|
}
|
|
968
|
-
res[this.rix[b * M + lc.row]] = t - lc.reference[
|
|
969
|
-
}
|
|
1217
|
+
res[this.rix[b * M + lc.row]] = t - lc.reference[j];
|
|
1218
|
+
});
|
|
970
1219
|
}
|
|
971
1220
|
}
|
|
972
1221
|
|
|
973
1222
|
// A basis of the conserved combinations of stretch amounts: the null space of the
|
|
974
|
-
// stoichiometry (a row per reaction, over the stretches it touches) together with
|
|
975
|
-
//
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1223
|
+
// stoichiometry (a row per reaction, over the stretches it touches) together with what the
|
|
1224
|
+
// terminals feed. A terminal held at a voltage (or behind a resistance, which holds it at
|
|
1225
|
+
// steady state) feeds each stretch it links freely: a unit row. With `drives`, one driven by a
|
|
1226
|
+
// current feeds its charged species only as that current allows, in proportion to their
|
|
1227
|
+
// charges, so a combination weighted by z there is conserved while it passes none: a battery's
|
|
1228
|
+
// cathode, its electrons behind an open circuit, keeps its charge. Exact integer data, so plain
|
|
1229
|
+
// elimination will do.
|
|
1230
|
+
_conservedMoieties(drives = false, alsoCurrent = [], skip = null) {
|
|
1231
|
+
const { model, n, z } = this, S = this.stretches.length;
|
|
1232
|
+
// With `drives`, an electrode port driven by a current whose reactions take its carrier from
|
|
1233
|
+
// the electrode has a column of its own, last: what the electrode holds, its charge Q/(zF),
|
|
1234
|
+
// which the current feeds and the reactions draw on (a port at no current with M⁺ + e⁻ = M(s)
|
|
1235
|
+
// keeps M⁺ + Q/F, or with no capacitance, Q = 0, the M⁺ alone).
|
|
1236
|
+
const byCurrent = (t, k) => t.drive.kind === 'I' || alsoCurrent.includes(k); // (with `alsoCurrent`, terminals counted as such)
|
|
1237
|
+
this.carriers = drives ? this.terms.flatMap((t, k) => (byCurrent(t, k) && k >= 2 && model.ports[k - 2].reactions.length > 0 ? [k] : [])) : [];
|
|
1238
|
+
const X = S + this.surfCols.length, C = X + this.carriers.length;
|
|
1239
|
+
// (With `skip`, a reaction left out: the combinations that it alone changes come out too, and
|
|
1240
|
+
// its own rows, over the columns, are handed back as `basis.skipped`.)
|
|
1241
|
+
const rows = [], skipped = [];
|
|
979
1242
|
model.interfaces.forEach((itf, f) => {
|
|
980
1243
|
for (const rx of itf.reactions) {
|
|
981
1244
|
const row = new Float64Array(C);
|
|
982
1245
|
for (const p of rx.part) row[this.stretchOf[(f + p.side) * n + p.i]] += p.nu;
|
|
983
|
-
rows.push(row);
|
|
1246
|
+
(rx === skip ? skipped : rows).push(row);
|
|
984
1247
|
}
|
|
985
1248
|
});
|
|
986
1249
|
// An electrode surface's species are columns too, after the stretches (S + j for surface
|
|
@@ -989,11 +1252,13 @@ export class Solver {
|
|
|
989
1252
|
model.ports.forEach((port, k) => {
|
|
990
1253
|
for (const rx of port.reactions) {
|
|
991
1254
|
const row = new Float64Array(C); // (the electrode's carrier is outside the device)
|
|
1255
|
+
if (rx === skip) skipped.push(row);
|
|
992
1256
|
for (const p of rx.part) {
|
|
993
1257
|
if (p.side === 0) row[this.stretchOf[port.region * n + p.i]] += p.nu;
|
|
994
1258
|
else if (p.side === 2) row[S + this.surfColOf[k] + p.s] += p.nu;
|
|
1259
|
+
else if (this.carriers.includes(k + 2)) row[X + this.carriers.indexOf(k + 2)] += p.nu;
|
|
995
1260
|
}
|
|
996
|
-
rows.push(row);
|
|
1261
|
+
if (rx !== skip) rows.push(row);
|
|
997
1262
|
}
|
|
998
1263
|
});
|
|
999
1264
|
model.regions.forEach((reg, q) => {
|
|
@@ -1002,19 +1267,35 @@ export class Solver {
|
|
|
1002
1267
|
const row = new Float64Array(C);
|
|
1003
1268
|
for (const p of rx.reactants) row[this.stretchOf[q * n + p.i]] -= p.nu;
|
|
1004
1269
|
for (const p of rx.products) row[this.stretchOf[q * n + p.i]] += p.nu;
|
|
1005
|
-
rows.push(row);
|
|
1270
|
+
(rx === skip ? skipped : rows).push(row);
|
|
1006
1271
|
}
|
|
1007
1272
|
});
|
|
1008
|
-
|
|
1009
|
-
if (!st.contactFed) return;
|
|
1273
|
+
const unit = (k) => {
|
|
1010
1274
|
const row = new Float64Array(C);
|
|
1011
1275
|
row[k] = 1;
|
|
1012
1276
|
rows.push(row);
|
|
1277
|
+
};
|
|
1278
|
+
this.terms.forEach((t, k) => {
|
|
1279
|
+
const fed = this.stretches.flatMap((st, j) => (this._feeds(k, st) ? [j] : []));
|
|
1280
|
+
if (!(drives && byCurrent(t, k))) return fed.forEach(unit);
|
|
1281
|
+
const charged = fed.filter((j) => z[this.stretches[j].species] !== 0).map((j) => [j, z[this.stretches[j].species]]);
|
|
1282
|
+
if (this.carriers.includes(k)) charged.push([X + this.carriers.indexOf(k), z[model.ports[k - 2].terminal]]);
|
|
1283
|
+
fed.filter((j) => z[this.stretches[j].species] === 0).forEach(unit);
|
|
1284
|
+
for (let a = 1; a < charged.length; a++) {
|
|
1285
|
+
const row = new Float64Array(C);
|
|
1286
|
+
row[charged[0][0]] = 1 / charged[0][1];
|
|
1287
|
+
row[charged[a][0]] = -1 / charged[a][1];
|
|
1288
|
+
rows.push(row);
|
|
1289
|
+
}
|
|
1013
1290
|
});
|
|
1014
|
-
// Reduced row echelon form; the free columns give the null space.
|
|
1291
|
+
// Reduced row echelon form; the free columns give the null space. Immobile stretches' columns
|
|
1292
|
+
// go first, so that a combination of them alone (trap states, X⁰ + X⁻) comes out on its own,
|
|
1293
|
+
// to be kept node by node, apart from any that moves (e⁻ − X⁰, the electrons the traps hold).
|
|
1294
|
+
const order = [...Array(C).keys()].sort((a, b) => (b < S && this._still(this.stretches[b])) - (a < S && this._still(this.stretches[a])));
|
|
1015
1295
|
const pivots = [];
|
|
1016
1296
|
let r = 0;
|
|
1017
|
-
for (
|
|
1297
|
+
for (const col of order) {
|
|
1298
|
+
if (r >= rows.length) break;
|
|
1018
1299
|
let best = r;
|
|
1019
1300
|
for (let i = r + 1; i < rows.length; i++) if (Math.abs(rows[i][col]) > Math.abs(rows[best][col])) best = i;
|
|
1020
1301
|
if (Math.abs(rows[best][col]) < 1e-9) continue;
|
|
@@ -1040,8 +1321,10 @@ export class Solver {
|
|
|
1040
1321
|
const v = -rows[i][free];
|
|
1041
1322
|
w[col] = Math.abs(v) < 1e-9 ? 0 : v;
|
|
1042
1323
|
});
|
|
1324
|
+
w.own = free; // (its weight is 1 there, and no other vector's is: the row it replaces)
|
|
1043
1325
|
basis.push(w);
|
|
1044
1326
|
}
|
|
1327
|
+
basis.skipped = skipped;
|
|
1045
1328
|
return basis;
|
|
1046
1329
|
}
|
|
1047
1330
|
|
|
@@ -1056,39 +1339,19 @@ export class Solver {
|
|
|
1056
1339
|
// sum of the others. That row is dense, so it's kept aside: the factorised matrix gets a pin
|
|
1057
1340
|
// (identity row) there instead, and _solveBordered restores the constraint.
|
|
1058
1341
|
_applyConstraints() {
|
|
1059
|
-
const {
|
|
1060
|
-
const vol = this.model.grid.vol;
|
|
1342
|
+
const { M, res } = this;
|
|
1061
1343
|
for (const cs of this.constraints) {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
cs.idx[len] = this.rix[b * M + r];
|
|
1073
|
-
cs.w[len++] = w * vol[g] * d[r];
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
// What the electrodes' surfaces hold of it: Γ Σ v·a·θ, with ∂θ_s/∂η_x = θ_s (δ_sx − θ_x).
|
|
1078
|
-
for (const { col, w } of cs.surface) {
|
|
1079
|
-
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity, ns = port.surface.length;
|
|
1080
|
-
reference += w * this.surfaceRef[col];
|
|
1081
|
-
port.nodes.forEach((g, j) => {
|
|
1082
|
-
const f = w * G * vol[g] * this.portArea[k][j], b = this.blockOfNode[g], tq = this.th[g * this.nSurf + q];
|
|
1083
|
-
amount += f * tq;
|
|
1084
|
-
for (let x = 0; x < ns; x++) {
|
|
1085
|
-
cs.idx[len] = this.rix[b * M + 1 + n + x];
|
|
1086
|
-
cs.w[len++] = f * tq * ((q === x ? 1 : 0) - this.th[g * this.nSurf + x]);
|
|
1087
|
-
}
|
|
1088
|
-
});
|
|
1344
|
+
const { amount, reference, dV } = this._lawAmount(cs);
|
|
1345
|
+
cs.res = amount - (cs.terminals.length > 0 ? (this.lawRefs.get(cs.key) ?? amount) : reference);
|
|
1346
|
+
if (cs.circuit >= 0) {
|
|
1347
|
+
// In place of the terminal's circuit row (its I = 0).
|
|
1348
|
+
const C = this.termC[cs.circuit];
|
|
1349
|
+
C.fill(0);
|
|
1350
|
+
for (let j = 0; j < cs.len; j++) C[cs.idx[j]] += cs.w[j];
|
|
1351
|
+
this.termRes[cs.circuit] = cs.res;
|
|
1352
|
+
this.termDI[cs.circuit] = dV;
|
|
1353
|
+
continue;
|
|
1089
1354
|
}
|
|
1090
|
-
cs.len = len;
|
|
1091
|
-
cs.res = amount - reference;
|
|
1092
1355
|
const b0 = Math.floor(cs.row / M), r0 = cs.row % M;
|
|
1093
1356
|
this._replaceRow(b0, r0);
|
|
1094
1357
|
this._j(b0, r0, b0, r0, 1);
|
|
@@ -1096,6 +1359,122 @@ export class Solver {
|
|
|
1096
1359
|
}
|
|
1097
1360
|
}
|
|
1098
1361
|
|
|
1362
|
+
// A conserved combination's amount as the state holds it (and its reference, closed), with its
|
|
1363
|
+
// derivatives in cs.idx/w (and dV, by the voltage of a terminal whose electrode's charge it
|
|
1364
|
+
// counts).
|
|
1365
|
+
_lawAmount(cs) {
|
|
1366
|
+
const { n, M, c, dA: d } = this;
|
|
1367
|
+
const vol = this.model.grid.vol;
|
|
1368
|
+
let amount = 0, reference = 0, len = 0, gross = 0;
|
|
1369
|
+
for (const { stretch, w } of cs.parts) {
|
|
1370
|
+
const st = this.stretches[stretch], i = st.species;
|
|
1371
|
+
if (cs.terminals.length === 0) reference += w * this.referenceAmounts[stretch];
|
|
1372
|
+
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
1373
|
+
amount += w * vol[g] * c[g * n + i];
|
|
1374
|
+
gross += Math.abs(w * vol[g] * c[g * n + i]);
|
|
1375
|
+
if (this.nodeConductor[g] >= 0) {
|
|
1376
|
+
// A metal's carriers: the sheet at a charged face, vol·c = ±D_f·A/(zF), held by the
|
|
1377
|
+
// face's displacement.
|
|
1378
|
+
const f = this.sheetFace[g];
|
|
1379
|
+
if (f >= 0 && this.loc[this.blockOfFace[f] * M] >= 0) {
|
|
1380
|
+
cs.idx[len] = this.rix[this.blockOfFace[f] * M];
|
|
1381
|
+
cs.w[len++] = (w * this.sheetSign[g] * this.model.grid.area[g]) / (this.z[i] * FARADAY);
|
|
1382
|
+
}
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
this._dc(g, i, d);
|
|
1386
|
+
const b = this.blockOfNode[g];
|
|
1387
|
+
for (let r = 0; r < M; r++) {
|
|
1388
|
+
if (d[r] === 0 || this.loc[b * M + r] < 0) continue;
|
|
1389
|
+
cs.idx[len] = this.rix[b * M + r];
|
|
1390
|
+
cs.w[len++] = w * vol[g] * d[r];
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
// What the electrodes' surfaces hold of it: Γ Σ v·a·θ, with ∂θ_s/∂η_x = θ_s (δ_sx − θ_x).
|
|
1395
|
+
for (const { col, w } of cs.surface) {
|
|
1396
|
+
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity, ns = port.surface.length;
|
|
1397
|
+
if (cs.terminals.length === 0) reference += w * this.surfaceRef[col];
|
|
1398
|
+
port.nodes.forEach((g, j) => {
|
|
1399
|
+
const f = w * G * vol[g] * this.portArea[k][j], b = this.blockOfNode[g], tq = this.th[g * this.nSurf + q];
|
|
1400
|
+
amount += f * tq;
|
|
1401
|
+
gross += Math.abs(f * tq);
|
|
1402
|
+
for (let x = 0; x < ns; x++) {
|
|
1403
|
+
cs.idx[len] = this.rix[b * M + 1 + n + x];
|
|
1404
|
+
cs.w[len++] = f * tq * ((q === x ? 1 : 0) - this.th[g * this.nSurf + x]);
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
// What an electrode holds, its charge Q/(zF) on the port's capacitance (none without one),
|
|
1409
|
+
// Q = Σ vol·a·C (V − zeroCharge − φ) over its nodes (as _portCapacitance has it).
|
|
1410
|
+
let dV = 0;
|
|
1411
|
+
for (const { k, w } of cs.carriers) {
|
|
1412
|
+
const port = this.model.ports[k - 2];
|
|
1413
|
+
if (!port.capacitance) continue;
|
|
1414
|
+
const f = w / (this.z[port.terminal] * FARADAY), { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k];
|
|
1415
|
+
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
1416
|
+
port.nodes.forEach((g, j) => {
|
|
1417
|
+
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
1418
|
+
const b = this.blockOfNode[g], s = vol[g] * this.portArea[k - 2][j] * Cs;
|
|
1419
|
+
amount += f * s * (V - zeroCharge - this.VT * (this.u[b * M] + this.uLo[b * M]));
|
|
1420
|
+
gross += Math.abs(f * s * (V - zeroCharge - this.VT * (this.u[b * M] + this.uLo[b * M])));
|
|
1421
|
+
dV += f * s;
|
|
1422
|
+
cs.idx[len] = this.rix[b * M];
|
|
1423
|
+
cs.w[len++] = -f * s * this.VT;
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
cs.len = len;
|
|
1427
|
+
return { amount, reference, dV, gross };
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// The same amount at the step's start, as the storage terms have it (BDF2's c* included).
|
|
1431
|
+
_lawStart(cs) {
|
|
1432
|
+
const { n, nSurf } = this, vol = this.model.grid.vol;
|
|
1433
|
+
let a = 0;
|
|
1434
|
+
for (const { stretch, w } of cs.parts) {
|
|
1435
|
+
const st = this.stretches[stretch], i = st.species;
|
|
1436
|
+
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) a += w * vol[g] * this.cOld[g * n + i];
|
|
1437
|
+
}
|
|
1438
|
+
for (const { col, w } of cs.surface) {
|
|
1439
|
+
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity;
|
|
1440
|
+
port.nodes.forEach((g, j) => (a += w * G * vol[g] * this.portArea[k][j] * this.thOld[g * nSurf + q]));
|
|
1441
|
+
}
|
|
1442
|
+
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);
|
|
1443
|
+
return a;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// Time steps: a terminal driven by a current that alone feeds a conserved combination can read
|
|
1447
|
+
// its current as the combination's change, F (S − S*)/(λ dt), which the step's balance rows sum
|
|
1448
|
+
// to exactly, rather than as the flux through it. Both are exact; they differ in round-off and
|
|
1449
|
+
// in what Newton can see. Through a metal, the flux is G Δη, its two η all but equal: a floating
|
|
1450
|
+
// gate's charging current fell below that at steps of ~0.03 s, and the step came out singular.
|
|
1451
|
+
// Over long steps the change is the better kept (a closed host at open circuit, read by its
|
|
1452
|
+
// flux, drifted from its charge by 3 mV over 5e11 s, and its steps failed by 4e12 s). Over
|
|
1453
|
+
// short ones it's the amount's round-off over dt (a battery's cathode holds 0.04 mol/m² of
|
|
1454
|
+
// electrons: 1e-6 A/m² at steps of 1e-6 s, enough to reject steps at tol 1e-8), where the flux,
|
|
1455
|
+
// its levels carried in two words (u + uLo), is far cleaner than ε G. So a step reads the change
|
|
1456
|
+
// where that's ε·F·S/(λ dt) below 1e-6 of the flux's ε G, as its start has them, keeps to it
|
|
1457
|
+
// throughout, and where a step read by the flux fails, it's tried once more read by the change
|
|
1458
|
+
// (see step).
|
|
1459
|
+
_circuitLaws(dt) {
|
|
1460
|
+
for (const cs of [...this.constraints, ...this.timeLaws]) {
|
|
1461
|
+
const k = cs.circuit;
|
|
1462
|
+
if (k < 0 || !this.floating.includes(k)) continue;
|
|
1463
|
+
const { amount, dV, gross } = this._lawAmount(cs), f = FARADAY / (cs.lambda * dt), C = this.termC[k];
|
|
1464
|
+
if (!this.lawForm.has(cs)) {
|
|
1465
|
+
let G = 0;
|
|
1466
|
+
for (let j = 0; j < C.length; j++) G = Math.max(G, Math.abs(C[j]));
|
|
1467
|
+
this.lawForm.set(cs, Math.abs(f) * gross < 1e-6 * G);
|
|
1468
|
+
}
|
|
1469
|
+
if (!this.lawForm.get(cs)) continue;
|
|
1470
|
+
C.fill(0);
|
|
1471
|
+
for (let j = 0; j < cs.len; j++) C[cs.idx[j]] += f * cs.w[j];
|
|
1472
|
+
const d = this.terms[k].drive, src = sourceAt(d.src, this.sourceTime, this.sourceBefore);
|
|
1473
|
+
this.termRes[k] = f * (amount - this._lawStart(cs)) - (d.kind === 'I' ? src : (src - this.termV[k]) / d.R);
|
|
1474
|
+
this.termDI[k] = f * dV + (d.kind === 'I' ? 0 : 1 / d.R);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1099
1478
|
// Solve the full Newton system, J δ = rhs, with the extra unknowns and rows that don't fit the
|
|
1100
1479
|
// block-tridiagonal matrix T: the floating terminals' voltages (with their circuit rows), and
|
|
1101
1480
|
// the rows pinned in T: the spectators' conserved amounts (see _applyConstraints) and the
|
|
@@ -1114,10 +1493,11 @@ export class Solver {
|
|
|
1114
1493
|
for (let a = 0; a < S; a++) {
|
|
1115
1494
|
if (!cols[a]) cols[a] = new Float64Array(N + 1);
|
|
1116
1495
|
if (a < P) {
|
|
1117
|
-
|
|
1118
|
-
|
|
1496
|
+
// The response to a unit pin, or where the pin moves a flat stretch, to its shift.
|
|
1497
|
+
const col = pins[a].col ?? [pins[a].row];
|
|
1498
|
+
for (const o of col) e[this.rix[o]] = 1;
|
|
1119
1499
|
this._solveLinear(e, cols[a]);
|
|
1120
|
-
e[
|
|
1500
|
+
for (const o of col) e[this.rix[o]] = 0;
|
|
1121
1501
|
} else this._solveLinear(this.termB[fl[a - P]], cols[a]);
|
|
1122
1502
|
}
|
|
1123
1503
|
// Row a of the small system, as a dot product with a compact vector (sparse for pins).
|
|
@@ -1194,13 +1574,72 @@ export class Solver {
|
|
|
1194
1574
|
// change of source is a discontinuity, so time stepping restarts its order.
|
|
1195
1575
|
redrive() {
|
|
1196
1576
|
this.atSteady = false;
|
|
1197
|
-
const before = this.floating;
|
|
1577
|
+
const before = this.floating, driven = this.drivenKey;
|
|
1198
1578
|
this.floating = this.terms.flatMap((t, k) => (t.drive.kind === 'I' || t.drive.R > 0 ? [k] : []));
|
|
1199
1579
|
if (this.floating.length !== before.length) this.deltaV = null;
|
|
1580
|
+
// A terminal newly driven by a current (or no longer) can close a combination behind it.
|
|
1581
|
+
// (and one put behind a resistance, or taken from it, its time steps' reading: see _laws)
|
|
1582
|
+
this.drivenKey = this._drivenKey();
|
|
1583
|
+
if (this.drivenKey !== driven) this._laws();
|
|
1200
1584
|
this._refreshSources();
|
|
1201
1585
|
this.history = [];
|
|
1202
1586
|
}
|
|
1203
1587
|
|
|
1588
|
+
// After a steady solve: each reaction that alone changes some combination nothing else feeds
|
|
1589
|
+
// (see _laws), whose total rate a steady state therefore sets to zero, but whose affinity has one
|
|
1590
|
+
// sign wherever it acts, so that its total can't be zero. Too slow to show, its rate lost to
|
|
1591
|
+
// round-off, it was left where the solve began: a slow complexation 43 mV off, an electrode
|
|
1592
|
+
// port's slow couple at its starting composition, each converged without a word. (A face
|
|
1593
|
+
// reaction stands on its affinity, so it's only caught here if that couldn't be arranged.)
|
|
1594
|
+
stuckReactions() {
|
|
1595
|
+
const { n, M, u, uLo, model } = this, grid = model.grid, S = this.stretches.length, X = S + this.surfCols.length, out = [];
|
|
1596
|
+
const passes = (k) => !this._noCurrent(this.terms[k]);
|
|
1597
|
+
const fed = (w) => w.some((x, k) => x !== 0 && ((k >= X && passes(this.carriers[k - X])) || (k < S && this.terms.some((t, j) => t.drive.kind === 'I' && passes(j) && this._feeds(j, this.stretches[k])))));
|
|
1598
|
+
const needed = (rx) => {
|
|
1599
|
+
const basis = this._conservedMoieties(true, [], rx);
|
|
1600
|
+
return basis.some((w) => !fed(w) && basis.skipped.some((row) => Math.abs(row.reduce((t, x, k) => t + x * w[k], 0)) > 1e-9));
|
|
1601
|
+
};
|
|
1602
|
+
const eta = (g, slot) => u[this.blockOfNode[g] * M + slot] + uLo[this.blockOfNode[g] * M + slot];
|
|
1603
|
+
const report = (where, as) => {
|
|
1604
|
+
if (as.length === 0 || !(as.every((a) => a > 1e-6) || as.every((a) => a < -1e-6))) return;
|
|
1605
|
+
out.push({ where, affinity: as.reduce((m, a) => (Math.abs(a) < Math.abs(m) ? a : m)) });
|
|
1606
|
+
};
|
|
1607
|
+
model.interfaces.forEach((itf, f) => {
|
|
1608
|
+
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1];
|
|
1609
|
+
itf.reactions.forEach((rx, q) => {
|
|
1610
|
+
if (this.affinities.some((af) => af.rx === rx) || !needed(rx)) return;
|
|
1611
|
+
report(`interfaces[${f}].reactions[${q}]`, [rx.part.reduce((a, p) => a - p.nu * eta(p.side ? gR : gL, 1 + p.i), rx.fixedA)]);
|
|
1612
|
+
});
|
|
1613
|
+
});
|
|
1614
|
+
model.reactions.forEach((mrx, x) => {
|
|
1615
|
+
const rx = this.rxs[x];
|
|
1616
|
+
if (rx.generation || !needed(mrx)) return;
|
|
1617
|
+
const as = [];
|
|
1618
|
+
for (let g = 0; g < this.nNodes; g++) if (rx.kfNode[g] > 0 || rx.srh?.[this.nodeMaterial[g]]) as.push(rx.sp.reduce((a, i, p) => a + rx.nu[p] * eta(g, 1 + i), rx.fixedA));
|
|
1619
|
+
report(`bulkReactions[${x}]`, as);
|
|
1620
|
+
});
|
|
1621
|
+
model.ports.forEach((port, k) => {
|
|
1622
|
+
port.reactions.forEach((rx, q) => {
|
|
1623
|
+
if (!needed(rx)) return;
|
|
1624
|
+
let aV = rx.fixedA;
|
|
1625
|
+
for (const p of rx.part) if (p.side === 1) aV -= p.nu * this.portEta(port, p.i);
|
|
1626
|
+
report(`ports[${k}].reactions[${q}]`, port.nodes.map((g) => rx.part.reduce((a, p) => (p.side === 1 ? a : a - p.nu * eta(g, p.side === 2 ? 1 + n + p.s : 1 + p.i)), aV)));
|
|
1627
|
+
});
|
|
1628
|
+
});
|
|
1629
|
+
return out;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// How each terminal is driven, as far as the laws care (see _laws): by a voltage or a current,
|
|
1633
|
+
// behind a resistance or not, and a current that's held at zero throughout (which feeds a
|
|
1634
|
+
// combination nothing, so a reaction's affinity may stand in for its summed balance).
|
|
1635
|
+
_drivenKey() {
|
|
1636
|
+
return this.terms.map((t) => `${t.drive.kind}${t.drive.R > 0 ? 'R' : ''}${this._noCurrent(t) ? '0' : ''}`).join();
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
_noCurrent(t) {
|
|
1640
|
+
return t.drive.kind === 'I' && !(t.drive.R > 0) && t.drive.src.value === 0;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1204
1643
|
// Held terminals take their sources' values at sourceTime (a floating one keeps its voltage,
|
|
1205
1644
|
// an unknown; one behind a resistance reads its source voltage in _circuit).
|
|
1206
1645
|
_refreshSources() {
|
|
@@ -1355,11 +1794,15 @@ export class Solver {
|
|
|
1355
1794
|
if (!mat.phiFree && !responds && mat.ideal && electrode) {
|
|
1356
1795
|
// Nor here, but an electrode spread through it does: start with the port's first reaction
|
|
1357
1796
|
// that can be balanced at equilibrium with the port's level (the electrode at open circuit).
|
|
1797
|
+
const bare = 1 - electrode.surface.reduce((t, sp) => t + sp.theta0, 0);
|
|
1358
1798
|
for (const rx of electrode.reactions) {
|
|
1359
1799
|
let a = rx.fixedA, s = 0;
|
|
1360
1800
|
for (const p of rx.part) {
|
|
1361
1801
|
if (p.side === 1) a -= p.nu * this.portEta(electrode, p.i);
|
|
1362
|
-
else if (
|
|
1802
|
+
else if (p.side === 2) {
|
|
1803
|
+
const sp = electrode.surface[p.s]; // (at its starting coverage)
|
|
1804
|
+
a -= p.nu * (sp.mu0 / model.RT + Math.log(sp.theta0 / bare));
|
|
1805
|
+
} else if (mode[p.i] === 2) {
|
|
1363
1806
|
a -= p.nu * (Math.log(cFix[p.i] / mat.cRef[p.i]) + mat.mu0[p.i] / model.RT);
|
|
1364
1807
|
s += p.nu * z[p.i];
|
|
1365
1808
|
} else s = NaN;
|
|
@@ -1404,6 +1847,31 @@ export class Solver {
|
|
|
1404
1847
|
}
|
|
1405
1848
|
}
|
|
1406
1849
|
}
|
|
1850
|
+
// An insertion host's φ is a gauge: c0 sets only its neutral combination's level, not how that
|
|
1851
|
+
// splits between its ion and its electrons. Start its ion level with the same ion's beside it
|
|
1852
|
+
// across a face (the equilibrium a link or a transfer reaction heads for), so its electrons,
|
|
1853
|
+
// and a floating contact on them, start where the cell's open-circuit voltage puts them, not
|
|
1854
|
+
// volts away. (A shift of φ̂ and each η by z·Δ: the concentrations don't change.)
|
|
1855
|
+
regions.forEach((reg, r) => {
|
|
1856
|
+
const mat = materials[reg.material];
|
|
1857
|
+
if (!mat.phiFree) return;
|
|
1858
|
+
const sides = [[r - 1, grid.regionStart[r], r > 0 ? grid.regionEnd[r - 1] : -1], [r + 1, grid.regionEnd[r], r + 1 < regions.length ? grid.regionStart[r + 1] : -1]];
|
|
1859
|
+
for (const [nr, gIn, gOut] of sides) {
|
|
1860
|
+
if (gOut < 0) continue;
|
|
1861
|
+
const other = materials[regions[nr].material];
|
|
1862
|
+
if (other.phiFree || other.conductor) continue;
|
|
1863
|
+
const i = z.findIndex((zi, j) => zi !== 0 && mat.present[j] && other.present[j]);
|
|
1864
|
+
if (i < 0) continue;
|
|
1865
|
+
const d = (u[this.blockOfNode[gOut] * M + 1 + i] - u[this.blockOfNode[gIn] * M + 1 + i]) / z[i];
|
|
1866
|
+
if (!Number.isFinite(d)) continue;
|
|
1867
|
+
for (let g = grid.regionStart[r]; g <= grid.regionEnd[r]; g++) {
|
|
1868
|
+
const b = this.blockOfNode[g];
|
|
1869
|
+
u[b * M] += d;
|
|
1870
|
+
for (let j = 0; j < n; j++) if (mat.present[j]) u[b * M + 1 + j] += z[j] * d;
|
|
1871
|
+
}
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1407
1875
|
// A floating contact facing a held one starts where the start's own composition puts it: the
|
|
1408
1876
|
// held voltage plus the difference between the two terminal species' levels beside each
|
|
1409
1877
|
// contact (c0 sets them, and φ was chosen for neutrality, not to match either contact), so
|
|
@@ -1652,6 +2120,7 @@ export class Solver {
|
|
|
1652
2120
|
res.fill(0);
|
|
1653
2121
|
this.dtNow = dt;
|
|
1654
2122
|
if (this.lin) this.lin.reset(1 / dt);
|
|
2123
|
+
if (this.steady && dt === Infinity) this._snapFlat();
|
|
1655
2124
|
this.computeConcentrations();
|
|
1656
2125
|
|
|
1657
2126
|
// Regions, each assembled by the kernel for its kind: a conductor (its carrier only), a
|
|
@@ -1683,13 +2152,16 @@ export class Solver {
|
|
|
1683
2152
|
// Terminals: ports (after every other term at their nodes, so a held level can read its
|
|
1684
2153
|
// flux), then contacts, then the circuit rows of the floating ones.
|
|
1685
2154
|
this._terminals(dt);
|
|
1686
|
-
if (
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
2155
|
+
if (Number.isFinite(dt) && (this.constraints.length > 0 || this.timeLaws.length > 0)) this._circuitLaws(dt);
|
|
2156
|
+
// The steady system's own rows (see _laws): levels held flat, conserved amounts, islands'
|
|
2157
|
+
// balances, the last two kept aside for _solveBordered.
|
|
2158
|
+
const steady = this.steady && dt === Infinity;
|
|
2159
|
+
if (steady) this._flatRows();
|
|
2160
|
+
if (steady && this.affinities.length > 0) this._applyAffinities();
|
|
2161
|
+
if (steady && this.constraints.length > 0) this._applyConstraints();
|
|
2162
|
+
if (steady && this.localConstraints.length > 0) this._applyLocalConstraints();
|
|
2163
|
+
this.pinnedIslands = steady && this.islandsOn ? this._applyIslands() : [];
|
|
2164
|
+
this.pins = steady ? [...this.constraints.filter((cs) => cs.circuit < 0), ...this.affinities, ...this.pinnedIslands] : [];
|
|
1693
2165
|
this.transformed = this.combining && dt !== Infinity;
|
|
1694
2166
|
if (this.transformed) this._chargeRows(dt);
|
|
1695
2167
|
}
|
|
@@ -2536,7 +3008,8 @@ export class Solver {
|
|
|
2536
3008
|
const b = this.blockOfNode[g], o = b * M + r, v = vol[g];
|
|
2537
3009
|
const deta = target - (u[o] + uLo[o]); // (μ̄_out − μ̄)/RT
|
|
2538
3010
|
if (link.type === 'equilibrium') {
|
|
2539
|
-
|
|
3011
|
+
// a device end node's level is its contact's business, where the contact links it
|
|
3012
|
+
if ((g === 0 && this.model.contacts.left.species[i].type !== 'blocked') || (g === this.nNodes - 1 && this.model.contacts.right.species[i].type !== 'blocked')) continue;
|
|
2540
3013
|
flux[i] += res[R[o]];
|
|
2541
3014
|
this._captureRow(b, r, zF, C);
|
|
2542
3015
|
this._replaceRow(b, r);
|
|
@@ -2678,6 +3151,9 @@ export class Solver {
|
|
|
2678
3151
|
const { M, u, uLo, res, VT } = this, R = this.rix, vol = this.model.grid.vol, area = this.portArea[k - 2];
|
|
2679
3152
|
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
2680
3153
|
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
3154
|
+
// Its current, the charging (Q − Q at the step's start)/dt; or, steady at no current, the
|
|
3155
|
+
// charge it keeps in place of that (see _captureLaws).
|
|
3156
|
+
const rate = Number.isFinite(dt) ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
2681
3157
|
let Q = 0, dQdV = 0;
|
|
2682
3158
|
port.nodes.forEach((g, w) => {
|
|
2683
3159
|
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
@@ -2690,12 +3166,12 @@ export class Solver {
|
|
|
2690
3166
|
res[R[b * M]] -= q;
|
|
2691
3167
|
this._j(b, 0, b, 0, s * VT);
|
|
2692
3168
|
B[R[b * M]] -= s;
|
|
2693
|
-
if (
|
|
3169
|
+
if (rate > 0) Ct[R[b * M]] += -s * VT * rate;
|
|
2694
3170
|
});
|
|
2695
3171
|
this.portQ[k - 2] = Q;
|
|
2696
|
-
if (
|
|
2697
|
-
this.termI[k] += (Q - this.portQStart[k - 2])
|
|
2698
|
-
this.termDI[k] += dQdV
|
|
3172
|
+
if (rate > 0) {
|
|
3173
|
+
this.termI[k] += (Q - (dt < Infinity ? this.portQStart[k - 2] : this.chargeRefs.get(k))) * rate;
|
|
3174
|
+
this.termDI[k] += dQdV * rate;
|
|
2699
3175
|
}
|
|
2700
3176
|
}
|
|
2701
3177
|
|
|
@@ -2816,10 +3292,13 @@ export class Solver {
|
|
|
2816
3292
|
this._j(b, 0, b, 0, Ac * link.C * VT);
|
|
2817
3293
|
B[R[b * M]] += -Ac * link.C;
|
|
2818
3294
|
this.contactD[side] = sgn * Din;
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
3295
|
+
// Its current, the charging (D_in − D_in at the step's start)/dt; or, steady at no current,
|
|
3296
|
+
// the charge it keeps in place of that (see _captureLaws).
|
|
3297
|
+
const rate = dyn ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
3298
|
+
if (rate > 0) {
|
|
3299
|
+
I += (Din - (dyn ? Dstart : this.chargeRefs.get(k))) * rate;
|
|
3300
|
+
C[R[b * M]] += -Ac * link.C * VT * rate;
|
|
3301
|
+
this.termDI[k] += Ac * link.C * rate;
|
|
2823
3302
|
}
|
|
2824
3303
|
} else if (link.type === 'pinned' || link.type === 'bulk') {
|
|
2825
3304
|
// The Poisson residual is the outside's charge, D_in.
|
|
@@ -2828,6 +3307,9 @@ export class Solver {
|
|
|
2828
3307
|
if (dyn && !(this.combining && this.lateStorage[g] === 1)) {
|
|
2829
3308
|
I += (Din - Dstart) / dt;
|
|
2830
3309
|
this._captureRow(b, 0, 1 / dt, C);
|
|
3310
|
+
} else if (!dyn && this.steady && this.chargeRefs.has(k)) {
|
|
3311
|
+
I += Din - this.chargeRefs.get(k);
|
|
3312
|
+
this._captureRow(b, 0, 1, C);
|
|
2831
3313
|
}
|
|
2832
3314
|
this._replaceRow(b, 0);
|
|
2833
3315
|
if (link.type === 'pinned') {
|
|
@@ -2860,12 +3342,42 @@ export class Solver {
|
|
|
2860
3342
|
|
|
2861
3343
|
// Steady solves: each flat stretch's level held at its contact's throughout (see
|
|
2862
3344
|
// flatStretches), in place of its balances, as an equilibrium link holds it at the end node.
|
|
3345
|
+
// A flat stretch's level: its contact's, through the terminal k that holds it, or (reaching
|
|
3346
|
+
// none) its first node's, which its amount fixes (that row is the amount's, a pin whose
|
|
3347
|
+
// response moves the whole stretch: see _applyConstraints).
|
|
3348
|
+
_flatLevel(st) {
|
|
3349
|
+
const { M, u, uLo, z, VT, model } = this, i = st.species;
|
|
3350
|
+
if (st.leftOpen || st.rightOpen) {
|
|
3351
|
+
const side = st.leftOpen ? 'left' : 'right', link = model.contacts[side].species[i], k = side === 'left' ? 0 : 1;
|
|
3352
|
+
return { k, hi: z[i] === 0 ? link.mu / model.RT : (z[i] * (this.termV[k] + link.offset)) / VT, lo: 0 };
|
|
3353
|
+
}
|
|
3354
|
+
const o = this.blockOfNode[st.nodes[0]] * M + 1 + i;
|
|
3355
|
+
return { k: -1, hi: u[o], lo: uLo[o] };
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
// Each flat stretch's nodes set to its first node's level exactly (where Newton's damped
|
|
3359
|
+
// updates have taken it, on the way to its contact's), so that it carries exactly no flux (the
|
|
3360
|
+
// expm1 form of a zero difference). Flat only to round-off, a level moved by its contact had
|
|
3361
|
+
// read as a current: a MOS capacitor's holes, their contact jumped to −6 V, as
|
|
3362
|
+
// 3e-4 A/m² through a bulk that conducts ~1e10 S/m² (the true current was 3.5e-7).
|
|
3363
|
+
_snapFlat() {
|
|
3364
|
+
const { M, u, uLo } = this;
|
|
3365
|
+
for (const st of this.flatStretches) {
|
|
3366
|
+
const f = this.blockOfNode[st.nodes[0]] * M + 1 + st.species, hi = u[f], lo = uLo[f];
|
|
3367
|
+
for (let g = st.nodes[0] + 1; g <= st.nodes[1]; g++) {
|
|
3368
|
+
const o = this.blockOfNode[g] * M + 1 + st.species;
|
|
3369
|
+
if (this.loc[o] < 0) continue;
|
|
3370
|
+
u[o] = hi;
|
|
3371
|
+
uLo[o] = lo;
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
|
|
2863
3376
|
_flatRows() {
|
|
2864
3377
|
const { M, u, uLo, res, z, VT, model } = this, R = this.rix;
|
|
2865
3378
|
for (const st of this.flatStretches) {
|
|
2866
|
-
const i = st.species,
|
|
2867
|
-
const
|
|
2868
|
-
const level = z[i] === 0 ? link.mu / model.RT : (z[i] * (this.termV[k] + link.offset)) / VT;
|
|
3379
|
+
const i = st.species, open = st.leftOpen || st.rightOpen;
|
|
3380
|
+
const { k, hi, lo } = this._flatLevel(st), level = hi + lo;
|
|
2869
3381
|
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
2870
3382
|
const b = this.blockOfNode[g], o = b * M + 1 + i;
|
|
2871
3383
|
if (this.loc[o] < 0) continue;
|
|
@@ -2873,7 +3385,7 @@ export class Solver {
|
|
|
2873
3385
|
for (const B of this.termB) B[R[o]] = 0; // (the row's old terminal terms: the end node's own link)
|
|
2874
3386
|
this._j(b, 1 + i, b, 1 + i, 1);
|
|
2875
3387
|
res[R[o]] = u[o] + uLo[o] - level;
|
|
2876
|
-
if (z[i] !== 0) this.termB[k][R[o]] += -z[i] / VT;
|
|
3388
|
+
if (open && z[i] !== 0) this.termB[k][R[o]] += -z[i] / VT;
|
|
2877
3389
|
}
|
|
2878
3390
|
// A face inside the stretch whose link holds the level continuous carries the species' flux
|
|
2879
3391
|
// as an unknown that only the edge balances, now replaced, determined: it's zero.
|
|
@@ -2889,24 +3401,32 @@ export class Solver {
|
|
|
2889
3401
|
}
|
|
2890
3402
|
}
|
|
2891
3403
|
|
|
2892
|
-
//
|
|
2893
|
-
//
|
|
2894
|
-
//
|
|
2895
|
-
|
|
2896
|
-
const { M, u, uLo, res } = this,
|
|
2897
|
-
for (const
|
|
2898
|
-
const
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
this.
|
|
2906
|
-
|
|
3404
|
+
// Steady solves: each reaction that alone changes a combination (see _laws) held at its
|
|
3405
|
+
// equilibrium, a = fixedA − Σ ν η = 0, in place of one of that combination's balance rows (a pin,
|
|
3406
|
+
// like an amount's).
|
|
3407
|
+
_applyAffinities() {
|
|
3408
|
+
const { M, u, uLo, res } = this, grid = this.model.grid;
|
|
3409
|
+
for (const af of this.affinities) {
|
|
3410
|
+
const b0 = Math.floor(af.row / M), r0 = af.row % M;
|
|
3411
|
+
this._replaceRow(b0, r0);
|
|
3412
|
+
this._j(b0, r0, b0, r0, 1);
|
|
3413
|
+
res[this.rix[af.row]] = 0;
|
|
3414
|
+
const gL = grid.regionEnd[af.f], gR = grid.regionStart[af.f + 1];
|
|
3415
|
+
let hi = af.rx.fixedA, lo = 0, len = 0;
|
|
3416
|
+
for (const p of af.rx.part) {
|
|
3417
|
+
const o = this.blockOfNode[p.side ? gR : gL] * M + 1 + p.i;
|
|
3418
|
+
hi -= p.nu * u[o];
|
|
3419
|
+
lo -= p.nu * uLo[o];
|
|
3420
|
+
if (this.loc[o] < 0) continue;
|
|
3421
|
+
af.idx[len] = this.rix[o];
|
|
3422
|
+
af.w[len++] = -p.nu;
|
|
2907
3423
|
}
|
|
3424
|
+
af.len = len;
|
|
3425
|
+
af.res = hi + lo;
|
|
2908
3426
|
}
|
|
2909
3427
|
}
|
|
3428
|
+
|
|
3429
|
+
// Zero one row of the Jacobian (all three blocks), ready to be replaced.
|
|
2910
3430
|
_replaceRow(b, rs) {
|
|
2911
3431
|
const l = this.loc[b * this.M + rs];
|
|
2912
3432
|
if (l < 0) return;
|
|
@@ -2994,6 +3514,11 @@ export class Solver {
|
|
|
2994
3514
|
rest.multiply(v, jv);
|
|
2995
3515
|
if (save.transformed) this._untransform(v); // (the dilute terms in the plain unknowns)
|
|
2996
3516
|
lin.apply(v, jv, 1, 1 / dt);
|
|
3517
|
+
// A flat stretch's rows hold each level to its first node's (see _flatRows).
|
|
3518
|
+
for (const p of pins) {
|
|
3519
|
+
if (!p.col) continue;
|
|
3520
|
+
for (const o of p.col) if (o !== p.row && this.loc[o] >= 0) jv[R[o]] -= x[R[p.row]];
|
|
3521
|
+
}
|
|
2997
3522
|
const out = new Float64Array(N + K);
|
|
2998
3523
|
for (let j = 0; j < N; j++) {
|
|
2999
3524
|
let t = jv[j];
|
|
@@ -3038,8 +3563,13 @@ export class Solver {
|
|
|
3038
3563
|
// stops refining.
|
|
3039
3564
|
const x0 = Float64Array.from(x);
|
|
3040
3565
|
const ok = gmresReal(op, pb, x, { m: 8, restarts: 1, tol: 1e-8 });
|
|
3041
|
-
|
|
3042
|
-
|
|
3566
|
+
// (Measured on the potentials, as Newton's own step is: a rate the plain solve left
|
|
3567
|
+
// unsolved, at a perturbed pivot, can need a correction of 41.7 mol/(m²·s).)
|
|
3568
|
+
const dx = new Float64Array(N + 1), dV = new Float64Array(K);
|
|
3569
|
+
for (let j = 0; j < N; j++) dx[j] = x[j] - x0[j];
|
|
3570
|
+
for (let a = 0; a < K; a++) dV[a] = x[N + a] - x0[N + a];
|
|
3571
|
+
if (this.transformed) this._untransform(dx);
|
|
3572
|
+
const c = this._maxPotentialStep(dx, dV);
|
|
3043
3573
|
if (!ok || !(c < 1)) return false;
|
|
3044
3574
|
for (let j = 0; j < N; j++) delta[j] = x[j];
|
|
3045
3575
|
for (let a = 0; a < K; a++) deltaV[a] = x[N + a];
|
|
@@ -3091,9 +3621,21 @@ export class Solver {
|
|
|
3091
3621
|
// A/m² of noise), their rows can throw Newton off instead: if it doesn't converge again
|
|
3092
3622
|
// within a dozen iterations, the plain solution stands.
|
|
3093
3623
|
this.islandsOn = islands;
|
|
3094
|
-
let rounds = 0, kept = null;
|
|
3624
|
+
let rounds = 0, kept = null, perturbed = false;
|
|
3625
|
+
// Converged by a small update, but solved at a perturbed pivot, or in a steady solve (where a
|
|
3626
|
+
// factorisation that lost a row's digits left a reaction's rate 3.6e7 off it, the update
|
|
3627
|
+
// small, an I = 0 electrode at a voltage 0.4 V from its steady one): the residual itself,
|
|
3628
|
+
// assembled at the state, must be small too (each row scaled by its largest Jacobian entry).
|
|
3629
|
+
const unsolved = () => {
|
|
3630
|
+
if (!perturbed && dt !== Infinity) return false;
|
|
3631
|
+
this.assemble(dt);
|
|
3632
|
+
this._equilibrate();
|
|
3633
|
+
let r = 0;
|
|
3634
|
+
for (let k = 0; k < this.SINK; k++) r = Math.max(r, Math.abs(this.res[k]));
|
|
3635
|
+
return !(r < 1e-6);
|
|
3636
|
+
};
|
|
3095
3637
|
const islandsOff = (it, result) => {
|
|
3096
|
-
if (dt !== Infinity || !this.
|
|
3638
|
+
if (dt !== Infinity || !this.steady || this.islands.length === 0 || rounds >= 2 || !(this._kirchhoff() > 1e-9)) return false;
|
|
3097
3639
|
this.islandsOn = true;
|
|
3098
3640
|
rounds++;
|
|
3099
3641
|
kept = { it, result, u: Float64Array.from(this.u), uLo: Float64Array.from(this.uLo), termV: Float64Array.from(this.termV) };
|
|
@@ -3136,6 +3678,13 @@ export class Solver {
|
|
|
3136
3678
|
} catch (err) {
|
|
3137
3679
|
return fail({ converged: false, iterations: it, history, error: err.message });
|
|
3138
3680
|
}
|
|
3681
|
+
// A pivot perturbed (one that cancelled to exactly nothing) can leave a row unsolved, its
|
|
3682
|
+
// update small while its residual isn't: the same couple written twice on an electrode at
|
|
3683
|
+
// open circuit converged with one spelling's rate row 41.7 mol/(m²·s) off, the couple
|
|
3684
|
+
// 1.5 RT from equilibrium. Refine the solves (exact J·v) wherever one was, and before
|
|
3685
|
+
// taking a small update for convergence there, look at the residual itself (see unsolved).
|
|
3686
|
+
perturbed = this.sys.perturbedPivots > 0;
|
|
3687
|
+
if (perturbed && !noRefine) refine = true;
|
|
3139
3688
|
try {
|
|
3140
3689
|
const pins = this.pins;
|
|
3141
3690
|
deltaV.fill(0);
|
|
@@ -3174,6 +3723,7 @@ export class Solver {
|
|
|
3174
3723
|
const last = history[history.length - 2], theta = step / last;
|
|
3175
3724
|
const contracted = dt !== Infinity && it >= 2 && step < 1e-6 && theta < 0.1 && (theta / (1 - theta)) * step < tol;
|
|
3176
3725
|
if (alpha === 1 && (step < tol || contracted)) {
|
|
3726
|
+
if (unsolved()) return fail({ converged: false, iterations: it, history, error: 'a row left unsolved at a pivot that cancelled exactly' });
|
|
3177
3727
|
const done = { converged: true, iterations: it, history, residual: rmax };
|
|
3178
3728
|
if (islandsOff(it, done)) continue;
|
|
3179
3729
|
this.computeConcentrations();
|
|
@@ -3194,6 +3744,7 @@ export class Solver {
|
|
|
3194
3744
|
if (refine) refined++;
|
|
3195
3745
|
else if (!noRefine && alpha === 1 && it >= 3 && step > 0.25 * p1 && (step < 1e-4 || (step > p1 && p1 > p2))) refine = true;
|
|
3196
3746
|
if (alpha === 1 && it >= 4 && (!refine || refined >= 1 || noRefine) && step < floor && p1 < floor && step > 0.25 * p1 && p1 > 0.25 * p2) {
|
|
3747
|
+
if (unsolved()) return fail({ converged: false, iterations: it, history, error: 'a row left unsolved at a pivot that cancelled exactly' });
|
|
3197
3748
|
const done = { converged: true, iterations: it, history, residual: rmax, roundoff: true };
|
|
3198
3749
|
if (islandsOff(it, done)) continue;
|
|
3199
3750
|
this.computeConcentrations();
|
|
@@ -3227,18 +3778,20 @@ export class Solver {
|
|
|
3227
3778
|
}
|
|
3228
3779
|
if (!(c.digits > (this.conditioning?.digits ?? -1))) return;
|
|
3229
3780
|
const { grid } = this.model;
|
|
3230
|
-
let x = NaN, where = '';
|
|
3781
|
+
let x = NaN, where = '', nodes = [];
|
|
3231
3782
|
for (let g = 0; g < this.nNodes; g++) {
|
|
3232
3783
|
if (this.blockOfNode[g] !== c.block) continue;
|
|
3233
3784
|
x = grid.x[g];
|
|
3234
3785
|
where = this.model.regions[grid.nodeRegion[g]].name;
|
|
3786
|
+
nodes = [g];
|
|
3235
3787
|
}
|
|
3236
3788
|
for (let f = 0; f < this.nFaces; f++) {
|
|
3237
3789
|
if (this.blockOfFace[f] !== c.block) continue;
|
|
3238
3790
|
x = grid.x[grid.regionEnd[f]];
|
|
3239
3791
|
where = `interfaces[${f}]`;
|
|
3792
|
+
nodes = [grid.regionEnd[f], grid.regionEnd[f] + 1];
|
|
3240
3793
|
}
|
|
3241
|
-
this.conditioning = { digits: c.digits, x, where };
|
|
3794
|
+
this.conditioning = { digits: c.digits, x, where, nodes };
|
|
3242
3795
|
}
|
|
3243
3796
|
|
|
3244
3797
|
/**
|
|
@@ -3301,11 +3854,7 @@ export class Solver {
|
|
|
3301
3854
|
this.portQEnd = Float64Array.from(this.portQ);
|
|
3302
3855
|
}
|
|
3303
3856
|
const DN = { ...this.contactDEnd }, QN = Float64Array.from(this.portQEnd);
|
|
3304
|
-
if (opts.guess)
|
|
3305
|
-
// Start Newton from a predicted state (e.g. extrapolated from the history).
|
|
3306
|
-
this.u.set(opts.guess);
|
|
3307
|
-
this.uLo.fill(0);
|
|
3308
|
-
}
|
|
3857
|
+
if (opts.guess) this._guess(opts.guess); // start Newton from a predicted state
|
|
3309
3858
|
let dtEff = dt;
|
|
3310
3859
|
if (bdf) {
|
|
3311
3860
|
const a0 = (1 + 2 * w) / (1 + w), b1 = (1 + w) / a0, b2 = (w * w) / (1 + w) / a0;
|
|
@@ -3328,26 +3877,35 @@ export class Solver {
|
|
|
3328
3877
|
// Implicit: sources at the step's end, as seen from within the step (before any jump there).
|
|
3329
3878
|
// A step landing on a breakpoint ends on it exactly.
|
|
3330
3879
|
const tEnd = this.landing !== undefined && Math.abs(this.time + dt - this.landing) <= 1e-9 * dt ? this.landing : this.time + dt;
|
|
3331
|
-
this.sourceTime = this.steady ? this.
|
|
3880
|
+
this.sourceTime = this.steady ? this.steadyAt : tEnd;
|
|
3332
3881
|
this.sourceBefore = !this.steady;
|
|
3333
3882
|
this.combining = true;
|
|
3883
|
+
this.lawForm = new Map(); // (see _circuitLaws)
|
|
3334
3884
|
let result;
|
|
3335
3885
|
try {
|
|
3336
3886
|
result = this.newton(dtEff, opts);
|
|
3337
|
-
if (!result.converged && dtEff === Infinity && this.
|
|
3887
|
+
if (!result.converged && dtEff === Infinity && this.steady && this.islands.length > 0) {
|
|
3338
3888
|
// Plain solves that don't converge may be lost along an island's level (two regions
|
|
3339
3889
|
// conducting 1e20 times less hold it): once more from the start, with the islands'
|
|
3340
3890
|
// summed rows in throughout (see newton).
|
|
3341
3891
|
this.u.set(this.uPrev);
|
|
3342
3892
|
this.uLo.set(this.uPrevLo);
|
|
3343
3893
|
this.termV.set(this.termVPrev);
|
|
3344
|
-
if (opts.guess)
|
|
3345
|
-
this.u.set(opts.guess);
|
|
3346
|
-
this.uLo.fill(0);
|
|
3347
|
-
}
|
|
3894
|
+
if (opts.guess) this._guess(opts.guess);
|
|
3348
3895
|
const again = this.newton(dtEff, { ...opts, islands: true });
|
|
3349
3896
|
result = { ...again, iterations: result.iterations + again.iterations };
|
|
3350
3897
|
}
|
|
3898
|
+
if (!result.converged && Number.isFinite(dtEff) && [...this.lawForm.values()].some((v) => !v)) {
|
|
3899
|
+
// A terminal read by its flux whose response is lost to round-off (a floating gate's
|
|
3900
|
+
// charging, below its metal's G Δη): once more, read by the change (see _circuitLaws).
|
|
3901
|
+
this.u.set(this.uPrev);
|
|
3902
|
+
this.uLo.set(this.uPrevLo);
|
|
3903
|
+
this.termV.set(this.termVPrev);
|
|
3904
|
+
if (opts.guess) this._guess(opts.guess);
|
|
3905
|
+
for (const cs of this.lawForm.keys()) this.lawForm.set(cs, true);
|
|
3906
|
+
const again = this.newton(dtEff, opts);
|
|
3907
|
+
result = { ...again, iterations: result.iterations + again.iterations };
|
|
3908
|
+
}
|
|
3351
3909
|
} finally {
|
|
3352
3910
|
this.combining = false;
|
|
3353
3911
|
}
|
|
@@ -3360,17 +3918,35 @@ export class Solver {
|
|
|
3360
3918
|
this.contactDOld = this.contactDStart;
|
|
3361
3919
|
this.portQOld = this.portQStart;
|
|
3362
3920
|
this._accumulateBoundaryIntake(dtEff, cN);
|
|
3921
|
+
// (In time, the state is no longer the steady solve's that a continuation would start from.)
|
|
3922
|
+
if (!this.steady) {
|
|
3923
|
+
this.solvedV = undefined;
|
|
3924
|
+
this.stepped = true;
|
|
3925
|
+
}
|
|
3363
3926
|
} else {
|
|
3364
3927
|
this.u.set(this.uPrev);
|
|
3365
3928
|
this.uLo.set(this.uPrevLo);
|
|
3366
3929
|
this.termV.set(this.termVPrev);
|
|
3367
|
-
this.sourceTime = this.time;
|
|
3930
|
+
this.sourceTime = this.steady ? this.steadyAt : this.time;
|
|
3368
3931
|
this.computeConcentrations();
|
|
3369
3932
|
}
|
|
3370
3933
|
this.sourceBefore = false;
|
|
3371
3934
|
return result;
|
|
3372
3935
|
}
|
|
3373
3936
|
|
|
3937
|
+
// Newton's unknowns set to a guess (e.g. extrapolated from the history). Only its unknowns: the
|
|
3938
|
+
// other slots never change (see _activeSlots), and among them is a gauge, an insertion host's
|
|
3939
|
+
// φ̂, that nothing pulls back. Extrapolated through steps that grow, it wandered off
|
|
3940
|
+
// geometrically, 147 to 8000 thermal units in a battery's first 100 s and 1e11 by 1e5 s, where
|
|
3941
|
+
// its species' levels (η − zφ̂, summed back by the host's statistics) lost their digits to it.
|
|
3942
|
+
_guess(guess) {
|
|
3943
|
+
const { u, uLo } = this;
|
|
3944
|
+
for (const k of this.fullOf) {
|
|
3945
|
+
u[k] = guess[k];
|
|
3946
|
+
uLo[k] = 0;
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3374
3950
|
/**
|
|
3375
3951
|
* Adaptive time stepping to tEnd: variable-step BDF2 (or backward Euler), with the local error
|
|
3376
3952
|
* estimated against an explicit predictor through the previous states, and controlled to
|
|
@@ -3379,6 +3955,11 @@ export class Solver {
|
|
|
3379
3955
|
* animation can call it once per frame; the step size carries over between calls.
|
|
3380
3956
|
*/
|
|
3381
3957
|
integrate(tEnd, opts = {}) {
|
|
3958
|
+
// tEnd is an absolute time. Not a number at all is an error; a NaN (a first animation frame's
|
|
3959
|
+
// clock, or a time read from a field that doesn't exist) does nothing, and says so.
|
|
3960
|
+
const absolute = `tEnd is the time to reach in s, absolute (the device is at t = ${this.time}, which a solution gives as its time)`;
|
|
3961
|
+
if (typeof tEnd !== 'number') throw new SolverError(`advance(tEnd): ${absolute}; got ${tEnd}`);
|
|
3962
|
+
if (Number.isNaN(tEnd)) return { converged: true, done: false, steps: 0, rejected: 0, iterations: 0, trace: { t: [], current: [], voltage: [] }, stopped: `advance(NaN) did nothing: ${absolute}` };
|
|
3382
3963
|
const { tol = 1e-3, dtMax = Infinity, budgetMs = Infinity, maxSteps = 100000, method = 'bdf2' } = opts;
|
|
3383
3964
|
const clock = () => (globalThis.performance ? globalThis.performance.now() : Date.now());
|
|
3384
3965
|
const start = clock();
|
|
@@ -3388,6 +3969,7 @@ export class Solver {
|
|
|
3388
3969
|
// Already there (an animation frame with no time to add): nothing to do, and the step size
|
|
3389
3970
|
// carried to the next call stays as it was.
|
|
3390
3971
|
if (!(tEnd > this.time)) return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace };
|
|
3972
|
+
const t0 = this.time;
|
|
3391
3973
|
let dt = this.dtNext ?? opts.dt0 ?? (tEnd - this.time) * 1e-4;
|
|
3392
3974
|
if (!(dt > 0)) dt = (tEnd - this.time) * 1e-4;
|
|
3393
3975
|
// The shortest step worth trying: round-off relative to the time, or the device's fastest
|
|
@@ -3400,8 +3982,10 @@ export class Solver {
|
|
|
3400
3982
|
if (steps + rejected >= maxSteps || clock() - start > budgetMs) break;
|
|
3401
3983
|
// Within round-off of the end (a target a hair past a breakpoint just landed on, say):
|
|
3402
3984
|
// snap to it rather than attempt a step of ~1e-13 s, which can't be resolved and, in a
|
|
3403
|
-
// strictly neutral device, fails outright.
|
|
3404
|
-
|
|
3985
|
+
// strictly neutral device, fails outright. Round-off of the time and negligible beside the
|
|
3986
|
+
// interval asked for: by the time alone, a GITT pulse of 0.01 s at t = 1e9 s was skipped
|
|
3987
|
+
// whole (the time moved, nothing else did), and from 1e10 s 17% of a pulse train's charge.
|
|
3988
|
+
if (tEnd - this.time <= 1e-10 * Math.abs(tEnd) && tEnd - this.time <= 1e-9 * (tEnd - t0)) {
|
|
3405
3989
|
this.time = tEnd;
|
|
3406
3990
|
break;
|
|
3407
3991
|
}
|
|
@@ -3495,7 +4079,10 @@ export class Solver {
|
|
|
3495
4079
|
// Stopped short, other than on the caller's wall-time budget: say where, and why.
|
|
3496
4080
|
let stopped;
|
|
3497
4081
|
const at = `advance stopped at t = ${this.time.toPrecision(6)} s of ${tEnd.toPrecision(6)}`;
|
|
3498
|
-
|
|
4082
|
+
// (What usually does it, most likely first. Round 11's agents read the old list, which led
|
|
4083
|
+
// with a drive's jump, as blaming a modest current pulse: a host emptied at its surface, and
|
|
4084
|
+
// an electrode at open circuit with no double layer, held only by a slow reaction.)
|
|
4085
|
+
if (failed) stopped = `${at}: a step did not converge even when shortened to ${dt.toExponential(1)} s. Look for: a host or surface driven to its limit (emptied or filled at its surface by a current it can no longer pass); an electrode at open circuit held only by a slow reaction, with no double-layer capacitance to keep its level (give its face one: phi capacitive); a drive's jump too big for one step (ramp it); a driven current the device cannot keep passing (a diode's reverse recovery past its storage time: stop there); or a device the steady solves also find hard.`;
|
|
3499
4086
|
else if (this.time < tEnd && steps + rejected >= maxSteps) stopped = `${at}: maxSteps (${maxSteps}) used up (${rejected} of them rejected). Call again to go on, or loosen tol.`;
|
|
3500
4087
|
return { converged: !failed, done: this.time >= tEnd, steps, rejected, iterations, trace, ...(stopped ? { stopped } : {}) };
|
|
3501
4088
|
}
|
|
@@ -3583,9 +4170,18 @@ export class Solver {
|
|
|
3583
4170
|
if (!(r >= 0 && r < regions.length)) fail(`${path}.region: no region ${JSON.stringify(p.region)}`);
|
|
3584
4171
|
if (!inside(r)) fail(`${path}.x (${p.x} m) is outside region ${JSON.stringify(p.region)}`);
|
|
3585
4172
|
} else {
|
|
3586
|
-
|
|
3587
|
-
|
|
4173
|
+
// At a face, the side where the probe has something to read; both, and it must be told.
|
|
4174
|
+
const has = (rr) => {
|
|
4175
|
+
const mat = this.model.materials[regions[rr].material];
|
|
4176
|
+
return i < 0 ? !mat.conductor && !mat.phiFree : mat.present[i];
|
|
4177
|
+
};
|
|
4178
|
+
const sides = regions.flatMap((_, rr) => (inside(rr) ? [rr] : []));
|
|
4179
|
+
if (!(Number.isFinite(p.x) && sides.length)) fail(`${path}.x must be a position in the device, in m (got ${p.x})`);
|
|
4180
|
+
const able = sides.filter(has);
|
|
4181
|
+
if (able.length > 1) fail(`${path}.x (${p.x} m) is on the face between ${able.map((rr) => JSON.stringify(regions[rr].name)).join(' and ')}: give region, the side to read`);
|
|
4182
|
+
r = able[0] ?? sides[0];
|
|
3588
4183
|
}
|
|
4184
|
+
if (i >= 0 && !this.model.materials[regions[r].material].present[i]) fail(`${path}: '${p.species}' is absent from region ${JSON.stringify(regions[r].name)}, so there's nothing to read at x = ${p.x} m`);
|
|
3589
4185
|
let g = grid.regionStart[r];
|
|
3590
4186
|
while (g + 1 < grid.regionEnd[r] && x[g + 1] < p.x) g++;
|
|
3591
4187
|
const w = x[g + 1] > x[g] ? (p.x - x[g]) / (x[g + 1] - x[g]) : 0;
|
|
@@ -3704,8 +4300,19 @@ export class Solver {
|
|
|
3704
4300
|
this.computeConcentrations();
|
|
3705
4301
|
}
|
|
3706
4302
|
|
|
4303
|
+
// What a floating terminal k passes into the device, which its circuit row holds exactly once
|
|
4304
|
+
// converged: the current it's driven by, or (V_source − V)/R behind a resistance (the source
|
|
4305
|
+
// as the last step saw it, before any jump at its end). Read as a flux through a metal instead,
|
|
4306
|
+
// it carried that flux's round-off, G Δη with G huge: a gate behind R read 1.5e-4 off wherever
|
|
4307
|
+
// Newton stopped an update early, and below ~1e-16 A/m², nothing but noise.
|
|
4308
|
+
passedCurrent(k) {
|
|
4309
|
+
const d = this.terms[k].drive, src = sourceAt(d.src, this.time, !this.atSteady);
|
|
4310
|
+
return d.kind === 'I' ? src : (src - this.termV[k]) / d.R;
|
|
4311
|
+
}
|
|
4312
|
+
|
|
3707
4313
|
// Terminal current toward +x at the right contact, from the last assembly at this state.
|
|
3708
4314
|
_terminalCurrent() {
|
|
4315
|
+
if (this.floating.includes(1)) return -this.passedCurrent(1);
|
|
3709
4316
|
let I = 0;
|
|
3710
4317
|
for (let i = 0; i < this.n; i++) I += FARADAY * this.z[i] * this.contactFlux.right[i];
|
|
3711
4318
|
const dt = this.lastDt;
|
|
@@ -3739,6 +4346,15 @@ export class Solver {
|
|
|
3739
4346
|
if (kT < 0) throw new SolverError(`impedance: no terminal named '${terminal}' (${this.terms.map((t) => t.name).join(', ')})`);
|
|
3740
4347
|
const dT = this.terms[kT].drive;
|
|
3741
4348
|
if (dT.R > 0) throw new SolverError(`impedance: terminal '${terminal}' has a series resistance; that belongs to the external circuit`);
|
|
4349
|
+
// A small signal returns through the terminals held at a voltage: with every other one driven
|
|
4350
|
+
// by a current (held open), none comes back, and the impedance is infinite (else round-off).
|
|
4351
|
+
const others = this.terms.filter((t, k) => k !== kT);
|
|
4352
|
+
if (others.length && others.every((t) => t.drive.kind === 'I')) {
|
|
4353
|
+
throw new SolverError(
|
|
4354
|
+
`impedance: every other terminal (${others.map((t) => t.name).join(', ')}) is driven by a current, so it is open to a small signal and no current returns through '${terminal}': ` +
|
|
4355
|
+
`its impedance is infinite. Measure at one of those instead (impedance(f, { terminal: '${others[0].name}' }), its voltage against '${terminal}'), or hold one of them at a voltage`,
|
|
4356
|
+
);
|
|
4357
|
+
}
|
|
3742
4358
|
const N = nB * M, fl = this.floating, K = fl.length;
|
|
3743
4359
|
this.sourceTime = this.time;
|
|
3744
4360
|
this.computeConcentrations();
|
|
@@ -4290,12 +4906,20 @@ export class Solver {
|
|
|
4290
4906
|
if (!Number.isFinite(dt)) return;
|
|
4291
4907
|
const last = this.model.regions.length - 1;
|
|
4292
4908
|
const { n, cOld } = this, vol = this.model.grid.vol;
|
|
4909
|
+
// A stretch alone in what a terminal driven by a current feeds takes in what that current
|
|
4910
|
+
// says, which the step kept exactly (see _circuitLaws); the flux through a metal is round-off.
|
|
4911
|
+
const driven = new Map();
|
|
4912
|
+
for (const cs of this.constraints) {
|
|
4913
|
+
if (cs.circuit < 0 || this.terms[cs.circuit].drive.kind !== 'I' || cs.parts.length !== 1 || cs.surface.length || cs.carriers.length) continue;
|
|
4914
|
+
driven.set(cs.parts[0].stretch, (cs.lambda * sourceAt(this.terms[cs.circuit].drive.src, this.sourceTime, this.sourceBefore)) / (cs.parts[0].w * FARADAY));
|
|
4915
|
+
}
|
|
4293
4916
|
this.stretches.forEach((st, k) => {
|
|
4294
4917
|
if (!st.connected) return;
|
|
4295
4918
|
let q = 0;
|
|
4296
4919
|
if (st.regions[0] === 0) q += this.contactFlux.left[st.species];
|
|
4297
4920
|
if (st.regions[1] === last) q -= this.contactFlux.right[st.species];
|
|
4298
4921
|
for (const p of st.ports) q += this.portFlux[p][st.species];
|
|
4922
|
+
if (driven.has(k)) q = driven.get(k);
|
|
4299
4923
|
let hist = 0;
|
|
4300
4924
|
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) hist += vol[g] * (cOld[g * n + st.species] - cN[g * n + st.species]);
|
|
4301
4925
|
this.boundaryIntake[k] += q * dt + hist;
|
|
@@ -4325,45 +4949,55 @@ export class Solver {
|
|
|
4325
4949
|
}
|
|
4326
4950
|
|
|
4327
4951
|
/**
|
|
4328
|
-
* Steady state
|
|
4329
|
-
*
|
|
4330
|
-
*
|
|
4331
|
-
*
|
|
4332
|
-
*
|
|
4333
|
-
*
|
|
4334
|
-
* If Newton fails, dt ramps up from a small value (pseudo-transient continuation) instead.
|
|
4952
|
+
* Steady state: where a transient from the present state ends, found as the solution of the
|
|
4953
|
+
* steady equations (dt = ∞, no storage term), with no slow modes to wait out, however slow the
|
|
4954
|
+
* physics (e.g. exponentially scarce minority carriers). What the storage term held is held by
|
|
4955
|
+
* the steady system's own rows instead (see _laws): each conserved amount in place of a balance
|
|
4956
|
+
* row that says nothing new, levels that carry nothing held flat, islands' summed balances.
|
|
4957
|
+
* Where Newton from here fails: continuation along the drives, then the pseudo-transient ramp.
|
|
4335
4958
|
* The clock is not advanced, and open-system conservation bookkeeping restarts here.
|
|
4336
4959
|
*/
|
|
4337
4960
|
// Steady state from the present one; `atSteady` says whether the state is one (until a step or
|
|
4338
4961
|
// a change of drive).
|
|
4339
4962
|
solveSteady(opts = {}) {
|
|
4340
|
-
this.flattening = true;
|
|
4341
4963
|
this.unreached = null; // (a driven current no held voltage reaches: see _floatingContinuation)
|
|
4964
|
+
this.charging = null; // (a driven current that only charges: see _solveSteadyAll)
|
|
4965
|
+
this._captureLaws();
|
|
4966
|
+
this.steady = true; // (the steady system's rows; sources at the present time)
|
|
4967
|
+
this.steadyAt = this.time; // (which a pseudo-transient's steps don't move on)
|
|
4342
4968
|
try {
|
|
4343
4969
|
return this._solveSteadyAll(opts);
|
|
4344
4970
|
} finally {
|
|
4345
|
-
this.
|
|
4971
|
+
this.steady = false;
|
|
4346
4972
|
}
|
|
4347
4973
|
}
|
|
4348
4974
|
|
|
4349
4975
|
_solveSteadyAll(opts) {
|
|
4350
|
-
// A terminal
|
|
4351
|
-
//
|
|
4352
|
-
//
|
|
4353
|
-
const { species } = this.model
|
|
4976
|
+
// A terminal driven by a current that can only store what comes in (a battery's cathode
|
|
4977
|
+
// behind it, closed but for that terminal; a gate, or a capacitance with no species through
|
|
4978
|
+
// it) has no steady state but at no current, where it keeps what it holds.
|
|
4979
|
+
const { species, regions } = this.model;
|
|
4354
4980
|
for (const k of this.floating) {
|
|
4355
|
-
const
|
|
4356
|
-
if (
|
|
4357
|
-
const
|
|
4358
|
-
if (
|
|
4359
|
-
|
|
4360
|
-
|
|
4981
|
+
const I = sourceAt(this.terms[k].drive.src, this.time);
|
|
4982
|
+
if (this.terms[k].drive.kind !== 'I' || I === 0) continue;
|
|
4983
|
+
const laws = this.constraints.filter((cs) => cs.terminals.includes(k));
|
|
4984
|
+
if (laws.length === 0 && !this._chargeOnly(k)) continue;
|
|
4985
|
+
const what = laws.flatMap((cs) => cs.parts.map(({ stretch }) => {
|
|
4986
|
+
const st = this.stretches[stretch], [r0, r1] = st.regions;
|
|
4987
|
+
return `${species[st.species].name} in ${regions[r0].name}${r1 > r0 ? `…${regions[r1].name}` : ''}`;
|
|
4988
|
+
}));
|
|
4989
|
+
this.charging = { terminal: this.terms[k].name, current: I, what };
|
|
4990
|
+
return { converged: false, steps: 0, iterations: 0, history: [] };
|
|
4361
4991
|
}
|
|
4362
4992
|
const r = this._steadyFromHere(opts);
|
|
4363
4993
|
this.atSteady = r.converged;
|
|
4364
4994
|
return r;
|
|
4365
4995
|
}
|
|
4366
4996
|
|
|
4997
|
+
// Steady state from the present one, in three stages: Newton from here; failing that, from a
|
|
4998
|
+
// solved state along the drives (the light ramped up, a current's terminal held at the voltages
|
|
4999
|
+
// that pass it, the voltages ramped from where they were last solved); failing that, the
|
|
5000
|
+
// pseudo-transient ramp, through time.
|
|
4367
5001
|
_steadyFromHere(opts) {
|
|
4368
5002
|
this.conditioning = null;
|
|
4369
5003
|
this.computeConcentrations();
|
|
@@ -4373,86 +5007,110 @@ export class Solver {
|
|
|
4373
5007
|
const [dl, dr] = [this.terms[0].drive, this.terms[1].drive];
|
|
4374
5008
|
const held = (d) => d.kind === 'V' && !(d.R > 0);
|
|
4375
5009
|
const target = sourceAt(dr.src, this.time), level = sourceAt(dl.src, this.time);
|
|
4376
|
-
const
|
|
5010
|
+
const allowed = opts.continuation !== false;
|
|
5011
|
+
const canContinue = allowed && held(dl) && held(dr) && target !== level;
|
|
4377
5012
|
const direct = this._directSteady();
|
|
4378
5013
|
const u0 = Float64Array.from(this.u), u0Lo = Float64Array.from(this.uLo), v0 = Float64Array.from(this.termV);
|
|
4379
|
-
|
|
4380
|
-
// pseudo-transient ramp: one direct attempt first.
|
|
4381
|
-
const quick = (canContinue || this.hasGeneration) && direct && opts.continuation !== false;
|
|
4382
|
-
let r = this._solveSteady(quick ? { ...opts, maxSteps: 1 } : opts);
|
|
4383
|
-
if (r.converged) this.solvedV = [level, target];
|
|
4384
|
-
if (r.converged) return r;
|
|
4385
|
-
// Generation (e.g. light) holding the device far from equilibrium: ramp it up from nearly
|
|
4386
|
-
// nothing, each solve warm from the last.
|
|
4387
|
-
let dimmer = false;
|
|
4388
|
-
if (this.hasGeneration && opts.continuation !== false) {
|
|
5014
|
+
const restart = () => {
|
|
4389
5015
|
this.u.set(u0);
|
|
4390
5016
|
this.uLo.set(u0Lo);
|
|
4391
5017
|
this.termV.set(v0);
|
|
4392
5018
|
this.computeConcentrations();
|
|
5019
|
+
};
|
|
5020
|
+
const solved = (q) => {
|
|
5021
|
+
if (q.converged) {
|
|
5022
|
+
this.solvedV = [level, target];
|
|
5023
|
+
this.stepped = false;
|
|
5024
|
+
}
|
|
5025
|
+
return q;
|
|
5026
|
+
};
|
|
5027
|
+
const after = (q, r) => ({ ...q, steps: q.steps + r.steps, iterations: q.iterations + r.iterations });
|
|
5028
|
+
|
|
5029
|
+
// 1. Newton from here. Where a continuation could follow, one direct attempt only, rather
|
|
5030
|
+
// than the pseudo-transient ramp straight away.
|
|
5031
|
+
const quick = (canContinue || this.hasGeneration) && direct && allowed;
|
|
5032
|
+
let r = this._steadySteps(quick ? { ...opts, maxSteps: 1 } : opts);
|
|
5033
|
+
if (r.converged) return solved(r);
|
|
5034
|
+
// A transient's state can be a worse start than a cold one: 1 ns after a lit MOS capacitor's
|
|
5035
|
+
// gate jumps, its surface is in deep depletion, minority carriers ten orders below where they
|
|
5036
|
+
// end, and Newton fails from there where it converges from the layout for the drives (and
|
|
5037
|
+
// the stages below took 9000 iterations from there, 2000 from the layout). What a steady
|
|
5038
|
+
// solve keeps it took from the state already, so from here on it goes as a cold device would.
|
|
5039
|
+
if (this.stepped && direct && allowed) {
|
|
5040
|
+
this._refreshSources();
|
|
5041
|
+
this.initFromComposition();
|
|
5042
|
+
u0.set(this.u);
|
|
5043
|
+
u0Lo.set(this.uLo);
|
|
5044
|
+
const q = this._steadySteps(quick ? { ...opts, maxSteps: 1 } : opts);
|
|
5045
|
+
if (q.converged) return solved(after(q, r));
|
|
5046
|
+
restart();
|
|
5047
|
+
r = after(q, r);
|
|
5048
|
+
}
|
|
5049
|
+
|
|
5050
|
+
// 2. Continuation. Generation (e.g. light) holding the device far from equilibrium: ramp it up
|
|
5051
|
+
// from nearly nothing, each solve warm from the last.
|
|
5052
|
+
let dimmer = false;
|
|
5053
|
+
if (this.hasGeneration && allowed) {
|
|
5054
|
+
restart();
|
|
4393
5055
|
this.dimmer = false;
|
|
4394
5056
|
const g = this._generationContinuation(opts, r);
|
|
4395
5057
|
dimmer = this.dimmer;
|
|
4396
|
-
if (g.converged)
|
|
4397
|
-
this.solvedV = [level, target];
|
|
4398
|
-
return g;
|
|
4399
|
-
}
|
|
5058
|
+
if (g.converged) return solved(g);
|
|
4400
5059
|
r = g;
|
|
4401
5060
|
}
|
|
4402
5061
|
// A terminal driven by a current (open circuit, say): hold it at a voltage instead, march the
|
|
4403
5062
|
// voltage until the current crosses its target, and float it from there.
|
|
4404
|
-
if (
|
|
4405
|
-
|
|
4406
|
-
this.uLo.set(u0Lo);
|
|
4407
|
-
this.termV.set(v0);
|
|
4408
|
-
this.computeConcentrations();
|
|
5063
|
+
if (allowed && this.floating.some((k) => this.terms[k].drive.kind === 'I' && !this._bound(k))) {
|
|
5064
|
+
restart();
|
|
4409
5065
|
const q = this._floatingContinuation(opts, r);
|
|
4410
5066
|
if (q.converged) return q;
|
|
4411
5067
|
r = q;
|
|
4412
5068
|
}
|
|
4413
|
-
if (
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
}
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
5069
|
+
if (canContinue) {
|
|
5070
|
+
// The right terminal's voltage ramped to its target: from the contacts' voltages at the last
|
|
5071
|
+
// converged solve when the state is that solution (a warm start), the left one first where
|
|
5072
|
+
// it moved, the right held where it was; otherwise from level terminals, consistent with a
|
|
5073
|
+
// cold start.
|
|
5074
|
+
restart();
|
|
5075
|
+
const was = this.solvedV;
|
|
5076
|
+
let c;
|
|
5077
|
+
if (was) {
|
|
5078
|
+
c = was[0] === level ? { ...r, converged: true, ramped: false } : this._continuation(opts, 0, was[0], level, r, true, was[1]);
|
|
5079
|
+
// (Ramping the right from level terminals, solve there first: a sweep's start.)
|
|
5080
|
+
if (c.converged) c = this._continuation(opts, 1, was[1], target, c, c.ramped || was[1] !== level);
|
|
5081
|
+
} else c = this._continuation(opts, 1, level, target, r, false);
|
|
5082
|
+
if (c.converged) return solved(c);
|
|
5083
|
+
// 3. The pseudo-transient ramp, from the start.
|
|
5084
|
+
restart();
|
|
5085
|
+
return solved(after(this._steadySteps(opts), c));
|
|
5086
|
+
}
|
|
5087
|
+
// 3. The pseudo-transient ramp (where the first attempt wasn't it already): from the dimmer
|
|
5088
|
+
// light's solution where the light's continuation got anywhere (a lit floating base charges
|
|
5089
|
+
// from there as it would in time), else from the start.
|
|
5090
|
+
if (!quick) return r;
|
|
5091
|
+
if (!dimmer) restart();
|
|
5092
|
+
else this.computeConcentrations();
|
|
5093
|
+
return after(this._steadySteps(opts), r);
|
|
5094
|
+
}
|
|
5095
|
+
|
|
5096
|
+
// Whether terminal k, driven by a current, feeds a combination that it alone can change, or
|
|
5097
|
+
// only charges a capacitance (so that its steady states are one per amount or charge, not one
|
|
5098
|
+
// per voltage).
|
|
5099
|
+
_bound(k) {
|
|
5100
|
+
return this.constraints.some((cs) => cs.terminals.includes(k)) || this._chargeOnly(k);
|
|
5101
|
+
}
|
|
5102
|
+
|
|
5103
|
+
// Whether terminal k passes current only by charging a capacitance: no charged species through
|
|
5104
|
+
// it (nor, a port, reactions), and a capacitance to charge (a gate's φ law, a port's own).
|
|
5105
|
+
_chargeOnly(k) {
|
|
5106
|
+
const t = this.terms[k], { species } = this.model;
|
|
5107
|
+
const ions = (links) => links.some((l, i) => l.type !== 'blocked' && species[i].z !== 0);
|
|
5108
|
+
if (t.kind === 'port') {
|
|
5109
|
+
const p = this.model.ports[t.index];
|
|
5110
|
+
return p.reactions.length === 0 && !ions(p.species) && !!p.capacitance;
|
|
5111
|
+
}
|
|
5112
|
+
const c = this.model.contacts[t.side];
|
|
5113
|
+
return !ions(c.species) && (c.phi.type === 'capacitive' || c.phi.type === 'pinned');
|
|
4456
5114
|
}
|
|
4457
5115
|
|
|
4458
5116
|
// A cold steady solve with a terminal driven by a current, which can fail where the same state
|
|
@@ -4461,7 +5119,7 @@ export class Solver {
|
|
|
4461
5119
|
// the start's until the current crosses its target, the crossing narrowed by bisection, and
|
|
4462
5120
|
// the terminal floated again from beside it.
|
|
4463
5121
|
_floatingContinuation(opts, r) {
|
|
4464
|
-
const k = this.floating.find((j) => this.terms[j].drive.kind === 'I');
|
|
5122
|
+
const k = this.floating.find((j) => this.terms[j].drive.kind === 'I' && !this._bound(j));
|
|
4465
5123
|
const drive = this.terms[k].drive, target = sourceAt(drive.src, this.time);
|
|
4466
5124
|
let steps = r.steps ?? 0, iterations = r.iterations ?? 0;
|
|
4467
5125
|
const history = (r.history ?? []).slice();
|
|
@@ -4524,7 +5182,7 @@ export class Solver {
|
|
|
4524
5182
|
this.terms[k].drive = drive;
|
|
4525
5183
|
this.redrive();
|
|
4526
5184
|
}
|
|
4527
|
-
const q = this.
|
|
5185
|
+
const q = this._steadySteps(opts);
|
|
4528
5186
|
steps += q.steps ?? 0;
|
|
4529
5187
|
iterations += q.iterations ?? 0;
|
|
4530
5188
|
history.push(...(q.history ?? []));
|
|
@@ -4567,7 +5225,7 @@ export class Solver {
|
|
|
4567
5225
|
try {
|
|
4568
5226
|
this.generationScale = s;
|
|
4569
5227
|
// (nearly dark: a cold start can need the full ramp, as a dark device's does)
|
|
4570
|
-
let q = this.
|
|
5228
|
+
let q = this._steadySteps(sub);
|
|
4571
5229
|
steps += q.steps;
|
|
4572
5230
|
iterations += q.iterations;
|
|
4573
5231
|
if (!q.converged) return { converged: false, steps, iterations, history };
|
|
@@ -4576,7 +5234,7 @@ export class Solver {
|
|
|
4576
5234
|
const next = Math.min(1, s * factor);
|
|
4577
5235
|
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
4578
5236
|
this.generationScale = next;
|
|
4579
|
-
q = this.
|
|
5237
|
+
q = this._steadySteps(sub);
|
|
4580
5238
|
steps += q.steps;
|
|
4581
5239
|
iterations += q.iterations;
|
|
4582
5240
|
history.push({ generation: next, converged: q.converged });
|
|
@@ -4612,7 +5270,7 @@ export class Solver {
|
|
|
4612
5270
|
if (hold !== undefined) this.sourceOverride.set(1 - k, hold);
|
|
4613
5271
|
let q;
|
|
4614
5272
|
if (!warm) {
|
|
4615
|
-
q = this.
|
|
5273
|
+
q = this._steadySteps(opts);
|
|
4616
5274
|
if (!q.converged) {
|
|
4617
5275
|
// A cold device's start was laid out for the target (a set() before the first solve
|
|
4618
5276
|
// builds it there): lay it out again, level.
|
|
@@ -4620,7 +5278,7 @@ export class Solver {
|
|
|
4620
5278
|
iterations += q.iterations;
|
|
4621
5279
|
this._refreshSources();
|
|
4622
5280
|
this.initFromComposition();
|
|
4623
|
-
q = this.
|
|
5281
|
+
q = this._steadySteps(opts);
|
|
4624
5282
|
}
|
|
4625
5283
|
// (lit, the level start may need the light ramped up too)
|
|
4626
5284
|
if (!q.converged && this.hasGeneration) q = this._generationContinuation(opts, q);
|
|
@@ -4632,7 +5290,7 @@ export class Solver {
|
|
|
4632
5290
|
const next = Math.abs(target - V) <= Math.abs(dV) ? target : V + dV;
|
|
4633
5291
|
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
4634
5292
|
this.sourceOverride.set(k, next);
|
|
4635
|
-
q = this.
|
|
5293
|
+
q = this._steadySteps(sub);
|
|
4636
5294
|
steps += q.steps;
|
|
4637
5295
|
iterations += q.iterations;
|
|
4638
5296
|
history.push({ continuation: next, converged: q.converged });
|
|
@@ -4656,23 +5314,11 @@ export class Solver {
|
|
|
4656
5314
|
}
|
|
4657
5315
|
}
|
|
4658
5316
|
|
|
4659
|
-
_solveSteady(opts = {}) {
|
|
4660
|
-
this.constrained = true; // spectators' amounts as constraints in the dt = ∞ solves
|
|
4661
|
-
this.steady = true;
|
|
4662
|
-
try {
|
|
4663
|
-
return this._steadySteps(opts);
|
|
4664
|
-
} finally {
|
|
4665
|
-
this.constrained = false;
|
|
4666
|
-
this.steady = false;
|
|
4667
|
-
}
|
|
4668
|
-
}
|
|
4669
|
-
|
|
4670
5317
|
_steadySteps({ maxSteps = 80, tol = 1e-11 } = {}) {
|
|
4671
5318
|
const time = this.time;
|
|
4672
5319
|
const tau = this.slowestTime();
|
|
4673
5320
|
const direct = this._directSteady();
|
|
4674
5321
|
const giant = direct ? Infinity : 1e6 * tau;
|
|
4675
|
-
if (direct) this._renormalizeSpectators(); // a starting point with the right amounts
|
|
4676
5322
|
let dt = giant, grow = 10; // (after a failure, dt grows by less, then back up to ×10)
|
|
4677
5323
|
let triedDirect = 0;
|
|
4678
5324
|
let totalIter = 0, steps = 0, converged = false;
|