driftlet 1.1.1 → 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 +4 -1
- package/docs/device.md +6 -5
- package/docs/numerics.md +13 -5
- package/docs/reliability.md +23 -0
- package/docs/statistics.md +1 -0
- package/llms.txt +6 -6
- package/package.json +2 -1
- package/src/channels.js +12 -7
- package/src/checks.js +6 -4
- package/src/data.js +1 -1
- package/src/describe.js +1 -1
- package/src/device.js +3 -4
- package/src/devices.js +9 -1
- package/src/equation.js +14 -3
- package/src/index.js +6 -3
- package/src/junction.js +9 -3
- package/src/kit.js +1 -1
- package/src/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 +167 -44
- package/src/stack.js +5 -1
- package/src/statistics.js +31 -10
- package/src/traces.js +10 -7
- package/src/types.js +96 -29
- package/types/index.d.ts +14 -5
- package/types/kit.d.ts +1 -1
- package/types/plot.d.ts +2 -2
- package/types/solution.d.ts +1 -0
- package/types/solver.d.ts +7 -1
- 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).
|
|
@@ -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();
|
|
@@ -834,6 +837,20 @@ export class Solver {
|
|
|
834
837
|
return true;
|
|
835
838
|
}
|
|
836
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
|
+
|
|
837
854
|
// Whether terminal k (a contact, or port k − 2) feeds stretch st: links its species there.
|
|
838
855
|
_feeds(k, st) {
|
|
839
856
|
return k === 0 ? st.leftOpen : k === 1 ? st.rightOpen : st.linked.includes(k - 2);
|
|
@@ -861,6 +878,19 @@ export class Solver {
|
|
|
861
878
|
const driven = (st) => this.terms.flatMap((t, k) => (t.drive.kind === 'I' && this._feeds(k, st) ? [k] : []));
|
|
862
879
|
const add = (parts, rowStretch, surface = [], circuit = -1, carriers = []) => {
|
|
863
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) }));
|
|
864
894
|
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
865
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;
|
|
866
896
|
for (const p of carriers) nNodes += model.ports[p.k - 2].nodes.length;
|
|
@@ -869,11 +899,11 @@ export class Solver {
|
|
|
869
899
|
parts,
|
|
870
900
|
surface,
|
|
871
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)
|
|
872
903
|
terminals: [...new Set([...parts.flatMap((p) => driven(this.stretches[p.stretch])), ...carriers.map((p) => p.k)])],
|
|
873
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(),
|
|
874
905
|
circuit, // (the terminal whose circuit row it stands in for, or −1 for a balance row's, `row`)
|
|
875
|
-
|
|
876
|
-
lambda: circuit < 0 ? 0 : (carriers.find((p) => p.k === circuit)?.w / this.z[model.ports[circuit - 2]?.terminal] || parts.map((p) => (driven(this.stretches[p.stretch]).includes(circuit) ? p.w / this.z[this.stretches[p.stretch].species] : 0)).find((x) => x !== 0)),
|
|
906
|
+
lambda,
|
|
877
907
|
row: circuit >= 0 ? -1 : this.blockOfNode[st.nodes[0]] * this.M + 1 + st.species,
|
|
878
908
|
idx: new Int32Array(nNodes * this.M),
|
|
879
909
|
w: new Float64Array(nNodes * this.M),
|
|
@@ -890,7 +920,12 @@ export class Solver {
|
|
|
890
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);
|
|
891
921
|
});
|
|
892
922
|
const S = this.stretches.length, T = this.terms.length, X = S + this.surfCols.length;
|
|
893
|
-
|
|
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);
|
|
894
929
|
// The rows replaced: a moving stretch's balance (one of still stretches alone is kept node by
|
|
895
930
|
// node, below), or the circuit row of a terminal driven at no current, which says nothing the
|
|
896
931
|
// combination's balance rows don't (their weighted sum is that terminal's current, weighted
|
|
@@ -940,11 +975,16 @@ export class Solver {
|
|
|
940
975
|
// A current held at zero passes nothing: an electrode at open circuit feeding a closed
|
|
941
976
|
// pool of Ag⁺ beside a slow Fe³⁺/Fe²⁺ couple settled 78 mV off, its iron not stationary.)
|
|
942
977
|
const passes = (k) => !this._noCurrent(this.terms[k]);
|
|
943
|
-
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))));
|
|
944
979
|
// Any of the combinations it alone changes will do, with any conserved amount added (whose
|
|
945
980
|
// summed balance is zero too): one of them needs a row of its own left to take. (Silver's,
|
|
946
981
|
// e⁻ − Fe³⁺ beside a closed iron couple that had Fe³⁺'s, is e⁻ + Fe²⁺ with the iron added.)
|
|
947
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;
|
|
948
988
|
const kept = laws.filter((l) => !fed(l));
|
|
949
989
|
const tries = [...news, ...news.flatMap((w) => kept.flatMap((l) => [1, -1].map((sg) => w.map((x, m) => x + sg * l[m]))))];
|
|
950
990
|
for (const w of tries) {
|
|
@@ -976,6 +1016,7 @@ export class Solver {
|
|
|
976
1016
|
break;
|
|
977
1017
|
}
|
|
978
1018
|
}
|
|
1019
|
+
this.contactQ = [null, null];
|
|
979
1020
|
for (let w of [...laws.filter(through), ...laws.filter((w) => !through(w))]) {
|
|
980
1021
|
for (const [j, k] of this.carriers.entries()) {
|
|
981
1022
|
const c = held.get(k);
|
|
@@ -984,10 +1025,13 @@ export class Solver {
|
|
|
984
1025
|
w = w.map((x, m) => (Math.abs(x - f * c[m]) < 1e-9 ? 0 : x - f * c[m]));
|
|
985
1026
|
w.own = own;
|
|
986
1027
|
}
|
|
987
|
-
const parts = [...w.keys()].filter((k) => k
|
|
988
|
-
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] }));
|
|
989
1030
|
const carriers = this.carriers.flatMap((k, j) => (w[X + j] !== 0 ? [{ k, w: w[X + j] }] : []));
|
|
990
|
-
|
|
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;
|
|
991
1035
|
if (parts.length === 1 && surface.length === 0 && this.stretches[parts[0].stretch].spectator) continue; // (above)
|
|
992
1036
|
if (parts.some((p) => !this._moves(this.stretches[p.stretch]) && !this._still(this.stretches[p.stretch]))) continue;
|
|
993
1037
|
const v = new Float64Array(S + T);
|
|
@@ -1005,11 +1049,14 @@ export class Solver {
|
|
|
1005
1049
|
if (!(scale > 1e-9)) continue;
|
|
1006
1050
|
const ok = (j) => j !== undefined && Math.abs(v[j]) > 1e-9 * scale;
|
|
1007
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);
|
|
1008
|
-
|
|
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.)
|
|
1009
1055
|
chosen.push([col, v.map((x) => x / v[col])]);
|
|
1010
|
-
if (col >=
|
|
1011
|
-
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);
|
|
1012
1058
|
}
|
|
1059
|
+
this.contactLaw = [0, 1].map((k) => this.constraints.some((cs) => cs.contacts.some((c) => c.t === k)));
|
|
1013
1060
|
// A combination that nothing carries anywhere is conserved node by node: of still stretches
|
|
1014
1061
|
// (trap states: X⁰ + X⁻ under e⁻ + X⁰ = X⁻), or of an electrode's surface species (adsorbates
|
|
1015
1062
|
// that only turn into each other). At each node its weighted sum stays what it was, a row local
|
|
@@ -1018,18 +1065,47 @@ export class Solver {
|
|
|
1018
1065
|
const span = ([g0, g1]) => Array.from({ length: g1 - g0 + 1 }, (_, j) => g0 + j);
|
|
1019
1066
|
for (const w of laws) {
|
|
1020
1067
|
const parts = [...w.keys()].filter((k) => w[k] !== 0);
|
|
1021
|
-
if (parts.some(
|
|
1022
|
-
const sets = parts.map((k) => (k
|
|
1023
|
-
if (sets.
|
|
1024
|
-
|
|
1025
|
-
|
|
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];
|
|
1026
1082
|
this.localConstraints.push({
|
|
1027
|
-
parts: parts.map((k) => (k
|
|
1028
|
-
row: slot(
|
|
1029
|
-
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,
|
|
1030
1086
|
reference: new Float64Array(sets[0].length),
|
|
1031
1087
|
});
|
|
1032
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
|
+
}
|
|
1033
1109
|
// Islands: the pieces of a stretch between its faces. One that nothing else feeds (no
|
|
1034
1110
|
// contact, port or reaction) holds its level only through its faces' fluxes, which can be
|
|
1035
1111
|
// ~1e-24 of its own conduction (a tiny conductance at the face, or a neighbour that barely
|
|
@@ -1108,7 +1184,7 @@ export class Solver {
|
|
|
1108
1184
|
if (fedBy.size !== 1) continue;
|
|
1109
1185
|
const [k] = fedBy;
|
|
1110
1186
|
if (!behind.includes(k) || this.timeLaws.some((cs) => cs.circuit === k)) continue;
|
|
1111
|
-
const zc = k >= 2 ? this.
|
|
1187
|
+
const zc = k >= 2 ? this._carrierZ(k) : 0;
|
|
1112
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);
|
|
1113
1189
|
if (!lambda) continue;
|
|
1114
1190
|
let nNodes = parts.reduce((m, p) => m + this.stretches[p.stretch].nodes[1] - this.stretches[p.stretch].nodes[0] + 1, 0);
|
|
@@ -1131,13 +1207,14 @@ export class Solver {
|
|
|
1131
1207
|
for (const k of charged) this.chargeRefs.set(k, k >= 2 ? this.portQ[k - 2] : k === 0 ? this.contactD.left : -this.contactD.right);
|
|
1132
1208
|
}
|
|
1133
1209
|
this.lawRefs.clear();
|
|
1134
|
-
if (this.constraints.some((cs) => cs.carriers.some(({ k }) => this.model.ports[k - 2].capacitance)) && charged.length === 0) this._assembleBookkeeping(Infinity);
|
|
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);
|
|
1135
1211
|
for (const cs of this.constraints) {
|
|
1136
1212
|
if (cs.terminals.length === 0) continue;
|
|
1137
1213
|
let a = 0;
|
|
1138
1214
|
for (const { stretch, w } of cs.parts) a += w * this.amount(this.stretches[stretch]);
|
|
1139
1215
|
for (const { col, w } of cs.surface) a += w * this.surfaceAmount(col);
|
|
1140
|
-
for (const { k, w } of cs.carriers) if (this.model.ports[k - 2].capacitance) a += (w * this.portQ[k - 2]) / (this.
|
|
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;
|
|
1141
1218
|
this.lawRefs.set(cs.key, a);
|
|
1142
1219
|
}
|
|
1143
1220
|
}
|
|
@@ -1149,7 +1226,10 @@ export class Solver {
|
|
|
1149
1226
|
for (const lc of this.localConstraints) {
|
|
1150
1227
|
lc.nodes.forEach((g, j) => {
|
|
1151
1228
|
let t = 0;
|
|
1152
|
-
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
|
+
}
|
|
1153
1233
|
lc.reference[j] = t;
|
|
1154
1234
|
});
|
|
1155
1235
|
}
|
|
@@ -1220,6 +1300,14 @@ export class Solver {
|
|
|
1220
1300
|
}
|
|
1221
1301
|
}
|
|
1222
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
|
+
|
|
1223
1311
|
// The local constraints' rows (steady solves only): Σ w c_i − reference at each node.
|
|
1224
1312
|
_applyLocalConstraints() {
|
|
1225
1313
|
const { n, M, c, th, nSurf, res, dA: d } = this;
|
|
@@ -1229,17 +1317,18 @@ export class Solver {
|
|
|
1229
1317
|
if (this.loc[b * M + lc.row] < 0) return;
|
|
1230
1318
|
this._replaceRow(b, lc.row);
|
|
1231
1319
|
let t = 0;
|
|
1232
|
-
for (const { i, q, w } of lc.parts) {
|
|
1320
|
+
for (const { i, q, k, w } of lc.parts) {
|
|
1233
1321
|
if (i !== undefined) {
|
|
1234
1322
|
t += w * c[g * n + i];
|
|
1235
1323
|
this._dc(g, i, d);
|
|
1236
1324
|
for (let s = 0; s < M; s++) if (d[s] !== 0) this._j(b, lc.row, b, s, w * d[s]);
|
|
1237
1325
|
continue;
|
|
1238
1326
|
}
|
|
1239
|
-
// A coverage, with ∂θ_q/∂η_x = θ_q (δ_qx − θ_x) over its surface's species.
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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]));
|
|
1243
1332
|
}
|
|
1244
1333
|
res[this.rix[b * M + lc.row]] = t - lc.reference[j];
|
|
1245
1334
|
});
|
|
@@ -1261,8 +1350,12 @@ export class Solver {
|
|
|
1261
1350
|
// which the current feeds and the reactions draw on (a port at no current with M⁺ + e⁻ = M(s)
|
|
1262
1351
|
// keeps M⁺ + Q/F, or with no capacitance, Q = 0, the M⁺ alone).
|
|
1263
1352
|
const byCurrent = (t, k) => t.drive.kind === 'I' || alsoCurrent.includes(k); // (with `alsoCurrent`, terminals counted as such)
|
|
1264
|
-
|
|
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] : [])) : [];
|
|
1265
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;
|
|
1266
1359
|
// (With `skip`, a reaction left out: the combinations that it alone changes come out too, and
|
|
1267
1360
|
// its own rows, over the columns, are handed back as `basis.skipped`.)
|
|
1268
1361
|
const rows = [], skipped = [];
|
|
@@ -1306,7 +1399,7 @@ export class Solver {
|
|
|
1306
1399
|
const fed = this.stretches.flatMap((st, j) => (this._feeds(k, st) ? [j] : []));
|
|
1307
1400
|
if (!(drives && byCurrent(t, k))) return fed.forEach(unit);
|
|
1308
1401
|
const charged = fed.filter((j) => z[this.stretches[j].species] !== 0).map((j) => [j, z[this.stretches[j].species]]);
|
|
1309
|
-
if (this.carriers.includes(k)) charged.push([X + this.carriers.indexOf(k),
|
|
1402
|
+
if (this.carriers.includes(k)) charged.push([X + this.carriers.indexOf(k), this._carrierZ(k)]);
|
|
1310
1403
|
fed.filter((j) => z[this.stretches[j].species] === 0).forEach(unit);
|
|
1311
1404
|
for (let a = 1; a < charged.length; a++) {
|
|
1312
1405
|
const row = new Float64Array(C);
|
|
@@ -1316,16 +1409,18 @@ export class Solver {
|
|
|
1316
1409
|
}
|
|
1317
1410
|
});
|
|
1318
1411
|
// Reduced row echelon form; the free columns give the null space. Immobile stretches' columns
|
|
1319
|
-
// go first, so that a combination of them alone (trap
|
|
1320
|
-
//
|
|
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.)
|
|
1321
1416
|
// Then the stretches by their starting amounts, least first, and by name: a trace species
|
|
1322
1417
|
// pivoted after an abundant one comes out added to it (A + B ⇌ C with B at 1 M and A at 1e-14
|
|
1323
1418
|
// gave B + C and A − B, where A + C was lost to round-off), and the basis followed the listing.
|
|
1324
|
-
const still = (k) => (k
|
|
1419
|
+
const still = (k) => ((isStretch(k) && this._still(this.stretches[k])) || isSurface(k) ? 1 : 0);
|
|
1325
1420
|
const rough = this.stretches.map((st) => this._roughAmount(st)), name = (k) => model.species[this.stretches[k].species].name;
|
|
1326
1421
|
const order = [...Array(C).keys()].sort((a, b) => {
|
|
1327
1422
|
if (still(a) !== still(b)) return still(b) - still(a);
|
|
1328
|
-
if (a
|
|
1423
|
+
if (!isStretch(a) || !isStretch(b)) return a - b;
|
|
1329
1424
|
if (rough[a] !== rough[b]) return rough[a] - rough[b];
|
|
1330
1425
|
const na = name(a), nb = name(b);
|
|
1331
1426
|
return na < nb ? -1 : na > nb ? 1 : a - b;
|
|
@@ -1386,8 +1481,16 @@ export class Solver {
|
|
|
1386
1481
|
const C = this.termC[cs.circuit];
|
|
1387
1482
|
C.fill(0);
|
|
1388
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
|
+
}
|
|
1389
1491
|
this.termRes[cs.circuit] = cs.res;
|
|
1390
|
-
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));
|
|
1391
1494
|
continue;
|
|
1392
1495
|
}
|
|
1393
1496
|
const b0 = Math.floor(cs.row / M), r0 = cs.row % M;
|
|
@@ -1445,24 +1548,25 @@ export class Solver {
|
|
|
1445
1548
|
}
|
|
1446
1549
|
// What an electrode holds, its charge Q/(zF) on the port's capacitance (none without one),
|
|
1447
1550
|
// Q = Σ vol·a·C (V − zeroCharge − φ) over its nodes (as _portCapacitance has it).
|
|
1448
|
-
|
|
1551
|
+
const dV = cs.dV ?? (cs.dV = new Float64Array(this.terms.length));
|
|
1552
|
+
dV.fill(0);
|
|
1449
1553
|
for (const { k, w } of cs.carriers) {
|
|
1450
1554
|
const port = this.model.ports[k - 2];
|
|
1451
1555
|
if (!port.capacitance) continue;
|
|
1452
|
-
const f = w / (this.
|
|
1556
|
+
const f = w / (this._carrierZ(k) * FARADAY), { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k];
|
|
1453
1557
|
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
1454
1558
|
port.nodes.forEach((g, j) => {
|
|
1455
1559
|
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
1456
1560
|
const b = this.blockOfNode[g], s = vol[g] * this.portArea[k - 2][j] * Cs;
|
|
1457
1561
|
amount += f * s * (V - zeroCharge - this.VT * (this.u[b * M] + this.uLo[b * M]));
|
|
1458
1562
|
gross += Math.abs(f * s * (V - zeroCharge - this.VT * (this.u[b * M] + this.uLo[b * M])));
|
|
1459
|
-
dV += f * s;
|
|
1563
|
+
dV[k] += f * s;
|
|
1460
1564
|
cs.idx[len] = this.rix[b * M];
|
|
1461
1565
|
cs.w[len++] = -f * s * this.VT;
|
|
1462
1566
|
});
|
|
1463
1567
|
}
|
|
1464
1568
|
cs.len = len;
|
|
1465
|
-
return { amount, reference, dV, gross };
|
|
1569
|
+
return { amount, reference, dV, gross }; // (dV by terminal)
|
|
1466
1570
|
}
|
|
1467
1571
|
|
|
1468
1572
|
// The same amount at the step's start, as the storage terms have it (BDF2's c* included).
|
|
@@ -1477,7 +1581,7 @@ export class Solver {
|
|
|
1477
1581
|
const [k, q] = this.surfCols[col], port = this.model.ports[k], G = port.surface[0].capacity;
|
|
1478
1582
|
port.nodes.forEach((g, j) => (a += w * G * vol[g] * this.portArea[k][j] * this.thOld[g * nSurf + q]));
|
|
1479
1583
|
}
|
|
1480
|
-
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);
|
|
1481
1585
|
return a;
|
|
1482
1586
|
}
|
|
1483
1587
|
|
|
@@ -1497,7 +1601,7 @@ export class Solver {
|
|
|
1497
1601
|
_circuitLaws(dt) {
|
|
1498
1602
|
for (const cs of [...this.constraints, ...this.timeLaws]) {
|
|
1499
1603
|
const k = cs.circuit;
|
|
1500
|
-
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)
|
|
1501
1605
|
const { amount, dV, gross } = this._lawAmount(cs), f = FARADAY / (cs.lambda * dt), C = this.termC[k];
|
|
1502
1606
|
if (!this.lawForm.has(cs)) {
|
|
1503
1607
|
let G = 0;
|
|
@@ -1509,7 +1613,8 @@ export class Solver {
|
|
|
1509
1613
|
for (let j = 0; j < cs.len; j++) C[cs.idx[j]] += f * cs.w[j];
|
|
1510
1614
|
const d = this.terms[k].drive, src = sourceAt(d.src, this.sourceTime, this.sourceBefore);
|
|
1511
1615
|
this.termRes[k] = f * (amount - this._lawStart(cs)) - (d.kind === 'I' ? src : (src - this.termV[k]) / d.R);
|
|
1512
|
-
this.termDI[k] = f * dV + (d.kind === 'I' ? 0 : 1 / d.R);
|
|
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));
|
|
1513
1618
|
}
|
|
1514
1619
|
}
|
|
1515
1620
|
|
|
@@ -1553,7 +1658,10 @@ export class Solver {
|
|
|
1553
1658
|
const A = Array.from({ length: S }, () => new Float64Array(S + 1));
|
|
1554
1659
|
for (let a = 0; a < S; a++) {
|
|
1555
1660
|
for (let b = 0; b < S; b++) A[a][b] = (b < P ? 1 : -1) * dot(a, cols[b]);
|
|
1556
|
-
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
|
+
}
|
|
1557
1665
|
A[a][S] = (a < P ? pins[a].res : this.termRes[fl[a - P]]) - dot(a, delta);
|
|
1558
1666
|
let mx = 0;
|
|
1559
1667
|
for (let b = 0; b < S; b++) mx = Math.max(mx, Math.abs(A[a][b]));
|
|
@@ -3082,6 +3190,7 @@ export class Solver {
|
|
|
3082
3190
|
this.termC[k].fill(0);
|
|
3083
3191
|
}
|
|
3084
3192
|
this.termDI.fill(0);
|
|
3193
|
+
for (const x of this.termDX) x.fill(0);
|
|
3085
3194
|
this.termI.fill(0);
|
|
3086
3195
|
this.qg.fill(0);
|
|
3087
3196
|
this.qgSlope.fill(0);
|
|
@@ -3413,6 +3522,13 @@ export class Solver {
|
|
|
3413
3522
|
// φ law (none where φ is undefined: nothing at the end node responds to it). The displacement
|
|
3414
3523
|
// into the device, D_in = sgn·D, adds (D_in − D_in at the step's start)/dt to the current.
|
|
3415
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
|
+
}
|
|
3416
3532
|
if (this.phiUndefined[g]) {
|
|
3417
3533
|
this.contactD[side] = 0;
|
|
3418
3534
|
} else if (link.type === 'capacitive') {
|
|
@@ -3422,6 +3538,7 @@ export class Solver {
|
|
|
3422
3538
|
this._j(b, 0, b, 0, Ac * link.C * VT);
|
|
3423
3539
|
B[R[b * M]] += -Ac * link.C;
|
|
3424
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];
|
|
3425
3542
|
// Its current, the charging (D_in − D_in at the step's start)/dt; or, steady at no current,
|
|
3426
3543
|
// the charge it keeps in place of that (see _captureLaws).
|
|
3427
3544
|
const rate = dyn ? 1 / dt : this.steady && this.chargeRefs.has(k) ? 1 : 0;
|
|
@@ -3434,6 +3551,10 @@ export class Solver {
|
|
|
3434
3551
|
// The Poisson residual is the outside's charge, D_in.
|
|
3435
3552
|
const Din = res[R[b * M]];
|
|
3436
3553
|
this.contactD[side] = sgn * Din;
|
|
3554
|
+
if (q) {
|
|
3555
|
+
q.D = Din;
|
|
3556
|
+
this._captureRow(b, 0, 1, q.row);
|
|
3557
|
+
}
|
|
3437
3558
|
if (dyn && !(this.combining && this.lateStorage[g] === 1)) {
|
|
3438
3559
|
I += (Din - Dstart) / dt;
|
|
3439
3560
|
this._captureRow(b, 0, 1 / dt, C);
|
|
@@ -3625,13 +3746,14 @@ export class Solver {
|
|
|
3625
3746
|
_refine(dt, delta, deltaV) {
|
|
3626
3747
|
const { res, termRes, termB, termC, termDI, rowDiv, floating: fl } = this;
|
|
3627
3748
|
const N = this.sys.size, K = fl.length, pins = this.pins, R = this.rix;
|
|
3628
|
-
const save = { res: Float64Array.from(res), termRes: Float64Array.from(termRes), termB: termB.map((b) => Float64Array.from(b)), termC: termC.map((c) => Float64Array.from(c)), termDI: Float64Array.from(termDI), transformed: this.transformed, pinRes: this.pins.map((p) => p.res) };
|
|
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) };
|
|
3629
3750
|
const restoreArrays = () => {
|
|
3630
3751
|
res.set(save.res);
|
|
3631
3752
|
termRes.set(save.termRes);
|
|
3632
3753
|
save.termB.forEach((b, k) => termB[k].set(b));
|
|
3633
3754
|
save.termC.forEach((c, k) => termC[k].set(c));
|
|
3634
3755
|
termDI.set(save.termDI);
|
|
3756
|
+
save.termDX.forEach((x, k) => this.termDX[k].set(x));
|
|
3635
3757
|
this.transformed = save.transformed;
|
|
3636
3758
|
this.pins.forEach((p, a) => (p.res = save.pinRes[a]));
|
|
3637
3759
|
};
|
|
@@ -3659,6 +3781,7 @@ export class Solver {
|
|
|
3659
3781
|
}
|
|
3660
3782
|
for (let a = 0; a < K; a++) {
|
|
3661
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];
|
|
3662
3785
|
for (let j = 0; j < N; j++) t += Ck[a][j] * x[j];
|
|
3663
3786
|
out[N + a] = t;
|
|
3664
3787
|
}
|
package/src/stack.js
CHANGED
|
@@ -33,6 +33,9 @@ export function combine(...parts) {
|
|
|
33
33
|
const species = [], materials = {};
|
|
34
34
|
parts.forEach((part, k) => {
|
|
35
35
|
if (!isObject(part)) fail(`combine: part ${k} must be { species, materials }`);
|
|
36
|
+
// (trap()'s whole result, given for its library, dropped the traps without a word)
|
|
37
|
+
if (part.library !== undefined) fail(`combine: part ${k} is { library, … }, as trap() returns: give its library here (trap(…).library), and its bulkReactions and c0 to the device`);
|
|
38
|
+
fields(part, `combine: part ${k}`, ['species', 'materials']);
|
|
36
39
|
for (const sp of part.species ?? []) {
|
|
37
40
|
const prev = species.find((s) => s.name === sp.name);
|
|
38
41
|
if (!prev) species.push({ ...sp });
|
|
@@ -100,6 +103,7 @@ export function ohmic(d = 0, carriers = ['e-', 'h+']) {
|
|
|
100
103
|
*/
|
|
101
104
|
export function bath(c, reference, d = 0, opts = {}) {
|
|
102
105
|
if (typeof reference !== 'string') [d, opts] = [reference ?? 0, isObject(d) ? d : {}];
|
|
106
|
+
checkOptions('bath', opts, ['offset', 'material']);
|
|
103
107
|
const { offset, material } = opts;
|
|
104
108
|
return {
|
|
105
109
|
...drive(d, 'bath'),
|
|
@@ -161,7 +165,7 @@ export function build(def) {
|
|
|
161
165
|
if (regions.length === 0) fail('build.stack has no layers');
|
|
162
166
|
// A contact holding a carrier its layer lacks: most often ohmic()'s default on a metal.
|
|
163
167
|
for (const [k, end] of [[0, regions[0]], [stack.length - 1, regions[regions.length - 1]]]) {
|
|
164
|
-
const held = isObject(stack[k].species) ? Object.keys(stack[k].species).filter((s) => stack[k].species[s] !== 'blocked') : [];
|
|
168
|
+
const held = isObject(stack[k].species) ? Object.keys(stack[k].species).filter((s) => stack[k].species[s] !== 'blocked' && stack[k].species[s]?.type !== 'blocked') : [];
|
|
165
169
|
const mat = materials[end.material];
|
|
166
170
|
if (!isObject(mat)) continue;
|
|
167
171
|
const has = (s) => (isObject(mat.conductor) ? mat.conductor.species === s : isObject(mat.species) && mat.species[s] !== undefined);
|
package/src/statistics.js
CHANGED
|
@@ -286,7 +286,7 @@ function debyeHuckelModel(idx, cRef, z, epsr, a, RT) {
|
|
|
286
286
|
idx,
|
|
287
287
|
evaluate(zeta, c, K) {
|
|
288
288
|
for (let b = 0; b < k; b++) y[b] = zeta[b] + Math.log(cRef[b]);
|
|
289
|
-
let st;
|
|
289
|
+
let st, mx = Infinity;
|
|
290
290
|
for (let it = 0; it < 100; it++) {
|
|
291
291
|
for (let b = 0; b < k; b++) cc[b] = Math.exp(y[b]);
|
|
292
292
|
st = state(cc);
|
|
@@ -297,7 +297,7 @@ function debyeHuckelModel(idx, cRef, z, epsr, a, RT) {
|
|
|
297
297
|
R[b] = y[b] + lng[b] - zeta[b] - Math.log(cRef[b]);
|
|
298
298
|
dot += s[b] * cc[b] * R[b];
|
|
299
299
|
}
|
|
300
|
-
|
|
300
|
+
mx = 0;
|
|
301
301
|
for (let b = 0; b < k; b++) {
|
|
302
302
|
let dy = R[b] + (st.A * s[b] * dot) / st.denom;
|
|
303
303
|
if (dy > 1) dy = 1;
|
|
@@ -307,6 +307,8 @@ function debyeHuckelModel(idx, cRef, z, epsr, a, RT) {
|
|
|
307
307
|
}
|
|
308
308
|
if (mx < 1e-14) break;
|
|
309
309
|
}
|
|
310
|
+
// (a stall at round-off, near the convexity limit, is converged; anything more isn't)
|
|
311
|
+
if (!(mx < 1e-10)) throw new SolverError(`debye-huckel: the concentrations at these levels did not converge (a step of ${mx.toExponential(2)} in ln c after 100 iterations)`);
|
|
310
312
|
for (let b = 0; b < k; b++) c[b] = Math.exp(y[b]);
|
|
311
313
|
st = state(c);
|
|
312
314
|
const f = st.A / st.denom;
|
|
@@ -315,18 +317,37 @@ function debyeHuckelModel(idx, cRef, z, epsr, a, RT) {
|
|
|
315
317
|
}
|
|
316
318
|
},
|
|
317
319
|
invert(zeta, fixed, target) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
320
|
+
// The free species by Newton in y = ln c, as evaluate() does, the fixed held at their targets:
|
|
321
|
+
// J = I − A s_F (s∘c)_Fᵀ over the free ones alone. (A fixed-point iteration contracted by
|
|
322
|
+
// 1 − denom, and near the convexity limit stopped short without a word.)
|
|
323
|
+
for (let b = 0; b < k; b++) {
|
|
324
|
+
y[b] = fixed[b] ? Math.log(target[b]) : zeta[b] + Math.log(cRef[b]);
|
|
325
|
+
cc[b] = Math.exp(y[b]);
|
|
326
|
+
}
|
|
327
|
+
let mx = 0;
|
|
328
|
+
for (let it = 0; it < 100; it++) {
|
|
329
|
+
const st = state(cc);
|
|
330
|
+
let dot = 0, s2cF = 0;
|
|
322
331
|
for (let b = 0; b < k; b++) {
|
|
323
332
|
if (fixed[b]) continue;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
333
|
+
lng[b] = y[b] + lng[b] - zeta[b] - Math.log(cRef[b]); // (the residual, in lng's place)
|
|
334
|
+
dot += s[b] * cc[b] * lng[b];
|
|
335
|
+
s2cF += s[b] * s[b] * cc[b];
|
|
327
336
|
}
|
|
328
|
-
|
|
337
|
+
const dF = 1 - st.A * s2cF; // (≥ denom > 0)
|
|
338
|
+
mx = 0;
|
|
339
|
+
for (let b = 0; b < k; b++) {
|
|
340
|
+
if (fixed[b]) continue;
|
|
341
|
+
let dy = lng[b] + (st.A * s[b] * dot) / dF;
|
|
342
|
+
if (dy > 1) dy = 1;
|
|
343
|
+
else if (dy < -1) dy = -1;
|
|
344
|
+
y[b] -= dy;
|
|
345
|
+
cc[b] = Math.exp(y[b]);
|
|
346
|
+
mx = Math.max(mx, Math.abs(dy));
|
|
347
|
+
}
|
|
348
|
+
if (mx < 1e-14) break;
|
|
329
349
|
}
|
|
350
|
+
if (!(mx < 1e-10)) throw new SolverError(`debye-huckel: the composition given did not converge to its levels (a step of ${mx.toExponential(2)} in ln c after 100 iterations)`);
|
|
330
351
|
state(cc);
|
|
331
352
|
for (let b = 0; b < k; b++) if (fixed[b]) zeta[b] = Math.log(target[b] / cRef[b]) + lng[b];
|
|
332
353
|
},
|
package/src/traces.js
CHANGED
|
@@ -37,6 +37,8 @@ const finite = (a) => {
|
|
|
37
37
|
*/
|
|
38
38
|
export function speciesRole(sol, name) {
|
|
39
39
|
const z = Object.fromEntries(sol.species.map((sp) => [sp.name, sp.z]));
|
|
40
|
+
if (!(name in z)) throw new DeviceError(`speciesRole: no species '${name}' in the solution`);
|
|
41
|
+
if (z[name] === 0) throw new DeviceError(`speciesRole: '${name}' is neutral, with no role in level diagrams (draw it muted, or in a colour of your own)`);
|
|
40
42
|
const role = (n) => (n === 'e-' ? 'electron' : z[n] > 0 ? 'cation' : 'anion');
|
|
41
43
|
const charged = sol.species.filter((sp) => sp.z !== 0).map((sp) => sp.name);
|
|
42
44
|
return { role: role(name), slot: charged.filter((n) => role(n) === role(name)).indexOf(name) };
|
|
@@ -88,7 +90,7 @@ export function traces(sol, { species, standard = true, phi = false, energy = fa
|
|
|
88
90
|
const shown = species ?? names.filter((name) => finite(sol.V[name]) && (!energy || carriers.includes(name)));
|
|
89
91
|
if (energy) {
|
|
90
92
|
for (const name of shown) {
|
|
91
|
-
if (!carriers.includes(name)) throw new
|
|
93
|
+
if (!carriers.includes(name)) throw new DeviceError(`traces: '${name}' has no band to draw as an energy; draw it as a species voltage (energy: false)`);
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
const sign = energy ? -1 : 1, unit = energy ? 'eV' : 'V';
|
|
@@ -98,22 +100,23 @@ export function traces(sol, { species, standard = true, phi = false, energy = fa
|
|
|
98
100
|
const pretty = (name) => (name in zOf ? typeset(name, zOf[name]) : name);
|
|
99
101
|
const series = [];
|
|
100
102
|
for (const name of shown) {
|
|
101
|
-
if (!names.includes(name)) throw new
|
|
103
|
+
if (!names.includes(name)) throw new DeviceError(`traces: no charged species '${name}' in the solution`);
|
|
102
104
|
const shift = shifts[name] ?? 0;
|
|
103
|
-
const at = (y) => (shift || energy ?
|
|
105
|
+
const at = (y) => (shift || energy ? Float64Array.from(y, (v) => sign * v + shift) : y);
|
|
104
106
|
const common = { species: name, role: role(name), slot: slotOf(name), ...(shift ? { shift } : {}) };
|
|
105
107
|
if (finite(sol.V[name])) series.push({ id: `V:${name}`, label: energy ? energyLabels[`V:${name}`] : `V_{${pretty(name)}}`, kind: 'level', ...common, y: at(sol.V[name]) });
|
|
106
108
|
if (standard && finite(sol.Vstd[name])) series.push({ id: `Vstd:${name}`, label: energy ? energyLabels[`Vstd:${name}`] : `V°_{${pretty(name)}}`, kind: 'standard', ...common, y: at(sol.Vstd[name]) });
|
|
107
109
|
}
|
|
108
110
|
if (Array.isArray(phi)) {
|
|
109
111
|
const known = sol.regions.flatMap((r) => [r.name, r.material]);
|
|
110
|
-
for (const name of phi) if (!known.includes(name)) throw new
|
|
112
|
+
for (const name of phi) if (!known.includes(name)) throw new DeviceError(`traces: no region or material '${name}' to draw φ in (${[...new Set(known)].join(', ')})`);
|
|
111
113
|
const inside = sol.regions.map((r) => phi.includes(r.name) || phi.includes(r.material));
|
|
112
|
-
series.push({ id: 'phi', label: energy ? '−qφ' : 'φ', kind: 'phi', role: 'phi', slot: 0, y:
|
|
113
|
-
} else if (phi) series.push({ id: 'phi', label: energy ? '−qφ' : 'φ', kind: 'phi', role: 'phi', slot: 0, y: energy ?
|
|
114
|
+
series.push({ id: 'phi', label: energy ? '−qφ' : 'φ', kind: 'phi', role: 'phi', slot: 0, y: Float64Array.from(sol.phi, (v, g) => (inside[sol.region[g]] ? sign * v : NaN)) });
|
|
115
|
+
} else if (phi) series.push({ id: 'phi', label: energy ? '−qφ' : 'φ', kind: 'phi', role: 'phi', slot: 0, y: energy ? Float64Array.from(sol.phi, (v) => -v) : sol.phi });
|
|
116
|
+
for (const lv of levels) if (typeof lv?.half?.equation !== 'string') throw new DeviceError('traces: each of levels is { half, label, standard }, its half a half() (as { half: half(\'Fe3+ + e- = Fe2+\') })');
|
|
114
117
|
const couples = [...new Set(levels.map((lv) => lv.half.equation))];
|
|
115
118
|
levels.forEach((lv, k) => {
|
|
116
|
-
const y = energy ?
|
|
119
|
+
const y = energy ? Float64Array.from(level(sol, lv.half, { standard: lv.standard }), (v) => -v) : level(sol, lv.half, { standard: lv.standard });
|
|
117
120
|
series.push({
|
|
118
121
|
id: `level:${k}`,
|
|
119
122
|
label: lv.label ?? `${lv.standard ? 'V° ' : ''}${lv.half.equation.split(' ').map(pretty).join(' ')}`,
|