driftlet 0.13.0 → 0.16.2

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/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: { [material]: { x: taus.map((t) => from + t / alpha), values: taus.map((t) => (flux * alpha * Math.exp(-t)) / fix) } },
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 sits relative to the terminal
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, { offset } = {}) {
101
- if (typeof reference !== 'string') return { ...drive(reference ?? 0, 'bath'), bath: { c: { ...c } } };
102
- return { ...drive(d, 'bath'), bath: { c: { ...c }, reference, ...(offset === undefined ? {} : { offset }) } };
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
  }
package/types/checks.d.ts CHANGED
@@ -18,7 +18,8 @@
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
- * - **grid** (a steady state, with `refine`, the default): the device solved again on a grid
21
+ * - **grid** (with `refine`, the default; in a transient, `ok: null` and a reminder, since the
22
+ * answer depends on the whole run): in a steady state, the device solved again on a grid
22
23
  * twice as fine everywhere, from this solution interpolated onto it. The current, each floating terminal's voltage and each
23
24
  * region's charge must agree to `tol` (relative, default 1e-2, a plot's accuracy; a benchmark
24
25
  * wants less). The discretisation is second order, so the change estimates this grid's error
package/types/kit.d.ts CHANGED
@@ -36,6 +36,7 @@ export function vacuumZeroCharge(def: object, workFunction: number, inside: Vacu
36
36
  export { live } from "./live.js";
37
37
  export { recorder } from "./recorder.js";
38
38
  export { check } from "./checks.js";
39
+ export { particles } from "./particles.js";
39
40
  export { polarization } from "./polarization.js";
40
41
  /**
41
42
  * One material seen from vacuum: a level inside it (the anchor) and how far above that level,
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A swarm of dots sampling a solution's concentrations and fluxes. Amounts are per area (a planar
3
+ * device's). `step(dt)` moves them on in the device's time, at the rates of the solution last
4
+ * given; `update(sol)` gives a new one.
5
+ * @param {object} sol a solution (from solve(), advance(), live(), …)
6
+ * @param {object} [opts]
7
+ * @param {string[]} [opts.species] which species (default: every species with a concentration)
8
+ * @param {number} [opts.dots] about how many dots per species (default 200), for the default weight
9
+ * @param {number|object} [opts.weight] amount per dot, mol/m²: one number for every species (so
10
+ * their dots compare), or { name: weight }; default, each species' total over `dots`
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 ∞)
13
+ * @param {number} [opts.cells] about how many cells across the device (default 120)
14
+ * @param {() => number} [opts.random] a uniform random source in [0, 1) (default Math.random)
15
+ */
16
+ export function particles(sol: object, { species, dots, weight, cap, cells, random }?: {
17
+ species?: string[];
18
+ dots?: number;
19
+ weight?: number | object;
20
+ cap?: number;
21
+ cells?: number;
22
+ random?: () => number;
23
+ }): {
24
+ cells: any[];
25
+ species: any;
26
+ weight: {};
27
+ sea: {};
28
+ dots: any[];
29
+ events: any[];
30
+ crossed: {};
31
+ time: number;
32
+ update: (next: any, { resample }?: {
33
+ resample?: boolean;
34
+ }) => /*elided*/ any;
35
+ step: (dt: any, { maxHops }?: {
36
+ maxHops?: number;
37
+ }) => /*elided*/ any;
38
+ expected: (nm: any) => Float64Array<ArrayBuffer>;
39
+ };
@@ -9,6 +9,7 @@ export function makeSolution(solver: any, result?: {}): {
9
9
  muStd: {};
10
10
  V: {};
11
11
  Vstd: {};
12
+ T: any;
12
13
  time: any;
13
14
  steady: any;
14
15
  converged: any;
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,36 @@ export class Solver {
134
138
  stretchOf: Int32Array<ArrayBuffer>;
135
139
  moieties: Float64Array<any>[];
136
140
  flatStretches: any[];
137
- levelStretches: any[];
138
- flattening: boolean;
141
+ _moves(st: any): boolean;
142
+ _still(st: any): boolean;
143
+ _feeds(k: any, st: any): any;
144
+ _laws(): void;
139
145
  constraints: any[];
140
146
  localConstraints: any[];
141
147
  islands: any[];
142
148
  pinnedIslands: any[];
143
149
  islandsOn: boolean;
144
150
  pins: any[];
145
- constrained: boolean;
151
+ timeLaws: any[];
152
+ carriers: any;
153
+ _captureLaws(): void;
146
154
  _captureLocal(): void;
147
155
  _applyIslands(): any[];
148
156
  islandRow: Float64Array<any>;
149
157
  _rowInto(o: any, add: any): void;
150
158
  linRow: Float64Array<any>;
151
159
  _applyLocalConstraints(): void;
152
- _conservedMoieties(): Float64Array<any>[];
160
+ _conservedMoieties(drives?: boolean, alsoCurrent?: any[]): Float64Array<any>[];
153
161
  _directSteady(): boolean;
154
162
  _applyConstraints(): void;
163
+ _lawAmount(cs: any): {
164
+ amount: number;
165
+ reference: number;
166
+ dV: number;
167
+ gross: number;
168
+ };
169
+ _lawStart(cs: any): number;
170
+ _circuitLaws(dt: any): void;
155
171
  _solveBordered(rhs: any, delta: any, deltaV: any, pins: any): void;
156
172
  dWork: Float64Array<any>;
157
173
  borderCols: any[];
@@ -209,9 +225,11 @@ export class Solver {
209
225
  mixWork: Float64Array<any>[];
210
226
  _dc(g: any, k: any, d: any): void;
211
227
  bulkRate(rx: any, g: any): number;
228
+ bulkForward(rx: any, g: any): any;
212
229
  bulkOneWay(rx: any, g: any): number;
213
230
  _srhRate(law: any, cn: any, cp: any, a: any): {
214
231
  rate: number;
232
+ forward: number;
215
233
  oneWay: number;
216
234
  Cn: number;
217
235
  Cp: number;
@@ -231,8 +249,13 @@ export class Solver {
231
249
  _portCapacitance(port: any, k: any, dt: any): void;
232
250
  _surfaceStorage(port: any, dt: any): void;
233
251
  _contact(side: any, dt: any): void;
252
+ _flatLevel(st: any): {
253
+ k: number;
254
+ hi: number;
255
+ lo: number;
256
+ };
257
+ _snapFlat(): void;
234
258
  _flatRows(): void;
235
- _levelRows(): void;
236
259
  _replaceRow(b: any, rs: any): void;
237
260
  _equilibrate(): void;
238
261
  _assembleDifference(dt: any): {
@@ -263,6 +286,7 @@ export class Solver {
263
286
  digits: any;
264
287
  x: number;
265
288
  where: string;
289
+ nodes: any[];
266
290
  };
267
291
  /**
268
292
  * Advance by dt, splitting the interval into halves (recursively) wherever Newton fails.
@@ -286,6 +310,8 @@ export class Solver {
286
310
  };
287
311
  contactDEnd: any;
288
312
  sourceBefore: boolean;
313
+ solvedV: any;
314
+ stepped: boolean;
289
315
  /**
290
316
  * Adaptive time stepping to tEnd: variable-step BDF2 (or backward Euler), with the local error
291
317
  * estimated against an explicit predictor through the previous states, and controlled to
@@ -320,7 +346,6 @@ export class Solver {
320
346
  };
321
347
  _errorNorm(ref: any): number;
322
348
  _warmFrom(src: any): void;
323
- solvedV: any;
324
349
  _snapshot(): {
325
350
  u: Float64Array<ArrayBuffer>;
326
351
  uLo: Float64Array<ArrayBuffer>;
@@ -387,6 +412,8 @@ export class Solver {
387
412
  g: any[];
388
413
  d: any[];
389
414
  }[];
415
+ segmentFluxes(): Float64Array<ArrayBuffer>[];
416
+ faceOneWay(f: any): any;
390
417
  _smallSignalProfiles(xr: any, xi: any): {
391
418
  phi: {
392
419
  re: Float64Array<any>;
@@ -400,13 +427,12 @@ export class Solver {
400
427
  slowestTime(): number;
401
428
  fastestTime(): number;
402
429
  /**
403
- * Steady state.
404
- * - If every species stretch is fed by a contact, nothing is conserved on its own, so the
405
- * true steady equations (dt = ∞, no storage term) are solved directly: no slow modes to
406
- * wait out, however slow the physics (e.g. exponentially scarce minority carriers).
407
- * - Otherwise backward-Euler steps at a huge dt, whose storage term pins each conserved
408
- * amount exactly; dt keeps growing ×10 while the state still moves.
409
- * If Newton fails, dt ramps up from a small value (pseudo-transient continuation) instead.
430
+ * Steady state: where a transient from the present state ends, found as the solution of the
431
+ * steady equations (dt = ∞, no storage term), with no slow modes to wait out, however slow the
432
+ * physics (e.g. exponentially scarce minority carriers). What the storage term held is held by
433
+ * the steady system's own rows instead (see _laws): each conserved amount in place of a balance
434
+ * row that says nothing new, levels that carry nothing held flat, islands' summed balances.
435
+ * Where Newton from here fails: continuation along the drives, then the pseudo-transient ramp.
410
436
  * The clock is not advanced, and open-system conservation bookkeeping restarts here.
411
437
  */
412
438
  solveSteady(opts?: {}): any;
@@ -416,9 +442,17 @@ export class Solver {
416
442
  low: any;
417
443
  high: any;
418
444
  };
445
+ charging: {
446
+ terminal: any;
447
+ current: any;
448
+ what: any[];
449
+ };
450
+ steadyAt: number;
419
451
  _solveSteadyAll(opts: any): any;
420
452
  _steadyFromHere(opts: any): any;
421
453
  dimmer: boolean;
454
+ _bound(k: any): boolean;
455
+ _chargeOnly(k: any): boolean;
422
456
  _floatingContinuation(opts: any, r: any): any;
423
457
  _generationContinuation(opts: any, r: any): {
424
458
  converged: boolean;
@@ -427,22 +461,6 @@ export class Solver {
427
461
  history: any;
428
462
  };
429
463
  _continuation(opts: any, k: any, level: any, target: any, r: any, warm: any, hold: any): any;
430
- _solveSteady(opts?: {}): {
431
- converged: boolean;
432
- steps: number;
433
- iterations: number;
434
- history: ({
435
- dt: number;
436
- converged: boolean;
437
- iterations: number;
438
- maxStep?: undefined;
439
- } | {
440
- dt: number;
441
- converged: boolean;
442
- iterations: number;
443
- maxStep: number;
444
- })[];
445
- };
446
464
  _steadySteps({ maxSteps, tol }?: {
447
465
  maxSteps?: number;
448
466
  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 sits relative to the terminal
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, { offset }?: {
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: