driftlet 1.1.0 → 1.2.1
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/conventions.md +2 -2
- package/docs/data.md +9 -0
- package/docs/device.md +9 -6
- package/docs/numerics.md +23 -10
- package/docs/reliability.md +23 -3
- package/docs/statistics.md +1 -0
- package/llms.txt +6 -6
- package/package.json +2 -1
- package/src/channels.js +12 -7
- package/src/checks.js +6 -4
- package/src/data.js +5 -3
- package/src/describe.js +1 -1
- package/src/device.js +13 -8
- package/src/devices.js +9 -1
- package/src/difference.js +42 -7
- package/src/equation.js +14 -3
- package/src/index.js +6 -3
- package/src/junction.js +9 -3
- package/src/kit.js +1 -1
- package/src/live.js +14 -9
- package/src/particles.js +4 -1
- package/src/plot.js +8 -4
- package/src/solution.js +1 -0
- package/src/solver.js +329 -110
- package/src/stack.js +5 -1
- package/src/statistics.js +31 -10
- package/src/traces.js +10 -7
- package/src/types.js +96 -29
- package/types/data.d.ts +2 -1
- package/types/difference.d.ts +8 -2
- package/types/index.d.ts +14 -5
- package/types/kit.d.ts +1 -1
- package/types/plot.d.ts +2 -2
- package/types/solution.d.ts +1 -0
- package/types/solver.d.ts +20 -3
- package/types/types.d.ts +179 -26
package/src/solver.js
CHANGED
|
@@ -503,6 +503,9 @@ export class Solver {
|
|
|
503
503
|
this.termB = this.terms.map(() => new Float64Array(this.sys.size + 1));
|
|
504
504
|
this.termC = this.terms.map(() => new Float64Array(this.sys.size + 1));
|
|
505
505
|
this.termDI = new Float64Array(this.terms.length);
|
|
506
|
+
// (and a circuit row's derivatives by other terminals' voltages: a law in one's row that counts
|
|
507
|
+
// another electrode's charge)
|
|
508
|
+
this.termDX = this.terms.map(() => new Float64Array(this.terms.length));
|
|
506
509
|
this.termRes = new Float64Array(this.terms.length);
|
|
507
510
|
this._refreshSources();
|
|
508
511
|
// A floating terminal starts level with the first held one (no current).
|
|
@@ -661,7 +664,7 @@ export class Solver {
|
|
|
661
664
|
}
|
|
662
665
|
this.res = new Float64Array(N + 1);
|
|
663
666
|
this.delta = new Float64Array(N + 1);
|
|
664
|
-
this.
|
|
667
|
+
this.rowDiv = new Float64Array(N + 1).fill(1); // what each row is divided by in the factorised system
|
|
665
668
|
}
|
|
666
669
|
|
|
667
670
|
_factor() {
|
|
@@ -783,7 +786,7 @@ export class Solver {
|
|
|
783
786
|
const ks = rx.part.map((p) => this.stretchOf[(f + p.side) * n + p.i]);
|
|
784
787
|
if (ks.some((k) => k < 0)) continue;
|
|
785
788
|
join(ks);
|
|
786
|
-
rxns.push([ks, (side) => rx.part.reduce((a, p) => a
|
|
789
|
+
rxns.push([ks, (side) => rx.part.reduce((a, p) => a - p.nu * this.contactEta(side, p.i), rx.fixedA)]); // (a face's ν: products +, so − Σ ν η)
|
|
787
790
|
}
|
|
788
791
|
});
|
|
789
792
|
const groups = new Map();
|
|
@@ -801,6 +804,19 @@ export class Solver {
|
|
|
801
804
|
this._laws();
|
|
802
805
|
}
|
|
803
806
|
|
|
807
|
+
// A stretch's amount from its regions' starting concentrations (c0), per unit area, before
|
|
808
|
+
// any state exists: Infinity where one isn't given (set by a level, or doping), to order it last.
|
|
809
|
+
_roughAmount(st) {
|
|
810
|
+
const { regions, grid } = this.model;
|
|
811
|
+
let t = 0;
|
|
812
|
+
for (let q = st.regions[0]; q <= st.regions[1]; q++) {
|
|
813
|
+
const reg = regions[q], c0 = reg.c0Profile[st.species] ? profileAt(reg.c0Profile[st.species], grid.x[grid.regionStart[q]]) : reg.c0[st.species];
|
|
814
|
+
if (!(c0 >= 0)) return Infinity;
|
|
815
|
+
t += c0 * (grid.x[grid.regionEnd[q]] - grid.x[grid.regionStart[q]]);
|
|
816
|
+
}
|
|
817
|
+
return t;
|
|
818
|
+
}
|
|
819
|
+
|
|
804
820
|
// Whether a stretch's species moves through it (mobile in every region, or a metal's carrier),
|
|
805
821
|
// or stays still in every region (D = 0: trap states, conserved node by node).
|
|
806
822
|
_moves(st) {
|
|
@@ -821,6 +837,20 @@ export class Solver {
|
|
|
821
837
|
return true;
|
|
822
838
|
}
|
|
823
839
|
|
|
840
|
+
// The charge number an electrode's carrier column counts its charge in, Q/(zF): its terminal
|
|
841
|
+
// species', or 1 for a port with none (a membrane's capacitance: Q/F).
|
|
842
|
+
_carrierZ(k) {
|
|
843
|
+
const t = this.model.ports[k - 2].terminal;
|
|
844
|
+
return t === null || t === undefined ? 1 : this.z[t];
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Whether contact k (0, 1) holds a charge its current counts, D_in on its φ law (capacitive,
|
|
848
|
+
// pinned or bulk, where φ is defined at its end node).
|
|
849
|
+
_contactCharges(k) {
|
|
850
|
+
const g = k === 0 ? 0 : this.nNodes - 1;
|
|
851
|
+
return ['capacitive', 'pinned', 'bulk'].includes(this.model.contacts[k === 0 ? 'left' : 'right'].phi.type) && !this.phiUndefined[g];
|
|
852
|
+
}
|
|
853
|
+
|
|
824
854
|
// Whether terminal k (a contact, or port k − 2) feeds stretch st: links its species there.
|
|
825
855
|
_feeds(k, st) {
|
|
826
856
|
return k === 0 ? st.leftOpen : k === 1 ? st.rightOpen : st.linked.includes(k - 2);
|
|
@@ -848,6 +878,19 @@ export class Solver {
|
|
|
848
878
|
const driven = (st) => this.terms.flatMap((t, k) => (t.drive.kind === 'I' && this._feeds(k, st) ? [k] : []));
|
|
849
879
|
const add = (parts, rowStretch, surface = [], circuit = -1, carriers = []) => {
|
|
850
880
|
const st = this.stretches[rowStretch];
|
|
881
|
+
// (the terminal's weight in it, w/z on what that feeds: the amount changes by λ I/F)
|
|
882
|
+
const own = carriers.find((p) => p.k === circuit);
|
|
883
|
+
const lambda = circuit < 0 ? 0 : ((own && own.w / this._carrierZ(circuit)) || parts.map((p) => (driven(this.stretches[p.stretch]).includes(circuit) ? p.w / this.z[this.stretches[p.stretch].species] : 0)).find((x) => x !== 0));
|
|
884
|
+
// In a circuit row, what passes through that terminal's capacitance is kept too, λ Q/F, as
|
|
885
|
+
// its current counts dQ/dt: a port or contact at no current that passes K⁺ and charges keeps
|
|
886
|
+
// n_K + Q/F. (Its K⁺ alone held, a membrane port's voltage settled 7 mV from the transient's.)
|
|
887
|
+
// A port's charge, as an electrode's carrier; a contact's, its displacement D_in, below.
|
|
888
|
+
if (circuit >= 2 && model.ports[circuit - 2].capacitance && !carriers.some((p) => p.k === circuit)) carriers = [...carriers, { k: circuit, w: lambda * this._carrierZ(circuit) }];
|
|
889
|
+
// Every contact driven by a current that feeds it and holds a charge, D_in, kept beside the
|
|
890
|
+
// ions it passes at its own λ (two contacts at no current passing K⁺, one a capacitor: its
|
|
891
|
+
// charge was left out where the law took the other's row).
|
|
892
|
+
const lam = (t) => parts.map((p) => (driven(this.stretches[p.stretch]).includes(t) && this.z[this.stretches[p.stretch].species] !== 0 ? p.w / this.z[this.stretches[p.stretch].species] : 0)).find((x) => x !== 0);
|
|
893
|
+
const contacts = circuit < 0 ? [] : [0, 1].filter((t) => lam(t) && this._contactCharges(t)).map((t) => ({ t, lambda: lam(t) }));
|
|
851
894
|
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
852
895
|
for (const p of surface) nNodes += model.ports[this.surfCols[p.col][0]].nodes.length * model.ports[this.surfCols[p.col][0]].surface.length;
|
|
853
896
|
for (const p of carriers) nNodes += model.ports[p.k - 2].nodes.length;
|
|
@@ -856,11 +899,11 @@ export class Solver {
|
|
|
856
899
|
parts,
|
|
857
900
|
surface,
|
|
858
901
|
carriers, // (what a port's electrode holds, Q/(zF) at weight w: see _conservedMoieties)
|
|
902
|
+
contacts, // (contacts whose charges it keeps, λ D_in/F each)
|
|
859
903
|
terminals: [...new Set([...parts.flatMap((p) => driven(this.stretches[p.stretch])), ...carriers.map((p) => p.k)])],
|
|
860
904
|
key: parts.map((p) => `${p.stretch}:${p.w}`).join() + surface.map((p) => `|${p.col}:${p.w}`).join() + carriers.map((p) => `|T${p.k}:${p.w}`).join(),
|
|
861
905
|
circuit, // (the terminal whose circuit row it stands in for, or −1 for a balance row's, `row`)
|
|
862
|
-
|
|
863
|
-
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)),
|
|
906
|
+
lambda,
|
|
864
907
|
row: circuit >= 0 ? -1 : this.blockOfNode[st.nodes[0]] * this.M + 1 + st.species,
|
|
865
908
|
idx: new Int32Array(nNodes * this.M),
|
|
866
909
|
w: new Float64Array(nNodes * this.M),
|
|
@@ -877,7 +920,12 @@ export class Solver {
|
|
|
877
920
|
if (this.flatStretches.includes(st)) this.constraints.at(-1).col = Array.from({ length: st.nodes[1] - st.nodes[0] + 1 }, (_, j) => this.blockOfNode[st.nodes[0] + j] * this.M + 1 + st.species);
|
|
878
921
|
});
|
|
879
922
|
const S = this.stretches.length, T = this.terms.length, X = S + this.surfCols.length;
|
|
880
|
-
|
|
923
|
+
// Two column spaces. A law's weights w: stretches [0, S), electrode surfaces' species [S, X)
|
|
924
|
+
// (surface column k − S), electrodes' charges [X, …) (carrier k − X). The rows a law can take,
|
|
925
|
+
// v: stretches' balance rows [0, S), then terminals' circuit rows (terminal j − S).
|
|
926
|
+
const isStretch = (k) => k < S, isSurface = (k) => k >= S && k < X, isCarrier = (k) => k >= X;
|
|
927
|
+
const circuitOf = (j) => (j >= S ? j - S : -1);
|
|
928
|
+
const zc = (k) => this._carrierZ(k);
|
|
881
929
|
// The rows replaced: a moving stretch's balance (one of still stretches alone is kept node by
|
|
882
930
|
// node, below), or the circuit row of a terminal driven at no current, which says nothing the
|
|
883
931
|
// combination's balance rows don't (their weighted sum is that terminal's current, weighted
|
|
@@ -927,11 +975,16 @@ export class Solver {
|
|
|
927
975
|
// A current held at zero passes nothing: an electrode at open circuit feeding a closed
|
|
928
976
|
// pool of Ag⁺ beside a slow Fe³⁺/Fe²⁺ couple settled 78 mV off, its iron not stationary.)
|
|
929
977
|
const passes = (k) => !this._noCurrent(this.terms[k]);
|
|
930
|
-
const fed = (w) => w.some((x, k) => x !== 0 && ((k
|
|
978
|
+
const fed = (w) => w.some((x, k) => x !== 0 && ((isCarrier(k) && passes(this.carriers[k - X])) || (isStretch(k) && driven(this.stretches[k]).some(passes))));
|
|
931
979
|
// Any of the combinations it alone changes will do, with any conserved amount added (whose
|
|
932
980
|
// summed balance is zero too): one of them needs a row of its own left to take. (Silver's,
|
|
933
981
|
// e⁻ − Fe³⁺ beside a closed iron couple that had Fe³⁺'s, is e⁻ + Fe²⁺ with the iron added.)
|
|
934
982
|
const news = this._conservedMoieties(true, [], rx).filter((w) => Math.abs(nu.reduce((t, x, k) => t + x * w[k], 0)) > 1e-9 && !fed(w));
|
|
983
|
+
// (One it changes only through immobile species has its rate alone in their balance at the
|
|
984
|
+
// face's node already, a row with no diffusion beside it to lose it to: left as it is. Stood
|
|
985
|
+
// in for by a mobile partner, Ag⁺ + Cl⁻ = S(immobile) beside a floating Ag electrode said its
|
|
986
|
+
// rate twice and lost the electrode's I = 0.)
|
|
987
|
+
if (news.some((w) => [...w.keys()].every((k) => !isStretch(k) || w[k] === 0 || this._still(this.stretches[k])))) continue;
|
|
935
988
|
const kept = laws.filter((l) => !fed(l));
|
|
936
989
|
const tries = [...news, ...news.flatMap((w) => kept.flatMap((l) => [1, -1].map((sg) => w.map((x, m) => x + sg * l[m]))))];
|
|
937
990
|
for (const w of tries) {
|
|
@@ -963,6 +1016,7 @@ export class Solver {
|
|
|
963
1016
|
break;
|
|
964
1017
|
}
|
|
965
1018
|
}
|
|
1019
|
+
this.contactQ = [null, null];
|
|
966
1020
|
for (let w of [...laws.filter(through), ...laws.filter((w) => !through(w))]) {
|
|
967
1021
|
for (const [j, k] of this.carriers.entries()) {
|
|
968
1022
|
const c = held.get(k);
|
|
@@ -971,10 +1025,13 @@ export class Solver {
|
|
|
971
1025
|
w = w.map((x, m) => (Math.abs(x - f * c[m]) < 1e-9 ? 0 : x - f * c[m]));
|
|
972
1026
|
w.own = own;
|
|
973
1027
|
}
|
|
974
|
-
const parts = [...w.keys()].filter((k) => k
|
|
975
|
-
const surface = [...w.keys()].filter((k) => k
|
|
1028
|
+
const parts = [...w.keys()].filter((k) => isStretch(k) && w[k] !== 0).map((k) => ({ stretch: k, w: w[k] }));
|
|
1029
|
+
const surface = [...w.keys()].filter((k) => isSurface(k) && w[k] !== 0).map((k) => ({ col: k - S, w: w[k] }));
|
|
976
1030
|
const carriers = this.carriers.flatMap((k, j) => (w[X + j] !== 0 ? [{ k, w: w[X + j] }] : []));
|
|
977
|
-
|
|
1031
|
+
// (A surface's own is kept node by node, below; one with an electrode's charge in it too is
|
|
1032
|
+
// the circuit's, here: below skips it, and a floating electrode whose adsorbates gave it
|
|
1033
|
+
// electrons settled 15 mV from where the transient ends.)
|
|
1034
|
+
if (parts.length === 0 && carriers.length === 0) continue;
|
|
978
1035
|
if (parts.length === 1 && surface.length === 0 && this.stretches[parts[0].stretch].spectator) continue; // (above)
|
|
979
1036
|
if (parts.some((p) => !this._moves(this.stretches[p.stretch]) && !this._still(this.stretches[p.stretch]))) continue;
|
|
980
1037
|
const v = new Float64Array(S + T);
|
|
@@ -992,11 +1049,14 @@ export class Solver {
|
|
|
992
1049
|
if (!(scale > 1e-9)) continue;
|
|
993
1050
|
const ok = (j) => j !== undefined && Math.abs(v[j]) > 1e-9 * scale;
|
|
994
1051
|
const col = [...Array(T).keys()].map((t) => S + t).find(ok) ?? [parts.find((p) => w[p.stretch] === 1)?.stretch, w.own].find(ok) ?? v.findIndex((x) => Math.abs(x) === scale);
|
|
995
|
-
|
|
1052
|
+
// (Other electrodes' charges in a circuit row not their own: their voltages enter it through
|
|
1053
|
+
// termDX. Two floating electrodes sharing a closed Fe³⁺, their law dropped, settled at 1.3 V
|
|
1054
|
+
// for −0.65.)
|
|
996
1055
|
chosen.push([col, v.map((x) => x / v[col])]);
|
|
997
|
-
if (col >=
|
|
998
|
-
add(parts, col <
|
|
1056
|
+
if (circuitOf(col) >= 0) held.set(circuitOf(col), w);
|
|
1057
|
+
add(parts, circuitOf(col) < 0 ? col : parts[0]?.stretch, surface, circuitOf(col), carriers);
|
|
999
1058
|
}
|
|
1059
|
+
this.contactLaw = [0, 1].map((k) => this.constraints.some((cs) => cs.contacts.some((c) => c.t === k)));
|
|
1000
1060
|
// A combination that nothing carries anywhere is conserved node by node: of still stretches
|
|
1001
1061
|
// (trap states: X⁰ + X⁻ under e⁻ + X⁰ = X⁻), or of an electrode's surface species (adsorbates
|
|
1002
1062
|
// that only turn into each other). At each node its weighted sum stays what it was, a row local
|
|
@@ -1005,18 +1065,47 @@ export class Solver {
|
|
|
1005
1065
|
const span = ([g0, g1]) => Array.from({ length: g1 - g0 + 1 }, (_, j) => g0 + j);
|
|
1006
1066
|
for (const w of laws) {
|
|
1007
1067
|
const parts = [...w.keys()].filter((k) => w[k] !== 0);
|
|
1008
|
-
if (parts.some(
|
|
1009
|
-
const sets = parts.map((k) => (k
|
|
1010
|
-
if (sets.
|
|
1011
|
-
|
|
1012
|
-
|
|
1068
|
+
if (parts.some(isCarrier)) continue; // (through an electrode: above)
|
|
1069
|
+
const sets = parts.map((k) => (isSurface(k) ? model.ports[this.surfCols[k - S][0]].nodes : this._still(this.stretches[k]) ? span(this.stretches[k].nodes) : null));
|
|
1070
|
+
if (sets.includes(null)) continue;
|
|
1071
|
+
// Its nodes: the immobile stretches', side by side, with any surface's window among them (a
|
|
1072
|
+
// surface's term only where it is: an immobile S that adsorbs on an electrode spread over
|
|
1073
|
+
// part of its region keeps S + Γaθ there and S alone elsewhere; required to match, that law
|
|
1074
|
+
// was lost, and a floating electrode's coverage drifted). Or a surface's alone.
|
|
1075
|
+
const own = parts.filter(isStretch), base = own.length > 0 ? sets[parts.indexOf(own[0])] : sets[0];
|
|
1076
|
+
const same = (set) => set.length === base.length && set.every((g, j) => g === base[j]);
|
|
1077
|
+
if (!parts.every((k, j) => (isStretch(k) ? same(sets[j]) : sets[j].every((g) => base.includes(g))))) continue;
|
|
1078
|
+
for (const k of own) this.stretches[k].conserved = true;
|
|
1079
|
+
const slot = (k) => (isStretch(k) ? 1 + this.stretches[k].species : 1 + this.n + this.surfCols[k - S][1]);
|
|
1080
|
+
// (its row a stretch's where it has one, present at every node; else the surface's)
|
|
1081
|
+
const rowPart = own.length > 0 ? (own.includes(w.own) ? w.own : own[0]) : parts.includes(w.own) ? w.own : parts[0];
|
|
1013
1082
|
this.localConstraints.push({
|
|
1014
|
-
parts: parts.map((k) => (k
|
|
1015
|
-
row: slot(
|
|
1016
|
-
nodes:
|
|
1083
|
+
parts: parts.map((k) => (isStretch(k) ? { i: this.stretches[k].species, w: w[k] } : { q: this.surfCols[k - S][1], k: this.surfCols[k - S][0], w: w[k] })),
|
|
1084
|
+
row: slot(rowPart),
|
|
1085
|
+
nodes: base,
|
|
1017
1086
|
reference: new Float64Array(sets[0].length),
|
|
1018
1087
|
});
|
|
1019
1088
|
}
|
|
1089
|
+
// An immobile stretch at a node where nothing touches it (no reaction there, no port or contact
|
|
1090
|
+
// linking it) keeps its concentration: its balance there is 0 = 0 in a steady state, which
|
|
1091
|
+
// holds nothing, and a charged one's level stayed where it was while φ moved (S⁺ beside a face
|
|
1092
|
+
// that makes it, the bath moved 0.2 V: S⁺ fell to e^{−0.2/V_T} of itself away from the face).
|
|
1093
|
+
for (const st of this.stretches) {
|
|
1094
|
+
if (!this._still(st)) continue;
|
|
1095
|
+
const i = st.species, touched = new Set();
|
|
1096
|
+
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) if (this.rxsIn[this.nodeMaterial[g]].some((rx) => rx.sp.includes(i))) touched.add(g);
|
|
1097
|
+
model.interfaces.forEach((itf, f) => {
|
|
1098
|
+
for (const rx of itf.reactions) for (const p of rx.part) if (p.i === i) touched.add(p.side === 0 ? grid.regionEnd[f] : grid.regionStart[f + 1]);
|
|
1099
|
+
});
|
|
1100
|
+
for (const port of model.ports) {
|
|
1101
|
+
if (port.species[i].type !== 'blocked' || port.reactions.some((rx) => rx.part.some((p) => p.side === 0 && p.i === i))) for (const g of port.nodes) touched.add(g);
|
|
1102
|
+
}
|
|
1103
|
+
if (st.leftOpen) touched.add(0);
|
|
1104
|
+
if (st.rightOpen) touched.add(this.nNodes - 1);
|
|
1105
|
+
for (const lc of this.localConstraints) if (lc.row === 1 + i) for (const g of lc.nodes) touched.add(g);
|
|
1106
|
+
const free = span(st.nodes).filter((g) => !touched.has(g));
|
|
1107
|
+
if (free.length > 0) this.localConstraints.push({ parts: [{ i, w: 1 }], row: 1 + i, nodes: free, reference: new Float64Array(free.length) });
|
|
1108
|
+
}
|
|
1020
1109
|
// Islands: the pieces of a stretch between its faces. One that nothing else feeds (no
|
|
1021
1110
|
// contact, port or reaction) holds its level only through its faces' fluxes, which can be
|
|
1022
1111
|
// ~1e-24 of its own conduction (a tiny conductance at the face, or a neighbour that barely
|
|
@@ -1095,7 +1184,7 @@ export class Solver {
|
|
|
1095
1184
|
if (fedBy.size !== 1) continue;
|
|
1096
1185
|
const [k] = fedBy;
|
|
1097
1186
|
if (!behind.includes(k) || this.timeLaws.some((cs) => cs.circuit === k)) continue;
|
|
1098
|
-
const zc = k >= 2 ? this.
|
|
1187
|
+
const zc = k >= 2 ? this._carrierZ(k) : 0;
|
|
1099
1188
|
const lambda = carriers.find((p) => p.k === k)?.w / zc || parts.map((p) => (this._feeds(k, this.stretches[p.stretch]) && this.z[this.stretches[p.stretch].species] !== 0 ? p.w / this.z[this.stretches[p.stretch].species] : 0)).find((x) => x !== 0);
|
|
1100
1189
|
if (!lambda) continue;
|
|
1101
1190
|
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
@@ -1118,13 +1207,14 @@ export class Solver {
|
|
|
1118
1207
|
for (const k of charged) this.chargeRefs.set(k, k >= 2 ? this.portQ[k - 2] : k === 0 ? this.contactD.left : -this.contactD.right);
|
|
1119
1208
|
}
|
|
1120
1209
|
this.lawRefs.clear();
|
|
1121
|
-
if (this.constraints.some((cs) => cs.carriers.some(({ k }) => this.model.ports[k - 2].capacitance)) && charged.length === 0) this._assembleBookkeeping(Infinity);
|
|
1210
|
+
if (this.constraints.some((cs) => cs.contacts.length > 0 || cs.carriers.some(({ k }) => this.model.ports[k - 2].capacitance)) && charged.length === 0) this._assembleBookkeeping(Infinity);
|
|
1122
1211
|
for (const cs of this.constraints) {
|
|
1123
1212
|
if (cs.terminals.length === 0) continue;
|
|
1124
1213
|
let a = 0;
|
|
1125
1214
|
for (const { stretch, w } of cs.parts) a += w * this.amount(this.stretches[stretch]);
|
|
1126
1215
|
for (const { col, w } of cs.surface) a += w * this.surfaceAmount(col);
|
|
1127
|
-
for (const { k, w } of cs.carriers) if (this.model.ports[k - 2].capacitance) a += (w * this.portQ[k - 2]) / (this.
|
|
1216
|
+
for (const { k, w } of cs.carriers) if (this.model.ports[k - 2].capacitance) a += (w * this.portQ[k - 2]) / (this._carrierZ(k) * FARADAY);
|
|
1217
|
+
for (const { t, lambda } of cs.contacts) a += (lambda * (t === 0 ? this.contactD.left : -this.contactD.right)) / FARADAY;
|
|
1128
1218
|
this.lawRefs.set(cs.key, a);
|
|
1129
1219
|
}
|
|
1130
1220
|
}
|
|
@@ -1136,7 +1226,10 @@ export class Solver {
|
|
|
1136
1226
|
for (const lc of this.localConstraints) {
|
|
1137
1227
|
lc.nodes.forEach((g, j) => {
|
|
1138
1228
|
let t = 0;
|
|
1139
|
-
for (const { i, q, w } of lc.parts)
|
|
1229
|
+
for (const { i, q, k, w } of lc.parts) {
|
|
1230
|
+
if (i !== undefined) t += w * c[g * n + i];
|
|
1231
|
+
else if (this.surfPort[g] === k) t += w * this._sites(g) * th[g * nSurf + q];
|
|
1232
|
+
}
|
|
1140
1233
|
lc.reference[j] = t;
|
|
1141
1234
|
});
|
|
1142
1235
|
}
|
|
@@ -1207,6 +1300,14 @@ export class Solver {
|
|
|
1207
1300
|
}
|
|
1208
1301
|
}
|
|
1209
1302
|
|
|
1303
|
+
// An electrode surface's sites per volume at node g, Γ a (mol/m³): what a coverage of 1 holds,
|
|
1304
|
+
// beside the concentrations of a local constraint. (Summed as θ alone, an immobile S that adsorbs
|
|
1305
|
+
// as A(ads) kept S + θ_A, mol/m³ and a fraction.)
|
|
1306
|
+
_sites(g) {
|
|
1307
|
+
const k = this.surfPort[g];
|
|
1308
|
+
return this.model.ports[k].surface[0].capacity * this.portArea[k][this.surfW[g]];
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1210
1311
|
// The local constraints' rows (steady solves only): Σ w c_i − reference at each node.
|
|
1211
1312
|
_applyLocalConstraints() {
|
|
1212
1313
|
const { n, M, c, th, nSurf, res, dA: d } = this;
|
|
@@ -1216,17 +1317,18 @@ export class Solver {
|
|
|
1216
1317
|
if (this.loc[b * M + lc.row] < 0) return;
|
|
1217
1318
|
this._replaceRow(b, lc.row);
|
|
1218
1319
|
let t = 0;
|
|
1219
|
-
for (const { i, q, w } of lc.parts) {
|
|
1320
|
+
for (const { i, q, k, w } of lc.parts) {
|
|
1220
1321
|
if (i !== undefined) {
|
|
1221
1322
|
t += w * c[g * n + i];
|
|
1222
1323
|
this._dc(g, i, d);
|
|
1223
1324
|
for (let s = 0; s < M; s++) if (d[s] !== 0) this._j(b, lc.row, b, s, w * d[s]);
|
|
1224
1325
|
continue;
|
|
1225
1326
|
}
|
|
1226
|
-
// A coverage, with ∂θ_q/∂η_x = θ_q (δ_qx − θ_x) over its surface's species.
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1327
|
+
// A coverage, with ∂θ_q/∂η_x = θ_q (δ_qx − θ_x) over its surface's species (where it is).
|
|
1328
|
+
if (this.surfPort[g] !== k) continue;
|
|
1329
|
+
const tq = th[g * nSurf + q], ns = this.model.ports[this.surfPort[g]].surface.length, ws = w * this._sites(g);
|
|
1330
|
+
t += ws * tq;
|
|
1331
|
+
for (let x = 0; x < ns; x++) this._j(b, lc.row, b, 1 + n + x, ws * tq * ((q === x ? 1 : 0) - th[g * nSurf + x]));
|
|
1230
1332
|
}
|
|
1231
1333
|
res[this.rix[b * M + lc.row]] = t - lc.reference[j];
|
|
1232
1334
|
});
|
|
@@ -1248,8 +1350,12 @@ export class Solver {
|
|
|
1248
1350
|
// which the current feeds and the reactions draw on (a port at no current with M⁺ + e⁻ = M(s)
|
|
1249
1351
|
// keeps M⁺ + Q/F, or with no capacitance, Q = 0, the M⁺ alone).
|
|
1250
1352
|
const byCurrent = (t, k) => t.drive.kind === 'I' || alsoCurrent.includes(k); // (with `alsoCurrent`, terminals counted as such)
|
|
1251
|
-
|
|
1353
|
+
// (Or a port with a capacitance that passes ions, a membrane's: what it holds is Q/F. One that
|
|
1354
|
+
// only charges keeps its charge alone, see _captureLaws.)
|
|
1355
|
+
this.carriers = drives ? this.terms.flatMap((t, k) => (byCurrent(t, k) && k >= 2 && (model.ports[k - 2].reactions.length > 0 || (model.ports[k - 2].capacitance && !this._chargeOnly(k))) ? [k] : [])) : [];
|
|
1252
1356
|
const X = S + this.surfCols.length, C = X + this.carriers.length;
|
|
1357
|
+
// (columns: stretches [0, S), surfaces' species [S, X), electrodes' charges [X, C), as in _laws)
|
|
1358
|
+
const isStretch = (k) => k < S, isSurface = (k) => k >= S && k < X;
|
|
1253
1359
|
// (With `skip`, a reaction left out: the combinations that it alone changes come out too, and
|
|
1254
1360
|
// its own rows, over the columns, are handed back as `basis.skipped`.)
|
|
1255
1361
|
const rows = [], skipped = [];
|
|
@@ -1293,7 +1399,7 @@ export class Solver {
|
|
|
1293
1399
|
const fed = this.stretches.flatMap((st, j) => (this._feeds(k, st) ? [j] : []));
|
|
1294
1400
|
if (!(drives && byCurrent(t, k))) return fed.forEach(unit);
|
|
1295
1401
|
const charged = fed.filter((j) => z[this.stretches[j].species] !== 0).map((j) => [j, z[this.stretches[j].species]]);
|
|
1296
|
-
if (this.carriers.includes(k)) charged.push([X + this.carriers.indexOf(k),
|
|
1402
|
+
if (this.carriers.includes(k)) charged.push([X + this.carriers.indexOf(k), this._carrierZ(k)]);
|
|
1297
1403
|
fed.filter((j) => z[this.stretches[j].species] === 0).forEach(unit);
|
|
1298
1404
|
for (let a = 1; a < charged.length; a++) {
|
|
1299
1405
|
const row = new Float64Array(C);
|
|
@@ -1303,9 +1409,22 @@ export class Solver {
|
|
|
1303
1409
|
}
|
|
1304
1410
|
});
|
|
1305
1411
|
// Reduced row echelon form; the free columns give the null space. Immobile stretches' columns
|
|
1306
|
-
// go first, so that a combination of them alone (trap
|
|
1307
|
-
//
|
|
1308
|
-
|
|
1412
|
+
// go first, and electrode surfaces' (as local), so that a combination of them alone (trap
|
|
1413
|
+
// states, X⁰ + X⁻; adsorbates, A(ads) + B(ads)) comes out on its own, to be kept node by node,
|
|
1414
|
+
// apart from any that moves (e⁻ − X⁰, the electrons the traps hold). (Pivoted after a closed
|
|
1415
|
+
// Cl⁻ in A(ads) + Cl⁻ = B(ads) + e⁻, the surface's own came out mixed with it, and was lost.)
|
|
1416
|
+
// Then the stretches by their starting amounts, least first, and by name: a trace species
|
|
1417
|
+
// pivoted after an abundant one comes out added to it (A + B ⇌ C with B at 1 M and A at 1e-14
|
|
1418
|
+
// gave B + C and A − B, where A + C was lost to round-off), and the basis followed the listing.
|
|
1419
|
+
const still = (k) => ((isStretch(k) && this._still(this.stretches[k])) || isSurface(k) ? 1 : 0);
|
|
1420
|
+
const rough = this.stretches.map((st) => this._roughAmount(st)), name = (k) => model.species[this.stretches[k].species].name;
|
|
1421
|
+
const order = [...Array(C).keys()].sort((a, b) => {
|
|
1422
|
+
if (still(a) !== still(b)) return still(b) - still(a);
|
|
1423
|
+
if (!isStretch(a) || !isStretch(b)) return a - b;
|
|
1424
|
+
if (rough[a] !== rough[b]) return rough[a] - rough[b];
|
|
1425
|
+
const na = name(a), nb = name(b);
|
|
1426
|
+
return na < nb ? -1 : na > nb ? 1 : a - b;
|
|
1427
|
+
});
|
|
1309
1428
|
const pivots = [];
|
|
1310
1429
|
let r = 0;
|
|
1311
1430
|
for (const col of order) {
|
|
@@ -1362,8 +1481,16 @@ export class Solver {
|
|
|
1362
1481
|
const C = this.termC[cs.circuit];
|
|
1363
1482
|
C.fill(0);
|
|
1364
1483
|
for (let j = 0; j < cs.len; j++) C[cs.idx[j]] += cs.w[j];
|
|
1484
|
+
for (const { t, lambda } of cs.contacts) {
|
|
1485
|
+
// A contact's charge, D_in, as _contact had it, with its derivatives.
|
|
1486
|
+
const q = this.contactQ[t], f = lambda / FARADAY;
|
|
1487
|
+
cs.res += f * q.D;
|
|
1488
|
+
for (let j = 0; j < C.length; j++) C[j] += f * q.row[j];
|
|
1489
|
+
dV[t] += f * q.dV;
|
|
1490
|
+
}
|
|
1365
1491
|
this.termRes[cs.circuit] = cs.res;
|
|
1366
|
-
this.termDI[cs.circuit] = dV;
|
|
1492
|
+
this.termDI[cs.circuit] = dV[cs.circuit];
|
|
1493
|
+
dV.forEach((x, k) => k !== cs.circuit && (this.termDX[cs.circuit][k] = x));
|
|
1367
1494
|
continue;
|
|
1368
1495
|
}
|
|
1369
1496
|
const b0 = Math.floor(cs.row / M), r0 = cs.row % M;
|
|
@@ -1421,24 +1548,25 @@ export class Solver {
|
|
|
1421
1548
|
}
|
|
1422
1549
|
// What an electrode holds, its charge Q/(zF) on the port's capacitance (none without one),
|
|
1423
1550
|
// Q = Σ vol·a·C (V − zeroCharge − φ) over its nodes (as _portCapacitance has it).
|
|
1424
|
-
|
|
1551
|
+
const dV = cs.dV ?? (cs.dV = new Float64Array(this.terms.length));
|
|
1552
|
+
dV.fill(0);
|
|
1425
1553
|
for (const { k, w } of cs.carriers) {
|
|
1426
1554
|
const port = this.model.ports[k - 2];
|
|
1427
1555
|
if (!port.capacitance) continue;
|
|
1428
|
-
const f = w / (this.
|
|
1556
|
+
const f = w / (this._carrierZ(k) * FARADAY), { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k];
|
|
1429
1557
|
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
1430
1558
|
port.nodes.forEach((g, j) => {
|
|
1431
1559
|
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
1432
1560
|
const b = this.blockOfNode[g], s = vol[g] * this.portArea[k - 2][j] * Cs;
|
|
1433
1561
|
amount += f * s * (V - zeroCharge - this.VT * (this.u[b * M] + this.uLo[b * M]));
|
|
1434
1562
|
gross += Math.abs(f * s * (V - zeroCharge - this.VT * (this.u[b * M] + this.uLo[b * M])));
|
|
1435
|
-
dV += f * s;
|
|
1563
|
+
dV[k] += f * s;
|
|
1436
1564
|
cs.idx[len] = this.rix[b * M];
|
|
1437
1565
|
cs.w[len++] = -f * s * this.VT;
|
|
1438
1566
|
});
|
|
1439
1567
|
}
|
|
1440
1568
|
cs.len = len;
|
|
1441
|
-
return { amount, reference, dV, gross };
|
|
1569
|
+
return { amount, reference, dV, gross }; // (dV by terminal)
|
|
1442
1570
|
}
|
|
1443
1571
|
|
|
1444
1572
|
// The same amount at the step's start, as the storage terms have it (BDF2's c* included).
|
|
@@ -1453,7 +1581,7 @@ export class Solver {
|
|
|
1453
1581
|
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity;
|
|
1454
1582
|
port.nodes.forEach((g, j) => (a += w * G * vol[g] * this.portArea[k][j] * this.thOld[g * nSurf + q]));
|
|
1455
1583
|
}
|
|
1456
|
-
for (const { k, w } of cs.carriers) if (this.model.ports[k - 2].capacitance) a += (w * this.portQStart[k - 2]) / (this.
|
|
1584
|
+
for (const { k, w } of cs.carriers) if (this.model.ports[k - 2].capacitance) a += (w * this.portQStart[k - 2]) / (this._carrierZ(k) * FARADAY);
|
|
1457
1585
|
return a;
|
|
1458
1586
|
}
|
|
1459
1587
|
|
|
@@ -1473,7 +1601,7 @@ export class Solver {
|
|
|
1473
1601
|
_circuitLaws(dt) {
|
|
1474
1602
|
for (const cs of [...this.constraints, ...this.timeLaws]) {
|
|
1475
1603
|
const k = cs.circuit;
|
|
1476
|
-
if (k < 0 || !this.floating.includes(k)) continue;
|
|
1604
|
+
if (k < 0 || !this.floating.includes(k) || cs.contacts?.length > 0) continue; // (a contact's charge: its own rows)
|
|
1477
1605
|
const { amount, dV, gross } = this._lawAmount(cs), f = FARADAY / (cs.lambda * dt), C = this.termC[k];
|
|
1478
1606
|
if (!this.lawForm.has(cs)) {
|
|
1479
1607
|
let G = 0;
|
|
@@ -1485,7 +1613,8 @@ export class Solver {
|
|
|
1485
1613
|
for (let j = 0; j < cs.len; j++) C[cs.idx[j]] += f * cs.w[j];
|
|
1486
1614
|
const d = this.terms[k].drive, src = sourceAt(d.src, this.sourceTime, this.sourceBefore);
|
|
1487
1615
|
this.termRes[k] = f * (amount - this._lawStart(cs)) - (d.kind === 'I' ? src : (src - this.termV[k]) / d.R);
|
|
1488
|
-
this.termDI[k] = f * dV + (d.kind === 'I' ? 0 : 1 / d.R);
|
|
1616
|
+
this.termDI[k] = f * dV[k] + (d.kind === 'I' ? 0 : 1 / d.R);
|
|
1617
|
+
dV.forEach((x, j) => j !== k && (this.termDX[k][j] = f * x));
|
|
1489
1618
|
}
|
|
1490
1619
|
}
|
|
1491
1620
|
|
|
@@ -1529,7 +1658,10 @@ export class Solver {
|
|
|
1529
1658
|
const A = Array.from({ length: S }, () => new Float64Array(S + 1));
|
|
1530
1659
|
for (let a = 0; a < S; a++) {
|
|
1531
1660
|
for (let b = 0; b < S; b++) A[a][b] = (b < P ? 1 : -1) * dot(a, cols[b]);
|
|
1532
|
-
if (a >= P)
|
|
1661
|
+
if (a >= P) {
|
|
1662
|
+
A[a][a] += this.termDI[fl[a - P]];
|
|
1663
|
+
for (let b = P; b < S; b++) if (b !== a) A[a][b] += this.termDX[fl[a - P]][fl[b - P]];
|
|
1664
|
+
}
|
|
1533
1665
|
A[a][S] = (a < P ? pins[a].res : this.termRes[fl[a - P]]) - dot(a, delta);
|
|
1534
1666
|
let mx = 0;
|
|
1535
1667
|
for (let b = 0; b < S; b++) mx = Math.max(mx, Math.abs(A[a][b]));
|
|
@@ -2270,10 +2402,23 @@ export class Solver {
|
|
|
2270
2402
|
return;
|
|
2271
2403
|
}
|
|
2272
2404
|
let q = this.rhoFixed[g], dq = 0;
|
|
2405
|
+
const lin = this.lin;
|
|
2273
2406
|
for (let i = 0; i < n; i++) {
|
|
2274
2407
|
const k = g * n + i, r = 1 + i;
|
|
2275
2408
|
if (!this.present[k]) continue;
|
|
2276
2409
|
const Kz = this._Kz(g, i);
|
|
2410
|
+
if (lin) {
|
|
2411
|
+
// In difference form (see DifferenceTerms): storage of i and its charge, in each
|
|
2412
|
+
// δη_j − z_j δφ̂ through K_ij.
|
|
2413
|
+
res[R[b * M + r]] += (v * (c[k] - this.cOld[k])) / dt;
|
|
2414
|
+
q += F * z[i] * c[k];
|
|
2415
|
+
const Kg = g * n * n + i * n;
|
|
2416
|
+
for (let j = 0; j < n; j++) {
|
|
2417
|
+
const Kij = this.nodeIdeal[g] ? (i === j ? c[k] : 0) : this.K[Kg + j];
|
|
2418
|
+
if (Kij !== 0) lin.node(R[b * M + 1 + j], R[b * M], z[j], v * Kij, -v * F * z[i] * Kij, R[b * M + r]);
|
|
2419
|
+
}
|
|
2420
|
+
continue;
|
|
2421
|
+
}
|
|
2277
2422
|
this._storage(g, i, dt, Kz);
|
|
2278
2423
|
q += F * z[i] * c[k];
|
|
2279
2424
|
dq += F * z[i] * Kz;
|
|
@@ -2282,7 +2427,7 @@ export class Solver {
|
|
|
2282
2427
|
// (where no charge responds and no field reaches, φ isn't defined, nor an unknown)
|
|
2283
2428
|
if (!this.phiUndefined[g]) {
|
|
2284
2429
|
res[R[b * M]] -= v * q;
|
|
2285
|
-
this._j(b, 0, b, 0, v * dq);
|
|
2430
|
+
if (!lin) this._j(b, 0, b, 0, v * dq);
|
|
2286
2431
|
}
|
|
2287
2432
|
}
|
|
2288
2433
|
|
|
@@ -2420,7 +2565,7 @@ export class Solver {
|
|
|
2420
2565
|
aHi += rx.nu[p] * u[b * M + 1 + rx.sp[p]];
|
|
2421
2566
|
aLo += rx.nu[p] * uLo[b * M + 1 + rx.sp[p]];
|
|
2422
2567
|
}
|
|
2423
|
-
const { rate, Cn, Cp, Ca } = this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], aHi + aLo);
|
|
2568
|
+
const { rate, Cn, Cp, Ca } = this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], aHi + aLo, ...this._srhDegen(g, g, law));
|
|
2424
2569
|
dr.fill(0);
|
|
2425
2570
|
this._dlnc(g, law.n.i, Cn, dr);
|
|
2426
2571
|
this._dlnc(g, law.p.i, Cp, dr);
|
|
@@ -2690,39 +2835,28 @@ export class Solver {
|
|
|
2690
2835
|
// still exactly zero at equilibrium. c_L and ex depend on every ζ at their node through K.
|
|
2691
2836
|
_segmentConcentrated(s, bL, bR, mat, h, vel) {
|
|
2692
2837
|
const R = this.rix;
|
|
2693
|
-
const { n, M,
|
|
2694
|
-
const
|
|
2838
|
+
const { n, M, res, z, K } = this;
|
|
2839
|
+
const KL = s * n * n, KR = (s + 1) * n * n;
|
|
2695
2840
|
for (let i = 0; i < n; i++) {
|
|
2696
2841
|
if (!mat.present[i] || mat.D[i] === 0) continue;
|
|
2697
2842
|
const r = 1 + i, zi = z[i];
|
|
2698
|
-
const
|
|
2699
|
-
const g = mat.D[i] / h;
|
|
2700
|
-
const pe = (vel * h) / mat.D[i];
|
|
2701
|
-
const d = zi * (u[bR * M] - u[bL * M]) + ex[gR * n + i] - ex[gL * n + i] - pe;
|
|
2702
|
-
const deta = u[bR * M + r] - u[bL * M + r] + (uLo[bR * M + r] - uLo[bL * M + r]) - pe;
|
|
2703
|
-
const E = Math.expm1(deta);
|
|
2704
|
-
const B = bernoulli(d), Bp = bernoulliDerivative(d);
|
|
2705
|
-
const gBc = g * B * cL;
|
|
2706
|
-
const N = -gBc * E;
|
|
2707
|
-
const dNdd = -g * Bp * cL * E;
|
|
2708
|
-
// Left node: ∂N/∂ζ_Lj = −gE(B + B′)K_L,ij − dNdd·δ_ij, plus the direct η_L,i term.
|
|
2709
|
-
const fL = -g * E * (B + Bp);
|
|
2710
|
-
jL[0] = -fL * this._Kz(gL, i);
|
|
2711
|
-
for (let j = 0; j < n; j++) jL[1 + j] = fL * K[KL + i * n + j];
|
|
2712
|
-
jL[r] += -dNdd + gBc * (E + 1);
|
|
2713
|
-
// Right node: ∂N/∂ζ_Rj = dNdd·(δ_ij − K_R,ij/c_R), plus the direct η_R,i term.
|
|
2714
|
-
jR.fill(0);
|
|
2715
|
-
if (cR > 0) {
|
|
2716
|
-
const fR = -dNdd / cR;
|
|
2717
|
-
jR[0] = -fR * this._Kz(gR, i);
|
|
2718
|
-
for (let j = 0; j < n; j++) jR[1 + j] = fR * K[KR + i * n + j];
|
|
2719
|
-
} else {
|
|
2720
|
-
jR[0] = dNdd * zi; // K/c → δ as c → 0
|
|
2721
|
-
jR[r] -= dNdd;
|
|
2722
|
-
}
|
|
2723
|
-
jR[r] += dNdd - gBc * (E + 1);
|
|
2843
|
+
const { N, E, gBc, dNdd, fL, cR } = this._fluxConcentrated(s, bL, bR, mat, h, vel, i);
|
|
2724
2844
|
res[R[bL * M + r]] += N;
|
|
2725
2845
|
res[R[bR * M + r]] -= N;
|
|
2846
|
+
if (this.lin) {
|
|
2847
|
+
// In difference form (see DifferenceTerms): b on Δη_i and z_i·∂N/∂d on Δφ̂, and the rest
|
|
2848
|
+
// as side terms in each δη_j − z_j δφ̂ at either end (c_L and the excess potentials).
|
|
2849
|
+
const rL = R[bL * M + r], rR = R[bR * M + r], pL = R[bL * M], pR = R[bR * M];
|
|
2850
|
+
this.lin.segment(rL, rR, pL, pR, zi, 0, -gBc * (E + 1), zi * dNdd);
|
|
2851
|
+
for (let j = 0; j < n; j++) {
|
|
2852
|
+
const wL = fL * K[KL + i * n + j] - (j === i ? dNdd : 0);
|
|
2853
|
+
const wR = cR > 0 ? dNdd * ((j === i ? 1 : 0) - K[KR + i * n + j] / cR) : 0;
|
|
2854
|
+
if (wL !== 0) this.lin.side(rL, rR, R[bL * M + 1 + j], pL, z[j], wL);
|
|
2855
|
+
if (wR !== 0) this.lin.side(rL, rR, R[bR * M + 1 + j], pR, z[j], wR);
|
|
2856
|
+
}
|
|
2857
|
+
continue;
|
|
2858
|
+
}
|
|
2859
|
+
const { jL, jR } = this;
|
|
2726
2860
|
for (let k = 0; k < M; k++) {
|
|
2727
2861
|
if (jL[k] !== 0) {
|
|
2728
2862
|
this._j(bL, r, bL, k, jL[k]);
|
|
@@ -2736,6 +2870,40 @@ export class Solver {
|
|
|
2736
2870
|
}
|
|
2737
2871
|
}
|
|
2738
2872
|
|
|
2873
|
+
// Species i's flux across segment s of a non-ideal region, toward +x, and its derivatives in
|
|
2874
|
+
// this.jL and this.jR (over the unknowns of the blocks bL and bR).
|
|
2875
|
+
_fluxConcentrated(s, bL, bR, mat, h, vel, i) {
|
|
2876
|
+
const { n, M, u, uLo, c, z, K, ex, jL, jR } = this;
|
|
2877
|
+
const gL = s, gR = s + 1, KL = gL * n * n, KR = gR * n * n, r = 1 + i, zi = z[i];
|
|
2878
|
+
const cL = c[gL * n + i], cR = c[gR * n + i];
|
|
2879
|
+
const g = mat.D[i] / h;
|
|
2880
|
+
const pe = (vel * h) / mat.D[i];
|
|
2881
|
+
const d = zi * (u[bR * M] - u[bL * M]) + ex[gR * n + i] - ex[gL * n + i] - pe;
|
|
2882
|
+
const deta = u[bR * M + r] - u[bL * M + r] + (uLo[bR * M + r] - uLo[bL * M + r]) - pe;
|
|
2883
|
+
const E = Math.expm1(deta);
|
|
2884
|
+
const B = bernoulli(d), Bp = bernoulliDerivative(d);
|
|
2885
|
+
const gBc = g * B * cL;
|
|
2886
|
+
const N = -gBc * E;
|
|
2887
|
+
const dNdd = -g * Bp * cL * E;
|
|
2888
|
+
// Left node: ∂N/∂ζ_Lj = −gE(B + B′)K_L,ij − dNdd·δ_ij, plus the direct η_L,i term.
|
|
2889
|
+
const fL = -g * E * (B + Bp);
|
|
2890
|
+
jL[0] = -fL * this._Kz(gL, i);
|
|
2891
|
+
for (let j = 0; j < n; j++) jL[1 + j] = fL * K[KL + i * n + j];
|
|
2892
|
+
jL[r] += -dNdd + gBc * (E + 1);
|
|
2893
|
+
// Right node: ∂N/∂ζ_Rj = dNdd·(δ_ij − K_R,ij/c_R), plus the direct η_R,i term.
|
|
2894
|
+
jR.fill(0);
|
|
2895
|
+
if (cR > 0) {
|
|
2896
|
+
const fR = -dNdd / cR;
|
|
2897
|
+
jR[0] = -fR * this._Kz(gR, i);
|
|
2898
|
+
for (let j = 0; j < n; j++) jR[1 + j] = fR * K[KR + i * n + j];
|
|
2899
|
+
} else {
|
|
2900
|
+
jR[0] = dNdd * zi; // K/c → δ as c → 0
|
|
2901
|
+
jR[r] -= dNdd;
|
|
2902
|
+
}
|
|
2903
|
+
jR[r] += dNdd - gBc * (E + 1);
|
|
2904
|
+
return { N, E, gBc, dNdd, fL, cR };
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2739
2907
|
// Ohmic conduction in a metal, in mixed form. The flux J (slot 0 of the left node) obeys
|
|
2740
2908
|
// η_R − η_L + J/g = 0, g = σ RT/(z²F² h) (Ohm's law, J = −g Δη),
|
|
2741
2909
|
// and enters the two nodes' carrier balances as outflow and inflow.
|
|
@@ -2856,7 +3024,7 @@ export class Solver {
|
|
|
2856
3024
|
aLo += rx.nu[p] * uLo[b * M + 1 + rx.sp[p]];
|
|
2857
3025
|
}
|
|
2858
3026
|
const law = rx.srh?.[this.nodeMaterial[g]];
|
|
2859
|
-
if (law) return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], aHi + aLo).rate;
|
|
3027
|
+
if (law) return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], aHi + aLo, ...this._srhDegen(g, g, law)).rate;
|
|
2860
3028
|
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g];
|
|
2861
3029
|
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]);
|
|
2862
3030
|
return P * -Math.expm1(-(aHi + aLo));
|
|
@@ -2870,7 +3038,7 @@ export class Solver {
|
|
|
2870
3038
|
if (law) {
|
|
2871
3039
|
let a = rx.fixedA;
|
|
2872
3040
|
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]]);
|
|
2873
|
-
return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], a).forward;
|
|
3041
|
+
return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], a, ...this._srhDegen(g, g, law)).forward;
|
|
2874
3042
|
}
|
|
2875
3043
|
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g];
|
|
2876
3044
|
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]);
|
|
@@ -2884,7 +3052,7 @@ export class Solver {
|
|
|
2884
3052
|
let a = rx.fixedA;
|
|
2885
3053
|
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]]);
|
|
2886
3054
|
const law = rx.srh?.[this.nodeMaterial[g]];
|
|
2887
|
-
if (law) return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], a).oneWay;
|
|
3055
|
+
if (law) return this._srhRate(law, c[g * n + law.n.i], c[g * n + law.p.i], a, ...this._srhDegen(g, g, law)).oneWay;
|
|
2888
3056
|
let P = rx.generation ? rx.kfNode[g] * this.generationScale : rx.kfNode[g];
|
|
2889
3057
|
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]);
|
|
2890
3058
|
return P * Math.max(1, Math.exp(-a));
|
|
@@ -2892,21 +3060,46 @@ export class Solver {
|
|
|
2892
3060
|
|
|
2893
3061
|
// SRH kinetics (see srhLaw in device.js): the rate, and its sensitivities to ln n, ln p and
|
|
2894
3062
|
// a = A/RT. With no n₁ given, the trap is midgap: n₁ = p₁ = n_i = √(n p e^{−a}).
|
|
2895
|
-
|
|
3063
|
+
// SRH at electron and hole concentrations cn, cp and affinity a. n₁p₁ = n p e^{−a} (n_i² for
|
|
3064
|
+
// Boltzmann carriers), and under degeneracy each carrier's emission scales with its own
|
|
3065
|
+
// γ = e^{−ex} (its excess chemical potential ex, c/K = ∂ln c/∂ζ its slope): n₁ = γ_n n₁₀ and
|
|
3066
|
+
// p₁ = γ_p p₁₀, n₁₀ the law's n1, or n_i at midgap. (γ_n γ_p split evenly onto both, as it was,
|
|
3067
|
+
// put a degenerate n⁺ region's γ_n on its holes' emission too.) The rate's derivatives in ln n,
|
|
3068
|
+
// ln p and a. For large negative a (strong extraction), np·(1 − e^{−a}) as −n p e^{−a}.
|
|
3069
|
+
_srhRate(law, cn, cp, a, xn = 0, xp = 0, sn = 1, sp = 1) {
|
|
2896
3070
|
const { tn, tp } = law, lnnp = Math.log(cn) + Math.log(cp);
|
|
2897
|
-
let
|
|
3071
|
+
let lnn1, kN = 0, kP = 0, kA = 0; // ln n₁ = kN ln n + kP ln p + kA a + const
|
|
2898
3072
|
if (Number.isNaN(law.n1)) {
|
|
2899
|
-
|
|
2900
|
-
kN =
|
|
3073
|
+
lnn1 = (lnnp - a - xn + xp) / 2; // γ_n n_i, with n_i² = n p e^{−a}/(γ_n γ_p)
|
|
3074
|
+
kN = (2 - sn) / 2;
|
|
3075
|
+
kP = sp / 2;
|
|
2901
3076
|
kA = -0.5;
|
|
2902
|
-
} else
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
3077
|
+
} else {
|
|
3078
|
+
lnn1 = Math.log(law.n1) - xn;
|
|
3079
|
+
kN = 1 - sn;
|
|
3080
|
+
}
|
|
3081
|
+
const n1 = Math.exp(lnn1), p1 = Math.exp(lnnp - a - lnn1), np = cn * cp;
|
|
3082
|
+
const den = tp * (cn + n1) + tn * (cp + p1);
|
|
3083
|
+
// np(1 − e^{−a}), with e^{−a} folded into np where it would overflow
|
|
3084
|
+
const f = -Math.expm1(-a), num = a > -700 ? np * f : np - Math.exp(lnnp - a);
|
|
3085
|
+
const rate = num / den;
|
|
2906
3086
|
const dDn = tp * cn + tp * n1 * kN + tn * p1 * (1 - kN);
|
|
2907
3087
|
const dDp = tp * n1 * kP + tn * cp + tn * p1 * (1 - kP);
|
|
2908
3088
|
const dDa = tp * n1 * kA + tn * p1 * (-1 - kA);
|
|
2909
|
-
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 };
|
|
3089
|
+
return { rate, forward: np / den, oneWay: a > -700 ? (np / den) * Math.max(1, Math.exp(-a)) : Math.exp(lnnp - a) / den, Cn: rate * (1 - dDn / den), Cp: rate * (1 - dDp / den), Ca: (a > -700 ? (np / den) * (1 - f) : Math.exp(lnnp - a) / den) - (rate * dDa) / den };
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
// _srhRate's degeneracy arguments: the electrons' at node gn, the holes' at gq.
|
|
3093
|
+
_srhDegen(gn, gq, law) {
|
|
3094
|
+
const [xn, sn] = this._degen(gn, law.n.i), [xp, sp] = this._degen(gq, law.p.i);
|
|
3095
|
+
return [xn, xp, sn, sp];
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
// A carrier's excess chemical potential at node g and ∂ln c/∂ζ there (0 and 1 if ideal).
|
|
3099
|
+
_degen(g, i) {
|
|
3100
|
+
if (this.nodeIdeal[g]) return [0, 1];
|
|
3101
|
+
const n = this.n, c = this.c[g * n + i], K = this.K[g * n * n + i * n + i];
|
|
3102
|
+
return [this.ex[g * n + i], c > 0 && K > 0 ? c / K : 1];
|
|
2910
3103
|
}
|
|
2911
3104
|
|
|
2912
3105
|
// A face reaction (see normalizeFaceReactions). Its rate r_k is an unknown of the face block,
|
|
@@ -2949,7 +3142,7 @@ export class Solver {
|
|
|
2949
3142
|
} else if (rx.srh) {
|
|
2950
3143
|
// SRH, with n and p each at its own side's edge node.
|
|
2951
3144
|
const { n: pn, p: pp } = rx.srh, gn = pn.side ? gR : gL, gq = pp.side ? gR : gL;
|
|
2952
|
-
const s = this._srhRate(rx.srh, c[gn * n + pn.i], c[gq * n + pp.i], aHi + aLo);
|
|
3145
|
+
const s = this._srhRate(rx.srh, c[gn * n + pn.i], c[gq * n + pp.i], aHi + aLo, ...this._srhDegen(gn, gq, { n: pn, p: pp }));
|
|
2953
3146
|
rate = s.rate;
|
|
2954
3147
|
this._dlnc(gn, pn.i, s.Cn, pn.side ? dR : dL);
|
|
2955
3148
|
this._dlnc(gq, pp.i, s.Cp, pp.side ? dR : dL);
|
|
@@ -2997,6 +3190,7 @@ export class Solver {
|
|
|
2997
3190
|
this.termC[k].fill(0);
|
|
2998
3191
|
}
|
|
2999
3192
|
this.termDI.fill(0);
|
|
3193
|
+
for (const x of this.termDX) x.fill(0);
|
|
3000
3194
|
this.termI.fill(0);
|
|
3001
3195
|
this.qg.fill(0);
|
|
3002
3196
|
this.qgSlope.fill(0);
|
|
@@ -3049,7 +3243,7 @@ export class Solver {
|
|
|
3049
3243
|
const r = 1 + i, target = this.portEta(port, i), zF = z[i] * F;
|
|
3050
3244
|
for (const g of port.nodes) {
|
|
3051
3245
|
const b = this.blockOfNode[g], o = b * M + r, v = vol[g];
|
|
3052
|
-
const deta = target -
|
|
3246
|
+
const deta = target - u[o] - uLo[o]; // (μ̄_out − μ̄)/RT
|
|
3053
3247
|
if (link.type === 'equilibrium') {
|
|
3054
3248
|
// a device end node's level is its contact's business, where the contact links it
|
|
3055
3249
|
if ((g === 0 && this.model.contacts.left.species[i].type !== 'blocked') || (g === this.nNodes - 1 && this.model.contacts.right.species[i].type !== 'blocked')) continue;
|
|
@@ -3294,7 +3488,7 @@ export class Solver {
|
|
|
3294
3488
|
const link = ct.species[i];
|
|
3295
3489
|
const o = b * M + 1 + i;
|
|
3296
3490
|
if (link.type === 'exchange') {
|
|
3297
|
-
res[R[o]] -= Ac * link.k * (link.mu / model.RT -
|
|
3491
|
+
res[R[o]] -= Ac * link.k * (link.mu / model.RT - u[o] - uLo[o]);
|
|
3298
3492
|
this._j(b, 1 + i, b, 1 + i, Ac * link.k);
|
|
3299
3493
|
} else if (link.type === 'conductance') {
|
|
3300
3494
|
const Vi = (VT * (u[o] + uLo[o])) / z[i];
|
|
@@ -3303,7 +3497,7 @@ export class Solver {
|
|
|
3303
3497
|
B[R[o]] += (-Ac * link.G) / (z[i] * F);
|
|
3304
3498
|
} else if (link.type === 'velocity') {
|
|
3305
3499
|
// N_in = v c (e^(η_out − η) − 1): v (c_eq − c), c_eq in equilibrium with the outside.
|
|
3306
|
-
const ci = c[g * n + i], E = Math.exp(Math.min(this.contactEta(side, i) -
|
|
3500
|
+
const ci = c[g * n + i], E = Math.exp(Math.min(this.contactEta(side, i) - u[o] - uLo[o], 700));
|
|
3307
3501
|
const N = Ac * link.v * ci * (E - 1), d = this.dA;
|
|
3308
3502
|
res[R[o]] -= N;
|
|
3309
3503
|
d.fill(0);
|
|
@@ -3321,13 +3515,20 @@ export class Solver {
|
|
|
3321
3515
|
const o = b * M + 1 + i;
|
|
3322
3516
|
this._replaceRow(b, 1 + i);
|
|
3323
3517
|
this._j(b, 1 + i, b, 1 + i, 1);
|
|
3324
|
-
res[R[o]] = u[o]
|
|
3518
|
+
res[R[o]] = u[o] - this.contactEta(side, i) + uLo[o]; // (the big terms first: u + uLo rounds to u)
|
|
3325
3519
|
if (z[i] !== 0) B[R[o]] += -z[i] / VT;
|
|
3326
3520
|
}
|
|
3327
3521
|
|
|
3328
3522
|
// φ law (none where φ is undefined: nothing at the end node responds to it). The displacement
|
|
3329
3523
|
// into the device, D_in = sgn·D, adds (D_in − D_in at the step's start)/dt to the current.
|
|
3330
3524
|
const link = ct.phi, Dstart = sgn * this.contactDStart[side];
|
|
3525
|
+
// (In a steady solve, what a law in this circuit row counts of D_in: see _applyConstraints.)
|
|
3526
|
+
const q = !dyn && this.steady && this.contactLaw[k] ? (this.contactQ[k] ??= { row: new Float64Array(this.sys.size + 1), D: 0, dV: 0 }) : null;
|
|
3527
|
+
if (q) {
|
|
3528
|
+
q.row.fill(0);
|
|
3529
|
+
q.D = 0;
|
|
3530
|
+
q.dV = 0;
|
|
3531
|
+
}
|
|
3331
3532
|
if (this.phiUndefined[g]) {
|
|
3332
3533
|
this.contactD[side] = 0;
|
|
3333
3534
|
} else if (link.type === 'capacitive') {
|
|
@@ -3337,6 +3538,7 @@ export class Solver {
|
|
|
3337
3538
|
this._j(b, 0, b, 0, Ac * link.C * VT);
|
|
3338
3539
|
B[R[b * M]] += -Ac * link.C;
|
|
3339
3540
|
this.contactD[side] = sgn * Din;
|
|
3541
|
+
if (q && this.loc[b * M] >= 0) [q.D, q.row[R[b * M]], q.dV] = [Din, -Ac * link.C * VT, Ac * link.C];
|
|
3340
3542
|
// Its current, the charging (D_in − D_in at the step's start)/dt; or, steady at no current,
|
|
3341
3543
|
// the charge it keeps in place of that (see _captureLaws).
|
|
3342
3544
|
const rate = dyn ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
@@ -3349,6 +3551,10 @@ export class Solver {
|
|
|
3349
3551
|
// The Poisson residual is the outside's charge, D_in.
|
|
3350
3552
|
const Din = res[R[b * M]];
|
|
3351
3553
|
this.contactD[side] = sgn * Din;
|
|
3554
|
+
if (q) {
|
|
3555
|
+
q.D = Din;
|
|
3556
|
+
this._captureRow(b, 0, 1, q.row);
|
|
3557
|
+
}
|
|
3352
3558
|
if (dyn && !(this.combining && this.lateStorage[g] === 1)) {
|
|
3353
3559
|
I += (Din - Dstart) / dt;
|
|
3354
3560
|
this._captureRow(b, 0, 1 / dt, C);
|
|
@@ -3360,7 +3566,7 @@ export class Solver {
|
|
|
3360
3566
|
if (link.type === 'pinned') {
|
|
3361
3567
|
// φ_edge = V_t − zeroCharge (the C → ∞ limit).
|
|
3362
3568
|
this._j(b, 0, b, 0, 1);
|
|
3363
|
-
res[R[b * M]] = u[b * M]
|
|
3569
|
+
res[R[b * M]] = u[b * M] - (Vt - link.zeroCharge) / VT + uLo[b * M];
|
|
3364
3570
|
B[R[b * M]] += -1 / VT;
|
|
3365
3571
|
} else {
|
|
3366
3572
|
// Plain bulk: local neutrality.
|
|
@@ -3422,14 +3628,14 @@ export class Solver {
|
|
|
3422
3628
|
const { M, u, uLo, res, z, VT, model } = this, R = this.rix;
|
|
3423
3629
|
for (const st of this.flatStretches) {
|
|
3424
3630
|
const i = st.species, open = st.leftOpen || st.rightOpen;
|
|
3425
|
-
const { k, hi, lo } = this._flatLevel(st)
|
|
3631
|
+
const { k, hi, lo } = this._flatLevel(st);
|
|
3426
3632
|
for (let g = st.nodes[0]; g <= st.nodes[1]; g++) {
|
|
3427
3633
|
const b = this.blockOfNode[g], o = b * M + 1 + i;
|
|
3428
3634
|
if (this.loc[o] < 0) continue;
|
|
3429
3635
|
this._replaceRow(b, 1 + i);
|
|
3430
3636
|
for (const B of this.termB) B[R[o]] = 0; // (the row's old terminal terms: the end node's own link)
|
|
3431
3637
|
this._j(b, 1 + i, b, 1 + i, 1);
|
|
3432
|
-
res[R[o]] = u[o] + uLo[o] -
|
|
3638
|
+
res[R[o]] = u[o] - hi + (uLo[o] - lo); // (the big terms first: hi + lo rounds to hi)
|
|
3433
3639
|
if (open && z[i] !== 0) this.termB[k][R[o]] += -z[i] / VT;
|
|
3434
3640
|
}
|
|
3435
3641
|
// A face inside the stretch whose link holds the level continuous carries the species' flux
|
|
@@ -3482,7 +3688,8 @@ export class Solver {
|
|
|
3482
3688
|
if (b < nB - 1) sys.C.fill(0, sys.offC[b] + l * sz[b + 1], sys.offC[b] + (l + 1) * sz[b + 1]);
|
|
3483
3689
|
}
|
|
3484
3690
|
|
|
3485
|
-
// Scale every row by its largest Jacobian entry (in place, residual too)
|
|
3691
|
+
// Scale every row by its largest Jacobian entry (in place, residual too), dividing by it: its
|
|
3692
|
+
// reciprocal overflows where it's subnormal (a co-ion crowded out of a double layer, ~1e-310).
|
|
3486
3693
|
_equilibrate() {
|
|
3487
3694
|
const { nB, sys, res, termB } = this, live = this._liveTerms();
|
|
3488
3695
|
const { A, B, C, sizes, offA, offB, offC, offX } = sys;
|
|
@@ -3495,16 +3702,16 @@ export class Solver {
|
|
|
3495
3702
|
for (let k = 0; k < m; k++) mx = Math.max(mx, Math.abs(B[ob + k]));
|
|
3496
3703
|
for (let k = 0; k < mn; k++) mx = Math.max(mx, Math.abs(C[oc + k]));
|
|
3497
3704
|
if (mx === 0) {
|
|
3498
|
-
this.
|
|
3705
|
+
this.rowDiv[offX[b] + r] = 1;
|
|
3499
3706
|
continue;
|
|
3500
3707
|
}
|
|
3501
|
-
const
|
|
3502
|
-
this.
|
|
3503
|
-
for (let k = 0; k < mp; k++) A[oa + k]
|
|
3504
|
-
for (let k = 0; k < m; k++) B[ob + k]
|
|
3505
|
-
for (let k = 0; k < mn; k++) C[oc + k]
|
|
3506
|
-
res[offX[b] + r]
|
|
3507
|
-
for (const k of live) termB[k][offX[b] + r]
|
|
3708
|
+
const d = mx;
|
|
3709
|
+
this.rowDiv[offX[b] + r] = d;
|
|
3710
|
+
for (let k = 0; k < mp; k++) A[oa + k] /= d;
|
|
3711
|
+
for (let k = 0; k < m; k++) B[ob + k] /= d;
|
|
3712
|
+
for (let k = 0; k < mn; k++) C[oc + k] /= d;
|
|
3713
|
+
res[offX[b] + r] /= d;
|
|
3714
|
+
for (const k of live) termB[k][offX[b] + r] /= d; // (each terminal's ∂/∂V)
|
|
3508
3715
|
}
|
|
3509
3716
|
}
|
|
3510
3717
|
}
|
|
@@ -3515,7 +3722,8 @@ export class Solver {
|
|
|
3515
3722
|
_assembleDifference(dt) {
|
|
3516
3723
|
const keep = this.sys;
|
|
3517
3724
|
const rest = this.sysRest ?? (this.sysRest = new BlockTridiagonal(keep.n, keep.sizes));
|
|
3518
|
-
const
|
|
3725
|
+
const n = this.n, nN = this.nNodes;
|
|
3726
|
+
const lin = this.diffTerms ?? (this.diffTerms = new DifferenceTerms(keep.size, nN * n * n, nN * n, 2 * nN * n * n));
|
|
3519
3727
|
this.sys = rest;
|
|
3520
3728
|
this.lin = lin;
|
|
3521
3729
|
try {
|
|
@@ -3536,15 +3744,16 @@ export class Solver {
|
|
|
3536
3744
|
// solve's and leave refined (in the transformed unknowns, as _solveBordered gives them); false
|
|
3537
3745
|
// if refining didn't help, leaving them as they came.
|
|
3538
3746
|
_refine(dt, delta, deltaV) {
|
|
3539
|
-
const { res, termRes, termB, termC, termDI,
|
|
3747
|
+
const { res, termRes, termB, termC, termDI, rowDiv, floating: fl } = this;
|
|
3540
3748
|
const N = this.sys.size, K = fl.length, pins = this.pins, R = this.rix;
|
|
3541
|
-
const save = { res: Float64Array.from(res), termRes: Float64Array.from(termRes), termB: termB.map((b) => Float64Array.from(b)), termC: termC.map((c) => Float64Array.from(c)), termDI: Float64Array.from(termDI), transformed: this.transformed, pinRes: this.pins.map((p) => p.res) };
|
|
3749
|
+
const save = { res: Float64Array.from(res), termRes: Float64Array.from(termRes), termB: termB.map((b) => Float64Array.from(b)), termC: termC.map((c) => Float64Array.from(c)), termDI: Float64Array.from(termDI), termDX: this.termDX.map((x) => Float64Array.from(x)), transformed: this.transformed, pinRes: this.pins.map((p) => p.res) };
|
|
3542
3750
|
const restoreArrays = () => {
|
|
3543
3751
|
res.set(save.res);
|
|
3544
3752
|
termRes.set(save.termRes);
|
|
3545
3753
|
save.termB.forEach((b, k) => termB[k].set(b));
|
|
3546
3754
|
save.termC.forEach((c, k) => termC[k].set(c));
|
|
3547
3755
|
termDI.set(save.termDI);
|
|
3756
|
+
save.termDX.forEach((x, k) => this.termDX[k].set(x));
|
|
3548
3757
|
this.transformed = save.transformed;
|
|
3549
3758
|
this.pins.forEach((p, a) => (p.res = save.pinRes[a]));
|
|
3550
3759
|
};
|
|
@@ -3568,10 +3777,11 @@ export class Solver {
|
|
|
3568
3777
|
for (let j = 0; j < N; j++) {
|
|
3569
3778
|
let t = jv[j];
|
|
3570
3779
|
for (let a = 0; a < K; a++) t += Bk[a][j] * x[N + a];
|
|
3571
|
-
out[j] = t
|
|
3780
|
+
out[j] = t / rowDiv[j];
|
|
3572
3781
|
}
|
|
3573
3782
|
for (let a = 0; a < K; a++) {
|
|
3574
3783
|
let t = Dk[a] * x[N + a];
|
|
3784
|
+
for (let b = 0; b < K; b++) if (b !== a) t += this.termDX[fl[a]][fl[b]] * x[N + b];
|
|
3575
3785
|
for (let j = 0; j < N; j++) t += Ck[a][j] * x[j];
|
|
3576
3786
|
out[N + a] = t;
|
|
3577
3787
|
}
|
|
@@ -4755,9 +4965,9 @@ export class Solver {
|
|
|
4755
4965
|
}
|
|
4756
4966
|
|
|
4757
4967
|
// The cuts where the impedance may read a small-signal current. Each is the linearised total
|
|
4758
|
-
// current toward +x through one segment s of
|
|
4759
|
-
//
|
|
4760
|
-
// displacement's. Without ports it's the same through every cut; with them, only between a
|
|
4968
|
+
// current toward +x through one segment s of a region, Σ (g + iω d)·δu over the unknowns idx:
|
|
4969
|
+
// the Scharfetter–Gummel fluxes' derivatives (as in _segmentsDilute, or _segmentConcentrated
|
|
4970
|
+
// for non-ideal statistics) and the displacement's. Without ports it's the same through every cut; with them, only between a
|
|
4761
4971
|
// contact and the nearest port's window (see _impedance).
|
|
4762
4972
|
_currentCuts() {
|
|
4763
4973
|
const { model, n, M, u, uLo, c, z } = this, F = FARADAY;
|
|
@@ -4765,7 +4975,7 @@ export class Solver {
|
|
|
4765
4975
|
const cuts = [];
|
|
4766
4976
|
for (let r = 0; r < regions.length; r++) {
|
|
4767
4977
|
const reg = regions[r], mat = materials[reg.material];
|
|
4768
|
-
if (mat.conductor ||
|
|
4978
|
+
if (mat.conductor || reg.mixing > 0) continue;
|
|
4769
4979
|
const vel = reg.velocity;
|
|
4770
4980
|
for (let s = grid.regionStart[r]; s < grid.regionEnd[r]; s++) {
|
|
4771
4981
|
const bL = s + r, bR = bL + 1, h = grid.segLength[s] / grid.segArea[s];
|
|
@@ -4779,6 +4989,15 @@ export class Solver {
|
|
|
4779
4989
|
const phiL = u[bL * M], phiR = u[bR * M], hasPhi = !this.phiUndefined[s];
|
|
4780
4990
|
for (let i = 0; i < n; i++) {
|
|
4781
4991
|
if (z[i] === 0 || !mat.present[i] || mat.D[i] === 0) continue;
|
|
4992
|
+
if (!mat.ideal) {
|
|
4993
|
+
this._fluxConcentrated(s, bL, bR, mat, h, vel, i);
|
|
4994
|
+
for (let k = 0; k < M; k++) {
|
|
4995
|
+
if (k === 0 && !hasPhi) continue;
|
|
4996
|
+
if (this.jL[k] !== 0) add(bL * M + k, z[i] * F * this.jL[k], 0);
|
|
4997
|
+
if (this.jR[k] !== 0) add(bR * M + k, z[i] * F * this.jR[k], 0);
|
|
4998
|
+
}
|
|
4999
|
+
continue;
|
|
5000
|
+
}
|
|
4782
5001
|
const zi = z[i], r1 = 1 + i, cL = c[s * n + i], g = mat.D[i] / h;
|
|
4783
5002
|
const pe = (vel * h) / mat.D[i];
|
|
4784
5003
|
const d = zi * (phiR - phiL) - pe;
|