driftlet 0.6.0 → 0.8.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/README.md +15 -6
- package/docs/device.md +138 -10
- package/docs/kit.md +58 -4
- package/docs/numerics.md +16 -0
- package/llms.txt +78 -15
- package/package.json +1 -1
- package/src/checks.js +59 -24
- package/src/describe.js +21 -3
- package/src/device.js +126 -17
- package/src/grid.js +73 -0
- package/src/index.js +1 -1
- package/src/junction.js +164 -0
- package/src/kit.js +2 -0
- package/src/polarization.js +84 -0
- package/src/recorder.js +1 -1
- package/src/solution.js +12 -4
- package/src/solver.js +240 -47
- package/src/types.js +15 -4
- package/types/grid.d.ts +29 -0
- package/types/index.d.ts +3 -3
- package/types/junction.d.ts +32 -0
- package/types/kit.d.ts +2 -0
- package/types/polarization.d.ts +31 -0
- package/types/recorder.d.ts +3 -3
- package/types/solver.d.ts +6 -2
- package/types/types.d.ts +41 -4
package/src/junction.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Liquid-junction potentials from formulas, for driftlet/kit: the diffusion potential φ_R − φ_L
|
|
2
|
+
// between two neutral solutions, as the classical theories give it. A device simulates the actual
|
|
3
|
+
// junction (steady between two baths, it is Planck's; stepping from a sharp boundary, a free
|
|
4
|
+
// diffusion junction); these are for comparison, and for quick estimates.
|
|
5
|
+
|
|
6
|
+
import { DeviceError } from './errors.js';
|
|
7
|
+
import { FARADAY, GAS_CONSTANT } from './constants.js';
|
|
8
|
+
|
|
9
|
+
const fail = (message) => {
|
|
10
|
+
throw new DeviceError(message);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// The ions both solutions name, with their charge and diffusivity, and both concentrations.
|
|
14
|
+
function prepare(left, right, ions, what) {
|
|
15
|
+
const names = [...new Set([...Object.keys(left), ...Object.keys(right)])];
|
|
16
|
+
return names.map((name) => {
|
|
17
|
+
const ion = ions[name];
|
|
18
|
+
if (!ion || !Number.isFinite(ion.z) || !(ion.D > 0)) fail(`${what}: no { z, D } for '${name}' in the ions given`);
|
|
19
|
+
const cL = left[name] ?? 0, cR = right[name] ?? 0;
|
|
20
|
+
if (!(cL >= 0 && cR >= 0)) fail(`${what}: concentrations must be ≥ 0 (mol/m³), got ${name} ${cL} and ${cR}`);
|
|
21
|
+
return { name, z: ion.z, D: ion.D, cL, cR };
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function neutral(list, side, what) {
|
|
25
|
+
let q = 0, s = 0;
|
|
26
|
+
for (const p of list) {
|
|
27
|
+
q += p.z * p[side];
|
|
28
|
+
s += Math.abs(p.z * p[side]);
|
|
29
|
+
}
|
|
30
|
+
if (Math.abs(q) > 1e-9 * s) fail(`${what}: the ${side === 'cL' ? 'left' : 'right'} solution carries a net charge (Σ z·c = ${q.toPrecision(3)} mol/m³)`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Henderson's liquid-junction potential, φ_R − φ_L (V): the junction taken to be the two solutions
|
|
35
|
+
* mixed in one proportion at every point (every ion's mixing fraction the same curve). Exact for a single binary salt, a close estimate otherwise (it's what
|
|
36
|
+
* JPCalc and most electrophysiology corrections use).
|
|
37
|
+
* @param {Record<string, number>} left mol/m³ by ion
|
|
38
|
+
* @param {Record<string, number>} right mol/m³ by ion
|
|
39
|
+
* @param {Record<string, { z: number, D: number }>} ions charge and diffusivity (m²/s), e.g. IONS
|
|
40
|
+
* @param {{ T?: number }} [opts] K (default 298.15)
|
|
41
|
+
*/
|
|
42
|
+
export function henderson(left, right, ions, { T = 298.15 } = {}) {
|
|
43
|
+
const list = prepare(left, right, ions, 'henderson');
|
|
44
|
+
neutral(list, 'cL', 'henderson');
|
|
45
|
+
neutral(list, 'cR', 'henderson');
|
|
46
|
+
let a = 0, b = 0, gL = 0, gR = 0;
|
|
47
|
+
for (const { z, D, cL, cR } of list) {
|
|
48
|
+
a += z * D * (cR - cL);
|
|
49
|
+
b += z * z * D * (cR - cL);
|
|
50
|
+
gL += z * z * D * cL;
|
|
51
|
+
gR += z * z * D * cR;
|
|
52
|
+
}
|
|
53
|
+
const VT = (GAS_CONSTANT * T) / FARADAY;
|
|
54
|
+
if (b === 0) return 0;
|
|
55
|
+
return (-VT * a * Math.log(gR / gL)) / b;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Planck's liquid-junction potential, φ_R − φ_L (V): the steady state of constrained diffusion,
|
|
60
|
+
* a junction zone held between the two solutions, electroneutral, carrying no current. Solved by
|
|
61
|
+
* shooting on the Nernst–Planck equations across it (its length drops out). Exact for that
|
|
62
|
+
* junction; equal to Henderson's for a single binary salt.
|
|
63
|
+
* @param {Record<string, number>} left mol/m³ by ion
|
|
64
|
+
* @param {Record<string, number>} right mol/m³ by ion
|
|
65
|
+
* @param {Record<string, { z: number, D: number }>} ions charge and diffusivity (m²/s), e.g. IONS
|
|
66
|
+
* @param {{ T?: number, steps?: number }} [opts] K (default 298.15); RK4 steps across (default 4000)
|
|
67
|
+
*/
|
|
68
|
+
export function planck(left, right, ions, { T = 298.15, steps = 4000 } = {}) {
|
|
69
|
+
const list = prepare(left, right, ions, 'planck');
|
|
70
|
+
neutral(list, 'cL', 'planck');
|
|
71
|
+
neutral(list, 'cR', 'planck');
|
|
72
|
+
const n = list.length, z = list.map((p) => p.z), D = list.map((p) => p.D), Dmax = Math.max(...D);
|
|
73
|
+
const scale = Math.max(...list.map((p) => Math.max(p.cL, p.cR)));
|
|
74
|
+
const cL = list.map((p) => p.cL / scale), cR = list.map((p) => p.cR / scale);
|
|
75
|
+
// Across the zone (x from 0 to 1, D's scale dropping out with its length), each ion's flux J_i is
|
|
76
|
+
// constant: c_i' = −J_i/D_i − z_i c_i ψ', with ψ = Fφ/RT, and neutrality fixes the field,
|
|
77
|
+
// ψ' = −Σ(z_i J_i/D_i)/Σ(z_i² c_i). Integrated from the left; the fluxes are chosen so the
|
|
78
|
+
// right comes out as given and no current flows.
|
|
79
|
+
const deriv = (c, J, out) => {
|
|
80
|
+
let num = 0, den = 0;
|
|
81
|
+
for (let i = 0; i < n; i++) {
|
|
82
|
+
num += (z[i] * J[i]) / D[i];
|
|
83
|
+
den += z[i] * z[i] * Math.max(c[i], 0);
|
|
84
|
+
}
|
|
85
|
+
const dpsi = -num / den;
|
|
86
|
+
for (let i = 0; i < n; i++) out[i] = -J[i] / D[i] - z[i] * c[i] * dpsi;
|
|
87
|
+
return dpsi;
|
|
88
|
+
};
|
|
89
|
+
const shoot = (J) => {
|
|
90
|
+
const c = cL.slice(), k1 = new Float64Array(n), k2 = new Float64Array(n), k3 = new Float64Array(n), k4 = new Float64Array(n), t = new Float64Array(n);
|
|
91
|
+
const h = 1 / steps;
|
|
92
|
+
let psi = 0;
|
|
93
|
+
for (let s = 0; s < steps; s++) {
|
|
94
|
+
const p1 = deriv(c, J, k1);
|
|
95
|
+
for (let i = 0; i < n; i++) t[i] = c[i] + (h / 2) * k1[i];
|
|
96
|
+
const p2 = deriv(t, J, k2);
|
|
97
|
+
for (let i = 0; i < n; i++) t[i] = c[i] + (h / 2) * k2[i];
|
|
98
|
+
const p3 = deriv(t, J, k3);
|
|
99
|
+
for (let i = 0; i < n; i++) t[i] = c[i] + h * k3[i];
|
|
100
|
+
const p4 = deriv(t, J, k4);
|
|
101
|
+
for (let i = 0; i < n; i++) c[i] += (h / 6) * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]);
|
|
102
|
+
psi += (h / 6) * (p1 + 2 * p2 + 2 * p3 + p4);
|
|
103
|
+
}
|
|
104
|
+
return { c, psi };
|
|
105
|
+
};
|
|
106
|
+
// Residuals: n − 1 ions reaching the right (the last follows by neutrality), and no current.
|
|
107
|
+
const residual = (J) => {
|
|
108
|
+
const { c, psi } = shoot(J), r = new Float64Array(n);
|
|
109
|
+
for (let i = 0; i < n - 1; i++) r[i] = c[i] - cR[i];
|
|
110
|
+
let I = 0;
|
|
111
|
+
for (let i = 0; i < n; i++) I += z[i] * J[i];
|
|
112
|
+
r[n - 1] = I / Dmax; // scaled like the concentrations
|
|
113
|
+
return { r, psi };
|
|
114
|
+
};
|
|
115
|
+
let J = list.map((_, i) => -D[i] * (cR[i] - cL[i])); // plain diffusion to start
|
|
116
|
+
let { r, psi } = residual(J);
|
|
117
|
+
for (let it = 0; it < 50; it++) {
|
|
118
|
+
const norm = Math.max(...r.map(Math.abs));
|
|
119
|
+
if (norm < 1e-12) break;
|
|
120
|
+
// Newton with a finite-difference Jacobian.
|
|
121
|
+
const Jac = [];
|
|
122
|
+
for (let k = 0; k < n; k++) {
|
|
123
|
+
const dJ = 1e-7 * Math.max(Math.abs(J[k]), 1e-3 * D[k]), Jp = J.slice();
|
|
124
|
+
Jp[k] += dJ;
|
|
125
|
+
const rp = residual(Jp).r;
|
|
126
|
+
Jac.push(Array.from(rp, (v, i) => (v - r[i]) / dJ));
|
|
127
|
+
}
|
|
128
|
+
const step = solveDense(Jac, r, n);
|
|
129
|
+
let lambda = 1, next;
|
|
130
|
+
for (;;) {
|
|
131
|
+
const Jn = J.map((v, k) => v - lambda * step[k]);
|
|
132
|
+
next = residual(Jn);
|
|
133
|
+
if (Math.max(...next.r.map(Math.abs)) < norm || lambda < 1e-3) {
|
|
134
|
+
J = Jn;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
lambda /= 2;
|
|
138
|
+
}
|
|
139
|
+
({ r, psi } = next);
|
|
140
|
+
}
|
|
141
|
+
if (!(Math.max(...r.map(Math.abs)) < 1e-8)) fail('planck: the shooting did not converge');
|
|
142
|
+
return ((GAS_CONSTANT * T) / FARADAY) * psi;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Solves Σ_k A[k][i] x_k = b_i (A given column by column), by Gaussian elimination with pivoting.
|
|
146
|
+
function solveDense(cols, b, n) {
|
|
147
|
+
const M = Array.from({ length: n }, (_, i) => [...cols.map((col) => col[i]), b[i]]);
|
|
148
|
+
for (let p = 0; p < n; p++) {
|
|
149
|
+
let best = p;
|
|
150
|
+
for (let i = p + 1; i < n; i++) if (Math.abs(M[i][p]) > Math.abs(M[best][p])) best = i;
|
|
151
|
+
[M[p], M[best]] = [M[best], M[p]];
|
|
152
|
+
for (let i = p + 1; i < n; i++) {
|
|
153
|
+
const f = M[i][p] / M[p][p];
|
|
154
|
+
for (let j = p; j <= n; j++) M[i][j] -= f * M[p][j];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const x = new Array(n).fill(0);
|
|
158
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
159
|
+
let s = M[i][n];
|
|
160
|
+
for (let j = i + 1; j < n; j++) s -= M[i][j] * x[j];
|
|
161
|
+
x[i] = s / M[i][i];
|
|
162
|
+
}
|
|
163
|
+
return x;
|
|
164
|
+
}
|
package/src/kit.js
CHANGED
|
@@ -12,7 +12,9 @@ export { live } from './live.js';
|
|
|
12
12
|
export { pulse, square, triangle, ramp, injector, recombination, photogeneration } from './sources.js';
|
|
13
13
|
export { describe, unitWarnings } from './describe.js';
|
|
14
14
|
export { recorder } from './recorder.js';
|
|
15
|
+
export { henderson, planck } from './junction.js';
|
|
15
16
|
export { check } from './checks.js';
|
|
17
|
+
export { polarization } from './polarization.js';
|
|
16
18
|
export { perovskiteCell, PEROVSKITE_SCANS, hysteresis } from './devices.js';
|
|
17
19
|
export { IONS, H2O, WATER_EPSR, SEMICONDUCTORS, METALS, aqueous, semiconductor, metal } from './data.js';
|
|
18
20
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Polarization curves at an electrode, for driftlet/kit: each reaction's rate with the solution
|
|
2
|
+
// beside the electrode held as it is and the metal's level moved, which is what an Evans diagram
|
|
3
|
+
// plots. Where the net current crosses zero is the spot's open-circuit (mixed) potential.
|
|
4
|
+
|
|
5
|
+
import { DeviceError } from './errors.js';
|
|
6
|
+
import { FARADAY } from './constants.js';
|
|
7
|
+
import { powi, powr } from './pow.js';
|
|
8
|
+
|
|
9
|
+
const fail = (message) => {
|
|
10
|
+
throw new DeviceError(message);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Each electrode reaction's rate at a face (a metal's) or at one spot of an electrode port, with
|
|
15
|
+
* the composition beside it as in `sol` and the metal's carrier at each level in `V` (in the
|
|
16
|
+
* terminals' convention, V = μ̄/(zF); against SHE, subtract φ for the usual tables' ions). The
|
|
17
|
+
* rates are the solver's own law, so at the electrode's actual level they are
|
|
18
|
+
* `sol.interfaces[f].rates` (or `sol.ports[k].rates` at that node).
|
|
19
|
+
* @param {import('./index.js').Device} device
|
|
20
|
+
* @param {object} sol a solution of it
|
|
21
|
+
* @param {{ face: number } | { port: string | number, x?: number }} where a face's index, or a port
|
|
22
|
+
* (name or index) and the position in its window (m, the device's x; default its first node)
|
|
23
|
+
* @param {number | ArrayLike<number>} V the metal's level(s), V
|
|
24
|
+
* @returns {{ V: Float64Array, current: Float64Array, reactions: { equation: string, level: number,
|
|
25
|
+
* rate: Float64Array, current: Float64Array }[] }} per reaction: its equilibrium level here (where
|
|
26
|
+
* its rate is zero), its rate (mol/(m²·s) of electrode, forward) and its current (A/m², anodic
|
|
27
|
+
* positive: into the solution); and their sum
|
|
28
|
+
*/
|
|
29
|
+
export function polarization(device, sol, where, V) {
|
|
30
|
+
const model = device.model, { species, grid, RT } = model, solver = device.solver;
|
|
31
|
+
const Vs = Float64Array.from(typeof V === 'number' ? [V] : V);
|
|
32
|
+
let reactions, nodeOf, carrier, names;
|
|
33
|
+
if (where?.face !== undefined) {
|
|
34
|
+
const f = where.face, itf = model.interfaces[f];
|
|
35
|
+
if (!itf) fail(`polarization: no face ${f} (the device has ${model.interfaces.length})`);
|
|
36
|
+
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1];
|
|
37
|
+
const metal = [gL, gR].findIndex((g) => solver.nodeConductor[g] >= 0);
|
|
38
|
+
if (metal < 0) fail(`polarization: face ${f} has no metal on either side`);
|
|
39
|
+
carrier = { side: metal, i: solver.nodeConductor[metal ? gR : gL] };
|
|
40
|
+
nodeOf = (p) => (p.side ? gR : gL);
|
|
41
|
+
reactions = itf.reactions;
|
|
42
|
+
names = device.def.interfaces?.[f]?.reactions;
|
|
43
|
+
} else if (where?.port !== undefined) {
|
|
44
|
+
const k = typeof where.port === 'number' ? where.port : model.ports.findIndex((p) => p.name === where.port);
|
|
45
|
+
const port = model.ports[k];
|
|
46
|
+
if (!port) fail(`polarization: no port ${JSON.stringify(where.port)}`);
|
|
47
|
+
if (port.reactions.length === 0) fail(`polarization: port ${port.name} has no reactions`);
|
|
48
|
+
let g = port.nodes[0];
|
|
49
|
+
if (where.x !== undefined) for (const h of port.nodes) if (Math.abs(grid.x[h] - where.x) < Math.abs(grid.x[g] - where.x)) g = h;
|
|
50
|
+
carrier = { side: 1, i: port.terminal };
|
|
51
|
+
nodeOf = () => g;
|
|
52
|
+
reactions = port.reactions;
|
|
53
|
+
names = device.def.ports?.[k]?.reactions;
|
|
54
|
+
} else fail('polarization: say where, { face: f } or { port: name, x }');
|
|
55
|
+
if (reactions.length === 0) fail('polarization: no reactions there');
|
|
56
|
+
|
|
57
|
+
const out = reactions.map((rx, k) => {
|
|
58
|
+
if (rx.srh) fail('polarization: SRH kinetics has no electrode level');
|
|
59
|
+
// a = A/RT = a0 + s·V: everything but the carrier at its level, and the carrier's part.
|
|
60
|
+
let a0 = rx.fixedA, s = 0, q = 0, pref = rx.vmax ? rx.vmax : rx.k0;
|
|
61
|
+
rx.part.forEach((p, x) => {
|
|
62
|
+
if (p.side === carrier.side && p.i === carrier.i) {
|
|
63
|
+
s -= (p.nu * species[p.i].z * FARADAY) / RT;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const g = nodeOf(p), name = species[p.i].name;
|
|
67
|
+
a0 -= (p.nu * sol.mu[name][g]) / RT;
|
|
68
|
+
q += p.side === carrier.side ? 0 : p.nu * species[p.i].z; // charge the forward reaction puts into the solution
|
|
69
|
+
const c = sol.c[name][g];
|
|
70
|
+
if (rx.vmax) {
|
|
71
|
+
if (p.nu < 0) pref *= powi(c / (c + rx.K[x]), -p.nu);
|
|
72
|
+
} else pref *= powr(c / solver.cRef[g * species.length + p.i], p.nu < 0 ? -p.nu * (1 - rx.alpha) : p.nu * rx.alpha);
|
|
73
|
+
});
|
|
74
|
+
if (s === 0) fail(`polarization: reaction ${k} there takes no ${species[carrier.i].name} from the metal`);
|
|
75
|
+
const rate = Float64Array.from(Vs, (v) => {
|
|
76
|
+
const a = a0 + s * v;
|
|
77
|
+
return rx.vmax ? pref * -Math.expm1(-a) : pref * (Math.exp(rx.alpha * a) - Math.exp(-(1 - rx.alpha) * a));
|
|
78
|
+
});
|
|
79
|
+
return { equation: names?.[k]?.equation ?? `reaction ${k}`, level: -a0 / s, rate, current: rate.map((r) => q * FARADAY * r) };
|
|
80
|
+
});
|
|
81
|
+
const current = new Float64Array(Vs.length);
|
|
82
|
+
for (const r of out) for (let j = 0; j < Vs.length; j++) current[j] += r.current[j];
|
|
83
|
+
return { V: Vs, current, reactions: out };
|
|
84
|
+
}
|
package/src/recorder.js
CHANGED
|
@@ -15,7 +15,7 @@ const now = () => (typeof performance !== 'undefined' ? performance.now() : Date
|
|
|
15
15
|
* neither, at the end of each `advance()` call. The trace (`t`, `current`, `voltage`, and
|
|
16
16
|
* `probes` if given) runs from the start through every accepted step of every call.
|
|
17
17
|
* @param {import('./index.js').Device} device
|
|
18
|
-
* @param {{ every?: number, times?: number[], probes?: { x: number, species
|
|
18
|
+
* @param {{ every?: number, times?: number[], probes?: { x: number, species?: string, quantity?: 'c' | 'V' | 'phi', region?: string | number }[] }} [opts]
|
|
19
19
|
*/
|
|
20
20
|
export function recorder(device, opts = {}) {
|
|
21
21
|
const { every, times, probes } = opts;
|
package/src/solution.js
CHANGED
|
@@ -91,7 +91,8 @@ export function makeSolution(solver, result = {}) {
|
|
|
91
91
|
const V = solver.termV[side === 'left' ? 0 : 1];
|
|
92
92
|
sol.contacts[side] = { V, flux, D, current: conduction + displacement };
|
|
93
93
|
if (ct.phi.type === 'capacitive' || ct.phi.type === 'pinned') {
|
|
94
|
-
// Charge
|
|
94
|
+
// Charge on the gate (or metal) plate, per area if the device is planar: +D at the left, −D
|
|
95
|
+
// at the right.
|
|
95
96
|
sol.gates[side] = { V, D, charge: side === 'left' ? D : -D };
|
|
96
97
|
}
|
|
97
98
|
}
|
|
@@ -105,7 +106,14 @@ export function makeSolution(solver, result = {}) {
|
|
|
105
106
|
flux[species[i].name] = solver.portFlux[k][i];
|
|
106
107
|
current += F * species[i].z * solver.portFlux[k][i];
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
+
const out = { name: port.name, V: solver.termV[2 + k], flux, current };
|
|
110
|
+
if (port.reactions.length > 0) {
|
|
111
|
+
// An electrode spread through the window: where it sits, and each reaction's rate there.
|
|
112
|
+
out.x = Float64Array.from(port.nodes, (g) => model.grid.x[g]);
|
|
113
|
+
out.area = Float64Array.from(solver.portArea[k]);
|
|
114
|
+
out.rates = solver.portRates[k].map((r) => Float64Array.from(r)); // mol/(m²·s), forward
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
109
117
|
});
|
|
110
118
|
sol.terminalVoltage = sol.contacts.right.V - sol.contacts.left.V;
|
|
111
119
|
// Every terminal (the contacts, then the ports by name): its voltage and its current into the
|
|
@@ -142,14 +150,14 @@ export function makeSolution(solver, result = {}) {
|
|
|
142
150
|
return { rate, regions, total };
|
|
143
151
|
});
|
|
144
152
|
|
|
145
|
-
// Total charge
|
|
153
|
+
// Total charge in the device (space charge plus sheet charges), per area if it's planar.
|
|
146
154
|
let q = 0;
|
|
147
155
|
for (let g = 0; g < nNodes; g++) {
|
|
148
156
|
let rho = solver.rhoFixed[g];
|
|
149
157
|
for (let i = 0; i < n; i++) rho += F * species[i].z * c[g * n + i];
|
|
150
158
|
q += grid.vol[g] * rho;
|
|
151
159
|
}
|
|
152
|
-
|
|
160
|
+
interfaces.forEach((itf, f) => (q += itf.sheetCharge * grid.area[grid.regionEnd[f]]));
|
|
153
161
|
sol.charge = q;
|
|
154
162
|
|
|
155
163
|
// Resolution warnings: where the model resolves a double layer (dipole or capacitive faces,
|