driftlet 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -2
- package/docs/alignment.md +146 -0
- package/docs/conventions.md +194 -0
- package/docs/data.md +120 -0
- package/docs/device.md +541 -0
- package/docs/kit.md +299 -0
- package/docs/logo-dark.svg +7 -0
- package/docs/logo.svg +7 -0
- package/docs/numerics.md +629 -0
- package/docs/statistics.md +226 -0
- package/docs/visualization.md +259 -0
- package/llms.txt +71 -9
- package/package.json +4 -1
- package/src/constants.js +11 -0
- package/src/describe.js +17 -2
- package/src/device.js +23 -8
- package/src/fermi.js +5 -3
- package/src/index.js +5 -2
- package/src/kit.js +1 -0
- package/src/plot.js +2 -1
- package/src/pow.js +24 -0
- package/src/solver.js +110 -22
- package/src/sources.js +103 -0
- package/src/statistics.js +4 -3
- package/src/types.js +6 -4
- package/types/constants.d.ts +10 -0
- package/types/index.d.ts +11 -2
- package/types/kit.d.ts +1 -0
- package/types/pow.d.ts +4 -0
- package/types/solver.d.ts +9 -3
- package/types/sources.d.ts +102 -0
- package/types/types.d.ts +11 -5
package/src/describe.js
CHANGED
|
@@ -65,13 +65,26 @@ export function unitWarnings(def) {
|
|
|
65
65
|
if (sp.cRef !== undefined) conc(`${sp_}.cRef`, sp.cRef);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
const regionX0 = [0];
|
|
69
|
+
for (const reg of def.regions ?? []) regionX0.push(regionX0.at(-1) + (isObject(reg) && reg.length > 0 ? reg.length : 0));
|
|
68
70
|
(def.regions ?? []).forEach((reg, r) => {
|
|
69
71
|
if (!isObject(reg)) return;
|
|
70
72
|
const path = `regions[${r}]`;
|
|
71
73
|
if (reg.length > 1) warn(`${path}.length`, `${num(reg.length)} m is over a metre; lengths are m (units.um, units.nm)`);
|
|
72
74
|
if (reg.length > 0 && reg.length < 1e-10) warn(`${path}.length`, `${num(reg.length)} m is less than an atom; lengths are m`);
|
|
73
75
|
if (Math.abs(reg.fixedCharge) > 1e10) warn(`${path}.fixedCharge`, `${num(reg.fixedCharge)} C/m³ is over 6e22 cm⁻³ of charge; fixedCharge is C/m³ (units.perCm3(N) * FARADAY)`);
|
|
74
|
-
for (const [sname, c] of Object.entries(reg.c0 ?? {}))
|
|
76
|
+
for (const [sname, c] of Object.entries(reg.c0 ?? {})) {
|
|
77
|
+
if (!isObject(c)) {
|
|
78
|
+
conc(`${path}.c0.${sname}`, c);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(c.c)) conc(`${path}.c0.${sname}.c`, Math.max(...c.c));
|
|
82
|
+
// A profile is against the device's x, not the region's own.
|
|
83
|
+
const x0 = regionX0[r], x1 = x0 + reg.length;
|
|
84
|
+
if (Array.isArray(c.x) && c.x.length > 0 && (c.x.at(-1) < x0 || c.x[0] > x1)) {
|
|
85
|
+
warn(`${path}.c0.${sname}.x`, `the profile (x from ${num(c.x[0])} to ${num(c.x.at(-1))} m) misses the region (${num(x0)} to ${num(x1)} m); x is the device's, in m`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
75
88
|
});
|
|
76
89
|
const capacitance = (path, phi) => {
|
|
77
90
|
if (isObject(phi) && phi.C > 10) warn(`${path}.phi.C`, `${num(phi.C)} F/m² is over 1000 µF/cm²; C is F/m² (1 µF/cm² = 0.01 F/m²)`);
|
|
@@ -129,7 +142,9 @@ export function describe(def) {
|
|
|
129
142
|
// The screening concentration: Σ z²c from the initial composition, else the fixed charge's.
|
|
130
143
|
let zzc = 0;
|
|
131
144
|
species.forEach((sp, i) => {
|
|
132
|
-
if (mat.present[i]
|
|
145
|
+
if (!mat.present[i]) return;
|
|
146
|
+
if (Number.isFinite(reg.c0[i])) zzc += sp.z * sp.z * reg.c0[i];
|
|
147
|
+
else if (reg.c0Profile[i]) zzc += sp.z * sp.z * Math.max(...reg.c0Profile[i].c);
|
|
133
148
|
});
|
|
134
149
|
if (zzc === 0) zzc = Math.abs(reg.fixedCharge) / FARADAY;
|
|
135
150
|
const Dmax = Math.max(0, ...species.map((sp, i) => (mat.present[i] && sp.z !== 0 ? mat.D[i] : 0)));
|
package/src/device.js
CHANGED
|
@@ -178,19 +178,33 @@ export function normalizeDevice(def) {
|
|
|
178
178
|
'so it must have the opposite sign to that carrier',
|
|
179
179
|
);
|
|
180
180
|
}
|
|
181
|
-
// Initial composition: the starting state, and the conserved amount of any spectator.
|
|
181
|
+
// Initial composition: the starting state, and the conserved amount of any spectator. Each
|
|
182
|
+
// entry is a number, or a profile { x, c } against the device's x (piecewise linear, constant
|
|
183
|
+
// beyond its ends), which is the starting state even for a species a contact feeds.
|
|
182
184
|
const c0 = new Float64Array(nSpecies).fill(NaN);
|
|
185
|
+
const c0Profile = new Array(nSpecies).fill(null);
|
|
183
186
|
if (reg.c0 !== undefined) {
|
|
184
187
|
need(isObject(reg.c0), `${path}.c0 must be an object mapping species names to concentrations`);
|
|
185
188
|
for (const [sname, v] of Object.entries(reg.c0)) {
|
|
186
|
-
|
|
189
|
+
const at = `${path}.c0.${sname}`;
|
|
190
|
+
need(speciesIndex.has(sname), `${at}: unknown species '${sname}'${known(speciesIndex)}`);
|
|
187
191
|
const i = speciesIndex.get(sname);
|
|
188
|
-
need(mat.present[i], `${
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
need(mat.present[i], `${at}: '${sname}' is absent from material '${mat.name}'`);
|
|
193
|
+
const none = `; to have none of '${sname}' here, leave it out of material '${mat.name}' (a region of another material without it, if it's present elsewhere)`;
|
|
194
|
+
if (isObject(v)) {
|
|
195
|
+
fields(v, at, ['x', 'c']);
|
|
196
|
+
need(
|
|
197
|
+
Array.isArray(v.x) && Array.isArray(v.c) && v.x.length > 0 && v.x.length === v.c.length,
|
|
198
|
+
`${at} must be a profile { x, c }: two arrays of the same length, positions (m, the device's x) and concentrations (mol/m³)`,
|
|
199
|
+
);
|
|
200
|
+
v.x.forEach((xk, k) => {
|
|
201
|
+
need(isFiniteNumber(xk) && (k === 0 || xk > v.x[k - 1]), `${at}.x must be finite and strictly increasing, but x[${k}] is ${JSON.stringify(xk)}`);
|
|
202
|
+
});
|
|
203
|
+
v.c.forEach((ck, k) => need(isFiniteNumber(ck) && ck > 0, `${at}.c[${k}] must be a concentration > 0 (mol/m³), got ${JSON.stringify(ck)}${none}`));
|
|
204
|
+
c0Profile[i] = { x: Float64Array.from(v.x), c: Float64Array.from(v.c) };
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
need(isFiniteNumber(v) && v > 0, `${at} must be a concentration > 0 (mol/m³) or a profile { x, c }, got ${JSON.stringify(v)}${none}`);
|
|
194
208
|
c0[i] = v;
|
|
195
209
|
}
|
|
196
210
|
}
|
|
@@ -200,6 +214,7 @@ export function normalizeDevice(def) {
|
|
|
200
214
|
length,
|
|
201
215
|
fixedCharge,
|
|
202
216
|
c0,
|
|
217
|
+
c0Profile,
|
|
203
218
|
background,
|
|
204
219
|
velocity,
|
|
205
220
|
mixing,
|
package/src/fermi.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// −2 ≤ x ≤ 50 piecewise Chebyshev fits, built on first use from an exact quadrature
|
|
7
7
|
// x > 50 Sommerfeld's asymptotic expansion (its error is ~e^{−x})
|
|
8
8
|
|
|
9
|
+
import { powi, powr } from './pow.js';
|
|
10
|
+
|
|
9
11
|
const INTERVALS = [-2, 1, 5, 12, 25, 50];
|
|
10
12
|
const NCHEB = 24;
|
|
11
13
|
const SQRT_PI = Math.sqrt(Math.PI);
|
|
@@ -64,7 +66,7 @@ function series(j, x) {
|
|
|
64
66
|
const e = Math.exp(x);
|
|
65
67
|
let term = e, s = 0;
|
|
66
68
|
for (let k = 1; k <= 40; k++) {
|
|
67
|
-
const t = term / k
|
|
69
|
+
const t = term / powr(k, j + 1);
|
|
68
70
|
s += k % 2 ? t : -t;
|
|
69
71
|
if (t < 1e-17 * s) break;
|
|
70
72
|
term *= e;
|
|
@@ -73,7 +75,7 @@ function series(j, x) {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
// η(2k) = (1 − 2^{1−2k}) ζ(2k), the Dirichlet eta function at even arguments.
|
|
76
|
-
const ETA = [Math.PI
|
|
78
|
+
const ETA = [powi(Math.PI, 2) / 12, (7 * powi(Math.PI, 4)) / 720, (31 * powi(Math.PI, 6)) / 30240, 0.99623300185264789922, 0.99903950759827156564, 0.99975768514385819085];
|
|
77
79
|
|
|
78
80
|
function asymptotic(j, x) {
|
|
79
81
|
// 𝓕_j(x) ~ x^{j+1}/Γ(j+2) · [1 + Σ_k 2η(2k) (j+1)(j)…(j+2−2k) x^{−2k}]
|
|
@@ -86,7 +88,7 @@ function asymptotic(j, x) {
|
|
|
86
88
|
s += 2 * ETA[k - 1] * fall * p;
|
|
87
89
|
}
|
|
88
90
|
const gamma = j === 0.5 ? (3 * SQRT_PI) / 4 : SQRT_PI / 2; // Γ(j + 2)
|
|
89
|
-
return (x
|
|
91
|
+
return (powr(x, j + 1) / gamma) * s;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
function fermi(j, x) {
|
package/src/index.js
CHANGED
|
@@ -146,9 +146,12 @@ export class Device {
|
|
|
146
146
|
/**
|
|
147
147
|
* Adaptive transient to time tEnd (s): variable-step BDF2 with local error control. With
|
|
148
148
|
* `budgetMs`, it returns after that much wall time even if tEnd isn't reached (`done` says
|
|
149
|
-
* which), so an animation can call it once per frame; the step size carries over.
|
|
149
|
+
* which), so an animation can call it once per frame; the step size carries over. `probes`
|
|
150
|
+
* read a species at points inside the device after every accepted step, into `trace.probes`
|
|
151
|
+
* (one array per probe, alongside `trace.t`): what a detector at x sees.
|
|
150
152
|
* @param {number} tEnd s
|
|
151
|
-
* @param {{ tol?: number, dt0?: number, dtMax?: number, budgetMs?: number, maxSteps?: number, method?: 'bdf2' | 'be'
|
|
153
|
+
* @param {{ tol?: number, dt0?: number, dtMax?: number, budgetMs?: number, maxSteps?: number, method?: 'bdf2' | 'be',
|
|
154
|
+
* probes?: { x: number, species: string, quantity?: 'c' | 'V', region?: string | number }[] }} [opts]
|
|
152
155
|
* @returns {Solution}
|
|
153
156
|
*/
|
|
154
157
|
advance(tEnd, opts) {
|
package/src/kit.js
CHANGED
|
@@ -9,6 +9,7 @@ export { build, combine, layer, ohmic, bath } from './stack.js';
|
|
|
9
9
|
export { parseEquation, stoichiometry, half, SHE, level } from './equation.js';
|
|
10
10
|
export { traces, speciesRole, typeset } from './traces.js';
|
|
11
11
|
export { live } from './live.js';
|
|
12
|
+
export { pulse, square, triangle, ramp, injector, recombination } from './sources.js';
|
|
12
13
|
export { describe, unitWarnings } from './describe.js';
|
|
13
14
|
export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal } from './data.js';
|
|
14
15
|
|
package/src/plot.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// rule on the page restyles them.
|
|
14
14
|
|
|
15
15
|
import { traces } from './traces.js';
|
|
16
|
+
import { powi } from './pow.js';
|
|
16
17
|
|
|
17
18
|
// Colours by role, light and dark (validated together for colour-vision deficiency; line weight,
|
|
18
19
|
// dash and the legend carry identity too). A role with more members than colours cycles.
|
|
@@ -83,7 +84,7 @@ const span = (label) => labelParts(label).reduce((n, p) => n + (p.sub ? 0.75 : 1
|
|
|
83
84
|
|
|
84
85
|
function ticks(a, b, n) {
|
|
85
86
|
const step0 = (b - a) / n;
|
|
86
|
-
const mag = 10
|
|
87
|
+
const mag = powi(10, Math.floor(Math.log10(step0)));
|
|
87
88
|
const step = [1, 2, 5, 10].map((m) => m * mag).find((s) => s >= step0) ?? 10 * mag;
|
|
88
89
|
const out = [];
|
|
89
90
|
for (let t = Math.ceil(a / step) * step; t <= b + 1e-9 * step; t += step) out.push(Math.abs(t) < 1e-9 * step ? 0 : t);
|
package/src/pow.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Powers that come out the same on every JavaScript engine. Math.pow (and **) doesn't: V8's
|
|
2
|
+
// changed between Node 22 and 24 by an ulp here and there, even for integer exponents, which was
|
|
3
|
+
// enough to flip a few accept/reject decisions in long adaptive transients. Integer exponents
|
|
4
|
+
// go by repeated squaring, half-integers add a correctly rounded square root, and anything else
|
|
5
|
+
// goes through exp and log (which agree across engine versions).
|
|
6
|
+
|
|
7
|
+
/** x^n for an integer n. */
|
|
8
|
+
export function powi(x, n) {
|
|
9
|
+
if (n < 0) return 1 / powi(x, -n);
|
|
10
|
+
let r = 1, b = x;
|
|
11
|
+
while (n > 0) {
|
|
12
|
+
if (n & 1) r *= b;
|
|
13
|
+
b *= b;
|
|
14
|
+
n >>= 1;
|
|
15
|
+
}
|
|
16
|
+
return r;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** x^y for x ≥ 0 and any real y. */
|
|
20
|
+
export function powr(x, y) {
|
|
21
|
+
if (Number.isInteger(y)) return powi(x, y);
|
|
22
|
+
if (Number.isInteger(2 * y)) return powi(x, (2 * y - 1) / 2) * Math.sqrt(x);
|
|
23
|
+
return x === 0 ? (y > 0 ? 0 : Infinity) : Math.exp(y * Math.log(x));
|
|
24
|
+
}
|
package/src/solver.js
CHANGED
|
@@ -145,6 +145,22 @@ import { BlockTridiagonal, ComplexBlockTridiagonal } from './blockTridiagonal.js
|
|
|
145
145
|
import { bernoulli, bernoulliDerivative } from './bernoulli.js';
|
|
146
146
|
import { EPS0, FARADAY } from './constants.js';
|
|
147
147
|
import { nextBreakpoint, sourceAt } from './device.js';
|
|
148
|
+
import { DeviceError } from './errors.js';
|
|
149
|
+
import { powi, powr } from './pow.js';
|
|
150
|
+
|
|
151
|
+
// A c0 profile's value at x: piecewise linear, constant beyond its ends.
|
|
152
|
+
function profileAt({ x, c }, at) {
|
|
153
|
+
if (at <= x[0]) return c[0];
|
|
154
|
+
const last = x.length - 1;
|
|
155
|
+
if (at >= x[last]) return c[last];
|
|
156
|
+
let lo = 0, hi = last;
|
|
157
|
+
while (hi - lo > 1) {
|
|
158
|
+
const mid = (lo + hi) >> 1;
|
|
159
|
+
if (x[mid] <= at) lo = mid;
|
|
160
|
+
else hi = mid;
|
|
161
|
+
}
|
|
162
|
+
return c[lo] + ((c[hi] - c[lo]) * (at - x[lo])) / (x[hi] - x[lo]);
|
|
163
|
+
}
|
|
148
164
|
|
|
149
165
|
export class SolverError extends Error {
|
|
150
166
|
constructor(message, details) {
|
|
@@ -738,9 +754,9 @@ export class Solver {
|
|
|
738
754
|
}
|
|
739
755
|
|
|
740
756
|
/**
|
|
741
|
-
* Cold start.
|
|
742
|
-
*
|
|
743
|
-
*
|
|
757
|
+
* Cold start. Each species takes its region's c0 where one is given (for a spectator, it fixes
|
|
758
|
+
* the conserved amount), else its contact's level. φ in each region is then chosen for local neutrality (node by node where a
|
|
759
|
+
* profile varies), or continued across the interface dipole if nothing there responds.
|
|
744
760
|
*/
|
|
745
761
|
initFromComposition() {
|
|
746
762
|
const { model, n, M, u, z } = this;
|
|
@@ -754,10 +770,15 @@ export class Solver {
|
|
|
754
770
|
if (r > 0) phiHat += interfaces[r - 1].dipole / this.VT;
|
|
755
771
|
const reg = regions[r], mat = materials[reg.material];
|
|
756
772
|
mode.fill(0);
|
|
773
|
+
const profiled = reg.c0Profile.some((p) => p !== null);
|
|
757
774
|
for (let i = 0; i < n; i++) {
|
|
758
775
|
if (!mat.present[i]) continue;
|
|
759
776
|
const st = this.stretches[this.stretchOf[r * n + i]];
|
|
760
|
-
if (
|
|
777
|
+
if (reg.c0Profile[i] || reg.c0[i] > 0) {
|
|
778
|
+
// A given c0 is the starting state, even where a contact feeds the species.
|
|
779
|
+
mode[i] = 2;
|
|
780
|
+
cFix[i] = reg.c0Profile[i] ? profileAt(reg.c0Profile[i], grid.x[grid.regionStart[r]]) : reg.c0[i];
|
|
781
|
+
} else if (!st.contactFed && mat.conductor) {
|
|
761
782
|
// A conductor away from the contacts: start uncharged, with its carrier's level in
|
|
762
783
|
// equilibrium with the first reaction on its left face that takes it, else at the
|
|
763
784
|
// running φ.
|
|
@@ -818,7 +839,8 @@ export class Solver {
|
|
|
818
839
|
return q;
|
|
819
840
|
};
|
|
820
841
|
const responds = mode.some((m, i) => m === 1 && z[i] !== 0);
|
|
821
|
-
|
|
842
|
+
const mustBalance = !responds && !mat.conductor && !mat.phiFree && mat.epsr === 0;
|
|
843
|
+
const checkNeutral = (where) => {
|
|
822
844
|
// Strictly neutral, with every charged species' amount given: its c0 must be neutral.
|
|
823
845
|
let q = reg.fixedCharge / FARADAY, scale = Math.abs(q);
|
|
824
846
|
for (let i = 0; i < n; i++) {
|
|
@@ -828,11 +850,12 @@ export class Solver {
|
|
|
828
850
|
}
|
|
829
851
|
if (Math.abs(q) > 1e-9 * scale) {
|
|
830
852
|
throw new SolverError(
|
|
831
|
-
`regions[${r}] (${reg.name}): its initial composition carries a net charge of ${q.toPrecision(3)} mol/m
|
|
853
|
+
`regions[${r}] (${reg.name}): its initial composition carries a net charge of ${q.toPrecision(3)} mol/m³${where} (Σ z·c0, with any fixed charge), ` +
|
|
832
854
|
'but ε = 0 makes it strictly neutral; adjust c0 so that it balances',
|
|
833
855
|
);
|
|
834
856
|
}
|
|
835
|
-
}
|
|
857
|
+
};
|
|
858
|
+
if (mustBalance && !profiled) checkNeutral('');
|
|
836
859
|
if (!mat.phiFree && !responds && mat.ideal && r > 0 && materials[regions[r - 1].material].conductor) {
|
|
837
860
|
// Nothing here fixes φ, but an electrode on the left does: start with its first reaction
|
|
838
861
|
// that takes a species from this side at equilibrium (the electrode at its open-circuit
|
|
@@ -853,8 +876,8 @@ export class Solver {
|
|
|
853
876
|
}
|
|
854
877
|
}
|
|
855
878
|
}
|
|
856
|
-
|
|
857
|
-
let lo =
|
|
879
|
+
const neutralPhi = (ph) => {
|
|
880
|
+
let lo = ph - 1, hi = ph + 1;
|
|
858
881
|
while (charge(lo) < 0 && lo > -1e4) lo -= 2 * (hi - lo);
|
|
859
882
|
while (charge(hi) > 0 && hi < 1e4) hi += 2 * (hi - lo);
|
|
860
883
|
for (let it = 0; it < 200 && hi - lo > 1e-12; it++) {
|
|
@@ -862,10 +885,17 @@ export class Solver {
|
|
|
862
885
|
if (charge(m) > 0) lo = m;
|
|
863
886
|
else hi = m;
|
|
864
887
|
}
|
|
865
|
-
|
|
866
|
-
}
|
|
888
|
+
return 0.5 * (lo + hi);
|
|
889
|
+
};
|
|
890
|
+
if (!mat.phiFree && responds) phiHat = neutralPhi(phiHat);
|
|
867
891
|
if (!mat.ideal) this._materialAt(mat, reg.background, eta, mode, cFix, phiHat, zeta, cc);
|
|
868
892
|
for (let g = grid.regionStart[r]; g <= grid.regionEnd[r]; g++) {
|
|
893
|
+
if (profiled) {
|
|
894
|
+
for (let i = 0; i < n; i++) if (reg.c0Profile[i]) cFix[i] = profileAt(reg.c0Profile[i], grid.x[g]);
|
|
895
|
+
if (mustBalance) checkNeutral(` at x = ${grid.x[g].toPrecision(4)} m`);
|
|
896
|
+
if (!mat.phiFree && responds) phiHat = neutralPhi(phiHat);
|
|
897
|
+
if (!mat.ideal) this._materialAt(mat, reg.background, eta, mode, cFix, phiHat, zeta, cc);
|
|
898
|
+
}
|
|
869
899
|
const b = this.blockOfNode[g];
|
|
870
900
|
u[b * M] = mat.conductor ? 0 : phiHat; // a metal's slot 0 is its segment flux
|
|
871
901
|
for (let i = 0; i < n; i++) {
|
|
@@ -1250,7 +1280,7 @@ export class Solver {
|
|
|
1250
1280
|
const i = rx.sp[p], nu = rx.nu[p];
|
|
1251
1281
|
if (nu > 0) {
|
|
1252
1282
|
const ci = c[g * n + i];
|
|
1253
|
-
P *= nu === 1 ? ci : ci
|
|
1283
|
+
P *= nu === 1 ? ci : powi(ci, nu);
|
|
1254
1284
|
this._dlnc(g, i, nu, dr);
|
|
1255
1285
|
}
|
|
1256
1286
|
aHi += nu * u[b * M + 1 + i];
|
|
@@ -1596,7 +1626,7 @@ export class Solver {
|
|
|
1596
1626
|
aLo -= p.nu * uLo[o];
|
|
1597
1627
|
if (this.nodeConductor[g] === p.i) continue; // a conductor's carrier has activity 1
|
|
1598
1628
|
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
1599
|
-
pref *= (c[g * n + p.i] / this.cRef[g * n + p.i]
|
|
1629
|
+
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
1600
1630
|
this._dlnc(g, p.i, e, p.side ? dR : dL);
|
|
1601
1631
|
}
|
|
1602
1632
|
const { g: bv, gp } = bvFactor(aHi + aLo, al);
|
|
@@ -1846,7 +1876,7 @@ export class Solver {
|
|
|
1846
1876
|
}
|
|
1847
1877
|
|
|
1848
1878
|
// Largest update among the potential-like unknowns (φ̂ and η at grid nodes).
|
|
1849
|
-
_maxPotentialStep(delta, deltaV = this.deltaV) {
|
|
1879
|
+
_maxPotentialStep(delta, deltaV = this.deltaV) { // (deltaV null: the device's potentials only)
|
|
1850
1880
|
const { n, M } = this;
|
|
1851
1881
|
const R = this.rix; // (the sink entry of delta is 0)
|
|
1852
1882
|
let mx = 0;
|
|
@@ -1868,7 +1898,7 @@ export class Solver {
|
|
|
1868
1898
|
newton(dt, { maxIter = 60, tol = 1e-10, maxStep = 10 } = {}) {
|
|
1869
1899
|
const { delta, res } = this;
|
|
1870
1900
|
const deltaV = this.deltaV ?? (this.deltaV = new Float64Array(this.floating.length));
|
|
1871
|
-
const history = [];
|
|
1901
|
+
const history = [], ownHistory = [];
|
|
1872
1902
|
// A steady solve that fails: record how nearly singular the system was, and where.
|
|
1873
1903
|
const fail = (r) => {
|
|
1874
1904
|
if (dt === Infinity) this._noteConditioning();
|
|
@@ -1899,15 +1929,22 @@ export class Solver {
|
|
|
1899
1929
|
if (!(err instanceof SolverError)) throw err;
|
|
1900
1930
|
return fail({ converged: false, iterations: it, history, error: err.message });
|
|
1901
1931
|
}
|
|
1932
|
+
// Convergence counts every unknown, floating terminal voltages too. Damping and the
|
|
1933
|
+
// divergence check count only the device's own potentials: a terminal voltage enters
|
|
1934
|
+
// linearly (conductance links, held levels, a capacitive face), so a large swing in one is
|
|
1935
|
+
// safe, and damping it would throttle the whole update (a current-driven port switching
|
|
1936
|
+
// off, its voltage collapsing by hundreds of volts, crawled 10 thermal units at a time).
|
|
1902
1937
|
const step = this._maxPotentialStep(delta, deltaV);
|
|
1938
|
+
const own = this._maxPotentialStep(delta, null);
|
|
1903
1939
|
history.push(step);
|
|
1940
|
+
ownHistory.push(own);
|
|
1904
1941
|
if (!Number.isFinite(step)) return fail({ converged: false, iterations: it, history, error: 'non-finite update' });
|
|
1905
1942
|
// Give up early on clear divergence; the caller will take a smaller step instead.
|
|
1906
|
-
if (
|
|
1943
|
+
if (own > 1e4 || (it > 6 && own > 10 * ownHistory[0])) {
|
|
1907
1944
|
this.computeConcentrations();
|
|
1908
1945
|
return fail({ converged: false, iterations: it, history, error: 'diverging' });
|
|
1909
1946
|
}
|
|
1910
|
-
const alpha =
|
|
1947
|
+
const alpha = own > maxStep ? maxStep / own : 1;
|
|
1911
1948
|
this._addToState(delta, -alpha);
|
|
1912
1949
|
this.floating.forEach((k, a) => (this.termV[k] -= alpha * deltaV[a]));
|
|
1913
1950
|
if (alpha === 1 && step < tol) {
|
|
@@ -2071,16 +2108,17 @@ export class Solver {
|
|
|
2071
2108
|
const { tol = 1e-3, dtMax = Infinity, budgetMs = Infinity, maxSteps = 100000, method = 'bdf2' } = opts;
|
|
2072
2109
|
const clock = () => (globalThis.performance ? globalThis.performance.now() : Date.now());
|
|
2073
2110
|
const start = clock();
|
|
2074
|
-
const
|
|
2111
|
+
const sample = this._probes(opts.probes);
|
|
2112
|
+
const trace = { t: [], current: [], voltage: [], ...(sample ? { probes: sample.out } : {}) };
|
|
2075
2113
|
let steps = 0, rejected = 0, iterations = 0, failed = false;
|
|
2076
2114
|
// Already there (an animation frame with no time to add): nothing to do, and the step size
|
|
2077
2115
|
// carried to the next call stays as it was.
|
|
2078
|
-
if (!(tEnd > this.time)) return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace
|
|
2116
|
+
if (!(tEnd > this.time)) return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace };
|
|
2079
2117
|
let dt = this.dtNext ?? opts.dt0 ?? (tEnd - this.time) * 1e-4;
|
|
2080
2118
|
if (!(dt > 0)) dt = (tEnd - this.time) * 1e-4;
|
|
2081
2119
|
let grow = 0; // longer first steps tried after a Newton failure (see below)
|
|
2082
2120
|
const pred = new Float64Array(this.u.length), guess = new Float64Array(this.u.length);
|
|
2083
|
-
const factor = (err, p) => (err > 0 ? Math.min(2, Math.max(0.2, 0.9 * (tol / err)
|
|
2121
|
+
const factor = (err, p) => (err > 0 ? Math.min(2, Math.max(0.2, 0.9 * Math.exp(Math.log(tol / err) / (p + 1)))) : 2);
|
|
2084
2122
|
while (this.time < tEnd) {
|
|
2085
2123
|
if (steps + rejected >= maxSteps || clock() - start > budgetMs) break;
|
|
2086
2124
|
// Within round-off of the end (a target a hair past a breakpoint just landed on, say):
|
|
@@ -2113,7 +2151,7 @@ export class Solver {
|
|
|
2113
2151
|
this._restore(snap);
|
|
2114
2152
|
const a = this.step(h / 2);
|
|
2115
2153
|
// The first half step is kept too, so it goes in the trace (a jump's charge is in it).
|
|
2116
|
-
half = a.converged ? { t: this.time, current: this._terminalCurrent(), voltage: this.termV[1] - this.termV[0] } : null;
|
|
2154
|
+
half = a.converged ? { t: this.time, current: this._terminalCurrent(), voltage: this.termV[1] - this.termV[0], probes: sample?.read() } : null;
|
|
2117
2155
|
const b = a.converged ? this.step(h / 2) : a;
|
|
2118
2156
|
iterations += a.iterations + (b === a ? 0 : b.iterations);
|
|
2119
2157
|
if (b.converged) {
|
|
@@ -2165,10 +2203,12 @@ export class Solver {
|
|
|
2165
2203
|
trace.t.push(half.t);
|
|
2166
2204
|
trace.current.push(half.current);
|
|
2167
2205
|
trace.voltage.push(half.voltage);
|
|
2206
|
+
if (sample) sample.push(half.probes);
|
|
2168
2207
|
}
|
|
2169
2208
|
trace.t.push(this.time);
|
|
2170
2209
|
trace.current.push(this._terminalCurrent());
|
|
2171
2210
|
trace.voltage.push(this.termV[1] - this.termV[0]);
|
|
2211
|
+
if (sample) sample.push(sample.read());
|
|
2172
2212
|
}
|
|
2173
2213
|
this.dtNext = dt;
|
|
2174
2214
|
this.landing = undefined;
|
|
@@ -2213,7 +2253,7 @@ export class Solver {
|
|
|
2213
2253
|
const l2 = ((t - t0) * (t - t1)) / ((t2 - t0) * (t2 - t1));
|
|
2214
2254
|
for (let k = 0; k < pred.length; k++) pred[k] = l0 * e0.u[k] + l1 * e1.u[k] + l2 * e2.u[k];
|
|
2215
2255
|
const w = h / hp;
|
|
2216
|
-
const Cc = (h
|
|
2256
|
+
const Cc = (h * h * h * (1 + w) * (1 + w)) / (w * (1 + 2 * w));
|
|
2217
2257
|
const Cp = h * (h + hp) * (h + hp + hpp);
|
|
2218
2258
|
return { scale: Cc / (Cc + Cp), order: 2 };
|
|
2219
2259
|
}
|
|
@@ -2224,6 +2264,54 @@ export class Solver {
|
|
|
2224
2264
|
return { scale: bdf ? 1 : h / (2 * h + hp), order: 1 };
|
|
2225
2265
|
}
|
|
2226
2266
|
|
|
2267
|
+
// Probes for a transient's trace: { x, species, quantity: 'c' | 'V', region } read after every
|
|
2268
|
+
// accepted step, linearly between the two nodes of x's region around it. At an interface x
|
|
2269
|
+
// belongs to both regions, so `region` (a name or index) picks the side; it defaults to the
|
|
2270
|
+
// first region that holds x.
|
|
2271
|
+
_probes(probes) {
|
|
2272
|
+
if (probes === undefined) return null;
|
|
2273
|
+
const { grid, regions, species } = this.model;
|
|
2274
|
+
const fail = (m) => {
|
|
2275
|
+
throw new DeviceError(m);
|
|
2276
|
+
};
|
|
2277
|
+
if (!Array.isArray(probes)) fail('advance: probes must be an array of { x, species, quantity, region }');
|
|
2278
|
+
const plan = probes.map((p, k) => {
|
|
2279
|
+
const path = `advance: probes[${k}]`;
|
|
2280
|
+
const i = species.findIndex((sp) => sp.name === p?.species);
|
|
2281
|
+
if (i < 0) fail(`${path}.species: no species ${JSON.stringify(p?.species)}`);
|
|
2282
|
+
const quantity = p.quantity ?? 'c';
|
|
2283
|
+
if (quantity !== 'c' && quantity !== 'V') fail(`${path}.quantity must be 'c' (mol/m³) or 'V' (the species voltage)`);
|
|
2284
|
+
if (quantity === 'V' && species[i].z === 0) fail(`${path}: '${p.species}' is neutral, so it has no voltage; read 'c'`);
|
|
2285
|
+
const x = grid.x, inside = (r) => p.x >= x[grid.regionStart[r]] && p.x <= x[grid.regionEnd[r]];
|
|
2286
|
+
let r;
|
|
2287
|
+
if (p.region !== undefined) {
|
|
2288
|
+
r = typeof p.region === 'number' ? p.region : regions.findIndex((reg) => reg.name === p.region);
|
|
2289
|
+
if (!(r >= 0 && r < regions.length)) fail(`${path}.region: no region ${JSON.stringify(p.region)}`);
|
|
2290
|
+
if (!inside(r)) fail(`${path}.x (${p.x} m) is outside region ${JSON.stringify(p.region)}`);
|
|
2291
|
+
} else {
|
|
2292
|
+
r = regions.findIndex((_, rr) => inside(rr));
|
|
2293
|
+
if (!(Number.isFinite(p.x) && r >= 0)) fail(`${path}.x must be a position in the device, in m (got ${p.x})`);
|
|
2294
|
+
}
|
|
2295
|
+
let g = grid.regionStart[r];
|
|
2296
|
+
while (g + 1 < grid.regionEnd[r] && x[g + 1] < p.x) g++;
|
|
2297
|
+
const w = x[g + 1] > x[g] ? (p.x - x[g]) / (x[g + 1] - x[g]) : 0;
|
|
2298
|
+
return { i, g, w, quantity };
|
|
2299
|
+
});
|
|
2300
|
+
const out = plan.map(() => []);
|
|
2301
|
+
const { n, M } = this;
|
|
2302
|
+
const at = (q, g) => {
|
|
2303
|
+
if (!this.present[g * n + q.i]) return NaN;
|
|
2304
|
+
if (q.quantity === 'c') return this.c[g * n + q.i];
|
|
2305
|
+
const o = this.blockOfNode[g] * M + 1 + q.i;
|
|
2306
|
+
return (this.VT * (this.u[o] + this.uLo[o])) / this.z[q.i];
|
|
2307
|
+
};
|
|
2308
|
+
return {
|
|
2309
|
+
out,
|
|
2310
|
+
read: () => plan.map((q) => (1 - q.w) * at(q, q.g) + q.w * at(q, q.g + 1)),
|
|
2311
|
+
push: (values) => values.forEach((v, k) => out[k].push(v)),
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2227
2315
|
// Largest difference from ref among the potentials that carry state, in thermal units.
|
|
2228
2316
|
_errorNorm(ref) {
|
|
2229
2317
|
const { n, M, u } = this;
|
package/src/sources.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// driftlet/kit: sources as plain definition data. Waveforms for a terminal's V or I, an injector
|
|
2
|
+
// port, and band-to-band recombination from a lifetime. Each returns an ordinary spec value.
|
|
3
|
+
|
|
4
|
+
import { DeviceError } from './errors.js';
|
|
5
|
+
|
|
6
|
+
const fail = (message) => {
|
|
7
|
+
throw new DeviceError(message);
|
|
8
|
+
};
|
|
9
|
+
const need = (ok, message) => ok || fail(message);
|
|
10
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A rectangular pulse, as a waveform for a terminal's `V` or `I`: `base` until `start`, then
|
|
14
|
+
* `base + amplitude` for `width`, then `base` again. With a `rise` > 0 its edges are ramps of
|
|
15
|
+
* that length, placed so the area stays `amplitude · width`; without one they're jumps, which
|
|
16
|
+
* the time stepping handles exactly (so a pulse with no rise must start after t = 0, the time a
|
|
17
|
+
* device starts at and solve() reads: it's off then).
|
|
18
|
+
* @param {{ start?: number, width: number, amplitude: number, rise?: number, base?: number }} opts s, s, V or A/m², s, V or A/m²
|
|
19
|
+
* @returns {{ t: number[], values: number[] }}
|
|
20
|
+
*/
|
|
21
|
+
export function pulse({ start = 0, width, amplitude, rise = 0, base = 0 } = {}) {
|
|
22
|
+
need(num(width) && width > 0, `pulse: width must be > 0 (s), got ${width}`);
|
|
23
|
+
need(num(amplitude), `pulse: amplitude must be a finite number (V or A/m²), got ${amplitude}`);
|
|
24
|
+
need(num(start) && start >= 0, `pulse: start must be ≥ 0 (s), got ${start}`);
|
|
25
|
+
need(num(rise) && rise >= 0 && rise <= width, `pulse: rise must be between 0 and the width (s), got ${rise}`);
|
|
26
|
+
need(num(base), `pulse: base must be a finite number, got ${base}`);
|
|
27
|
+
need(rise > 0 || start > 0, 'pulse: with no rise, start after t = 0, so the device starts (and solve() reads it) before the pulse');
|
|
28
|
+
const top = base + amplitude;
|
|
29
|
+
return { t: [start, start + rise, start + width, start + width + rise], values: [base, top, top, base] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A square wave, repeating: `high` for `duty · period`, then `low`, from t = 0.
|
|
34
|
+
* @param {{ high: number, low: number, period: number, duty?: number }} opts
|
|
35
|
+
* @returns {{ t: number[], values: number[], repeat: true }}
|
|
36
|
+
*/
|
|
37
|
+
export function square({ high, low, period, duty = 0.5 } = {}) {
|
|
38
|
+
need(num(high) && num(low), `square: high and low must be finite numbers, got ${high} and ${low}`);
|
|
39
|
+
need(num(period) && period > 0, `square: period must be > 0 (s), got ${period}`);
|
|
40
|
+
need(num(duty) && duty > 0 && duty < 1, `square: duty must be between 0 and 1, got ${duty}`);
|
|
41
|
+
const on = duty * period;
|
|
42
|
+
return { t: [0, on, on, period], values: [high, high, low, low], repeat: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A triangle wave, repeating: from `from` to `to` and back in each `period`, from t = 0 (a
|
|
47
|
+
* cyclic voltammogram's sweep, from its starting potential).
|
|
48
|
+
* @param {{ from: number, to: number, period: number }} opts
|
|
49
|
+
* @returns {{ t: number[], values: number[], repeat: true }}
|
|
50
|
+
*/
|
|
51
|
+
export function triangle({ from, to, period } = {}) {
|
|
52
|
+
need(num(from) && num(to), `triangle: from and to must be finite numbers, got ${from} and ${to}`);
|
|
53
|
+
need(num(period) && period > 0, `triangle: period must be > 0 (s), got ${period}`);
|
|
54
|
+
return { t: [0, period / 2, period], values: [from, to, from], repeat: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A ramp from `from` to `to` over `duration`, starting at `start`, constant on either side.
|
|
59
|
+
* @param {{ from: number, to: number, duration: number, start?: number }} opts
|
|
60
|
+
* @returns {{ t: number[], values: number[] }}
|
|
61
|
+
*/
|
|
62
|
+
export function ramp({ from, to, duration, start = 0 } = {}) {
|
|
63
|
+
need(num(from) && num(to), `ramp: from and to must be finite numbers, got ${from} and ${to}`);
|
|
64
|
+
need(num(duration) && duration > 0, `ramp: duration must be > 0 (s), got ${duration}`);
|
|
65
|
+
need(num(start) && start >= 0, `ramp: start must be ≥ 0 (s), got ${start}`);
|
|
66
|
+
return { t: [start, start + duration], values: [from, to] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A port that injects a species into a window of a region at a driven current `I` (A/m², a
|
|
71
|
+
* number or a waveform), spread evenly over the window. It's an ordinary port: driven by `I`,
|
|
72
|
+
* its terminal the species, linked by a conductance small enough that at the peak current the
|
|
73
|
+
* port sits `headroom` volts above the window's level of that species, so every node of the
|
|
74
|
+
* window takes the same share (like a current source). At I = 0 it still links the window,
|
|
75
|
+
* weakly.
|
|
76
|
+
* @param {{ name?: string, region: string | number, from: number, to: number, species: string,
|
|
77
|
+
* I: number | { t: number[], values: number[], repeat?: boolean }, headroom?: number }} opts
|
|
78
|
+
* @returns {object} a port, for the definition's `ports`
|
|
79
|
+
*/
|
|
80
|
+
export function injector({ name = 'injector', region, from, to, species, I, headroom = 2.5 } = {}) {
|
|
81
|
+
need(region !== undefined, 'injector: give the region (its name or index)');
|
|
82
|
+
need(num(from) && num(to) && to > from, `injector: from and to must bound the window, in m from the region's left end (got ${from}, ${to})`);
|
|
83
|
+
need(typeof species === 'string', 'injector: give the species it injects, e.g. species: \'h+\'');
|
|
84
|
+
need(num(headroom) && headroom > 0, `injector: headroom must be > 0 (V), got ${headroom}`);
|
|
85
|
+
const peak = num(I) ? Math.abs(I) : Array.isArray(I?.values) && I.values.every(num) ? Math.max(...I.values.map(Math.abs)) : NaN;
|
|
86
|
+
need(Number.isFinite(peak), 'injector: I must be a current (A/m²) or a waveform { t, values }');
|
|
87
|
+
const G = peak > 0 ? peak / ((to - from) * headroom) : 1; // S/m³
|
|
88
|
+
return { name, region, from, to, terminal: species, species: { [species]: { type: 'conductance', G } }, I };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Band-to-band recombination e⁻ + h⁺ ⇌ 0 in a material, from the minority-carrier lifetime it
|
|
93
|
+
* should give at a majority density: kf = 1/(τ · majority). (The rate is kf (np − n_i²), so τ is
|
|
94
|
+
* the low-injection lifetime of whichever carrier is the minority.)
|
|
95
|
+
* @param {{ material: string, tau: number, majority: number }} opts material name, s, mol/m³
|
|
96
|
+
* @returns {{ equation: string, kf: Record<string, number> }} a bulk reaction
|
|
97
|
+
*/
|
|
98
|
+
export function recombination({ material, tau, majority } = {}) {
|
|
99
|
+
need(typeof material === 'string', 'recombination: give the material, by name');
|
|
100
|
+
need(num(tau) && tau > 0, `recombination: tau must be > 0 (s), got ${tau}`);
|
|
101
|
+
need(num(majority) && majority > 0, `recombination: majority must be the majority carrier density > 0 (mol/m³; units.perCm3 converts), got ${majority}`);
|
|
102
|
+
return { equation: 'e- + h+ = 0', kf: { [material]: 1 / (tau * majority) } };
|
|
103
|
+
}
|
package/src/statistics.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { FARADAY, EPS0, AVOGADRO } from './constants.js';
|
|
18
18
|
import { fermiHalf, fermiMinusHalf } from './fermi.js';
|
|
19
|
+
import { powi } from './pow.js';
|
|
19
20
|
import { SolverError } from './solver.js';
|
|
20
21
|
|
|
21
22
|
const MODEL_TYPES = ['ideal', 'fermi-dirac', 'lattice', 'redlich-kister', 'debye-huckel', 'insertion', 'custom'];
|
|
@@ -189,7 +190,7 @@ function fermiDiracModel(idx, cRef, order) {
|
|
|
189
190
|
const r = target[a] / cRef[a];
|
|
190
191
|
// ln 𝓕(ζ) is increasing with slope 𝓕′/𝓕 ∈ (0, 1]
|
|
191
192
|
const f = (y) => ({ v: Math.log(F(y)), d: dF(y) / F(y) });
|
|
192
|
-
const guess = r < 1 ? Math.log(r) : order === 0.5 ? Math.cbrt((0.75 * Math.sqrt(Math.PI) * r
|
|
193
|
+
const guess = r < 1 ? Math.log(r) : order === 0.5 ? Math.cbrt(powi(0.75 * Math.sqrt(Math.PI) * r, 2)) : r;
|
|
193
194
|
zeta[a] = solveIncreasing(f, Math.log(r), Math.log(r) - 1, Math.max(guess, Math.log(r)) + 10, guess);
|
|
194
195
|
}
|
|
195
196
|
},
|
|
@@ -264,7 +265,7 @@ function debyeHuckelModel(idx, cRef, z, epsr, a, RT) {
|
|
|
264
265
|
let q = 0;
|
|
265
266
|
for (let b = 0; b < k; b++) q += s[b] * c[b];
|
|
266
267
|
const kappa = Math.sqrt((FARADAY * FARADAY * q) / (eps * RT));
|
|
267
|
-
const A = kappa > 0 ? (lB * FARADAY * FARADAY) / (4 * eps * RT * kappa * (1 + kappa * a)
|
|
268
|
+
const A = kappa > 0 ? (lB * FARADAY * FARADAY) / (4 * eps * RT * kappa * (1 + kappa * a) * (1 + kappa * a)) : 0;
|
|
268
269
|
let s2c = 0;
|
|
269
270
|
for (let b = 0; b < k; b++) s2c += s[b] * s[b] * c[b];
|
|
270
271
|
for (let b = 0; b < k; b++) lng[b] = (-s[b] * lB * kappa) / (2 * (1 + kappa * a));
|
|
@@ -425,7 +426,7 @@ function choleskyOk(K, k) {
|
|
|
425
426
|
const L = Float64Array.from(K);
|
|
426
427
|
for (let j = 0; j < k; j++) {
|
|
427
428
|
let d = L[j * k + j];
|
|
428
|
-
for (let p = 0; p < j; p++) d -= L[j * k + p]
|
|
429
|
+
for (let p = 0; p < j; p++) d -= L[j * k + p] * L[j * k + p];
|
|
429
430
|
if (!(d > 0)) return false;
|
|
430
431
|
d = Math.sqrt(d);
|
|
431
432
|
L[j * k + j] = d;
|
package/src/types.js
CHANGED
|
@@ -57,8 +57,9 @@
|
|
|
57
57
|
* @property {number} length m
|
|
58
58
|
* @property {string} [name]
|
|
59
59
|
* @property {number} [fixedCharge] immobile charge density, C/m³
|
|
60
|
-
* @property {Record<string, number>} [c0] initial concentrations,
|
|
61
|
-
* not connected to a contact: it fixes the amount they conserve)
|
|
60
|
+
* @property {Record<string, number | { x: number[], c: number[] }>} [c0] initial concentrations,
|
|
61
|
+
* mol/m³ (required for species not connected to a contact: it fixes the amount they conserve);
|
|
62
|
+
* or a profile against the device's x (m), piecewise linear; either is the starting state
|
|
62
63
|
* @property {number} [velocity] imposed flow toward +x, m/s, carrying every mobile species
|
|
63
64
|
* @property {number} [mixing] eddy (turbulent) mixing diffusivity, m²/s; current-free
|
|
64
65
|
* @property {GridOptions} [grid]
|
|
@@ -204,8 +205,9 @@
|
|
|
204
205
|
* @property {number} time s
|
|
205
206
|
* @property {boolean} [done] advance(): whether tEnd was reached (false if the frame budget ran out)
|
|
206
207
|
* @property {number} [rejected] advance(): steps rejected by error control or Newton failure
|
|
207
|
-
* @property {{ t: number[], current: number[], voltage: number[] }} [trace] advance():
|
|
208
|
-
* current (A/m², toward +x) and voltage after each accepted step
|
|
208
|
+
* @property {{ t: number[], current: number[], voltage: number[], probes?: number[][] }} [trace] advance():
|
|
209
|
+
* terminal current (A/m², toward +x) and voltage after each accepted step, and each probe's
|
|
210
|
+
* reading (mol/m³, or V for quantity 'V'), interpolated between the nodes around its x
|
|
209
211
|
*/
|
|
210
212
|
|
|
211
213
|
/**
|
package/types/constants.d.ts
CHANGED
|
@@ -30,4 +30,14 @@ export const units: Readonly<{
|
|
|
30
30
|
um: (x: any) => number;
|
|
31
31
|
/** cm²/s → m²/s */
|
|
32
32
|
cm2PerS: (x: any) => number;
|
|
33
|
+
/** mol/m³ → mol/L */
|
|
34
|
+
toMolar: (x: any) => number;
|
|
35
|
+
/** mol/m³ → particles per cm³ */
|
|
36
|
+
toPerCm3: (x: any) => number;
|
|
37
|
+
/** m → nm */
|
|
38
|
+
toNm: (x: any) => number;
|
|
39
|
+
/** m → µm */
|
|
40
|
+
toUm: (x: any) => number;
|
|
41
|
+
/** m²/s → cm²/s */
|
|
42
|
+
toCm2PerS: (x: any) => number;
|
|
33
43
|
}>;
|