driftlet 1.0.0 → 1.1.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 +2 -2
- package/docs/data.md +125 -10
- package/docs/device.md +32 -12
- package/docs/kit.md +19 -3
- package/docs/reliability.md +1 -0
- 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 +100 -11
- package/src/describe.js +1 -1
- package/src/device.js +39 -18
- package/src/errors.js +18 -0
- package/src/index.js +3 -1
- package/src/kit.js +1 -1
- package/src/polarization.js +2 -2
- package/src/solution.js +14 -7
- package/src/solver.js +48 -14
- package/src/stack.js +2 -1
- package/src/types.js +4 -1
- package/types/data.d.ts +42 -5
- package/types/errors.d.ts +8 -0
- package/types/kit.d.ts +1 -1
- package/types/solver.d.ts +2 -0
- package/types/types.d.ts +7 -1
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
|
*/
|
|
@@ -84,21 +85,49 @@ export function aqueous(names, { epsr = WATER_EPSR, material = 'water' } = {}) {
|
|
|
84
85
|
return { species, materials: { [material]: { epsr, species: entries } } };
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// How the semiconductors' data move with temperature, for semiconductor(name, { T }):
|
|
89
|
+
// - the gap by Varshni's form, Eg(T) = Eg0 − αT²/(T + β) (Y. P. Varshni, Physica 34, 149, 1967),
|
|
90
|
+
// with Thurmond's parameters (C. D. Thurmond, J. Electrochem. Soc. 122, 1133, 1975, as
|
|
91
|
+
// tabulated in Sze, 2nd ed.): Eg0 (eV), alpha (eV/K), beta (K). Only the shift from 300 K is
|
|
92
|
+
// used, on the table's Eg, so T = 300 K is the table: Varshni's own Eg(300) differs from it by
|
|
93
|
+
// 4.5 meV (Si, 1.1245), 3.4 meV (Ge, 0.6634) and 1.5 meV (GaAs, 1.4225).
|
|
94
|
+
// - Nc and Nv ∝ T^1.5 (parabolic bands, effective masses held fixed).
|
|
95
|
+
// - mobilities ∝ T^−gn and T^−gp, the lattice-limited power laws for lightly doped material
|
|
96
|
+
// (Sze, 2nd ed., ch. 1): approximate, and wrong where impurity scattering matters (heavy
|
|
97
|
+
// doping, or cold). D follows from μ and T by Einstein's relation, D = μkT/q.
|
|
98
|
+
// - the electron affinity chi isn't moved.
|
|
99
|
+
const WITH_T = {
|
|
100
|
+
Si: { Eg0: 1.17, alpha: 4.73e-4, beta: 636, gn: 2.42, gp: 2.2 },
|
|
101
|
+
Ge: { Eg0: 0.7437, alpha: 4.774e-4, beta: 235, gn: 1.66, gp: 2.33 },
|
|
102
|
+
GaAs: { Eg0: 1.519, alpha: 5.405e-4, beta: 204, gn: 1.0, gp: 2.1 },
|
|
103
|
+
};
|
|
104
|
+
const T_RANGE = [150, 500];
|
|
105
|
+
|
|
87
106
|
/**
|
|
88
107
|
* 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
|
|
108
|
+
* c_ref = Nc and Nv, D from the mobilities by the Einstein relation at T, and μ°_e⁻ = 0 (φ
|
|
109
|
+
* anchored at the conduction band) and μ°_h⁺ = Eg·F. The data are for 300 K; another T (150 to
|
|
110
|
+
* 500 K) moves them: the gap by Varshni's shift from 300 K, Nc and Nv as T^1.5, the mobilities by
|
|
111
|
+
* lattice-limited power laws (light doping). The material carries its `T`, and a device at
|
|
112
|
+
* another temperature warns: give the device the same T.
|
|
91
113
|
* Statistics are ideal (Boltzmann); add Fermi–Dirac to the material for degenerate doping.
|
|
92
114
|
* @param {string} name 'Si', 'Ge' or 'GaAs'
|
|
93
|
-
* @param {{ material?: string }} [opts]
|
|
115
|
+
* @param {{ material?: string, T?: number }} [opts] the material's name, and the temperature
|
|
116
|
+
* (K, default 300, the table's)
|
|
94
117
|
* @returns {{ species: object[], materials: Record<string, object> }}
|
|
95
118
|
*/
|
|
96
|
-
export function semiconductor(name, { material = name } = {}) {
|
|
97
|
-
|
|
98
|
-
checkOptions('semiconductor', arguments[1], ['material']);
|
|
119
|
+
export function semiconductor(name, { material = name, T = 300 } = {}) {
|
|
120
|
+
checkOptions('semiconductor', arguments[1], ['material', 'T']);
|
|
99
121
|
const sc = SEMICONDUCTORS[name];
|
|
100
122
|
if (!sc) fail(`semiconductor: no data for '${name}' (the library has ${Object.keys(SEMICONDUCTORS).join(', ')})`);
|
|
101
|
-
|
|
123
|
+
if (typeof T !== 'number') fail(`semiconductor: T must be a number, the temperature in K, got ${shown(T)}`);
|
|
124
|
+
if (!(Number.isFinite(T) && T >= T_RANGE[0] && T <= T_RANGE[1])) {
|
|
125
|
+
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}`);
|
|
126
|
+
}
|
|
127
|
+
const { alpha, beta, gn, gp } = WITH_T[name];
|
|
128
|
+
const t = T / 300, varshni = (T) => (alpha * T * T) / (T + beta);
|
|
129
|
+
const Eg = sc.Eg - (varshni(T) - varshni(300));
|
|
130
|
+
const VT = (GAS_CONSTANT * T) / FARADAY;
|
|
102
131
|
const perCm3 = (n) => (n * 1e6) / AVOGADRO;
|
|
103
132
|
return {
|
|
104
133
|
species: [
|
|
@@ -107,16 +136,76 @@ export function semiconductor(name, { material = name } = {}) {
|
|
|
107
136
|
],
|
|
108
137
|
materials: {
|
|
109
138
|
[material]: {
|
|
139
|
+
T, // the temperature the data are for: a device at another warns
|
|
110
140
|
epsr: sc.epsr,
|
|
111
141
|
species: {
|
|
112
|
-
'e-': { D: sc.mun * 1e-4 * VT, mu0: 0, cRef: perCm3(sc.Nc) },
|
|
113
|
-
'h+': { D: sc.mup * 1e-4 * VT, mu0:
|
|
142
|
+
'e-': { D: sc.mun * powr(t, -gn) * 1e-4 * VT, mu0: 0, cRef: perCm3(sc.Nc * powr(t, 1.5)) },
|
|
143
|
+
'h+': { D: sc.mup * powr(t, -gp) * 1e-4 * VT, mu0: Eg * FARADAY, cRef: perCm3(sc.Nv * powr(t, 1.5)) },
|
|
114
144
|
},
|
|
115
145
|
},
|
|
116
146
|
},
|
|
117
147
|
};
|
|
118
148
|
}
|
|
119
149
|
|
|
150
|
+
/**
|
|
151
|
+
* A trap level in a semiconductor that holds charge: its two charge states as immobile species
|
|
152
|
+
* (D = 0), filled and emptied by capturing carriers, e⁻ + X⁰ = X⁻ and X⁻ + h⁺ = X⁰ (acceptor-like)
|
|
153
|
+
* or X⁺ + e⁻ = X⁰ and X⁰ + h⁺ = X⁺ (donor-like). In steady state it recombines at exactly the
|
|
154
|
+
* `srh` law's rate with the same τ_n, τ_p and n₁, and its charge is in Poisson's equation, which
|
|
155
|
+
* the `srh` law (the trap's occupancy eliminated) leaves out. Each node keeps its own total.
|
|
156
|
+
* @param {{ species: object[], materials: Record<string, object> }} lib a library entry with e⁻
|
|
157
|
+
* and h⁺ (semiconductor()), returned with the trap's states added
|
|
158
|
+
* @param {{ density: number, tauN: number, tauP: number, n1?: number, charge?: 'acceptor' | 'donor',
|
|
159
|
+
* name?: string, material?: string, T?: number }} opts density N_t (mol/m³); τ_n = 1/(c_n N_t)
|
|
160
|
+
* and τ_p = 1/(c_p N_t) (s) at that density; n₁, the electron density that half-fills it
|
|
161
|
+
* (mol/m³; default n_i, midgap); T (K, default the material's T, else 300)
|
|
162
|
+
* @returns {{ library: object, bulkReactions: object[], c0: Record<string, number> }} the library
|
|
163
|
+
* for build(), the capture reactions, and c0 for each layer holding the trap (its total N_t)
|
|
164
|
+
*/
|
|
165
|
+
export function trap(lib, { density, tauN, tauP, n1, charge = 'acceptor', name = 'X', material, T } = {}) {
|
|
166
|
+
checkOptions('trap', arguments[1], ['density', 'tauN', 'tauP', 'n1', 'charge', 'name', 'material', 'T']);
|
|
167
|
+
if (!lib || typeof lib !== 'object' || typeof lib.materials !== 'object') fail('trap: give the library entry first, semiconductor(\'Si\') say, then { density, tauN, tauP }');
|
|
168
|
+
const names = Object.keys(lib.materials);
|
|
169
|
+
const m = material ?? (names.length === 1 ? names[0] : fail(`trap: the library has materials ${names.join(', ')}: say which, with material`));
|
|
170
|
+
const mat = lib.materials[m];
|
|
171
|
+
if (!mat) fail(`trap: no material '${m}' in the library (it has ${names.join(', ')})`);
|
|
172
|
+
const el = mat.species?.['e-'], ho = mat.species?.['h+'];
|
|
173
|
+
if (!el || !ho) fail(`trap: material '${m}' needs e- and h+ (a semiconductor)`);
|
|
174
|
+
const pos = (v, what) => (Number.isFinite(v) && v > 0 ? v : fail(`trap: ${what}, got ${shown(v)}`));
|
|
175
|
+
pos(density, 'density must be the trap density N_t > 0 (mol/m³; units.perCm3(1e15), say)');
|
|
176
|
+
pos(tauN, 'tauN must be the electron lifetime τ_n = 1/(c_n N_t) > 0 (s)');
|
|
177
|
+
pos(tauP, 'tauP must be the hole lifetime τ_p = 1/(c_p N_t) > 0 (s)');
|
|
178
|
+
// (the material's own temperature, where it gives one: semiconductor()'s does)
|
|
179
|
+
if (T === undefined) T = mat.T ?? 300;
|
|
180
|
+
pos(T, 'T must be the temperature in K');
|
|
181
|
+
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`);
|
|
182
|
+
if (charge !== 'acceptor' && charge !== 'donor') fail(`trap: charge must be 'acceptor' (0/−) or 'donor' (+/0), got ${JSON.stringify(charge)}`);
|
|
183
|
+
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)}`);
|
|
184
|
+
const RT = GAS_CONSTANT * T, cRefE = el.cRef ?? lib.species.find((sp) => sp.name === 'e-')?.cRef;
|
|
185
|
+
const cRefH = ho.cRef ?? lib.species.find((sp) => sp.name === 'h+')?.cRef;
|
|
186
|
+
const ni = Math.sqrt(cRefE * cRefH * Math.exp(-(ho.mu0 - el.mu0) / RT));
|
|
187
|
+
const half = n1 === undefined ? ni : pos(n1, 'n1 must be the electron density that half-fills the trap > 0 (mol/m³)');
|
|
188
|
+
// The filled state's μ° sits an electron's μ at n₁ above the empty one's: half full at n = n₁.
|
|
189
|
+
const [empty, full] = charge === 'acceptor' ? [`${name}0`, `${name}-`] : [`${name}+`, `${name}0`];
|
|
190
|
+
const zEmpty = charge === 'acceptor' ? 0 : 1;
|
|
191
|
+
const states = {
|
|
192
|
+
[empty]: { D: 0, mu0: 0, cRef: density },
|
|
193
|
+
[full]: { D: 0, mu0: el.mu0 + RT * Math.log(half / cRefE), cRef: density },
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
library: {
|
|
197
|
+
species: [...lib.species, { name: empty, z: zEmpty }, { name: full, z: zEmpty - 1 }],
|
|
198
|
+
materials: { ...lib.materials, [m]: { ...mat, species: { ...mat.species, ...states } } },
|
|
199
|
+
},
|
|
200
|
+
bulkReactions: [
|
|
201
|
+
{ equation: `e- + ${empty} = ${full}`, kf: { [m]: 1 / (tauN * density) } },
|
|
202
|
+
{ equation: `${full} + h+ = ${empty}`, kf: { [m]: 1 / (tauP * density) } },
|
|
203
|
+
],
|
|
204
|
+
// (each node keeps the total, X⁰ + X⁻ say; the split is the solve's to find)
|
|
205
|
+
c0: { [empty]: density / 2, [full]: density / 2 },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
120
209
|
/**
|
|
121
210
|
* A metal from `METALS` as a conductor material, its carrier `e-`.
|
|
122
211
|
* @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,17 @@ export function normalizeDevice(def) {
|
|
|
306
308
|
};
|
|
307
309
|
});
|
|
308
310
|
|
|
311
|
+
// A material whose data are for another temperature than the device's gives the wrong n_i, gap
|
|
312
|
+
// and mobilities, silently: say so (for each material a region uses).
|
|
313
|
+
materials.forEach((m, k) => {
|
|
314
|
+
if (m.dataT === undefined || Math.abs(m.dataT - T) <= 0.01 || !regions.some((reg) => reg.material === k)) return;
|
|
315
|
+
warnings.push(
|
|
316
|
+
`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)' : ''}, ` +
|
|
317
|
+
`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}, ` +
|
|
318
|
+
`or remake the material for ${T} K (semiconductor(name, { T: ${T} }))`,
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
309
322
|
// --- interfaces (one per face between consecutive regions)
|
|
310
323
|
const nFaces = regions.length - 1;
|
|
311
324
|
const idefs = def.interfaces ?? [];
|
|
@@ -355,8 +368,8 @@ export function normalizeDevice(def) {
|
|
|
355
368
|
// A contact that passes nothing can't be driven by a current.
|
|
356
369
|
for (const side of ['left', 'right']) {
|
|
357
370
|
const ct = contacts[side];
|
|
358
|
-
const passes = ct.species.some((l) => l.type !== 'blocked') || ct.phi.type === 'capacitive' || ct.phi.type === 'pinned';
|
|
359
|
-
need(passes || ct.drive.kind === 'V', `contacts.${side}.I: this contact passes no current (no linked species, no gate)`);
|
|
371
|
+
const passes = ct.species.some((l, i) => l.type !== 'blocked' && species[i].z !== 0) || ct.phi.type === 'capacitive' || ct.phi.type === 'pinned';
|
|
372
|
+
need(passes || ct.drive.kind === 'V', `contacts.${side}.I: this contact passes no current (no linked charged species, no gate)`);
|
|
360
373
|
ct.passes = passes;
|
|
361
374
|
}
|
|
362
375
|
checkLevel(terminals.map((t) => t.drive), terminals.map((t) => (t.kind === 'port' ? ports[t.index] : contacts[t.side])));
|
|
@@ -490,7 +503,7 @@ function stoichiometry(map, path, fixed, fixedPath, species, speciesIndex, RT) {
|
|
|
490
503
|
const list = [];
|
|
491
504
|
let fixedA = 0, charge = 0, nFixed = 0;
|
|
492
505
|
for (const [name, nu] of Object.entries(map)) {
|
|
493
|
-
need(Number.isInteger(nu) && nu !== 0, `${path}.${name} must be a non-zero integer, got ${
|
|
506
|
+
need(Number.isInteger(nu) && nu !== 0, `${path}.${name} must be a non-zero integer, got ${shown(nu)}`);
|
|
494
507
|
if (speciesIndex.has(name)) {
|
|
495
508
|
const i = speciesIndex.get(name);
|
|
496
509
|
list.push({ i, nu });
|
|
@@ -889,7 +902,13 @@ function portReactions(pdef, path, mat, terminal, species, speciesIndex, RT, sur
|
|
|
889
902
|
need(part.some((p) => p.side === 0 || p.side === 2), `${rpath}: no species of the port's region or its surface takes part`);
|
|
890
903
|
need(charge === 0, `${rpath}: charge is not balanced (Σ ν z = ${charge})`);
|
|
891
904
|
need(rdef.bare === undefined || typeof rdef.bare === 'boolean', `${rpath}.bare must be true or false`);
|
|
892
|
-
|
|
905
|
+
const alpha = transferCoefficient(rdef.alpha, `${rpath}.alpha`), bare = rdef.bare === true;
|
|
906
|
+
// Langmuir sites: the reaction fills Δ of them going forward, and its transition state holds
|
|
907
|
+
// m bare ones (those it fills, or one if it runs only on bare metal), so the forward rate
|
|
908
|
+
// goes as θ₀^m and the backward as θ₀^(m−Δ): the prefactor's θ₀^(m(1−α)+(m−Δ)α), with each
|
|
909
|
+
// surface species at its coverage θ.
|
|
910
|
+
const fills = part.reduce((t, p) => t + (p.side === 2 ? p.nu : 0), 0), m = Math.max(fills, 0, bare ? 1 : 0);
|
|
911
|
+
return { part, fixedA, k0: positive(rdef.k0, `${rpath}.k0 (mol/(m²·s))`), alpha, bare, sites: m * (1 - alpha) + (m - fills) * alpha };
|
|
893
912
|
});
|
|
894
913
|
}
|
|
895
914
|
|
|
@@ -1042,7 +1061,7 @@ function gatedBy(raw, gates, lpath, where) {
|
|
|
1042
1061
|
return Object.entries(raw).map(([gname, p]) => {
|
|
1043
1062
|
const q = gates.findIndex((gt) => gt.name === gname);
|
|
1044
1063
|
need(q >= 0, `${lpath}.gates.${gname}: no such gate${gates.length ? ` (there are ${gates.map((gt) => gt.name).join(', ')})` : ` (define it in ${where}.gates)`}`);
|
|
1045
|
-
need(Number.isInteger(p) && p >= 1 && p <= 8, `${lpath}.gates.${gname}: the exponent must be a whole number from 1 to 8, got ${
|
|
1064
|
+
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)}`);
|
|
1046
1065
|
return [q, p];
|
|
1047
1066
|
});
|
|
1048
1067
|
}
|
|
@@ -1075,6 +1094,8 @@ function normalizeConductor(mat, mname, path, species, speciesIndex) {
|
|
|
1075
1094
|
for (const k of ['epsr', 'species', 'statistics']) {
|
|
1076
1095
|
need(mat[k] === undefined, `${path}.${k}: a conductor takes only { conductor: { species, conductivity } }`);
|
|
1077
1096
|
}
|
|
1097
|
+
fields(mat, path, ['conductor', 'T']);
|
|
1098
|
+
fields(mdef, `${path}.conductor`, ['species', 'conductivity']);
|
|
1078
1099
|
need(speciesIndex.has(mdef.species), `${path}.conductor.species: unknown species ${JSON.stringify(mdef.species)}${known(speciesIndex)}`);
|
|
1079
1100
|
const i = speciesIndex.get(mdef.species);
|
|
1080
1101
|
need(species[i].z !== 0, `${path}.conductor.species: the conductor's carrier must be charged`);
|
|
@@ -1169,7 +1190,7 @@ function defaultInterfaceLinks(matL, matR, species) {
|
|
|
1169
1190
|
|
|
1170
1191
|
function transferCoefficient(v, path) {
|
|
1171
1192
|
if (v === undefined) return 0.5;
|
|
1172
|
-
need(isFiniteNumber(v) && v >= 0 && v <= 1, `${path} must be between 0 and 1, got ${
|
|
1193
|
+
need(isFiniteNumber(v) && v >= 0 && v <= 1, `${path} must be between 0 and 1, got ${shown(v)}`);
|
|
1173
1194
|
return v;
|
|
1174
1195
|
}
|
|
1175
1196
|
|
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/polarization.js
CHANGED
|
@@ -115,10 +115,10 @@ export function polarization(device, sol, where, V, opts = {}) {
|
|
|
115
115
|
const out = parts.map(({ rx, k, base, s, q, pref, surf }) => {
|
|
116
116
|
const rate = Float64Array.from(Vs, (v, j) => {
|
|
117
117
|
const { lnA, bare } = at[j];
|
|
118
|
-
let a = base + s * v, p = pref * (rx.
|
|
118
|
+
let a = base + s * v, p = pref * (rx.sites ? powr(bare, rx.sites) : 1);
|
|
119
119
|
for (const u of surf) {
|
|
120
120
|
a -= u.nu * lnA[u.s];
|
|
121
|
-
p *= Math.exp(u.e * lnA[u.s]);
|
|
121
|
+
p *= Math.exp(u.e * lnA[u.s]) * powr(bare, u.e); // its coverage θ = θ₀ a
|
|
122
122
|
}
|
|
123
123
|
return rx.vmax ? p * -Math.expm1(-a) : p * (Math.exp(rx.alpha * a) - Math.exp(-(1 - rx.alpha) * a));
|
|
124
124
|
});
|
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
|
);
|
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) => {
|
|
@@ -1968,9 +1990,9 @@ export class Solver {
|
|
|
1968
1990
|
}
|
|
1969
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;
|
|
1970
1992
|
a -= p.nu * (u[o] + uLo[o]);
|
|
1971
|
-
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);
|
|
1972
1994
|
}
|
|
1973
|
-
if (rx.
|
|
1995
|
+
if (rx.sites) pref *= powr(this.th0[g], rx.sites);
|
|
1974
1996
|
return pref * bvFactor(a, rx.alpha).g;
|
|
1975
1997
|
}
|
|
1976
1998
|
|
|
@@ -2193,6 +2215,11 @@ export class Solver {
|
|
|
2193
2215
|
if (f >= 0) this._j(b, r, this.blockOfFace[f], 0, (this.sheetSign[g] * this.model.grid.area[g]) / (z[im] * FARADAY * dt));
|
|
2194
2216
|
}
|
|
2195
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
|
+
|
|
2196
2223
|
// Dilute nodes g0…g1 (ideal statistics, c = c_ref e^ζ): storage of each species and the space
|
|
2197
2224
|
// charge in the Gauss row, written straight into the diagonal blocks by local index. A
|
|
2198
2225
|
// dielectric is the case with no species.
|
|
@@ -2212,7 +2239,7 @@ export class Solver {
|
|
|
2212
2239
|
for (let i = 0; i < n; i++) {
|
|
2213
2240
|
const k = g * n + i;
|
|
2214
2241
|
if (!present[k]) continue;
|
|
2215
|
-
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));
|
|
2216
2243
|
if (stores) res[R[lb + 1 + i]] += (v * (ck - cOld[k])) / dt;
|
|
2217
2244
|
q += F * z[i] * ck;
|
|
2218
2245
|
dq += F * z[i] * z[i] * ck;
|
|
@@ -3093,18 +3120,19 @@ export class Solver {
|
|
|
3093
3120
|
aLo -= p.nu * uLo[o];
|
|
3094
3121
|
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
3095
3122
|
if (p.side === 2) {
|
|
3096
|
-
// a surface species at
|
|
3097
|
-
pref *=
|
|
3123
|
+
// a surface species at its coverage θ = θ₀e^ζ, with ∂ln θ_s/∂η_x = δ_sx − θ_x
|
|
3124
|
+
pref *= powr(th[g * nSurf + p.s], e);
|
|
3098
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];
|
|
3099
3127
|
} else {
|
|
3100
3128
|
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
3101
3129
|
this._dlnc(g, p.i, e, d);
|
|
3102
3130
|
}
|
|
3103
3131
|
}
|
|
3104
|
-
if (rx.
|
|
3105
|
-
//
|
|
3106
|
-
for (let x = 0; x < surf.length; x++) d[1 + n + x] -= th[g * nSurf + x];
|
|
3107
|
-
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);
|
|
3108
3136
|
}
|
|
3109
3137
|
const { g: bv, gp } = bvFactor(aHi + aLo, al);
|
|
3110
3138
|
const rate = pref * bv;
|
|
@@ -3236,8 +3264,9 @@ export class Solver {
|
|
|
3236
3264
|
// passes, so their flux is zero.)
|
|
3237
3265
|
const closedIons = this.combining && dyn && this.chargeNode[g] === 1;
|
|
3238
3266
|
let I = 0;
|
|
3267
|
+
const held = this.portHeldEnd.subarray(k * n, (k + 1) * n);
|
|
3239
3268
|
for (let i = 0; i < n; i++) {
|
|
3240
|
-
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];
|
|
3241
3270
|
const nIn = active ? res[R[b * M + 1 + i]] : 0;
|
|
3242
3271
|
flux[i] = sgn * nIn; // (toward +x)
|
|
3243
3272
|
if (active && z[i] !== 0) {
|
|
@@ -3250,7 +3279,7 @@ export class Solver {
|
|
|
3250
3279
|
if (this.combining && dyn && this.lateStorage[g] === 1) {
|
|
3251
3280
|
const v = model.grid.vol[g], { cOld } = this;
|
|
3252
3281
|
for (let i = 0; i < n; i++) {
|
|
3253
|
-
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;
|
|
3254
3283
|
const ci = c[g * n + i], st = (v * (ci - cOld[g * n + i])) / dt;
|
|
3255
3284
|
res[R[b * M + 1 + i]] += st;
|
|
3256
3285
|
flux[i] += sgn * st;
|
|
@@ -4866,6 +4895,11 @@ export class Solver {
|
|
|
4866
4895
|
}
|
|
4867
4896
|
mu[name].re[g] = RT * xr[b * M + 1 + i];
|
|
4868
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
|
+
}
|
|
4869
4903
|
// δc_i = Σ_j K_ij (δη_j − z_j δφ̂)
|
|
4870
4904
|
let sr = 0, si = 0;
|
|
4871
4905
|
for (let j = 0; j < n; j++) {
|
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, checkOptions } from './errors.js';
|
|
7
|
+
import { DeviceError, checkOptions, shown } from './errors.js';
|
|
8
8
|
import { FARADAY } from './constants.js';
|
|
9
9
|
|
|
10
10
|
const fail = (message) => {
|
|
@@ -131,6 +131,7 @@ const STACK_FIELDS = ['T', 'library', 'species', 'materials', 'stack', 'bulkReac
|
|
|
131
131
|
export function build(def) {
|
|
132
132
|
if (!isObject(def)) fail('build: give { library, stack, … }');
|
|
133
133
|
fields(def, 'build', STACK_FIELDS);
|
|
134
|
+
if (def.T !== undefined && !(typeof def.T === 'number' && Number.isFinite(def.T) && def.T > 0)) fail(`build.T must be the temperature in K, a positive number, got ${shown(def.T)}`);
|
|
134
135
|
if (def.library !== undefined && !Array.isArray(def.library)) fail('build.library must be a list of { species, materials }');
|
|
135
136
|
const { species, materials } = combine(...(def.library ?? []), { species: def.species ?? [], materials: def.materials ?? {} });
|
|
136
137
|
|