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.
Files changed (47) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +285 -0
  3. package/llms.txt +299 -0
  4. package/package.json +54 -0
  5. package/src/bernoulli.js +29 -0
  6. package/src/blockTridiagonal.js +492 -0
  7. package/src/constants.js +36 -0
  8. package/src/data.js +127 -0
  9. package/src/describe.js +190 -0
  10. package/src/device.js +954 -0
  11. package/src/equation.js +188 -0
  12. package/src/errors.js +8 -0
  13. package/src/fermi.js +106 -0
  14. package/src/grid.js +111 -0
  15. package/src/index.js +207 -0
  16. package/src/kit.js +76 -0
  17. package/src/live.js +217 -0
  18. package/src/merge.js +11 -0
  19. package/src/plot.js +267 -0
  20. package/src/solution.js +246 -0
  21. package/src/solver.js +2884 -0
  22. package/src/stack.js +172 -0
  23. package/src/statistics.js +593 -0
  24. package/src/traces.js +125 -0
  25. package/src/types.js +221 -0
  26. package/src/worker.js +24 -0
  27. package/types/bernoulli.d.ts +4 -0
  28. package/types/blockTridiagonal.d.ts +92 -0
  29. package/types/constants.d.ts +33 -0
  30. package/types/data.d.ts +73 -0
  31. package/types/describe.d.ts +15 -0
  32. package/types/device.d.ts +20 -0
  33. package/types/equation.d.ts +102 -0
  34. package/types/errors.d.ts +3 -0
  35. package/types/fermi.d.ts +3 -0
  36. package/types/grid.d.ts +28 -0
  37. package/types/index.d.ts +103 -0
  38. package/types/kit.d.ts +61 -0
  39. package/types/live.d.ts +101 -0
  40. package/types/merge.d.ts +1 -0
  41. package/types/plot.d.ts +60 -0
  42. package/types/solution.d.ts +18 -0
  43. package/types/solver.d.ts +342 -0
  44. package/types/stack.d.ts +59 -0
  45. package/types/statistics.d.ts +10 -0
  46. package/types/traces.d.ts +118 -0
  47. 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
+ }