driftlet 0.16.4 → 1.0.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 +3 -3
- package/docs/device.md +28 -14
- package/docs/kit.md +3 -1
- package/docs/numerics.md +12 -0
- package/docs/statistics.md +2 -1
- package/docs/templates.md +260 -0
- package/llms.txt +106 -300
- package/package.json +15 -3
- package/src/channels.js +6 -4
- package/src/checks.js +7 -2
- package/src/data.js +5 -1
- package/src/device.js +10 -4
- package/src/devices.js +2 -0
- package/src/equation.js +2 -1
- package/src/errors.js +33 -0
- package/src/grid.js +12 -0
- package/src/index.js +17 -7
- package/src/junction.js +3 -1
- package/src/kit.js +1 -1
- package/src/live.js +2 -0
- package/src/particles.js +2 -0
- package/src/plot.js +4 -0
- package/src/polarization.js +4 -3
- package/src/recorder.js +2 -1
- package/src/solution.js +20 -5
- package/src/solver.js +70 -16
- package/src/sources.js +8 -1
- package/src/stack.js +14 -1
- package/src/statistics.js +1 -1
- package/src/traces.js +3 -0
- package/src/types.js +5 -2
- package/types/channels.d.ts +3 -3
- package/types/checks.d.ts +1 -1
- package/types/data.d.ts +3 -3
- package/types/devices.d.ts +1 -1
- package/types/equation.d.ts +1 -1
- package/types/errors.d.ts +8 -0
- package/types/index.d.ts +2 -5
- package/types/junction.d.ts +2 -2
- package/types/kit.d.ts +1 -1
- package/types/live.d.ts +1 -1
- package/types/particles.d.ts +1 -1
- package/types/plot.d.ts +3 -3
- package/types/polarization.d.ts +1 -1
- package/types/recorder.d.ts +1 -1
- package/types/solver.d.ts +2 -0
- package/types/sources.d.ts +7 -7
- package/types/traces.d.ts +1 -1
- package/types/types.d.ts +11 -2
package/src/solver.js
CHANGED
|
@@ -373,7 +373,8 @@ export class Solver {
|
|
|
373
373
|
// φ is undefined in a conductor or an insertion host, and in a charge-free region (no
|
|
374
374
|
// charged species) unless its electrostatic cluster, the regions joined to it by faces that
|
|
375
375
|
// aren't neutral, reaches something that fixes φ: a region with charged species, a gate or
|
|
376
|
-
// pinned contact, or a
|
|
376
|
+
// pinned contact, or a face that isn't neutral to a conductor or to a strictly neutral region
|
|
377
|
+
// with ions. (Else φ there is a free constant.)
|
|
377
378
|
this.phiUndefined = new Uint8Array(nNodes);
|
|
378
379
|
const charged = (mat) => species.some((sp, i) => mat.present[i] && sp.z !== 0);
|
|
379
380
|
const pins = (ct) => ct.phi.type === 'capacitive' || ct.phi.type === 'pinned';
|
|
@@ -385,8 +386,11 @@ export class Solver {
|
|
|
385
386
|
while (r1 + 1 < regions.length && model.interfaces[r1].phi.type !== 'neutral' && !free(r1 + 1)) r1++;
|
|
386
387
|
let anchor = (r0 === 0 && pins(contacts.left)) || (r1 === regions.length - 1 && pins(contacts.right));
|
|
387
388
|
for (let r = r0; r <= r1; r++) if (charged(materials[regions[r].material])) anchor = true;
|
|
388
|
-
|
|
389
|
-
|
|
389
|
+
// A neighbour across a face that isn't neutral: a conductor (a gate), or a strictly neutral
|
|
390
|
+
// region whose ions fix its φ.
|
|
391
|
+
const fixes = (r) => materials[regions[r].material].conductor || (!materials[regions[r].material].phiFree && charged(materials[regions[r].material]));
|
|
392
|
+
if (r0 > 0 && model.interfaces[r0 - 1].phi.type !== 'neutral' && fixes(r0 - 1)) anchor = true;
|
|
393
|
+
if (r1 < regions.length - 1 && model.interfaces[r1].phi.type !== 'neutral' && fixes(r1 + 1)) anchor = true;
|
|
390
394
|
for (let r = r0; r <= r1; r++) anchored[r] = anchor ? 1 : 2;
|
|
391
395
|
r0 = r1;
|
|
392
396
|
}
|
|
@@ -585,6 +589,16 @@ export class Solver {
|
|
|
585
589
|
const mat = materials[regions[grid.nodeRegion[g]].material], law = contacts[k === 0 ? 'left' : 'right'].phi.type;
|
|
586
590
|
if (!mat.conductor && mat.epsr === 0 && (law === 'bulk' || law === 'neutral') && !this.chargeNode[g] && !this.phiUndefined[g] && this.nodeIdeal[g]) this.lateStorage[g] = 1;
|
|
587
591
|
});
|
|
592
|
+
// An end node's species held by a port (its contact blocks it, so the level is the port's):
|
|
593
|
+
// its row is the port's, storage and all, and the contact passes none of it.
|
|
594
|
+
this.portHeldEnd = new Uint8Array(2 * n);
|
|
595
|
+
model.ports.forEach((port) => {
|
|
596
|
+
[0, nNodes - 1].forEach((g, k) => {
|
|
597
|
+
if (!port.nodes.includes(g)) return;
|
|
598
|
+
const ct = contacts[k === 0 ? 'left' : 'right'];
|
|
599
|
+
for (let i = 0; i < n; i++) if (port.species[i].type === 'equilibrium' && ct.species[i].type === 'blocked') this.portHeldEnd[k * n + i] = 1;
|
|
600
|
+
});
|
|
601
|
+
});
|
|
588
602
|
this.combining = false;
|
|
589
603
|
this.bookkeeping = regions.map((_, r) => {
|
|
590
604
|
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r], nodes = [], segs = [];
|
|
@@ -1895,6 +1909,14 @@ export class Solver {
|
|
|
1895
1909
|
// toward their mixed potential.
|
|
1896
1910
|
for (const k of this.floating) {
|
|
1897
1911
|
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1912
|
+
const gate = t.kind === 'contact' ? model.contacts[t.side].phi : null;
|
|
1913
|
+
if (gate?.type === 'capacitive') {
|
|
1914
|
+
// A gate contact likewise: at its zero-charge level from the φ beside it (a pinned
|
|
1915
|
+
// one's charge is the field's, already zero against a neutral start).
|
|
1916
|
+
const g = t.side === 'left' ? 0 : this.nNodes - 1;
|
|
1917
|
+
if (!this.phiUndefined[g]) this.termV[k] = gate.zeroCharge + this.VT * u[this.blockOfNode[g] * M];
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1898
1920
|
if (!port?.capacitance) continue;
|
|
1899
1921
|
let w = 0, sum = 0;
|
|
1900
1922
|
port.nodes.forEach((g, j) => {
|
|
@@ -1905,6 +1927,22 @@ export class Solver {
|
|
|
1905
1927
|
});
|
|
1906
1928
|
if (w > 0) this.termV[k] = port.capacitance.zeroCharge + sum / w;
|
|
1907
1929
|
}
|
|
1930
|
+
// A floating port linked to its carrier (no reactions, no double layer: a wire into an
|
|
1931
|
+
// insertion host) starts at that carrier's level across its window, not at 0 V.
|
|
1932
|
+
for (const k of this.floating) {
|
|
1933
|
+
const t = this.terms[k], port = t.kind === 'port' ? model.ports[t.index] : null;
|
|
1934
|
+
const i = port?.terminal, link = i === null || i === undefined ? null : port.species[i];
|
|
1935
|
+
if (!port || port.reactions.length > 0 || port.capacitance || !link || link.type === 'blocked' || z[i] === 0) continue;
|
|
1936
|
+
let w = 0, sum = 0;
|
|
1937
|
+
port.nodes.forEach((g, j) => {
|
|
1938
|
+
const a = this.model.grid.vol[g] * (this.portArea[t.index]?.[j] ?? 1); // (a window with no area: by volume)
|
|
1939
|
+
w += a;
|
|
1940
|
+
sum += (a * this.VT * u[this.blockOfNode[g] * M + 1 + i]) / z[i];
|
|
1941
|
+
});
|
|
1942
|
+
// (Levels at the start mean something only against a held terminal's, as for a contact.)
|
|
1943
|
+
const held = [0, 1].find((h) => !this.floating.includes(h) && Number.isFinite(level(this.terms[h].side)));
|
|
1944
|
+
if (w > 0 && held !== undefined) this.termV[k] = this.termV[held] + sum / w - (link.offset ?? 0) - level(this.terms[held].side);
|
|
1945
|
+
}
|
|
1908
1946
|
// Gates start open as far as the starting voltage across their face holds them, α/(α + β).
|
|
1909
1947
|
for (const gt of this.gateList) {
|
|
1910
1948
|
const { gate, o } = gt, V = this._gateVoltage(gt), a = gateRate(gate.alpha, V)[0], b = gateRate(gate.beta, V)[0];
|
|
@@ -1952,9 +1990,9 @@ export class Solver {
|
|
|
1952
1990
|
}
|
|
1953
1991
|
const o = b * M + (p.side === 2 ? 1 + n + p.s : 1 + p.i), e = p.nu < 0 ? -p.nu * (1 - rx.alpha) : p.nu * rx.alpha;
|
|
1954
1992
|
a -= p.nu * (u[o] + uLo[o]);
|
|
1955
|
-
pref *= p.side === 2 ?
|
|
1993
|
+
pref *= p.side === 2 ? powr(th[g * nSurf + p.s], e) : powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
1956
1994
|
}
|
|
1957
|
-
if (rx.
|
|
1995
|
+
if (rx.sites) pref *= powr(this.th0[g], rx.sites);
|
|
1958
1996
|
return pref * bvFactor(a, rx.alpha).g;
|
|
1959
1997
|
}
|
|
1960
1998
|
|
|
@@ -2177,6 +2215,11 @@ export class Solver {
|
|
|
2177
2215
|
if (f >= 0) this._j(b, r, this.blockOfFace[f], 0, (this.sheetSign[g] * this.model.grid.area[g]) / (z[im] * FARADAY * dt));
|
|
2178
2216
|
}
|
|
2179
2217
|
|
|
2218
|
+
// Whether a port holds species i at node g, a device end node (see portHeldEnd).
|
|
2219
|
+
_portHeldAt(g, i) {
|
|
2220
|
+
return g === 0 ? this.portHeldEnd[i] === 1 : g === this.nNodes - 1 && this.portHeldEnd[this.n + i] === 1;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2180
2223
|
// Dilute nodes g0…g1 (ideal statistics, c = c_ref e^ζ): storage of each species and the space
|
|
2181
2224
|
// charge in the Gauss row, written straight into the diagonal blocks by local index. A
|
|
2182
2225
|
// dielectric is the case with no species.
|
|
@@ -2196,7 +2239,7 @@ export class Solver {
|
|
|
2196
2239
|
for (let i = 0; i < n; i++) {
|
|
2197
2240
|
const k = g * n + i;
|
|
2198
2241
|
if (!present[k]) continue;
|
|
2199
|
-
const ck = c[k], l = loc[lb + 1 + i], stores = !(late && z[i] !== 0);
|
|
2242
|
+
const ck = c[k], l = loc[lb + 1 + i], stores = !(late && z[i] !== 0 && !this._portHeldAt(g, i));
|
|
2200
2243
|
if (stores) res[R[lb + 1 + i]] += (v * (ck - cOld[k])) / dt;
|
|
2201
2244
|
q += F * z[i] * ck;
|
|
2202
2245
|
dq += F * z[i] * z[i] * ck;
|
|
@@ -3077,18 +3120,19 @@ export class Solver {
|
|
|
3077
3120
|
aLo -= p.nu * uLo[o];
|
|
3078
3121
|
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
3079
3122
|
if (p.side === 2) {
|
|
3080
|
-
// a surface species at
|
|
3081
|
-
pref *=
|
|
3123
|
+
// a surface species at its coverage θ = θ₀e^ζ, with ∂ln θ_s/∂η_x = δ_sx − θ_x
|
|
3124
|
+
pref *= powr(th[g * nSurf + p.s], e);
|
|
3082
3125
|
d[1 + n + p.s] += e;
|
|
3126
|
+
for (let x = 0; x < surf.length; x++) d[1 + n + x] -= e * th[g * nSurf + x];
|
|
3083
3127
|
} else {
|
|
3084
3128
|
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
3085
3129
|
this._dlnc(g, p.i, e, d);
|
|
3086
3130
|
}
|
|
3087
3131
|
}
|
|
3088
|
-
if (rx.
|
|
3089
|
-
//
|
|
3090
|
-
for (let x = 0; x < surf.length; x++) d[1 + n + x] -= th[g * nSurf + x];
|
|
3091
|
-
pref *= this.th0[g];
|
|
3132
|
+
if (rx.sites) {
|
|
3133
|
+
// the bare sites its transition state holds: θ₀ = 1 − Σθ, with ∂ln θ₀/∂η_s = −θ_s
|
|
3134
|
+
for (let x = 0; x < surf.length; x++) d[1 + n + x] -= rx.sites * th[g * nSurf + x];
|
|
3135
|
+
pref *= powr(this.th0[g], rx.sites);
|
|
3092
3136
|
}
|
|
3093
3137
|
const { g: bv, gp } = bvFactor(aHi + aLo, al);
|
|
3094
3138
|
const rate = pref * bv;
|
|
@@ -3220,8 +3264,9 @@ export class Solver {
|
|
|
3220
3264
|
// passes, so their flux is zero.)
|
|
3221
3265
|
const closedIons = this.combining && dyn && this.chargeNode[g] === 1;
|
|
3222
3266
|
let I = 0;
|
|
3267
|
+
const held = this.portHeldEnd.subarray(k * n, (k + 1) * n);
|
|
3223
3268
|
for (let i = 0; i < n; i++) {
|
|
3224
|
-
const active = this.loc[b * M + 1 + i] >= 0 && !(closedIons && z[i] !== 0);
|
|
3269
|
+
const active = this.loc[b * M + 1 + i] >= 0 && !(closedIons && z[i] !== 0) && !held[i];
|
|
3225
3270
|
const nIn = active ? res[R[b * M + 1 + i]] : 0;
|
|
3226
3271
|
flux[i] = sgn * nIn; // (toward +x)
|
|
3227
3272
|
if (active && z[i] !== 0) {
|
|
@@ -3234,7 +3279,7 @@ export class Solver {
|
|
|
3234
3279
|
if (this.combining && dyn && this.lateStorage[g] === 1) {
|
|
3235
3280
|
const v = model.grid.vol[g], { cOld } = this;
|
|
3236
3281
|
for (let i = 0; i < n; i++) {
|
|
3237
|
-
if (z[i] === 0 || this.loc[b * M + 1 + i] < 0) continue;
|
|
3282
|
+
if (z[i] === 0 || this.loc[b * M + 1 + i] < 0 || held[i]) continue;
|
|
3238
3283
|
const ci = c[g * n + i], st = (v * (ci - cOld[g * n + i])) / dt;
|
|
3239
3284
|
res[R[b * M + 1 + i]] += st;
|
|
3240
3285
|
flux[i] += sgn * st;
|
|
@@ -3967,8 +4012,12 @@ export class Solver {
|
|
|
3967
4012
|
const trace = { t: [], current: [], voltage: [], ...(sample ? { probes: sample.out } : {}) };
|
|
3968
4013
|
let steps = 0, rejected = 0, iterations = 0, failed = false;
|
|
3969
4014
|
// Already there (an animation frame with no time to add): nothing to do, and the step size
|
|
3970
|
-
// carried to the next call stays as it was.
|
|
3971
|
-
|
|
4015
|
+
// carried to the next call stays as it was. A time already passed, or reached again outside
|
|
4016
|
+
// an animation, is most likely advance(dt) read as a duration: say so.
|
|
4017
|
+
if (!(tEnd > this.time)) {
|
|
4018
|
+
const relative = tEnd < this.time || (this.time > 0 && opts.budgetMs === undefined);
|
|
4019
|
+
return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace, ...(relative ? { stopped: `advance(${tEnd}) did nothing: ${absolute}. To run for a further dt, advance(sol.time + dt)` } : {}) };
|
|
4020
|
+
}
|
|
3972
4021
|
const t0 = this.time;
|
|
3973
4022
|
let dt = this.dtNext ?? opts.dt0 ?? (tEnd - this.time) * 1e-4;
|
|
3974
4023
|
if (!(dt > 0)) dt = (tEnd - this.time) * 1e-4;
|
|
@@ -4846,6 +4895,11 @@ export class Solver {
|
|
|
4846
4895
|
}
|
|
4847
4896
|
mu[name].re[g] = RT * xr[b * M + 1 + i];
|
|
4848
4897
|
mu[name].im[g] = RT * xi[b * M + 1 + i];
|
|
4898
|
+
if (this.nodeConductor[g] >= 0) {
|
|
4899
|
+
// a metal has a Fermi level but no concentration (as a solution reports it)
|
|
4900
|
+
c[name].re[g] = c[name].im[g] = NaN;
|
|
4901
|
+
continue;
|
|
4902
|
+
}
|
|
4849
4903
|
// δc_i = Σ_j K_ij (δη_j − z_j δφ̂)
|
|
4850
4904
|
let sr = 0, si = 0;
|
|
4851
4905
|
for (let j = 0; j < n; j++) {
|
package/src/sources.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// driftlet/kit: sources as plain definition data. Waveforms for a terminal's V or I, an injector
|
|
2
2
|
// port, and band-to-band recombination from a lifetime. Each returns an ordinary spec value.
|
|
3
3
|
|
|
4
|
-
import { DeviceError } from './errors.js';
|
|
4
|
+
import { DeviceError, checkOptions } from './errors.js';
|
|
5
5
|
|
|
6
6
|
const fail = (message) => {
|
|
7
7
|
throw new DeviceError(message);
|
|
@@ -19,6 +19,7 @@ const num = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
|
19
19
|
* @returns {{ t: number[], values: number[] }}
|
|
20
20
|
*/
|
|
21
21
|
export function pulse({ start = 0, width, amplitude, rise = 0, base = 0 } = {}) {
|
|
22
|
+
checkOptions('pulse', arguments[0], ['start', 'width', 'amplitude', 'rise', 'base']);
|
|
22
23
|
need(num(width) && width > 0, `pulse: width must be > 0 (s), got ${width}`);
|
|
23
24
|
need(num(amplitude), `pulse: amplitude must be a finite number (V or A/m²), got ${amplitude}`);
|
|
24
25
|
need(num(start) && start >= 0, `pulse: start must be ≥ 0 (s), got ${start}`);
|
|
@@ -35,6 +36,7 @@ export function pulse({ start = 0, width, amplitude, rise = 0, base = 0 } = {})
|
|
|
35
36
|
* @returns {{ t: number[], values: number[], repeat: true }}
|
|
36
37
|
*/
|
|
37
38
|
export function square({ high, low, period, duty = 0.5 } = {}) {
|
|
39
|
+
checkOptions('square', arguments[0], ['high', 'low', 'period', 'duty']);
|
|
38
40
|
need(num(high) && num(low), `square: high and low must be finite numbers, got ${high} and ${low}`);
|
|
39
41
|
need(num(period) && period > 0, `square: period must be > 0 (s), got ${period}`);
|
|
40
42
|
need(num(duty) && duty > 0 && duty < 1, `square: duty must be between 0 and 1, got ${duty}`);
|
|
@@ -49,6 +51,7 @@ export function square({ high, low, period, duty = 0.5 } = {}) {
|
|
|
49
51
|
* @returns {{ t: number[], values: number[], repeat: true }}
|
|
50
52
|
*/
|
|
51
53
|
export function triangle({ from, to, period } = {}) {
|
|
54
|
+
checkOptions('triangle', arguments[0], ['from', 'to', 'period']);
|
|
52
55
|
need(num(from) && num(to), `triangle: from and to must be finite numbers, got ${from} and ${to}`);
|
|
53
56
|
need(num(period) && period > 0, `triangle: period must be > 0 (s), got ${period}`);
|
|
54
57
|
return { t: [0, period / 2, period], values: [from, to, from], repeat: true };
|
|
@@ -60,6 +63,7 @@ export function triangle({ from, to, period } = {}) {
|
|
|
60
63
|
* @returns {{ t: number[], values: number[] }}
|
|
61
64
|
*/
|
|
62
65
|
export function ramp({ from, to, duration, start = 0 } = {}) {
|
|
66
|
+
checkOptions('ramp', arguments[0], ['from', 'to', 'duration', 'start']);
|
|
63
67
|
need(num(from) && num(to), `ramp: from and to must be finite numbers, got ${from} and ${to}`);
|
|
64
68
|
need(num(duration) && duration > 0, `ramp: duration must be > 0 (s), got ${duration}`);
|
|
65
69
|
need(num(start) && start >= 0, `ramp: start must be ≥ 0 (s), got ${start}`);
|
|
@@ -78,6 +82,7 @@ export function ramp({ from, to, duration, start = 0 } = {}) {
|
|
|
78
82
|
* @returns {object} a port, for the definition's `ports`
|
|
79
83
|
*/
|
|
80
84
|
export function injector({ name = 'injector', region, from, to, species, I, headroom = 2.5 } = {}) {
|
|
85
|
+
checkOptions('injector', arguments[0], ['name', 'region', 'from', 'to', 'species', 'I', 'headroom']);
|
|
81
86
|
need(region !== undefined, 'injector: give the region (its name or index)');
|
|
82
87
|
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
88
|
need(typeof species === 'string', 'injector: give the species it injects, e.g. species: \'h+\'');
|
|
@@ -96,6 +101,7 @@ export function injector({ name = 'injector', region, from, to, species, I, head
|
|
|
96
101
|
* @returns {{ equation: string, kf: Record<string, number> }} a bulk reaction
|
|
97
102
|
*/
|
|
98
103
|
export function recombination({ material, tau, majority } = {}) {
|
|
104
|
+
checkOptions('recombination', arguments[0], ['material', 'tau', 'majority']);
|
|
99
105
|
need(typeof material === 'string', 'recombination: give the material, by name');
|
|
100
106
|
need(num(tau) && tau > 0, `recombination: tau must be > 0 (s), got ${tau}`);
|
|
101
107
|
need(num(majority) && majority > 0, `recombination: majority must be the majority carrier density > 0 (mol/m³; units.perCm3 converts), got ${majority}`);
|
|
@@ -117,6 +123,7 @@ export function recombination({ material, tau, majority } = {}) {
|
|
|
117
123
|
* @returns {{ equation: string, fixed: Record<string, number>, kf: Record<string, { x: number[], values: number[] }> }} a bulk reaction
|
|
118
124
|
*/
|
|
119
125
|
export function photogeneration({ material, flux, alpha, mu, from = 0, to, makes = 'e- + h+' } = {}) {
|
|
126
|
+
checkOptions('photogeneration', arguments[0], ['material', 'flux', 'alpha', 'mu', 'from', 'to', 'makes']);
|
|
120
127
|
need(typeof material === 'string', 'photogeneration: give the material, by name');
|
|
121
128
|
need(num(flux) && flux >= 0, `photogeneration: flux must be the photon flux ≥ 0 entering the material (mol/(m²·s)), got ${flux}`);
|
|
122
129
|
need(num(alpha) && alpha > 0, `photogeneration: alpha must be the absorption coefficient > 0 (1/m; 1/cm × 100), got ${alpha}`);
|
package/src/stack.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// that you didn't write: it only moves things to where the definition keeps them, and converts
|
|
5
5
|
// the kit's doping shorthand (donors and acceptors).
|
|
6
6
|
|
|
7
|
-
import { DeviceError } from './errors.js';
|
|
7
|
+
import { DeviceError, checkOptions } from './errors.js';
|
|
8
8
|
import { FARADAY } from './constants.js';
|
|
9
9
|
|
|
10
10
|
const fail = (message) => {
|
|
@@ -158,8 +158,21 @@ export function build(def) {
|
|
|
158
158
|
}
|
|
159
159
|
if (pendingFace) fail(`${at(pendingFace.k)}: a face after the last layer (the contact is the boundary there)`);
|
|
160
160
|
if (regions.length === 0) fail('build.stack has no layers');
|
|
161
|
+
// A contact holding a carrier its layer lacks: most often ohmic()'s default on a metal.
|
|
162
|
+
for (const [k, end] of [[0, regions[0]], [stack.length - 1, regions[regions.length - 1]]]) {
|
|
163
|
+
const held = isObject(stack[k].species) ? Object.keys(stack[k].species).filter((s) => stack[k].species[s] !== 'blocked') : [];
|
|
164
|
+
const mat = materials[end.material];
|
|
165
|
+
if (!isObject(mat)) continue;
|
|
166
|
+
const has = (s) => (isObject(mat.conductor) ? mat.conductor.species === s : isObject(mat.species) && mat.species[s] !== undefined);
|
|
167
|
+
const lacks = held.filter((s) => !has(s));
|
|
168
|
+
if (lacks.length === 0) continue;
|
|
169
|
+
const kept = held.filter(has);
|
|
170
|
+
const hint = stack[k].phi === 'bulk' && kept.length > 0 ? `: write ohmic(V, [${kept.map((s) => `'${s}'`).join(', ')}]) (ohmic() holds e- and h+ unless told${isObject(mat.conductor) ? '; a metal has only its carrier' : ''})` : '';
|
|
171
|
+
fail(`${at(k)}: the contact holds ${lacks.map((s) => `'${s}'`).join(' and ')}, which its layer's material '${end.material}' doesn't have${hint}`);
|
|
172
|
+
}
|
|
161
173
|
|
|
162
174
|
function region(item, path) {
|
|
175
|
+
checkOptions(path, item, ['name', 'material', 'length', 'fixedCharge', 'c0', 'velocity', 'mixing', 'grid', 'donors', 'acceptors']);
|
|
163
176
|
const { donors, acceptors, ...r } = item;
|
|
164
177
|
if (donors !== undefined || acceptors !== undefined) {
|
|
165
178
|
if (r.fixedCharge !== undefined) fail(`${path}: give doping as donors and acceptors, or fixedCharge, not both`);
|
package/src/statistics.js
CHANGED
|
@@ -157,7 +157,7 @@ function tableOccupancy(xs, zs, fail) {
|
|
|
157
157
|
const x = k / 4000;
|
|
158
158
|
const q = r(x);
|
|
159
159
|
if (x > 0 && x < 1 && !(1 + x * (1 - x) * q.d > 1e-9)) {
|
|
160
|
-
fail(`ocv:
|
|
160
|
+
fail(`ocv: near x = ${x.toFixed(3)} the curve is flat or rising against the ideal-solution slope, (RT/F) ln(x/(1−x)), as on a two-phase plateau (not supported): more points alone won't help; give it some of that slope, adding −a (RT/F) ln(x/(1−x)) to E (a = 0.1 passes at points 0.01 apart, less with points closer near the ends; docs/statistics.md)`);
|
|
161
161
|
}
|
|
162
162
|
rmin = Math.min(rmin, q.v);
|
|
163
163
|
rmax = Math.max(rmax, q.v);
|
package/src/traces.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// range. Plain data, for any plotting toolkit (driftlet/plot draws it as SVG).
|
|
4
4
|
|
|
5
5
|
import { level } from './equation.js';
|
|
6
|
+
import { DeviceError, checkOptions } from './errors.js';
|
|
6
7
|
|
|
7
8
|
const finite = (a) => {
|
|
8
9
|
for (let k = 0; k < a.length; k++) if (Number.isFinite(a[k])) return true;
|
|
@@ -78,6 +79,8 @@ export function typeset(name, z) {
|
|
|
78
79
|
* faces: number[], range: [number, number] }}
|
|
79
80
|
*/
|
|
80
81
|
export function traces(sol, { species, standard = true, phi = false, energy = false, levels = [], labels = {}, shifts = {} } = {}) {
|
|
82
|
+
checkOptions('traces', arguments[1], ['species', 'standard', 'phi', 'energy', 'levels', 'labels', 'shifts']);
|
|
83
|
+
if (!Array.isArray(levels)) throw new DeviceError("traces: levels must be a list, [{ half }, …], of the half-reactions whose levels to draw");
|
|
81
84
|
const names = Object.keys(sol.V);
|
|
82
85
|
const role = (name) => speciesRole(sol, name).role;
|
|
83
86
|
const slotOf = (name) => speciesRole(sol, name).slot;
|
package/src/types.js
CHANGED
|
@@ -163,7 +163,8 @@
|
|
|
163
163
|
* species and the terminal species at the port's level
|
|
164
164
|
* @property {Record<string, { mu0: number, capacity: number, theta0?: number }>} [surface] with reactions: species
|
|
165
165
|
* on the electrode's sites, Langmuir (μ = μ° + RT ln(θ/θ₀)), capacity in mol of sites per m² of electrode,
|
|
166
|
-
* shared;
|
|
166
|
+
* shared; reactions run on Langmuir sites (what fills sites needs bare ones), and one with `bare: true`
|
|
167
|
+
* needs a bare site even if it fills none (θ₀ = 1 − Σθ)
|
|
167
168
|
* @property {{ C: number, zeroCharge?: number }} [capacitance] a capacitance spread through the window (F/m² of
|
|
168
169
|
* electrode): σ = C (V − zeroCharge − φ) on the port's side, aσ in each node's charge balance; a gate
|
|
169
170
|
* along a channel, a double layer
|
|
@@ -218,7 +219,8 @@
|
|
|
218
219
|
* @property {Record<string, { V: number, current: number }>} terminals each terminal (left, right,
|
|
219
220
|
* then the ports by name): its voltage, and its current into the device (A/m²)
|
|
220
221
|
* @property {{ left: ContactResult, right: ContactResult }} contacts
|
|
221
|
-
* @property {{ left?: { V: number, D: number, charge: number }, right?: { V: number, D: number, charge: number } }}
|
|
222
|
+
* @property {{ left?: { V: number, D: number, charge: number }, right?: { V: number, D: number, charge: number } }} plates the charge on a gate or
|
|
223
|
+
* Stern plate at a capacitive contact
|
|
222
224
|
* @property {{ name: string, V: number, flux: Record<string, number>, current: number, x?: Float64Array,
|
|
223
225
|
* area?: Float64Array, rates?: Float64Array[], coverage?: Record<string, Float64Array>, bare?: Float64Array,
|
|
224
226
|
* sigma?: Float64Array, charge?: number }[]} ports what each internal port brings into the device;
|
|
@@ -248,6 +250,7 @@
|
|
|
248
250
|
* @property {{ re: Float64Array, im: Float64Array }} Z impedance −δV/δI, Ω·m²
|
|
249
251
|
* @property {{ phi: ComplexProfile, mu: Record<string, ComplexProfile>, c: Record<string, ComplexProfile> }[]} [profiles]
|
|
250
252
|
* per frequency, per unit excitation (volt, or A/m² in current mode)
|
|
253
|
+
* @property {string[]} warnings the steady state's warnings, which hold for its impedance too
|
|
251
254
|
*/
|
|
252
255
|
|
|
253
256
|
/** @typedef {{ re: Float64Array, im: Float64Array }} ComplexProfile */
|
package/types/channels.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function ghkCurrent(z: any, inside: any, outside: any, V: any, T: any): n
|
|
|
21
21
|
* port along a region: links that are their linear conductances, G = g × area per volume, gated the
|
|
22
22
|
* same, each ion's reversal potential following from the port's bath and the concentrations inside
|
|
23
23
|
* (so `inside`, `outside` and `at` aren't needed).
|
|
24
|
-
* @param {{ inside: object, outside: object, T
|
|
24
|
+
* @param {{ inside: object, outside: object, T: number, g?: { Na?: number, K?: number, leak?: number },
|
|
25
25
|
* leak?: string, leakZ?: number, at?: number, names?: { Na?: string, K?: string }, linear?: boolean, area?: number }} opts concentrations in
|
|
26
26
|
* mol/m³ by species name, K, S/m², V
|
|
27
27
|
* @returns {{ gates: object, species: object }}
|
|
@@ -29,7 +29,7 @@ export function ghkCurrent(z: any, inside: any, outside: any, V: any, T: any): n
|
|
|
29
29
|
export function hodgkinHuxley({ inside, outside, T, g, leak, leakZ, at, names, linear, area }?: {
|
|
30
30
|
inside: object;
|
|
31
31
|
outside: object;
|
|
32
|
-
T
|
|
32
|
+
T: number;
|
|
33
33
|
g?: {
|
|
34
34
|
Na?: number;
|
|
35
35
|
K?: number;
|
|
@@ -44,7 +44,7 @@ export function hodgkinHuxley({ inside, outside, T, g, leak, leakZ, at, names, l
|
|
|
44
44
|
};
|
|
45
45
|
linear?: boolean;
|
|
46
46
|
area?: number;
|
|
47
|
-
}): {
|
|
47
|
+
}, ...args: any[]): {
|
|
48
48
|
gates: object;
|
|
49
49
|
species: object;
|
|
50
50
|
};
|
package/types/checks.d.ts
CHANGED
package/types/data.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export function aqueous(names: string[], { epsr, material }?: {
|
|
10
10
|
epsr?: number;
|
|
11
11
|
material?: string;
|
|
12
|
-
}): {
|
|
12
|
+
}, ...args: any[]): {
|
|
13
13
|
species: object[];
|
|
14
14
|
materials: Record<string, object>;
|
|
15
15
|
};
|
|
@@ -24,7 +24,7 @@ export function aqueous(names: string[], { epsr, material }?: {
|
|
|
24
24
|
*/
|
|
25
25
|
export function semiconductor(name: string, { material }?: {
|
|
26
26
|
material?: string;
|
|
27
|
-
}): {
|
|
27
|
+
}, ...args: any[]): {
|
|
28
28
|
species: object[];
|
|
29
29
|
materials: Record<string, object>;
|
|
30
30
|
};
|
|
@@ -36,7 +36,7 @@ export function semiconductor(name: string, { material }?: {
|
|
|
36
36
|
*/
|
|
37
37
|
export function metal(name: string, { material }?: {
|
|
38
38
|
material?: string;
|
|
39
|
-
}): {
|
|
39
|
+
}, ...args: any[]): {
|
|
40
40
|
species: object[];
|
|
41
41
|
materials: Record<string, object>;
|
|
42
42
|
};
|
package/types/devices.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export function perovskiteCell({ V, light, bulkSRH }?: {
|
|
|
19
19
|
};
|
|
20
20
|
light?: number;
|
|
21
21
|
bulkSRH?: boolean;
|
|
22
|
-
}): import("./types.js").DeviceDefinition;
|
|
22
|
+
}, ...args: any[]): import("./types.js").DeviceDefinition;
|
|
23
23
|
/**
|
|
24
24
|
* A J–V loop's figures of merit, from a transient's trace (`{ t, voltage, current }`, as a
|
|
25
25
|
* recorder or advance() gives it): the loop splits at time `turn` into the sweep before (`rev`)
|
package/types/equation.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export function level(sol: {
|
|
|
78
78
|
fixed?: Record<string, number>;
|
|
79
79
|
}, { standard }?: {
|
|
80
80
|
standard?: boolean;
|
|
81
|
-
}): Float64Array;
|
|
81
|
+
}, ...args: any[]): Float64Array;
|
|
82
82
|
/**
|
|
83
83
|
* The standard hydrogen electrode, 2 H⁺ + 2 e⁻ ⇌ H₂, with μ(H₂) = 0 (the element in its standard
|
|
84
84
|
* state, on the usual table convention). Needs `H+` among the species.
|
package/types/errors.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A function's options: a name it doesn't take (a typo) is an error, not silently ignored.
|
|
3
|
+
* @param {string} where the function, for the message
|
|
4
|
+
* @param {unknown} opts
|
|
5
|
+
* @param {string[]} allowed
|
|
6
|
+
* @param {new (message: string) => Error} [Kind]
|
|
7
|
+
*/
|
|
8
|
+
export function checkOptions(where: string, opts: unknown, allowed: string[], Kind?: new (message: string) => Error): void;
|
|
1
9
|
export class DeviceError extends Error {
|
|
2
10
|
constructor(message: any);
|
|
3
11
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
export { DeviceError } from "./device.js";
|
|
1
2
|
export { SolverError } from "./solver.js";
|
|
2
|
-
export { BlockTridiagonal } from "./blockTridiagonal.js";
|
|
3
3
|
export * from "./constants.js";
|
|
4
4
|
/** @typedef {import('./types.js').DeviceDefinition} DeviceDefinition */
|
|
5
5
|
/** @typedef {import('./types.js').Solution} Solution */
|
|
@@ -72,7 +72,7 @@ export class Device {
|
|
|
72
72
|
/**
|
|
73
73
|
* Small-signal impedance Z(f) about the steady state (solved first), at one terminal: its
|
|
74
74
|
* held voltage is perturbed (or its driven current), the others keep their drives.
|
|
75
|
-
* Z = δV/δI with I into the device (Ω·m²)
|
|
75
|
+
* Z = δV/δI with I into the device (Ω·m²), with the steady state's `warnings`.
|
|
76
76
|
* @param {ArrayLike<number>} frequencies Hz
|
|
77
77
|
* @param {{ terminal?: string, profiles?: boolean }} [opts] the terminal (default 'right'), and
|
|
78
78
|
* whether to return complex profiles per frequency
|
|
@@ -110,6 +110,3 @@ export class Device {
|
|
|
110
110
|
}
|
|
111
111
|
export type DeviceDefinition = import("./types.js").DeviceDefinition;
|
|
112
112
|
export type Solution = import("./types.js").Solution;
|
|
113
|
-
export { DeviceError, normalizeDevice } from "./device.js";
|
|
114
|
-
export { buildGrid, gradedCells } from "./grid.js";
|
|
115
|
-
export { bernoulli, bernoulliDerivative } from "./bernoulli.js";
|
package/types/junction.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export function henderson(left: Record<string, number>, right: Record<string, nu
|
|
|
12
12
|
D: number;
|
|
13
13
|
}>, { T }?: {
|
|
14
14
|
T?: number;
|
|
15
|
-
}): number;
|
|
15
|
+
}, ...args: any[]): number;
|
|
16
16
|
/**
|
|
17
17
|
* Planck's liquid-junction potential, φ_R − φ_L (V): the steady state of constrained diffusion,
|
|
18
18
|
* a junction zone held between the two solutions, electroneutral, carrying no current. Solved by
|
|
@@ -29,4 +29,4 @@ export function planck(left: Record<string, number>, right: Record<string, numbe
|
|
|
29
29
|
}>, { T, steps }?: {
|
|
30
30
|
T?: number;
|
|
31
31
|
steps?: number;
|
|
32
|
-
}): number;
|
|
32
|
+
}, ...args: any[]): number;
|
package/types/kit.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ export type VacuumSide = {
|
|
|
59
59
|
offset: number;
|
|
60
60
|
};
|
|
61
61
|
export { build, combine, layer, ohmic, bath } from "./stack.js";
|
|
62
|
-
export {
|
|
62
|
+
export { half, SHE, level } from "./equation.js";
|
|
63
63
|
export { traces, speciesRole, typeset } from "./traces.js";
|
|
64
64
|
export { pulse, square, triangle, ramp, injector, recombination, photogeneration } from "./sources.js";
|
|
65
65
|
export { hodgkinHuxley, ghkCurrent } from "./channels.js";
|
package/types/live.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function live(def: object, { worker, onsolution, maxRamp }?: {
|
|
|
28
28
|
worker?: boolean | (() => any);
|
|
29
29
|
onsolution?: (solution: object, info: object) => void;
|
|
30
30
|
maxRamp?: number;
|
|
31
|
-
}): Live;
|
|
31
|
+
}, ...args: any[]): Live;
|
|
32
32
|
/**
|
|
33
33
|
* What a live device does with each change, on whichever thread solves it: apply, solve warm,
|
|
34
34
|
* ramp from the last good state if that fails, and fall back to the last good solution.
|
package/types/particles.d.ts
CHANGED
package/types/plot.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export function themeColor(role: "electron" | "cation" | "anion" | "redox", slot: number, { dark }?: {
|
|
9
9
|
dark?: boolean;
|
|
10
|
-
}): string;
|
|
10
|
+
}, ...args: any[]): string;
|
|
11
11
|
/**
|
|
12
12
|
* A label split into plain and subscript runs. Labels mark subscripts TeX-style: `V_{e⁻}`, or
|
|
13
13
|
* `C_ox` for a run of letters, digits and charge signs. Everything else is literal.
|
|
@@ -34,7 +34,7 @@ export function levelChart(tr: ReturnType<typeof traces>, { width, height, ylabe
|
|
|
34
34
|
xlim?: [number, number];
|
|
35
35
|
title?: string;
|
|
36
36
|
ytick?: (v: number) => string;
|
|
37
|
-
}): string;
|
|
37
|
+
}, ...args: any[]): string;
|
|
38
38
|
/**
|
|
39
39
|
* A solution's level diagram as an SVG string: each charged species' voltage (thick) and
|
|
40
40
|
* standard level (thin), regions as bands; or with `energy: true`, the familiar energy-up band
|
|
@@ -43,7 +43,7 @@ export function levelChart(tr: ReturnType<typeof traces>, { width, height, ylabe
|
|
|
43
43
|
* @param {Parameters<typeof traces>[1] & Parameters<typeof levelChart>[1]} [opts]
|
|
44
44
|
* @returns {string}
|
|
45
45
|
*/
|
|
46
|
-
export function bandDiagram(sol: import("./types.js").Solution, opts?: Parameters<typeof traces>[1] & Parameters<typeof levelChart>[1]): string;
|
|
46
|
+
export function bandDiagram(sol: import("./types.js").Solution, opts?: Parameters<typeof traces>[1] & Parameters<typeof levelChart>[1], ...args: any[]): string;
|
|
47
47
|
export const THEME: Readonly<{
|
|
48
48
|
light: {
|
|
49
49
|
electron: string[];
|
package/types/polarization.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function polarization(device: import("./index.js").Device, sol: object, w
|
|
|
24
24
|
x?: number;
|
|
25
25
|
}, V: number | ArrayLike<number>, opts?: {
|
|
26
26
|
surface?: "held" | "equilibrium";
|
|
27
|
-
}): {
|
|
27
|
+
}, ...args: any[]): {
|
|
28
28
|
V: Float64Array;
|
|
29
29
|
current: Float64Array;
|
|
30
30
|
bare?: Float64Array;
|
package/types/recorder.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export function recorder(device: import("./index.js").Device, opts?: {
|
|
|
19
19
|
species?: string;
|
|
20
20
|
gate?: string;
|
|
21
21
|
})[];
|
|
22
|
-
}): {
|
|
22
|
+
}, ...args: any[]): {
|
|
23
23
|
advance: (tEnd: number, aopts?: object) => import("./types.js").Solution;
|
|
24
24
|
frame: (t: number) => import("./types.js").Solution;
|
|
25
25
|
frames: import("./types.js").Solution[];
|
package/types/solver.d.ts
CHANGED
|
@@ -115,6 +115,7 @@ export class Solver {
|
|
|
115
115
|
chargeNode: Uint8Array<any>;
|
|
116
116
|
chargeNodes: Int32Array<ArrayBuffer>;
|
|
117
117
|
lateStorage: Uint8Array<any>;
|
|
118
|
+
portHeldEnd: Uint8Array<ArrayBuffer>;
|
|
118
119
|
combining: boolean;
|
|
119
120
|
bookkeeping: any;
|
|
120
121
|
bookkeepingFaces: number[];
|
|
@@ -209,6 +210,7 @@ export class Solver {
|
|
|
209
210
|
dtNow: any;
|
|
210
211
|
transformed: any;
|
|
211
212
|
_nodeConductor(g: any, dt: any): void;
|
|
213
|
+
_portHeldAt(g: any, i: any): boolean;
|
|
212
214
|
_nodesDilute(g0: any, g1: any, dt: any): void;
|
|
213
215
|
_nodeConcentrated(g: any, dt: any): void;
|
|
214
216
|
_storage(g: any, i: any, dt: any, Kz?: number): void;
|
package/types/sources.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export function pulse({ start, width, amplitude, rise, base }?: {
|
|
|
13
13
|
amplitude: number;
|
|
14
14
|
rise?: number;
|
|
15
15
|
base?: number;
|
|
16
|
-
}): {
|
|
16
|
+
}, ...args: any[]): {
|
|
17
17
|
t: number[];
|
|
18
18
|
values: number[];
|
|
19
19
|
};
|
|
@@ -27,7 +27,7 @@ export function square({ high, low, period, duty }?: {
|
|
|
27
27
|
low: number;
|
|
28
28
|
period: number;
|
|
29
29
|
duty?: number;
|
|
30
|
-
}): {
|
|
30
|
+
}, ...args: any[]): {
|
|
31
31
|
t: number[];
|
|
32
32
|
values: number[];
|
|
33
33
|
repeat: true;
|
|
@@ -42,7 +42,7 @@ export function triangle({ from, to, period }?: {
|
|
|
42
42
|
from: number;
|
|
43
43
|
to: number;
|
|
44
44
|
period: number;
|
|
45
|
-
}): {
|
|
45
|
+
}, ...args: any[]): {
|
|
46
46
|
t: number[];
|
|
47
47
|
values: number[];
|
|
48
48
|
repeat: true;
|
|
@@ -57,7 +57,7 @@ export function ramp({ from, to, duration, start }?: {
|
|
|
57
57
|
to: number;
|
|
58
58
|
duration: number;
|
|
59
59
|
start?: number;
|
|
60
|
-
}): {
|
|
60
|
+
}, ...args: any[]): {
|
|
61
61
|
t: number[];
|
|
62
62
|
values: number[];
|
|
63
63
|
};
|
|
@@ -84,7 +84,7 @@ export function injector({ name, region, from, to, species, I, headroom }?: {
|
|
|
84
84
|
repeat?: boolean;
|
|
85
85
|
};
|
|
86
86
|
headroom?: number;
|
|
87
|
-
}): object;
|
|
87
|
+
}, ...args: any[]): object;
|
|
88
88
|
/**
|
|
89
89
|
* Band-to-band recombination e⁻ + h⁺ ⇌ 0 in a material, from the minority-carrier lifetime it
|
|
90
90
|
* should give at a majority density: kf = 1/(τ · majority). (The rate is kf (np − n_i²), so τ is
|
|
@@ -96,7 +96,7 @@ export function recombination({ material, tau, majority }?: {
|
|
|
96
96
|
material: string;
|
|
97
97
|
tau: number;
|
|
98
98
|
majority: number;
|
|
99
|
-
}): {
|
|
99
|
+
}, ...args: any[]): {
|
|
100
100
|
equation: string;
|
|
101
101
|
kf: Record<string, number>;
|
|
102
102
|
};
|
|
@@ -122,7 +122,7 @@ export function photogeneration({ material, flux, alpha, mu, from, to, makes }?:
|
|
|
122
122
|
from?: number;
|
|
123
123
|
to: number;
|
|
124
124
|
makes?: string;
|
|
125
|
-
}): {
|
|
125
|
+
}, ...args: any[]): {
|
|
126
126
|
equation: string;
|
|
127
127
|
fixed: Record<string, number>;
|
|
128
128
|
kf: Record<string, {
|
package/types/traces.d.ts
CHANGED