driftlet 0.14.0 → 0.16.4
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 +12 -1
- package/docs/alignment.md +6 -3
- package/docs/data.md +3 -1
- package/docs/device.md +102 -33
- package/docs/kit.md +55 -17
- package/docs/numerics.md +244 -121
- package/docs/reliability.md +15 -9
- package/docs/statistics.md +28 -4
- package/llms.txt +73 -41
- package/package.json +1 -1
- package/src/blockTridiagonal.js +85 -7
- package/src/checks.js +83 -6
- package/src/describe.js +46 -5
- package/src/device.js +33 -10
- package/src/index.js +23 -10
- package/src/particles.js +131 -55
- package/src/solution.js +164 -8
- package/src/solver.js +923 -277
- package/src/sources.js +10 -2
- package/src/stack.js +16 -6
- package/src/statistics.js +7 -3
- package/types/blockTridiagonal.d.ts +1 -0
- package/types/checks.d.ts +8 -2
- package/types/index.d.ts +2 -2
- package/types/particles.d.ts +3 -6
- package/types/solver.d.ts +50 -29
- package/types/stack.d.ts +6 -4
package/src/sources.js
CHANGED
|
@@ -127,13 +127,21 @@ export function photogeneration({ material, flux, alpha, mu, from = 0, to, makes
|
|
|
127
127
|
// nothing is left), then `to`.
|
|
128
128
|
const depth = alpha * (to - from), reach = Math.min(depth, 50), n = Math.ceil(reach / 0.05), step = reach / n;
|
|
129
129
|
const taus = Array.from({ length: n + 1 }, (_, k) => k * step);
|
|
130
|
-
if (depth > 50) taus.push(depth);
|
|
130
|
+
if (depth > 50 * (1 + 1e-9)) taus.push(depth); // (not a round-off's worth past the last point)
|
|
131
131
|
// A straight line between two points of e^{−τ} encloses (Δτ/2)·coth(Δτ/2) times too much: each
|
|
132
132
|
// value is scaled back by that, so the total is the exponential's own (to ~Δτ⁴).
|
|
133
133
|
const fix = (step / 2) / Math.tanh(step / 2);
|
|
134
|
+
// Nothing outside [from, to]: a profile holds its end values beyond its table, so it drops to
|
|
135
|
+
// zero a hair beyond each end (the light enters a layer of a material that's also elsewhere).
|
|
136
|
+
const hair = 1e-10 * (to - from);
|
|
134
137
|
return {
|
|
135
138
|
equation: `photon = ${makes}`,
|
|
136
139
|
fixed: { photon: mu },
|
|
137
|
-
kf: {
|
|
140
|
+
kf: {
|
|
141
|
+
[material]: {
|
|
142
|
+
x: [from - hair, ...taus.map((t) => from + t / alpha), to + hair],
|
|
143
|
+
values: [0, ...taus.map((t) => (flux * alpha * Math.exp(-t)) / fix), 0],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
138
146
|
};
|
|
139
147
|
}
|
package/src/stack.js
CHANGED
|
@@ -89,17 +89,27 @@ export function ohmic(d = 0, carriers = ['e-', 'h+']) {
|
|
|
89
89
|
/**
|
|
90
90
|
* A bath: a neutral composition `c` (mol/m³) held at the contact. With a `reference`, the charged
|
|
91
91
|
* species a reference electrode would sense (Cl⁻ for Ag/AgCl), its levels are referred to that
|
|
92
|
-
* species, which is the terminal. Without one, `bath(c, drive)`, the terminal voltage is the
|
|
92
|
+
* species, which is the terminal. Without one, `bath(c, drive, opts)`, the terminal voltage is the
|
|
93
93
|
* bath's φ: an ideal salt bridge, the convention of membrane potentials.
|
|
94
94
|
* @param {Record<string, number>} c
|
|
95
95
|
* @param {string | number | object} [reference] the reference species, or the drive
|
|
96
96
|
* @param {number | object} [drive]
|
|
97
|
-
* @param {{ offset?: number }} [opts] where the reference species
|
|
98
|
-
* voltage, V_ref = V + offset (V)
|
|
97
|
+
* @param {{ offset?: number, material?: string }} [opts] `offset`: where the reference species
|
|
98
|
+
* sits relative to the terminal voltage, V_ref = V + offset (V). `material`: the bath's own
|
|
99
|
+
* material (a solution against an ion exchanger), if not the end layer's
|
|
99
100
|
*/
|
|
100
|
-
export function bath(c, reference, d = 0,
|
|
101
|
-
if (typeof reference !== 'string')
|
|
102
|
-
|
|
101
|
+
export function bath(c, reference, d = 0, opts = {}) {
|
|
102
|
+
if (typeof reference !== 'string') [d, opts] = [reference ?? 0, isObject(d) ? d : {}];
|
|
103
|
+
const { offset, material } = opts;
|
|
104
|
+
return {
|
|
105
|
+
...drive(d, 'bath'),
|
|
106
|
+
bath: {
|
|
107
|
+
c: { ...c },
|
|
108
|
+
...(typeof reference === 'string' ? { reference } : {}),
|
|
109
|
+
...(offset === undefined ? {} : { offset }),
|
|
110
|
+
...(material === undefined ? {} : { material }),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
103
113
|
}
|
|
104
114
|
|
|
105
115
|
const STACK_FIELDS = ['T', 'library', 'species', 'materials', 'stack', 'bulkReactions', 'ports', 'grid', 'geometry'];
|
package/src/statistics.js
CHANGED
|
@@ -23,16 +23,20 @@ const MODEL_TYPES = ['ideal', 'fermi-dirac', 'lattice', 'redlich-kister', 'debye
|
|
|
23
23
|
|
|
24
24
|
const logistic = (y) => (y >= 0 ? 1 / (1 + Math.exp(-y)) : Math.exp(y) / (1 + Math.exp(y)));
|
|
25
25
|
|
|
26
|
-
// Safeguarded Newton for an increasing function: f(y) = target on [lo, hi].
|
|
26
|
+
// Safeguarded Newton for an increasing function: f(y) = target on [lo, hi]. A Newton step that
|
|
27
|
+
// leaves the bracket, or that wouldn't halve the last step (Newton zigzagging across an
|
|
28
|
+
// inflection, its bracket shrinking by a little each time: a steep tabulated OCV's ran out of
|
|
29
|
+
// iterations 0.2 thermal units from its root), bisects instead.
|
|
27
30
|
function solveIncreasing(f, target, lo, hi, guess) {
|
|
28
|
-
let y = Math.min(hi, Math.max(lo, guess));
|
|
31
|
+
let y = Math.min(hi, Math.max(lo, guess)), last = Infinity;
|
|
29
32
|
for (let it = 0; it < 200; it++) {
|
|
30
33
|
const { v, d } = f(y);
|
|
31
34
|
const r = v - target;
|
|
32
35
|
if (r > 0) hi = y;
|
|
33
36
|
else lo = y;
|
|
34
37
|
let next = y - r / d;
|
|
35
|
-
if (!(next > lo && next < hi)) next = 0.5 * (lo + hi);
|
|
38
|
+
if (!(next > lo && next < hi) || Math.abs(next - y) > 0.5 * last) next = 0.5 * (lo + hi);
|
|
39
|
+
last = Math.abs(next - y);
|
|
36
40
|
if (Math.abs(next - y) <= 1e-15 * Math.max(1, Math.abs(y)) || hi - lo <= 1e-15 * Math.max(1, Math.abs(y))) return next;
|
|
37
41
|
y = next;
|
|
38
42
|
}
|
|
@@ -46,6 +46,7 @@ export class BlockTridiagonal {
|
|
|
46
46
|
* Throws if a diagonal block is exactly singular (unless `staticPivots`).
|
|
47
47
|
*/
|
|
48
48
|
factor(): void;
|
|
49
|
+
_noisyPivots(i: any): number;
|
|
49
50
|
/**
|
|
50
51
|
* Solve with the current factorisation. `rhs` and `out` are Float64Arrays of length `size`
|
|
51
52
|
* (node-major); they may be the same array.
|
package/types/checks.d.ts
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* zero; under light it's J = F(G − R), and it says where every carrier went.
|
|
19
19
|
* - **conservation** (a transient): every closed, unreacting stretch keeps what it held plus
|
|
20
20
|
* what came in.
|
|
21
|
+
* - **order** (a steady state, with `refine` and `order`, the defaults): the device solved from
|
|
22
|
+
* its definition twice, its species listed as given and in reverse, which must give the same
|
|
23
|
+
* steady state (each terminal's voltage and current, each species' amount). A solve that
|
|
24
|
+
* depends on the listing has converged somewhere that isn't steady without noticing (round 11's
|
|
25
|
+
* fuzzer found it in ~1% of random reacting devices, by exactly this test).
|
|
21
26
|
* - **grid** (with `refine`, the default; in a transient, `ok: null` and a reminder, since the
|
|
22
27
|
* answer depends on the whole run): in a steady state, the device solved again on a grid
|
|
23
28
|
* twice as fine everywhere, from this solution interpolated onto it. The current, each floating terminal's voltage and each
|
|
@@ -26,14 +31,15 @@
|
|
|
26
31
|
* (about 3/4 of it).
|
|
27
32
|
* @param {import('./index.js').Device} device
|
|
28
33
|
* @param {object} [sol] the solution to check (default: the device's present state)
|
|
29
|
-
* @param {{ refine?: boolean, tol?: number }} [opts]
|
|
34
|
+
* @param {{ refine?: boolean, tol?: number, order?: boolean }} [opts]
|
|
30
35
|
* Every check passed when `ok`; an item with `ok: null` (a warning, or a check that couldn't be
|
|
31
36
|
* made) asks you to look, and doesn't fail it. `text` is a line per check, marked ok, FAIL or ?.
|
|
32
37
|
* @returns {{ ok: boolean, items: CheckItem[], text: string }}
|
|
33
38
|
*/
|
|
34
|
-
export function check(device: import("./index.js").Device, sol?: object, { refine, tol }?: {
|
|
39
|
+
export function check(device: import("./index.js").Device, sol?: object, { refine, tol, order }?: {
|
|
35
40
|
refine?: boolean;
|
|
36
41
|
tol?: number;
|
|
42
|
+
order?: boolean;
|
|
37
43
|
}): {
|
|
38
44
|
ok: boolean;
|
|
39
45
|
items: CheckItem[];
|
package/types/index.d.ts
CHANGED
|
@@ -13,8 +13,8 @@ export class Device {
|
|
|
13
13
|
get solver(): any;
|
|
14
14
|
get grid(): any;
|
|
15
15
|
/**
|
|
16
|
-
* Change part of the definition (deep-merged). A contact given V or I drops the other
|
|
17
|
-
* `{ contacts: { right: { I: 0 } } }` switches it to open circuit. Ports are patched by name,
|
|
16
|
+
* Change part of the definition (deep-merged). A contact given V or I drops the other (and
|
|
17
|
+
* given I, any series R), so `{ contacts: { right: { I: 0 } } }` switches it to open circuit. Ports are patched by name,
|
|
18
18
|
* `{ ports: { gate: { V: 0.2 } } }` (an array replaces them all). A change to the terminals'
|
|
19
19
|
* drives alone (V, I, R) is applied in place, cheaply. Otherwise the device is rebuilt, keeping the current state as
|
|
20
20
|
* the warm start when the grid and species are unchanged (else restarting from the regions' c0).
|
package/types/particles.d.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* @param {number} [opts.dots] about how many dots per species (default 200), for the default weight
|
|
9
9
|
* @param {number|object} [opts.weight] amount per dot, mol/m²: one number for every species (so
|
|
10
10
|
* their dots compare), or { name: weight }; default, each species' total over `dots`
|
|
11
|
-
* @param {number} [opts.cap] the most dots a cell holds on average before
|
|
11
|
+
* @param {number} [opts.cap] the most dots a cell of the asked-for width holds on average before
|
|
12
|
+
* it's a sea (a density: a wider cell holds more; default ∞)
|
|
12
13
|
* @param {number} [opts.cells] about how many cells across the device (default 120)
|
|
13
14
|
* @param {() => number} [opts.random] a uniform random source in [0, 1) (default Math.random)
|
|
14
15
|
*/
|
|
@@ -20,11 +21,7 @@ export function particles(sol: object, { species, dots, weight, cap, cells, rand
|
|
|
20
21
|
cells?: number;
|
|
21
22
|
random?: () => number;
|
|
22
23
|
}): {
|
|
23
|
-
cells:
|
|
24
|
-
x0: any;
|
|
25
|
-
x1: any;
|
|
26
|
-
region: any;
|
|
27
|
-
}[];
|
|
24
|
+
cells: any[];
|
|
28
25
|
species: any;
|
|
29
26
|
weight: {};
|
|
30
27
|
sea: {};
|
package/types/solver.d.ts
CHANGED
|
@@ -84,6 +84,7 @@ export class Solver {
|
|
|
84
84
|
left: number;
|
|
85
85
|
right: number;
|
|
86
86
|
};
|
|
87
|
+
lawForm: Map<any, any>;
|
|
87
88
|
terms: any;
|
|
88
89
|
termV: Float64Array<any>;
|
|
89
90
|
termVPrev: Float64Array<any>;
|
|
@@ -92,6 +93,9 @@ export class Solver {
|
|
|
92
93
|
sourceOverride: Map<any, any>;
|
|
93
94
|
steady: boolean;
|
|
94
95
|
floating: any;
|
|
96
|
+
drivenKey: any;
|
|
97
|
+
lawRefs: Map<any, any>;
|
|
98
|
+
chargeRefs: Map<any, any>;
|
|
95
99
|
termB: any;
|
|
96
100
|
termC: any;
|
|
97
101
|
termDI: Float64Array<any>;
|
|
@@ -134,24 +138,37 @@ export class Solver {
|
|
|
134
138
|
stretchOf: Int32Array<ArrayBuffer>;
|
|
135
139
|
moieties: Float64Array<any>[];
|
|
136
140
|
flatStretches: any[];
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
_moves(st: any): boolean;
|
|
142
|
+
_still(st: any): boolean;
|
|
143
|
+
_feeds(k: any, st: any): any;
|
|
144
|
+
_laws(): void;
|
|
139
145
|
constraints: any[];
|
|
146
|
+
affinities: any[];
|
|
140
147
|
localConstraints: any[];
|
|
141
148
|
islands: any[];
|
|
142
149
|
pinnedIslands: any[];
|
|
143
150
|
islandsOn: boolean;
|
|
144
151
|
pins: any[];
|
|
145
|
-
|
|
152
|
+
timeLaws: any[];
|
|
153
|
+
carriers: any;
|
|
154
|
+
_captureLaws(): void;
|
|
146
155
|
_captureLocal(): void;
|
|
147
156
|
_applyIslands(): any[];
|
|
148
157
|
islandRow: Float64Array<any>;
|
|
149
158
|
_rowInto(o: any, add: any): void;
|
|
150
159
|
linRow: Float64Array<any>;
|
|
151
160
|
_applyLocalConstraints(): void;
|
|
152
|
-
_conservedMoieties(): Float64Array<any>[];
|
|
161
|
+
_conservedMoieties(drives?: boolean, alsoCurrent?: any[], skip?: any): Float64Array<any>[];
|
|
153
162
|
_directSteady(): boolean;
|
|
154
163
|
_applyConstraints(): void;
|
|
164
|
+
_lawAmount(cs: any): {
|
|
165
|
+
amount: number;
|
|
166
|
+
reference: number;
|
|
167
|
+
dV: number;
|
|
168
|
+
gross: number;
|
|
169
|
+
};
|
|
170
|
+
_lawStart(cs: any): number;
|
|
171
|
+
_circuitLaws(dt: any): void;
|
|
155
172
|
_solveBordered(rhs: any, delta: any, deltaV: any, pins: any): void;
|
|
156
173
|
dWork: Float64Array<any>;
|
|
157
174
|
borderCols: any[];
|
|
@@ -162,6 +179,9 @@ export class Solver {
|
|
|
162
179
|
amount(stretch: any): number;
|
|
163
180
|
redrive(): void;
|
|
164
181
|
deltaV: Float64Array<any>;
|
|
182
|
+
stuckReactions(): any[];
|
|
183
|
+
_drivenKey(): any;
|
|
184
|
+
_noCurrent(t: any): boolean;
|
|
165
185
|
_refreshSources(): void;
|
|
166
186
|
/** η_i (μ̄/RT) of a port's outside level for species i (its electrode's carrier: at V itself). */
|
|
167
187
|
portEta(port: any, i: any): number;
|
|
@@ -233,8 +253,14 @@ export class Solver {
|
|
|
233
253
|
_portCapacitance(port: any, k: any, dt: any): void;
|
|
234
254
|
_surfaceStorage(port: any, dt: any): void;
|
|
235
255
|
_contact(side: any, dt: any): void;
|
|
256
|
+
_flatLevel(st: any): {
|
|
257
|
+
k: number;
|
|
258
|
+
hi: number;
|
|
259
|
+
lo: number;
|
|
260
|
+
};
|
|
261
|
+
_snapFlat(): void;
|
|
236
262
|
_flatRows(): void;
|
|
237
|
-
|
|
263
|
+
_applyAffinities(): void;
|
|
238
264
|
_replaceRow(b: any, rs: any): void;
|
|
239
265
|
_equilibrate(): void;
|
|
240
266
|
_assembleDifference(dt: any): {
|
|
@@ -265,6 +291,7 @@ export class Solver {
|
|
|
265
291
|
digits: any;
|
|
266
292
|
x: number;
|
|
267
293
|
where: string;
|
|
294
|
+
nodes: any[];
|
|
268
295
|
};
|
|
269
296
|
/**
|
|
270
297
|
* Advance by dt, splitting the interval into halves (recursively) wherever Newton fails.
|
|
@@ -288,6 +315,9 @@ export class Solver {
|
|
|
288
315
|
};
|
|
289
316
|
contactDEnd: any;
|
|
290
317
|
sourceBefore: boolean;
|
|
318
|
+
solvedV: any;
|
|
319
|
+
stepped: boolean;
|
|
320
|
+
_guess(guess: any): void;
|
|
291
321
|
/**
|
|
292
322
|
* Adaptive time stepping to tEnd: variable-step BDF2 (or backward Euler), with the local error
|
|
293
323
|
* estimated against an explicit predictor through the previous states, and controlled to
|
|
@@ -322,7 +352,6 @@ export class Solver {
|
|
|
322
352
|
};
|
|
323
353
|
_errorNorm(ref: any): number;
|
|
324
354
|
_warmFrom(src: any): void;
|
|
325
|
-
solvedV: any;
|
|
326
355
|
_snapshot(): {
|
|
327
356
|
u: Float64Array<ArrayBuffer>;
|
|
328
357
|
uLo: Float64Array<ArrayBuffer>;
|
|
@@ -351,6 +380,7 @@ export class Solver {
|
|
|
351
380
|
sourceTime: number;
|
|
352
381
|
};
|
|
353
382
|
_restore(s: any): void;
|
|
383
|
+
passedCurrent(k: any): any;
|
|
354
384
|
_terminalCurrent(): number;
|
|
355
385
|
/**
|
|
356
386
|
* Small-signal impedance about the current (steady) state, seen at one terminal. Linearising
|
|
@@ -404,13 +434,12 @@ export class Solver {
|
|
|
404
434
|
slowestTime(): number;
|
|
405
435
|
fastestTime(): number;
|
|
406
436
|
/**
|
|
407
|
-
* Steady state
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
* If Newton fails, dt ramps up from a small value (pseudo-transient continuation) instead.
|
|
437
|
+
* Steady state: where a transient from the present state ends, found as the solution of the
|
|
438
|
+
* steady equations (dt = ∞, no storage term), with no slow modes to wait out, however slow the
|
|
439
|
+
* physics (e.g. exponentially scarce minority carriers). What the storage term held is held by
|
|
440
|
+
* the steady system's own rows instead (see _laws): each conserved amount in place of a balance
|
|
441
|
+
* row that says nothing new, levels that carry nothing held flat, islands' summed balances.
|
|
442
|
+
* Where Newton from here fails: continuation along the drives, then the pseudo-transient ramp.
|
|
414
443
|
* The clock is not advanced, and open-system conservation bookkeeping restarts here.
|
|
415
444
|
*/
|
|
416
445
|
solveSteady(opts?: {}): any;
|
|
@@ -420,9 +449,17 @@ export class Solver {
|
|
|
420
449
|
low: any;
|
|
421
450
|
high: any;
|
|
422
451
|
};
|
|
452
|
+
charging: {
|
|
453
|
+
terminal: any;
|
|
454
|
+
current: any;
|
|
455
|
+
what: any[];
|
|
456
|
+
};
|
|
457
|
+
steadyAt: number;
|
|
423
458
|
_solveSteadyAll(opts: any): any;
|
|
424
459
|
_steadyFromHere(opts: any): any;
|
|
425
460
|
dimmer: boolean;
|
|
461
|
+
_bound(k: any): boolean;
|
|
462
|
+
_chargeOnly(k: any): boolean;
|
|
426
463
|
_floatingContinuation(opts: any, r: any): any;
|
|
427
464
|
_generationContinuation(opts: any, r: any): {
|
|
428
465
|
converged: boolean;
|
|
@@ -431,22 +468,6 @@ export class Solver {
|
|
|
431
468
|
history: any;
|
|
432
469
|
};
|
|
433
470
|
_continuation(opts: any, k: any, level: any, target: any, r: any, warm: any, hold: any): any;
|
|
434
|
-
_solveSteady(opts?: {}): {
|
|
435
|
-
converged: boolean;
|
|
436
|
-
steps: number;
|
|
437
|
-
iterations: number;
|
|
438
|
-
history: ({
|
|
439
|
-
dt: number;
|
|
440
|
-
converged: boolean;
|
|
441
|
-
iterations: number;
|
|
442
|
-
maxStep?: undefined;
|
|
443
|
-
} | {
|
|
444
|
-
dt: number;
|
|
445
|
-
converged: boolean;
|
|
446
|
-
iterations: number;
|
|
447
|
-
maxStep: number;
|
|
448
|
-
})[];
|
|
449
|
-
};
|
|
450
471
|
_steadySteps({ maxSteps, tol }?: {
|
|
451
472
|
maxSteps?: number;
|
|
452
473
|
tol?: number;
|
package/types/stack.d.ts
CHANGED
|
@@ -33,16 +33,18 @@ export function ohmic(d?: number, carriers?: string[]): any;
|
|
|
33
33
|
/**
|
|
34
34
|
* A bath: a neutral composition `c` (mol/m³) held at the contact. With a `reference`, the charged
|
|
35
35
|
* species a reference electrode would sense (Cl⁻ for Ag/AgCl), its levels are referred to that
|
|
36
|
-
* species, which is the terminal. Without one, `bath(c, drive)`, the terminal voltage is the
|
|
36
|
+
* species, which is the terminal. Without one, `bath(c, drive, opts)`, the terminal voltage is the
|
|
37
37
|
* bath's φ: an ideal salt bridge, the convention of membrane potentials.
|
|
38
38
|
* @param {Record<string, number>} c
|
|
39
39
|
* @param {string | number | object} [reference] the reference species, or the drive
|
|
40
40
|
* @param {number | object} [drive]
|
|
41
|
-
* @param {{ offset?: number }} [opts] where the reference species
|
|
42
|
-
* voltage, V_ref = V + offset (V)
|
|
41
|
+
* @param {{ offset?: number, material?: string }} [opts] `offset`: where the reference species
|
|
42
|
+
* sits relative to the terminal voltage, V_ref = V + offset (V). `material`: the bath's own
|
|
43
|
+
* material (a solution against an ion exchanger), if not the end layer's
|
|
43
44
|
*/
|
|
44
|
-
export function bath(c: Record<string, number>, reference?: string | number | object, d?: number,
|
|
45
|
+
export function bath(c: Record<string, number>, reference?: string | number | object, d?: number, opts?: {
|
|
45
46
|
offset?: number;
|
|
47
|
+
material?: string;
|
|
46
48
|
}): any;
|
|
47
49
|
/**
|
|
48
50
|
* Build a plain device definition from a stack:
|