driftlet 1.3.3 → 1.4.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.
package/src/solver.js CHANGED
@@ -10,8 +10,9 @@ import { Assembly } from './assemble.js';
10
10
  import { BlockTridiagonal } from './blockTridiagonal.js';
11
11
  import { bernoulli, SG_FAR, sgFlux } from './bernoulli.js';
12
12
  import { BoundaryTerms } from './boundaries.js';
13
- import { FARADAY } from './constants.js';
13
+ import { EPS0, FARADAY } from './constants.js';
14
14
  import { sourceAt } from './device.js';
15
+ import { diffuseScratch, diffuseUnits } from './diffuse.js';
15
16
  import { DeviceError } from './errors.js';
16
17
  import { conservedMoieties, findLaws, noCurrent } from './laws.js';
17
18
  import { newton as newtonFn } from './newton.js';
@@ -60,15 +61,29 @@ export class SolverError extends Error {
60
61
  }
61
62
  }
62
63
 
64
+ // The species a face's diffuse layer on that side (0 left, 1 right) passes to its reactions:
65
+ // each participant there that the layer holds (charged, or on its lattice), but SRH's (refused
66
+ // beside a layer), in species order. A flux across the layer lowers each one's level at the plane.
67
+ function drawSpecies(itf, side, species) {
68
+ const lat = itf.diffuse.lattice[side], out = new Set();
69
+ for (const rx of itf.reactions) {
70
+ if (rx.srh) continue;
71
+ for (const p of rx.part) if (p.side === side && (species[p.i].z !== 0 || lat?.on[p.i])) out.add(p.i);
72
+ }
73
+ return [...out].sort((a, b) => a - b);
74
+ }
75
+
63
76
  export class Solver {
64
77
  constructor(model) {
65
78
  this.model = model;
66
79
  const { grid, species, materials, regions, contacts } = model;
67
80
  const n = species.length;
68
81
  // Slots per block: φ̂ (or D, or a conductor's segment flux J), one per species, and at a face
69
- // one per face reaction (its rate) and one per gate (its fraction open), as many as the
70
- // busiest face has.
71
- const nRx = model.interfaces.reduce((m, itf) => Math.max(m, itf.reactions.length + itf.gates.length), 0);
82
+ // one per face reaction (its rate), one per gate (its fraction open) and one per diffuse
83
+ // layer (its drop) and one per species that layer passes to a face reaction (its level at the
84
+ // plane: drawSpecies), as many as the busiest face has.
85
+ const nDiff = (itf) => (itf.diffuse ? [0, 1].reduce((m, side) => m + ((side ? itf.diffuse.right : itf.diffuse.left) ? 1 + drawSpecies(itf, side, species).length : 0), 0) : 0);
86
+ const nRx = model.interfaces.reduce((m, itf) => Math.max(m, itf.reactions.length + itf.gates.length + nDiff(itf)), 0);
72
87
  // (and at an electrode port's nodes, one per surface species: its coverage's η)
73
88
  const nSurf = model.ports.reduce((m, port) => Math.max(m, port.surface.length), 0);
74
89
  // (and a membrane port's gates, after them: each one's fraction open)
@@ -122,6 +137,36 @@ export class Solver {
122
137
  let j = 0;
123
138
  for (let f = 0; f < nFaces; j += model.interfaces[f].gates.length, f++) this.gateIndex[f] = j;
124
139
  this.portGateIndex = model.ports.map((port) => ((j += port.nodes.length * port.gates.length), j - port.nodes.length * port.gates.length));
140
+ // Diffuse layers (src/diffuse.js): one per strictly neutral side of a face that has them, in
141
+ // face order, left side first. Each: its face f, side (0 left, 1 right), edge node g and its
142
+ // block b, its drop s = Fψ/RT in slot o of the face's block (after the gates), the layer's
143
+ // constants, its lattice (crowded: { cMax, on }, the species on it; or null, dilute), which
144
+ // species it holds an excess of (the charged, and a lattice's neutral ones, displaced), and
145
+ // its scratch (q, Γ and their derivatives, D, from computeConcentrations). Γ
146
+ // [d·n + i], the excesses, now and at the step's start; diffAt[f] = [left d, right d] (−1: none).
147
+ // And the species it passes to the face's reactions (the drawn: each one's level at the plane
148
+ // in slot drawSlot[i], after both sides' drops, −1 where not drawn; draw[i] = 1 for them), and
149
+ // their diffusivities there, Dm.
150
+ this.diffList = [];
151
+ this.diffAt = model.interfaces.map(() => [-1, -1]);
152
+ model.interfaces.forEach((itf, f) => {
153
+ if (!itf.diffuse) return;
154
+ let slot = 1 + n + itf.reactions.length + itf.gates.length;
155
+ const sides = [0, 1].filter((side) => (side ? itf.diffuse.right : itf.diffuse.left));
156
+ let level = slot + sides.length;
157
+ for (const side of sides) {
158
+ const g = side ? grid.regionStart[f + 1] : grid.regionEnd[f];
159
+ this.diffAt[f][side] = this.diffList.length;
160
+ const lat = itf.diffuse.lattice[side], drawSlot = new Int32Array(n).fill(-1);
161
+ for (const i of drawSpecies(itf, side, species)) drawSlot[i] = level++;
162
+ const draw = drawSlot.some((q) => q >= 0) ? Uint8Array.from(drawSlot, (q) => (q >= 0 ? 1 : 0)) : null;
163
+ const Dm = model.materials[regions[grid.nodeRegion[g]].material].D;
164
+ this.diffList.push({ f, side, g, b: this.blockOfNode[g], slot, o: this.blockOfFace[f] * M + slot, units: diffuseUnits(itf.diffuse.epsr * EPS0, model.RT, FARADAY), dl: diffuseScratch(n), lat, holds: Uint8Array.from(species, (sp, i) => (sp.z !== 0 || lat?.on[i] ? 1 : 0)), drawSlot, draw, Dm });
165
+ slot++;
166
+ }
167
+ });
168
+ this.gam = new Float64Array(this.diffList.length * n);
169
+ this.gamOld = new Float64Array(this.diffList.length * n);
125
170
 
126
171
  // Per-node material data, flattened [g·n + i].
127
172
  this.present = new Uint8Array(nNodes * n);
@@ -177,6 +222,7 @@ export class Solver {
177
222
  // A face with φ undefined on both sides holds no charge, whatever its law says.
178
223
  model.interfaces.forEach((itf, f) => {
179
224
  if (itf.phi.type !== 'neutral' && this.phiUndefined[grid.regionEnd[f]] && this.phiUndefined[grid.regionEnd[f] + 1]) itf.phi = { type: 'neutral' };
225
+ // (a diffuse side has ions, so φ is defined there: the face above never has one)
180
226
  });
181
227
 
182
228
  // Unknowns as compensated double-doubles, u + uLo. Only differences of η need the extra
@@ -438,6 +484,10 @@ export class Solver {
438
484
  for (let i = 0; i < n; i++) active[bf * M + 1 + i] = itf.links[i].type === 'blocked' ? 0 : 1;
439
485
  for (let k = 0; k < itf.reactions.length + itf.gates.length; k++) active[bf * M + 1 + n + k] = 1;
440
486
  });
487
+ for (const d of this.diffList) {
488
+ active[d.o] = 1;
489
+ for (const q of d.drawSlot) if (q >= 0) active[this.blockOfFace[d.f] * M + q] = 1;
490
+ }
441
491
  const sizes = new Int32Array(nB), loc = (this.loc = new Int32Array(nB * M).fill(-1));
442
492
  for (let b = 0; b < nB; b++) for (let r = 0; r < M; r++) if (active[b * M + r]) loc[b * M + r] = sizes[b]++;
443
493
  const sys = (this.sys = new BlockTridiagonal(nB, sizes));
@@ -624,6 +674,18 @@ export class Solver {
624
674
  this.contactLaw = found.contactLaw;
625
675
  this.carriers = found.carriers;
626
676
  this.unkept = found.unkept; // (laws no row here can keep: see _directSteady)
677
+ // (room in each law's derivative for the diffuse layers' excesses, which src/laws.js, reading
678
+ // the structure alone, doesn't count: each layer's drop and its edge node's slots)
679
+ if (this.diffList.length > 0) {
680
+ const more = this.diffList.length * (this.M + 1);
681
+ for (const cs of [...this.constraints, ...this.timeLaws]) {
682
+ const idx = new Int32Array(cs.idx.length + more), w = new Float64Array(cs.w.length + more);
683
+ idx.set(cs.idx);
684
+ w.set(cs.w);
685
+ cs.idx = idx;
686
+ cs.w = w;
687
+ }
688
+ }
627
689
  this.contactQ = [null, null]; // (each contact's charge as _contact captures it, for contactLaw)
628
690
  this.pinnedIslands = [];
629
691
  this.islandsOn = false; // (newton() turns them on, see there)
@@ -652,12 +714,22 @@ export class Solver {
652
714
  return port.surface[0].capacity * s;
653
715
  }
654
716
 
655
- /** Total amount (mol per unit area) of a stretch's species in the current state. */
717
+ /** Total amount (mol per unit area) of a stretch's species in the current state, its faces' diffuse layers' included. */
656
718
  amount(stretch) {
657
719
  const { n, c, model } = this;
658
720
  const vol = model.grid.vol;
659
721
  let s = 0;
660
722
  for (let g = stretch.nodes[0]; g <= stretch.nodes[1]; g++) s += vol[g] * c[g * n + stretch.species];
723
+ return s + this._diffuseHeld(stretch, this.gam);
724
+ }
725
+
726
+ // What the faces' diffuse layers hold of a stretch's species (mol, per area if planar), by the
727
+ // excesses gam (now, or gamOld at the step's start).
728
+ _diffuseHeld(stretch, gam) {
729
+ let s = 0;
730
+ this.diffList.forEach((dl, k) => {
731
+ if (dl.g >= stretch.nodes[0] && dl.g <= stretch.nodes[1]) s += this.model.grid.area[dl.g] * gam[k * this.n + stretch.species];
732
+ });
661
733
  return s;
662
734
  }
663
735
 
@@ -848,6 +920,7 @@ export class Solver {
848
920
  cOld: Float64Array.from(this.cOld),
849
921
  thOld: Float64Array.from(this.thOld),
850
922
  th0Old: Float64Array.from(this.th0Old),
923
+ gamOld: Float64Array.from(this.gamOld),
851
924
  gateOld: Float64Array.from(this.gateOld),
852
925
  time: this.time,
853
926
  lastDt: this.lastDt,
@@ -872,6 +945,7 @@ export class Solver {
872
945
  this.cOld.set(s.cOld);
873
946
  this.thOld.set(s.thOld);
874
947
  this.th0Old.set(s.th0Old);
948
+ this.gamOld.set(s.gamOld);
875
949
  this.gateOld.set(s.gateOld);
876
950
  this.time = s.time;
877
951
  this.lastDt = s.lastDt;
@@ -972,19 +1046,22 @@ export class Solver {
972
1046
  if (link.type !== 'permeability') return null;
973
1047
  const kl = gl * n + i, kr = gr * n + i;
974
1048
  const shift = this.mu0hat[kr] - this.mu0hat[kl] - Math.log(this.cRef[kr] / this.cRef[kl]);
975
- const d = z[i] * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M])) + shift;
1049
+ // (between the diffuse layers' planes, where the face has them: see _face)
1050
+ const [dL, dR] = this.diffAt[f], sL = dL < 0 ? 0 : u[this.diffList[dL].o], sR = dR < 0 ? 0 : u[this.diffList[dR].o];
1051
+ const cl = this.c[kl] * Math.exp(-z[i] * sL), cr = this.c[kr] * Math.exp(-z[i] * sR);
1052
+ const d = z[i] * (u[bR * M] - u[bL * M] + (uLo[bR * M] - uLo[bL * M]) + sR - sL) + shift;
976
1053
  const deta = u[bR * M + 1 + i] - u[bL * M + 1 + i] + (uLo[bR * M + 1 + i] - uLo[bL * M + 1 + i]);
977
1054
  const gated = link.gates ?? [];
978
1055
  const P = link.P * (gated.length ? this._open(gated, (q) => u[this._gateSlot(f, q)]) : 1);
979
- const right = P * bernoulli(d) * this.c[kl];
1056
+ const right = P * bernoulli(d) * cl;
980
1057
  // (far up a drift, right × e^Δη is 0·∞: the backward one is then sgFlux's, whole)
981
- return [right, d < SG_FAR ? right * Math.exp(deta) : sgFlux(P, d, deta, this.c[kl], this.c[kr], this.sg)[3]];
1058
+ return [right, d < SG_FAR ? right * Math.exp(deta) : sgFlux(P, d, deta, cl, cr, this.sg)[3]];
982
1059
  });
983
1060
  }
984
1061
 
985
1062
  // Add this step's contact fluxes (at the converged state) to each stretch's intake.
986
1063
  // (A BDF2 step also moves each amount by Σ v (c* − c_n), its history term.)
987
- _accumulateBoundaryIntake(dt, cN) {
1064
+ _accumulateBoundaryIntake(dt, cN, gamN = this.gamOld) {
988
1065
  this._assembleBookkeeping(dt);
989
1066
  this.contactDEnd = { ...this.contactD };
990
1067
  this.portQEnd = Float64Array.from(this.portQ);
@@ -1008,6 +1085,7 @@ export class Solver {
1008
1085
  if (driven.has(k)) q = driven.get(k);
1009
1086
  let hist = 0;
1010
1087
  for (let g = st.nodes[0]; g <= st.nodes[1]; g++) hist += vol[g] * (cOld[g * n + st.species] - cN[g * n + st.species]);
1088
+ hist += this._diffuseHeld(st, this.gamOld) - this._diffuseHeld(st, gamN);
1011
1089
  this.boundaryIntake[k] += q * dt + hist;
1012
1090
  });
1013
1091
  }
package/src/statistics.js CHANGED
@@ -207,6 +207,7 @@ function latticeModel(idx, cRef, cMax) {
207
207
  return {
208
208
  type: 'lattice',
209
209
  idx,
210
+ cMax, // (read by a face's diffuse layer beside it, which crowds on the same sites: src/diffuse.js)
210
211
  // c_a = c_max w_a / (1 + Σ w), w_a = (c_ref,a / c_max) e^{ζ_a}; K = diag(c) − c cᵀ/c_max.
211
212
  evaluate(zeta, c, K) {
212
213
  let m = 0;
package/src/steady.js CHANGED
@@ -72,7 +72,8 @@
72
72
  // (∞ where the device can be solved directly; else 1e6·tau): a first step at ∞ that fails is
73
73
  // retried once with maxStep 3. A step that fails sends dt down (to tau·1e-6 from giant, then
74
74
  // ÷4, stopping below min(1e-15·tau, 1e-2·fastestTime)); one that converges sends it back up
75
- // (×2 after a failure, growing by ×1.5 to ×10 a step; past 1e6·tau, giant); on a direct device,
75
+ // (×2 after a failure, growing by ×1.5 to ×10 a step; past 1e6·tau, giant, or on a direct
76
+ // device past 1e12·tau); on a direct device,
76
77
  // once dt ≥ tau, a step at ∞ is tried once a decade of dt, the state kept where it fails. Past
77
78
  // giant (not direct), dt grows ×10 a step to 1e6·giant. Converged where a step at ∞ converged,
78
79
  // or, not direct, where a step of dt ≥ giant moved the potentials under tol (finished by one
@@ -133,7 +134,7 @@
133
134
  // here is more than u, uLo, termV and time: this.solvedV, this.stepped, whether the history
134
135
  // is empty (this.violent) and the drives, all of which choose the stages.
135
136
  // 5. Reach: time steps reach a mode as slow as ~1e12·tau (1e6·tau a step, past the giant; on a
136
- // direct device no finite step is longer than 1e6·tau). A reaction that alone settles a
137
+ // direct device by finite steps to 1e12·tau). A reaction that alone settles a
137
138
  // closed combination, slower than that, isn't waited out: the solve fails or converges where
138
139
  // it began, and stuckReactions names it (Solution's warnings, both ways).
139
140
  // Tests: every Device.solve goes through here; test/steady.test.js (the stages, the laws, the
@@ -707,7 +708,10 @@ export class SteadySolves {
707
708
  if (dt < giant) {
708
709
  dt *= grow; // ramping up after a failure
709
710
  grow = Math.min(10, grow * 1.5);
710
- if (dt > 1e6 * tau) dt = giant; // then the direct steady solve (or the giant step)
711
+ // Then the giant step; or, solved directly, on to 1e12 τ, the direct solve tried once a
712
+ // decade (above): jumping to dt = ∞ at 1e6 τ, its failure sent dt back to 1e-6 τ, round
713
+ // and round below 70 s, where traps filled by 1e9 cm⁻³ electrons took 1e3 s (τ 7e-5 s).
714
+ if (dt > (direct ? 1e12 : 1e6) * tau) dt = giant;
711
715
  } else if (dt < 1e6 * giant) {
712
716
  dt *= 10; // conserved amounts present and still moving: let dt keep growing (capped)
713
717
  }
@@ -754,7 +758,9 @@ export class SteadySolves {
754
758
  // it, and there's nothing here to shift.
755
759
  if (!st.spectator || this.model.materials[this.model.regions[st.regions[0]].material].conductor) return;
756
760
  const want = this.referenceAmounts[k];
757
- const ideal = this.nodeIdeal.subarray(st.nodes[0], st.nodes[1] + 1).every((v) => v === 1);
761
+ // (one shift of η scales an ideal stretch's amount exactly, but not what a diffuse layer
762
+ // at its edge holds, whose excess isn't proportional to c: iterate there too)
763
+ const ideal = this.nodeIdeal.subarray(st.nodes[0], st.nodes[1] + 1).every((v) => v === 1) && this._diffuseHeld(st, this.gam) === 0;
758
764
  for (let it = 0; it < (ideal ? 1 : 30); it++) {
759
765
  const now = this.amount(st);
760
766
  if (!(now > 0) || now === want) return;
@@ -766,6 +772,9 @@ export class SteadySolves {
766
772
  const dc = this.nodeIdeal[g] ? this.c[g * n + st.species] : this.K[g * n * n + st.species * (n + 1)];
767
773
  dA += this.model.grid.vol[g] * dc;
768
774
  }
775
+ this.diffList.forEach((dl, d) => {
776
+ if (dl.g >= st.nodes[0] && dl.g <= st.nodes[1]) dA += this.model.grid.area[dl.g] * dl.dl.dgdc[st.species * (n + 1)] * this.c[dl.g * n + st.species];
777
+ });
769
778
  shift = (want - now) / dA;
770
779
  if (!(Math.abs(shift) > 1e-16)) return;
771
780
  }
package/src/timeSteps.js CHANGED
@@ -8,7 +8,8 @@
8
8
  // in docs/internals.md. The Solver's own step, advance and integrate call them (the API, the
9
9
  // steady solves' time stepping and the kit call those). What they use of it:
10
10
  // s.newton(dt, opts) solves the equations at the step's end, from the state as it stands,
11
- // with storage (c − c*)/dt against s.cOld, s.thOld, s.th0Old, s.gateOld,
11
+ // with storage (c − c*)/dt against s.cOld, s.thOld, s.th0Old, s.gamOld (the
12
+ // diffuse layers' excesses), s.gateOld,
12
13
  // s.contactDStart, s.portQStart; leaves the state at its last iterate.
13
14
  // s._snapshot() / s._restore(snap) the whole time state (u, uLo, termV, the start-of-step
14
15
  // values, time, lastDt, history, the boundary intake, sourceTime).
@@ -17,7 +18,7 @@
17
18
  //
18
19
  // The state: u + uLo (Newton's unknowns, η and φ̂ in thermal units), termV (terminal voltages),
19
20
  // time. The history: s.history, the accepted steps before this one, newest first, at most 3,
20
- // each { t (its start), dt, u (its start state), c, th, th0, g (gates), D (contacts' D), Q
21
+ // each { t (its start), dt, u (its start state), c, th, th0, gam, g (gates), D (contacts' D), Q
21
22
  // (ports' charges) }: what BDF2 and the predictor need.
22
23
  //
23
24
  // step(s, dt, opts) → newton's result (+ bdf)
@@ -128,7 +129,7 @@ export function step(s, dt, opts = {}) {
128
129
  // What the step overwrites before Newton runs, for a failed one to put back: the Solution read
129
130
  // after it takes its contacts' fluxes against the start-of-step values (a host emptied by its
130
131
  // current read its contact's Li⁺ flux 8× off at the same state).
131
- const was = { cOld: Float64Array.from(s.cOld), thOld: Float64Array.from(s.thOld), th0Old: Float64Array.from(s.th0Old), gateOld: Float64Array.from(s.gateOld), contactDStart: s.contactDStart, portQStart: s.portQStart, atSteady: s.atSteady };
132
+ const was = { cOld: Float64Array.from(s.cOld), thOld: Float64Array.from(s.thOld), th0Old: Float64Array.from(s.th0Old), gamOld: Float64Array.from(s.gamOld), gateOld: Float64Array.from(s.gateOld), contactDStart: s.contactDStart, portQStart: s.portQStart, atSteady: s.atSteady };
132
133
  s.atSteady = false;
133
134
  const prev = s.history[0];
134
135
  const w = prev ? dt / prev.dt : 0;
@@ -137,7 +138,7 @@ export function step(s, dt, opts = {}) {
137
138
  s.uPrevLo.set(s.uLo);
138
139
  s.termVPrev.set(s.termV);
139
140
  s.computeConcentrations();
140
- const cN = Float64Array.from(s.c), thN = Float64Array.from(s.th), th0N = Float64Array.from(s.th0);
141
+ const cN = Float64Array.from(s.c), thN = Float64Array.from(s.th), th0N = Float64Array.from(s.th0), gamN = Float64Array.from(s.gam);
141
142
  const gN = Float64Array.from(s.gateList, (gt) => s.u[gt.o] + s.uLo[gt.o]);
142
143
  // Contact displacement before the step: as it was at the end of the previous step (under
143
144
  // the parameters then), so a gate-voltage change shows up as displacement current.
@@ -155,6 +156,7 @@ export function step(s, dt, opts = {}) {
155
156
  for (let k = 0; k < cOld.length; k++) cOld[k] = b1 * cN[k] - b2 * prev.c[k];
156
157
  for (let k = 0; k < thN.length; k++) s.thOld[k] = b1 * thN[k] - b2 * prev.th[k];
157
158
  for (let k = 0; k < th0N.length; k++) s.th0Old[k] = b1 * th0N[k] - b2 * prev.th0[k];
159
+ for (let k = 0; k < gamN.length; k++) s.gamOld[k] = b1 * gamN[k] - b2 * prev.gam[k];
158
160
  for (let k = 0; k < gN.length; k++) s.gateOld[k] = b1 * gN[k] - b2 * prev.g[k];
159
161
  s.contactDStart = { left: b1 * DN.left - b2 * prev.D.left, right: b1 * DN.right - b2 * prev.D.right };
160
162
  s.portQStart = QN.map((q, k) => b1 * q - b2 * prev.Q[k]);
@@ -163,6 +165,7 @@ export function step(s, dt, opts = {}) {
163
165
  s.cOld.set(cN);
164
166
  s.thOld.set(thN);
165
167
  s.th0Old.set(th0N);
168
+ s.gamOld.set(gamN);
166
169
  s.gateOld.set(gN);
167
170
  s.contactDStart = DN;
168
171
  s.portQStart = QN;
@@ -205,13 +208,13 @@ export function step(s, dt, opts = {}) {
205
208
  }
206
209
  result.bdf = bdf;
207
210
  if (result.converged) {
208
- s.history.unshift({ t: s.time, dt, u: Float64Array.from(s.uPrev), c: cN, th: thN, th0: th0N, g: gN, D: DN, Q: QN });
211
+ s.history.unshift({ t: s.time, dt, u: Float64Array.from(s.uPrev), c: cN, th: thN, th0: th0N, gam: gamN, g: gN, D: DN, Q: QN });
209
212
  if (s.history.length > 3) s.history.length = 3;
210
213
  s.time = tEnd;
211
214
  s.lastDt = dtEff;
212
215
  s.contactDOld = s.contactDStart;
213
216
  s.portQOld = s.portQStart;
214
- s._accumulateBoundaryIntake(dtEff, cN);
217
+ s._accumulateBoundaryIntake(dtEff, cN, gamN);
215
218
  // (In time, the state is no longer the steady solve's that a continuation would start from.)
216
219
  if (!s.steady) {
217
220
  s.solvedV = undefined;
@@ -224,6 +227,7 @@ export function step(s, dt, opts = {}) {
224
227
  s.cOld.set(was.cOld);
225
228
  s.thOld.set(was.thOld);
226
229
  s.th0Old.set(was.th0Old);
230
+ s.gamOld.set(was.gamOld);
227
231
  s.gateOld.set(was.gateOld);
228
232
  s.contactDStart = was.contactDStart;
229
233
  s.portQStart = was.portQStart;
package/src/types.js CHANGED
@@ -114,6 +114,15 @@
114
114
  * @property {Record<string, number>} [K] with vmax: a half-saturation concentration (mol/m³) for
115
115
  * every species the forward reaction consumes
116
116
  */
117
+ /**
118
+ * A face's diffuse layer on one side, in a solution.
119
+ * @typedef {object} DiffuseLayerResult
120
+ * @property {number} psi the drop from the side's edge node to the plane, φ_plane − φ_edge (V)
121
+ * @property {number} charge the layer's charge (C/m²)
122
+ * @property {Record<string, number>} excess each ion's surface excess, ∫ (c − c_edge) dx (mol/m²)
123
+ * @property {Record<string, number>} plane each ion's concentration at the plane, c e^{−zFψ/RT} (mol/m³; on a
124
+ * lattice, over its crowding; for what a face reaction takes or makes there, times its level's drop across the layer)
125
+ */
117
126
  /**
118
127
  * One face between consecutive regions. For different materials under a 'pinned' or
119
128
  * 'capacitive' law, exactly one alignment (step or dipole) is required.
@@ -124,6 +133,9 @@
124
133
  * @property {InterfacePhiLaw} [phi] electrostatic law (default 'pinned'; 'neutral' between ε = 0 materials;
125
134
  * beside a conductor 'neutral' or capacitive, required next to an ε > 0 material)
126
135
  * @property {number} [sheetCharge] C/m²
136
+ * @property {{ epsr: number }} [diffuse] a Gouy–Chapman diffuse layer below the grid on each strictly
137
+ * neutral (ε = 0) side of a charged face, of relative permittivity epsr: the side's share of the
138
+ * face's charge held as its ions' surface excesses, the face's reactions at its plane
127
139
  * @property {Record<string, InterfaceSpeciesLink>} [species] per-species laws (default equilibrium
128
140
  * where present on both sides and mobile on one, blocked otherwise; a species reacting at the
129
141
  * face and present on both sides has no default)
@@ -283,11 +295,13 @@
283
295
  * side holds; with gates, each gate's fraction open at each node and the voltage they follow (V)
284
296
  * @property {{ left: string, right: string, dipole: number, sheetCharge: number, D: number, N: Record<string, number>,
285
297
  * rates: number[], links: Record<string, string>, oneWay?: Record<string, [number, number]>,
286
- * gates?: Record<string, number>, V?: number }[]} interfaces each face: the regions it joins, its dipole
298
+ * gates?: Record<string, number>, V?: number,
299
+ * diffuse?: { left?: DiffuseLayerResult, right?: DiffuseLayerResult } }[]} interfaces each face: the regions it joins, its dipole
287
300
  * (V) and sheet charge (C/m²), its displacement D (C/m²), each species' flux N across it and each face
288
301
  * reaction's net rate (mol/(m²·s), toward +x and forward), how each species that crosses it does (its
289
302
  * link type); for those crossing by permeability, the one-way fluxes [toward +x, toward −x] whose
290
- * difference is N; with gates, each one's fraction open and the voltage they follow, φ_right − φ_left (V)
303
+ * difference is N; with gates, each one's fraction open and the voltage they follow, φ_right − φ_left (V; between the diffuse layers' planes, where it has them);
304
+ * with a diffuse layer, each side's
291
305
  * @property {{ rate: Float64Array, forward: Float64Array, nu: Record<string, number>, regions: number[], total: number }[]} bulkReactions
292
306
  * each bulk reaction's net rate per node (mol/(m³·s), forward minus backward, NaN where it doesn't
293
307
  * run), its one-way forward rate (the backward is forward − rate), what one forward reaction makes
@@ -2,12 +2,20 @@ export function gateRate(law: any, V: any): number[];
2
2
  export class BoundaryTerms {
3
3
  _portRate(port: any, rx: any, g: any): number;
4
4
  _face(f: any): void;
5
+ _diffuseDraws(d: any, bf: any): void;
6
+ _diffuseTerms(d: any, bf: any, Af: any): void;
7
+ dW: Float64Array<any>;
8
+ _diffuseChain(d: any): any;
9
+ dV2: Float64Array<any>;
10
+ _diffuseLayers(): void;
5
11
  _gateSlot(f: any, q: any): any;
6
12
  _gateVoltage(gt: any): number;
7
13
  _portVoltage(k: any, g: any): number;
8
14
  _faceVoltage(f: any): number;
9
15
  _faceGates(f: any, bf: any, bL: any, bR: any): void;
10
16
  _faceReaction(rx: any, k: any, f: any, bf: any, bL: any, bR: any, Af: any): void;
17
+ dWr: Float64Array<any>;
18
+ aWr: Float64Array<any>;
11
19
  _liveTerms(): any;
12
20
  _allTerms: any;
13
21
  _terminals(dt: any): void;
@@ -0,0 +1,25 @@
1
+ /** The diffuse layer's charge, excesses and their derivatives (see the contract above). */
2
+ export function diffuseLayer(c: any, z: any, n: any, s: any, { A, B }: {
3
+ A: any;
4
+ B: any;
5
+ }, out: any, lat?: any, draw?: any): any;
6
+ /** A scratch object for diffuseLayer over n species. */
7
+ export function diffuseScratch(n: any): {
8
+ q: number;
9
+ dqds: number;
10
+ dqdc: Float64Array<any>;
11
+ gam: Float64Array<any>;
12
+ dgds: Float64Array<any>;
13
+ dgdc: Float64Array<ArrayBuffer>;
14
+ K: Float64Array<any>;
15
+ dKds: Float64Array<any>;
16
+ dKdc: Float64Array<ArrayBuffer>;
17
+ D: number;
18
+ dDds: number;
19
+ dDdc: Float64Array<any>;
20
+ };
21
+ /** The constants A = √(εRT/2)/F and B = √(2εRT) for a layer of permittivity eps (F/m) at RT (J/mol). */
22
+ export function diffuseUnits(eps: any, RT: any, F: any): {
23
+ A: number;
24
+ B: number;
25
+ };
package/types/newton.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export function maxPotentialStep(s: any, delta: any, deltaV?: any): number;
1
+ export function maxPotentialStep(s: any, delta: any, deltaV?: any, layers?: boolean): number;
2
2
  export function newton(s: any, dt: any, { maxIter, tol, maxStep, islands }?: {
3
3
  maxIter?: number;
4
4
  tol?: number;
package/types/solver.d.ts CHANGED
@@ -32,6 +32,10 @@ export class Solver {
32
32
  gateOld: Float64Array<ArrayBuffer>;
33
33
  gateIndex: Int32Array<ArrayBuffer>;
34
34
  portGateIndex: any;
35
+ diffList: any[];
36
+ diffAt: any;
37
+ gam: Float64Array<ArrayBuffer>;
38
+ gamOld: Float64Array<ArrayBuffer>;
35
39
  present: Uint8Array<ArrayBuffer>;
36
40
  cRef: Float64Array<ArrayBuffer>;
37
41
  mu0hat: Float64Array<ArrayBuffer>;
@@ -234,8 +238,9 @@ export class Solver {
234
238
  _surfaceStart(g: any): void;
235
239
  /** Amount (mol per unit area) of surface column j's species, Γ Σ v·a·θ, in the current state. */
236
240
  surfaceAmount(j: any): number;
237
- /** Total amount (mol per unit area) of a stretch's species in the current state. */
241
+ /** Total amount (mol per unit area) of a stretch's species in the current state, its faces' diffuse layers' included. */
238
242
  amount(stretch: any): number;
243
+ _diffuseHeld(stretch: any, gam: any): number;
239
244
  redrive(): void;
240
245
  deltaV: any;
241
246
  _drivenKey(): any;
@@ -280,6 +285,7 @@ export class Solver {
280
285
  cOld: Float64Array<ArrayBuffer>;
281
286
  thOld: Float64Array<ArrayBuffer>;
282
287
  th0Old: Float64Array<ArrayBuffer>;
288
+ gamOld: Float64Array<ArrayBuffer>;
283
289
  gateOld: Float64Array<ArrayBuffer>;
284
290
  time: number;
285
291
  lastDt: number;
@@ -308,6 +314,6 @@ export class Solver {
308
314
  _terminalCurrent(): number;
309
315
  segmentFluxes(): Float64Array<ArrayBuffer>[];
310
316
  faceOneWay(f: any): any;
311
- _accumulateBoundaryIntake(dt: any, cN: any): void;
317
+ _accumulateBoundaryIntake(dt: any, cN: any, gamN?: Float64Array<ArrayBuffer>): void;
312
318
  }
313
319
  import { BlockTridiagonal } from './blockTridiagonal.js';
package/types/types.d.ts CHANGED
@@ -250,6 +250,28 @@ export type FaceReactionDef = {
250
250
  */
251
251
  K?: Record<string, number>;
252
252
  };
253
+ /**
254
+ * A face's diffuse layer on one side, in a solution.
255
+ */
256
+ export type DiffuseLayerResult = {
257
+ /**
258
+ * the drop from the side's edge node to the plane, φ_plane − φ_edge (V)
259
+ */
260
+ psi: number;
261
+ /**
262
+ * the layer's charge (C/m²)
263
+ */
264
+ charge: number;
265
+ /**
266
+ * each ion's surface excess, ∫ (c − c_edge) dx (mol/m²)
267
+ */
268
+ excess: Record<string, number>;
269
+ /**
270
+ * each ion's concentration at the plane, c e^{−zFψ/RT} (mol/m³; on a
271
+ * lattice, over its crowding; for what a face reaction takes or makes there, times its level's drop across the layer)
272
+ */
273
+ plane: Record<string, number>;
274
+ };
253
275
  /**
254
276
  * One face between consecutive regions. For different materials under a 'pinned' or
255
277
  * 'capacitive' law, exactly one alignment (step or dipole) is required.
@@ -279,6 +301,14 @@ export type InterfaceDef = {
279
301
  * C/m²
280
302
  */
281
303
  sheetCharge?: number;
304
+ /**
305
+ * a Gouy–Chapman diffuse layer below the grid on each strictly
306
+ * neutral (ε = 0) side of a charged face, of relative permittivity epsr: the side's share of the
307
+ * face's charge held as its ions' surface excesses, the face's reactions at its plane
308
+ */
309
+ diffuse?: {
310
+ epsr: number;
311
+ };
282
312
  /**
283
313
  * per-species laws (default equilibrium
284
314
  * where present on both sides and mobile on one, blocked otherwise; a species reacting at the
@@ -717,7 +747,8 @@ export type Solution = {
717
747
  * (V) and sheet charge (C/m²), its displacement D (C/m²), each species' flux N across it and each face
718
748
  * reaction's net rate (mol/(m²·s), toward +x and forward), how each species that crosses it does (its
719
749
  * link type); for those crossing by permeability, the one-way fluxes [toward +x, toward −x] whose
720
- * difference is N; with gates, each one's fraction open and the voltage they follow, φ_right − φ_left (V)
750
+ * difference is N; with gates, each one's fraction open and the voltage they follow, φ_right − φ_left (V; between the diffuse layers' planes, where it has them);
751
+ * with a diffuse layer, each side's
721
752
  */
722
753
  interfaces: {
723
754
  left: string;
@@ -731,6 +762,10 @@ export type Solution = {
731
762
  oneWay?: Record<string, [number, number]>;
732
763
  gates?: Record<string, number>;
733
764
  V?: number;
765
+ diffuse?: {
766
+ left?: DiffuseLayerResult;
767
+ right?: DiffuseLayerResult;
768
+ };
734
769
  }[];
735
770
  /**
736
771
  * each bulk reaction's net rate per node (mol/(m³·s), forward minus backward, NaN where it doesn't