driftlet 1.1.1 → 1.2.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/live.js CHANGED
@@ -57,26 +57,31 @@ export class Session {
57
57
  if (this.good && errors.every(Boolean)) return done(this.good.sol, { failed: true }); // nothing changed
58
58
  const target = this.device.def;
59
59
  try {
60
- let sol = this.device.solve();
61
- // Ramp from the last good state in more and more steps.
62
- for (let n = 2; !sol.converged && this.good && n <= this.maxRamp; n *= 2) {
60
+ let sol = this.device.solve(), stuck = false;
61
+ // Ramp from the last good state in more and more steps. (Its in-between states can be
62
+ // invalid where the two ends aren't, a bath neutral at both but not between: then on to the
63
+ // fresh solve.)
64
+ ramping: for (let n = 2; !sol.converged && this.good && n <= this.maxRamp; n *= 2) {
63
65
  ramp = n;
64
66
  this.device._rollback(this.good.cp);
65
67
  const from = this.good.cp.def;
66
68
  for (let k = 1; k <= n; k++) {
67
- this.device.set(between(from, target, k / n));
69
+ try {
70
+ this.device.set(between(from, target, k / n));
71
+ } catch {
72
+ stuck = true; // (and sol, a converged step short of the target, isn't the answer)
73
+ break ramping;
74
+ }
68
75
  sol = this.device.solve();
69
76
  if (!sol.converged) break;
70
77
  }
71
78
  }
72
- if (!sol.converged) {
79
+ if (stuck || !sol.converged) {
73
80
  // Last, from scratch (with the solver's own continuations).
74
81
  const fresh = new Device(target);
75
82
  const cold = fresh.solve();
76
- if (cold.converged) {
77
- this.device = fresh;
78
- sol = cold;
79
- }
83
+ if (cold.converged) this.device = fresh;
84
+ if (cold.converged || stuck) sol = cold;
80
85
  }
81
86
  if (sol.converged) {
82
87
  this.good = { cp: this.device._checkpoint(), sol };
package/src/particles.js CHANGED
@@ -55,6 +55,8 @@ const NONE = 0, DOT = 1, OUTSIDE = 2; // nothing (closed); a cell of dots; a sea
55
55
  */
56
56
  export function particles(sol, { species, dots = 200, weight, cap = Infinity, cells = 120, random = Math.random } = {}) {
57
57
  checkOptions('particles', arguments[1], ['species', 'dots', 'weight', 'cap', 'cells', 'random']);
58
+ // (amounts per area, against fluxes that off a plane are per device: planar only)
59
+ if (sol.geometry !== undefined && sol.geometry !== 'planar') throw new RangeError(`particles: a planar device's solution, not a ${sol.geometry} one`);
58
60
  const names = species ?? sol.species.map((s) => s.name).filter((nm) => sol.c[nm].some((v) => v > 0));
59
61
  for (const nm of names) if (!sol.c[nm]) throw new RangeError(`particles: no species ${JSON.stringify(nm)} in the solution`);
60
62
  if (!(dots > 0 && Number.isFinite(dots))) throw new RangeError(`particles: dots must be a positive number, got ${dots}`);
@@ -172,7 +174,8 @@ export function particles(sol, { species, dots = 200, weight, cap = Infinity, ce
172
174
  }
173
175
  [left[k], right[k]] = [side(k - 1), side(k)];
174
176
  const hD = A.h / 2 / D[A.mid] + B.h / 2 / D[B.mid];
175
- pair(k, J[A.b], (Math.sqrt((N[k - 1] / A.h) * (N[k] / B.h)) * shape(psi[k] - psi[k - 1])) / hD);
177
+ // (none of it where either cell is empty: an underflow to 0 makes both ψ infinite)
178
+ pair(k, J[A.b], N[k - 1] > 0 && N[k] > 0 ? (Math.sqrt((N[k - 1] / A.h) * (N[k] / B.h)) * shape(psi[k] - psi[k - 1])) / hD : 0);
176
179
  } else if (A || B) {
177
180
  // out of the cells: through a contact, or a face to a metal
178
181
  const cl = A ?? B, g = A ? A.b : B.a;
package/src/plot.js CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  import { traces } from './traces.js';
16
16
  import { powi } from './pow.js';
17
- import { checkOptions } from './errors.js';
17
+ import { DeviceError, checkOptions } from './errors.js';
18
18
 
19
19
  // Colours by role, light and dark (validated together for colour-vision deficiency; line weight,
20
20
  // dash and the legend carry identity too). A role with more members than colours cycles.
@@ -23,16 +23,20 @@ export const THEME = Object.freeze({
23
23
  dark: { electron: ['#5b95d6'], cation: ['#e8584f', '#d0772a', '#d65d95', '#b88a3e'], anion: ['#219c8f', '#2aa5bb', '#3aa874'], redox: ['#8b7ff0', '#a86bc9', '#7a9ae0'] },
24
24
  });
25
25
  const ROLES = ['electron', 'cation', 'anion', 'redox'];
26
+ const MUTED = { light: '#52514e', dark: '#c3c2b7' }; // (φ's, and lines of no species)
26
27
 
27
28
  /**
28
29
  * The colour of a trace's role and slot (a CSS colour), for drawing other charts in the same
29
30
  * colours as the level diagrams.
30
- * @param {'electron' | 'cation' | 'anion' | 'redox'} role
31
+ * @param {'electron' | 'cation' | 'anion' | 'redox' | 'phi'} role
31
32
  * @param {number} slot
32
33
  * @param {{ dark?: boolean }} [opts]
33
34
  */
34
35
  export function themeColor(role, slot, { dark = false } = {}) {
35
36
  checkOptions('themeColor', arguments[2], ['dark']);
37
+ if (role === 'phi') return MUTED[dark ? 'dark' : 'light'];
38
+ if (!ROLES.includes(role)) throw new DeviceError(`themeColor: no role '${role}' (${[...ROLES, 'phi'].join(', ')})`);
39
+ if (!(Number.isInteger(slot) && slot >= 0)) throw new DeviceError(`themeColor: slot must be a whole number ≥ 0, got ${slot}`);
36
40
  const list = THEME[dark ? 'dark' : 'light'][role];
37
41
  return list[slot % list.length];
38
42
  }
@@ -118,8 +122,8 @@ function lengthUnit(L) {
118
122
 
119
123
  const style = (id) => {
120
124
  const vars = (mode) => ROLES.flatMap((role) => THEME[mode][role].map((c, k) => `--driftlet-${role}${role === 'electron' ? '' : `-${k + 1}`}:${c};`)).join('');
121
- const light = `${vars('light')}--driftlet-ink:#0b0b0b;--driftlet-muted:#52514e;--driftlet-grid:#e4e3df;--driftlet-band:#f1f0ec;--driftlet-face:#a8a7a1`;
122
- const dark = `${vars('dark')}--driftlet-ink:#ffffff;--driftlet-muted:#c3c2b7;--driftlet-grid:#3a3a37;--driftlet-band:#262624;--driftlet-face:#6b6a64`;
125
+ const light = `${vars('light')}--driftlet-ink:#0b0b0b;--driftlet-muted:${MUTED.light};--driftlet-grid:#e4e3df;--driftlet-band:#f1f0ec;--driftlet-face:#a8a7a1`;
126
+ const dark = `${vars('dark')}--driftlet-ink:#ffffff;--driftlet-muted:${MUTED.dark};--driftlet-grid:#3a3a37;--driftlet-band:#262624;--driftlet-face:#6b6a64`;
123
127
  return (
124
128
  `<style>` +
125
129
  `:where(#${id}){${light}}` +
package/src/solution.js CHANGED
@@ -27,6 +27,7 @@ export function makeSolution(solver, result = {}) {
27
27
  V: {},
28
28
  Vstd: {},
29
29
  T: model.T,
30
+ geometry: model.geometry.type, // (planar, or amounts per device rather than per area)
30
31
  time: solver.time,
31
32
  steady: solver.atSteady,
32
33
  converged: result.converged ?? true,