driftlet 0.8.0 → 0.9.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 +4 -2
- package/docs/device.md +86 -2
- package/docs/kit.md +2 -2
- package/llms.txt +6 -6
- package/package.json +1 -1
- package/src/describe.js +3 -1
- package/src/device.js +74 -16
- package/src/index.js +1 -0
- package/src/polarization.js +13 -1
- package/src/solution.js +20 -2
- package/src/solver.js +161 -23
- package/src/stack.js +3 -2
- package/src/types.js +10 -3
- package/types/solver.d.ts +20 -1
- package/types/stack.d.ts +1 -1
- package/types/types.d.ts +25 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ The same equations go by different names in different fields. If your problem is
|
|
|
30
30
|
|
|
31
31
|
| If you work on | you may call it | start from | checked against |
|
|
32
32
|
|---|---|---|---|
|
|
33
|
-
| Semiconductor devices | drift–diffusion, van Roosbroeck, quasi-Fermi levels, Scharfetter–Gummel; pn, Schottky, MOS, heterojunctions | [first example](#a-semiconductor-junction), [pn](https://markblundeberg.github.io/driftlet/demos/pn.html) and [MOS](https://markblundeberg.github.io/driftlet/demos/mos.html) demos, [`contacts`](test/contacts.test.js), [`metal`](test/metal.test.js) tests | exact built-in potential, Shockley J–V, depletion charge, MOS C–V |
|
|
33
|
+
| Semiconductor devices | drift–diffusion, van Roosbroeck, quasi-Fermi levels, Scharfetter–Gummel; pn, Schottky, MOS, heterojunctions; thin-film transistors (gradual channel) | [first example](#a-semiconductor-junction), [pn](https://markblundeberg.github.io/driftlet/demos/pn.html) and [MOS](https://markblundeberg.github.io/driftlet/demos/mos.html) demos, [`contacts`](test/contacts.test.js), [`metal`](test/metal.test.js) tests | exact built-in potential, Shockley J–V, depletion charge, MOS C–V |
|
|
34
34
|
| Solar cells | photogeneration, radiative and SRH recombination | [solar](https://markblundeberg.github.io/driftlet/demos/solar.html) [organic (excitons)](https://markblundeberg.github.io/driftlet/demos/organic.html) and [perovskite (mobile ions)](https://markblundeberg.github.io/driftlet/demos/perovskite.html) demos, [`ionmonger`](test/ionmonger.test.js), [`reactions`](test/reactions.test.js) and [`generation`](test/generation.test.js) tests: generation is a reaction from a photon reservoir (uniform, or Beer–Lambert with `photogeneration()`), SRH as a rate law, in the bulk and at faces | `J_sc = qG(L_n + L_p + W)`; J_sc under Beer–Lambert against collection theory; Shockley J–V in the dark |
|
|
35
35
|
| Electrochemistry, corrosion | Nernst–Planck, concentration polarization, limiting current, Butler–Volmer, Warburg, cyclic voltammetry, salt bridges and liquid junctions, mixed potentials, electrodes under a film | [second example](#an-electrochemical-cell), [cyclic voltammetry](https://markblundeberg.github.io/driftlet/demos/redox.html), [Daniell cell](https://markblundeberg.github.io/driftlet/demos/daniell.html), [saturation](https://markblundeberg.github.io/driftlet/demos/saturation.html), [impedance](https://markblundeberg.github.io/driftlet/demos/impedance.html) and [liquid-junction](https://markblundeberg.github.io/driftlet/demos/junction.html) demos, [`circuit`](test/circuit.test.js), [`kinetics`](test/kinetics.test.js), [`impedance`](test/impedance.test.js), [`junction`](test/junction.test.js), [`ports`](test/ports.test.js) tests | `i_lim·tanh(V/4V_T)`, Butler–Volmer closed form, finite-length Warburg; junction potentials against Henderson, Planck, JPCalc and LJPcalc; Wagner–Traud |
|
|
36
36
|
| Batteries, intercalation | OCV, insertion hosts, chemical diffusion | [insertion demo](https://markblundeberg.github.io/driftlet/demos/insertion.html), [`statistics`](test/statistics.test.js) test | composition vs OCV, π²D/4L² relaxation |
|
|
@@ -55,7 +55,7 @@ import { bandDiagram } from 'driftlet/plot'; // a level diagram as an SVG str
|
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
Or straight from a CDN in a page, pinned to a version:
|
|
58
|
-
`https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
58
|
+
`https://cdn.jsdelivr.net/npm/driftlet@0.9.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
|
|
59
59
|
TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
|
|
60
60
|
[`llms.txt`](llms.txt).
|
|
61
61
|
|
|
@@ -262,6 +262,8 @@ Every physics feature is tested against analytic results (`npm test`, node's bui
|
|
|
262
262
|
| Metal regions | ohmic; Schottky face and MOS gate equal their contact forms; Ag \| AgNO₃ \| Ag with metal electrodes; bipolar electrode at V/2 | 1e-12; 1e-9; 1e-9; 1e-9 |
|
|
263
263
|
| Internal ports | transmission line σV tanh(L/λ)/λ (O(h²)); held level; MOS low-frequency C–V with a grounded channel | 1e-4; 1e-9; 2e-3 |
|
|
264
264
|
| Cross-sections (spherical, cylindrical, any A(x)) | steady diffusion to a sphere and a cylinder (exact on any grid); Cottrell with the spherical term; uptake by a sphere filling from its surface; Debye–Hückel around a charged sphere, potential and charge | 1e-10; 3e-4; 2e-4; 1e-3, 3e-4 |
|
|
265
|
+
| A capacitance through a port | a thin-film transistor against the charge-sheet model (below threshold to saturation, second order); de Levie impedance; gate impedance against dQ/dV | 5e-4; 1e-3; 1e-6 |
|
|
266
|
+
| Electrode surfaces (coverage) | Langmuir isotherm against potential; the charge to fill a surface; the active–passive curve, blocking exactly (1 − θ) | 1e-9; 1e-3; 1e-9 |
|
|
265
267
|
| Electrodes spread through a port | transmission line with the reaction's linear kinetics as the conductance (O(h²)), rates along it; Wagner–Traud mixed potential of two Butler–Volmer couples, floating | 1e-4, 2e-3; 1e-6 V |
|
|
266
268
|
| Impedance | Macdonald blocking-electrode spectrum, 100 Hz–1 GHz; finite-length Warburg (Ag \| AgNO₃ \| Ag); DC limit = differential resistance | 3e-4; 3e-5; 1e-6 |
|
|
267
269
|
| Conservation | per step, and against time-integrated contact fluxes | 1e-11 relative |
|
package/docs/device.md
CHANGED
|
@@ -504,8 +504,11 @@ a terminal like any other. Held at a voltage, it's a potentiostat. At `I: 0` (or
|
|
|
504
504
|
else to carry current), the reactions settle at the mixed potential, the corrosion potential where
|
|
505
505
|
anodic and cathodic currents cancel over the window, while each node can be a net anode or a net
|
|
506
506
|
cathode. A floating one starts there, for the start's composition. Each solution reports the
|
|
507
|
-
window's nodes and every reaction's rate at each
|
|
508
|
-
(
|
|
507
|
+
window's nodes and every reaction's rate at each: `ports[k].x[j]` (m) and `ports[k].area[j]`
|
|
508
|
+
(m²/m³) for the window's node j, and `ports[k].rates[q][j]` for reaction q there (mol/(m²·s) of
|
|
509
|
+
electrode, forward), so reaction first, then node. They're typed arrays (`Float64Array`), so
|
|
510
|
+
`Array.from` them before mapping to anything but numbers. Per volume of the window it's
|
|
511
|
+
`rates[q][j] * area[j]`, and per area of a film's floor (thickness h, `area` 1/h) the rate itself.
|
|
509
512
|
|
|
510
513
|
Validated against the transmission line (linear kinetics along a bar, test/ports.test.js) and the
|
|
511
514
|
Wagner–Traud mixed potential of two Butler–Volmer couples. In 1D, a film on a metal is a slice
|
|
@@ -515,6 +518,87 @@ the film's thickness varies, give the device its [cross-section](#geometry) too,
|
|
|
515
518
|
travelling along the film squeeze through where it thins: for a drop on a metal, seen from its
|
|
516
519
|
centre, $`A = 2\pi r\,h(r)`$ with `area` $`1/h(r)`$.
|
|
517
520
|
|
|
521
|
+
The region must be strictly neutral ($`\varepsilon = 0`$), as in porous-electrode theory: each spot's
|
|
522
|
+
double layer, the metal's countercharge, is below the grid, so the reactions can't leave the
|
|
523
|
+
solution charged.
|
|
524
|
+
|
|
525
|
+
### An electrode's surface: adsorbates and passive films
|
|
526
|
+
|
|
527
|
+
An electrode port can carry species on its surface, sharing its sites: an adsorbed intermediate,
|
|
528
|
+
or a passive film taken as a coverage. Each has a coverage $`\theta`$ at every node of the window,
|
|
529
|
+
Langmuir statistics, $`\mu = \mu^\circ + RT\ln(\theta/\theta_0)`$ with $`\theta_0 = 1 - \sum\theta`$ the
|
|
530
|
+
bare fraction, and `capacity` $`\Gamma`$, mol of sites per m² of electrode. Reactions name them like
|
|
531
|
+
species, and a reaction with `bare: true` runs only on bare metal: its rate is multiplied by
|
|
532
|
+
$`\theta_0`$, both ways, so equilibrium stays exact. Written for adsorption itself, that's Langmuir
|
|
533
|
+
kinetics (on at a rate ∝ $`\theta_0`$, off ∝ $`\theta`$).
|
|
534
|
+
|
|
535
|
+
```js nocheck
|
|
536
|
+
ports: [{
|
|
537
|
+
name: 'iron', region: 'film', terminal: 'e-', V: 0, area: 1 / 100e-6,
|
|
538
|
+
surface: { 'Fe(OH)2': { mu0: -490e3, capacity: 2e-5, theta0: 1e-6 } }, // theta0: the start
|
|
539
|
+
reactions: [
|
|
540
|
+
{ equation: 'Fe2+ + 2 e- = Fe(s)', fixed: { 'Fe(s)': 0 }, k0: 5e-6, alpha: 0.5, bare: true }, // dissolves where bare
|
|
541
|
+
{ equation: 'Fe(s) + 2 OH- = Fe(OH)2 + 2 e-', fixed: { 'Fe(s)': 0 }, k0: 1e-6, alpha: 0.5, bare: true }, // the film forms
|
|
542
|
+
{ equation: 'O2 + 2 H2O + 4 e- = 4 OH-', fixed: { H2O: -237.13e3 }, k0: 3e-9, alpha: 0.125 }, // on film or not
|
|
543
|
+
],
|
|
544
|
+
}]
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
A film forms where the potential and the pH favour it, through its own thermodynamics, and blocks
|
|
548
|
+
what needs bare metal: the dissolution current climbs with potential, peaks, and falls as the film
|
|
549
|
+
covers the metal, the active–passive curve. Each node's surface stores
|
|
550
|
+
$`\Gamma\,d\theta/dt`$ per area of electrode, so filling it passes a current, and a solution reports
|
|
551
|
+
`ports[k].coverage[name][j]`. It's a monolayer picture: a real passive oxide is nanometres thick,
|
|
552
|
+
grows, and breaks down under chloride, none of which a coverage has.
|
|
553
|
+
|
|
554
|
+
Two cautions. Keep a film's stability within reason: blocking the bare fraction to 1e-3–1e-6 is
|
|
555
|
+
already a passive metal, and far below that ($`\theta_0 \lesssim 10^{-10}`$) the coverage's η barely
|
|
556
|
+
affects anything, so Newton struggles with it. And an electrode port has no double-layer
|
|
557
|
+
capacitance (its solution is strictly neutral), so its potential follows its kinetics instantly.
|
|
558
|
+
With a passivating film, current against potential has the active–passive peak, and when the
|
|
559
|
+
last active patch covers over, the potential where no current flows can vanish from the active
|
|
560
|
+
branch and must jump to the passive one. A real electrode makes that jump quickly but
|
|
561
|
+
continuously, through its double layer; here it's a discontinuity, and a transient stops there.
|
|
562
|
+
Give the port a [capacitance](#a-capacitance-through-a-window-a-gate-along-a-channel-a-double-layer) and the jump becomes a quick, continuous swing.
|
|
563
|
+
|
|
564
|
+
Validated in test/coverage.test.js: the Langmuir isotherm against the electrode's potential (to
|
|
565
|
+
1e-9), the charge to fill the surface, $`F\,\Gamma\,\Delta\theta`$ per area of electrode, and the
|
|
566
|
+
active–passive curve with blocking exactly $`(1-\theta)`$ times Butler–Volmer.
|
|
567
|
+
|
|
568
|
+
### A capacitance through a window: a gate along a channel, a double layer
|
|
569
|
+
|
|
570
|
+
A port can hold charge across a capacitance spread through its window: per area of electrode,
|
|
571
|
+
$`\sigma = C\,(V - \mathtt{zeroCharge} - \phi)`$ on the port's side (C in F/m²), with `area` per volume
|
|
572
|
+
as for reactions, and the window holds the opposite. Each node's charge balance gains $`a\sigma`$:
|
|
573
|
+
Gauss's law averaged across the section,
|
|
574
|
+
$`-\partial_x(\varepsilon\,\partial_x\phi) = \rho + a\sigma`$, and at $`\varepsilon = 0`$ neutrality
|
|
575
|
+
$`\rho + a\sigma = 0`$, the mobile species rearranging to supply it (counter-ions in, co-ions out).
|
|
576
|
+
The port passes the charging current, $`d(\int a\sigma\,dV)/dt`$, none in a steady state. Solutions
|
|
577
|
+
report `ports[k].sigma` at each node of the window and the total `ports[k].charge`.
|
|
578
|
+
|
|
579
|
+
```js nocheck
|
|
580
|
+
// a thin-film transistor: the gate along a 10 nm channel through 1 mF/m² (35 nm of SiO₂)
|
|
581
|
+
ports: [{ name: 'gate', region: 'channel', V: 1.5, area: 1 / 10e-9, capacitance: { C: 1e-3, zeroCharge: 0.3 } }]
|
|
582
|
+
// a porous electrode's double layer, alongside its reactions
|
|
583
|
+
ports: [{ name: 'iron', region: 'film', terminal: 'e-', V: 0, area: 1e4, capacitance: { C: 0.2, zeroCharge: -0.4 }, reactions: [/* … */] }]
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
That's the gradual-channel approximation of a field-effect transistor, exact for a thin film
|
|
587
|
+
with no body (a TFT, an organic or oxide transistor) while the channel is long against the gate
|
|
588
|
+
dielectric, and failing where it pinches off (where the 2D field takes over). It's also the
|
|
589
|
+
Bernards model of an organic electrochemical transistor (a volumetric capacitance), cable theory
|
|
590
|
+
for an axon's membrane, and a porous electrode's double layer, which lets an electrode port's
|
|
591
|
+
potential move continuously: without it, a passivating film that covers the last active patch
|
|
592
|
+
forces a jump (see above). The gate's flat level pins the channel's standard level through C, as
|
|
593
|
+
a blocked spectator's flat level does in an electrolyte, and the current saturates as the
|
|
594
|
+
carriers run out at the drain, in both.
|
|
595
|
+
|
|
596
|
+
Validated in test/capacitance.test.js: a thin-film transistor's current against the
|
|
597
|
+
charge-sheet model, $`I/W = (\mu/L)[(\sigma_s^2 - \sigma_d^2)/(2C) + V_T(\sigma_s - \sigma_d)]`$, from
|
|
598
|
+
below threshold through saturation (second order in the grid); a porous electrode's de Levie
|
|
599
|
+
impedance $`\sqrt{r/y}\coth(L\sqrt{ry})`$, the double layer in series with the ions' chemical
|
|
600
|
+
capacitance; and the gate's low-frequency impedance against $`dQ/dV_G`$ from steady states.
|
|
601
|
+
|
|
518
602
|
## Terminals
|
|
519
603
|
|
|
520
604
|
The two contacts and every port are the device's terminals, named `left`, `right` and by each
|
package/docs/kit.md
CHANGED
|
@@ -64,7 +64,7 @@ console.log(`I = ${sol.current.toFixed(1)} A/m²`);
|
|
|
64
64
|
(`combine()` does the merging on its own). A species or material given twice must be given
|
|
65
65
|
identically.
|
|
66
66
|
- Nested lists in the stack are flattened, so a helper can return several items.
|
|
67
|
-
- Reactions pass through as written (see below)
|
|
67
|
+
- Reactions pass through as written (see below), and so do `T`, `grid`, `ports` and `geometry`.
|
|
68
68
|
|
|
69
69
|
## Reactions and half-reactions
|
|
70
70
|
|
|
@@ -123,7 +123,7 @@ console.log(curve.reactions[0].level, curve.current[40]); // the couple's level
|
|
|
123
123
|
solution beside it held as `sol` has it and the metal's level set to each `V` (an electron
|
|
124
124
|
voltage, the terminals' convention). It works at a face (`{ face: f }`, the face's metal side) or
|
|
125
125
|
at a spot of an [electrode port](device.md#electrodes-spread-through-a-window)
|
|
126
|
-
(`{ port: name, x }
|
|
126
|
+
(`{ port: name, x }`, with its surface's coverages there held too). Each reaction comes back with its rate (mol/(m²·s), forward), its partial
|
|
127
127
|
current (A/m², anodic positive: into the solution) and its `level`, where the rate vanishes (the
|
|
128
128
|
couple's redox level there, as `level()` gives it). The net current is their sum. Plotting
|
|
129
129
|
log|current| against V gives the Evans diagram. The net crosses zero at the spot's open-circuit
|
package/llms.txt
CHANGED
|
@@ -14,9 +14,9 @@ docs/conventions.md (signs and levels) and docs/data.md (the data library).
|
|
|
14
14
|
From a CDN, pin the version:
|
|
15
15
|
|
|
16
16
|
```js nocheck
|
|
17
|
-
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
18
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
19
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
17
|
+
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.9.0/src/index.js';
|
|
18
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.9.0/src/kit.js';
|
|
19
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.9.0/src/plot.js';
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
@@ -319,9 +319,9 @@ solve), as long as it settles rather than ping-pongs.
|
|
|
319
319
|
<span id="readout"></span>
|
|
320
320
|
<div id="figure"></div>
|
|
321
321
|
<script type="module">
|
|
322
|
-
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
323
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
324
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.
|
|
322
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.9.0/src/index.js';
|
|
323
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.9.0/src/kit.js';
|
|
324
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.9.0/src/plot.js';
|
|
325
325
|
|
|
326
326
|
const def = build({
|
|
327
327
|
T: 300,
|
package/package.json
CHANGED
package/src/describe.js
CHANGED
|
@@ -216,7 +216,9 @@ export function describe(def) {
|
|
|
216
216
|
lines.push(` port ${port.name}: ${drive(p)}, in ${regions[port.region].name}`);
|
|
217
217
|
// an electrode spread through the window
|
|
218
218
|
if (port.reactions.length) lines.push(` an electrode (its ${species[port.terminal].name} at the port's level), ${typeof p.area === 'number' ? `${p.area} m²` : 'a profile of m²'} of it per m³, reacting:`);
|
|
219
|
-
for (const rx of p.reactions ?? []) lines.push(` ${rx.equation}: k0 ${rx.k0} mol/(m²·s), α ${rx.alpha}`);
|
|
219
|
+
for (const rx of p.reactions ?? []) lines.push(` ${rx.equation}: k0 ${rx.k0} mol/(m²·s), α ${rx.alpha}${rx.bare ? ', on bare metal' : ''}`);
|
|
220
|
+
if (port.capacitance) lines.push(` a capacitance of ${num(port.capacitance.C)} F/m² through the window (zero charge at ${num(port.capacitance.zeroCharge)} V), ${typeof p.area === 'number' ? `${p.area} m²` : 'a profile of m²'} per m³`);
|
|
221
|
+
if (port.surface.length) lines.push(` its surface: ${port.surface.map((sp) => sp.name).join(', ')} on ${num(port.surface[0].capacity)} mol/m² of sites (Langmuir)`);
|
|
220
222
|
});
|
|
221
223
|
if ((def.bulkReactions ?? []).length) {
|
|
222
224
|
lines.push('bulk reactions:');
|
package/src/device.js
CHANGED
|
@@ -351,6 +351,15 @@ export function normalizeDevice(def) {
|
|
|
351
351
|
need(nodes.length > 0, `ports[${k}]: the window [${port.from}, ${port.to}] m holds no grid node; widen it or refine the grid`);
|
|
352
352
|
port.nodes = Int32Array.from(nodes);
|
|
353
353
|
});
|
|
354
|
+
// A node's surface belongs to one electrode.
|
|
355
|
+
const surfaced = new Int32Array(grid.nNodes).fill(-1);
|
|
356
|
+
ports.forEach((port, k) => {
|
|
357
|
+
if (port.surface.length === 0) return;
|
|
358
|
+
for (const g of port.nodes) {
|
|
359
|
+
need(surfaced[g] < 0, `ports[${k}]: its window overlaps ports[${surfaced[g]}]'s, and both have a surface; a spot of metal has one surface`);
|
|
360
|
+
surfaced[g] = k;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
354
363
|
|
|
355
364
|
return {
|
|
356
365
|
T, RT, F: FARADAY, species, speciesIndex, materials, materialIndex, regions, interfaces, reactions, contacts, terminals, grid, warnings, ports, geometry,
|
|
@@ -649,7 +658,7 @@ function normalizeGeometry(g) {
|
|
|
649
658
|
// the window, `area` (m²/m³) of it per volume.
|
|
650
659
|
function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT) {
|
|
651
660
|
need(isObject(pdef), `${path} must be an object`);
|
|
652
|
-
fields(pdef, path, ['name', 'region', 'from', 'to', 'V', 'I', 'R', 'terminal', 'species', 'reactions', 'area']);
|
|
661
|
+
fields(pdef, path, ['name', 'region', 'from', 'to', 'V', 'I', 'R', 'terminal', 'species', 'reactions', 'area', 'surface', 'capacitance']);
|
|
653
662
|
let r;
|
|
654
663
|
if (Number.isInteger(pdef.region)) r = pdef.region;
|
|
655
664
|
else r = regions.findIndex((reg) => reg.name === pdef.region);
|
|
@@ -666,8 +675,11 @@ function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT
|
|
|
666
675
|
need(speciesIndex.has(pdef.terminal), `${path}.terminal: unknown species ${JSON.stringify(pdef.terminal)}${known(speciesIndex)}`);
|
|
667
676
|
terminal = speciesIndex.get(pdef.terminal);
|
|
668
677
|
}
|
|
669
|
-
const
|
|
670
|
-
|
|
678
|
+
const surface = portSurface(pdef, path, speciesIndex);
|
|
679
|
+
const reactions = portReactions(pdef, path, mat, terminal, species, speciesIndex, RT, surface);
|
|
680
|
+
surface.forEach((sp, s) => need(reactions.some((rx) => rx.part.some((p) => p.side === 2 && p.s === s)), `${path}.surface.${sp.name}: no reaction of the port makes or uses it`));
|
|
681
|
+
const capacitance = portCapacitance(pdef, path, mat);
|
|
682
|
+
need(pdef.species === undefined ? reactions.length > 0 || capacitance !== null : isObject(pdef.species), `${path}.species must map species names to port links`);
|
|
671
683
|
const links = species.map(() => ({ type: 'blocked' }));
|
|
672
684
|
for (const [sname, raw] of Object.entries(pdef.species ?? {})) {
|
|
673
685
|
const lpath = `${path}.species.${sname}`;
|
|
@@ -697,48 +709,94 @@ function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT
|
|
|
697
709
|
if (link.type === 'exchange') level.k = positive(link.k, `${lpath}.k (mol/(m³·s))`);
|
|
698
710
|
links[i] = { type: link.type, ...level };
|
|
699
711
|
}
|
|
700
|
-
need(links.some((l) => l.type !== 'blocked') || reactions.length > 0, `${path}.species: the port exchanges no species`);
|
|
712
|
+
need(links.some((l) => l.type !== 'blocked') || reactions.length > 0 || capacitance !== null, `${path}.species: the port exchanges no species`);
|
|
701
713
|
// One that exchanges only neutral species (an O₂ supply) carries no current, so its voltage is
|
|
702
714
|
// nobody's business: it can't be driven by one.
|
|
703
|
-
const passes = reactions.length > 0 || links.some((l, i) => l.type !== 'blocked' && species[i].z !== 0);
|
|
715
|
+
const passes = reactions.length > 0 || capacitance !== null || links.some((l, i) => l.type !== 'blocked' && species[i].z !== 0);
|
|
704
716
|
need(passes || drive.kind === 'V', `${path}.I: this port passes no current (it exchanges only neutral species), so give it no drive`);
|
|
705
717
|
let area = null;
|
|
706
|
-
if (reactions.length > 0) {
|
|
718
|
+
if (reactions.length > 0 || capacitance !== null) {
|
|
707
719
|
need(pdef.area !== undefined, `${path}.area: give the electrode's area per volume of the window (m²/m³, e.g. 1/h for a film of thickness h on it), a number or a profile`);
|
|
708
720
|
area = typeof pdef.area === 'number' ? { value: positive(pdef.area, `${path}.area (m²/m³)`) } : profile(pdef.area, `${path}.area`, 'areas per volume (m²/m³)', (a) => a >= 0, 'an area per volume ≥ 0');
|
|
709
|
-
} else need(pdef.area === undefined, `${path}.area: only a port with reactions has an
|
|
710
|
-
|
|
721
|
+
} else need(pdef.area === undefined, `${path}.area: only an electrode (a port with reactions or a capacitance) has an area`);
|
|
722
|
+
need(surface.length === 0 || reactions.length > 0, `${path}.surface: only an electrode (a port with reactions) has a surface`);
|
|
723
|
+
return { name: pdef.name ?? path, region: r, from, to, span: reg.length, drive, terminal, species: links, passes, reactions, area, surface, capacitance };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// A capacitance spread through the window: a gate along a channel, an electrode's double layer.
|
|
727
|
+
// Per area of electrode it holds σ = C (V − zeroCharge − φ) on the port's side, and the window
|
|
728
|
+
// the opposite: the region's charge balance (Gauss's law, or neutrality at ε = 0) gains aσ.
|
|
729
|
+
function portCapacitance(pdef, path, mat) {
|
|
730
|
+
if (pdef.capacitance === undefined) return null;
|
|
731
|
+
const cpath = `${path}.capacitance`;
|
|
732
|
+
need(isObject(pdef.capacitance), `${cpath} must be { C, zeroCharge }`);
|
|
733
|
+
fields(pdef.capacitance, cpath, ['C', 'zeroCharge']);
|
|
734
|
+
need(!mat.conductor && !mat.phiFree, `${cpath}: a capacitance couples to φ, which a ${mat.conductor ? 'conductor' : 'region with no free charge'} doesn't have`);
|
|
735
|
+
return { C: positive(pdef.capacitance.C, `${cpath}.C (F/m² of electrode)`), zeroCharge: pdef.capacitance.zeroCharge === undefined ? 0 : finite(pdef.capacitance.zeroCharge, `${cpath}.zeroCharge (V)`) };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// An electrode port's surface species: coverages θ of its sites (Langmuir: each takes one site,
|
|
739
|
+
// μ = μ° + RT ln(θ/θ₀) with θ₀ = 1 − Σθ the bare fraction), Γ mol of sites per m² of electrode.
|
|
740
|
+
function portSurface(pdef, path, speciesIndex) {
|
|
741
|
+
if (pdef.surface === undefined) return [];
|
|
742
|
+
need(isObject(pdef.surface), `${path}.surface must map surface species to { mu0, capacity, theta0 }`);
|
|
743
|
+
const list = Object.entries(pdef.surface).map(([name, d]) => {
|
|
744
|
+
const spath = `${path}.surface.${name}`;
|
|
745
|
+
need(!speciesIndex.has(name), `${spath}: '${name}' is a species of the device; a surface species is the port's own, with its own name`);
|
|
746
|
+
need(isObject(d), `${spath} must be { mu0, capacity, theta0 }`);
|
|
747
|
+
fields(d, spath, ['mu0', 'capacity', 'theta0']);
|
|
748
|
+
const theta0 = d.theta0 === undefined ? 1e-6 : positive(d.theta0, `${spath}.theta0`);
|
|
749
|
+
return { name, mu0: finite(d.mu0, `${spath}.mu0 (J/mol)`), capacity: positive(d.capacity, `${spath}.capacity (mol of sites per m² of electrode)`), theta0 };
|
|
750
|
+
});
|
|
751
|
+
need(list.length > 0, `${path}.surface: give at least one surface species, or leave it out`);
|
|
752
|
+
const caps = new Set(list.map((sp) => sp.capacity));
|
|
753
|
+
need(caps.size === 1, `${path}.surface: the species share the electrode's sites, so give them one capacity`);
|
|
754
|
+
need(list.reduce((t, sp) => t + sp.theta0, 0) < 1, `${path}.surface: the starting coverages (theta0) must sum to less than 1`);
|
|
755
|
+
return list;
|
|
711
756
|
}
|
|
712
757
|
|
|
713
758
|
// A port's reactions: Butler–Volmer, as at a face, between species of the port's region and the
|
|
714
759
|
// port's terminal species (a metal's carrier, activity 1, at the port's level), per area of the
|
|
715
760
|
// electrode: part side 0 for the region's species, side 1 for the carrier.
|
|
716
|
-
function portReactions(pdef, path, mat, terminal, species, speciesIndex, RT) {
|
|
761
|
+
function portReactions(pdef, path, mat, terminal, species, speciesIndex, RT, surface = []) {
|
|
717
762
|
need(pdef.reactions === undefined || Array.isArray(pdef.reactions), `${path}.reactions must be an array`);
|
|
718
763
|
const list = pdef.reactions ?? [];
|
|
719
764
|
if (list.length === 0) return [];
|
|
720
765
|
need(!mat.conductor, `${path}.reactions: a port on a conductor is a wire; reactions belong on a face, or on a port in the solution`);
|
|
766
|
+
// The metal's countercharge (each spot's double layer) is below the grid, so the solution
|
|
767
|
+
// beside it must be neutral, as in porous-electrode theory; with ε > 0 the reactions would
|
|
768
|
+
// leave a net charge with nothing to balance it.
|
|
769
|
+
need(mat.epsr === 0 || mat.phiFree, `${path}.reactions: an electrode spread through a window needs its region strictly neutral (ε = 0): its double layers are below the grid`);
|
|
721
770
|
need(terminal !== null && species[terminal].z !== 0, `${path}.terminal: a port with reactions needs its electrode's carrier as its terminal species (e.g. 'e-')`);
|
|
722
771
|
need(!mat.present[terminal], `${path}.terminal: '${species[terminal].name}' is in the port's region too, so a reaction can't tell the electrode's from the region's`);
|
|
723
772
|
const carrier = species[terminal].name;
|
|
724
773
|
return list.map((rdef, k) => {
|
|
725
774
|
const rpath = `${path}.reactions[${k}]`;
|
|
726
775
|
need(isObject(rdef) && typeof rdef.equation === 'string', `${rpath} must be { equation, fixed, k0, alpha }`);
|
|
727
|
-
fields(rdef, rpath, ['equation', 'fixed', 'k0', 'alpha']);
|
|
776
|
+
fields(rdef, rpath, ['equation', 'fixed', 'k0', 'alpha', 'bare']);
|
|
728
777
|
const holds = (name) => speciesIndex.has(name) && Boolean(mat.present[speciesIndex.get(name)]);
|
|
729
|
-
const
|
|
778
|
+
const onSurface = (name) => surface.findIndex((sp) => sp.name === name);
|
|
779
|
+
const sides = faceSides(parseEquation(rdef.equation, `${rpath}.equation`), { name: mat.name, holds, conductor: false }, { name: 'the electrode', holds: (name) => name === carrier || onSurface(name) >= 0, conductor: true }, rdef.fixed, `${rpath}.equation`);
|
|
730
780
|
const part = [];
|
|
731
781
|
let fixedA = 0, charge = 0;
|
|
732
|
-
|
|
733
|
-
|
|
782
|
+
// The electrode's side: its carrier, and its surface species (side 2, neutral, by index s).
|
|
783
|
+
const electrode = {};
|
|
784
|
+
for (const [name, nu] of Object.entries(sides.right)) {
|
|
785
|
+
const s = onSurface(name);
|
|
786
|
+
if (s >= 0) part.push({ s, nu, side: 2 });
|
|
787
|
+
else electrode[name] = nu;
|
|
788
|
+
}
|
|
789
|
+
for (const [map, side] of [[sides.left, 0], [electrode, 1]]) {
|
|
790
|
+
const st = stoichiometry(map, `${rpath}.equation`, rdef.fixed, `${rpath}.fixed`, species, speciesIndex, RT);
|
|
734
791
|
for (const { i, nu } of st.list) part.push({ i, nu, side });
|
|
735
792
|
fixedA += st.fixedA;
|
|
736
793
|
charge += st.charge;
|
|
737
794
|
}
|
|
738
795
|
need(part.some((p) => p.side === 1), `${rpath}: no '${carrier}' from the electrode takes part`);
|
|
739
|
-
need(part.some((p) => p.side === 0), `${rpath}: no species of the port's region takes part`);
|
|
796
|
+
need(part.some((p) => p.side === 0 || p.side === 2), `${rpath}: no species of the port's region or its surface takes part`);
|
|
740
797
|
need(charge === 0, `${rpath}: charge is not balanced (Σ ν z = ${charge})`);
|
|
741
|
-
|
|
798
|
+
need(rdef.bare === undefined || typeof rdef.bare === 'boolean', `${rpath}.bare must be true or false`);
|
|
799
|
+
return { part, fixedA, k0: positive(rdef.k0, `${rpath}.k0 (mol/(m²·s))`), alpha: transferCoefficient(rdef.alpha, `${rpath}.alpha`), bare: rdef.bare === true };
|
|
742
800
|
});
|
|
743
801
|
}
|
|
744
802
|
|
|
@@ -762,7 +820,7 @@ function checkAnchors(regions, materials, interfaces, contacts, species, ports =
|
|
|
762
820
|
const anchored = new Set();
|
|
763
821
|
if (anchors(contacts.left)) anchored.add(find(0));
|
|
764
822
|
if (anchors(contacts.right)) anchored.add(find(nR - 1));
|
|
765
|
-
for (const port of ports) if (port.reactions.length > 0 || port.species.some((l, i) => l.type !== 'blocked' && species[i].z !== 0)) anchored.add(find(port.region));
|
|
823
|
+
for (const port of ports) if (port.reactions.length > 0 || port.capacitance || port.species.some((l, i) => l.type !== 'blocked' && species[i].z !== 0)) anchored.add(find(port.region));
|
|
766
824
|
for (let r = 0; r < nR; r++) {
|
|
767
825
|
const mat = materials[regions[r].material];
|
|
768
826
|
const charged = species.some((sp, i) => mat.present[i] && sp.z !== 0);
|
package/src/index.js
CHANGED
|
@@ -100,6 +100,7 @@ export class Device {
|
|
|
100
100
|
solver.computeConcentrations();
|
|
101
101
|
solver.time = old.time;
|
|
102
102
|
solver.contactDEnd = old.contactDEnd;
|
|
103
|
+
if (old.portQEnd?.length === solver.portQ.length) solver.portQEnd = old.portQEnd;
|
|
103
104
|
solver.solvedV = old.solvedV; // where the carried-over state was solved (for continuation)
|
|
104
105
|
// Floating terminals keep their voltages (a good start), where the terminals match.
|
|
105
106
|
if (old.terms.length === solver.terms.length) {
|
package/src/polarization.js
CHANGED
|
@@ -29,7 +29,7 @@ const fail = (message) => {
|
|
|
29
29
|
export function polarization(device, sol, where, V) {
|
|
30
30
|
const model = device.model, { species, grid, RT } = model, solver = device.solver;
|
|
31
31
|
const Vs = Float64Array.from(typeof V === 'number' ? [V] : V);
|
|
32
|
-
let reactions, nodeOf, carrier, names;
|
|
32
|
+
let reactions, nodeOf, carrier, names, surface = null; // (an electrode port's coverages at the spot)
|
|
33
33
|
if (where?.face !== undefined) {
|
|
34
34
|
const f = where.face, itf = model.interfaces[f];
|
|
35
35
|
if (!itf) fail(`polarization: no face ${f} (the device has ${model.interfaces.length})`);
|
|
@@ -49,6 +49,10 @@ export function polarization(device, sol, where, V) {
|
|
|
49
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
50
|
carrier = { side: 1, i: port.terminal };
|
|
51
51
|
nodeOf = () => g;
|
|
52
|
+
if (port.surface.length > 0) {
|
|
53
|
+
const w = port.nodes.indexOf(g), theta = port.surface.map((sp) => sol.ports[k].coverage[sp.name][w]);
|
|
54
|
+
surface = { sp: port.surface, theta, bare: sol.ports[k].bare[w] };
|
|
55
|
+
}
|
|
52
56
|
reactions = port.reactions;
|
|
53
57
|
names = device.def.ports?.[k]?.reactions;
|
|
54
58
|
} else fail('polarization: say where, { face: f } or { port: name, x }');
|
|
@@ -59,6 +63,13 @@ export function polarization(device, sol, where, V) {
|
|
|
59
63
|
// a = A/RT = a0 + s·V: everything but the carrier at its level, and the carrier's part.
|
|
60
64
|
let a0 = rx.fixedA, s = 0, q = 0, pref = rx.vmax ? rx.vmax : rx.k0;
|
|
61
65
|
rx.part.forEach((p, x) => {
|
|
66
|
+
if (p.side === 2) {
|
|
67
|
+
// a surface species, at activity θ/θ₀ (μ = μ° + RT ln(θ/θ₀))
|
|
68
|
+
const ln = Math.log(surface.theta[p.s] / surface.bare), e = p.nu < 0 ? -p.nu * (1 - rx.alpha) : p.nu * rx.alpha;
|
|
69
|
+
a0 -= (p.nu * (surface.sp[p.s].mu0 / RT + ln));
|
|
70
|
+
pref *= Math.exp(e * ln);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
62
73
|
if (p.side === carrier.side && p.i === carrier.i) {
|
|
63
74
|
s -= (p.nu * species[p.i].z * FARADAY) / RT;
|
|
64
75
|
return;
|
|
@@ -72,6 +83,7 @@ export function polarization(device, sol, where, V) {
|
|
|
72
83
|
} else pref *= powr(c / solver.cRef[g * species.length + p.i], p.nu < 0 ? -p.nu * (1 - rx.alpha) : p.nu * rx.alpha);
|
|
73
84
|
});
|
|
74
85
|
if (s === 0) fail(`polarization: reaction ${k} there takes no ${species[carrier.i].name} from the metal`);
|
|
86
|
+
if (rx.bare) pref *= surface ? surface.bare : 1; // on bare metal only
|
|
75
87
|
const rate = Float64Array.from(Vs, (v) => {
|
|
76
88
|
const a = a0 + s * v;
|
|
77
89
|
return rx.vmax ? pref * -Math.expm1(-a) : pref * (Math.exp(rx.alpha * a) - Math.exp(-(1 - rx.alpha) * a));
|
package/src/solution.js
CHANGED
|
@@ -106,12 +106,30 @@ export function makeSolution(solver, result = {}) {
|
|
|
106
106
|
flux[species[i].name] = solver.portFlux[k][i];
|
|
107
107
|
current += F * species[i].z * solver.portFlux[k][i];
|
|
108
108
|
}
|
|
109
|
+
if (port.capacitance) {
|
|
110
|
+
// a capacitance's charging current, and its charge (the port's side), like a gate's
|
|
111
|
+
const dt = solver.lastDt;
|
|
112
|
+
current += Number.isFinite(dt) ? (solver.portQ[k] - solver.portQOld[k]) / dt : 0;
|
|
113
|
+
}
|
|
109
114
|
const out = { name: port.name, V: solver.termV[2 + k], flux, current };
|
|
110
|
-
if (port.
|
|
111
|
-
|
|
115
|
+
if (port.capacitance) out.charge = solver.portQ[k];
|
|
116
|
+
if (port.capacitance) {
|
|
117
|
+
// its charge per area of electrode at each node, σ = C (V − zeroCharge − φ)
|
|
118
|
+
const { C: Cs, zeroCharge } = port.capacitance;
|
|
119
|
+
out.sigma = Float64Array.from(port.nodes, (g) => Cs * (solver.termV[2 + k] - zeroCharge - phi[g]));
|
|
120
|
+
}
|
|
121
|
+
if (port.area !== null) {
|
|
112
122
|
out.x = Float64Array.from(port.nodes, (g) => model.grid.x[g]);
|
|
113
123
|
out.area = Float64Array.from(solver.portArea[k]);
|
|
124
|
+
}
|
|
125
|
+
if (port.reactions.length > 0) {
|
|
126
|
+
// An electrode spread through the window: each reaction's rate at its nodes.
|
|
114
127
|
out.rates = solver.portRates[k].map((r) => Float64Array.from(r)); // mol/(m²·s), forward
|
|
128
|
+
// its surface: each species' coverage θ at each node of the window
|
|
129
|
+
if (port.surface.length > 0) {
|
|
130
|
+
out.coverage = Object.fromEntries(port.surface.map((sp, q) => [sp.name, Float64Array.from(port.nodes, (g) => solver.th[g * solver.nSurf + q])]));
|
|
131
|
+
out.bare = Float64Array.from(port.nodes, (g) => solver.th0[g]); // θ₀, to full precision however covered
|
|
132
|
+
}
|
|
115
133
|
}
|
|
116
134
|
return out;
|
|
117
135
|
});
|
package/src/solver.js
CHANGED
|
@@ -195,13 +195,16 @@ export class Solver {
|
|
|
195
195
|
// Slots per block: φ̂ (or D, or a conductor's segment flux J), one per species, and at a face
|
|
196
196
|
// one per face reaction (its rate), as many as the busiest face has.
|
|
197
197
|
const nRx = model.interfaces.reduce((m, itf) => Math.max(m, itf.reactions.length), 0);
|
|
198
|
-
|
|
198
|
+
// (and at an electrode port's nodes, one per surface species: its coverage's η)
|
|
199
|
+
const nSurf = model.ports.reduce((m, port) => Math.max(m, port.surface.length), 0);
|
|
200
|
+
const M = n + 1 + Math.max(nRx, nSurf);
|
|
199
201
|
const nNodes = grid.nNodes;
|
|
200
202
|
const nFaces = regions.length - 1;
|
|
201
203
|
const nB = nNodes + nFaces;
|
|
202
204
|
this.n = n;
|
|
203
205
|
this.M = M;
|
|
204
206
|
this.nRx = nRx;
|
|
207
|
+
this.nSurf = nSurf;
|
|
205
208
|
this.nNodes = nNodes;
|
|
206
209
|
this.nFaces = nFaces;
|
|
207
210
|
this.nB = nB;
|
|
@@ -214,6 +217,18 @@ export class Solver {
|
|
|
214
217
|
this.blockOfFace = new Int32Array(nFaces);
|
|
215
218
|
for (let f = 0; f < nFaces; f++) this.blockOfFace[f] = grid.regionEnd[f] + f + 1;
|
|
216
219
|
|
|
220
|
+
// Electrode ports' surfaces: each node's port (−1: none) and its index in the port's window;
|
|
221
|
+
// the coverages there, θ [g·nSurf + s], now and at the step's start.
|
|
222
|
+
this.surfPort = new Int32Array(nNodes).fill(-1);
|
|
223
|
+
this.surfW = new Int32Array(nNodes);
|
|
224
|
+
model.ports.forEach((port, k) => {
|
|
225
|
+
if (port.surface.length > 0) port.nodes.forEach((g, w) => ((this.surfPort[g] = k), (this.surfW[g] = w)));
|
|
226
|
+
});
|
|
227
|
+
this.th = new Float64Array(nNodes * nSurf);
|
|
228
|
+
this.thOld = new Float64Array(nNodes * nSurf);
|
|
229
|
+
this.th0 = new Float64Array(nNodes); // the bare fraction θ₀, directly (1 − Σθ cancels as θ → 1)
|
|
230
|
+
this.th0Old = new Float64Array(nNodes);
|
|
231
|
+
|
|
217
232
|
// Per-node material data, flattened [g·n + i].
|
|
218
233
|
this.present = new Uint8Array(nNodes * n);
|
|
219
234
|
this.cRef = new Float64Array(nNodes * n);
|
|
@@ -327,6 +342,12 @@ export class Solver {
|
|
|
327
342
|
this.contactFlux = { left: new Float64Array(n), right: new Float64Array(n) };
|
|
328
343
|
this.portFlux = model.ports.map(() => new Float64Array(n)); // into the device, mol/(m²·s)
|
|
329
344
|
this.contactD = { left: 0, right: 0 };
|
|
345
|
+
// A capacitive port's charge (its side's, Σ vol·a·σ over its window), as the contacts' D: now,
|
|
346
|
+
// at the step's start and at the previous step's end.
|
|
347
|
+
this.portQ = new Float64Array(model.ports.length);
|
|
348
|
+
this.portQStart = new Float64Array(model.ports.length);
|
|
349
|
+
this.portQOld = new Float64Array(model.ports.length);
|
|
350
|
+
this.portQEnd = null;
|
|
330
351
|
this.contactDOld = { left: 0, right: 0 };
|
|
331
352
|
this.contactDStart = { left: 0, right: 0 };
|
|
332
353
|
// Terminals (the two contacts, then the ports): each one's voltage (V), held by its source or
|
|
@@ -453,6 +474,8 @@ export class Solver {
|
|
|
453
474
|
}
|
|
454
475
|
active[b * M] = this.phiUndefined[g] ? 0 : 1;
|
|
455
476
|
for (let i = 0; i < n; i++) active[b * M + 1 + i] = this.present[g * n + i];
|
|
477
|
+
const k = this.surfPort[g];
|
|
478
|
+
if (k >= 0) for (let q = 0; q < model.ports[k].surface.length; q++) active[b * M + 1 + n + q] = 1;
|
|
456
479
|
}
|
|
457
480
|
model.interfaces.forEach((itf, f) => {
|
|
458
481
|
const bf = this.blockOfFace[f];
|
|
@@ -1055,6 +1078,13 @@ export class Solver {
|
|
|
1055
1078
|
const shift = level(t.side) - level(this.terms[held].side);
|
|
1056
1079
|
if (Number.isFinite(shift)) this.termV[k] = this.termV[held] + shift;
|
|
1057
1080
|
}
|
|
1081
|
+
// Electrode surfaces at their starting coverages: η = μ°/RT + ln(θ/θ₀).
|
|
1082
|
+
for (let g = 0; g < this.nNodes; g++) {
|
|
1083
|
+
const k = this.surfPort[g];
|
|
1084
|
+
if (k < 0) continue;
|
|
1085
|
+
const surf = model.ports[k].surface, b = this.blockOfNode[g], bare = 1 - surf.reduce((t, sp) => t + sp.theta0, 0);
|
|
1086
|
+
surf.forEach((sp, q) => (u[b * M + 1 + n + q] = sp.mu0 / model.RT + Math.log(sp.theta0 / bare)));
|
|
1087
|
+
}
|
|
1058
1088
|
this.computeConcentrations();
|
|
1059
1089
|
// A floating electrode spread through a port starts where its reactions pass the current it's
|
|
1060
1090
|
// set (none, behind a resistance): at its mixed potential in the start's composition, not
|
|
@@ -1088,16 +1118,18 @@ export class Solver {
|
|
|
1088
1118
|
|
|
1089
1119
|
// A port reaction's rate per area at node g (mol/(m²·s), forward), at the present state.
|
|
1090
1120
|
_portRate(port, rx, g) {
|
|
1091
|
-
const { n, M, u, uLo, c } = this, b = this.blockOfNode[g];
|
|
1121
|
+
const { n, M, u, uLo, c, nSurf, th } = this, b = this.blockOfNode[g], RT = this.model.RT;
|
|
1092
1122
|
let pref = rx.k0, a = rx.fixedA;
|
|
1093
1123
|
for (const p of rx.part) {
|
|
1094
1124
|
if (p.side === 1) {
|
|
1095
1125
|
a -= p.nu * this.portEta(port, p.i);
|
|
1096
1126
|
continue;
|
|
1097
1127
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1128
|
+
const o = b * M + (p.side === 2 ? 1 + n + p.s : 1 + p.i), e = p.nu < 0 ? -p.nu * (1 - rx.alpha) : p.nu * rx.alpha;
|
|
1129
|
+
a -= p.nu * (u[o] + uLo[o]);
|
|
1130
|
+
pref *= p.side === 2 ? Math.exp(e * (u[o] + uLo[o] - port.surface[p.s].mu0 / RT)) : powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
1100
1131
|
}
|
|
1132
|
+
if (rx.bare) pref *= this.th0[g];
|
|
1101
1133
|
// (far from equilibrium, the plain difference: bvFactor's product would give 0·∞ there)
|
|
1102
1134
|
return pref * (Math.abs(a) > 50 ? Math.exp(rx.alpha * a) - Math.exp(-(1 - rx.alpha) * a) : bvFactor(a, rx.alpha).g);
|
|
1103
1135
|
}
|
|
@@ -1128,6 +1160,23 @@ export class Solver {
|
|
|
1128
1160
|
}
|
|
1129
1161
|
}
|
|
1130
1162
|
|
|
1163
|
+
// Each electrode surface's coverages from its η: Langmuir on shared sites, θ_s = e^{ζ_s}/(1 + Σ e^{ζ}),
|
|
1164
|
+
// ζ = η − μ°/RT (so θ_s/θ₀ = e^{ζ_s}), at every node of a surfaced window.
|
|
1165
|
+
_coverages() {
|
|
1166
|
+
const { n, M, u, uLo, nSurf, th } = this, RT = this.model.RT;
|
|
1167
|
+
for (let g = 0; g < this.nNodes; g++) {
|
|
1168
|
+
const k = this.surfPort[g];
|
|
1169
|
+
if (k < 0) continue;
|
|
1170
|
+
const surf = this.model.ports[k].surface, b = this.blockOfNode[g];
|
|
1171
|
+
let m = 0;
|
|
1172
|
+
for (let q = 0; q < surf.length; q++) m = Math.max(m, u[b * M + 1 + n + q] + uLo[b * M + 1 + n + q] - surf[q].mu0 / RT);
|
|
1173
|
+
let sum = Math.exp(-m);
|
|
1174
|
+
for (let q = 0; q < surf.length; q++) sum += (th[g * nSurf + q] = Math.exp(u[b * M + 1 + n + q] + uLo[b * M + 1 + n + q] - surf[q].mu0 / RT - m));
|
|
1175
|
+
for (let q = 0; q < surf.length; q++) th[g * nSurf + q] /= sum;
|
|
1176
|
+
this.th0[g] = Math.exp(-m) / sum;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1131
1180
|
computeConcentrations() {
|
|
1132
1181
|
const { n, M, u, uLo, c, z } = this;
|
|
1133
1182
|
for (let g = 0; g < this.nNodes; g++) {
|
|
@@ -1150,6 +1199,7 @@ export class Solver {
|
|
|
1150
1199
|
c[k] = this.present[k] ? this.cRef[k] * Math.exp(u[b * M + 1 + i] + uLo[b * M + 1 + i] - this.mu0hat[k] - z[i] * phiHat) : 0;
|
|
1151
1200
|
}
|
|
1152
1201
|
}
|
|
1202
|
+
this._coverages();
|
|
1153
1203
|
}
|
|
1154
1204
|
|
|
1155
1205
|
_work(md) {
|
|
@@ -1969,7 +2019,7 @@ export class Solver {
|
|
|
1969
2019
|
this.termDI[k] = 0;
|
|
1970
2020
|
this.termI[k] = 0;
|
|
1971
2021
|
}
|
|
1972
|
-
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k));
|
|
2022
|
+
this.model.ports.forEach((port, k) => this._port(port, this.portFlux[k], 2 + k, dt));
|
|
1973
2023
|
this._contact('left', dt);
|
|
1974
2024
|
this._contact('right', dt);
|
|
1975
2025
|
for (const k of this.floating) this._circuit(k);
|
|
@@ -2002,7 +2052,7 @@ export class Solver {
|
|
|
2002
2052
|
// An internal port (terminal k): its exchange with each node of its window, as a source per
|
|
2003
2053
|
// volume. A held ('equilibrium') level replaces the node's balance row; the source is then that
|
|
2004
2054
|
// row's residual, read just before. Its current into the device is Σ z F × the sources.
|
|
2005
|
-
_port(port, flux, k) {
|
|
2055
|
+
_port(port, flux, k, dt) {
|
|
2006
2056
|
const R = this.rix;
|
|
2007
2057
|
const { n, M, u, uLo, res, z, VT } = this;
|
|
2008
2058
|
const F = FARADAY, vol = this.model.grid.vol;
|
|
@@ -2038,6 +2088,8 @@ export class Solver {
|
|
|
2038
2088
|
}
|
|
2039
2089
|
}
|
|
2040
2090
|
port.reactions.forEach((rx, x) => this._portReaction(port, rx, this.portArea[k - 2], this.portRates[k - 2][x], flux, k));
|
|
2091
|
+
if (port.capacitance) this._portCapacitance(port, k, dt);
|
|
2092
|
+
if (port.surface.length > 0 && Number.isFinite(dt)) this._surfaceStorage(port, dt);
|
|
2041
2093
|
for (let i = 0; i < n; i++) this.termI[k] += z[i] * F * flux[i];
|
|
2042
2094
|
}
|
|
2043
2095
|
|
|
@@ -2046,49 +2098,111 @@ export class Solver {
|
|
|
2046
2098
|
// volume. Each node's rate is explicit (no unknown of its own): it enters the balances of the
|
|
2047
2099
|
// species it makes and consumes, and the port's current.
|
|
2048
2100
|
_portReaction(port, rx, area, rates, flux, k) {
|
|
2049
|
-
const { n, M, u, uLo, c, res, z, VT } = this, R = this.rix, F = FARADAY, vol = this.model.grid.vol;
|
|
2050
|
-
const B = this.termB[k], C = this.termC[k], al = rx.alpha, d = this.dA;
|
|
2101
|
+
const { n, M, u, uLo, c, res, z, VT, nSurf, th } = this, R = this.rix, F = FARADAY, vol = this.model.grid.vol, RT = this.model.RT;
|
|
2102
|
+
const B = this.termB[k], C = this.termC[k], al = rx.alpha, d = this.dA, surf = port.surface;
|
|
2051
2103
|
// The carrier's part of the affinity, and how it moves with the port's voltage.
|
|
2052
2104
|
let aV = rx.fixedA, daV = 0, q = 0; // q: charge the forward reaction brings into the device, per event
|
|
2053
2105
|
for (const p of rx.part) {
|
|
2054
2106
|
if (p.side === 1) {
|
|
2055
2107
|
aV -= p.nu * this.portEta(port, p.i);
|
|
2056
2108
|
daV -= (p.nu * z[p.i]) / VT;
|
|
2057
|
-
} else q += p.nu * z[p.i];
|
|
2109
|
+
} else if (p.side === 0) q += p.nu * z[p.i];
|
|
2058
2110
|
}
|
|
2111
|
+
const slot = (p) => (p.side === 2 ? 1 + n + p.s : 1 + p.i);
|
|
2059
2112
|
port.nodes.forEach((g, w) => {
|
|
2060
2113
|
const b = this.blockOfNode[g], s = vol[g] * area[w];
|
|
2061
2114
|
d.fill(0);
|
|
2062
2115
|
let pref = rx.k0, aHi = aV, aLo = 0;
|
|
2063
2116
|
for (const p of rx.part) {
|
|
2064
2117
|
if (p.side === 1) continue;
|
|
2065
|
-
const o = b * M +
|
|
2118
|
+
const o = b * M + slot(p);
|
|
2066
2119
|
aHi -= p.nu * u[o];
|
|
2067
2120
|
aLo -= p.nu * uLo[o];
|
|
2068
2121
|
const e = p.nu < 0 ? -p.nu * (1 - al) : p.nu * al;
|
|
2069
|
-
|
|
2070
|
-
|
|
2122
|
+
if (p.side === 2) {
|
|
2123
|
+
// a surface species at activity θ/θ₀ = e^ζ
|
|
2124
|
+
pref *= Math.exp(e * (u[o] + uLo[o] - surf[p.s].mu0 / RT));
|
|
2125
|
+
d[1 + n + p.s] += e;
|
|
2126
|
+
} else {
|
|
2127
|
+
pref *= powr(c[g * n + p.i] / this.cRef[g * n + p.i], e);
|
|
2128
|
+
this._dlnc(g, p.i, e, d);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
if (rx.bare) {
|
|
2132
|
+
// on bare metal only: the free fraction θ₀ = 1 − Σθ, with ∂ln θ₀/∂η_s = −θ_s
|
|
2133
|
+
for (let x = 0; x < surf.length; x++) d[1 + n + x] -= th[g * nSurf + x];
|
|
2134
|
+
pref *= this.th0[g];
|
|
2071
2135
|
}
|
|
2072
2136
|
const { g: bv, gp } = bvFactor(aHi + aLo, al);
|
|
2073
2137
|
const rate = pref * bv;
|
|
2074
2138
|
rates[w] = rate;
|
|
2075
|
-
// ∂rate/∂slot = rate·∂ln(prefactor) + pref·g′·∂a, with ∂a/∂η
|
|
2139
|
+
// ∂rate/∂slot = rate·∂ln(prefactor) + pref·g′·∂a, with ∂a/∂η = −ν
|
|
2076
2140
|
for (let t = 0; t < M; t++) d[t] *= rate;
|
|
2077
|
-
for (const p of rx.part) if (p.side
|
|
2141
|
+
for (const p of rx.part) if (p.side !== 1) d[slot(p)] -= pref * gp * p.nu;
|
|
2078
2142
|
const dV = pref * gp * daV;
|
|
2079
2143
|
for (const p of rx.part) {
|
|
2080
2144
|
if (p.side === 1) continue;
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2145
|
+
// the region's species per volume of the window; a surface's per area of electrode
|
|
2146
|
+
const row = slot(p), o = b * M + row, f = p.side === 2 ? 1 : s;
|
|
2147
|
+
res[R[o]] -= f * p.nu * rate;
|
|
2148
|
+
if (p.side === 0) flux[p.i] += s * p.nu * rate;
|
|
2149
|
+
for (let t = 0; t < M; t++) if (d[t] !== 0) this._j(b, row, b, t, -f * p.nu * d[t]);
|
|
2150
|
+
B[R[o]] -= f * p.nu * dV;
|
|
2086
2151
|
}
|
|
2087
2152
|
for (let t = 0; t < M; t++) if (d[t] !== 0) C[R[b * M + t]] += q * F * s * d[t];
|
|
2088
2153
|
this.termDI[k] += q * F * s * dV;
|
|
2089
2154
|
});
|
|
2090
2155
|
}
|
|
2091
2156
|
|
|
2157
|
+
// A capacitance spread through a port's window: per area of electrode, σ = C (V − zeroCharge − φ)
|
|
2158
|
+
// on the port's side, so each node's charge balance (its φ row) gains vol·a·σ, and the port
|
|
2159
|
+
// passes the charging current d(Σ vol·a·σ)/dt (none in a steady state). An end node whose φ its
|
|
2160
|
+
// contact sets is left to the contact.
|
|
2161
|
+
_portCapacitance(port, k, dt) {
|
|
2162
|
+
const { M, u, uLo, res, VT } = this, R = this.rix, vol = this.model.grid.vol, area = this.portArea[k - 2];
|
|
2163
|
+
const { C: Cs, zeroCharge } = port.capacitance, V = this.termV[k], B = this.termB[k], Ct = this.termC[k];
|
|
2164
|
+
const sets = (side) => ['pinned', 'bulk'].includes(this.model.contacts[side].phi.type);
|
|
2165
|
+
let Q = 0, dQdV = 0;
|
|
2166
|
+
port.nodes.forEach((g, w) => {
|
|
2167
|
+
if ((g === 0 && sets('left')) || (g === this.nNodes - 1 && sets('right')) || this.phiUndefined[g]) return;
|
|
2168
|
+
const b = this.blockOfNode[g], s = vol[g] * area[w] * Cs;
|
|
2169
|
+
const q = s * (V - zeroCharge - VT * (u[b * M] + uLo[b * M]));
|
|
2170
|
+
Q += q;
|
|
2171
|
+
dQdV += s;
|
|
2172
|
+
res[R[b * M]] -= q;
|
|
2173
|
+
this._j(b, 0, b, 0, s * VT);
|
|
2174
|
+
B[R[b * M]] -= s;
|
|
2175
|
+
if (Number.isFinite(dt)) Ct[R[b * M]] += (-s * VT) / dt;
|
|
2176
|
+
});
|
|
2177
|
+
this.portQ[k - 2] = Q;
|
|
2178
|
+
if (Number.isFinite(dt)) {
|
|
2179
|
+
this.termI[k] += (Q - this.portQStart[k - 2]) / dt;
|
|
2180
|
+
this.termDI[k] += dQdV / dt;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// An electrode surface's storage, per area of electrode: Γ (θ_s − θ_s,old)/dt in each surface
|
|
2185
|
+
// species' row, with ∂θ_s/∂η_x = θ_s (δ_sx − θ_x).
|
|
2186
|
+
_surfaceStorage(port, dt) {
|
|
2187
|
+
const { n, M, res, nSurf, th, thOld } = this, R = this.rix, surf = port.surface, G = surf[0].capacity;
|
|
2188
|
+
for (const g of port.nodes) {
|
|
2189
|
+
const b = this.blockOfNode[g];
|
|
2190
|
+
// Near full coverage θ − θ_old cancels (both ~1, the change ~θ₀): the dominant species'
|
|
2191
|
+
// change comes from the small ones instead, Δθ = −Δθ₀ − Σ_others Δθ.
|
|
2192
|
+
let top = 0;
|
|
2193
|
+
for (let q = 1; q < surf.length; q++) if (th[g * nSurf + q] > th[g * nSurf + top]) top = q;
|
|
2194
|
+
for (let q = 0; q < surf.length; q++) {
|
|
2195
|
+
const tq = th[g * nSurf + q];
|
|
2196
|
+
let change = tq - thOld[g * nSurf + q];
|
|
2197
|
+
if (q === top && tq > 0.5) {
|
|
2198
|
+
change = -(this.th0[g] - this.th0Old[g]);
|
|
2199
|
+
for (let x = 0; x < surf.length; x++) if (x !== q) change -= th[g * nSurf + x] - thOld[g * nSurf + x];
|
|
2200
|
+
}
|
|
2201
|
+
res[R[b * M + 1 + n + q]] += (G * change) / dt;
|
|
2202
|
+
for (let x = 0; x < surf.length; x++) this._j(b, 1 + n + q, b, 1 + n + x, ((G * tq) / dt) * ((q === x ? 1 : 0) - th[g * nSurf + x]));
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2092
2206
|
|
|
2093
2207
|
// One contact (terminal 0 or 1): record the flux through its outer face and the current into
|
|
2094
2208
|
// the device (read from the end box before anything here is added: whatever the box needs
|
|
@@ -2245,6 +2359,8 @@ export class Solver {
|
|
|
2245
2359
|
for (let i = 0; i < n; i++) {
|
|
2246
2360
|
if (this.present[g * n + i]) mx = Math.max(mx, Math.abs(delta[R[b * M + 1 + i]]));
|
|
2247
2361
|
}
|
|
2362
|
+
const k = this.surfPort[g]; // an electrode surface's coverages, as η
|
|
2363
|
+
if (k >= 0) for (let q = 0; q < this.model.ports[k].surface.length; q++) mx = Math.max(mx, Math.abs(delta[R[b * M + 1 + n + q]]));
|
|
2248
2364
|
}
|
|
2249
2365
|
return mx;
|
|
2250
2366
|
}
|
|
@@ -2401,14 +2517,15 @@ export class Solver {
|
|
|
2401
2517
|
this.uPrevLo.set(this.uLo);
|
|
2402
2518
|
this.termVPrev.set(this.termV);
|
|
2403
2519
|
this.computeConcentrations();
|
|
2404
|
-
const cN = Float64Array.from(this.c);
|
|
2520
|
+
const cN = Float64Array.from(this.c), thN = Float64Array.from(this.th), th0N = Float64Array.from(this.th0);
|
|
2405
2521
|
// Contact displacement before the step: as it was at the end of the previous step (under
|
|
2406
2522
|
// the parameters then), so a gate-voltage change shows up as displacement current.
|
|
2407
|
-
if (!this.contactDEnd) {
|
|
2523
|
+
if (!this.contactDEnd || !this.portQEnd) {
|
|
2408
2524
|
this._assembleBookkeeping(dt);
|
|
2409
2525
|
this.contactDEnd = { ...this.contactD };
|
|
2526
|
+
this.portQEnd = Float64Array.from(this.portQ);
|
|
2410
2527
|
}
|
|
2411
|
-
const DN = { ...this.contactDEnd };
|
|
2528
|
+
const DN = { ...this.contactDEnd }, QN = Float64Array.from(this.portQEnd);
|
|
2412
2529
|
if (opts.guess) {
|
|
2413
2530
|
// Start Newton from a predicted state (e.g. extrapolated from the history).
|
|
2414
2531
|
this.u.set(opts.guess);
|
|
@@ -2419,11 +2536,17 @@ export class Solver {
|
|
|
2419
2536
|
const a0 = (1 + 2 * w) / (1 + w), b1 = (1 + w) / a0, b2 = (w * w) / (1 + w) / a0;
|
|
2420
2537
|
const { cOld } = this;
|
|
2421
2538
|
for (let k = 0; k < cOld.length; k++) cOld[k] = b1 * cN[k] - b2 * prev.c[k];
|
|
2539
|
+
for (let k = 0; k < thN.length; k++) this.thOld[k] = b1 * thN[k] - b2 * prev.th[k];
|
|
2540
|
+
for (let k = 0; k < th0N.length; k++) this.th0Old[k] = b1 * th0N[k] - b2 * prev.th0[k];
|
|
2422
2541
|
this.contactDStart = { left: b1 * DN.left - b2 * prev.D.left, right: b1 * DN.right - b2 * prev.D.right };
|
|
2542
|
+
this.portQStart = QN.map((q, k) => b1 * q - b2 * prev.Q[k]);
|
|
2423
2543
|
dtEff = dt / a0;
|
|
2424
2544
|
} else {
|
|
2425
2545
|
this.cOld.set(cN);
|
|
2546
|
+
this.thOld.set(thN);
|
|
2547
|
+
this.th0Old.set(th0N);
|
|
2426
2548
|
this.contactDStart = DN;
|
|
2549
|
+
this.portQStart = QN;
|
|
2427
2550
|
}
|
|
2428
2551
|
// Implicit: sources at the step's end, as seen from within the step (before any jump there).
|
|
2429
2552
|
// A step landing on a breakpoint ends on it exactly.
|
|
@@ -2439,11 +2562,12 @@ export class Solver {
|
|
|
2439
2562
|
}
|
|
2440
2563
|
result.bdf = bdf;
|
|
2441
2564
|
if (result.converged) {
|
|
2442
|
-
this.history.unshift({ t: this.time, dt, u: Float64Array.from(this.uPrev), c: cN, D: DN });
|
|
2565
|
+
this.history.unshift({ t: this.time, dt, u: Float64Array.from(this.uPrev), c: cN, th: thN, th0: th0N, D: DN, Q: QN });
|
|
2443
2566
|
if (this.history.length > 3) this.history.length = 3;
|
|
2444
2567
|
this.time = tEnd;
|
|
2445
2568
|
this.lastDt = dtEff;
|
|
2446
2569
|
this.contactDOld = this.contactDStart;
|
|
2570
|
+
this.portQOld = this.portQStart;
|
|
2447
2571
|
this._accumulateBoundaryIntake(dtEff, cN);
|
|
2448
2572
|
} else {
|
|
2449
2573
|
this.u.set(this.uPrev);
|
|
@@ -2727,12 +2851,17 @@ export class Solver {
|
|
|
2727
2851
|
u: Float64Array.from(this.u),
|
|
2728
2852
|
uLo: Float64Array.from(this.uLo),
|
|
2729
2853
|
cOld: Float64Array.from(this.cOld),
|
|
2854
|
+
thOld: Float64Array.from(this.thOld),
|
|
2855
|
+
th0Old: Float64Array.from(this.th0Old),
|
|
2730
2856
|
time: this.time,
|
|
2731
2857
|
lastDt: this.lastDt,
|
|
2732
2858
|
atSteady: this.atSteady,
|
|
2733
2859
|
contactDStart: this.contactDStart,
|
|
2734
2860
|
contactDOld: this.contactDOld,
|
|
2735
2861
|
contactDEnd: this.contactDEnd,
|
|
2862
|
+
portQStart: this.portQStart,
|
|
2863
|
+
portQOld: this.portQOld,
|
|
2864
|
+
portQEnd: this.portQEnd,
|
|
2736
2865
|
boundaryIntake: Float64Array.from(this.boundaryIntake),
|
|
2737
2866
|
history: this.history.slice(),
|
|
2738
2867
|
termV: Float64Array.from(this.termV),
|
|
@@ -2744,12 +2873,17 @@ export class Solver {
|
|
|
2744
2873
|
this.u.set(s.u);
|
|
2745
2874
|
this.uLo.set(s.uLo);
|
|
2746
2875
|
this.cOld.set(s.cOld);
|
|
2876
|
+
this.thOld.set(s.thOld);
|
|
2877
|
+
this.th0Old.set(s.th0Old);
|
|
2747
2878
|
this.time = s.time;
|
|
2748
2879
|
this.lastDt = s.lastDt;
|
|
2749
2880
|
this.atSteady = s.atSteady;
|
|
2750
2881
|
this.contactDStart = s.contactDStart;
|
|
2751
2882
|
this.contactDOld = s.contactDOld;
|
|
2752
2883
|
this.contactDEnd = s.contactDEnd;
|
|
2884
|
+
this.portQStart = s.portQStart;
|
|
2885
|
+
this.portQOld = s.portQOld;
|
|
2886
|
+
this.portQEnd = s.portQEnd;
|
|
2753
2887
|
this.boundaryIntake.set(s.boundaryIntake);
|
|
2754
2888
|
this.history = s.history.slice();
|
|
2755
2889
|
this.termV.set(s.termV);
|
|
@@ -2786,8 +2920,11 @@ export class Solver {
|
|
|
2786
2920
|
this.sourceTime = this.time;
|
|
2787
2921
|
this.computeConcentrations();
|
|
2788
2922
|
this.cOld.set(this.c);
|
|
2923
|
+
this.thOld.set(this.th);
|
|
2924
|
+
this.th0Old.set(this.th0);
|
|
2789
2925
|
this.assemble(Infinity);
|
|
2790
2926
|
this.contactDStart = { ...this.contactD };
|
|
2927
|
+
this.portQStart = Float64Array.from(this.portQ);
|
|
2791
2928
|
this.assemble(Infinity);
|
|
2792
2929
|
// The steady parts, then the storage parts from an assembly at a tiny dt.
|
|
2793
2930
|
const J = { A: sys.A.slice(), B: sys.B.slice(), C: sys.C.slice() };
|
|
@@ -3075,6 +3212,7 @@ export class Solver {
|
|
|
3075
3212
|
_accumulateBoundaryIntake(dt, cN) {
|
|
3076
3213
|
this._assembleBookkeeping(dt);
|
|
3077
3214
|
this.contactDEnd = { ...this.contactD };
|
|
3215
|
+
this.portQEnd = Float64Array.from(this.portQ);
|
|
3078
3216
|
if (!Number.isFinite(dt)) return;
|
|
3079
3217
|
const last = this.model.regions.length - 1;
|
|
3080
3218
|
const { n, cOld } = this, vol = this.model.grid.vol;
|
package/src/stack.js
CHANGED
|
@@ -100,7 +100,7 @@ export function bath(c, reference, d = 0, { offset } = {}) {
|
|
|
100
100
|
return { ...drive(d, 'bath'), bath: { c: { ...c }, reference, ...(offset === undefined ? {} : { offset }) } };
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
const STACK_FIELDS = ['T', 'library', 'species', 'materials', 'stack', 'bulkReactions', 'ports', 'grid'];
|
|
103
|
+
const STACK_FIELDS = ['T', 'library', 'species', 'materials', 'stack', 'bulkReactions', 'ports', 'grid', 'geometry'];
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* Build a plain device definition from a stack:
|
|
@@ -112,7 +112,7 @@ const STACK_FIELDS = ['T', 'library', 'species', 'materials', 'stack', 'bulkReac
|
|
|
112
112
|
* between them get the default face. Nested lists are flattened.
|
|
113
113
|
* - Layers may give doping as `donors` and `acceptors` (mol/m³) in place of `fixedCharge`.
|
|
114
114
|
* - `library`: pieces with species and materials, merged with `species` and `materials`.
|
|
115
|
-
* - `T`, `grid`, `ports` pass through.
|
|
115
|
+
* - `T`, `grid`, `ports`, `geometry` pass through.
|
|
116
116
|
* @param {object} def
|
|
117
117
|
* @returns {object} a device definition for `new Device`
|
|
118
118
|
*/
|
|
@@ -168,5 +168,6 @@ export function build(def) {
|
|
|
168
168
|
if (def.bulkReactions !== undefined) out.bulkReactions = def.bulkReactions;
|
|
169
169
|
if (def.ports !== undefined) out.ports = def.ports;
|
|
170
170
|
if (def.grid !== undefined) out.grid = def.grid;
|
|
171
|
+
if (def.geometry !== undefined) out.geometry = def.geometry;
|
|
171
172
|
return out;
|
|
172
173
|
}
|
package/src/types.js
CHANGED
|
@@ -157,10 +157,16 @@
|
|
|
157
157
|
* @property {string} [terminal] species whose offset defaults to 0; with reactions, the electrode's carrier
|
|
158
158
|
* @property {Record<string, 'blocked' | 'equilibrium' | { type: 'equilibrium', offset?: number, mu?: number }
|
|
159
159
|
* | { type: 'conductance', G: number, offset?: number } | { type: 'exchange', k: number, mu: number }>} [species]
|
|
160
|
-
* @property {{ equation: string, fixed?: Record<string, number>, k0: number, alpha: number }[]} [reactions] an
|
|
160
|
+
* @property {{ equation: string, fixed?: Record<string, number>, k0: number, alpha: number, bare?: boolean }[]} [reactions] an
|
|
161
161
|
* electrode spread through the window: Butler–Volmer per area (k0 in mol/(m²·s)) between the region's
|
|
162
162
|
* species and the terminal species at the port's level
|
|
163
|
-
* @property {
|
|
163
|
+
* @property {Record<string, { mu0: number, capacity: number, theta0?: number }>} [surface] with reactions: species
|
|
164
|
+
* on the electrode's sites, Langmuir (μ = μ° + RT ln(θ/θ₀)), capacity in mol of sites per m² of electrode,
|
|
165
|
+
* shared; a reaction with `bare: true` runs only on the bare fraction θ₀ = 1 − Σθ
|
|
166
|
+
* @property {{ C: number, zeroCharge?: number }} [capacitance] a capacitance spread through the window (F/m² of
|
|
167
|
+
* electrode): σ = C (V − zeroCharge − φ) on the port's side, aσ in each node's charge balance; a gate
|
|
168
|
+
* along a channel, a double layer
|
|
169
|
+
* @property {number | { x: number[], values: number[] }} [area] with reactions or a capacitance: electrode area per volume,
|
|
164
170
|
* m²/m³ (1/h for a film of thickness h), or a profile against the device's x
|
|
165
171
|
*/
|
|
166
172
|
|
|
@@ -209,7 +215,8 @@
|
|
|
209
215
|
* @property {{ left: ContactResult, right: ContactResult }} contacts
|
|
210
216
|
* @property {{ left?: { V: number, D: number, charge: number }, right?: { V: number, D: number, charge: number } }} gates
|
|
211
217
|
* @property {{ name: string, V: number, flux: Record<string, number>, current: number, x?: Float64Array,
|
|
212
|
-
* area?: Float64Array, rates?: Float64Array[]
|
|
218
|
+
* area?: Float64Array, rates?: Float64Array[], coverage?: Record<string, Float64Array>, bare?: Float64Array,
|
|
219
|
+
* sigma?: Float64Array, charge?: number }[]} ports what each internal port brings into the device;
|
|
213
220
|
* with reactions, also its window's nodes (x, m), area per volume there, and each reaction's rate per area
|
|
214
221
|
* at each node (mol/(m²·s), forward)
|
|
215
222
|
* @property {{ left: string, right: string, dipole: number, sheetCharge: number, D: number, N: Record<string, number>, rates: number[] }[]} interfaces
|
package/types/solver.d.ts
CHANGED
|
@@ -8,12 +8,19 @@ export class Solver {
|
|
|
8
8
|
n: any;
|
|
9
9
|
M: any;
|
|
10
10
|
nRx: any;
|
|
11
|
+
nSurf: any;
|
|
11
12
|
nNodes: any;
|
|
12
13
|
nFaces: number;
|
|
13
14
|
nB: any;
|
|
14
15
|
VT: number;
|
|
15
16
|
blockOfNode: Int32Array<any>;
|
|
16
17
|
blockOfFace: Int32Array<ArrayBuffer>;
|
|
18
|
+
surfPort: Int32Array<any>;
|
|
19
|
+
surfW: Int32Array<any>;
|
|
20
|
+
th: Float64Array<ArrayBuffer>;
|
|
21
|
+
thOld: Float64Array<ArrayBuffer>;
|
|
22
|
+
th0: Float64Array<any>;
|
|
23
|
+
th0Old: Float64Array<any>;
|
|
17
24
|
present: Uint8Array<ArrayBuffer>;
|
|
18
25
|
cRef: Float64Array<ArrayBuffer>;
|
|
19
26
|
mu0hat: Float64Array<ArrayBuffer>;
|
|
@@ -54,6 +61,10 @@ export class Solver {
|
|
|
54
61
|
left: number;
|
|
55
62
|
right: number;
|
|
56
63
|
};
|
|
64
|
+
portQ: Float64Array<any>;
|
|
65
|
+
portQStart: Float64Array<any>;
|
|
66
|
+
portQOld: Float64Array<any>;
|
|
67
|
+
portQEnd: any;
|
|
57
68
|
contactDOld: {
|
|
58
69
|
left: number;
|
|
59
70
|
right: number;
|
|
@@ -133,6 +144,7 @@ export class Solver {
|
|
|
133
144
|
initFromComposition(): void;
|
|
134
145
|
_portRate(port: any, rx: any, g: any): number;
|
|
135
146
|
_materialAt(mat: any, background: any, eta: any, mode: any, cFix: any, ph: any, zeta: any, cc: any): void;
|
|
147
|
+
_coverages(): void;
|
|
136
148
|
computeConcentrations(): void;
|
|
137
149
|
_work(md: any): any;
|
|
138
150
|
_nodeStatistics(g: any, b: any, phiHat: any): void;
|
|
@@ -172,8 +184,10 @@ export class Solver {
|
|
|
172
184
|
_terminals(dt: any): void;
|
|
173
185
|
_circuit(k: any): void;
|
|
174
186
|
_captureRow(b: any, s: any, w: any, into: any): number;
|
|
175
|
-
_port(port: any, flux: any, k: any): void;
|
|
187
|
+
_port(port: any, flux: any, k: any, dt: any): void;
|
|
176
188
|
_portReaction(port: any, rx: any, area: any, rates: any, flux: any, k: any): void;
|
|
189
|
+
_portCapacitance(port: any, k: any, dt: any): void;
|
|
190
|
+
_surfaceStorage(port: any, dt: any): void;
|
|
177
191
|
_contact(side: any, dt: any): void;
|
|
178
192
|
_replaceRow(b: any, rs: any): void;
|
|
179
193
|
_equilibrate(): void;
|
|
@@ -257,6 +271,8 @@ export class Solver {
|
|
|
257
271
|
u: Float64Array<ArrayBuffer>;
|
|
258
272
|
uLo: Float64Array<ArrayBuffer>;
|
|
259
273
|
cOld: Float64Array<ArrayBuffer>;
|
|
274
|
+
thOld: Float64Array<ArrayBuffer>;
|
|
275
|
+
th0Old: Float64Array<ArrayBuffer>;
|
|
260
276
|
time: number;
|
|
261
277
|
lastDt: number;
|
|
262
278
|
atSteady: boolean;
|
|
@@ -269,6 +285,9 @@ export class Solver {
|
|
|
269
285
|
right: number;
|
|
270
286
|
};
|
|
271
287
|
contactDEnd: any;
|
|
288
|
+
portQStart: Float64Array<any>;
|
|
289
|
+
portQOld: Float64Array<any>;
|
|
290
|
+
portQEnd: any;
|
|
272
291
|
boundaryIntake: Float64Array<ArrayBuffer>;
|
|
273
292
|
history: any[];
|
|
274
293
|
termV: Float64Array<ArrayBuffer>;
|
package/types/stack.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export function bath(c: Record<string, number>, reference: string, d?: number, {
|
|
|
53
53
|
* between them get the default face. Nested lists are flattened.
|
|
54
54
|
* - Layers may give doping as `donors` and `acceptors` (mol/m³) in place of `fixedCharge`.
|
|
55
55
|
* - `library`: pieces with species and materials, merged with `species` and `materials`.
|
|
56
|
-
* - `T`, `grid`, `ports` pass through.
|
|
56
|
+
* - `T`, `grid`, `ports`, `geometry` pass through.
|
|
57
57
|
* @param {object} def
|
|
58
58
|
* @returns {object} a device definition for `new Device`
|
|
59
59
|
*/
|
package/types/types.d.ts
CHANGED
|
@@ -380,9 +380,29 @@ export type PortDef = {
|
|
|
380
380
|
fixed?: Record<string, number>;
|
|
381
381
|
k0: number;
|
|
382
382
|
alpha: number;
|
|
383
|
+
bare?: boolean;
|
|
383
384
|
}[];
|
|
384
385
|
/**
|
|
385
|
-
* with reactions:
|
|
386
|
+
* with reactions: species
|
|
387
|
+
* on the electrode's sites, Langmuir (μ = μ° + RT ln(θ/θ₀)), capacity in mol of sites per m² of electrode,
|
|
388
|
+
* shared; a reaction with `bare: true` runs only on the bare fraction θ₀ = 1 − Σθ
|
|
389
|
+
*/
|
|
390
|
+
surface?: Record<string, {
|
|
391
|
+
mu0: number;
|
|
392
|
+
capacity: number;
|
|
393
|
+
theta0?: number;
|
|
394
|
+
}>;
|
|
395
|
+
/**
|
|
396
|
+
* a capacitance spread through the window (F/m² of
|
|
397
|
+
* electrode): σ = C (V − zeroCharge − φ) on the port's side, aσ in each node's charge balance; a gate
|
|
398
|
+
* along a channel, a double layer
|
|
399
|
+
*/
|
|
400
|
+
capacitance?: {
|
|
401
|
+
C: number;
|
|
402
|
+
zeroCharge?: number;
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* with reactions or a capacitance: electrode area per volume,
|
|
386
406
|
* m²/m³ (1/h for a film of thickness h), or a profile against the device's x
|
|
387
407
|
*/
|
|
388
408
|
area?: number | {
|
|
@@ -545,6 +565,10 @@ export type Solution = {
|
|
|
545
565
|
x?: Float64Array;
|
|
546
566
|
area?: Float64Array;
|
|
547
567
|
rates?: Float64Array[];
|
|
568
|
+
coverage?: Record<string, Float64Array>;
|
|
569
|
+
bare?: Float64Array;
|
|
570
|
+
sigma?: Float64Array;
|
|
571
|
+
charge?: number;
|
|
548
572
|
}[];
|
|
549
573
|
interfaces: {
|
|
550
574
|
left: string;
|