driftlet 0.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/LICENSE +14 -0
- package/README.md +285 -0
- package/llms.txt +299 -0
- package/package.json +54 -0
- package/src/bernoulli.js +29 -0
- package/src/blockTridiagonal.js +492 -0
- package/src/constants.js +36 -0
- package/src/data.js +127 -0
- package/src/describe.js +190 -0
- package/src/device.js +954 -0
- package/src/equation.js +188 -0
- package/src/errors.js +8 -0
- package/src/fermi.js +106 -0
- package/src/grid.js +111 -0
- package/src/index.js +207 -0
- package/src/kit.js +76 -0
- package/src/live.js +217 -0
- package/src/merge.js +11 -0
- package/src/plot.js +267 -0
- package/src/solution.js +246 -0
- package/src/solver.js +2884 -0
- package/src/stack.js +172 -0
- package/src/statistics.js +593 -0
- package/src/traces.js +125 -0
- package/src/types.js +221 -0
- package/src/worker.js +24 -0
- package/types/bernoulli.d.ts +4 -0
- package/types/blockTridiagonal.d.ts +92 -0
- package/types/constants.d.ts +33 -0
- package/types/data.d.ts +73 -0
- package/types/describe.d.ts +15 -0
- package/types/device.d.ts +20 -0
- package/types/equation.d.ts +102 -0
- package/types/errors.d.ts +3 -0
- package/types/fermi.d.ts +3 -0
- package/types/grid.d.ts +28 -0
- package/types/index.d.ts +103 -0
- package/types/kit.d.ts +61 -0
- package/types/live.d.ts +101 -0
- package/types/merge.d.ts +1 -0
- package/types/plot.d.ts +60 -0
- package/types/solution.d.ts +18 -0
- package/types/solver.d.ts +342 -0
- package/types/stack.d.ts +59 -0
- package/types/statistics.d.ts +10 -0
- package/types/traces.d.ts +118 -0
- package/types/types.d.ts +574 -0
package/src/stack.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// A device as a stack, for driftlet/kit: the layers left to right with the faces between them
|
|
2
|
+
// and a contact at each end, as a device is drawn. `build()` turns it into the plain definition
|
|
3
|
+
// (species, materials, regions, interfaces, contacts) that `new Device` takes. Nothing is added
|
|
4
|
+
// that you didn't write: it only moves things to where the definition keeps them, and converts
|
|
5
|
+
// the kit's doping shorthand (donors and acceptors).
|
|
6
|
+
|
|
7
|
+
import { DeviceError } from './errors.js';
|
|
8
|
+
import { FARADAY } from './constants.js';
|
|
9
|
+
|
|
10
|
+
const fail = (message) => {
|
|
11
|
+
throw new DeviceError(message);
|
|
12
|
+
};
|
|
13
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
14
|
+
|
|
15
|
+
function fields(obj, path, allowed) {
|
|
16
|
+
for (const k of Object.keys(obj)) if (obj[k] !== undefined && !allowed.includes(k)) fail(`${path}.${k}: unknown field (expected one of ${allowed.join(', ')})`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const same = (a, b) => {
|
|
20
|
+
if (a === b) return true;
|
|
21
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null || Array.isArray(a) !== Array.isArray(b)) return false;
|
|
22
|
+
const ka = Object.keys(a), kb = Object.keys(b);
|
|
23
|
+
return ka.length === kb.length && ka.every((k) => same(a[k], b[k]));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pieces of a definition (species and materials, as in a library entry) merged into one. A
|
|
28
|
+
* species or material given twice must be given identically.
|
|
29
|
+
* @param {...{ species?: object[], materials?: Record<string, object> }} parts
|
|
30
|
+
* @returns {{ species: object[], materials: Record<string, object> }}
|
|
31
|
+
*/
|
|
32
|
+
export function combine(...parts) {
|
|
33
|
+
const species = [], materials = {};
|
|
34
|
+
parts.forEach((part, k) => {
|
|
35
|
+
if (!isObject(part)) fail(`combine: part ${k} must be { species, materials }`);
|
|
36
|
+
for (const sp of part.species ?? []) {
|
|
37
|
+
const prev = species.find((s) => s.name === sp.name);
|
|
38
|
+
if (!prev) species.push({ ...sp });
|
|
39
|
+
else if (!same(prev, sp)) fail(`combine: species '${sp.name}' is given twice, differently (${JSON.stringify(prev)} and ${JSON.stringify(sp)})`);
|
|
40
|
+
}
|
|
41
|
+
for (const [name, mat] of Object.entries(part.materials ?? {})) {
|
|
42
|
+
if (materials[name] === undefined) materials[name] = mat;
|
|
43
|
+
else if (!same(materials[name], mat)) fail(`combine: material '${name}' is given twice, differently`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return { species, materials };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A layer of the stack: a region of `material`, `length` m long, with any other region fields
|
|
51
|
+
* (name, c0, grid, …) and the kit's doping shorthand `donors`, `acceptors` (mol/m³).
|
|
52
|
+
* @param {string} material
|
|
53
|
+
* @param {number} length m
|
|
54
|
+
* @param {object} [more]
|
|
55
|
+
*/
|
|
56
|
+
export function layer(material, length, more = {}) {
|
|
57
|
+
return { material, length, ...more };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A drive: a number is a held voltage, a waveform { t, values } a voltage waveform, and an
|
|
61
|
+
// object with V, I or R is the drive itself.
|
|
62
|
+
function drive(d, path) {
|
|
63
|
+
if (d === undefined) return { V: 0 };
|
|
64
|
+
if (typeof d === 'number') return { V: d };
|
|
65
|
+
if (isObject(d) && Array.isArray(d.t)) return { V: d };
|
|
66
|
+
if (isObject(d)) {
|
|
67
|
+
fields(d, path, ['V', 'I', 'R']);
|
|
68
|
+
return { ...d };
|
|
69
|
+
}
|
|
70
|
+
return fail(`${path} must be a voltage, a waveform, or { V }, { I }, { V, R }`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* An ohmic contact: each carrier in equilibrium with a metal at the terminal voltage, the
|
|
75
|
+
* first being the terminal species, and every other sharing its voltage (offset 0, as holes and
|
|
76
|
+
* electrons do at a metal). The default, both e⁻ and h⁺, gives n·p = n_i² at the contact
|
|
77
|
+
* (infinite recombination); one carrier alone is a selective contact, and `['e-']` is a
|
|
78
|
+
* current collector on a metal region.
|
|
79
|
+
* @param {number | object} [drive] a voltage, a waveform, or { V }, { I }, { V, R }
|
|
80
|
+
* @param {string[]} [carriers] default ['e-', 'h+']
|
|
81
|
+
*/
|
|
82
|
+
export function ohmic(d = 0, carriers = ['e-', 'h+']) {
|
|
83
|
+
if (!Array.isArray(carriers) || carriers.length === 0) fail('ohmic: carriers must be a non-empty list of species names');
|
|
84
|
+
const species = {};
|
|
85
|
+
carriers.forEach((name, k) => (species[name] = k === 0 ? 'equilibrium' : { type: 'equilibrium', offset: 0 }));
|
|
86
|
+
return { ...drive(d, 'ohmic'), terminal: carriers[0], species, phi: 'bulk' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A bath: a neutral composition `c` (mol/m³) held at the contact, its levels referred to the
|
|
91
|
+
* charged `reference` species (the ion a reference electrode would sense, e.g. Cl⁻ for
|
|
92
|
+
* Ag/AgCl), which is the terminal.
|
|
93
|
+
* @param {Record<string, number>} c
|
|
94
|
+
* @param {string} reference
|
|
95
|
+
* @param {number | object} [drive]
|
|
96
|
+
* @param {{ offset?: number }} [opts] where the reference species sits relative to the terminal
|
|
97
|
+
* voltage, V_ref = V + offset (V)
|
|
98
|
+
*/
|
|
99
|
+
export function bath(c, reference, d = 0, { offset } = {}) {
|
|
100
|
+
return { ...drive(d, 'bath'), bath: { c: { ...c }, reference, ...(offset === undefined ? {} : { offset }) } };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const STACK_FIELDS = ['T', 'library', 'species', 'materials', 'stack', 'bulkReactions', 'ports', 'grid'];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build a plain device definition from a stack:
|
|
107
|
+
*
|
|
108
|
+
* build({ library: [Si], stack: [ohmic(0), layer('Si', 1e-6, { donors }), layer(...), ohmic(0.5)] })
|
|
109
|
+
*
|
|
110
|
+
* - `stack`: a contact, then layers (regions: objects with a `material`) and faces (anything
|
|
111
|
+
* else: an interface's fields) left to right, then a contact. Two adjacent layers with no face
|
|
112
|
+
* between them get the default face. Nested lists are flattened.
|
|
113
|
+
* - Layers may give doping as `donors` and `acceptors` (mol/m³) in place of `fixedCharge`.
|
|
114
|
+
* - `library`: pieces with species and materials, merged with `species` and `materials`.
|
|
115
|
+
* - `T`, `grid`, `ports` pass through.
|
|
116
|
+
* @param {object} def
|
|
117
|
+
* @returns {object} a device definition for `new Device`
|
|
118
|
+
*/
|
|
119
|
+
export function build(def) {
|
|
120
|
+
if (!isObject(def)) fail('build: give { library, stack, … }');
|
|
121
|
+
fields(def, 'build', STACK_FIELDS);
|
|
122
|
+
if (def.library !== undefined && !Array.isArray(def.library)) fail('build.library must be a list of { species, materials }');
|
|
123
|
+
const { species, materials } = combine(...(def.library ?? []), { species: def.species ?? [], materials: def.materials ?? {} });
|
|
124
|
+
|
|
125
|
+
if (!Array.isArray(def.stack)) fail('build.stack must be a list: a contact, layers and faces, a contact');
|
|
126
|
+
const stack = def.stack.flat(Infinity);
|
|
127
|
+
if (stack.length < 3) fail('build.stack needs a contact, at least one layer, and a contact');
|
|
128
|
+
for (const k of [0, stack.length - 1]) {
|
|
129
|
+
if (!isObject(stack[k]) || stack[k].material !== undefined) fail(`stack[${k}]: the stack starts and ends with a contact`);
|
|
130
|
+
}
|
|
131
|
+
const at = (k) => `stack[${k}]`;
|
|
132
|
+
const regions = [], interfaces = [];
|
|
133
|
+
let pendingFace = null; // the face written since the last layer
|
|
134
|
+
for (let k = 1; k < stack.length - 1; k++) {
|
|
135
|
+
const item = stack[k];
|
|
136
|
+
if (!isObject(item)) fail(`${at(k)} must be a layer (with a material) or a face (an interface's fields)`);
|
|
137
|
+
if (item.material === undefined) {
|
|
138
|
+
if (regions.length === 0) fail(`${at(k)}: a face before the first layer (the contact is the boundary there)`);
|
|
139
|
+
if (pendingFace) fail(`${at(k)}: two faces in a row, with no layer between them`);
|
|
140
|
+
pendingFace = { item, k };
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (regions.length > 0) interfaces.push(pendingFace ? pendingFace.item : {});
|
|
144
|
+
pendingFace = null;
|
|
145
|
+
regions.push(region(item, at(k)));
|
|
146
|
+
}
|
|
147
|
+
if (pendingFace) fail(`${at(pendingFace.k)}: a face after the last layer (the contact is the boundary there)`);
|
|
148
|
+
if (regions.length === 0) fail('build.stack has no layers');
|
|
149
|
+
|
|
150
|
+
function region(item, path) {
|
|
151
|
+
const { donors, acceptors, ...r } = item;
|
|
152
|
+
if (donors !== undefined || acceptors !== undefined) {
|
|
153
|
+
if (r.fixedCharge !== undefined) fail(`${path}: give doping as donors and acceptors, or fixedCharge, not both`);
|
|
154
|
+
for (const [k, v] of [['donors', donors], ['acceptors', acceptors]]) {
|
|
155
|
+
if (v !== undefined && !(Number.isFinite(v) && v >= 0)) fail(`${path}.${k} must be a concentration ≥ 0 (mol/m³)`);
|
|
156
|
+
}
|
|
157
|
+
r.fixedCharge = ((donors ?? 0) - (acceptors ?? 0)) * FARADAY;
|
|
158
|
+
}
|
|
159
|
+
return r;
|
|
160
|
+
}
|
|
161
|
+
const out = {};
|
|
162
|
+
if (def.T !== undefined) out.T = def.T;
|
|
163
|
+
out.species = species;
|
|
164
|
+
out.materials = materials;
|
|
165
|
+
out.regions = regions;
|
|
166
|
+
if (interfaces.some((f) => Object.keys(f).length > 0)) out.interfaces = interfaces;
|
|
167
|
+
out.contacts = { left: stack[0], right: stack[stack.length - 1] };
|
|
168
|
+
if (def.bulkReactions !== undefined) out.bulkReactions = def.bulkReactions;
|
|
169
|
+
if (def.ports !== undefined) out.ports = def.ports;
|
|
170
|
+
if (def.grid !== undefined) out.grid = def.grid;
|
|
171
|
+
return out;
|
|
172
|
+
}
|