driftlet 1.0.1 → 1.1.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 +2 -2
- package/docs/data.md +131 -10
- package/docs/device.md +20 -4
- package/docs/kit.md +19 -3
- package/docs/numerics.md +10 -5
- package/docs/reliability.md +1 -3
- package/docs/templates.md +1 -1
- package/llms.txt +31 -16
- package/package.json +1 -1
- package/src/checks.js +7 -2
- package/src/data.js +104 -13
- package/src/describe.js +1 -1
- package/src/device.js +36 -15
- package/src/difference.js +42 -7
- package/src/errors.js +18 -0
- package/src/index.js +3 -1
- package/src/kit.js +1 -1
- package/src/solution.js +14 -7
- package/src/solver.js +164 -68
- package/src/stack.js +2 -1
- package/src/types.js +2 -0
- package/types/data.d.ts +44 -6
- package/types/difference.d.ts +8 -2
- package/types/errors.d.ts +8 -0
- package/types/kit.d.ts +1 -1
- package/types/solver.d.ts +13 -2
- package/types/types.d.ts +5 -0
package/src/data.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// only. How levels line up at an interface, and a surface's work function, belong to that
|
|
4
4
|
// interface or surface and are never in here.
|
|
5
5
|
|
|
6
|
-
import { DeviceError, checkOptions } from './errors.js';
|
|
6
|
+
import { DeviceError, checkOptions, shown } from './errors.js';
|
|
7
7
|
import { FARADAY, GAS_CONSTANT, AVOGADRO } from './constants.js';
|
|
8
|
+
import { powr } from './pow.js';
|
|
8
9
|
|
|
9
10
|
const fail = (message) => {
|
|
10
11
|
throw new DeviceError(message);
|
|
@@ -43,7 +44,7 @@ export const WATER_EPSR = 78.3;
|
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Semiconductors at 300 K (Sze, Physics of Semiconductor Devices, 2nd ed., 1981, appendix):
|
|
46
|
-
* relative permittivity, band gap Eg (eV), electron affinity chi (
|
|
47
|
+
* relative permittivity, band gap Eg (eV), electron affinity chi (eV), effective densities of
|
|
47
48
|
* states Nc and Nv (cm⁻³), and drift mobilities of lightly doped material mun and mup
|
|
48
49
|
* (cm²/(V·s)).
|
|
49
50
|
*/
|
|
@@ -65,7 +66,8 @@ export const METALS = freeze(
|
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
68
|
* Water with the named ions, as a library piece for `build()`: each ion as a species
|
|
68
|
-
* (c_ref = 1 M) and the material `water` holding them, with D and μ° from `IONS`.
|
|
69
|
+
* (c_ref = 1 M) and the material `water` holding them, with D and μ° from `IONS`. The data are
|
|
70
|
+
* 25 °C's, and the material says so (T: 298.15): a device at another temperature warns.
|
|
69
71
|
* @param {string[]} names ions from `IONS`
|
|
70
72
|
* @param {{ epsr?: number, material?: string }} [opts] permittivity (default that of water;
|
|
71
73
|
* 0 makes the solution strictly neutral, without double layers), and the material's name
|
|
@@ -81,24 +83,53 @@ export function aqueous(names, { epsr = WATER_EPSR, material = 'water' } = {}) {
|
|
|
81
83
|
species.push({ name, z: ion.z, cRef: 1000 });
|
|
82
84
|
entries[name] = { D: ion.D, mu0: ion.mu0 };
|
|
83
85
|
}
|
|
84
|
-
|
|
86
|
+
// (25 °C data: a device at another temperature warns)
|
|
87
|
+
return { species, materials: { [material]: { epsr, species: entries, T: 298.15 } } };
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
// How the semiconductors' data move with temperature, for semiconductor(name, { T }):
|
|
91
|
+
// - the gap by Varshni's form, Eg(T) = Eg0 − αT²/(T + β) (Y. P. Varshni, Physica 34, 149, 1967),
|
|
92
|
+
// with Thurmond's parameters (C. D. Thurmond, J. Electrochem. Soc. 122, 1133, 1975, as
|
|
93
|
+
// tabulated in Sze, 2nd ed.): Eg0 (eV), alpha (eV/K), beta (K). Only the shift from 300 K is
|
|
94
|
+
// used, on the table's Eg, so T = 300 K is the table: Varshni's own Eg(300) differs from it by
|
|
95
|
+
// 4.5 meV (Si, 1.1245), 3.4 meV (Ge, 0.6634) and 1.5 meV (GaAs, 1.4225).
|
|
96
|
+
// - Nc and Nv ∝ T^1.5 (parabolic bands, effective masses held fixed).
|
|
97
|
+
// - mobilities ∝ T^−gn and T^−gp, the lattice-limited power laws for lightly doped material
|
|
98
|
+
// (Sze, 2nd ed., ch. 1): approximate, and wrong where impurity scattering matters (heavy
|
|
99
|
+
// doping, or cold). D follows from μ and T by Einstein's relation, D = μkT/q.
|
|
100
|
+
// - the electron affinity chi isn't moved.
|
|
101
|
+
const WITH_T = {
|
|
102
|
+
Si: { Eg0: 1.17, alpha: 4.73e-4, beta: 636, gn: 2.42, gp: 2.2 },
|
|
103
|
+
Ge: { Eg0: 0.7437, alpha: 4.774e-4, beta: 235, gn: 1.66, gp: 2.33 },
|
|
104
|
+
GaAs: { Eg0: 1.519, alpha: 5.405e-4, beta: 204, gn: 1.0, gp: 2.1 },
|
|
105
|
+
};
|
|
106
|
+
const T_RANGE = [150, 500];
|
|
107
|
+
|
|
87
108
|
/**
|
|
88
109
|
* A semiconductor from `SEMICONDUCTORS`, as a library piece: electrons `e-` and holes `h+`, with
|
|
89
|
-
* c_ref = Nc and Nv, D from the mobilities by the Einstein relation at
|
|
90
|
-
* anchored at the conduction band) and μ°_h⁺ = Eg·F. The data are for 300 K
|
|
110
|
+
* c_ref = Nc and Nv, D from the mobilities by the Einstein relation at T, and μ°_e⁻ = 0 (φ
|
|
111
|
+
* anchored at the conduction band) and μ°_h⁺ = Eg·F. The data are for 300 K; another T (150 to
|
|
112
|
+
* 500 K) moves them: the gap by Varshni's shift from 300 K, Nc and Nv as T^1.5, the mobilities by
|
|
113
|
+
* lattice-limited power laws (light doping). The material carries its `T`, and a device at
|
|
114
|
+
* another temperature warns: give the device the same T.
|
|
91
115
|
* Statistics are ideal (Boltzmann); add Fermi–Dirac to the material for degenerate doping.
|
|
92
116
|
* @param {string} name 'Si', 'Ge' or 'GaAs'
|
|
93
|
-
* @param {{ material?: string }} [opts]
|
|
117
|
+
* @param {{ material?: string, T?: number }} [opts] the material's name, and the temperature
|
|
118
|
+
* (K, default 300, the table's)
|
|
94
119
|
* @returns {{ species: object[], materials: Record<string, object> }}
|
|
95
120
|
*/
|
|
96
|
-
export function semiconductor(name, { material = name } = {}) {
|
|
97
|
-
|
|
98
|
-
checkOptions('semiconductor', arguments[1], ['material']);
|
|
121
|
+
export function semiconductor(name, { material = name, T = 300 } = {}) {
|
|
122
|
+
checkOptions('semiconductor', arguments[1], ['material', 'T']);
|
|
99
123
|
const sc = SEMICONDUCTORS[name];
|
|
100
124
|
if (!sc) fail(`semiconductor: no data for '${name}' (the library has ${Object.keys(SEMICONDUCTORS).join(', ')})`);
|
|
101
|
-
|
|
125
|
+
if (typeof T !== 'number') fail(`semiconductor: T must be a number, the temperature in K, got ${shown(T)}`);
|
|
126
|
+
if (!(Number.isFinite(T) && T >= T_RANGE[0] && T <= T_RANGE[1])) {
|
|
127
|
+
fail(`semiconductor: T must be the temperature in K, from ${T_RANGE[0]} to ${T_RANGE[1]} (the range its temperature laws are good for), got ${T}`);
|
|
128
|
+
}
|
|
129
|
+
const { alpha, beta, gn, gp } = WITH_T[name];
|
|
130
|
+
const t = T / 300, varshni = (T) => (alpha * T * T) / (T + beta);
|
|
131
|
+
const Eg = sc.Eg - (varshni(T) - varshni(300));
|
|
132
|
+
const VT = (GAS_CONSTANT * T) / FARADAY;
|
|
102
133
|
const perCm3 = (n) => (n * 1e6) / AVOGADRO;
|
|
103
134
|
return {
|
|
104
135
|
species: [
|
|
@@ -107,16 +138,76 @@ export function semiconductor(name, { material = name } = {}) {
|
|
|
107
138
|
],
|
|
108
139
|
materials: {
|
|
109
140
|
[material]: {
|
|
141
|
+
T, // the temperature the data are for: a device at another warns
|
|
110
142
|
epsr: sc.epsr,
|
|
111
143
|
species: {
|
|
112
|
-
'e-': { D: sc.mun * 1e-4 * VT, mu0: 0, cRef: perCm3(sc.Nc) },
|
|
113
|
-
'h+': { D: sc.mup * 1e-4 * VT, mu0:
|
|
144
|
+
'e-': { D: sc.mun * powr(t, -gn) * 1e-4 * VT, mu0: 0, cRef: perCm3(sc.Nc * powr(t, 1.5)) },
|
|
145
|
+
'h+': { D: sc.mup * powr(t, -gp) * 1e-4 * VT, mu0: Eg * FARADAY, cRef: perCm3(sc.Nv * powr(t, 1.5)) },
|
|
114
146
|
},
|
|
115
147
|
},
|
|
116
148
|
},
|
|
117
149
|
};
|
|
118
150
|
}
|
|
119
151
|
|
|
152
|
+
/**
|
|
153
|
+
* A trap level in a semiconductor that holds charge: its two charge states as immobile species
|
|
154
|
+
* (D = 0), filled and emptied by capturing carriers, e⁻ + X⁰ = X⁻ and X⁻ + h⁺ = X⁰ (acceptor-like)
|
|
155
|
+
* or X⁺ + e⁻ = X⁰ and X⁰ + h⁺ = X⁺ (donor-like). In steady state it recombines at exactly the
|
|
156
|
+
* `srh` law's rate with the same τ_n, τ_p and n₁, and its charge is in Poisson's equation, which
|
|
157
|
+
* the `srh` law (the trap's occupancy eliminated) leaves out. Each node keeps its own total.
|
|
158
|
+
* @param {{ species: object[], materials: Record<string, object> }} lib a library entry with e⁻
|
|
159
|
+
* and h⁺ (semiconductor()), returned with the trap's states added
|
|
160
|
+
* @param {{ density: number, tauN: number, tauP: number, n1?: number, charge?: 'acceptor' | 'donor',
|
|
161
|
+
* name?: string, material?: string, T?: number }} opts density N_t (mol/m³); τ_n = 1/(c_n N_t)
|
|
162
|
+
* and τ_p = 1/(c_p N_t) (s) at that density; n₁, the electron density that half-fills it
|
|
163
|
+
* (mol/m³; default n_i, midgap); T (K, default the material's T, else 300)
|
|
164
|
+
* @returns {{ library: object, bulkReactions: object[], c0: Record<string, number> }} the library
|
|
165
|
+
* for build(), the capture reactions, and c0 for each layer holding the trap (its total N_t)
|
|
166
|
+
*/
|
|
167
|
+
export function trap(lib, { density, tauN, tauP, n1, charge = 'acceptor', name = 'X', material, T } = {}) {
|
|
168
|
+
checkOptions('trap', arguments[1], ['density', 'tauN', 'tauP', 'n1', 'charge', 'name', 'material', 'T']);
|
|
169
|
+
if (!lib || typeof lib !== 'object' || typeof lib.materials !== 'object') fail('trap: give the library entry first, semiconductor(\'Si\') say, then { density, tauN, tauP }');
|
|
170
|
+
const names = Object.keys(lib.materials);
|
|
171
|
+
const m = material ?? (names.length === 1 ? names[0] : fail(`trap: the library has materials ${names.join(', ')}: say which, with material`));
|
|
172
|
+
const mat = lib.materials[m];
|
|
173
|
+
if (!mat) fail(`trap: no material '${m}' in the library (it has ${names.join(', ')})`);
|
|
174
|
+
const el = mat.species?.['e-'], ho = mat.species?.['h+'];
|
|
175
|
+
if (!el || !ho) fail(`trap: material '${m}' needs e- and h+ (a semiconductor)`);
|
|
176
|
+
const pos = (v, what) => (Number.isFinite(v) && v > 0 ? v : fail(`trap: ${what}, got ${shown(v)}`));
|
|
177
|
+
pos(density, 'density must be the trap density N_t > 0 (mol/m³; units.perCm3(1e15), say)');
|
|
178
|
+
pos(tauN, 'tauN must be the electron lifetime τ_n = 1/(c_n N_t) > 0 (s)');
|
|
179
|
+
pos(tauP, 'tauP must be the hole lifetime τ_p = 1/(c_p N_t) > 0 (s)');
|
|
180
|
+
// (the material's own temperature, where it gives one: semiconductor()'s does)
|
|
181
|
+
if (T === undefined) T = mat.T ?? 300;
|
|
182
|
+
pos(T, 'T must be the temperature in K');
|
|
183
|
+
if (mat.T !== undefined && Math.abs(T - mat.T) > 0.01) fail(`trap: T is ${T} K, but material '${m}' is for ${mat.T} K; leave T out (the trap takes the material's), or remake the material for ${T} K`);
|
|
184
|
+
if (charge !== 'acceptor' && charge !== 'donor') fail(`trap: charge must be 'acceptor' (0/−) or 'donor' (+/0), got ${JSON.stringify(charge)}`);
|
|
185
|
+
if (typeof name !== 'string' || !/^[A-Za-z][\w()]*$/.test(name)) fail(`trap: name must be a plain name for the trap (its states are name0 and name- or name+), got ${JSON.stringify(name)}`);
|
|
186
|
+
const RT = GAS_CONSTANT * T, cRefE = el.cRef ?? lib.species.find((sp) => sp.name === 'e-')?.cRef;
|
|
187
|
+
const cRefH = ho.cRef ?? lib.species.find((sp) => sp.name === 'h+')?.cRef;
|
|
188
|
+
const ni = Math.sqrt(cRefE * cRefH * Math.exp(-(ho.mu0 - el.mu0) / RT));
|
|
189
|
+
const half = n1 === undefined ? ni : pos(n1, 'n1 must be the electron density that half-fills the trap > 0 (mol/m³)');
|
|
190
|
+
// The filled state's μ° sits an electron's μ at n₁ above the empty one's: half full at n = n₁.
|
|
191
|
+
const [empty, full] = charge === 'acceptor' ? [`${name}0`, `${name}-`] : [`${name}+`, `${name}0`];
|
|
192
|
+
const zEmpty = charge === 'acceptor' ? 0 : 1;
|
|
193
|
+
const states = {
|
|
194
|
+
[empty]: { D: 0, mu0: 0, cRef: density },
|
|
195
|
+
[full]: { D: 0, mu0: el.mu0 + RT * Math.log(half / cRefE), cRef: density },
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
library: {
|
|
199
|
+
species: [...lib.species, { name: empty, z: zEmpty }, { name: full, z: zEmpty - 1 }],
|
|
200
|
+
materials: { ...lib.materials, [m]: { ...mat, species: { ...mat.species, ...states } } },
|
|
201
|
+
},
|
|
202
|
+
bulkReactions: [
|
|
203
|
+
{ equation: `e- + ${empty} = ${full}`, kf: { [m]: 1 / (tauN * density) } },
|
|
204
|
+
{ equation: `${full} + h+ = ${empty}`, kf: { [m]: 1 / (tauP * density) } },
|
|
205
|
+
],
|
|
206
|
+
// (each node keeps the total, X⁰ + X⁻ say; the split is the solve's to find)
|
|
207
|
+
c0: { [empty]: density / 2, [full]: density / 2 },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
120
211
|
/**
|
|
121
212
|
* A metal from `METALS` as a conductor material, its carrier `e-`.
|
|
122
213
|
* @param {string} name
|
package/src/describe.js
CHANGED
|
@@ -138,7 +138,7 @@ export function describe(def) {
|
|
|
138
138
|
const { species, materials, regions, grid } = model;
|
|
139
139
|
const RT = GAS_CONSTANT * model.T;
|
|
140
140
|
const lines = [];
|
|
141
|
-
const warnings = unitWarnings(def);
|
|
141
|
+
const warnings = [...model.warnings, ...unitWarnings(def)]; // (the definition's own: a material's data at another T, …)
|
|
142
142
|
const total = grid.x[grid.x.length - 1] - grid.x[0];
|
|
143
143
|
lines.push(`${regions.length} region${regions.length > 1 ? 's' : ''}, ${si(total, 'm')}, ${grid.x.length} nodes, T = ${model.T} K`);
|
|
144
144
|
const geo = model.geometry;
|
package/src/device.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { buildGrid, applyGeometry } from './grid.js';
|
|
10
10
|
import { FARADAY, GAS_CONSTANT } from './constants.js';
|
|
11
11
|
import { normalizeStatistics } from './statistics.js';
|
|
12
|
-
import { DeviceError } from './errors.js';
|
|
12
|
+
import { DeviceError, shown } from './errors.js';
|
|
13
13
|
import { stoichiometry as equationStoichiometry, faceSides, parseEquation } from './equation.js';
|
|
14
14
|
|
|
15
15
|
export { DeviceError };
|
|
@@ -47,12 +47,12 @@ function fields(obj, path, allowed) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function finite(v, path) {
|
|
50
|
-
need(isFiniteNumber(v), `${path} must be a finite number, got ${
|
|
50
|
+
need(isFiniteNumber(v), `${path} must be a finite number, got ${shown(v)}`);
|
|
51
51
|
return v;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function positive(v, path) {
|
|
55
|
-
need(isFiniteNumber(v) && v > 0, `${path} must be a positive number, got ${
|
|
55
|
+
need(isFiniteNumber(v) && v > 0, `${path} must be a positive number, got ${shown(v)}`);
|
|
56
56
|
return v;
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -108,9 +108,9 @@ function profile(v, path, what, ok, okText) {
|
|
|
108
108
|
`${path} must be a profile { x, values }: two arrays of the same length, positions (m, the device's x) and ${what}`,
|
|
109
109
|
);
|
|
110
110
|
v.x.forEach((xk, k) => {
|
|
111
|
-
need(isFiniteNumber(xk) && (k === 0 || xk > v.x[k - 1]), `${path}.x must be finite and strictly increasing, but x[${k}] is ${
|
|
111
|
+
need(isFiniteNumber(xk) && (k === 0 || xk > v.x[k - 1]), `${path}.x must be finite and strictly increasing, but x[${k}] is ${shown(xk)}`);
|
|
112
112
|
});
|
|
113
|
-
v.values.forEach((y, k) => need(isFiniteNumber(y) && ok(y), `${path}.values[${k}] must be ${okText}, got ${
|
|
113
|
+
v.values.forEach((y, k) => need(isFiniteNumber(y) && ok(y), `${path}.values[${k}] must be ${okText}, got ${shown(y)}`));
|
|
114
114
|
return { x: Float64Array.from(v.x), values: Float64Array.from(v.values) };
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -138,7 +138,7 @@ function srhLaw(sdef, path, list, species, timeKeys) {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function nonNegative(v, path) {
|
|
141
|
-
need(isFiniteNumber(v) && v >= 0, `${path} must be a non-negative number, got ${
|
|
141
|
+
need(isFiniteNumber(v) && v >= 0, `${path} must be a non-negative number, got ${shown(v)}`);
|
|
142
142
|
return v;
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -151,7 +151,7 @@ export function normalizeDevice(def) {
|
|
|
151
151
|
need(isObject(def), 'device definition must be an object');
|
|
152
152
|
fields(def, 'device', ['T', 'species', 'materials', 'regions', 'interfaces', 'bulkReactions', 'contacts', 'ports', 'grid', 'geometry']);
|
|
153
153
|
|
|
154
|
-
const T = def.T === undefined ? 298.15 : positive(def.T, 'T');
|
|
154
|
+
const T = def.T === undefined ? 298.15 : positive(def.T, 'T (the temperature, K)');
|
|
155
155
|
const RT = GAS_CONSTANT * T;
|
|
156
156
|
|
|
157
157
|
// --- species
|
|
@@ -164,7 +164,7 @@ export function normalizeDevice(def) {
|
|
|
164
164
|
fields(sp, path, ['name', 'z', 'cRef']);
|
|
165
165
|
need(typeof sp.name === 'string' && sp.name.length > 0, `${path}.name must be a non-empty string`);
|
|
166
166
|
need(!speciesIndex.has(sp.name), `${path}.name: duplicate species '${sp.name}'`);
|
|
167
|
-
need(Number.isInteger(sp.z), `${path}.z (charge number) must be an integer, got ${
|
|
167
|
+
need(Number.isInteger(sp.z), `${path}.z (charge number) must be an integer, got ${shown(sp.z)}`);
|
|
168
168
|
const cRef = sp.cRef === undefined ? undefined : positive(sp.cRef, `${path}.cRef`);
|
|
169
169
|
speciesIndex.set(sp.name, i);
|
|
170
170
|
species.push({ name: sp.name, z: sp.z, cRef });
|
|
@@ -179,13 +179,15 @@ export function normalizeDevice(def) {
|
|
|
179
179
|
for (const [mname, mat] of Object.entries(def.materials)) {
|
|
180
180
|
const path = `materials.${mname}`;
|
|
181
181
|
need(isObject(mat), `${path} must be an object`);
|
|
182
|
+
// The temperature a material's data are for, where it says (semiconductor()'s do).
|
|
183
|
+
const dataT = mat.T === undefined ? undefined : positive(mat.T, `${path}.T (the temperature its data are for, K)`);
|
|
182
184
|
if (mat.conductor !== undefined) {
|
|
183
185
|
materialIndex.set(mname, materials.length);
|
|
184
|
-
materials.push(normalizeConductor(mat, mname, path, species, speciesIndex));
|
|
186
|
+
materials.push({ ...normalizeConductor(mat, mname, path, species, speciesIndex), dataT });
|
|
185
187
|
continue;
|
|
186
188
|
}
|
|
187
189
|
// ε = 0 makes the material strictly neutral: Poisson becomes local neutrality there.
|
|
188
|
-
fields(mat, path, ['epsr', 'species', 'statistics']);
|
|
190
|
+
fields(mat, path, ['epsr', 'species', 'statistics', 'T']);
|
|
189
191
|
const epsr = nonNegative(mat.epsr, `${path}.epsr`);
|
|
190
192
|
need(isObject(mat.species), `${path}.species must be an object mapping species names to parameters`);
|
|
191
193
|
const present = new Uint8Array(nSpecies);
|
|
@@ -205,7 +207,7 @@ export function normalizeDevice(def) {
|
|
|
205
207
|
need(c !== undefined, `${spath}.cRef: no reference concentration (give it here or on the species)`);
|
|
206
208
|
cRef[i] = positive(c, `${spath}.cRef`);
|
|
207
209
|
}
|
|
208
|
-
const m = { name: mname, epsr, present, D, mu0, cRef };
|
|
210
|
+
const m = { name: mname, epsr, present, D, mu0, cRef, dataT };
|
|
209
211
|
const st = normalizeStatistics({ ...m, statistics: mat.statistics }, path, species, speciesIndex, RT, { need, finite, positive });
|
|
210
212
|
m.models = st.models;
|
|
211
213
|
m.modelOf = st.modelOf;
|
|
@@ -285,7 +287,7 @@ export function normalizeDevice(def) {
|
|
|
285
287
|
c0Profile[i] = profile(v, at, 'concentrations (mol/m³)', (ck) => ck > 0, `a concentration > 0 (mol/m³)${none}`);
|
|
286
288
|
continue;
|
|
287
289
|
}
|
|
288
|
-
need(isFiniteNumber(v) && v > 0, `${at} must be a concentration > 0 (mol/m³) or a profile { x, values }, got ${
|
|
290
|
+
need(isFiniteNumber(v) && v > 0, `${at} must be a concentration > 0 (mol/m³) or a profile { x, values }, got ${shown(v)}${none}`);
|
|
289
291
|
c0[i] = v;
|
|
290
292
|
}
|
|
291
293
|
}
|
|
@@ -306,6 +308,23 @@ export function normalizeDevice(def) {
|
|
|
306
308
|
};
|
|
307
309
|
});
|
|
308
310
|
|
|
311
|
+
// A material whose data are for another temperature than the device's is silently wrong there
|
|
312
|
+
// (a semiconductor's n_i, gap and mobilities; water's μ°, ε and D): say so (for each material a
|
|
313
|
+
// region uses).
|
|
314
|
+
materials.forEach((m, k) => {
|
|
315
|
+
if (m.dataT === undefined || Math.abs(m.dataT - T) <= 0.01 || !regions.some((reg) => reg.material === k)) return;
|
|
316
|
+
const own = Object.keys(def.materials[m.name].species ?? {}), semi = own.includes('e-') && own.includes('h+');
|
|
317
|
+
warnings.push(
|
|
318
|
+
`materials.${m.name}: its data are for ${m.dataT} K, but the device is at ${T} K${def.T === undefined ? ' (the default, as it gives no T)' : ''}, ` +
|
|
319
|
+
(semi
|
|
320
|
+
? `so they don't hold there (a semiconductor's n_i, gap and mobilities are ${m.dataT} K's): give the device T: ${m.dataT}, ` +
|
|
321
|
+
`or remake the material for ${T} K (semiconductor(name, { T: ${T} }))`
|
|
322
|
+
: `so they don't hold there: its μ° (so equilibrium constants and electrode potentials, Kw's by ~6× between 6 and 25 °C), ` +
|
|
323
|
+
`permittivity and diffusivities are ${m.dataT} K's. Give the device T: ${m.dataT}, or give the material data for ${T} K ` +
|
|
324
|
+
`and its T (aqueous() has none at other temperatures yet)`),
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
309
328
|
// --- interfaces (one per face between consecutive regions)
|
|
310
329
|
const nFaces = regions.length - 1;
|
|
311
330
|
const idefs = def.interfaces ?? [];
|
|
@@ -490,7 +509,7 @@ function stoichiometry(map, path, fixed, fixedPath, species, speciesIndex, RT) {
|
|
|
490
509
|
const list = [];
|
|
491
510
|
let fixedA = 0, charge = 0, nFixed = 0;
|
|
492
511
|
for (const [name, nu] of Object.entries(map)) {
|
|
493
|
-
need(Number.isInteger(nu) && nu !== 0, `${path}.${name} must be a non-zero integer, got ${
|
|
512
|
+
need(Number.isInteger(nu) && nu !== 0, `${path}.${name} must be a non-zero integer, got ${shown(nu)}`);
|
|
494
513
|
if (speciesIndex.has(name)) {
|
|
495
514
|
const i = speciesIndex.get(name);
|
|
496
515
|
list.push({ i, nu });
|
|
@@ -1048,7 +1067,7 @@ function gatedBy(raw, gates, lpath, where) {
|
|
|
1048
1067
|
return Object.entries(raw).map(([gname, p]) => {
|
|
1049
1068
|
const q = gates.findIndex((gt) => gt.name === gname);
|
|
1050
1069
|
need(q >= 0, `${lpath}.gates.${gname}: no such gate${gates.length ? ` (there are ${gates.map((gt) => gt.name).join(', ')})` : ` (define it in ${where}.gates)`}`);
|
|
1051
|
-
need(Number.isInteger(p) && p >= 1 && p <= 8, `${lpath}.gates.${gname}: the exponent must be a whole number from 1 to 8, got ${
|
|
1070
|
+
need(Number.isInteger(p) && p >= 1 && p <= 8, `${lpath}.gates.${gname}: the exponent must be a whole number from 1 to 8, got ${shown(p)}`);
|
|
1052
1071
|
return [q, p];
|
|
1053
1072
|
});
|
|
1054
1073
|
}
|
|
@@ -1081,6 +1100,8 @@ function normalizeConductor(mat, mname, path, species, speciesIndex) {
|
|
|
1081
1100
|
for (const k of ['epsr', 'species', 'statistics']) {
|
|
1082
1101
|
need(mat[k] === undefined, `${path}.${k}: a conductor takes only { conductor: { species, conductivity } }`);
|
|
1083
1102
|
}
|
|
1103
|
+
fields(mat, path, ['conductor', 'T']);
|
|
1104
|
+
fields(mdef, `${path}.conductor`, ['species', 'conductivity']);
|
|
1084
1105
|
need(speciesIndex.has(mdef.species), `${path}.conductor.species: unknown species ${JSON.stringify(mdef.species)}${known(speciesIndex)}`);
|
|
1085
1106
|
const i = speciesIndex.get(mdef.species);
|
|
1086
1107
|
need(species[i].z !== 0, `${path}.conductor.species: the conductor's carrier must be charged`);
|
|
@@ -1175,7 +1196,7 @@ function defaultInterfaceLinks(matL, matR, species) {
|
|
|
1175
1196
|
|
|
1176
1197
|
function transferCoefficient(v, path) {
|
|
1177
1198
|
if (v === undefined) return 0.5;
|
|
1178
|
-
need(isFiniteNumber(v) && v >= 0 && v <= 1, `${path} must be between 0 and 1, got ${
|
|
1199
|
+
need(isFiniteNumber(v) && v >= 0 && v <= 1, `${path} must be between 0 and 1, got ${shown(v)}`);
|
|
1179
1200
|
return v;
|
|
1180
1201
|
}
|
|
1181
1202
|
|
package/src/difference.js
CHANGED
|
@@ -8,13 +8,19 @@
|
|
|
8
8
|
// storage and space charge depend on δη_i − z_i δφ̂ alone; the displacement on δφ̂_R − δφ̂_L. A
|
|
9
9
|
// uniform shift of a region (z_i s on each η_i, s on φ̂) then gives exactly zero, as it must.
|
|
10
10
|
//
|
|
11
|
+
// Non-ideal statistics couple the species: c_i depends on every ζ_j, so a node's storage of i
|
|
12
|
+
// has a term in each δη_j − z_j δφ̂ (K_ij), and a flux has side terms in those at either end (its
|
|
13
|
+
// c_L and the excess potentials in its drift). Each is a term in the same differences, so the
|
|
14
|
+
// shift still gives exactly zero.
|
|
15
|
+
//
|
|
11
16
|
// Indices are compact (the solver's rix), with the sink (index N) for an unknown that isn't one:
|
|
12
17
|
// vectors passed in are read there as zero.
|
|
13
18
|
export class DifferenceTerms {
|
|
14
|
-
constructor(N, nodes, segments) {
|
|
19
|
+
constructor(N, nodes, segments, sides = 0) {
|
|
15
20
|
this.N = N;
|
|
16
21
|
this.dead = new Uint8Array(N + 1); // rows replaced since (a contact's level, a held stretch)
|
|
17
|
-
this.
|
|
22
|
+
this.nR = new Int32Array(nodes); // node term: its balance row, η column, φ̂ column (= Gauss row)
|
|
23
|
+
this.nI = new Int32Array(nodes);
|
|
18
24
|
this.nP = new Int32Array(nodes);
|
|
19
25
|
this.nZ = new Float64Array(nodes);
|
|
20
26
|
this.nMass = new Float64Array(nodes); // storage per unit (δη − z δφ̂), times 1/dt (or iω)
|
|
@@ -24,6 +30,9 @@ export class DifferenceTerms {
|
|
|
24
30
|
this.sC = new Float64Array(3 * segments); // a, b, d
|
|
25
31
|
this.dI = new Int32Array(2 * segments); // displacement: φ̂_L, φ̂_R
|
|
26
32
|
this.dK = new Float64Array(segments);
|
|
33
|
+
this.xI = new Int32Array(4 * sides); // a flux's side term: its two rows, an η column, its φ̂ column
|
|
34
|
+
this.xZ = new Float64Array(sides);
|
|
35
|
+
this.xC = new Float64Array(sides);
|
|
27
36
|
this.combos = []; // [rk, ri, w]: row rk += w × row ri (strictly neutral nodes' charge rows)
|
|
28
37
|
this.t = new Float64Array(N + 1);
|
|
29
38
|
this.x = new Float64Array(N + 1);
|
|
@@ -33,13 +42,16 @@ export class DifferenceTerms {
|
|
|
33
42
|
// Afresh for an assembly; `rate` multiplies storage in captured rows (1/dt).
|
|
34
43
|
reset(rate = 0) {
|
|
35
44
|
this.rate = rate;
|
|
36
|
-
this.nn = this.ns = this.nd = 0;
|
|
45
|
+
this.nn = this.ns = this.nd = this.nx = 0;
|
|
37
46
|
this.dead.fill(0);
|
|
38
47
|
this.combos.length = 0;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
// Storage in row r (mass) and charge in Gauss row p, per unit δη_i − z δφ̂ (row r = i, unless
|
|
51
|
+
// non-ideal statistics couple the species).
|
|
52
|
+
node(i, p, z, mass, charge, r = i) {
|
|
42
53
|
const k = this.nn++;
|
|
54
|
+
this.nR[k] = r;
|
|
43
55
|
this.nI[k] = i;
|
|
44
56
|
this.nP[k] = p;
|
|
45
57
|
this.nZ[k] = z;
|
|
@@ -59,6 +71,17 @@ export class DifferenceTerms {
|
|
|
59
71
|
this.sC[3 * k + 2] = d;
|
|
60
72
|
}
|
|
61
73
|
|
|
74
|
+
// A flux from row rL to row rR, per unit δη_i − z δφ̂ at one end.
|
|
75
|
+
side(rL, rR, i, p, z, w) {
|
|
76
|
+
const k = this.nx++;
|
|
77
|
+
this.xI[4 * k] = rL;
|
|
78
|
+
this.xI[4 * k + 1] = rR;
|
|
79
|
+
this.xI[4 * k + 2] = i;
|
|
80
|
+
this.xI[4 * k + 3] = p;
|
|
81
|
+
this.xZ[k] = z;
|
|
82
|
+
this.xC[k] = w;
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
displacement(pL, pR, k) {
|
|
63
86
|
const q = this.nd++;
|
|
64
87
|
this.dI[2 * q] = pL;
|
|
@@ -83,7 +106,7 @@ export class DifferenceTerms {
|
|
|
83
106
|
for (let k = 0; k < this.nn; k++) {
|
|
84
107
|
const i = this.nI[k], p = this.nP[k], z = this.nZ[k];
|
|
85
108
|
let f = 0;
|
|
86
|
-
if (row ===
|
|
109
|
+
if (row === this.nR[k]) f += mass * this.nMass[k];
|
|
87
110
|
if (row === p) f += this.nCharge[k];
|
|
88
111
|
if (f === 0) continue;
|
|
89
112
|
add(i, f);
|
|
@@ -99,6 +122,12 @@ export class DifferenceTerms {
|
|
|
99
122
|
add(this.sI[4 * k + 2], s * (-z * a - d));
|
|
100
123
|
add(this.sI[4 * k + 3], s * d);
|
|
101
124
|
}
|
|
125
|
+
for (let k = 0; k < this.nx; k++) {
|
|
126
|
+
const s = row === this.xI[4 * k] ? 1 : row === this.xI[4 * k + 1] ? -1 : 0;
|
|
127
|
+
if (s === 0) continue;
|
|
128
|
+
add(this.xI[4 * k + 2], s * this.xC[k]);
|
|
129
|
+
add(this.xI[4 * k + 3], -s * this.xZ[k] * this.xC[k]);
|
|
130
|
+
}
|
|
102
131
|
for (let q = 0; q < this.nd; q++) {
|
|
103
132
|
const pL = this.dI[2 * q], pR = this.dI[2 * q + 1], s = row === pL ? 1 : row === pR ? -1 : 0;
|
|
104
133
|
if (s === 0) continue;
|
|
@@ -114,10 +143,10 @@ export class DifferenceTerms {
|
|
|
114
143
|
for (let j = 0; j < N; j++) x[j] = v[j];
|
|
115
144
|
x[N] = 0;
|
|
116
145
|
t.fill(0);
|
|
117
|
-
const { nI, nP, nZ, nMass, nCharge } = this;
|
|
146
|
+
const { nR, nI, nP, nZ, nMass, nCharge } = this;
|
|
118
147
|
for (let k = 0; k < this.nn; k++) {
|
|
119
148
|
const i = nI[k], p = nP[k], w = x[i] - nZ[k] * x[p];
|
|
120
|
-
t[
|
|
149
|
+
t[nR[k]] += mass * nMass[k] * w;
|
|
121
150
|
t[p] += steady * nCharge[k] * w;
|
|
122
151
|
}
|
|
123
152
|
if (steady !== 0) {
|
|
@@ -128,6 +157,12 @@ export class DifferenceTerms {
|
|
|
128
157
|
t[iL] += dN;
|
|
129
158
|
t[iR] -= dN;
|
|
130
159
|
}
|
|
160
|
+
const { xI, xZ, xC } = this;
|
|
161
|
+
for (let k = 0; k < this.nx; k++) {
|
|
162
|
+
const dN = steady * xC[k] * (x[xI[4 * k + 2]] - xZ[k] * x[xI[4 * k + 3]]);
|
|
163
|
+
t[xI[4 * k]] += dN;
|
|
164
|
+
t[xI[4 * k + 1]] -= dN;
|
|
165
|
+
}
|
|
131
166
|
for (let q = 0; q < this.nd; q++) {
|
|
132
167
|
const pL = dI[2 * q], pR = dI[2 * q + 1], dD = -steady * dK[q] * (x[pR] - x[pL]);
|
|
133
168
|
t[pL] += dD;
|
package/src/errors.js
CHANGED
|
@@ -39,3 +39,21 @@ export function checkOptions(where, opts, allowed, Kind = DeviceError) {
|
|
|
39
39
|
throw new Kind(`${where}: no option '${k}'${near ? ` (did you mean '${near}'?)` : ''}; the options are ${allowed.join(', ')}`);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A value as an error message shows it: a number as itself (NaN and Infinity too, which JSON
|
|
45
|
+
* would print as null), a string quoted and called one ("350", a string, where a number was
|
|
46
|
+
* wanted).
|
|
47
|
+
* @param {unknown} v
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function shown(v) {
|
|
51
|
+
if (typeof v === 'number' || typeof v === 'bigint' || v === undefined) return String(v);
|
|
52
|
+
if (typeof v === 'string') return `${JSON.stringify(v)} (a string)`;
|
|
53
|
+
if (typeof v === 'function') return 'a function';
|
|
54
|
+
try {
|
|
55
|
+
return JSON.stringify(v, (k, x) => (typeof x === 'number' && !Number.isFinite(x) ? String(x) : x)) ?? String(v);
|
|
56
|
+
} catch {
|
|
57
|
+
return String(v);
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/index.js
CHANGED
|
@@ -187,7 +187,9 @@ export class Device {
|
|
|
187
187
|
// A solve that fails reports the state it failed in, and leaves the device as it was: a
|
|
188
188
|
// device with no steady state (a host filling at a current) can still advance() from there.
|
|
189
189
|
const snap = this.solver._snapshot();
|
|
190
|
-
const
|
|
190
|
+
const result = this.solver.solveSteady(opts);
|
|
191
|
+
// (a failed solve's state isn't a solution: its accuracy warnings are left out)
|
|
192
|
+
const sol = makeSolution(this.solver, result.converged ? result : { ...result, unsolved: true });
|
|
191
193
|
if (!sol.converged) this.solver._restore(snap);
|
|
192
194
|
return sol;
|
|
193
195
|
}
|
package/src/kit.js
CHANGED
|
@@ -18,7 +18,7 @@ export { check } from './checks.js';
|
|
|
18
18
|
export { particles } from './particles.js';
|
|
19
19
|
export { polarization } from './polarization.js';
|
|
20
20
|
export { perovskiteCell, PEROVSKITE_SCANS, hysteresis } from './devices.js';
|
|
21
|
-
export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal } from './data.js';
|
|
21
|
+
export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal, trap } from './data.js';
|
|
22
22
|
|
|
23
23
|
const fail = (message) => {
|
|
24
24
|
throw new DeviceError(message);
|
package/src/solution.js
CHANGED
|
@@ -292,6 +292,13 @@ export function makeSolution(solver, result = {}) {
|
|
|
292
292
|
'Check the definition with describe() (from driftlet/kit); approach this state in smaller steps (a sweep, or advance() in time); or refine the grid.',
|
|
293
293
|
);
|
|
294
294
|
}
|
|
295
|
+
// The warnings below judge a solution's accuracy (its grid, its double layers). Of the state a
|
|
296
|
+
// steady solve gave up in, which isn't a solution, they say nothing useful (a driven current
|
|
297
|
+
// it couldn't pass leaves the device at ±26 V, where depletion reaches the contacts), so they're
|
|
298
|
+
// left out there; the two about the state itself (degenerate carriers, a species denser than
|
|
299
|
+
// any material) stay, as possible causes, and say they're of that state. (A failed time step
|
|
300
|
+
// restores the last good state, which is a solution: its warnings all stand.)
|
|
301
|
+
const solved = !result.unsolved, stopped = solved ? '' : ' (in the state the unconverged solve stopped in)';
|
|
295
302
|
const debye = (g) => {
|
|
296
303
|
const mat = model.materials[model.regions[grid.nodeRegion[g]].material];
|
|
297
304
|
const s2 = solver.screening(g); // zᵀKz: Σ z² c for ideal statistics
|
|
@@ -347,7 +354,7 @@ export function makeSolution(solver, result = {}) {
|
|
|
347
354
|
);
|
|
348
355
|
};
|
|
349
356
|
interfaces.forEach((itf, f) => {
|
|
350
|
-
if (itf.phi.type === 'neutral') return;
|
|
357
|
+
if (itf.phi.type === 'neutral' || !solved) return;
|
|
351
358
|
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1];
|
|
352
359
|
check(gL, grid.segLength[gL - 1], `interfaces[${f}] (left side)`);
|
|
353
360
|
check(gR, grid.segLength[gR], `interfaces[${f}] (right side)`);
|
|
@@ -357,11 +364,11 @@ export function makeSolution(solver, result = {}) {
|
|
|
357
364
|
boxed(gR, grid.segLength[gR], itf.phi, gL, `interfaces[${f}] (right side)`, sigma, reacting(1));
|
|
358
365
|
});
|
|
359
366
|
const resolves = (ct) => ct.phi.type === 'capacitive' || ct.phi.type === 'pinned';
|
|
360
|
-
if (resolves(contacts.left)) {
|
|
367
|
+
if (solved && resolves(contacts.left)) {
|
|
361
368
|
check(0, grid.segLength[0], 'contacts.left');
|
|
362
369
|
boxed(0, grid.segLength[0], contacts.left.phi, -1, 'contacts.left');
|
|
363
370
|
}
|
|
364
|
-
if (resolves(contacts.right)) {
|
|
371
|
+
if (solved && resolves(contacts.right)) {
|
|
365
372
|
check(nNodes - 1, grid.segLength[nNodes - 2], 'contacts.right');
|
|
366
373
|
boxed(nNodes - 1, grid.segLength[nNodes - 2], contacts.right.phi, -1, 'contacts.right');
|
|
367
374
|
}
|
|
@@ -380,7 +387,7 @@ export function makeSolution(solver, result = {}) {
|
|
|
380
387
|
}
|
|
381
388
|
if (top > mat.cRef[i]) {
|
|
382
389
|
sol.warnings.push(
|
|
383
|
-
`${reg.name}: ${sp.name} reaches ${top.toExponential(2)} mol/m³ at x = ${at.toExponential(3)} m, above its cRef (${mat.cRef[i].toExponential(2)}, the band's effective density of states), where Boltzmann statistics no longer hold: ` +
|
|
390
|
+
`${reg.name}${stopped}: ${sp.name} reaches ${top.toExponential(2)} mol/m³ at x = ${at.toExponential(3)} m, above its cRef (${mat.cRef[i].toExponential(2)}, the band's effective density of states), where Boltzmann statistics no longer hold: ` +
|
|
384
391
|
'give the material Fermi–Dirac statistics (docs/statistics.md), or ask whether this state is physical (a surface driven past what can carry its carriers away).',
|
|
385
392
|
);
|
|
386
393
|
}
|
|
@@ -410,7 +417,7 @@ export function makeSolution(solver, result = {}) {
|
|
|
410
417
|
}
|
|
411
418
|
if (top > 1e6) {
|
|
412
419
|
sol.warnings.push(
|
|
413
|
-
`${reg.name}: ${sp.name} reaches ${top.toExponential(2)} mol/m³ at x = ${at.toExponential(3)} m, denser than any material: ` +
|
|
420
|
+
`${reg.name}${stopped}: ${sp.name} reaches ${top.toExponential(2)} mol/m³ at x = ${at.toExponential(3)} m, denser than any material: ` +
|
|
414
421
|
'a level set where the device cannot follow it. Check what holds it there (the V or bath of a contact, its mu0 and cRef) with describe() from driftlet/kit.',
|
|
415
422
|
);
|
|
416
423
|
}
|
|
@@ -421,7 +428,7 @@ export function makeSolution(solver, result = {}) {
|
|
|
421
428
|
// ρ λ_D at the node beside it; against the region's own space charge (half Σ|ρ| over it, the
|
|
422
429
|
// two signs of a double layer), warn past 1e-3.
|
|
423
430
|
for (const side of ['left', 'right']) {
|
|
424
|
-
if (contacts[side].phi.type !== 'bulk') continue;
|
|
431
|
+
if (contacts[side].phi.type !== 'bulk' || !solved) continue;
|
|
425
432
|
const r = side === 'left' ? 0 : model.regions.length - 1, a = grid.regionStart[r], b = grid.regionEnd[r];
|
|
426
433
|
const mat = model.materials[model.regions[r].material];
|
|
427
434
|
if (!(mat.epsr > 0) || mat.conductor || b - a < 3) continue;
|
|
@@ -458,7 +465,7 @@ export function makeSolution(solver, result = {}) {
|
|
|
458
465
|
// a concentration ratio r across it). Flag a region's end cell whose field is well above its
|
|
459
466
|
// neighbour's while its concentration changes steeply, for species carrying the current. (A
|
|
460
467
|
// pn depletion region's field changes smoothly from cell to cell, which SG handles.)
|
|
461
|
-
if (Number.isFinite(sol.current)) {
|
|
468
|
+
if (solved && Number.isFinite(sol.current)) {
|
|
462
469
|
const share = species.map((sp, i) =>
|
|
463
470
|
Math.max(Math.abs(sp.z * (sol.contacts.left.flux[sp.name] ?? 0)), Math.abs(sp.z * (sol.contacts.right.flux[sp.name] ?? 0))),
|
|
464
471
|
);
|