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/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
  }
@@ -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, so
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).
@@ -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 it's a sea (default ∞)
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
- 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[];
146
+ affinities: any[];
140
147
  localConstraints: any[];
141
148
  islands: any[];
142
149
  pinnedIslands: any[];
143
150
  islandsOn: boolean;
144
151
  pins: any[];
145
- constrained: boolean;
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
- _levelRows(): void;
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
- * - If every species stretch is fed by a contact, nothing is conserved on its own, so the
409
- * true steady equations (dt = ∞, no storage term) are solved directly: no slow modes to
410
- * wait out, however slow the physics (e.g. exponentially scarce minority carriers).
411
- * - Otherwise backward-Euler steps at a huge dt, whose storage term pins each conserved
412
- * amount exactly; dt keeps growing ×10 while the state still moves.
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 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: