driftlet 0.14.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 +87 -28
- package/docs/kit.md +47 -16
- package/docs/numerics.md +193 -114
- package/docs/reliability.md +15 -9
- package/docs/statistics.md +28 -4
- package/llms.txt +68 -38
- package/package.json +1 -1
- package/src/blockTridiagonal.js +12 -6
- package/src/checks.js +11 -4
- package/src/describe.js +46 -5
- package/src/device.js +33 -10
- package/src/index.js +19 -7
- package/src/particles.js +131 -55
- package/src/solution.js +145 -8
- package/src/solver.js +708 -260
- package/src/sources.js +10 -2
- package/src/stack.js +16 -6
- package/src/statistics.js +7 -3
- package/types/particles.d.ts +3 -6
- package/types/solver.d.ts +43 -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
|
}
|
|
@@ -2536,7 +2881,8 @@ export class Solver {
|
|
|
2536
2881
|
const b = this.blockOfNode[g], o = b * M + r, v = vol[g];
|
|
2537
2882
|
const deta = target - (u[o] + uLo[o]); // (μ̄_out − μ̄)/RT
|
|
2538
2883
|
if (link.type === 'equilibrium') {
|
|
2539
|
-
|
|
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;
|
|
2540
2886
|
flux[i] += res[R[o]];
|
|
2541
2887
|
this._captureRow(b, r, zF, C);
|
|
2542
2888
|
this._replaceRow(b, r);
|
|
@@ -2678,6 +3024,9 @@ export class Solver {
|
|
|
2678
3024
|
const { M, u, uLo, res, VT } = this, R = this.rix, vol = this.model.grid.vol, area = this.portArea[k - 2];
|
|
2679
3025
|
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
2680
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;
|
|
2681
3030
|
let Q = 0, dQdV = 0;
|
|
2682
3031
|
port.nodes.forEach((g, w) => {
|
|
2683
3032
|
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
@@ -2690,12 +3039,12 @@ export class Solver {
|
|
|
2690
3039
|
res[R[b * M]] -= q;
|
|
2691
3040
|
this._j(b, 0, b, 0, s * VT);
|
|
2692
3041
|
B[R[b * M]] -= s;
|
|
2693
|
-
if (
|
|
3042
|
+
if (rate > 0) Ct[R[b * M]] += -s * VT * rate;
|
|
2694
3043
|
});
|
|
2695
3044
|
this.portQ[k - 2] = Q;
|
|
2696
|
-
if (
|
|
2697
|
-
this.termI[k] += (Q - this.portQStart[k - 2])
|
|
2698
|
-
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;
|
|
2699
3048
|
}
|
|
2700
3049
|
}
|
|
2701
3050
|
|
|
@@ -2816,10 +3165,13 @@ export class Solver {
|
|
|
2816
3165
|
this._j(b, 0, b, 0, Ac * link.C * VT);
|
|
2817
3166
|
B[R[b * M]] += -Ac * link.C;
|
|
2818
3167
|
this.contactD[side] = sgn * Din;
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
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;
|
|
2823
3175
|
}
|
|
2824
3176
|
} else if (link.type === 'pinned' || link.type === 'bulk') {
|
|
2825
3177
|
// The Poisson residual is the outside's charge, D_in.
|
|
@@ -2828,6 +3180,9 @@ export class Solver {
|
|
|
2828
3180
|
if (dyn && !(this.combining && this.lateStorage[g] === 1)) {
|
|
2829
3181
|
I += (Din - Dstart) / dt;
|
|
2830
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);
|
|
2831
3186
|
}
|
|
2832
3187
|
this._replaceRow(b, 0);
|
|
2833
3188
|
if (link.type === 'pinned') {
|
|
@@ -2860,12 +3215,42 @@ export class Solver {
|
|
|
2860
3215
|
|
|
2861
3216
|
// Steady solves: each flat stretch's level held at its contact's throughout (see
|
|
2862
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
|
+
|
|
2863
3249
|
_flatRows() {
|
|
2864
3250
|
const { M, u, uLo, res, z, VT, model } = this, R = this.rix;
|
|
2865
3251
|
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;
|
|
3252
|
+
const i = st.species, open = st.leftOpen || st.rightOpen;
|
|
3253
|
+
const { k, hi, lo } = this._flatLevel(st), level = hi + lo;
|
|
2869
3254
|
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
2870
3255
|
const b = this.blockOfNode[g], o = b * M + 1 + i;
|
|
2871
3256
|
if (this.loc[o] < 0) continue;
|
|
@@ -2873,7 +3258,7 @@ export class Solver {
|
|
|
2873
3258
|
for (const B of this.termB) B[R[o]] = 0; // (the row's old terminal terms: the end node's own link)
|
|
2874
3259
|
this._j(b, 1 + i, b, 1 + i, 1);
|
|
2875
3260
|
res[R[o]] = u[o] + uLo[o] - level;
|
|
2876
|
-
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;
|
|
2877
3262
|
}
|
|
2878
3263
|
// A face inside the stretch whose link holds the level continuous carries the species' flux
|
|
2879
3264
|
// as an unknown that only the edge balances, now replaced, determined: it's zero.
|
|
@@ -2890,23 +3275,6 @@ export class Solver {
|
|
|
2890
3275
|
}
|
|
2891
3276
|
|
|
2892
3277
|
// Zero one row of the Jacobian (all three blocks), ready to be replaced.
|
|
2893
|
-
// Steady solves: each level stretch's levels held equal (see levelStretches), node to node in
|
|
2894
|
-
// place of the balances. The first node's row is the amount's (see _applyConstraints).
|
|
2895
|
-
_levelRows() {
|
|
2896
|
-
const { M, u, uLo, res } = this, R = this.rix;
|
|
2897
|
-
for (const st of this.levelStretches) {
|
|
2898
|
-
const i = st.species;
|
|
2899
|
-
for (let g = st.nodes[0] + 1; g <= st.nodes[1]; g++) {
|
|
2900
|
-
const b = this.blockOfNode[g], o = b * M + 1 + i, p = o - M;
|
|
2901
|
-
if (this.loc[o] < 0 || this.loc[p] < 0) continue;
|
|
2902
|
-
this._replaceRow(b, 1 + i);
|
|
2903
|
-
for (const B of this.termB) B[R[o]] = 0;
|
|
2904
|
-
this._j(b, 1 + i, b, 1 + i, 1);
|
|
2905
|
-
this._j(b, 1 + i, b - 1, 1 + i, -1);
|
|
2906
|
-
res[R[o]] = u[o] + uLo[o] - (u[p] + uLo[p]);
|
|
2907
|
-
}
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
3278
|
_replaceRow(b, rs) {
|
|
2911
3279
|
const l = this.loc[b * this.M + rs];
|
|
2912
3280
|
if (l < 0) return;
|
|
@@ -2994,6 +3362,11 @@ export class Solver {
|
|
|
2994
3362
|
rest.multiply(v, jv);
|
|
2995
3363
|
if (save.transformed) this._untransform(v); // (the dilute terms in the plain unknowns)
|
|
2996
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
|
+
}
|
|
2997
3370
|
const out = new Float64Array(N + K);
|
|
2998
3371
|
for (let j = 0; j < N; j++) {
|
|
2999
3372
|
let t = jv[j];
|
|
@@ -3093,7 +3466,7 @@ export class Solver {
|
|
|
3093
3466
|
this.islandsOn = islands;
|
|
3094
3467
|
let rounds = 0, kept = null;
|
|
3095
3468
|
const islandsOff = (it, result) => {
|
|
3096
|
-
if (dt !== Infinity || !this.
|
|
3469
|
+
if (dt !== Infinity || !this.steady || this.islands.length === 0 || rounds >= 2 || !(this._kirchhoff() > 1e-9)) return false;
|
|
3097
3470
|
this.islandsOn = true;
|
|
3098
3471
|
rounds++;
|
|
3099
3472
|
kept = { it, result, u: Float64Array.from(this.u), uLo: Float64Array.from(this.uLo), termV: Float64Array.from(this.termV) };
|
|
@@ -3227,18 +3600,20 @@ export class Solver {
|
|
|
3227
3600
|
}
|
|
3228
3601
|
if (!(c.digits > (this.conditioning?.digits ?? -1))) return;
|
|
3229
3602
|
const { grid } = this.model;
|
|
3230
|
-
let x = NaN, where = '';
|
|
3603
|
+
let x = NaN, where = '', nodes = [];
|
|
3231
3604
|
for (let g = 0; g < this.nNodes; g++) {
|
|
3232
3605
|
if (this.blockOfNode[g] !== c.block) continue;
|
|
3233
3606
|
x = grid.x[g];
|
|
3234
3607
|
where = this.model.regions[grid.nodeRegion[g]].name;
|
|
3608
|
+
nodes = [g];
|
|
3235
3609
|
}
|
|
3236
3610
|
for (let f = 0; f < this.nFaces; f++) {
|
|
3237
3611
|
if (this.blockOfFace[f] !== c.block) continue;
|
|
3238
3612
|
x = grid.x[grid.regionEnd[f]];
|
|
3239
3613
|
where = `interfaces[${f}]`;
|
|
3614
|
+
nodes = [grid.regionEnd[f], grid.regionEnd[f] + 1];
|
|
3240
3615
|
}
|
|
3241
|
-
this.conditioning = { digits: c.digits, x, where };
|
|
3616
|
+
this.conditioning = { digits: c.digits, x, where, nodes };
|
|
3242
3617
|
}
|
|
3243
3618
|
|
|
3244
3619
|
/**
|
|
@@ -3328,13 +3703,14 @@ export class Solver {
|
|
|
3328
3703
|
// Implicit: sources at the step's end, as seen from within the step (before any jump there).
|
|
3329
3704
|
// A step landing on a breakpoint ends on it exactly.
|
|
3330
3705
|
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.
|
|
3706
|
+
this.sourceTime = this.steady ? this.steadyAt : tEnd;
|
|
3332
3707
|
this.sourceBefore = !this.steady;
|
|
3333
3708
|
this.combining = true;
|
|
3709
|
+
this.lawForm = new Map(); // (see _circuitLaws)
|
|
3334
3710
|
let result;
|
|
3335
3711
|
try {
|
|
3336
3712
|
result = this.newton(dtEff, opts);
|
|
3337
|
-
if (!result.converged && dtEff === Infinity && this.
|
|
3713
|
+
if (!result.converged && dtEff === Infinity && this.steady && this.islands.length > 0) {
|
|
3338
3714
|
// Plain solves that don't converge may be lost along an island's level (two regions
|
|
3339
3715
|
// conducting 1e20 times less hold it): once more from the start, with the islands'
|
|
3340
3716
|
// summed rows in throughout (see newton).
|
|
@@ -3348,6 +3724,20 @@ export class Solver {
|
|
|
3348
3724
|
const again = this.newton(dtEff, { ...opts, islands: true });
|
|
3349
3725
|
result = { ...again, iterations: result.iterations + again.iterations };
|
|
3350
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
|
+
}
|
|
3351
3741
|
} finally {
|
|
3352
3742
|
this.combining = false;
|
|
3353
3743
|
}
|
|
@@ -3360,11 +3750,16 @@ export class Solver {
|
|
|
3360
3750
|
this.contactDOld = this.contactDStart;
|
|
3361
3751
|
this.portQOld = this.portQStart;
|
|
3362
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
|
+
}
|
|
3363
3758
|
} else {
|
|
3364
3759
|
this.u.set(this.uPrev);
|
|
3365
3760
|
this.uLo.set(this.uPrevLo);
|
|
3366
3761
|
this.termV.set(this.termVPrev);
|
|
3367
|
-
this.sourceTime = this.time;
|
|
3762
|
+
this.sourceTime = this.steady ? this.steadyAt : this.time;
|
|
3368
3763
|
this.computeConcentrations();
|
|
3369
3764
|
}
|
|
3370
3765
|
this.sourceBefore = false;
|
|
@@ -3379,6 +3774,11 @@ export class Solver {
|
|
|
3379
3774
|
* animation can call it once per frame; the step size carries over between calls.
|
|
3380
3775
|
*/
|
|
3381
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}` };
|
|
3382
3782
|
const { tol = 1e-3, dtMax = Infinity, budgetMs = Infinity, maxSteps = 100000, method = 'bdf2' } = opts;
|
|
3383
3783
|
const clock = () => (globalThis.performance ? globalThis.performance.now() : Date.now());
|
|
3384
3784
|
const start = clock();
|
|
@@ -3583,9 +3983,18 @@ export class Solver {
|
|
|
3583
3983
|
if (!(r >= 0 && r < regions.length)) fail(`${path}.region: no region ${JSON.stringify(p.region)}`);
|
|
3584
3984
|
if (!inside(r)) fail(`${path}.x (${p.x} m) is outside region ${JSON.stringify(p.region)}`);
|
|
3585
3985
|
} else {
|
|
3586
|
-
|
|
3587
|
-
|
|
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];
|
|
3588
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`);
|
|
3589
3998
|
let g = grid.regionStart[r];
|
|
3590
3999
|
while (g + 1 < grid.regionEnd[r] && x[g + 1] < p.x) g++;
|
|
3591
4000
|
const w = x[g + 1] > x[g] ? (p.x - x[g]) / (x[g + 1] - x[g]) : 0;
|
|
@@ -3739,6 +4148,15 @@ export class Solver {
|
|
|
3739
4148
|
if (kT < 0) throw new SolverError(`impedance: no terminal named '${terminal}' (${this.terms.map((t) => t.name).join(', ')})`);
|
|
3740
4149
|
const dT = this.terms[kT].drive;
|
|
3741
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
|
+
}
|
|
3742
4160
|
const N = nB * M, fl = this.floating, K = fl.length;
|
|
3743
4161
|
this.sourceTime = this.time;
|
|
3744
4162
|
this.computeConcentrations();
|
|
@@ -4290,12 +4708,20 @@ export class Solver {
|
|
|
4290
4708
|
if (!Number.isFinite(dt)) return;
|
|
4291
4709
|
const last = this.model.regions.length - 1;
|
|
4292
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
|
+
}
|
|
4293
4718
|
this.stretches.forEach((st, k) => {
|
|
4294
4719
|
if (!st.connected) return;
|
|
4295
4720
|
let q = 0;
|
|
4296
4721
|
if (st.regions[0] === 0) q += this.contactFlux.left[st.species];
|
|
4297
4722
|
if (st.regions[1] === last) q -= this.contactFlux.right[st.species];
|
|
4298
4723
|
for (const p of st.ports) q += this.portFlux[p][st.species];
|
|
4724
|
+
if (driven.has(k)) q = driven.get(k);
|
|
4299
4725
|
let hist = 0;
|
|
4300
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]);
|
|
4301
4727
|
this.boundaryIntake[k] += q * dt + hist;
|
|
@@ -4325,45 +4751,55 @@ export class Solver {
|
|
|
4325
4751
|
}
|
|
4326
4752
|
|
|
4327
4753
|
/**
|
|
4328
|
-
* Steady state
|
|
4329
|
-
*
|
|
4330
|
-
*
|
|
4331
|
-
*
|
|
4332
|
-
*
|
|
4333
|
-
*
|
|
4334
|
-
* 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.
|
|
4335
4760
|
* The clock is not advanced, and open-system conservation bookkeeping restarts here.
|
|
4336
4761
|
*/
|
|
4337
4762
|
// Steady state from the present one; `atSteady` says whether the state is one (until a step or
|
|
4338
4763
|
// a change of drive).
|
|
4339
4764
|
solveSteady(opts = {}) {
|
|
4340
|
-
this.flattening = true;
|
|
4341
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)
|
|
4342
4770
|
try {
|
|
4343
4771
|
return this._solveSteadyAll(opts);
|
|
4344
4772
|
} finally {
|
|
4345
|
-
this.
|
|
4773
|
+
this.steady = false;
|
|
4346
4774
|
}
|
|
4347
4775
|
}
|
|
4348
4776
|
|
|
4349
4777
|
_solveSteadyAll(opts) {
|
|
4350
|
-
// A terminal
|
|
4351
|
-
//
|
|
4352
|
-
//
|
|
4353
|
-
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;
|
|
4354
4782
|
for (const k of this.floating) {
|
|
4355
|
-
const
|
|
4356
|
-
if (
|
|
4357
|
-
const
|
|
4358
|
-
if (
|
|
4359
|
-
|
|
4360
|
-
|
|
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: [] };
|
|
4361
4793
|
}
|
|
4362
4794
|
const r = this._steadyFromHere(opts);
|
|
4363
4795
|
this.atSteady = r.converged;
|
|
4364
4796
|
return r;
|
|
4365
4797
|
}
|
|
4366
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.
|
|
4367
4803
|
_steadyFromHere(opts) {
|
|
4368
4804
|
this.conditioning = null;
|
|
4369
4805
|
this.computeConcentrations();
|
|
@@ -4373,86 +4809,110 @@ export class Solver {
|
|
|
4373
4809
|
const [dl, dr] = [this.terms[0].drive, this.terms[1].drive];
|
|
4374
4810
|
const held = (d) => d.kind === 'V' && !(d.R > 0);
|
|
4375
4811
|
const target = sourceAt(dr.src, this.time), level = sourceAt(dl.src, this.time);
|
|
4376
|
-
const
|
|
4812
|
+
const allowed = opts.continuation !== false;
|
|
4813
|
+
const canContinue = allowed && held(dl) && held(dr) && target !== level;
|
|
4377
4814
|
const direct = this._directSteady();
|
|
4378
4815
|
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) {
|
|
4816
|
+
const restart = () => {
|
|
4389
4817
|
this.u.set(u0);
|
|
4390
4818
|
this.uLo.set(u0Lo);
|
|
4391
4819
|
this.termV.set(v0);
|
|
4392
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();
|
|
4393
4857
|
this.dimmer = false;
|
|
4394
4858
|
const g = this._generationContinuation(opts, r);
|
|
4395
4859
|
dimmer = this.dimmer;
|
|
4396
|
-
if (g.converged)
|
|
4397
|
-
this.solvedV = [level, target];
|
|
4398
|
-
return g;
|
|
4399
|
-
}
|
|
4860
|
+
if (g.converged) return solved(g);
|
|
4400
4861
|
r = g;
|
|
4401
4862
|
}
|
|
4402
4863
|
// A terminal driven by a current (open circuit, say): hold it at a voltage instead, march the
|
|
4403
4864
|
// 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();
|
|
4865
|
+
if (allowed && this.floating.some((k) => this.terms[k].drive.kind === 'I' && !this._bound(k))) {
|
|
4866
|
+
restart();
|
|
4409
4867
|
const q = this._floatingContinuation(opts, r);
|
|
4410
4868
|
if (q.converged) return q;
|
|
4411
4869
|
r = q;
|
|
4412
4870
|
}
|
|
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
|
-
|
|
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');
|
|
4456
4916
|
}
|
|
4457
4917
|
|
|
4458
4918
|
// A cold steady solve with a terminal driven by a current, which can fail where the same state
|
|
@@ -4461,7 +4921,7 @@ export class Solver {
|
|
|
4461
4921
|
// the start's until the current crosses its target, the crossing narrowed by bisection, and
|
|
4462
4922
|
// the terminal floated again from beside it.
|
|
4463
4923
|
_floatingContinuation(opts, r) {
|
|
4464
|
-
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));
|
|
4465
4925
|
const drive = this.terms[k].drive, target = sourceAt(drive.src, this.time);
|
|
4466
4926
|
let steps = r.steps ?? 0, iterations = r.iterations ?? 0;
|
|
4467
4927
|
const history = (r.history ?? []).slice();
|
|
@@ -4524,7 +4984,7 @@ export class Solver {
|
|
|
4524
4984
|
this.terms[k].drive = drive;
|
|
4525
4985
|
this.redrive();
|
|
4526
4986
|
}
|
|
4527
|
-
const q = this.
|
|
4987
|
+
const q = this._steadySteps(opts);
|
|
4528
4988
|
steps += q.steps ?? 0;
|
|
4529
4989
|
iterations += q.iterations ?? 0;
|
|
4530
4990
|
history.push(...(q.history ?? []));
|
|
@@ -4567,7 +5027,7 @@ export class Solver {
|
|
|
4567
5027
|
try {
|
|
4568
5028
|
this.generationScale = s;
|
|
4569
5029
|
// (nearly dark: a cold start can need the full ramp, as a dark device's does)
|
|
4570
|
-
let q = this.
|
|
5030
|
+
let q = this._steadySteps(sub);
|
|
4571
5031
|
steps += q.steps;
|
|
4572
5032
|
iterations += q.iterations;
|
|
4573
5033
|
if (!q.converged) return { converged: false, steps, iterations, history };
|
|
@@ -4576,7 +5036,7 @@ export class Solver {
|
|
|
4576
5036
|
const next = Math.min(1, s * factor);
|
|
4577
5037
|
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
4578
5038
|
this.generationScale = next;
|
|
4579
|
-
q = this.
|
|
5039
|
+
q = this._steadySteps(sub);
|
|
4580
5040
|
steps += q.steps;
|
|
4581
5041
|
iterations += q.iterations;
|
|
4582
5042
|
history.push({ generation: next, converged: q.converged });
|
|
@@ -4612,7 +5072,7 @@ export class Solver {
|
|
|
4612
5072
|
if (hold !== undefined) this.sourceOverride.set(1 - k, hold);
|
|
4613
5073
|
let q;
|
|
4614
5074
|
if (!warm) {
|
|
4615
|
-
q = this.
|
|
5075
|
+
q = this._steadySteps(opts);
|
|
4616
5076
|
if (!q.converged) {
|
|
4617
5077
|
// A cold device's start was laid out for the target (a set() before the first solve
|
|
4618
5078
|
// builds it there): lay it out again, level.
|
|
@@ -4620,7 +5080,7 @@ export class Solver {
|
|
|
4620
5080
|
iterations += q.iterations;
|
|
4621
5081
|
this._refreshSources();
|
|
4622
5082
|
this.initFromComposition();
|
|
4623
|
-
q = this.
|
|
5083
|
+
q = this._steadySteps(opts);
|
|
4624
5084
|
}
|
|
4625
5085
|
// (lit, the level start may need the light ramped up too)
|
|
4626
5086
|
if (!q.converged && this.hasGeneration) q = this._generationContinuation(opts, q);
|
|
@@ -4632,7 +5092,7 @@ export class Solver {
|
|
|
4632
5092
|
const next = Math.abs(target - V) <= Math.abs(dV) ? target : V + dV;
|
|
4633
5093
|
const u1 = Float64Array.from(this.u), u1Lo = Float64Array.from(this.uLo), v1 = Float64Array.from(this.termV);
|
|
4634
5094
|
this.sourceOverride.set(k, next);
|
|
4635
|
-
q = this.
|
|
5095
|
+
q = this._steadySteps(sub);
|
|
4636
5096
|
steps += q.steps;
|
|
4637
5097
|
iterations += q.iterations;
|
|
4638
5098
|
history.push({ continuation: next, converged: q.converged });
|
|
@@ -4656,23 +5116,11 @@ export class Solver {
|
|
|
4656
5116
|
}
|
|
4657
5117
|
}
|
|
4658
5118
|
|
|
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
5119
|
_steadySteps({ maxSteps = 80, tol = 1e-11 } = {}) {
|
|
4671
5120
|
const time = this.time;
|
|
4672
5121
|
const tau = this.slowestTime();
|
|
4673
5122
|
const direct = this._directSteady();
|
|
4674
5123
|
const giant = direct ? Infinity : 1e6 * tau;
|
|
4675
|
-
if (direct) this._renormalizeSpectators(); // a starting point with the right amounts
|
|
4676
5124
|
let dt = giant, grow = 10; // (after a failure, dt grows by less, then back up to ×10)
|
|
4677
5125
|
let triedDirect = 0;
|
|
4678
5126
|
let totalIter = 0, steps = 0, converged = false;
|