driftlet 0.13.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/alignment.md +6 -3
- package/docs/data.md +3 -1
- package/docs/device.md +97 -33
- package/docs/kit.md +95 -8
- package/docs/logo-dark.svg +3 -2
- package/docs/logo.svg +3 -2
- package/docs/numerics.md +193 -114
- package/docs/reliability.md +16 -10
- package/docs/statistics.md +28 -4
- package/llms.txt +325 -335
- package/package.json +1 -1
- package/src/blockTridiagonal.js +12 -6
- package/src/checks.js +16 -5
- package/src/describe.js +46 -5
- package/src/device.js +88 -10
- package/src/index.js +19 -7
- package/src/kit.js +1 -0
- package/src/particles.js +407 -0
- package/src/solution.js +171 -10
- package/src/solver.js +797 -262
- package/src/sources.js +10 -2
- package/src/stack.js +16 -6
- package/src/statistics.js +7 -3
- package/types/checks.d.ts +2 -1
- package/types/kit.d.ts +1 -0
- package/types/particles.d.ts +39 -0
- package/types/solution.d.ts +1 -0
- package/types/solver.d.ts +47 -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.terms.map((t) => `${t.drive.kind}${t.drive.R > 0 ? 'R' : ''}`).join();
|
|
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,84 @@ 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
|
+
for (let w of [...laws.filter(through), ...laws.filter((w) => !through(w))]) {
|
|
886
|
+
for (const [j, k] of this.carriers.entries()) {
|
|
887
|
+
const c = held.get(k);
|
|
888
|
+
if (w[X + j] === 0 || !c) continue;
|
|
889
|
+
const f = w[X + j] / c[X + j], own = w.own;
|
|
890
|
+
w = w.map((x, m) => (Math.abs(x - f * c[m]) < 1e-9 ? 0 : x - f * c[m]));
|
|
891
|
+
w.own = own;
|
|
892
|
+
}
|
|
783
893
|
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
|
-
|
|
894
|
+
const surface = [...w.keys()].filter((k) => k >= S && k < X && w[k] !== 0).map((k) => ({ col: k - S, w: w[k] }));
|
|
895
|
+
const carriers = this.carriers.flatMap((k, j) => (w[X + j] !== 0 ? [{ k, w: w[X + j] }] : []));
|
|
896
|
+
if (parts.length === 0) continue; // (a surface's own: node by node, below)
|
|
786
897
|
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
|
-
|
|
898
|
+
if (parts.some((p) => !this._moves(this.stretches[p.stretch]) && !this._still(this.stretches[p.stretch]))) continue;
|
|
899
|
+
const v = new Float64Array(S + T);
|
|
900
|
+
for (const { stretch: k, w: wk } of parts) {
|
|
901
|
+
const st = this.stretches[k];
|
|
902
|
+
if (this._moves(st) && !spectators.has(rowOf(k))) v[k] = wk;
|
|
903
|
+
if (this.z[st.species] !== 0) for (const t of driven(st)) v[S + t] = wk / this.z[st.species];
|
|
904
|
+
}
|
|
905
|
+
for (const { k, w: wk } of carriers) v[S + k] = wk / zc(k);
|
|
906
|
+
for (const [col, row] of chosen) {
|
|
907
|
+
const f = v[col];
|
|
908
|
+
if (f !== 0) for (let j = 0; j < S + T; j++) v[j] -= f * row[j];
|
|
909
|
+
}
|
|
910
|
+
const scale = v.reduce((m, x) => Math.max(m, Math.abs(x)), 0);
|
|
911
|
+
if (!(scale > 1e-9)) continue;
|
|
912
|
+
const ok = (j) => j !== undefined && Math.abs(v[j]) > 1e-9 * scale;
|
|
913
|
+
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);
|
|
914
|
+
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)
|
|
915
|
+
chosen.push([col, v.map((x) => x / v[col])]);
|
|
916
|
+
if (col >= S) held.set(col - S, w);
|
|
917
|
+
add(parts, col < S ? col : parts[0].stretch, surface, col < S ? -1 : col - S, carriers);
|
|
918
|
+
}
|
|
919
|
+
// A combination that nothing carries anywhere is conserved node by node: of still stretches
|
|
920
|
+
// (trap states: X⁰ + X⁻ under e⁻ + X⁰ = X⁻), or of an electrode's surface species (adsorbates
|
|
921
|
+
// that only turn into each other). At each node its weighted sum stays what it was, a row local
|
|
922
|
+
// to that node's block, in place of one of their balances.
|
|
794
923
|
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];
|
|
924
|
+
const span = ([g0, g1]) => Array.from({ length: g1 - g0 + 1 }, (_, j) => g0 + j);
|
|
925
|
+
for (const w of laws) {
|
|
926
|
+
const parts = [...w.keys()].filter((k) => w[k] !== 0);
|
|
927
|
+
if (parts.some((k) => k >= X)) continue; // (through an electrode: above)
|
|
928
|
+
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));
|
|
929
|
+
if (sets.some((set) => set === null || set.length !== sets[0].length || set.some((g, j) => g !== sets[0][j]))) continue; // (side by side only)
|
|
930
|
+
for (const k of parts) if (k < S) this.stretches[k].conserved = true;
|
|
931
|
+
const slot = (k) => (k < S ? 1 + this.stretches[k].species : 1 + this.n + this.surfCols[k - S][1]);
|
|
806
932
|
this.localConstraints.push({
|
|
807
|
-
parts: parts.map((
|
|
808
|
-
row:
|
|
809
|
-
nodes:
|
|
810
|
-
reference: new Float64Array(
|
|
933
|
+
parts: parts.map((k) => (k < S ? { i: this.stretches[k].species, w: w[k] } : { q: this.surfCols[k - S][1], w: w[k] })),
|
|
934
|
+
row: slot(parts.includes(w.own) ? w.own : parts[0]),
|
|
935
|
+
nodes: sets[0],
|
|
936
|
+
reference: new Float64Array(sets[0].length),
|
|
811
937
|
});
|
|
812
938
|
}
|
|
813
939
|
// Islands: the pieces of a stretch between its faces. One that nothing else feeds (no
|
|
@@ -821,7 +947,7 @@ export class Solver {
|
|
|
821
947
|
this.pinnedIslands = [];
|
|
822
948
|
this.islandsOn = false; // (newton() turns them on, see there)
|
|
823
949
|
this.pins = [];
|
|
824
|
-
const used = new Set(this.constraints.map((cs) => cs.row));
|
|
950
|
+
const used = new Set(this.constraints.map((cs) => cs.row).filter((o) => o >= 0));
|
|
825
951
|
for (const st of this.stretches) {
|
|
826
952
|
const i = st.species, [s0, s1] = st.regions;
|
|
827
953
|
if (!st.mobile || conductor(st)) continue;
|
|
@@ -870,19 +996,68 @@ export class Solver {
|
|
|
870
996
|
}
|
|
871
997
|
}
|
|
872
998
|
for (const isl of this.islands) isl.outside = isl.outside.filter((o) => !used.has(o));
|
|
873
|
-
|
|
999
|
+
|
|
1000
|
+
// Time steps only: a terminal behind a resistance that alone feeds a combination reads its
|
|
1001
|
+
// current by that combination's change too (see _circuitLaws), its combinations found with it
|
|
1002
|
+
// counted as driven by a current. (Not in the steady system, where it holds its level.)
|
|
1003
|
+
this.timeLaws = [];
|
|
1004
|
+
const behind = this.floating.filter((k) => this.terms[k].drive.kind !== 'I');
|
|
1005
|
+
if (behind.length > 0) {
|
|
1006
|
+
const steadyCarriers = this.carriers, more = this._conservedMoieties(true, behind), X2 = S + this.surfCols.length;
|
|
1007
|
+
for (const w of more) {
|
|
1008
|
+
const parts = [...w.keys()].filter((k) => k < S && w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
1009
|
+
if (parts.length === 0 || parts.some((p) => !this._moves(this.stretches[p.stretch]) && !this._still(this.stretches[p.stretch]))) continue;
|
|
1010
|
+
const surface = [...w.keys()].filter((k) => k >= S && k < X2 && w[k] !== 0).map((k) => ({ col: k - S, w: w[k] }));
|
|
1011
|
+
const carriers = this.carriers.flatMap((k, j) => (w[X2 + j] !== 0 ? [{ k, w: w[X2 + j] }] : []));
|
|
1012
|
+
// Fed by one terminal behind a resistance and nothing else that passes current.
|
|
1013
|
+
const fedBy = new Set([...carriers.map((p) => p.k), ...parts.flatMap((p) => this.floating.filter((k) => this._feeds(k, this.stretches[p.stretch])))]);
|
|
1014
|
+
if (fedBy.size !== 1) continue;
|
|
1015
|
+
const [k] = fedBy;
|
|
1016
|
+
if (!behind.includes(k) || this.timeLaws.some((cs) => cs.circuit === k)) continue;
|
|
1017
|
+
const zc = k >= 2 ? this.z[model.ports[k - 2].terminal] : 0;
|
|
1018
|
+
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);
|
|
1019
|
+
if (!lambda) continue;
|
|
1020
|
+
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
1021
|
+
for (const p of surface) nNodes += model.ports[this.surfCols[p.col][0]].nodes.length * model.ports[this.surfCols[p.col][0]].surface.length;
|
|
1022
|
+
for (const p of carriers) nNodes += model.ports[p.k - 2].nodes.length;
|
|
1023
|
+
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 });
|
|
1024
|
+
}
|
|
1025
|
+
this.carriers = steadyCarriers;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// What each combination fed by a terminal driven at no current holds now, which a steady solve
|
|
1030
|
+
// keeps: a closed battery at open circuit stays at the charge it had.
|
|
1031
|
+
_captureLaws() {
|
|
1032
|
+
// A terminal at no current that passes current only by charging a capacitance keeps its charge.
|
|
1033
|
+
this.chargeRefs.clear();
|
|
1034
|
+
const charged = this.floating.filter((k) => this.terms[k].drive.kind === 'I' && this._chargeOnly(k));
|
|
1035
|
+
if (charged.length > 0) {
|
|
1036
|
+
this._assembleBookkeeping(Infinity);
|
|
1037
|
+
for (const k of charged) this.chargeRefs.set(k, k >= 2 ? this.portQ[k - 2] : k === 0 ? this.contactD.left : -this.contactD.right);
|
|
1038
|
+
}
|
|
1039
|
+
this.lawRefs.clear();
|
|
1040
|
+
if (this.constraints.some((cs) => cs.carriers.some(({ k }) => this.model.ports[k - 2].capacitance)) && charged.length === 0) this._assembleBookkeeping(Infinity);
|
|
1041
|
+
for (const cs of this.constraints) {
|
|
1042
|
+
if (cs.terminals.length === 0) continue;
|
|
1043
|
+
let a = 0;
|
|
1044
|
+
for (const { stretch, w } of cs.parts) a += w * this.amount(this.stretches[stretch]);
|
|
1045
|
+
for (const { col, w } of cs.surface) a += w * this.surfaceAmount(col);
|
|
1046
|
+
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);
|
|
1047
|
+
this.lawRefs.set(cs.key, a);
|
|
1048
|
+
}
|
|
874
1049
|
}
|
|
875
1050
|
|
|
876
1051
|
// Each local constraint's weighted sum at each node, as the state holds it now: what a steady
|
|
877
1052
|
// solve keeps.
|
|
878
1053
|
_captureLocal() {
|
|
879
|
-
const { n, c } = this;
|
|
1054
|
+
const { n, c, th, nSurf } = this;
|
|
880
1055
|
for (const lc of this.localConstraints) {
|
|
881
|
-
|
|
1056
|
+
lc.nodes.forEach((g, j) => {
|
|
882
1057
|
let t = 0;
|
|
883
|
-
for (const { i, w } of lc.parts) t += w * c[g * n + i];
|
|
884
|
-
lc.reference[
|
|
885
|
-
}
|
|
1058
|
+
for (const { i, q, w } of lc.parts) t += w * (i !== undefined ? c[g * n + i] : th[g * nSurf + q]);
|
|
1059
|
+
lc.reference[j] = t;
|
|
1060
|
+
});
|
|
886
1061
|
}
|
|
887
1062
|
}
|
|
888
1063
|
|
|
@@ -892,7 +1067,7 @@ export class Solver {
|
|
|
892
1067
|
_applyIslands() {
|
|
893
1068
|
const { u, res } = this, R = this.rix, out = [];
|
|
894
1069
|
for (const isl of this.islands) {
|
|
895
|
-
if (this.
|
|
1070
|
+
if (this.flatStretches.includes(isl.stretch)) continue;
|
|
896
1071
|
const b0 = Math.floor(isl.row / this.M), r0 = isl.row % this.M;
|
|
897
1072
|
if (this.loc[isl.row] < 0) continue;
|
|
898
1073
|
// Gathered by column (the faces' u, then each outside row's entries, which cancel those u
|
|
@@ -953,28 +1128,47 @@ export class Solver {
|
|
|
953
1128
|
|
|
954
1129
|
// The local constraints' rows (steady solves only): Σ w c_i − reference at each node.
|
|
955
1130
|
_applyLocalConstraints() {
|
|
956
|
-
const { n, M, c, res, dA: d } = this;
|
|
1131
|
+
const { n, M, c, th, nSurf, res, dA: d } = this;
|
|
957
1132
|
for (const lc of this.localConstraints) {
|
|
958
|
-
|
|
1133
|
+
lc.nodes.forEach((g, j) => {
|
|
959
1134
|
const b = this.blockOfNode[g];
|
|
960
|
-
if (this.loc[b * M + lc.row] < 0)
|
|
1135
|
+
if (this.loc[b * M + lc.row] < 0) return;
|
|
961
1136
|
this._replaceRow(b, lc.row);
|
|
962
1137
|
let t = 0;
|
|
963
|
-
for (const { i, w } of lc.parts) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1138
|
+
for (const { i, q, w } of lc.parts) {
|
|
1139
|
+
if (i !== undefined) {
|
|
1140
|
+
t += w * c[g * n + i];
|
|
1141
|
+
this._dc(g, i, d);
|
|
1142
|
+
for (let s = 0; s < M; s++) if (d[s] !== 0) this._j(b, lc.row, b, s, w * d[s]);
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
// A coverage, with ∂θ_q/∂η_x = θ_q (δ_qx − θ_x) over its surface's species.
|
|
1146
|
+
const tq = th[g * nSurf + q], ns = this.model.ports[this.surfPort[g]].surface.length;
|
|
1147
|
+
t += w * tq;
|
|
1148
|
+
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
1149
|
}
|
|
968
|
-
res[this.rix[b * M + lc.row]] = t - lc.reference[
|
|
969
|
-
}
|
|
1150
|
+
res[this.rix[b * M + lc.row]] = t - lc.reference[j];
|
|
1151
|
+
});
|
|
970
1152
|
}
|
|
971
1153
|
}
|
|
972
1154
|
|
|
973
1155
|
// 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
|
-
|
|
1156
|
+
// stoichiometry (a row per reaction, over the stretches it touches) together with what the
|
|
1157
|
+
// terminals feed. A terminal held at a voltage (or behind a resistance, which holds it at
|
|
1158
|
+
// steady state) feeds each stretch it links freely: a unit row. With `drives`, one driven by a
|
|
1159
|
+
// current feeds its charged species only as that current allows, in proportion to their
|
|
1160
|
+
// charges, so a combination weighted by z there is conserved while it passes none: a battery's
|
|
1161
|
+
// cathode, its electrons behind an open circuit, keeps its charge. Exact integer data, so plain
|
|
1162
|
+
// elimination will do.
|
|
1163
|
+
_conservedMoieties(drives = false, alsoCurrent = []) {
|
|
1164
|
+
const { model, n, z } = this, S = this.stretches.length;
|
|
1165
|
+
// With `drives`, an electrode port driven by a current whose reactions take its carrier from
|
|
1166
|
+
// the electrode has a column of its own, last: what the electrode holds, its charge Q/(zF),
|
|
1167
|
+
// which the current feeds and the reactions draw on (a port at no current with M⁺ + e⁻ = M(s)
|
|
1168
|
+
// keeps M⁺ + Q/F, or with no capacitance, Q = 0, the M⁺ alone).
|
|
1169
|
+
const byCurrent = (t, k) => t.drive.kind === 'I' || alsoCurrent.includes(k); // (with `alsoCurrent`, terminals counted as such)
|
|
1170
|
+
this.carriers = drives ? this.terms.flatMap((t, k) => (byCurrent(t, k) && k >= 2 && model.ports[k - 2].reactions.length > 0 ? [k] : [])) : [];
|
|
1171
|
+
const X = S + this.surfCols.length, C = X + this.carriers.length;
|
|
978
1172
|
const rows = [];
|
|
979
1173
|
model.interfaces.forEach((itf, f) => {
|
|
980
1174
|
for (const rx of itf.reactions) {
|
|
@@ -992,6 +1186,7 @@ export class Solver {
|
|
|
992
1186
|
for (const p of rx.part) {
|
|
993
1187
|
if (p.side === 0) row[this.stretchOf[port.region * n + p.i]] += p.nu;
|
|
994
1188
|
else if (p.side === 2) row[S + this.surfColOf[k] + p.s] += p.nu;
|
|
1189
|
+
else if (this.carriers.includes(k + 2)) row[X + this.carriers.indexOf(k + 2)] += p.nu;
|
|
995
1190
|
}
|
|
996
1191
|
rows.push(row);
|
|
997
1192
|
}
|
|
@@ -1005,16 +1200,32 @@ export class Solver {
|
|
|
1005
1200
|
rows.push(row);
|
|
1006
1201
|
}
|
|
1007
1202
|
});
|
|
1008
|
-
|
|
1009
|
-
if (!st.contactFed) return;
|
|
1203
|
+
const unit = (k) => {
|
|
1010
1204
|
const row = new Float64Array(C);
|
|
1011
1205
|
row[k] = 1;
|
|
1012
1206
|
rows.push(row);
|
|
1207
|
+
};
|
|
1208
|
+
this.terms.forEach((t, k) => {
|
|
1209
|
+
const fed = this.stretches.flatMap((st, j) => (this._feeds(k, st) ? [j] : []));
|
|
1210
|
+
if (!(drives && byCurrent(t, k))) return fed.forEach(unit);
|
|
1211
|
+
const charged = fed.filter((j) => z[this.stretches[j].species] !== 0).map((j) => [j, z[this.stretches[j].species]]);
|
|
1212
|
+
if (this.carriers.includes(k)) charged.push([X + this.carriers.indexOf(k), z[model.ports[k - 2].terminal]]);
|
|
1213
|
+
fed.filter((j) => z[this.stretches[j].species] === 0).forEach(unit);
|
|
1214
|
+
for (let a = 1; a < charged.length; a++) {
|
|
1215
|
+
const row = new Float64Array(C);
|
|
1216
|
+
row[charged[0][0]] = 1 / charged[0][1];
|
|
1217
|
+
row[charged[a][0]] = -1 / charged[a][1];
|
|
1218
|
+
rows.push(row);
|
|
1219
|
+
}
|
|
1013
1220
|
});
|
|
1014
|
-
// Reduced row echelon form; the free columns give the null space.
|
|
1221
|
+
// Reduced row echelon form; the free columns give the null space. Immobile stretches' columns
|
|
1222
|
+
// go first, so that a combination of them alone (trap states, X⁰ + X⁻) comes out on its own,
|
|
1223
|
+
// to be kept node by node, apart from any that moves (e⁻ − X⁰, the electrons the traps hold).
|
|
1224
|
+
const order = [...Array(C).keys()].sort((a, b) => (b < S && this._still(this.stretches[b])) - (a < S && this._still(this.stretches[a])));
|
|
1015
1225
|
const pivots = [];
|
|
1016
1226
|
let r = 0;
|
|
1017
|
-
for (
|
|
1227
|
+
for (const col of order) {
|
|
1228
|
+
if (r >= rows.length) break;
|
|
1018
1229
|
let best = r;
|
|
1019
1230
|
for (let i = r + 1; i < rows.length; i++) if (Math.abs(rows[i][col]) > Math.abs(rows[best][col])) best = i;
|
|
1020
1231
|
if (Math.abs(rows[best][col]) < 1e-9) continue;
|
|
@@ -1040,6 +1251,7 @@ export class Solver {
|
|
|
1040
1251
|
const v = -rows[i][free];
|
|
1041
1252
|
w[col] = Math.abs(v) < 1e-9 ? 0 : v;
|
|
1042
1253
|
});
|
|
1254
|
+
w.own = free; // (its weight is 1 there, and no other vector's is: the row it replaces)
|
|
1043
1255
|
basis.push(w);
|
|
1044
1256
|
}
|
|
1045
1257
|
return basis;
|
|
@@ -1056,39 +1268,19 @@ export class Solver {
|
|
|
1056
1268
|
// sum of the others. That row is dense, so it's kept aside: the factorised matrix gets a pin
|
|
1057
1269
|
// (identity row) there instead, and _solveBordered restores the constraint.
|
|
1058
1270
|
_applyConstraints() {
|
|
1059
|
-
const {
|
|
1060
|
-
const vol = this.model.grid.vol;
|
|
1271
|
+
const { M, res } = this;
|
|
1061
1272
|
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
|
-
});
|
|
1273
|
+
const { amount, reference, dV } = this._lawAmount(cs);
|
|
1274
|
+
cs.res = amount - (cs.terminals.length > 0 ? (this.lawRefs.get(cs.key) ?? amount) : reference);
|
|
1275
|
+
if (cs.circuit >= 0) {
|
|
1276
|
+
// In place of the terminal's circuit row (its I = 0).
|
|
1277
|
+
const C = this.termC[cs.circuit];
|
|
1278
|
+
C.fill(0);
|
|
1279
|
+
for (let j = 0; j < cs.len; j++) C[cs.idx[j]] += cs.w[j];
|
|
1280
|
+
this.termRes[cs.circuit] = cs.res;
|
|
1281
|
+
this.termDI[cs.circuit] = dV;
|
|
1282
|
+
continue;
|
|
1089
1283
|
}
|
|
1090
|
-
cs.len = len;
|
|
1091
|
-
cs.res = amount - reference;
|
|
1092
1284
|
const b0 = Math.floor(cs.row / M), r0 = cs.row % M;
|
|
1093
1285
|
this._replaceRow(b0, r0);
|
|
1094
1286
|
this._j(b0, r0, b0, r0, 1);
|
|
@@ -1096,6 +1288,122 @@ export class Solver {
|
|
|
1096
1288
|
}
|
|
1097
1289
|
}
|
|
1098
1290
|
|
|
1291
|
+
// A conserved combination's amount as the state holds it (and its reference, closed), with its
|
|
1292
|
+
// derivatives in cs.idx/w (and dV, by the voltage of a terminal whose electrode's charge it
|
|
1293
|
+
// counts).
|
|
1294
|
+
_lawAmount(cs) {
|
|
1295
|
+
const { n, M, c, dA: d } = this;
|
|
1296
|
+
const vol = this.model.grid.vol;
|
|
1297
|
+
let amount = 0, reference = 0, len = 0, gross = 0;
|
|
1298
|
+
for (const { stretch, w } of cs.parts) {
|
|
1299
|
+
const st = this.stretches[stretch], i = st.species;
|
|
1300
|
+
if (cs.terminals.length === 0) reference += w * this.referenceAmounts[stretch];
|
|
1301
|
+
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
1302
|
+
amount += w * vol[g] * c[g * n + i];
|
|
1303
|
+
gross += Math.abs(w * vol[g] * c[g * n + i]);
|
|
1304
|
+
if (this.nodeConductor[g] >= 0) {
|
|
1305
|
+
// A metal's carriers: the sheet at a charged face, vol·c = ±D_f·A/(zF), held by the
|
|
1306
|
+
// face's displacement.
|
|
1307
|
+
const f = this.sheetFace[g];
|
|
1308
|
+
if (f >= 0 && this.loc[this.blockOfFace[f] * M] >= 0) {
|
|
1309
|
+
cs.idx[len] = this.rix[this.blockOfFace[f] * M];
|
|
1310
|
+
cs.w[len++] = (w * this.sheetSign[g] * this.model.grid.area[g]) / (this.z[i] * FARADAY);
|
|
1311
|
+
}
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
this._dc(g, i, d);
|
|
1315
|
+
const b = this.blockOfNode[g];
|
|
1316
|
+
for (let r = 0; r < M; r++) {
|
|
1317
|
+
if (d[r] === 0 || this.loc[b * M + r] < 0) continue;
|
|
1318
|
+
cs.idx[len] = this.rix[b * M + r];
|
|
1319
|
+
cs.w[len++] = w * vol[g] * d[r];
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
// What the electrodes' surfaces hold of it: Γ Σ v·a·θ, with ∂θ_s/∂η_x = θ_s (δ_sx − θ_x).
|
|
1324
|
+
for (const { col, w } of cs.surface) {
|
|
1325
|
+
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity, ns = port.surface.length;
|
|
1326
|
+
if (cs.terminals.length === 0) reference += w * this.surfaceRef[col];
|
|
1327
|
+
port.nodes.forEach((g, j) => {
|
|
1328
|
+
const f = w * G * vol[g] * this.portArea[k][j], b = this.blockOfNode[g], tq = this.th[g * this.nSurf + q];
|
|
1329
|
+
amount += f * tq;
|
|
1330
|
+
gross += Math.abs(f * tq);
|
|
1331
|
+
for (let x = 0; x < ns; x++) {
|
|
1332
|
+
cs.idx[len] = this.rix[b * M + 1 + n + x];
|
|
1333
|
+
cs.w[len++] = f * tq * ((q === x ? 1 : 0) - this.th[g * this.nSurf + x]);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
// What an electrode holds, its charge Q/(zF) on the port's capacitance (none without one),
|
|
1338
|
+
// Q = Σ vol·a·C (V − zeroCharge − φ) over its nodes (as _portCapacitance has it).
|
|
1339
|
+
let dV = 0;
|
|
1340
|
+
for (const { k, w } of cs.carriers) {
|
|
1341
|
+
const port = this.model.ports[k - 2];
|
|
1342
|
+
if (!port.capacitance) continue;
|
|
1343
|
+
const f = w / (this.z[port.terminal] * FARADAY), { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k];
|
|
1344
|
+
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
1345
|
+
port.nodes.forEach((g, j) => {
|
|
1346
|
+
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
1347
|
+
const b = this.blockOfNode[g], s = vol[g] * this.portArea[k - 2][j] * Cs;
|
|
1348
|
+
amount += f * s * (V - zeroCharge - this.VT * (this.u[b * M] + this.uLo[b * M]));
|
|
1349
|
+
gross += Math.abs(f * s * (V - zeroCharge - this.VT * (this.u[b * M] + this.uLo[b * M])));
|
|
1350
|
+
dV += f * s;
|
|
1351
|
+
cs.idx[len] = this.rix[b * M];
|
|
1352
|
+
cs.w[len++] = -f * s * this.VT;
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
cs.len = len;
|
|
1356
|
+
return { amount, reference, dV, gross };
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// The same amount at the step's start, as the storage terms have it (BDF2's c* included).
|
|
1360
|
+
_lawStart(cs) {
|
|
1361
|
+
const { n, nSurf } = this, vol = this.model.grid.vol;
|
|
1362
|
+
let a = 0;
|
|
1363
|
+
for (const { stretch, w } of cs.parts) {
|
|
1364
|
+
const st = this.stretches[stretch], i = st.species;
|
|
1365
|
+
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) a += w * vol[g] * this.cOld[g * n + i];
|
|
1366
|
+
}
|
|
1367
|
+
for (const { col, w } of cs.surface) {
|
|
1368
|
+
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity;
|
|
1369
|
+
port.nodes.forEach((g, j) => (a += w * G * vol[g] * this.portArea[k][j] * this.thOld[g * nSurf + q]));
|
|
1370
|
+
}
|
|
1371
|
+
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);
|
|
1372
|
+
return a;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Time steps: a terminal driven by a current that alone feeds a conserved combination can read
|
|
1376
|
+
// its current as the combination's change, F (S − S*)/(λ dt), which the step's balance rows sum
|
|
1377
|
+
// to exactly, rather than as the flux through it. Both are exact; they differ in round-off and
|
|
1378
|
+
// in what Newton can see. Through a metal, the flux is G Δη, its two η all but equal: a floating
|
|
1379
|
+
// gate's charging current fell below that at steps of ~0.03 s, and the step came out singular.
|
|
1380
|
+
// Over long steps the change is the better kept (a closed host at open circuit, read by its
|
|
1381
|
+
// flux, drifted from its charge by 3 mV over 5e11 s, and its steps failed by 4e12 s). Over
|
|
1382
|
+
// short ones it's the amount's round-off over dt (a battery's cathode holds 0.04 mol/m² of
|
|
1383
|
+
// electrons: 1e-6 A/m² at steps of 1e-6 s, enough to reject steps at tol 1e-8), where the flux,
|
|
1384
|
+
// its levels carried in two words (u + uLo), is far cleaner than ε G. So a step reads the change
|
|
1385
|
+
// where that's ε·F·S/(λ dt) below 1e-6 of the flux's ε G, as its start has them, keeps to it
|
|
1386
|
+
// throughout, and where a step read by the flux fails, it's tried once more read by the change
|
|
1387
|
+
// (see step).
|
|
1388
|
+
_circuitLaws(dt) {
|
|
1389
|
+
for (const cs of [...this.constraints, ...this.timeLaws]) {
|
|
1390
|
+
const k = cs.circuit;
|
|
1391
|
+
if (k < 0 || !this.floating.includes(k)) continue;
|
|
1392
|
+
const { amount, dV, gross } = this._lawAmount(cs), f = FARADAY / (cs.lambda * dt), C = this.termC[k];
|
|
1393
|
+
if (!this.lawForm.has(cs)) {
|
|
1394
|
+
let G = 0;
|
|
1395
|
+
for (let j = 0; j < C.length; j++) G = Math.max(G, Math.abs(C[j]));
|
|
1396
|
+
this.lawForm.set(cs, Math.abs(f) * gross < 1e-6 * G);
|
|
1397
|
+
}
|
|
1398
|
+
if (!this.lawForm.get(cs)) continue;
|
|
1399
|
+
C.fill(0);
|
|
1400
|
+
for (let j = 0; j < cs.len; j++) C[cs.idx[j]] += f * cs.w[j];
|
|
1401
|
+
const d = this.terms[k].drive, src = sourceAt(d.src, this.sourceTime, this.sourceBefore);
|
|
1402
|
+
this.termRes[k] = f * (amount - this._lawStart(cs)) - (d.kind === 'I' ? src : (src - this.termV[k]) / d.R);
|
|
1403
|
+
this.termDI[k] = f * dV + (d.kind === 'I' ? 0 : 1 / d.R);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1099
1407
|
// Solve the full Newton system, J δ = rhs, with the extra unknowns and rows that don't fit the
|
|
1100
1408
|
// block-tridiagonal matrix T: the floating terminals' voltages (with their circuit rows), and
|
|
1101
1409
|
// the rows pinned in T: the spectators' conserved amounts (see _applyConstraints) and the
|
|
@@ -1114,10 +1422,11 @@ export class Solver {
|
|
|
1114
1422
|
for (let a = 0; a < S; a++) {
|
|
1115
1423
|
if (!cols[a]) cols[a] = new Float64Array(N + 1);
|
|
1116
1424
|
if (a < P) {
|
|
1117
|
-
|
|
1118
|
-
|
|
1425
|
+
// The response to a unit pin, or where the pin moves a flat stretch, to its shift.
|
|
1426
|
+
const col = pins[a].col ?? [pins[a].row];
|
|
1427
|
+
for (const o of col) e[this.rix[o]] = 1;
|
|
1119
1428
|
this._solveLinear(e, cols[a]);
|
|
1120
|
-
e[
|
|
1429
|
+
for (const o of col) e[this.rix[o]] = 0;
|
|
1121
1430
|
} else this._solveLinear(this.termB[fl[a - P]], cols[a]);
|
|
1122
1431
|
}
|
|
1123
1432
|
// Row a of the small system, as a dot product with a compact vector (sparse for pins).
|
|
@@ -1194,9 +1503,13 @@ export class Solver {
|
|
|
1194
1503
|
// change of source is a discontinuity, so time stepping restarts its order.
|
|
1195
1504
|
redrive() {
|
|
1196
1505
|
this.atSteady = false;
|
|
1197
|
-
const before = this.floating;
|
|
1506
|
+
const before = this.floating, driven = this.drivenKey;
|
|
1198
1507
|
this.floating = this.terms.flatMap((t, k) => (t.drive.kind === 'I' || t.drive.R > 0 ? [k] : []));
|
|
1199
1508
|
if (this.floating.length !== before.length) this.deltaV = null;
|
|
1509
|
+
// A terminal newly driven by a current (or no longer) can close a combination behind it.
|
|
1510
|
+
// (and one put behind a resistance, or taken from it, its time steps' reading: see _laws)
|
|
1511
|
+
this.drivenKey = this.terms.map((t) => `${t.drive.kind}${t.drive.R > 0 ? 'R' : ''}`).join();
|
|
1512
|
+
if (this.drivenKey !== driven) this._laws();
|
|
1200
1513
|
this._refreshSources();
|
|
1201
1514
|
this.history = [];
|
|
1202
1515
|
}
|
|
@@ -1355,11 +1668,15 @@ export class Solver {
|
|
|
1355
1668
|
if (!mat.phiFree && !responds && mat.ideal && electrode) {
|
|
1356
1669
|
// Nor here, but an electrode spread through it does: start with the port's first reaction
|
|
1357
1670
|
// that can be balanced at equilibrium with the port's level (the electrode at open circuit).
|
|
1671
|
+
const bare = 1 - electrode.surface.reduce((t, sp) => t + sp.theta0, 0);
|
|
1358
1672
|
for (const rx of electrode.reactions) {
|
|
1359
1673
|
let a = rx.fixedA, s = 0;
|
|
1360
1674
|
for (const p of rx.part) {
|
|
1361
1675
|
if (p.side === 1) a -= p.nu * this.portEta(electrode, p.i);
|
|
1362
|
-
else if (
|
|
1676
|
+
else if (p.side === 2) {
|
|
1677
|
+
const sp = electrode.surface[p.s]; // (at its starting coverage)
|
|
1678
|
+
a -= p.nu * (sp.mu0 / model.RT + Math.log(sp.theta0 / bare));
|
|
1679
|
+
} else if (mode[p.i] === 2) {
|
|
1363
1680
|
a -= p.nu * (Math.log(cFix[p.i] / mat.cRef[p.i]) + mat.mu0[p.i] / model.RT);
|
|
1364
1681
|
s += p.nu * z[p.i];
|
|
1365
1682
|
} else s = NaN;
|
|
@@ -1404,6 +1721,31 @@ export class Solver {
|
|
|
1404
1721
|
}
|
|
1405
1722
|
}
|
|
1406
1723
|
}
|
|
1724
|
+
// An insertion host's φ is a gauge: c0 sets only its neutral combination's level, not how that
|
|
1725
|
+
// splits between its ion and its electrons. Start its ion level with the same ion's beside it
|
|
1726
|
+
// across a face (the equilibrium a link or a transfer reaction heads for), so its electrons,
|
|
1727
|
+
// and a floating contact on them, start where the cell's open-circuit voltage puts them, not
|
|
1728
|
+
// volts away. (A shift of φ̂ and each η by z·Δ: the concentrations don't change.)
|
|
1729
|
+
regions.forEach((reg, r) => {
|
|
1730
|
+
const mat = materials[reg.material];
|
|
1731
|
+
if (!mat.phiFree) return;
|
|
1732
|
+
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]];
|
|
1733
|
+
for (const [nr, gIn, gOut] of sides) {
|
|
1734
|
+
if (gOut < 0) continue;
|
|
1735
|
+
const other = materials[regions[nr].material];
|
|
1736
|
+
if (other.phiFree || other.conductor) continue;
|
|
1737
|
+
const i = z.findIndex((zi, j) => zi !== 0 && mat.present[j] && other.present[j]);
|
|
1738
|
+
if (i < 0) continue;
|
|
1739
|
+
const d = (u[this.blockOfNode[gOut] * M + 1 + i] - u[this.blockOfNode[gIn] * M + 1 + i]) / z[i];
|
|
1740
|
+
if (!Number.isFinite(d)) continue;
|
|
1741
|
+
for (let g = grid.regionStart[r]; g <= grid.regionEnd[r]; g++) {
|
|
1742
|
+
const b = this.blockOfNode[g];
|
|
1743
|
+
u[b * M] += d;
|
|
1744
|
+
for (let j = 0; j < n; j++) if (mat.present[j]) u[b * M + 1 + j] += z[j] * d;
|
|
1745
|
+
}
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1407
1749
|
// A floating contact facing a held one starts where the start's own composition puts it: the
|
|
1408
1750
|
// held voltage plus the difference between the two terminal species' levels beside each
|
|
1409
1751
|
// contact (c0 sets them, and φ was chosen for neutrality, not to match either contact), so
|
|
@@ -1652,6 +1994,7 @@ export class Solver {
|
|
|
1652
1994
|
res.fill(0);
|
|
1653
1995
|
this.dtNow = dt;
|
|
1654
1996
|
if (this.lin) this.lin.reset(1 / dt);
|
|
1997
|
+
if (this.steady && dt === Infinity) this._snapFlat();
|
|
1655
1998
|
this.computeConcentrations();
|
|
1656
1999
|
|
|
1657
2000
|
// Regions, each assembled by the kernel for its kind: a conductor (its carrier only), a
|
|
@@ -1683,13 +2026,15 @@ export class Solver {
|
|
|
1683
2026
|
// Terminals: ports (after every other term at their nodes, so a held level can read its
|
|
1684
2027
|
// flux), then contacts, then the circuit rows of the floating ones.
|
|
1685
2028
|
this._terminals(dt);
|
|
1686
|
-
if (
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
2029
|
+
if (Number.isFinite(dt) && (this.constraints.length > 0 || this.timeLaws.length > 0)) this._circuitLaws(dt);
|
|
2030
|
+
// The steady system's own rows (see _laws): levels held flat, conserved amounts, islands'
|
|
2031
|
+
// balances, the last two kept aside for _solveBordered.
|
|
2032
|
+
const steady = this.steady && dt === Infinity;
|
|
2033
|
+
if (steady) this._flatRows();
|
|
2034
|
+
if (steady && this.constraints.length > 0) this._applyConstraints();
|
|
2035
|
+
if (steady && this.localConstraints.length > 0) this._applyLocalConstraints();
|
|
2036
|
+
this.pinnedIslands = steady && this.islandsOn ? this._applyIslands() : [];
|
|
2037
|
+
this.pins = steady ? [...this.constraints.filter((cs) => cs.circuit < 0), ...this.pinnedIslands] : [];
|
|
1693
2038
|
this.transformed = this.combining && dt !== Infinity;
|
|
1694
2039
|
if (this.transformed) this._chargeRows(dt);
|
|
1695
2040
|
}
|
|
@@ -2347,6 +2692,21 @@ export class Solver {
|
|
|
2347
2692
|
return P * -Math.expm1(-(aHi + aLo));
|
|
2348
2693
|
}
|
|
2349
2694
|
|
|
2695
|
+
// A bulk reaction's forward one-way rate at node g (mol/(m³·s)); its backward one is that
|
|
2696
|
+
// times e^{−a}, so their difference is bulkRate's.
|
|
2697
|
+
bulkForward(rx, g) {
|
|
2698
|
+
const { n, M, u, uLo, c } = this, b = this.blockOfNode[g];
|
|
2699
|
+
const law = rx.srh?.[this.nodeMaterial[g]];
|
|
2700
|
+
if (law) {
|
|
2701
|
+
let a = rx.fixedA;
|
|
2702
|
+
for (let p = 0; p < rx.sp.length; p++) a += rx.nu[p] * (u[b * M + 1 + rx.sp[p]] + uLo[b * M + 1 + rx.sp[p]]);
|
|
2703
|
+
return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], a).forward;
|
|
2704
|
+
}
|
|
2705
|
+
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g];
|
|
2706
|
+
for (let p = 0; p < rx.sp.length; p++) if (rx.nu[p] > 0) P *= rx.nu[p] === 1 ? c[g * n + rx.sp[p]] : powi(c[g * n + rx.sp[p]], rx.nu[p]);
|
|
2707
|
+
return P;
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2350
2710
|
// The larger of a bulk reaction's two one-way rates at node g (mol/(m³·s)): the scale its net
|
|
2351
2711
|
// rate is read against (in equilibrium the net rate is their round-off).
|
|
2352
2712
|
bulkOneWay(rx, g) {
|
|
@@ -2376,7 +2736,7 @@ export class Solver {
|
|
|
2376
2736
|
const dDn = tp * cn + tp * n1 * kN + tn * p1 * (1 - kN);
|
|
2377
2737
|
const dDp = tp * n1 * kP + tn * cp + tn * p1 * (1 - kP);
|
|
2378
2738
|
const dDa = tp * n1 * kA + tn * p1 * (-1 - kA);
|
|
2379
|
-
return { rate, oneWay: (np / den) * Math.max(1, Math.exp(-a)), Cn: rate * (1 - dDn / den), Cp: rate * (1 - dDp / den), Ca: (np / den) * (1 - f) - (rate * dDa) / den };
|
|
2739
|
+
return { rate, forward: np / den, oneWay: (np / den) * Math.max(1, Math.exp(-a)), Cn: rate * (1 - dDn / den), Cp: rate * (1 - dDp / den), Ca: (np / den) * (1 - f) - (rate * dDa) / den };
|
|
2380
2740
|
}
|
|
2381
2741
|
|
|
2382
2742
|
// A face reaction (see normalizeFaceReactions). Its rate r_k is an unknown of the face block,
|
|
@@ -2521,7 +2881,8 @@ export class Solver {
|
|
|
2521
2881
|
const b = this.blockOfNode[g], o = b * M + r, v = vol[g];
|
|
2522
2882
|
const deta = target - (u[o] + uLo[o]); // (μ̄_out − μ̄)/RT
|
|
2523
2883
|
if (link.type === 'equilibrium') {
|
|
2524
|
-
|
|
2884
|
+
// a device end node's level is its contact's business, where the contact links it
|
|
2885
|
+
if ((g === 0 && this.model.contacts.left.species[i].type !== 'blocked') || (g === this.nNodes - 1 && this.model.contacts.right.species[i].type !== 'blocked')) continue;
|
|
2525
2886
|
flux[i] += res[R[o]];
|
|
2526
2887
|
this._captureRow(b, r, zF, C);
|
|
2527
2888
|
this._replaceRow(b, r);
|
|
@@ -2663,6 +3024,9 @@ export class Solver {
|
|
|
2663
3024
|
const { M, u, uLo, res, VT } = this, R = this.rix, vol = this.model.grid.vol, area = this.portArea[k - 2];
|
|
2664
3025
|
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
2665
3026
|
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
3027
|
+
// Its current, the charging (Q − Q at the step's start)/dt; or, steady at no current, the
|
|
3028
|
+
// charge it keeps in place of that (see _captureLaws).
|
|
3029
|
+
const rate = Number.isFinite(dt) ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
2666
3030
|
let Q = 0, dQdV = 0;
|
|
2667
3031
|
port.nodes.forEach((g, w) => {
|
|
2668
3032
|
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
@@ -2675,12 +3039,12 @@ export class Solver {
|
|
|
2675
3039
|
res[R[b * M]] -= q;
|
|
2676
3040
|
this._j(b, 0, b, 0, s * VT);
|
|
2677
3041
|
B[R[b * M]] -= s;
|
|
2678
|
-
if (
|
|
3042
|
+
if (rate > 0) Ct[R[b * M]] += -s * VT * rate;
|
|
2679
3043
|
});
|
|
2680
3044
|
this.portQ[k - 2] = Q;
|
|
2681
|
-
if (
|
|
2682
|
-
this.termI[k] += (Q - this.portQStart[k - 2])
|
|
2683
|
-
this.termDI[k] += dQdV
|
|
3045
|
+
if (rate > 0) {
|
|
3046
|
+
this.termI[k] += (Q - (dt < Infinity ? this.portQStart[k - 2] : this.chargeRefs.get(k))) * rate;
|
|
3047
|
+
this.termDI[k] += dQdV * rate;
|
|
2684
3048
|
}
|
|
2685
3049
|
}
|
|
2686
3050
|
|
|
@@ -2801,10 +3165,13 @@ export class Solver {
|
|
|
2801
3165
|
this._j(b, 0, b, 0, Ac * link.C * VT);
|
|
2802
3166
|
B[R[b * M]] += -Ac * link.C;
|
|
2803
3167
|
this.contactD[side] = sgn * Din;
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
3168
|
+
// Its current, the charging (D_in − D_in at the step's start)/dt; or, steady at no current,
|
|
3169
|
+
// the charge it keeps in place of that (see _captureLaws).
|
|
3170
|
+
const rate = dyn ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
3171
|
+
if (rate > 0) {
|
|
3172
|
+
I += (Din - (dyn ? Dstart : this.chargeRefs.get(k))) * rate;
|
|
3173
|
+
C[R[b * M]] += -Ac * link.C * VT * rate;
|
|
3174
|
+
this.termDI[k] += Ac * link.C * rate;
|
|
2808
3175
|
}
|
|
2809
3176
|
} else if (link.type === 'pinned' || link.type === 'bulk') {
|
|
2810
3177
|
// The Poisson residual is the outside's charge, D_in.
|
|
@@ -2813,6 +3180,9 @@ export class Solver {
|
|
|
2813
3180
|
if (dyn && !(this.combining && this.lateStorage[g] === 1)) {
|
|
2814
3181
|
I += (Din - Dstart) / dt;
|
|
2815
3182
|
this._captureRow(b, 0, 1 / dt, C);
|
|
3183
|
+
} else if (!dyn && this.steady && this.chargeRefs.has(k)) {
|
|
3184
|
+
I += Din - this.chargeRefs.get(k);
|
|
3185
|
+
this._captureRow(b, 0, 1, C);
|
|
2816
3186
|
}
|
|
2817
3187
|
this._replaceRow(b, 0);
|
|
2818
3188
|
if (link.type === 'pinned') {
|
|
@@ -2845,12 +3215,42 @@ export class Solver {
|
|
|
2845
3215
|
|
|
2846
3216
|
// Steady solves: each flat stretch's level held at its contact's throughout (see
|
|
2847
3217
|
// flatStretches), in place of its balances, as an equilibrium link holds it at the end node.
|
|
3218
|
+
// A flat stretch's level: its contact's, through the terminal k that holds it, or (reaching
|
|
3219
|
+
// none) its first node's, which its amount fixes (that row is the amount's, a pin whose
|
|
3220
|
+
// response moves the whole stretch: see _applyConstraints).
|
|
3221
|
+
_flatLevel(st) {
|
|
3222
|
+
const { M, u, uLo, z, VT, model } = this, i = st.species;
|
|
3223
|
+
if (st.leftOpen || st.rightOpen) {
|
|
3224
|
+
const side = st.leftOpen ? 'left' : 'right', link = model.contacts[side].species[i], k = side === 'left' ? 0 : 1;
|
|
3225
|
+
return { k, hi: z[i] === 0 ? link.mu / model.RT : (z[i] * (this.termV[k] + link.offset)) / VT, lo: 0 };
|
|
3226
|
+
}
|
|
3227
|
+
const o = this.blockOfNode[st.nodes[0]] * M + 1 + i;
|
|
3228
|
+
return { k: -1, hi: u[o], lo: uLo[o] };
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
// Each flat stretch's nodes set to its first node's level exactly (where Newton's damped
|
|
3232
|
+
// updates have taken it, on the way to its contact's), so that it carries exactly no flux (the
|
|
3233
|
+
// expm1 form of a zero difference). Flat only to round-off, a level moved by its contact had
|
|
3234
|
+
// read as a current: a MOS capacitor's holes, their contact jumped to −6 V, as
|
|
3235
|
+
// 3e-4 A/m² through a bulk that conducts ~1e10 S/m² (the true current was 3.5e-7).
|
|
3236
|
+
_snapFlat() {
|
|
3237
|
+
const { M, u, uLo } = this;
|
|
3238
|
+
for (const st of this.flatStretches) {
|
|
3239
|
+
const f = this.blockOfNode[st.nodes[0]] * M + 1 + st.species, hi = u[f], lo = uLo[f];
|
|
3240
|
+
for (let g = st.nodes[0] + 1; g <= st.nodes[1]; g++) {
|
|
3241
|
+
const o = this.blockOfNode[g] * M + 1 + st.species;
|
|
3242
|
+
if (this.loc[o] < 0) continue;
|
|
3243
|
+
u[o] = hi;
|
|
3244
|
+
uLo[o] = lo;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
|
|
2848
3249
|
_flatRows() {
|
|
2849
3250
|
const { M, u, uLo, res, z, VT, model } = this, R = this.rix;
|
|
2850
3251
|
for (const st of this.flatStretches) {
|
|
2851
|
-
const i = st.species,
|
|
2852
|
-
const
|
|
2853
|
-
const level = z[i] === 0 ? link.mu / model.RT : (z[i] * (this.termV[k] + link.offset)) / VT;
|
|
3252
|
+
const i = st.species, open = st.leftOpen || st.rightOpen;
|
|
3253
|
+
const { k, hi, lo } = this._flatLevel(st), level = hi + lo;
|
|
2854
3254
|
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
2855
3255
|
const b = this.blockOfNode[g], o = b * M + 1 + i;
|
|
2856
3256
|
if (this.loc[o] < 0) continue;
|
|
@@ -2858,7 +3258,7 @@ export class Solver {
|
|
|
2858
3258
|
for (const B of this.termB) B[R[o]] = 0; // (the row's old terminal terms: the end node's own link)
|
|
2859
3259
|
this._j(b, 1 + i, b, 1 + i, 1);
|
|
2860
3260
|
res[R[o]] = u[o] + uLo[o] - level;
|
|
2861
|
-
if (z[i] !== 0) this.termB[k][R[o]] += -z[i] / VT;
|
|
3261
|
+
if (open && z[i] !== 0) this.termB[k][R[o]] += -z[i] / VT;
|
|
2862
3262
|
}
|
|
2863
3263
|
// A face inside the stretch whose link holds the level continuous carries the species' flux
|
|
2864
3264
|
// as an unknown that only the edge balances, now replaced, determined: it's zero.
|
|
@@ -2875,23 +3275,6 @@ export class Solver {
|
|
|
2875
3275
|
}
|
|
2876
3276
|
|
|
2877
3277
|
// Zero one row of the Jacobian (all three blocks), ready to be replaced.
|
|
2878
|
-
// Steady solves: each level stretch's levels held equal (see levelStretches), node to node in
|
|
2879
|
-
// place of the balances. The first node's row is the amount's (see _applyConstraints).
|
|
2880
|
-
_levelRows() {
|
|
2881
|
-
const { M, u, uLo, res } = this, R = this.rix;
|
|
2882
|
-
for (const st of this.levelStretches) {
|
|
2883
|
-
const i = st.species;
|
|
2884
|
-
for (let g = st.nodes[0] + 1; g <= st.nodes[1]; g++) {
|
|
2885
|
-
const b = this.blockOfNode[g], o = b * M + 1 + i, p = o - M;
|
|
2886
|
-
if (this.loc[o] < 0 || this.loc[p] < 0) continue;
|
|
2887
|
-
this._replaceRow(b, 1 + i);
|
|
2888
|
-
for (const B of this.termB) B[R[o]] = 0;
|
|
2889
|
-
this._j(b, 1 + i, b, 1 + i, 1);
|
|
2890
|
-
this._j(b, 1 + i, b - 1, 1 + i, -1);
|
|
2891
|
-
res[R[o]] = u[o] + uLo[o] - (u[p] + uLo[p]);
|
|
2892
|
-
}
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
3278
|
_replaceRow(b, rs) {
|
|
2896
3279
|
const l = this.loc[b * this.M + rs];
|
|
2897
3280
|
if (l < 0) return;
|
|
@@ -2979,6 +3362,11 @@ export class Solver {
|
|
|
2979
3362
|
rest.multiply(v, jv);
|
|
2980
3363
|
if (save.transformed) this._untransform(v); // (the dilute terms in the plain unknowns)
|
|
2981
3364
|
lin.apply(v, jv, 1, 1 / dt);
|
|
3365
|
+
// A flat stretch's rows hold each level to its first node's (see _flatRows).
|
|
3366
|
+
for (const p of pins) {
|
|
3367
|
+
if (!p.col) continue;
|
|
3368
|
+
for (const o of p.col) if (o !== p.row && this.loc[o] >= 0) jv[R[o]] -= x[R[p.row]];
|
|
3369
|
+
}
|
|
2982
3370
|
const out = new Float64Array(N + K);
|
|
2983
3371
|
for (let j = 0; j < N; j++) {
|
|
2984
3372
|
let t = jv[j];
|
|
@@ -3078,7 +3466,7 @@ export class Solver {
|
|
|
3078
3466
|
this.islandsOn = islands;
|
|
3079
3467
|
let rounds = 0, kept = null;
|
|
3080
3468
|
const islandsOff = (it, result) => {
|
|
3081
|
-
if (dt !== Infinity || !this.
|
|
3469
|
+
if (dt !== Infinity || !this.steady || this.islands.length === 0 || rounds >= 2 || !(this._kirchhoff() > 1e-9)) return false;
|
|
3082
3470
|
this.islandsOn = true;
|
|
3083
3471
|
rounds++;
|
|
3084
3472
|
kept = { it, result, u: Float64Array.from(this.u), uLo: Float64Array.from(this.uLo), termV: Float64Array.from(this.termV) };
|
|
@@ -3152,7 +3540,13 @@ export class Solver {
|
|
|
3152
3540
|
const alpha = own > maxStep ? maxStep / own : 1;
|
|
3153
3541
|
this._addToState(delta, -alpha);
|
|
3154
3542
|
this.floating.forEach((k, a) => (this.termV[k] -= alpha * deltaV[a]));
|
|
3155
|
-
|
|
3543
|
+
// A time step's Newton contracts fast from its predictor (1e-2, 1e-9, 1e-15 thermal
|
|
3544
|
+
// units). The next update would be about θ = step/(the last one) times this one, so where
|
|
3545
|
+
// θ/(1 − θ) of it (Hairer and Wanner's estimate) is under tol, this update converged it:
|
|
3546
|
+
// the iteration that would only show that is skipped, a third of a typical step's work.
|
|
3547
|
+
const last = history[history.length - 2], theta = step / last;
|
|
3548
|
+
const contracted = dt !== Infinity && it >= 2 && step < 1e-6 && theta < 0.1 && (theta / (1 - theta)) * step < tol;
|
|
3549
|
+
if (alpha === 1 && (step < tol || contracted)) {
|
|
3156
3550
|
const done = { converged: true, iterations: it, history, residual: rmax };
|
|
3157
3551
|
if (islandsOff(it, done)) continue;
|
|
3158
3552
|
this.computeConcentrations();
|
|
@@ -3206,18 +3600,20 @@ export class Solver {
|
|
|
3206
3600
|
}
|
|
3207
3601
|
if (!(c.digits > (this.conditioning?.digits ?? -1))) return;
|
|
3208
3602
|
const { grid } = this.model;
|
|
3209
|
-
let x = NaN, where = '';
|
|
3603
|
+
let x = NaN, where = '', nodes = [];
|
|
3210
3604
|
for (let g = 0; g < this.nNodes; g++) {
|
|
3211
3605
|
if (this.blockOfNode[g] !== c.block) continue;
|
|
3212
3606
|
x = grid.x[g];
|
|
3213
3607
|
where = this.model.regions[grid.nodeRegion[g]].name;
|
|
3608
|
+
nodes = [g];
|
|
3214
3609
|
}
|
|
3215
3610
|
for (let f = 0; f < this.nFaces; f++) {
|
|
3216
3611
|
if (this.blockOfFace[f] !== c.block) continue;
|
|
3217
3612
|
x = grid.x[grid.regionEnd[f]];
|
|
3218
3613
|
where = `interfaces[${f}]`;
|
|
3614
|
+
nodes = [grid.regionEnd[f], grid.regionEnd[f] + 1];
|
|
3219
3615
|
}
|
|
3220
|
-
this.conditioning = { digits: c.digits, x, where };
|
|
3616
|
+
this.conditioning = { digits: c.digits, x, where, nodes };
|
|
3221
3617
|
}
|
|
3222
3618
|
|
|
3223
3619
|
/**
|
|
@@ -3307,13 +3703,14 @@ export class Solver {
|
|
|
3307
3703
|
// Implicit: sources at the step's end, as seen from within the step (before any jump there).
|
|
3308
3704
|
// A step landing on a breakpoint ends on it exactly.
|
|
3309
3705
|
const tEnd = this.landing !== undefined && Math.abs(this.time + dt - this.landing) <= 1e-9 * dt ? this.landing : this.time + dt;
|
|
3310
|
-
this.sourceTime = this.steady ? this.
|
|
3706
|
+
this.sourceTime = this.steady ? this.steadyAt : tEnd;
|
|
3311
3707
|
this.sourceBefore = !this.steady;
|
|
3312
3708
|
this.combining = true;
|
|
3709
|
+
this.lawForm = new Map(); // (see _circuitLaws)
|
|
3313
3710
|
let result;
|
|
3314
3711
|
try {
|
|
3315
3712
|
result = this.newton(dtEff, opts);
|
|
3316
|
-
if (!result.converged && dtEff === Infinity && this.
|
|
3713
|
+
if (!result.converged && dtEff === Infinity && this.steady && this.islands.length > 0) {
|
|
3317
3714
|
// Plain solves that don't converge may be lost along an island's level (two regions
|
|
3318
3715
|
// conducting 1e20 times less hold it): once more from the start, with the islands'
|
|
3319
3716
|
// summed rows in throughout (see newton).
|
|
@@ -3327,6 +3724,20 @@ export class Solver {
|
|
|
3327
3724
|
const again = this.newton(dtEff, { ...opts, islands: true });
|
|
3328
3725
|
result = { ...again, iterations: result.iterations + again.iterations };
|
|
3329
3726
|
}
|
|
3727
|
+
if (!result.converged && Number.isFinite(dtEff) && [...this.lawForm.values()].some((v) => !v)) {
|
|
3728
|
+
// A terminal read by its flux whose response is lost to round-off (a floating gate's
|
|
3729
|
+
// charging, below its metal's G Δη): once more, read by the change (see _circuitLaws).
|
|
3730
|
+
this.u.set(this.uPrev);
|
|
3731
|
+
this.uLo.set(this.uPrevLo);
|
|
3732
|
+
this.termV.set(this.termVPrev);
|
|
3733
|
+
if (opts.guess) {
|
|
3734
|
+
this.u.set(opts.guess);
|
|
3735
|
+
this.uLo.fill(0);
|
|
3736
|
+
}
|
|
3737
|
+
for (const cs of this.lawForm.keys()) this.lawForm.set(cs, true);
|
|
3738
|
+
const again = this.newton(dtEff, opts);
|
|
3739
|
+
result = { ...again, iterations: result.iterations + again.iterations };
|
|
3740
|
+
}
|
|
3330
3741
|
} finally {
|
|
3331
3742
|
this.combining = false;
|
|
3332
3743
|
}
|
|
@@ -3339,11 +3750,16 @@ export class Solver {
|
|
|
3339
3750
|
this.contactDOld = this.contactDStart;
|
|
3340
3751
|
this.portQOld = this.portQStart;
|
|
3341
3752
|
this._accumulateBoundaryIntake(dtEff, cN);
|
|
3753
|
+
// (In time, the state is no longer the steady solve's that a continuation would start from.)
|
|
3754
|
+
if (!this.steady) {
|
|
3755
|
+
this.solvedV = undefined;
|
|
3756
|
+
this.stepped = true;
|
|
3757
|
+
}
|
|
3342
3758
|
} else {
|
|
3343
3759
|
this.u.set(this.uPrev);
|
|
3344
3760
|
this.uLo.set(this.uPrevLo);
|
|
3345
3761
|
this.termV.set(this.termVPrev);
|
|
3346
|
-
this.sourceTime = this.time;
|
|
3762
|
+
this.sourceTime = this.steady ? this.steadyAt : this.time;
|
|
3347
3763
|
this.computeConcentrations();
|
|
3348
3764
|
}
|
|
3349
3765
|
this.sourceBefore = false;
|
|
@@ -3358,6 +3774,11 @@ export class Solver {
|
|
|
3358
3774
|
* animation can call it once per frame; the step size carries over between calls.
|
|
3359
3775
|
*/
|
|
3360
3776
|
integrate(tEnd, opts = {}) {
|
|
3777
|
+
// tEnd is an absolute time. Not a number at all is an error; a NaN (a first animation frame's
|
|
3778
|
+
// clock, or a time read from a field that doesn't exist) does nothing, and says so.
|
|
3779
|
+
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)`;
|
|
3780
|
+
if (typeof tEnd !== 'number') throw new SolverError(`advance(tEnd): ${absolute}; got ${tEnd}`);
|
|
3781
|
+
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}` };
|
|
3361
3782
|
const { tol = 1e-3, dtMax = Infinity, budgetMs = Infinity, maxSteps = 100000, method = 'bdf2' } = opts;
|
|
3362
3783
|
const clock = () => (globalThis.performance ? globalThis.performance.now() : Date.now());
|
|
3363
3784
|
const start = clock();
|
|
@@ -3562,9 +3983,18 @@ export class Solver {
|
|
|
3562
3983
|
if (!(r >= 0 && r < regions.length)) fail(`${path}.region: no region ${JSON.stringify(p.region)}`);
|
|
3563
3984
|
if (!inside(r)) fail(`${path}.x (${p.x} m) is outside region ${JSON.stringify(p.region)}`);
|
|
3564
3985
|
} else {
|
|
3565
|
-
|
|
3566
|
-
|
|
3986
|
+
// At a face, the side where the probe has something to read; both, and it must be told.
|
|
3987
|
+
const has = (rr) => {
|
|
3988
|
+
const mat = this.model.materials[regions[rr].material];
|
|
3989
|
+
return i < 0 ? !mat.conductor && !mat.phiFree : mat.present[i];
|
|
3990
|
+
};
|
|
3991
|
+
const sides = regions.flatMap((_, rr) => (inside(rr) ? [rr] : []));
|
|
3992
|
+
if (!(Number.isFinite(p.x) && sides.length)) fail(`${path}.x must be a position in the device, in m (got ${p.x})`);
|
|
3993
|
+
const able = sides.filter(has);
|
|
3994
|
+
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`);
|
|
3995
|
+
r = able[0] ?? sides[0];
|
|
3567
3996
|
}
|
|
3997
|
+
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`);
|
|
3568
3998
|
let g = grid.regionStart[r];
|
|
3569
3999
|
while (g + 1 < grid.regionEnd[r] && x[g + 1] < p.x) g++;
|
|
3570
4000
|
const w = x[g + 1] > x[g] ? (p.x - x[g]) / (x[g + 1] - x[g]) : 0;
|
|
@@ -3718,6 +4148,15 @@ export class Solver {
|
|
|
3718
4148
|
if (kT < 0) throw new SolverError(`impedance: no terminal named '${terminal}' (${this.terms.map((t) => t.name).join(', ')})`);
|
|
3719
4149
|
const dT = this.terms[kT].drive;
|
|
3720
4150
|
if (dT.R > 0) throw new SolverError(`impedance: terminal '${terminal}' has a series resistance; that belongs to the external circuit`);
|
|
4151
|
+
// A small signal returns through the terminals held at a voltage: with every other one driven
|
|
4152
|
+
// by a current (held open), none comes back, and the impedance is infinite (else round-off).
|
|
4153
|
+
const others = this.terms.filter((t, k) => k !== kT);
|
|
4154
|
+
if (others.length && others.every((t) => t.drive.kind === 'I')) {
|
|
4155
|
+
throw new SolverError(
|
|
4156
|
+
`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}': ` +
|
|
4157
|
+
`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`,
|
|
4158
|
+
);
|
|
4159
|
+
}
|
|
3721
4160
|
const N = nB * M, fl = this.floating, K = fl.length;
|
|
3722
4161
|
this.sourceTime = this.time;
|
|
3723
4162
|
this.computeConcentrations();
|
|
@@ -4120,6 +4559,72 @@ export class Solver {
|
|
|
4120
4559
|
return cuts;
|
|
4121
4560
|
}
|
|
4122
4561
|
|
|
4562
|
+
// Each species' flux toward +x across every segment, at the current state (concentrations
|
|
4563
|
+
// computed): totals through the cross-section, mol/s (mol/(m²·s) for a planar device). A face's
|
|
4564
|
+
// segment, between its pair of nodes, carries the face's flux; a conductor's, its carrier's.
|
|
4565
|
+
// These are the fluxes the balances add up (displacement aside), from the same formulas.
|
|
4566
|
+
segmentFluxes() {
|
|
4567
|
+
const { model, n, M, u, uLo, c, z, ex } = this;
|
|
4568
|
+
const { grid, materials, regions, interfaces } = model;
|
|
4569
|
+
const out = Array.from({ length: n }, () => new Float64Array(grid.nNodes - 1));
|
|
4570
|
+
for (let r = 0; r < regions.length; r++) {
|
|
4571
|
+
const reg = regions[r], mat = materials[reg.material], vel = reg.velocity;
|
|
4572
|
+
for (let s = grid.regionStart[r]; s < grid.regionEnd[r]; s++) {
|
|
4573
|
+
const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s];
|
|
4574
|
+
if (mat.conductor) {
|
|
4575
|
+
out[mat.conductor.i][s] = u[bL * M];
|
|
4576
|
+
continue;
|
|
4577
|
+
}
|
|
4578
|
+
for (let i = 0; i < n; i++) {
|
|
4579
|
+
if (!mat.present[i] || mat.D[i] === 0) continue;
|
|
4580
|
+
const pe = (vel * h) / mat.D[i];
|
|
4581
|
+
const d = z[i] * (u[bR * M] - u[bL * M]) + ex[(s + 1) * n + i] - ex[s * n + i] - pe;
|
|
4582
|
+
const deta = u[bR * M + 1 + i] - u[bL * M + 1 + i] + (uLo[bR * M + 1 + i] - uLo[bL * M + 1 + i]) - pe;
|
|
4583
|
+
out[i][s] = -(mat.D[i] / h) * bernoulli(d) * c[s * n + i] * Math.expm1(deta);
|
|
4584
|
+
}
|
|
4585
|
+
if (reg.mixing > 0) {
|
|
4586
|
+
// Eddy mixing (see _segmentMixing): −(D_mix/h) c̄_i (Δη_i − z_i q), c̄ the logarithmic mean.
|
|
4587
|
+
const on = [...Array(n).keys()].filter((i) => mat.present[i] && mat.D[i] > 0);
|
|
4588
|
+
const cb = [], de = [];
|
|
4589
|
+
let S = 0, Q = 0;
|
|
4590
|
+
for (const k of on) {
|
|
4591
|
+
const a = c[s * n + k], b = c[(s + 1) * n + k];
|
|
4592
|
+
cb[k] = a === b ? a : (b - a) / Math.log(b / a);
|
|
4593
|
+
de[k] = u[bR * M + 1 + k] - u[bL * M + 1 + k] + (uLo[bR * M + 1 + k] - uLo[bL * M + 1 + k]);
|
|
4594
|
+
S += z[k] * z[k] * cb[k];
|
|
4595
|
+
Q += z[k] * cb[k] * de[k];
|
|
4596
|
+
}
|
|
4597
|
+
const q = S > 0 ? Q / S : 0;
|
|
4598
|
+
for (const i of on) out[i][s] -= (reg.mixing / h) * cb[i] * (de[i] - z[i] * q);
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
interfaces.forEach((itf, f) => {
|
|
4603
|
+
const gL = grid.regionEnd[f], b = this.blockOfFace[f];
|
|
4604
|
+
for (let i = 0; i < n; i++) if (itf.links[i].type !== 'blocked') out[i][gL] = grid.area[gL] * u[b * M + 1 + i];
|
|
4605
|
+
});
|
|
4606
|
+
return out;
|
|
4607
|
+
}
|
|
4608
|
+
|
|
4609
|
+
// A face's one-way fluxes for each species crossing it by permeability, per area: toward +x
|
|
4610
|
+
// and toward −x, whose difference is its net flux (Ussing's unidirectional fluxes; GHK's two
|
|
4611
|
+
// terms between like solutions). null for a species crossing any other way.
|
|
4612
|
+
faceOneWay(f) {
|
|
4613
|
+
const { model, n, M, u, uLo, z } = this, itf = model.interfaces[f], grid = model.grid;
|
|
4614
|
+
const bL = this.blockOfFace[f] - 1, bR = bL + 2, gl = grid.regionEnd[f], gr = gl + 1;
|
|
4615
|
+
return itf.links.map((link, i) => {
|
|
4616
|
+
if (link.type !== 'permeability') return null;
|
|
4617
|
+
const kl = gl * n + i, kr = gr * n + i;
|
|
4618
|
+
const shift = this.mu0hat[kr] - this.mu0hat[kl] - Math.log(this.cRef[kr] / this.cRef[kl]);
|
|
4619
|
+
const d = z[i] * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M])) + shift;
|
|
4620
|
+
const deta = u[bR * M + 1 + i] - u[bL * M + 1 + i] + (uLo[bR * M + 1 + i] - uLo[bL * M + 1 + i]);
|
|
4621
|
+
const gated = link.gates ?? [];
|
|
4622
|
+
const P = link.P * (gated.length ? this._open(gated, (q) => u[this._gateSlot(f, q)]) : 1);
|
|
4623
|
+
const right = P * bernoulli(d) * this.c[kl];
|
|
4624
|
+
return [right, right * Math.exp(deta)];
|
|
4625
|
+
});
|
|
4626
|
+
}
|
|
4627
|
+
|
|
4123
4628
|
// Complex profiles of δφ (V), δμ̄ (J/mol) and δc (mol/m³) from a small-signal solution.
|
|
4124
4629
|
_smallSignalProfiles(xr, xi) {
|
|
4125
4630
|
const { n, M, z, VT, model } = this;
|
|
@@ -4203,12 +4708,20 @@ export class Solver {
|
|
|
4203
4708
|
if (!Number.isFinite(dt)) return;
|
|
4204
4709
|
const last = this.model.regions.length - 1;
|
|
4205
4710
|
const { n, cOld } = this, vol = this.model.grid.vol;
|
|
4711
|
+
// A stretch alone in what a terminal driven by a current feeds takes in what that current
|
|
4712
|
+
// says, which the step kept exactly (see _circuitLaws); the flux through a metal is round-off.
|
|
4713
|
+
const driven = new Map();
|
|
4714
|
+
for (const cs of this.constraints) {
|
|
4715
|
+
if (cs.circuit < 0 || this.terms[cs.circuit].drive.kind !== 'I' || cs.parts.length !== 1 || cs.surface.length || cs.carriers.length) continue;
|
|
4716
|
+
driven.set(cs.parts[0].stretch, (cs.lambda * sourceAt(this.terms[cs.circuit].drive.src, this.sourceTime, this.sourceBefore)) / (cs.parts[0].w * FARADAY));
|
|
4717
|
+
}
|
|
4206
4718
|
this.stretches.forEach((st, k) => {
|
|
4207
4719
|
if (!st.connected) return;
|
|
4208
4720
|
let q = 0;
|
|
4209
4721
|
if (st.regions[0] === 0) q += this.contactFlux.left[st.species];
|
|
4210
4722
|
if (st.regions[1] === last) q -= this.contactFlux.right[st.species];
|
|
4211
4723
|
for (const p of st.ports) q += this.portFlux[p][st.species];
|
|
4724
|
+
if (driven.has(k)) q = driven.get(k);
|
|
4212
4725
|
let hist = 0;
|
|
4213
4726
|
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) hist += vol[g] * (cOld[g * n + st.species] - cN[g * n + st.species]);
|
|
4214
4727
|
this.boundaryIntake[k] += q * dt + hist;
|
|
@@ -4238,45 +4751,55 @@ export class Solver {
|
|
|
4238
4751
|
}
|
|
4239
4752
|
|
|
4240
4753
|
/**
|
|
4241
|
-
* Steady state
|
|
4242
|
-
*
|
|
4243
|
-
*
|
|
4244
|
-
*
|
|
4245
|
-
*
|
|
4246
|
-
*
|
|
4247
|
-
* If Newton fails, dt ramps up from a small value (pseudo-transient continuation) instead.
|
|
4754
|
+
* Steady state: where a transient from the present state ends, found as the solution of the
|
|
4755
|
+
* steady equations (dt = ∞, no storage term), with no slow modes to wait out, however slow the
|
|
4756
|
+
* physics (e.g. exponentially scarce minority carriers). What the storage term held is held by
|
|
4757
|
+
* the steady system's own rows instead (see _laws): each conserved amount in place of a balance
|
|
4758
|
+
* row that says nothing new, levels that carry nothing held flat, islands' summed balances.
|
|
4759
|
+
* Where Newton from here fails: continuation along the drives, then the pseudo-transient ramp.
|
|
4248
4760
|
* The clock is not advanced, and open-system conservation bookkeeping restarts here.
|
|
4249
4761
|
*/
|
|
4250
4762
|
// Steady state from the present one; `atSteady` says whether the state is one (until a step or
|
|
4251
4763
|
// a change of drive).
|
|
4252
4764
|
solveSteady(opts = {}) {
|
|
4253
|
-
this.flattening = true;
|
|
4254
4765
|
this.unreached = null; // (a driven current no held voltage reaches: see _floatingContinuation)
|
|
4766
|
+
this.charging = null; // (a driven current that only charges: see _solveSteadyAll)
|
|
4767
|
+
this._captureLaws();
|
|
4768
|
+
this.steady = true; // (the steady system's rows; sources at the present time)
|
|
4769
|
+
this.steadyAt = this.time; // (which a pseudo-transient's steps don't move on)
|
|
4255
4770
|
try {
|
|
4256
4771
|
return this._solveSteadyAll(opts);
|
|
4257
4772
|
} finally {
|
|
4258
|
-
this.
|
|
4773
|
+
this.steady = false;
|
|
4259
4774
|
}
|
|
4260
4775
|
}
|
|
4261
4776
|
|
|
4262
4777
|
_solveSteadyAll(opts) {
|
|
4263
|
-
// A terminal
|
|
4264
|
-
//
|
|
4265
|
-
//
|
|
4266
|
-
const { species } = this.model
|
|
4778
|
+
// A terminal driven by a current that can only store what comes in (a battery's cathode
|
|
4779
|
+
// behind it, closed but for that terminal; a gate, or a capacitance with no species through
|
|
4780
|
+
// it) has no steady state but at no current, where it keeps what it holds.
|
|
4781
|
+
const { species, regions } = this.model;
|
|
4267
4782
|
for (const k of this.floating) {
|
|
4268
|
-
const
|
|
4269
|
-
if (
|
|
4270
|
-
const
|
|
4271
|
-
if (
|
|
4272
|
-
|
|
4273
|
-
|
|
4783
|
+
const I = sourceAt(this.terms[k].drive.src, this.time);
|
|
4784
|
+
if (this.terms[k].drive.kind !== 'I' || I === 0) continue;
|
|
4785
|
+
const laws = this.constraints.filter((cs) => cs.terminals.includes(k));
|
|
4786
|
+
if (laws.length === 0 && !this._chargeOnly(k)) continue;
|
|
4787
|
+
const what = laws.flatMap((cs) => cs.parts.map(({ stretch }) => {
|
|
4788
|
+
const st = this.stretches[stretch], [r0, r1] = st.regions;
|
|
4789
|
+
return `${species[st.species].name} in ${regions[r0].name}${r1 > r0 ? `…${regions[r1].name}` : ''}`;
|
|
4790
|
+
}));
|
|
4791
|
+
this.charging = { terminal: this.terms[k].name, current: I, what };
|
|
4792
|
+
return { converged: false, steps: 0, iterations: 0, history: [] };
|
|
4274
4793
|
}
|
|
4275
4794
|
const r = this._steadyFromHere(opts);
|
|
4276
4795
|
this.atSteady = r.converged;
|
|
4277
4796
|
return r;
|
|
4278
4797
|
}
|
|
4279
4798
|
|
|
4799
|
+
// Steady state from the present one, in three stages: Newton from here; failing that, from a
|
|
4800
|
+
// solved state along the drives (the light ramped up, a current's terminal held at the voltages
|
|
4801
|
+
// that pass it, the voltages ramped from where they were last solved); failing that, the
|
|
4802
|
+
// pseudo-transient ramp, through time.
|
|
4280
4803
|
_steadyFromHere(opts) {
|
|
4281
4804
|
this.conditioning = null;
|
|
4282
4805
|
this.computeConcentrations();
|
|
@@ -4286,86 +4809,110 @@ export class Solver {
|
|
|
4286
4809
|
const [dl, dr] = [this.terms[0].drive, this.terms[1].drive];
|
|
4287
4810
|
const held = (d) => d.kind === 'V' && !(d.R > 0);
|
|
4288
4811
|
const target = sourceAt(dr.src, this.time), level = sourceAt(dl.src, this.time);
|
|
4289
|
-
const
|
|
4812
|
+
const allowed = opts.continuation !== false;
|
|
4813
|
+
const canContinue = allowed && held(dl) && held(dr) && target !== level;
|
|
4290
4814
|
const direct = this._directSteady();
|
|
4291
4815
|
const u0 = Float64Array.from(this.u), u0Lo = Float64Array.from(this.uLo), v0 = Float64Array.from(this.termV);
|
|
4292
|
-
|
|
4293
|
-
// pseudo-transient ramp: one direct attempt first.
|
|
4294
|
-
const quick = (canContinue || this.hasGeneration) && direct && opts.continuation !== false;
|
|
4295
|
-
let r = this._solveSteady(quick ? { ...opts, maxSteps: 1 } : opts);
|
|
4296
|
-
if (r.converged) this.solvedV = [level, target];
|
|
4297
|
-
if (r.converged) return r;
|
|
4298
|
-
// Generation (e.g. light) holding the device far from equilibrium: ramp it up from nearly
|
|
4299
|
-
// nothing, each solve warm from the last.
|
|
4300
|
-
let dimmer = false;
|
|
4301
|
-
if (this.hasGeneration && opts.continuation !== false) {
|
|
4816
|
+
const restart = () => {
|
|
4302
4817
|
this.u.set(u0);
|
|
4303
4818
|
this.uLo.set(u0Lo);
|
|
4304
4819
|
this.termV.set(v0);
|
|
4305
4820
|
this.computeConcentrations();
|
|
4821
|
+
};
|
|
4822
|
+
const solved = (q) => {
|
|
4823
|
+
if (q.converged) {
|
|
4824
|
+
this.solvedV = [level, target];
|
|
4825
|
+
this.stepped = false;
|
|
4826
|
+
}
|
|
4827
|
+
return q;
|
|
4828
|
+
};
|
|
4829
|
+
const after = (q, r) => ({ ...q, steps: q.steps + r.steps, iterations: q.iterations + r.iterations });
|
|
4830
|
+
|
|
4831
|
+
// 1. Newton from here. Where a continuation could follow, one direct attempt only, rather
|
|
4832
|
+
// than the pseudo-transient ramp straight away.
|
|
4833
|
+
const quick = (canContinue || this.hasGeneration) && direct && allowed;
|
|
4834
|
+
let r = this._steadySteps(quick ? { ...opts, maxSteps: 1 } : opts);
|
|
4835
|
+
if (r.converged) return solved(r);
|
|
4836
|
+
// A transient's state can be a worse start than a cold one: 1 ns after a lit MOS capacitor's
|
|
4837
|
+
// gate jumps, its surface is in deep depletion, minority carriers ten orders below where they
|
|
4838
|
+
// end, and Newton fails from there where it converges from the layout for the drives (and
|
|
4839
|
+
// the stages below took 9000 iterations from there, 2000 from the layout). What a steady
|
|
4840
|
+
// solve keeps it took from the state already, so from here on it goes as a cold device would.
|
|
4841
|
+
if (this.stepped && direct && allowed) {
|
|
4842
|
+
this._refreshSources();
|
|
4843
|
+
this.initFromComposition();
|
|
4844
|
+
u0.set(this.u);
|
|
4845
|
+
u0Lo.set(this.uLo);
|
|
4846
|
+
const q = this._steadySteps(quick ? { ...opts, maxSteps: 1 } : opts);
|
|
4847
|
+
if (q.converged) return solved(after(q, r));
|
|
4848
|
+
restart();
|
|
4849
|
+
r = after(q, r);
|
|
4850
|
+
}
|
|
4851
|
+
|
|
4852
|
+
// 2. Continuation. Generation (e.g. light) holding the device far from equilibrium: ramp it up
|
|
4853
|
+
// from nearly nothing, each solve warm from the last.
|
|
4854
|
+
let dimmer = false;
|
|
4855
|
+
if (this.hasGeneration && allowed) {
|
|
4856
|
+
restart();
|
|
4306
4857
|
this.dimmer = false;
|
|
4307
4858
|
const g = this._generationContinuation(opts, r);
|
|
4308
4859
|
dimmer = this.dimmer;
|
|
4309
|
-
if (g.converged)
|
|
4310
|
-
this.solvedV = [level, target];
|
|
4311
|
-
return g;
|
|
4312
|
-
}
|
|
4860
|
+
if (g.converged) return solved(g);
|
|
4313
4861
|
r = g;
|
|
4314
4862
|
}
|
|
4315
4863
|
// A terminal driven by a current (open circuit, say): hold it at a voltage instead, march the
|
|
4316
4864
|
// voltage until the current crosses its target, and float it from there.
|
|
4317
|
-
if (
|
|
4318
|
-
|
|
4319
|
-
this.uLo.set(u0Lo);
|
|
4320
|
-
this.termV.set(v0);
|
|
4321
|
-
this.computeConcentrations();
|
|
4865
|
+
if (allowed && this.floating.some((k) => this.terms[k].drive.kind === 'I' && !this._bound(k))) {
|
|
4866
|
+
restart();
|
|
4322
4867
|
const q = this._floatingContinuation(opts, r);
|
|
4323
4868
|
if (q.converged) return q;
|
|
4324
4869
|
r = q;
|
|
4325
4870
|
}
|
|
4326
|
-
if (
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
}
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4871
|
+
if (canContinue) {
|
|
4872
|
+
// The right terminal's voltage ramped to its target: from the contacts' voltages at the last
|
|
4873
|
+
// converged solve when the state is that solution (a warm start), the left one first where
|
|
4874
|
+
// it moved, the right held where it was; otherwise from level terminals, consistent with a
|
|
4875
|
+
// cold start.
|
|
4876
|
+
restart();
|
|
4877
|
+
const was = this.solvedV;
|
|
4878
|
+
let c;
|
|
4879
|
+
if (was) {
|
|
4880
|
+
c = was[0] === level ? { ...r, converged: true, ramped: false } : this._continuation(opts, 0, was[0], level, r, true, was[1]);
|
|
4881
|
+
// (Ramping the right from level terminals, solve there first: a sweep's start.)
|
|
4882
|
+
if (c.converged) c = this._continuation(opts, 1, was[1], target, c, c.ramped || was[1] !== level);
|
|
4883
|
+
} else c = this._continuation(opts, 1, level, target, r, false);
|
|
4884
|
+
if (c.converged) return solved(c);
|
|
4885
|
+
// 3. The pseudo-transient ramp, from the start.
|
|
4886
|
+
restart();
|
|
4887
|
+
return solved(after(this._steadySteps(opts), c));
|
|
4888
|
+
}
|
|
4889
|
+
// 3. The pseudo-transient ramp (where the first attempt wasn't it already): from the dimmer
|
|
4890
|
+
// light's solution where the light's continuation got anywhere (a lit floating base charges
|
|
4891
|
+
// from there as it would in time), else from the start.
|
|
4892
|
+
if (!quick) return r;
|
|
4893
|
+
if (!dimmer) restart();
|
|
4894
|
+
else this.computeConcentrations();
|
|
4895
|
+
return after(this._steadySteps(opts), r);
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4898
|
+
// Whether terminal k, driven by a current, feeds a combination that it alone can change, or
|
|
4899
|
+
// only charges a capacitance (so that its steady states are one per amount or charge, not one
|
|
4900
|
+
// per voltage).
|
|
4901
|
+
_bound(k) {
|
|
4902
|
+
return this.constraints.some((cs) => cs.terminals.includes(k)) || this._chargeOnly(k);
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4905
|
+
// Whether terminal k passes current only by charging a capacitance: no charged species through
|
|
4906
|
+
// it (nor, a port, reactions), and a capacitance to charge (a gate's φ law, a port's own).
|
|
4907
|
+
_chargeOnly(k) {
|
|
4908
|
+
const t = this.terms[k], { species } = this.model;
|
|
4909
|
+
const ions = (links) => links.some((l, i) => l.type !== 'blocked' && species[i].z !== 0);
|
|
4910
|
+
if (t.kind === 'port') {
|
|
4911
|
+
const p = this.model.ports[t.index];
|
|
4912
|
+
return p.reactions.length === 0 && !ions(p.species) && !!p.capacitance;
|
|
4913
|
+
}
|
|
4914
|
+
const c = this.model.contacts[t.side];
|
|
4915
|
+
return !ions(c.species) && (c.phi.type === 'capacitive' || c.phi.type === 'pinned');
|
|
4369
4916
|
}
|
|
4370
4917
|
|
|
4371
4918
|
// A cold steady solve with a terminal driven by a current, which can fail where the same state
|
|
@@ -4374,7 +4921,7 @@ export class Solver {
|
|
|
4374
4921
|
// the start's until the current crosses its target, the crossing narrowed by bisection, and
|
|
4375
4922
|
// the terminal floated again from beside it.
|
|
4376
4923
|
_floatingContinuation(opts, r) {
|
|
4377
|
-
const k = this.floating.find((j) => this.terms[j].drive.kind === 'I');
|
|
4924
|
+
const k = this.floating.find((j) => this.terms[j].drive.kind === 'I' && !this._bound(j));
|
|
4378
4925
|
const drive = this.terms[k].drive, target = sourceAt(drive.src, this.time);
|
|
4379
4926
|
let steps = r.steps ?? 0, iterations = r.iterations ?? 0;
|
|
4380
4927
|
const history = (r.history ?? []).slice();
|
|
@@ -4437,7 +4984,7 @@ export class Solver {
|
|
|
4437
4984
|
this.terms[k].drive = drive;
|
|
4438
4985
|
this.redrive();
|
|
4439
4986
|
}
|
|
4440
|
-
const q = this.
|
|
4987
|
+
const q = this._steadySteps(opts);
|
|
4441
4988
|
steps += q.steps ?? 0;
|
|
4442
4989
|
iterations += q.iterations ?? 0;
|
|
4443
4990
|
history.push(...(q.history ?? []));
|
|
@@ -4480,7 +5027,7 @@ export class Solver {
|
|
|
4480
5027
|
try {
|
|
4481
5028
|
this.generationScale = s;
|
|
4482
5029
|
// (nearly dark: a cold start can need the full ramp, as a dark device's does)
|
|
4483
|
-
let q = this.
|
|
5030
|
+
let q = this._steadySteps(sub);
|
|
4484
5031
|
steps += q.steps;
|
|
4485
5032
|
iterations += q.iterations;
|
|
4486
5033
|
if (!q.converged) return { converged: false, steps, iterations, history };
|
|
@@ -4489,7 +5036,7 @@ export class Solver {
|
|
|
4489
5036
|
const next = Math.min(1, s * factor);
|
|
4490
5037
|
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
4491
5038
|
this.generationScale = next;
|
|
4492
|
-
q = this.
|
|
5039
|
+
q = this._steadySteps(sub);
|
|
4493
5040
|
steps += q.steps;
|
|
4494
5041
|
iterations += q.iterations;
|
|
4495
5042
|
history.push({ generation: next, converged: q.converged });
|
|
@@ -4525,7 +5072,7 @@ export class Solver {
|
|
|
4525
5072
|
if (hold !== undefined) this.sourceOverride.set(1 - k, hold);
|
|
4526
5073
|
let q;
|
|
4527
5074
|
if (!warm) {
|
|
4528
|
-
q = this.
|
|
5075
|
+
q = this._steadySteps(opts);
|
|
4529
5076
|
if (!q.converged) {
|
|
4530
5077
|
// A cold device's start was laid out for the target (a set() before the first solve
|
|
4531
5078
|
// builds it there): lay it out again, level.
|
|
@@ -4533,7 +5080,7 @@ export class Solver {
|
|
|
4533
5080
|
iterations += q.iterations;
|
|
4534
5081
|
this._refreshSources();
|
|
4535
5082
|
this.initFromComposition();
|
|
4536
|
-
q = this.
|
|
5083
|
+
q = this._steadySteps(opts);
|
|
4537
5084
|
}
|
|
4538
5085
|
// (lit, the level start may need the light ramped up too)
|
|
4539
5086
|
if (!q.converged && this.hasGeneration) q = this._generationContinuation(opts, q);
|
|
@@ -4545,7 +5092,7 @@ export class Solver {
|
|
|
4545
5092
|
const next = Math.abs(target - V) <= Math.abs(dV) ? target : V + dV;
|
|
4546
5093
|
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
4547
5094
|
this.sourceOverride.set(k, next);
|
|
4548
|
-
q = this.
|
|
5095
|
+
q = this._steadySteps(sub);
|
|
4549
5096
|
steps += q.steps;
|
|
4550
5097
|
iterations += q.iterations;
|
|
4551
5098
|
history.push({ continuation: next, converged: q.converged });
|
|
@@ -4569,23 +5116,11 @@ export class Solver {
|
|
|
4569
5116
|
}
|
|
4570
5117
|
}
|
|
4571
5118
|
|
|
4572
|
-
_solveSteady(opts = {}) {
|
|
4573
|
-
this.constrained = true; // spectators' amounts as constraints in the dt = ∞ solves
|
|
4574
|
-
this.steady = true;
|
|
4575
|
-
try {
|
|
4576
|
-
return this._steadySteps(opts);
|
|
4577
|
-
} finally {
|
|
4578
|
-
this.constrained = false;
|
|
4579
|
-
this.steady = false;
|
|
4580
|
-
}
|
|
4581
|
-
}
|
|
4582
|
-
|
|
4583
5119
|
_steadySteps({ maxSteps = 80, tol = 1e-11 } = {}) {
|
|
4584
5120
|
const time = this.time;
|
|
4585
5121
|
const tau = this.slowestTime();
|
|
4586
5122
|
const direct = this._directSteady();
|
|
4587
5123
|
const giant = direct ? Infinity : 1e6 * tau;
|
|
4588
|
-
if (direct) this._renormalizeSpectators(); // a starting point with the right amounts
|
|
4589
5124
|
let dt = giant, grow = 10; // (after a failure, dt grows by less, then back up to ×10)
|
|
4590
5125
|
let triedDirect = 0;
|
|
4591
5126
|
let totalIter = 0, steps = 0, converged = false;
|