driftlet 0.7.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 +11 -5
- package/docs/device.md +163 -3
- package/docs/kit.md +27 -3
- package/docs/numerics.md +16 -0
- package/llms.txt +14 -9
- package/package.json +1 -1
- package/src/checks.js +10 -9
- package/src/describe.js +14 -1
- package/src/device.js +153 -13
- package/src/grid.js +73 -0
- package/src/index.js +1 -0
- package/src/kit.js +1 -0
- package/src/polarization.js +96 -0
- package/src/solution.js +30 -4
- package/src/solver.js +325 -48
- package/src/stack.js +3 -2
- package/src/types.js +22 -4
- package/types/grid.d.ts +29 -0
- package/types/kit.d.ts +1 -0
- package/types/polarization.d.ts +31 -0
- package/types/solver.d.ts +26 -3
- package/types/stack.d.ts +1 -1
- package/types/types.d.ts +65 -4
package/README.md
CHANGED
|
@@ -26,13 +26,13 @@ minor versions.
|
|
|
26
26
|
## Is it for your problem?
|
|
27
27
|
|
|
28
28
|
The same equations go by different names in different fields. If your problem is one-dimensional
|
|
29
|
-
(planar), it's probably here:
|
|
29
|
+
(planar, or radial: spheres and cylinders, or any cross-section varying along x), it's probably here:
|
|
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
|
-
| Electrochemistry | Nernst–Planck, concentration polarization, limiting current, Butler–Volmer, Warburg, cyclic voltammetry, salt bridges and liquid junctions | [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) tests | `i_lim·tanh(V/4V_T)`, Butler–Volmer closed form, finite-length Warburg; junction potentials against Henderson, Planck, JPCalc and LJPcalc |
|
|
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 |
|
|
37
37
|
| Double layers, colloids | Poisson–Boltzmann, Gouy–Chapman–Stern, Debye screening, crowding (Bikerman) | [double-layer demo](https://markblundeberg.github.io/driftlet/demos/double-layer.html), [`equilibrium`](test/equilibrium.test.js) test | Gouy–Chapman charge and profile, Kilic–Bazant–Ajdari |
|
|
38
38
|
| Membranes, desalination | Donnan, ion exchange, liquid junctions, water dissociation | [membrane demo](https://markblundeberg.github.io/driftlet/demos/membrane.html), [`equilibrium`](test/equilibrium.test.js), [`neutral`](test/neutral.test.js) tests | Donnan partition, Planck EMF |
|
|
@@ -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
|
|
|
@@ -204,7 +204,9 @@ current. Supported physics:
|
|
|
204
204
|
|
|
205
205
|
## What it doesn't do
|
|
206
206
|
|
|
207
|
-
- **More than one dimension.**
|
|
207
|
+
- **More than one dimension.** 1D only: planar, or a cross-section varying along x (spheres,
|
|
208
|
+
cylinders, a thinning film), so no porous electrodes with diffusion inside their particles
|
|
209
|
+
(P2D models). For 2D/3D,
|
|
208
210
|
see [ChargeTransport.jl](https://github.com/WIAS-PDELib/ChargeTransport.jl), TCAD tools or
|
|
209
211
|
COMSOL.
|
|
210
212
|
- **Cross-diffusion.** Each species moves down its own μ̄ with its own D (the statistics can be
|
|
@@ -259,6 +261,10 @@ Every physics feature is tested against analytic results (`npm test`, node's bui
|
|
|
259
261
|
| Advection and eddy mixing | exact convection–diffusion profile; `D + D_mix`; junction EMF unchanged by mixing | 1e-12; 1e-12; 1e-5 |
|
|
260
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 |
|
|
261
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
|
+
| 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 |
|
|
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 |
|
|
262
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 |
|
|
263
269
|
| Conservation | per step, and against time-integrated contact fluxes | 1e-11 relative |
|
|
264
270
|
| Strictly neutral limit (ε = 0) | Planck EMF; polarization with no overlimiting; Donnan at neutral faces | 1e-5; 1e-4; 1e-9 |
|
package/docs/device.md
CHANGED
|
@@ -24,6 +24,7 @@ too (`FARADAY`, `GAS_CONSTANT`, `EPS0`, …).
|
|
|
24
24
|
| `contacts` | `{ left, right }` |
|
|
25
25
|
| `ports` | internal ports: outside phases exchanging with a window of nodes |
|
|
26
26
|
| `grid` | default spacing for every region |
|
|
27
|
+
| `geometry` | the cross-section $`A(x)`$: planar (default), spherical or cylindrical shells, or a profile (see [geometry](#geometry)) |
|
|
27
28
|
|
|
28
29
|
## Species
|
|
29
30
|
|
|
@@ -445,7 +446,16 @@ contact: held at a voltage, behind a resistance, or driven by a current (a refer
|
|
|
445
446
|
is a port at `I: 0`). Each solution reports `ports[k]`, with `{ name, V, flux, current }`: what
|
|
446
447
|
the port brings into the device.
|
|
447
448
|
|
|
448
|
-
A held (`'equilibrium'`) level leaves the device's two end nodes to their contacts.
|
|
449
|
+
A held (`'equilibrium'`) level leaves the device's two end nodes to their contacts. A port that
|
|
450
|
+
exchanges only neutral species carries no current, so its voltage means nothing: give it no drive.
|
|
451
|
+
|
|
452
|
+
An exchange link's source is linear in $`\mu_{out} - \mu`$, so it grows without bound as the
|
|
453
|
+
species runs out ($`\ln c`$). A supply limited by diffusion through a film above the window, such as O₂
|
|
454
|
+
reaching a thin layer of water from the air, is linear in $`c`$ instead: $`k(c_{sat} - c)`$. For that,
|
|
455
|
+
use a [bulk reaction](#bulk-reactions) with the outside phase as a fixed participant, e.g.
|
|
456
|
+
`{ equation: 'Air = O2', fixed: { Air: muSat }, kf: { water: profile } }`. Its rate is
|
|
457
|
+
$`k_f (1 - c/c_{sat})`$, and a profile gives $`k_f`$ its spatial variation (a film of thickness $`h(x)`$:
|
|
458
|
+
$`D c_{sat}/h^2`$).
|
|
449
459
|
|
|
450
460
|
A port driven by a current (`I`, or a waveform) floats to whatever voltage delivers it: with a
|
|
451
461
|
conductance `G` per volume over a window of width $`w`$, it sits about $`I/(G w)`$ above the
|
|
@@ -467,6 +477,128 @@ steady state can be computed there. A port holding the electrons beside the oxid
|
|
|
467
477
|
channel's potential anchors it, as source and drain would, and the device then shows the
|
|
468
478
|
low-frequency C–V.
|
|
469
479
|
|
|
480
|
+
### Electrodes spread through a window
|
|
481
|
+
|
|
482
|
+
A port can also hold an electrode: a metal whose carrier sits at the port's level, reacting all
|
|
483
|
+
through the window rather than at one face. That's the floor under a thin film of electrolyte, the
|
|
484
|
+
walls of a crevice or a pit, or the matrix of a porous electrode, wherever the metal conducts well
|
|
485
|
+
enough to be one level. Give the port its `reactions` (Butler–Volmer, as at a face), the carrier as
|
|
486
|
+
its `terminal` (the species need be in no material), and the electrode's `area` per volume of the
|
|
487
|
+
window:
|
|
488
|
+
|
|
489
|
+
```js nocheck
|
|
490
|
+
ports: [{
|
|
491
|
+
name: 'iron', region: 'film', terminal: 'e-', V: 0,
|
|
492
|
+
area: 1 / 100e-6, // m²/m³: a film 100 µm thick on the metal; or a profile against x
|
|
493
|
+
reactions: [
|
|
494
|
+
{ equation: 'Fe2+ + 2 e- = Fe(s)', fixed: { 'Fe(s)': 0 }, k0: 5e-6, alpha: 0.5 },
|
|
495
|
+
{ equation: 'O2 + 2 H2O + 4 e- = 4 OH-', fixed: { H2O: -237.13e3 }, k0: 3e-9, alpha: 0.125 },
|
|
496
|
+
],
|
|
497
|
+
}]
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Each node's rate per area is the face's law, with the region's species at that node and the
|
|
501
|
+
carrier at activity 1 and level $`V`$. Times the area per volume, it makes and consumes the
|
|
502
|
+
region's species there, and the electrons it takes or gives are the port's current. The port is
|
|
503
|
+
a terminal like any other. Held at a voltage, it's a potentiostat. At `I: 0` (or with nothing
|
|
504
|
+
else to carry current), the reactions settle at the mixed potential, the corrosion potential where
|
|
505
|
+
anodic and cathodic currents cancel over the window, while each node can be a net anode or a net
|
|
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: `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.
|
|
512
|
+
|
|
513
|
+
Validated against the transmission line (linear kinetics along a bar, test/ports.test.js) and the
|
|
514
|
+
Wagner–Traud mixed potential of two Butler–Volmer couples. In 1D, a film on a metal is a slice
|
|
515
|
+
along the metal: the film's thickness enters through `area`, and through a bulk reaction for
|
|
516
|
+
anything that reaches the film from above (O₂ from the air, see the exchange link above). Where
|
|
517
|
+
the film's thickness varies, give the device its [cross-section](#geometry) too, so that ions
|
|
518
|
+
travelling along the film squeeze through where it thins: for a drop on a metal, seen from its
|
|
519
|
+
centre, $`A = 2\pi r\,h(r)`$ with `area` $`1/h(r)`$.
|
|
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
|
+
|
|
470
602
|
## Terminals
|
|
471
603
|
|
|
472
604
|
The two contacts and every port are the device's terminals, named `left`, `right` and by each
|
|
@@ -521,6 +653,34 @@ cells exceeds the limiting current by 1.4% at 0.5 V, while `{ hmin: 10e-9, hmax:
|
|
|
521
653
|
within 0.1% with as many nodes. Where a species carrying the current is steep across a region's
|
|
522
654
|
end cells in this way, the solution's `warnings` say so, with a rough estimate of the excess.
|
|
523
655
|
|
|
656
|
+
## Geometry
|
|
657
|
+
|
|
658
|
+
A device is planar unless it says otherwise: a slab whose cross-section $`A`$ is 1 m², so every
|
|
659
|
+
current, flux, amount and charge reads per m². With `geometry`, the cross-section varies along x,
|
|
660
|
+
and the conservation laws hold for totals through it: a box holds $`\int A\,dx`$, and what crosses
|
|
661
|
+
a face is the flux there times $`A`$ there.
|
|
662
|
+
|
|
663
|
+
```js nocheck
|
|
664
|
+
geometry: { type: 'spherical', r0: 10e-6 }, // shells, r = r0 + x: a microelectrode of radius r0
|
|
665
|
+
geometry: { type: 'cylindrical', r0: 0 }, // a wire or fibre from its axis (per metre of length)
|
|
666
|
+
geometry: { area: { x: [0, 1e-3], values: [3e-6, 1e-7] } }, // A (m²) against x: a film thinning, a pit
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
This is how radial problems become 1D, with the right $`1/r^2`$ (or $`1/r`$) in every divergence and in
|
|
670
|
+
Gauss's law, without writing them out. The finite volumes integrate over shells, so conservation
|
|
671
|
+
is exact as in a slab. Each segment's transport is weighted by its length over $`\int dx/A`$, which
|
|
672
|
+
makes steady diffusion between two nodes exact for any $`A(x)`$ (the $`1/r`$ and $`\ln r`$ profiles
|
|
673
|
+
on any grid). Where $`A`$ vanishes at an end (a sphere's centre, `r0: 0`), the segment takes $`A`$ at
|
|
674
|
+
its middle, and nothing passes the end. Only an end may have $`A = 0`$. Flow (`velocity`) is for
|
|
675
|
+
planar devices.
|
|
676
|
+
|
|
677
|
+
With a cross-section, the solution's currents, contact and port fluxes, terminal currents and
|
|
678
|
+
`charge` are totals (A, mol/s; per metre for cylinders), and so are `I` drives. Face laws,
|
|
679
|
+
rates, `interfaces[f].N` and `D`, and concentrations stay per area or per volume. Validated against
|
|
680
|
+
steady diffusion to a sphere and a cylinder (exact), Cottrell's transient with the spherical term,
|
|
681
|
+
uptake by a sphere filling from its surface, and Debye–Hückel screening around a charged sphere
|
|
682
|
+
(test/geometry.test.js).
|
|
683
|
+
|
|
524
684
|
## Using a device
|
|
525
685
|
|
|
526
686
|
```js nocheck
|
|
@@ -610,14 +770,14 @@ console.log(`${run.steps} steps; I(0.01 s) ≈ ${run.trace.current[run.trace.t.f
|
|
|
610
770
|
| `phi` | bookkeeping $`\phi`$, V (`NaN` where undefined) |
|
|
611
771
|
| `c[name]`, `mu[name]`, `muStd[name]` | concentration, $`\bar\mu`$, standard level $`\mu^\circ + zF\phi`$ (`NaN` where absent) |
|
|
612
772
|
| `V[name]`, `Vstd[name]` | species voltage $`\bar\mu/(zF)`$ and standard level as a voltage (charged species) |
|
|
613
|
-
| `current`, `terminalVoltage` | current toward +x through the device (A/m
|
|
773
|
+
| `current`, `terminalVoltage` | current toward +x through the device (A/m², or A through a [cross-section](#geometry)) and $`V_{\mathrm{right}} - V_{\mathrm{left}}`$ |
|
|
614
774
|
| `terminals[name]` | `{ V, current }` for each terminal (contacts and ports), current into the device |
|
|
615
775
|
| `contacts.left/right` | `{ V, flux: {name}, D, current }` at each contact |
|
|
616
776
|
| `gates.left/right` | charge on a gate or Stern plate, where the contact is capacitive |
|
|
617
777
|
| `ports[k]` | `{ name, V, flux: {name}, current }`: what each internal port brings into the device |
|
|
618
778
|
| `interfaces[f]` | `{ left, right, dipole, sheetCharge, D, N: {name}, rates }`: the names of the regions the face joins, what crosses it by its links, and each face reaction's rate (mol/(m²·s)) |
|
|
619
779
|
| `bulkReactions[k]` | `{ rate, regions, total }`: each bulk reaction's forward rate at every node (mol/(m³·s), `NaN` where it doesn't run), and integrated over each region and the device (mol/(m²·s)), as the balances count it: a charge-balance check is J = F(generation − recombination), which the kit's `check()` does for every species |
|
|
620
|
-
| `charge` | total charge in the device, C/m² |
|
|
780
|
+
| `charge` | total charge in the device, C/m² (C through a cross-section) |
|
|
621
781
|
| `conservation` | per species stretch: amount, reference, intake through contacts, drift |
|
|
622
782
|
| `warnings` | e.g. unresolved double layers, conventions a statistics model relies on, and for a failed solve, where the system is nearly singular |
|
|
623
783
|
| `converged`, `iterations`, `steps`, `substeps`, `history`, `time` | solver bookkeeping |
|
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
|
|
|
@@ -82,7 +82,7 @@ $`\mu_{\mathrm{H_2}} = 0`$, so its standard level is $`\phi + \mu^\circ_{\mathrm
|
|
|
82
82
|
|
|
83
83
|
```js
|
|
84
84
|
import { Device } from 'driftlet';
|
|
85
|
-
import { build, layer, ohmic, half, level } from 'driftlet/kit';
|
|
85
|
+
import { build, layer, ohmic, half, level, polarization } from 'driftlet/kit';
|
|
86
86
|
|
|
87
87
|
const silver = half('Ag+ + e- = Ag(s)', { 'Ag(s)': 0 });
|
|
88
88
|
const stern = { phi: { type: 'capacitive', C: 0.2 }, zeroCharge: 0.1 }; // an assumed pzc
|
|
@@ -108,11 +108,35 @@ const def = build({
|
|
|
108
108
|
grid: { hmin: 0.1e-9, hmax: 100e-9, ratio: 1.15 },
|
|
109
109
|
});
|
|
110
110
|
console.log(JSON.stringify(def.interfaces[0].reactions[0])); // { left: { e-: -1, Ag(s): 1 }, right: { Ag+: -1 }, … }
|
|
111
|
-
const
|
|
111
|
+
const dev = new Device(def), sol = dev.solve();
|
|
112
112
|
const redox = level(sol, silver); // V_Ag⁺(x) in the solution
|
|
113
113
|
console.log(`redox level at mid-cell: ${redox[sol.x.length >> 1].toFixed(4)} V`);
|
|
114
|
+
// The left electrode's polarization curve, 0.2 V either side of its own level (see below).
|
|
115
|
+
const metalSide = sol.V['e-'][0];
|
|
116
|
+
const curve = polarization(dev, sol, { face: 0 }, Array.from({ length: 81 }, (_, j) => metalSide - 0.2 + j * 0.005));
|
|
117
|
+
console.log(curve.reactions[0].level, curve.current[40]); // the couple's level here, and the current at the metal's own
|
|
114
118
|
```
|
|
115
119
|
|
|
120
|
+
### Polarization curves: an Evans diagram
|
|
121
|
+
|
|
122
|
+
`polarization(device, sol, where, V)` evaluates every electrode reaction at one spot with the
|
|
123
|
+
solution beside it held as `sol` has it and the metal's level set to each `V` (an electron
|
|
124
|
+
voltage, the terminals' convention). It works at a face (`{ face: f }`, the face's metal side) or
|
|
125
|
+
at a spot of an [electrode port](device.md#electrodes-spread-through-a-window)
|
|
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
|
+
current (A/m², anodic positive: into the solution) and its `level`, where the rate vanishes (the
|
|
128
|
+
couple's redox level there, as `level()` gives it). The net current is their sum. Plotting
|
|
129
|
+
log|current| against V gives the Evans diagram. The net crosses zero at the spot's open-circuit
|
|
130
|
+
potential, between the couples' levels, and the metal's actual level is where the solve put it.
|
|
131
|
+
The law is the solver's own, so at that level the rates are the solution's. Against SHE, subtract
|
|
132
|
+
the SHE level (`sol.phi` with the usual tables' ions).
|
|
133
|
+
|
|
134
|
+
The example above ends with the left electrode's curve.
|
|
135
|
+
|
|
136
|
+
Concentrations are held, so these are the curves of that instant (for a mixed potential, the
|
|
137
|
+
kinetic picture). Away from the actual level the composition beside the electrode would change,
|
|
138
|
+
which is the transport the full solve accounts for.
|
|
139
|
+
|
|
116
140
|
## Sources
|
|
117
141
|
|
|
118
142
|
Helpers that write sources as plain data:
|
package/docs/numerics.md
CHANGED
|
@@ -117,6 +117,22 @@ Grids grade geometrically from `hmin` at each region end up to `hmax`. They're n
|
|
|
117
117
|
automatically, to avoid performance surprises. Instead, solutions warn when a double layer the
|
|
118
118
|
model is meant to resolve is coarser than the local Debye length.
|
|
119
119
|
|
|
120
|
+
### A cross-section
|
|
121
|
+
|
|
122
|
+
With a [`geometry`](device.md#geometry), the device has a cross-section $`A(x)`$ (spherical
|
|
123
|
+
shells $`4\pi r^2`$, cylindrical $`2\pi r`$ with $`r = r_0 + x`$, or a profile), and every balance
|
|
124
|
+
is for totals through it:
|
|
125
|
+
$`\partial_t (A c_i) + \partial_x (A N_i) = A \sum_r \nu_{ir} r_r`$, and Gauss's law
|
|
126
|
+
$`-\partial_x (A\varepsilon\,\partial_x\phi) = A\rho`$. The box method carries this without writing out any
|
|
127
|
+
$`1/r`$ terms: a box's volume becomes $`\int A\,dx`$ over it (exactly, for each shape), and what a
|
|
128
|
+
segment, a face or a contact passes is multiplied by the area it passes through. A segment's
|
|
129
|
+
$`1/h`$ becomes $`1/\int dx/A`$ over it, its resistance per unit conductivity, which is the exact
|
|
130
|
+
weighting for steady diffusion (a constant total flux $`AN`$, so $`\Delta c = AN\int dx/A\,/D`$).
|
|
131
|
+
Scharfetter–Gummel keeps its form with that weighting, and so does the displacement. Where $`A`$
|
|
132
|
+
vanishes at an end (a sphere's centre), $`\int dx/A`$ diverges, so that segment takes $`A`$ at its
|
|
133
|
+
middle. A flux node's unknowns stay per area, entering each side's balance times the face's
|
|
134
|
+
area. A planar device has $`A = 1`$ and every factor is exactly 1, so it's unchanged to the last bit.
|
|
135
|
+
|
|
120
136
|
### What lives where
|
|
121
137
|
|
|
122
138
|
| Place | Holds |
|
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`.
|
|
@@ -50,7 +50,7 @@ From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
|
|
|
50
50
|
V_T = GAS_CONSTANT·T/FARADAY). `driftlet/kit`: `build`, `layer`, `ohmic`, `bath`, `half`,
|
|
51
51
|
`level`, `SHE`, `describe`, `live`, `traces`, the sources (`pulse`, `square`, `triangle`, `ramp`,
|
|
52
52
|
`injector`, `recombination`, `photogeneration`), the data library (`aqueous`, `semiconductor`,
|
|
53
|
-
`metal`, `IONS`, …), `check` (a solution's checks), `henderson` and `planck` (liquid-junction potentials), `recorder` (a transient's trace and frames across calls), the curated
|
|
53
|
+
`metal`, `IONS`, …), `check` (a solution's checks), `polarization` (an electrode's reactions at hypothetical metal levels, with the solution held: an Evans diagram), `henderson` and `planck` (liquid-junction potentials), `recorder` (a transient's trace and frames across calls), the curated
|
|
54
54
|
`perovskiteCell` with IonMonger's `PEROVSKITE_SCANS` and `hysteresis`, and the vacuum-level helpers. `driftlet/plot` (a quick look; `traces()` is the data for your own charts): `bandDiagram`, `levelChart`, `labelParts` (labels mark subscripts as `V_{e⁻}`), `themeColor`.
|
|
55
55
|
- **Reactions** are written as equations, `{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }`, in
|
|
56
56
|
`new Device`, `build()` and `set()` alike. A coefficient is a number and a space (`2 e-`).
|
|
@@ -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,
|
|
@@ -405,11 +405,16 @@ has the long form):
|
|
|
405
405
|
do. It checks the setup: a solver's output looks authoritative even when an input is wrong, and
|
|
406
406
|
a mismatch is the cheapest way to catch it. Where the two should differ (high injection, a
|
|
407
407
|
finite pulse), say why.
|
|
408
|
-
- **Stay honestly inside 1D.** driftlet models
|
|
408
|
+
- **Stay honestly inside 1D.** driftlet models devices along one coordinate: planar, or with a
|
|
409
|
+
cross-section A(x) (`geometry`: spherical or cylindrical shells, or a profile), in which case
|
|
410
|
+
currents and fluxes are totals through it. Where the real device isn't one,
|
|
409
411
|
say so and pick the slice that still tells the true story: a MOSFET's gate stack, read as the
|
|
410
412
|
MOS capacitor its channel sits on (pinch-off along the channel is 2D); a BJT along its
|
|
411
413
|
emitter–base–collector axis; one particle of a porous battery electrode, or a thin dense film,
|
|
412
|
-
rather than the whole porous electrode (that's a P2D model).
|
|
414
|
+
rather than the whole porous electrode with diffusion inside its particles (that's a P2D model).
|
|
415
|
+
A metal that reacts all along a film of electrolyte (corroding iron under a drop, a crevice's
|
|
416
|
+
walls, a well-conducting porous matrix) is a port with `reactions` and an `area` per volume:
|
|
417
|
+
an electrode spread through a window, not a face at each end.
|
|
413
418
|
- **Check `sol.converged` and `sol.warnings`** on every solve, and pin the CDN version you
|
|
414
419
|
tested against.
|
|
415
420
|
|
package/package.json
CHANGED
package/src/checks.js
CHANGED
|
@@ -65,7 +65,7 @@ export function check(device, sol, { refine = true, tol = 1e-2 } = {}) {
|
|
|
65
65
|
// The rest checks a solution, which an unconverged state isn't.
|
|
66
66
|
if (sol.converged) {
|
|
67
67
|
const ledger = steady ? balance(device, sol) : null;
|
|
68
|
-
items.push(ledger ?? conservation(sol));
|
|
68
|
+
items.push(ledger ?? conservation(sol, Math.max(...device.model.grid.area)));
|
|
69
69
|
if (steady && refine) items.push(gridCheck(device, sol, tol, ledger.details.gross));
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -164,26 +164,27 @@ function balance(device, sol) {
|
|
|
164
164
|
// carrier, σRT/(z²F²L)), and by its bulk reactions, their one-way rates. In equilibrium the net
|
|
165
165
|
// flows are round-off of these.
|
|
166
166
|
function naturalScales(device, sol) {
|
|
167
|
-
const { model, solver } = device, { species } = model;
|
|
168
|
-
|
|
167
|
+
const { model, solver } = device, { species, grid } = model;
|
|
168
|
+
// (totals through the cross-section, as the ledgers are: its largest in each region)
|
|
169
|
+
const area = model.regions.map((_, r) => Math.max(...grid.area.subarray(grid.regionStart[r], grid.regionEnd[r] + 1)));
|
|
169
170
|
return species.map((sp, i) => {
|
|
170
171
|
let S = 0;
|
|
171
172
|
model.regions.forEach((reg, r) => {
|
|
172
173
|
const mat = model.materials[reg.material], L = reg.length;
|
|
173
174
|
if (mat.conductor) {
|
|
174
|
-
if (mat.conductor.i === i) S = Math.max(S, (mat.conductor.sigma * model.RT) / (sp.z * sp.z * FARADAY * FARADAY * L));
|
|
175
|
+
if (mat.conductor.i === i) S = Math.max(S, (area[r] * mat.conductor.sigma * model.RT) / (sp.z * sp.z * FARADAY * FARADAY * L));
|
|
175
176
|
return;
|
|
176
177
|
}
|
|
177
178
|
if (!mat.present[i] || !(mat.D[i] > 0)) return;
|
|
178
179
|
let cmax = 0;
|
|
179
180
|
for (let g = 0; g < sol.x.length; g++) if (sol.region[g] === r && Number.isFinite(sol.c[sp.name][g])) cmax = Math.max(cmax, sol.c[sp.name][g]);
|
|
180
|
-
S = Math.max(S, (mat.D[i] * cmax) / L);
|
|
181
|
+
S = Math.max(S, (area[r] * mat.D[i] * cmax) / L);
|
|
181
182
|
});
|
|
182
183
|
solver.rxs.forEach((rx) => {
|
|
183
184
|
const p = rx.sp.indexOf(i);
|
|
184
185
|
if (p < 0) return;
|
|
185
186
|
let gross = 0;
|
|
186
|
-
for (let g = 0; g < sol.x.length; g++) if (rx.kf[solver.nodeMaterial[g]] > 0) gross +=
|
|
187
|
+
for (let g = 0; g < sol.x.length; g++) if (rx.kf[solver.nodeMaterial[g]] > 0) gross += grid.vol[g] * solver.bulkOneWay(rx, g);
|
|
187
188
|
S = Math.max(S, Math.abs(rx.nu[p]) * gross);
|
|
188
189
|
});
|
|
189
190
|
return S;
|
|
@@ -191,12 +192,12 @@ function naturalScales(device, sol) {
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
// Closed, unreacting stretches keep their amounts in a transient.
|
|
194
|
-
function conservation(sol) {
|
|
195
|
+
function conservation(sol, area = 1) {
|
|
195
196
|
const closed = sol.conservation.filter((st) => !st.connected && !st.reactive);
|
|
196
197
|
if (closed.length === 0) return { name: 'conservation', ok: true, summary: 'nothing closed to conserve (every species reaches a terminal or reacts)' };
|
|
197
198
|
// Relative to the amount, but a drift below 1e-20 mol/m² (1e-15 C/m²) is nothing: a floating
|
|
198
|
-
// metal's excess electrons are themselves ~0.
|
|
199
|
-
const worst = Math.max(...closed.map((st) => (Math.abs(st.amount - st.reference - st.intake) < 1e-20 ? 0 : Math.abs(st.drift))));
|
|
199
|
+
// metal's excess electrons are themselves ~0. (Amounts are totals through the cross-section.)
|
|
200
|
+
const worst = Math.max(...closed.map((st) => (Math.abs(st.amount - st.reference - st.intake) < 1e-20 * area ? 0 : Math.abs(st.drift))));
|
|
200
201
|
return {
|
|
201
202
|
name: 'conservation',
|
|
202
203
|
ok: worst < 1e-8,
|
package/src/describe.js
CHANGED
|
@@ -125,6 +125,11 @@ export function describe(def) {
|
|
|
125
125
|
const warnings = unitWarnings(def);
|
|
126
126
|
const total = grid.x[grid.x.length - 1] - grid.x[0];
|
|
127
127
|
lines.push(`${regions.length} region${regions.length > 1 ? 's' : ''}, ${si(total, 'm')}, ${grid.x.length} nodes, T = ${model.T} K`);
|
|
128
|
+
const geo = model.geometry;
|
|
129
|
+
if (geo.type !== 'planar') {
|
|
130
|
+
const shape = geo.type === 'profile' ? `a cross-section from ${num(Math.min(...grid.area))} to ${num(Math.max(...grid.area))} m²` : `${geo.type} shells, r = ${si(geo.r0, 'm')} + x${geo.type === 'cylindrical' ? ' (per metre of length)' : ''}`;
|
|
131
|
+
lines.push(`geometry: ${shape}; currents and fluxes are totals through it (A, mol/s)`);
|
|
132
|
+
}
|
|
128
133
|
lines.push(`species: ${species.map((sp) => `${sp.name} (z = ${sp.z > 0 ? '+' : ''}${sp.z})`).join(', ')}`);
|
|
129
134
|
|
|
130
135
|
lines.push('regions:');
|
|
@@ -206,7 +211,15 @@ export function describe(def) {
|
|
|
206
211
|
const term = ct.terminal === null ? '' : ` (terminal ${species[ct.terminal].name})`;
|
|
207
212
|
lines.push(` ${side}: ${drive(cdef)}${term}; ${linked.length ? `exchanges ${linked.join(', ')}` : 'exchanges nothing'}; φ ${ct.phi.type}`);
|
|
208
213
|
}
|
|
209
|
-
(def.ports ?? []).forEach((p, k) =>
|
|
214
|
+
(def.ports ?? []).forEach((p, k) => {
|
|
215
|
+
const port = model.ports[k];
|
|
216
|
+
lines.push(` port ${port.name}: ${drive(p)}, in ${regions[port.region].name}`);
|
|
217
|
+
// an electrode spread through the window
|
|
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}${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)`);
|
|
222
|
+
});
|
|
210
223
|
if ((def.bulkReactions ?? []).length) {
|
|
211
224
|
lines.push('bulk reactions:');
|
|
212
225
|
for (const rx of def.bulkReactions) lines.push(` ${rx.equation ?? equation(rx.nu)}${rx.srh ? ' (SRH)' : ''} in ${Object.keys(rx.kf ?? rx.srh ?? {}).join(', ')}`);
|