driftlet 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/device.md +28 -8
- package/docs/numerics.md +10 -4
- package/docs/reliability.md +18 -13
- package/docs/statistics.md +13 -0
- package/llms.txt +11 -8
- package/package.json +1 -1
- package/src/describe.js +2 -0
- package/src/impedance.js +146 -44
- package/src/index.js +1 -0
- package/src/newton.js +23 -5
- package/src/solution.js +5 -0
- package/src/solver.js +12 -3
- package/src/start.js +92 -60
- package/src/steady.js +71 -8
- package/src/timeSteps.js +13 -1
- package/types/impedance.d.ts +2 -2
- package/types/solver.d.ts +2 -0
- package/types/start.d.ts +8 -0
- package/types/steady.d.ts +4 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ import { bandDiagram } from 'driftlet/plot'; // a level diagram as an SVG str
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Or straight from a CDN in a page, pinned to a version:
|
|
59
|
-
`https://cdn.jsdelivr.net/npm/driftlet@1.3.
|
|
59
|
+
`https://cdn.jsdelivr.net/npm/driftlet@1.3.2/src/index.js` (and `src/kit.js`, `src/plot.js`).
|
|
60
60
|
TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
|
|
61
61
|
[`llms.txt`](llms.txt).
|
|
62
62
|
|
package/docs/device.md
CHANGED
|
@@ -112,9 +112,11 @@ regions: [
|
|
|
112
112
|
material. In a strictly neutral region, $`\sum z \cdot c_0`$ must be zero.
|
|
113
113
|
Where two strictly neutral regions meet at a face that holds ions in equilibrium and their
|
|
114
114
|
`c0` leaves it out of equilibrium (a different fixed charge or immobile ion on each side), the
|
|
115
|
-
two nodes at the face start in it, as the face reaches it at $`t = 0^+`$: the ions
|
|
116
|
-
shared between them so their $`\bar\mu`$ is equal across it and each side neutral,
|
|
117
|
-
one's amount over the two nodes kept, everything else as `c0` has it.
|
|
115
|
+
two nodes at the face start a transient in it, as the face reaches it at $`t = 0^+`$: the ions
|
|
116
|
+
it passes shared between them so their $`\bar\mu`$ is equal across it and each side neutral,
|
|
117
|
+
each one's amount over the two nodes kept, everything else as `c0` has it. (The first time
|
|
118
|
+
step from a cold start does this; a steady solve starts from `c0` as given, which it reaches
|
|
119
|
+
better from.)
|
|
118
120
|
An entry can instead be a profile, `{ x: [...], values: [...] }`, against the device's x (m, not
|
|
119
121
|
the region's own), piecewise linear between its points and constant beyond its ends, so a
|
|
120
122
|
transient can start from a state it didn't simulate reaching: a packet already injected, a
|
|
@@ -541,6 +543,12 @@ levels are that material's, and the end node takes them through a Donnan step.
|
|
|
541
543
|
means. For example, a Schottky barrier $`\phi_B`$ on n-type material with
|
|
542
544
|
$`\mu^\circ_{\mathrm{e}^-} = 0`$ is `zeroCharge: φ_B`.
|
|
543
545
|
|
|
546
|
+
An ideally blocking electrode (inert: no reactions, nothing crossing) is a contact with no
|
|
547
|
+
terminal species and no links, `{ V, phi: 'pinned', zeroCharge }`: the solution's diffuse layer
|
|
548
|
+
is its whole double layer (the validation suite's Macdonald impedance is this cell), and a Stern layer in
|
|
549
|
+
series is `phi: { type: 'capacitive', C }` in place of `'pinned'`. (A metal layer behind a
|
|
550
|
+
capacitive face of large C does the same at more cost, and its C is warned of as a unit slip.)
|
|
551
|
+
|
|
544
552
|
A Schottky contact is that pinned barrier with its carriers passing at their emission velocities:
|
|
545
553
|
`{ V, terminal: 'e-', species: { 'e-': { type: 'velocity', v }, 'h+': { type: 'velocity', v: vp, offset: 0 } }, phi: 'pinned', zeroCharge: φ_B }`,
|
|
546
554
|
with Bethe's thermionic-emission velocity $`v = A^* T^2/(F N_c)`$ ($`N_c`$ in mol/m³, the data
|
|
@@ -753,8 +761,12 @@ as for reactions, and the window holds the opposite. Each node's charge balance
|
|
|
753
761
|
Gauss's law averaged across the section, $`-\partial_x(\varepsilon\,\partial_x\phi) = \rho + a\sigma`$,
|
|
754
762
|
and at $`\varepsilon = 0`$ neutrality $`\rho + a\sigma = 0`$, the mobile species rearranging to supply
|
|
755
763
|
it (counter-ions in, co-ions out). The port passes the charging current, $`d(\int a\sigma\,dV)/dt`$,
|
|
756
|
-
none in a steady state.
|
|
757
|
-
|
|
764
|
+
none in a steady state. `zeroCharge` is in the frame a contact's is (see the capacitive contact
|
|
765
|
+
above): $`V - \phi`$ at zero charge, with $`\phi`$ the device's own, which a bath or contact sets
|
|
766
|
+
(beside a bath referenced to Cl⁻, with the library's μ°, it sits near −1.4 V). Omitted, it's 0, so
|
|
767
|
+
a port held at V = 0 there starts charged by 1.4 V; to start it uncharged, give `zeroCharge` = V −
|
|
768
|
+
φ at its window (`sol.phi` after a solve). Capacitances on overlapping windows add (a channel's
|
|
769
|
+
top and bottom gates). A capacitance alone, driven by a current, only charges: it starts uncharged, `advance()`
|
|
758
770
|
follows it, and `solve()` refuses (there's no steady state, as for a gate contact driven by one).
|
|
759
771
|
|
|
760
772
|
```js nocheck
|
|
@@ -1004,14 +1016,19 @@ const now = dev.solution(); // snapshot of the current state
|
|
|
1004
1016
|
as a transient slows, so give `dtMax` to resolve a signal in time.
|
|
1005
1017
|
- `impedance(frequencies, { terminal, profiles })` solves the steady state, then linearises about
|
|
1006
1018
|
it: $`Z(f) = \delta V/\delta I`$ in Ω·m² at one terminal (`'right'` by default), with I into the
|
|
1007
|
-
device
|
|
1019
|
+
device, its input impedance whether it's held or driven by a current (for a driven one, about
|
|
1020
|
+
the state its drive holds: solve() it held, then set its I, solve(), then impedance()); the other terminals
|
|
1008
1021
|
keep their drives (held ones at AC ground, driven ones open), so at least one other must be held
|
|
1009
1022
|
for the current to return (with every other one driven, the impedance is infinite, and an error
|
|
1010
1023
|
says to measure at a driven one). A terminal behind a resistance can't be the one measured: the
|
|
1011
1024
|
resistance belongs to the external circuit. The grid must resolve how far the signal reaches,
|
|
1012
1025
|
and nothing warns of it: in a porous electrode, $`\sqrt{\kappa/(\omega C a)}`$ (cells of 2 µm
|
|
1013
1026
|
against a 3.4 µm depth put Z 7% off, falling as the cells do; at an electrode, the diffusion
|
|
1014
|
-
length $`\sqrt{2D/\omega}`$ at the highest frequency
|
|
1027
|
+
length $`\sqrt{2D/\omega}`$ at the highest frequency, and coarser than that, a port's window
|
|
1028
|
+
edges falling mid-cell moved a faradaic C 17%); and a double layer's capacitance wants cells
|
|
1029
|
+
well inside its Debye length (0.1 M NaCl: hmin λ_D/2.4 read C_dl 2.1% low, 0.1 nm 0.5%, 0.05 nm
|
|
1030
|
+
to 5e-4; the warning about unresolved double layers starts only at λ_D). `relError` is the
|
|
1031
|
+
round-off's, not the grid's. The result is `{ f, Z: { re, im },
|
|
1015
1032
|
relError, warnings }`: `relError[k]`, each Z's estimated error relative to $`|Z|`$ (from
|
|
1016
1033
|
round-off in the current and the solves' residuals, and cautious: it can exceed the true error
|
|
1017
1034
|
many times), and the warnings, the steady state's and one where a Z isn't resolved (an error
|
|
@@ -1022,7 +1039,10 @@ const now = dev.solution(); // snapshot of the current state
|
|
|
1022
1039
|
terminal is driven by a current.
|
|
1023
1040
|
- `set(patch)` merges plain objects deeply (arrays are replaced), except that a contact given `V`
|
|
1024
1041
|
or `I` drops the other (and given `I`, any series `R`): `{ contacts: { right: { I: 0 } } }`
|
|
1025
|
-
switches it to open circuit, whether it was held or behind a resistance.
|
|
1042
|
+
switches it to open circuit, whether it was held or behind a resistance. Given `V` alone, a terminal
|
|
1043
|
+
behind a resistance stays behind it: `{ V, R: undefined }` takes the resistance off (`R: 0` and
|
|
1044
|
+
`null` are refused, and `undefined` doesn't survive JSON, so a worker's patch says it this way in
|
|
1045
|
+
JavaScript). A patch that changes only the
|
|
1026
1046
|
terminals' drives (`V`, `I`, `R`) updates them in place, cheaply, keeping everything else.
|
|
1027
1047
|
Otherwise the device is rebuilt: the current state carries over while the species, regions
|
|
1028
1048
|
and terminals are unchanged (on a new grid, interpolated onto its nodes, so a floating gate
|
package/docs/numerics.md
CHANGED
|
@@ -661,10 +661,16 @@ About a steady state, a small sinusoidal excitation at one terminal gives, to fi
|
|
|
661
661
|
$`v \cdot K`$, displacement), and likewise $`C`$ and $`C'`$ for the terminal currents. The
|
|
662
662
|
time-derivative parts are read off two assemblies, $`J(dt) = J + M/dt`$, with a tiny $`dt`$ so that
|
|
663
663
|
the subtraction loses nothing.
|
|
664
|
-
- **At the measured terminal $`T`$,**
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
664
|
+
- **At the measured terminal $`T`$,** its voltage is perturbed ($`\delta V_T = 1`$) and
|
|
665
|
+
$`\delta I_T`$ read, held or driven by a current alike: $`Z = \delta V_T/\delta I_T`$ is its input
|
|
666
|
+
impedance either way, with $`I`$ into the device. (Solving a driven one's $`\delta V_T`$ for a unit
|
|
667
|
+
current instead read its current through its own circuit row, at the contact alone: a reverse
|
|
668
|
+
diode's 4.3% off $`dV/dI`$.) The other terminals keep their drives: held ones at AC ground,
|
|
669
|
+
driven ones open. A floating one's circuit row reads its current where that reading's error is
|
|
670
|
+
least: at its contact, or at any cut between it and a port's window, the same current (through a
|
|
671
|
+
metal it's the round-off of $`G\,\Delta\eta`$); and where every terminal but $`T`$ floats, the
|
|
672
|
+
response to $`T`$ with them held (nearly every level shifted alike, its current a difference of
|
|
673
|
+
large terms) is read through the identity that shifting every terminal alike passes nothing.
|
|
668
674
|
|
|
669
675
|
Each frequency costs one complex block-Thomas factorisation (rows scaled by their largest
|
|
670
676
|
entry), plus one back-substitution per floating terminal for the bordering. Conserved (blocked)
|
package/docs/reliability.md
CHANGED
|
@@ -97,7 +97,7 @@ recipes.
|
|
|
97
97
|
|
|
98
98
|
The validation suite shows driftlet is right where it converges. Whether it converges, on
|
|
99
99
|
devices nobody tuned it for, is what `npm run stress` tests. It builds random but plausible
|
|
100
|
-
devices in
|
|
100
|
+
devices in six families:
|
|
101
101
|
|
|
102
102
|
| Family | What's drawn |
|
|
103
103
|
|---|---|
|
|
@@ -105,6 +105,7 @@ devices in five families:
|
|
|
105
105
|
| MOS capacitors | Si, Ge or GaAs, doped 1e15–1e18 cm⁻³; oxide 1–20 nm; Au, Al or Pt gate at any flat-band offset; with or without generation |
|
|
106
106
|
| Electrolyte cells | one to three salts from the data library's ions, 0.1 mM–1 M, 1 µm–1 mm; double layers resolved or strictly neutral; bath, reversible or blocking electrodes |
|
|
107
107
|
| Electrodes | Ag, Cu or Zn deposition, or Fe³⁺/Fe²⁺ on platinum, with Butler–Volmer kinetics over four decades of k₀; with or without a supporting salt; against a second electrode or a bath |
|
|
108
|
+
| Reacting solutions | complexations (Na⁺ + A⁻ = NaA, B + A⁻ = BA⁻, …) in 2–3 regions whose faces block some species, so conserved combinations overlap; against a bath, another bath, closed or a K⁺ electrode; sometimes an electrode at no current reducing a closed M⁺ |
|
|
108
109
|
| Liquid junctions | two mixed solutions, 1 µM–1 M, sharp or graded, at open circuit |
|
|
109
110
|
|
|
110
111
|
Each device goes through what a user would do with it: a cold solve at no bias, and one at a
|
|
@@ -114,26 +115,30 @@ after a voltage step; and the impedance about equilibrium, from far below its sl
|
|
|
114
115
|
to 1 GHz. Every result is judged by `check()`, and by what must hold whatever the device:
|
|
115
116
|
levels flat at equilibrium with no current, warm and cold solves agreeing, the current driven
|
|
116
117
|
being the one passed, the impedance passive (to 1e-3 of |Z|) and, at low frequency, equal to
|
|
117
|
-
the steady dI/dV, a MOS capacitance never above its oxide's, and its gate charge with the back
|
|
118
|
+
the steady dI/dV where it says it's resolved (`relError` under 1%), a MOS capacitance never above its oxide's, and its gate charge with the back
|
|
118
119
|
contact moved what it is with the gate moved the other way. Each case is seeded from its family and index, so
|
|
119
120
|
any failure reruns alone (`npm run stress -- semi 79`).
|
|
120
121
|
|
|
121
|
-
At 500 devices per family (about
|
|
122
|
+
At 500 devices per family (about five minutes on a desktop), 16,379 of 16,381 scenarios pass:
|
|
122
123
|
|
|
123
|
-
| Family | Cold at 0 V | Cold at bias | Warm sweep | Open circuit, current or back contact | Transient | Impedance |
|
|
124
|
-
|
|
125
|
-
| Semiconductor stacks | 500/500 | 500/500 | 500/500 | 105/105 | 500/500 | 395/395 |
|
|
126
|
-
| MOS capacitors | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 |
|
|
127
|
-
| Electrolyte cells | 500/500 | 500/500 | 500/500 | | 500/500 | 381/381 |
|
|
128
|
-
| Electrodes | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 |
|
|
129
|
-
|
|
|
124
|
+
| Family | Cold at 0 V | Cold at bias | Species reversed | Warm sweep | Open circuit, current or back contact | Transient | Impedance |
|
|
125
|
+
|---|---|---|---|---|---|---|---|
|
|
126
|
+
| Semiconductor stacks | 500/500 | 500/500 | 500/500 | 499/500 | 105/105 | 500/500 | 395/395 |
|
|
127
|
+
| MOS capacitors | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 |
|
|
128
|
+
| Electrolyte cells | 500/500 | 500/500 | 500/500 | 500/500 | | 500/500 | 381/381 |
|
|
129
|
+
| Electrodes | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 | 500/500 |
|
|
130
|
+
| Reacting solutions | 500/500 | 500/500 | 500/500 | 500/500 | | 500/500 | |
|
|
131
|
+
| Liquid junctions | | | | | 500/500 | 499/500 | |
|
|
130
132
|
|
|
131
133
|
(Impedance only where the device has an equilibrium to linearise about; open circuit only for
|
|
132
134
|
lit cells.) The MOS back contact came last: before steady solves held a group of reacting
|
|
133
135
|
carriers flat where it reaches one contact alone, 43 of the 500 failed it, 5 of them silently
|
|
134
|
-
(a gate charge up to 2× off).
|
|
135
|
-
coarse for its double layers
|
|
136
|
-
|
|
136
|
+
(a gate charge up to 2× off). Of the two that fail, one is among the known limits below, a junction on a grid far too
|
|
137
|
+
coarse for its double layers; the other is a diode whose current, a small difference of large fluxes, a warm sweep
|
|
138
|
+
reaches to 1.8e-6 of what a cold solve does, against the 1e-6 the check asks: Newton's tolerance, not the physics
|
|
139
|
+
(its own current varies 2.8e-6 along the device, its balances converged).
|
|
140
|
+
`bench/stress.json` keeps the summary, and CI runs `npm run stress -- --check` against it: any
|
|
141
|
+
scenario failing that passes there fails the build.
|
|
137
142
|
|
|
138
143
|
Most of what driftlet's numerics do differently began as a failure here: the impedance's
|
|
139
144
|
current read across the quietest cut of the device rather than at the contact, GMRES judged by
|
package/docs/statistics.md
CHANGED
|
@@ -73,6 +73,19 @@ of exponentially. With one species it's Langmuir filling of sites. The dilute re
|
|
|
73
73
|
$`c_{\mathrm{ref}}`$ is kept. To use the half-filling convention of site models
|
|
74
74
|
($`e^\zeta = x/(1-x)`$), set `cRef` equal to `cMax`.
|
|
75
75
|
|
|
76
|
+
`cMax` from an ion's size: a cube of side $`a`$ per site gives $`c_{\mathrm{max}} = 1/(N_A a^3)`$, so
|
|
77
|
+
$`a`$ = 0.66 nm (a hydrated ion, about) is 5.8 M, and 0.5 nm is 13 M. The model counts sites only:
|
|
78
|
+
D falling with concentration, solvent counterflow and ion pairing are left out. On a material from
|
|
79
|
+
`aqueous()`, add it to the material it returns:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import { AVOGADRO } from 'driftlet';
|
|
83
|
+
import { aqueous } from 'driftlet/kit';
|
|
84
|
+
|
|
85
|
+
const aq = aqueous(['Cu2+', 'SO42-', 'H+']);
|
|
86
|
+
aq.materials.water.statistics = [{ type: 'lattice', species: ['Cu2+', 'SO42-', 'H+'], cMax: 1 / (AVOGADRO * 0.66e-9 ** 3) }];
|
|
87
|
+
```
|
|
88
|
+
|
|
76
89
|
### Redlich–Kister
|
|
77
90
|
|
|
78
91
|
One species on its own $`c_{\mathrm{max}}`$ sites, with the excess Gibbs energy of mixing per site
|
package/llms.txt
CHANGED
|
@@ -30,9 +30,9 @@ materials, insertion hosts), data.md and reliability.md (what's validated, and t
|
|
|
30
30
|
From a CDN, pin the version you tested against:
|
|
31
31
|
|
|
32
32
|
```js nocheck
|
|
33
|
-
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.
|
|
34
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.
|
|
35
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.
|
|
33
|
+
import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.2/src/index.js';
|
|
34
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.2/src/kit.js';
|
|
35
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.2/src/plot.js';
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`. Every export is listed at the end.
|
|
@@ -219,9 +219,9 @@ a CDN needs a long virtual-time budget, 30 s.)
|
|
|
219
219
|
<span id="readout"></span>
|
|
220
220
|
<div id="figure"></div>
|
|
221
221
|
<script type="module">
|
|
222
|
-
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.
|
|
223
|
-
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.
|
|
224
|
-
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.
|
|
222
|
+
import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.2/src/index.js';
|
|
223
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.2/src/kit.js';
|
|
224
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.2/src/plot.js';
|
|
225
225
|
|
|
226
226
|
const def = build({
|
|
227
227
|
T: 300,
|
|
@@ -317,6 +317,8 @@ const mos = (Vg, zeroCharge) =>
|
|
|
317
317
|
layer('Si', units.um(1), { name: 'Si', acceptors: NA }),
|
|
318
318
|
ohmic(0),
|
|
319
319
|
],
|
|
320
|
+
// The inversion layer's supply: with it, the low-frequency C–V at every frequency, even
|
|
321
|
+
// impedance at 1 MHz. Leave it out for the high-frequency curve (see below the code).
|
|
320
322
|
ports: [{ name: 'channel', region: 'Si', from: 0, to: units.nm(1), V: 0, terminal: 'e-', species: { 'e-': 'equilibrium' } }],
|
|
321
323
|
grid: { hmin: units.nm(0.25), hmax: units.nm(50), ratio: 1.15 },
|
|
322
324
|
});
|
|
@@ -383,8 +385,9 @@ electrons' V minus φ in the bulk (with the library's μ°), and a Cl⁻ termina
|
|
|
383
385
|
that, so an electrode held at 0 V beside it is strongly anodic: `describe(def)` says where SHE
|
|
384
386
|
sits, and an offset of that much (`bath(c, 'Cl-', 0, { offset })`) makes the terminal read SHE.
|
|
385
387
|
The working electrode's current is positive when oxidising. The layer's resistance is in series
|
|
386
|
-
(uncompensated IR: keep it thin, or the supporting salt strong); a 100:1 excess of salt
|
|
387
|
-
leaves the limiting current about 1% off pure diffusion
|
|
388
|
+
(uncompensated IR: keep it thin, or the supporting salt strong); a 100:1 excess of a 1:1 salt
|
|
389
|
+
still leaves the limiting current about 1% off pure diffusion, and migration counts for more with
|
|
390
|
+
more charge: Cu²⁺ in 1 M H₂SO₄ (20:1) reads 3% above it, with no acid 2×.
|
|
388
391
|
|
|
389
392
|
```js
|
|
390
393
|
import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driftlet",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Dependency-free 1D drift–diffusion–reaction (Poisson–Nernst–Planck) solver for semiconductors, electrochemistry and nerves, fast enough for live web demos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/describe.js
CHANGED
|
@@ -114,6 +114,8 @@ export function unitWarnings(def) {
|
|
|
114
114
|
if (!isObject(p)) return;
|
|
115
115
|
capacitance(`ports[${k}].capacitance.C`, p.capacitance?.C);
|
|
116
116
|
kinetics(`ports[${k}]`, p.reactions);
|
|
117
|
+
// (an area per volume typed in cm²/cm³ is 100 times too small: a mesh at 10 cm²/cm³ went by)
|
|
118
|
+
if (typeof p.area === 'number' && p.area > 0 && p.area < 100) warn(`ports[${k}].area`, `${num(p.area)} m²/m³ is under 1 cm² of electrode per cm³; area is m²/m³ (1 cm²/cm³ = 100 m²/m³)`);
|
|
117
119
|
});
|
|
118
120
|
for (const side of ['left', 'right']) {
|
|
119
121
|
const c = def.contacts?.[side];
|
package/src/impedance.js
CHANGED
|
@@ -22,33 +22,44 @@
|
|
|
22
22
|
// → { f, Z: { re, im }, relError, profiles? }, Z in Ω·m² (Ω for a non-planar device) at each
|
|
23
23
|
// frequency.
|
|
24
24
|
// Throws a SolverError where the terminal doesn't exist, has a series resistance, or every
|
|
25
|
-
// other terminal is driven by a current (no return path: Z infinite),
|
|
26
|
-
// isn't positive and finite.
|
|
25
|
+
// other terminal is driven by a current (no return path: Z infinite), where a frequency
|
|
26
|
+
// isn't positive and finite, or where the linearised system is singular at one.
|
|
27
27
|
// Linearised about the present state: (J + iωS)·δx + Σ_k B_k δV_k = 0 for the grid's rows and,
|
|
28
28
|
// for each floating terminal k (driven by a current), (C_k + iωC′_k)·δx + (DI_k + iωDI′_k) δV_k
|
|
29
29
|
// + Σ_k2 (DX + iωDX′)_k,k2 δV_k2 = δI_k, with J the steady Jacobian (dt = ∞) and S, C′, DI′,
|
|
30
|
-
// DX′ the storage parts, (X(dt) − X(∞))·dt at dt = 1e-30.
|
|
30
|
+
// DX′ the storage parts, (X(dt) − X(∞))·dt at dt = 1e-30. The measured terminal is always
|
|
31
|
+
// excited by its voltage: driven by a current, it's measured as if held (δV = 1, Z = 1/δI, the
|
|
32
|
+
// same input impedance), its circuit row out of the small system, its current read as a held
|
|
33
|
+
// one's (with its circuit law's change where it has one); its profiles then scaled by Z, per
|
|
34
|
+
// unit current. A floating terminal that alone feeds
|
|
31
35
|
// a conserved combination (a circuit law: a constraint or time law with its circuit row) has
|
|
32
36
|
// its current read by that combination's change at every frequency, iω F δS/λ plus what a
|
|
33
|
-
// resistance passes: at dt = 1e-30 every circuit law
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// (AC open).
|
|
37
|
+
// resistance passes: at dt = 1e-30 every circuit law with a current to count (λ set) reads by
|
|
38
|
+
// the change (this.lawForm), and
|
|
39
|
+
// its steady parts are C = 0, DI = 1/R (0 driven by a current), DX = 0. δI is the measured
|
|
40
|
+
// terminal's current into the device. Every other terminal keeps its drive: held, δV = 0 (AC
|
|
41
|
+
// ground); driven, δI = 0 (AC open).
|
|
38
42
|
// Each frequency: J + iωS with each row divided by its largest entry, factorised, preconditions
|
|
39
43
|
// GMRES (m 12, 8 restarts, tol 1e-8; its best iterate kept) on the exact J·v (difference form).
|
|
40
|
-
//
|
|
44
|
+
// A floating contact otherwise reads its current, in its circuit row, at the reading of least
|
|
45
|
+
// error by the solved responses (its own, or any cut between it and a port's window: the same
|
|
46
|
+
// current); and where every other terminal floats, a reading with no direct V dependence (a
|
|
47
|
+
// cut's, a law's change) reads y as Σ X_b, every terminal's uniform shift passing nothing.
|
|
48
|
+
// Solved: y, the response with the floating terminals held (to the measured one's δV = 1),
|
|
41
49
|
// and X_k, to each floating terminal's unit δV_k; then the floating terminals' rows, a small
|
|
42
50
|
// complex system, for their δV; δx = y − Σ X_k δV_k.
|
|
43
51
|
// The current read (held terminal): DI + iωDI′ + Σ (DX + iωDX′) δV + (C + iωC′)·δx, its error
|
|
44
|
-
// estimated from its terms' round-off
|
|
52
|
+
// estimated from its terms' round-off, the solves' residuals, and each unknown's round-off in
|
|
53
|
+
// its own row, ε Σ_k |A_jk x_k| / |A_jj| (a metal's current unknown, G Δη to a huge G: a
|
|
54
|
+
// bound, cautious where the coupled system pins it better); for a contact, also across
|
|
45
55
|
// each cut (_currentCuts: a segment's linearised total current, conduction and displacement,
|
|
46
56
|
// between that contact and the nearest port's window where there are ports), the reading with
|
|
47
57
|
// the least error taken; and where every other terminal floats, by Kirchhoff, Σ δV_k/R_k over
|
|
48
58
|
// those behind a resistance, its error (ε + the solves' residual)·|δV_k|/R_k. With no floating
|
|
49
59
|
// terminal, cuts, and that error over 1% of the current:
|
|
50
60
|
// the response split into each region's uniform shift (_regionShift) and a remainder solved to
|
|
51
|
-
// GMRES's floor, the shift's effect taken only at the rows it can touch (_regionEdgeRows
|
|
61
|
+
// GMRES's floor, the shift's effect taken only at the rows it can touch (_regionEdgeRows; a
|
|
62
|
+
// metal's carriers shifted, its current unknown not), and
|
|
52
63
|
// that reading kept where its error is less.
|
|
53
64
|
// relError: per frequency, the chosen reading's error estimate over |δI| (held); driven, the
|
|
54
65
|
// same estimate of its unit current read back from δx. Cautious: it can exceed the true error
|
|
@@ -75,8 +86,7 @@
|
|
|
75
86
|
// Also written, left as the last assembly at dt = ∞ left them: this.sourceTime (the present
|
|
76
87
|
// time), cOld, thOld, th0Old, gateOld (the present state's), contactDStart, portQStart, the
|
|
77
88
|
// residual, the Jacobian and the terminal terms, sysRest and the concentrations; this.lawForm
|
|
78
|
-
// (every circuit law by the change, until a step sets it afresh).
|
|
79
|
-
// node's slot 0 as φ̂, a metal's flux unknown too: harmless while a metal region is one cell.
|
|
89
|
+
// (every circuit law by the change, until a step sets it afresh).
|
|
80
90
|
// Tests: test/impedance.test.js (Macdonald's blocking electrodes, a junction's depletion
|
|
81
91
|
// capacitance, a MOS capacitor at both terminals, ports, profiles, Fermi–Dirac against
|
|
82
92
|
// Boltzmann); the demos' EIS.
|
|
@@ -218,8 +228,8 @@ export class SmallSignal {
|
|
|
218
228
|
* gives (J + iωM)·δx + Σ B_k δV_k = rhs for the grid's rows, and (C_k + iωC′_k)·δx +
|
|
219
229
|
* (∂I_k/∂V_k)·δV_k = δI_set,k for each floating terminal's circuit row, with J the steady
|
|
220
230
|
* Jacobian and M the storage part (from J(dt) = J + M/dt), B_k = ∂res/∂V_k and C_k = ∂I_k/∂x.
|
|
221
|
-
* At the measured terminal,
|
|
222
|
-
*
|
|
231
|
+
* At the measured terminal, its voltage is perturbed (rhs = −B δV) and its current read, held
|
|
232
|
+
* or driven by a current alike (one input impedance; see the contract). The other terminals
|
|
223
233
|
* keep their drives: held ones at AC ground, driven ones at AC open circuit.
|
|
224
234
|
* Z = δV/δI with I into the device (positive real part for a passive device).
|
|
225
235
|
* @param {ArrayLike<number>} frequencies Hz
|
|
@@ -249,8 +259,14 @@ export class SmallSignal {
|
|
|
249
259
|
`its impedance is infinite. Measure at one of those instead (impedance(f, { terminal: '${others[0].name}' }), its voltage against '${terminal}'), or hold one of them at a voltage`,
|
|
250
260
|
);
|
|
251
261
|
}
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
// Z is the device's input impedance at kT, the same whether its voltage is perturbed and its
|
|
263
|
+
// current read or the other way round. A terminal driven by a current is measured as if held
|
|
264
|
+
// (δV = 1, Z = 1/δI) to read that current as a held one's is (cuts, Kirchhoff, a circuit law's
|
|
265
|
+
// change): read through its own circuit row instead, at its contact alone, it was a metal's or
|
|
266
|
+
// a barrier's round-off (a reverse-biased diode's contact 4.3% off dV/dI; a Schottky
|
|
267
|
+
// contact's Re Z −32 for 1282), with an error estimate that never saw it.
|
|
268
|
+
const N = nB * M, fl = this.floating.filter((k) => k !== kT), K = fl.length;
|
|
269
|
+
const kirchhoff = others.every((t) => t.drive.kind === 'I' || t.drive.R > 0);
|
|
254
270
|
this.sourceTime = this.time;
|
|
255
271
|
this.computeConcentrations();
|
|
256
272
|
this.cOld.set(this.c);
|
|
@@ -271,11 +287,14 @@ export class SmallSignal {
|
|
|
271
287
|
// flux at dt = ∞ and by the change at dt = 1e-30, a floating gate's current was counted
|
|
272
288
|
// twice (its capacitance 2× dQ/dV); read by the flux throughout, through a metal it is the
|
|
273
289
|
// round-off of G Δη (a gate behind 1 kΩ·m², measured at the back contact: Re Z = −2.7).
|
|
274
|
-
|
|
290
|
+
// (A law that counts no current, λ unset, as for a terminal at no current that feeds
|
|
291
|
+
// nothing, can't be read by its change: forced to, a closed Fe³⁺/Fe²⁺ cell's Z was NaN.)
|
|
292
|
+
const byChange = (cs) => cs.circuit >= 0 && !(cs.contacts?.length > 0) && Number.isFinite(cs.lambda) && cs.lambda !== 0;
|
|
293
|
+
this.lawForm = new Map([...this.constraints, ...this.timeLaws].map((cs) => [cs, byChange(cs)]));
|
|
275
294
|
this.assemble(dts);
|
|
276
295
|
for (const cs of [...this.constraints, ...this.timeLaws]) {
|
|
277
296
|
const k = cs.circuit;
|
|
278
|
-
if (
|
|
297
|
+
if (!byChange(cs) || !(fl.includes(k) || (k === kT && dT.kind === 'I'))) continue;
|
|
279
298
|
Ct[k].fill(0);
|
|
280
299
|
DI[k] = this.terms[k].drive.R > 0 ? 1 / this.terms[k].drive.R : 0;
|
|
281
300
|
DX[k].fill(0);
|
|
@@ -288,11 +307,31 @@ export class SmallSignal {
|
|
|
288
307
|
// Where the measured contact's current may be read across a segment: anywhere without
|
|
289
308
|
// ports; with them, only between it and the nearest window (a port's exchange takes current
|
|
290
309
|
// in or out along it). Never for a port's own terminal.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
310
|
+
const allCuts = this._currentCuts(), windows = this.model.ports.flatMap((p) => Array.from(p.nodes));
|
|
311
|
+
const lo = windows.length ? Math.min(...windows) : Infinity, hi = windows.length ? Math.max(...windows) : -Infinity;
|
|
312
|
+
const cutsOf = (k) => allCuts.filter((cut) => (k === 0 ? cut.s + 1 <= lo : cut.s >= hi));
|
|
313
|
+
const cuts = kT < 2 ? cutsOf(kT) : null;
|
|
314
|
+
// A floating contact on a metal: its circuit row's current through the metal is the current
|
|
315
|
+
// unknown G Δη, round-off to a huge G (a Pt electrode behind 3000 Ω·m², read at the bath: Z −
|
|
316
|
+
// R came out 70.5 for 3070.5). Any cut between it and a port's window carries the same current
|
|
317
|
+
// (a metal stores none; the total current is continuous through its face), so each frequency
|
|
318
|
+
// reads it where its error, by the solved responses, is least (see readings, below).
|
|
319
|
+
const lawRead = new Set([...this.constraints, ...this.timeLaws].filter(byChange).map((cs) => cs.circuit));
|
|
320
|
+
const choices = new Map(); // k → [{ c, cs, di, dis, dx, dxs }]: its own reading, then each cut's
|
|
321
|
+
for (const k of fl) {
|
|
322
|
+
if (k > 1 || lawRead.has(k)) continue;
|
|
323
|
+
const own = { c: Ct[k].slice(), cs: Cs[k].slice(), di: DI[k], dis: DIs[k], dx: DX[k].slice(), dxs: DXs[k].slice() }, list = [own];
|
|
324
|
+
const sgn = k === 0 ? 1 : -1, R = this.terms[k].drive.R;
|
|
325
|
+
for (const cut of cutsOf(k)) {
|
|
326
|
+
const c = new Float64Array(own.c.length), cs = new Float64Array(own.c.length);
|
|
327
|
+
cut.idx.forEach((x, a) => {
|
|
328
|
+
if (this.rix[x] >= this.sys.size) return;
|
|
329
|
+
c[this.rix[x]] += sgn * cut.g[a];
|
|
330
|
+
cs[this.rix[x]] += sgn * cut.d[a];
|
|
331
|
+
});
|
|
332
|
+
list.push({ c, cs, di: R > 0 ? 1 / R : 0, dis: 0, dx: new Float64Array(own.dx.length), dxs: new Float64Array(own.dx.length) });
|
|
333
|
+
}
|
|
334
|
+
if (list.length > 1) choices.set(k, list);
|
|
296
335
|
}
|
|
297
336
|
// J·v for GMRES, exact: the assembled J holds a flux's dependence on η_L and η_R as two
|
|
298
337
|
// entries, and where both are huge (an inversion layer) and v nearly uniform, J·v loses the
|
|
@@ -348,7 +387,12 @@ export class SmallSignal {
|
|
|
348
387
|
}
|
|
349
388
|
}
|
|
350
389
|
}
|
|
351
|
-
|
|
390
|
+
try {
|
|
391
|
+
csys.factor();
|
|
392
|
+
} catch (err) {
|
|
393
|
+
// (it threw a bare Error, its block number all it said: a cell 2.8 V off its pzc)
|
|
394
|
+
throw new SolverError(`impedance: the linearised equations are singular at ${f} Hz (${err.message}): some part of the device is held by nothing at that frequency, as a double layer far from where it balances can leave it. See the steady state's warnings (solve()).`);
|
|
395
|
+
}
|
|
352
396
|
const sx = (X, vr, out) => {
|
|
353
397
|
for (let blk = 0; blk < nB; blk++) {
|
|
354
398
|
const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
|
|
@@ -404,24 +448,77 @@ export class SmallSignal {
|
|
|
404
448
|
};
|
|
405
449
|
let accuracy = 1e-14; // (relative, of the solves: the readouts' error is weighed with it)
|
|
406
450
|
// y: the response with the floating terminals held; X_k: to a unit δV_k.
|
|
407
|
-
const heldT = dT.kind === 'V';
|
|
408
451
|
for (let j = 0; j < Nc; j++) {
|
|
409
|
-
rr[j] =
|
|
452
|
+
rr[j] = -Bt[kT][j] * scale[j];
|
|
410
453
|
ri[j] = 0;
|
|
411
454
|
}
|
|
412
|
-
solve(rr, ri, yr, yi,
|
|
455
|
+
solve(rr, ri, yr, yi, kT, 1);
|
|
413
456
|
fl.forEach((k, a) => {
|
|
414
457
|
for (let j = 0; j < Nc; j++) rr[j] = Bt[k][j] * scale[j];
|
|
415
458
|
ri.fill(0);
|
|
416
459
|
solve(rr, ri, Xr[a], Xi[a], k, -1);
|
|
417
460
|
});
|
|
461
|
+
// Each unknown's round-off in its own row: |x_j| to ε Σ_k |A_jk x_k| / |A_jj|, A = J + iωS. A
|
|
462
|
+
// metal's current unknown, G Δη to a huge G, is known only to ε G |δη| this way, which the
|
|
463
|
+
// terms' round-off alone never saw (a platinum electrode's current read 20% off at 1 nHz,
|
|
464
|
+
// its estimate 1e-8).
|
|
465
|
+
const rowFloor = (xr0, xi0) => {
|
|
466
|
+
const out = new Float64Array(Nc);
|
|
467
|
+
for (let blk = 0; blk < nB; blk++) {
|
|
468
|
+
const m = sizes[blk], mp = blk > 0 ? sizes[blk - 1] : 0, mn = blk < nB - 1 ? sizes[blk + 1] : 0;
|
|
469
|
+
for (let r = 0; r < m; r++) {
|
|
470
|
+
let acc = 0;
|
|
471
|
+
const row = (X, o, len, x0) => {
|
|
472
|
+
for (let cc = 0; cc < len; cc++) acc += (Math.abs(J[X][o + cc]) + w * Math.abs(S[X][o + cc])) * Math.hypot(xr0[x0 + cc], xi0[x0 + cc]);
|
|
473
|
+
};
|
|
474
|
+
if (mp) row('A', offA[blk] + r * mp, mp, offX[blk - 1]);
|
|
475
|
+
row('B', offB[blk] + r * m, m, offX[blk]);
|
|
476
|
+
if (mn) row('C', offC[blk] + r * mn, mn, offX[blk + 1]);
|
|
477
|
+
const dIdx = offB[blk] + r * m + r, diag = Math.hypot(J.B[dIdx], w * S.B[dIdx]);
|
|
478
|
+
out[offX[blk] + r] = diag > 0 ? (1.1e-16 * acc) / diag : 0;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
};
|
|
483
|
+
// Each floating metal contact's reading, of those it can take, with the least error by the
|
|
484
|
+
// solved responses (their terms' round-off and each unknown's in its own row).
|
|
485
|
+
const gaugeRead = new Set(lawRead); // (readings with no direct dependence on any V: see y below)
|
|
486
|
+
if (choices.size) {
|
|
487
|
+
const eps = 1.1e-16, parts = [[yr, yi], ...fl.map((_, b) => [Xr[b], Xi[b]])];
|
|
488
|
+
const mag = new Float64Array(Nc), floor = new Float64Array(Nc);
|
|
489
|
+
for (const [pr, pi] of parts) {
|
|
490
|
+
const rf = rowFloor(pr, pi);
|
|
491
|
+
for (let j = 0; j < Nc; j++) {
|
|
492
|
+
mag[j] += Math.hypot(pr[j], pi[j]);
|
|
493
|
+
floor[j] += rf[j];
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const [k, list] of choices) {
|
|
497
|
+
let best = null, least = Infinity;
|
|
498
|
+
for (const r of list) {
|
|
499
|
+
let e = eps * (Math.abs(r.di) + w * Math.abs(r.dis));
|
|
500
|
+
for (let j = 0; j < Nc; j++) e += (Math.abs(r.c[j]) + w * Math.abs(r.cs[j])) * (eps * mag[j] + floor[j]);
|
|
501
|
+
if (e < least) [best, least] = [r, e];
|
|
502
|
+
}
|
|
503
|
+
if (best !== list[0]) gaugeRead.add(k);
|
|
504
|
+
Ct[k].set(best.c);
|
|
505
|
+
Cs[k].set(best.cs);
|
|
506
|
+
DI[k] = best.di;
|
|
507
|
+
DIs[k] = best.dis;
|
|
508
|
+
DX[k].set(best.dx);
|
|
509
|
+
DXs[k].set(best.dxs);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
418
512
|
// The floating terminals' circuit rows, a small complex system for their δV.
|
|
419
513
|
const Ar = Array.from({ length: K }, () => new Float64Array(K + 1)), Ai = Array.from({ length: K }, () => new Float64Array(K + 1));
|
|
420
514
|
fl.forEach((k, a) => {
|
|
515
|
+
let sre = 0, sim = 0;
|
|
421
516
|
for (let b = 0; b < K; b++) {
|
|
422
517
|
const [re, im] = dot(k, w, Xr[b], Xi[b]);
|
|
423
518
|
Ar[a][b] = -re;
|
|
424
519
|
Ai[a][b] = -im;
|
|
520
|
+
sre += re;
|
|
521
|
+
sim += im;
|
|
425
522
|
}
|
|
426
523
|
Ar[a][a] += DI[k];
|
|
427
524
|
Ai[a][a] += w * DIs[k];
|
|
@@ -432,9 +529,14 @@ export class SmallSignal {
|
|
|
432
529
|
Ar[a][b] += DX[k][k2];
|
|
433
530
|
Ai[a][b] += w * DXs[k][k2];
|
|
434
531
|
});
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
532
|
+
// Every terminal shifted alike moves nothing and passes no current, and y + Σ (−X_b) is
|
|
533
|
+
// that shift where kT and the floating terminals are all there are: so a reading with
|
|
534
|
+
// no direct dependence on a V reads y as Σ X_b, exactly, from responses it resolves, where
|
|
535
|
+
// y itself is nearly that shift and its current a difference of them (a Pt electrode
|
|
536
|
+
// behind 3000 Ω·m², at 1 nHz: its cuts read y 10% apart).
|
|
537
|
+
const [yre, yim] = kirchhoff && gaugeRead.has(k) ? [sre, sim] : dot(k, w, yr, yi);
|
|
538
|
+
Ar[a][K] = -yre - DX[k][kT];
|
|
539
|
+
Ai[a][K] = -yim - w * DXs[k][kT];
|
|
438
540
|
});
|
|
439
541
|
const dV = complexSolve(Ar, Ai, K);
|
|
440
542
|
// δx = y − Σ X_k δV_k
|
|
@@ -475,7 +577,8 @@ export class SmallSignal {
|
|
|
475
577
|
const [re, im] = dot(kT, w, p.r, p.i);
|
|
476
578
|
Ir += re;
|
|
477
579
|
Ii += im;
|
|
478
|
-
|
|
580
|
+
p.rf ??= rowFloor(p.r, p.i);
|
|
581
|
+
for (let j = 0; j < Nc; j++) err += (Math.abs(c[j]) + w * Math.abs(cs[j])) * (eps * Math.hypot(p.r[j], p.i[j]) + p.floor + p.rf[j]);
|
|
479
582
|
}
|
|
480
583
|
// Kirchhoff, where every other terminal floats: what they pass comes back through kT,
|
|
481
584
|
// δI = Σ δV_k/R_k over those behind a resistance (one driven by a current passes no small
|
|
@@ -503,7 +606,7 @@ export class SmallSignal {
|
|
|
503
606
|
// (g + iωd)(xr + i xi)
|
|
504
607
|
re += gk * p.fr[x] - dk * p.fi[x];
|
|
505
608
|
im += gk * p.fi[x] + dk * p.fr[x];
|
|
506
|
-
e += (Math.abs(gk) + Math.abs(dk)) * (eps * Math.hypot(p.fr[x], p.fi[x]) + p.floor);
|
|
609
|
+
e += (Math.abs(gk) + Math.abs(dk)) * (eps * Math.hypot(p.fr[x], p.fi[x]) + p.floor + (this.rix[x] < Nc ? p.rf[this.rix[x]] : 0));
|
|
507
610
|
}
|
|
508
611
|
}
|
|
509
612
|
if (e < err) [err, Ir, Ii] = [e, sgn * re, sgn * im];
|
|
@@ -524,7 +627,7 @@ export class SmallSignal {
|
|
|
524
627
|
return [fr, fi];
|
|
525
628
|
};
|
|
526
629
|
let Zr, Zi;
|
|
527
|
-
|
|
630
|
+
{
|
|
528
631
|
// δI per volt: (C + iωC′)·δx + (∂I/∂V)(1 + iω′) at the terminal…
|
|
529
632
|
let { Ir, Ii, err } = read([{ r: yr, i: yi, fr: xr, fi: xi, floor: accuracy * largest(yr, yi) }]);
|
|
530
633
|
// …and where the solves leave that unresolved (the estimate past 1% of the current), the
|
|
@@ -565,14 +668,10 @@ export class SmallSignal {
|
|
|
565
668
|
const d2 = Ir * Ir + Ii * Ii; // Z = 1/δI
|
|
566
669
|
Zr = Ir / d2;
|
|
567
670
|
Zi = -Ii / d2;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
Zr = dV.re[a];
|
|
573
|
-
Zi = dV.im[a];
|
|
574
|
-
const back = read([{ r: yr, i: yi, fr: xr, fi: xi, floor: accuracy * largest(yr, yi) }], [Zr, Zi]);
|
|
575
|
-
out.relError[q] = back.err / Math.hypot(back.Ir, back.Ii);
|
|
671
|
+
}
|
|
672
|
+
// (profiles per unit excitation: for a terminal driven by a current, per A/m², δx·Z)
|
|
673
|
+
if (profiles && dT.kind === 'I') {
|
|
674
|
+
for (let j = 0; j < N; j++) [xr[j], xi[j]] = [xr[j] * Zr - xi[j] * Zi, xr[j] * Zi + xi[j] * Zr];
|
|
576
675
|
}
|
|
577
676
|
out.Z.re[q] = Zr;
|
|
578
677
|
out.Z.im[q] = Zi;
|
|
@@ -590,9 +689,12 @@ export class SmallSignal {
|
|
|
590
689
|
for (let r = 0; r < model.regions.length; r++) {
|
|
591
690
|
let sr = 0, si = 0, k = 0;
|
|
592
691
|
const g0 = grid.regionStart[r], g1 = grid.regionEnd[r];
|
|
692
|
+
// (a metal's slot 0 is its segment's current, not φ̂: shifted, a platinum region of many
|
|
693
|
+
// cells read its electrode's current 8% off, at an error estimate of 1e-8)
|
|
694
|
+
const phiAt = (g) => this.nodeConductor[g] < 0 && loc[this.blockOfNode[g] * M] >= 0;
|
|
593
695
|
for (let g = g0; g <= g1; g++) {
|
|
594
696
|
const o = this.blockOfNode[g] * M;
|
|
595
|
-
if (
|
|
697
|
+
if (phiAt(g)) {
|
|
596
698
|
sr += yr[R[o]];
|
|
597
699
|
si += yi[R[o]];
|
|
598
700
|
k++;
|
|
@@ -614,7 +716,7 @@ export class SmallSignal {
|
|
|
614
716
|
si /= k;
|
|
615
717
|
for (let g = g0; g <= g1; g++) {
|
|
616
718
|
const o = this.blockOfNode[g] * M;
|
|
617
|
-
if (
|
|
719
|
+
if (phiAt(g)) {
|
|
618
720
|
er[R[o]] = sr;
|
|
619
721
|
ei[R[o]] = si;
|
|
620
722
|
}
|
package/src/index.js
CHANGED
|
@@ -132,6 +132,7 @@ export class Device {
|
|
|
132
132
|
solver.sourceTime = old.sourceTime;
|
|
133
133
|
solver._refreshSources();
|
|
134
134
|
solver.dtNext = old.dtNext;
|
|
135
|
+
solver.coldStart = old.coldStart; // (a carried state is as cold as it was: see equilibrateFaces)
|
|
135
136
|
solver.contactDEnd = old.contactDEnd;
|
|
136
137
|
if (old.portQEnd?.length === solver.portQ.length) solver.portQEnd = old.portQEnd;
|
|
137
138
|
solver.solvedV = old.solvedV; // where the carried-over state was solved (for continuation)
|
package/src/newton.js
CHANGED
|
@@ -75,7 +75,9 @@
|
|
|
75
75
|
// Also written: s.islandsOn (as asked, or on by 8); s.lostMode, s.accepted (7); s.delta, s.deltaV,
|
|
76
76
|
// s.rowDiv; what an assembly writes, at the returned state where a probe or 6's check ran;
|
|
77
77
|
// s.conditioning (a steady solve's failure: the worst cancellation a factorisation showed, for
|
|
78
|
-
// its message).
|
|
78
|
+
// its message); s.modelError, the message of a SolverError a statistics model threw at a state
|
|
79
|
+
// Newton reached (in the assembly, or computing the concentrations where it stops), which fails
|
|
80
|
+
// the call rather than leaving it (cleared by solveSteady and integrate, for their failures).
|
|
79
81
|
// Tests: every solve and step goes through here; test/newton.test.js (the islands round, the
|
|
80
82
|
// residual check's failure) and the MOS gate step in test/ports.test.js (a lost mode, to 1e-6);
|
|
81
83
|
// test/jacobian.test.js checks the system it solves. The devices that needed each branch are in
|
|
@@ -401,6 +403,19 @@ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands
|
|
|
401
403
|
};
|
|
402
404
|
// A steady solve that fails: record how nearly singular the system was, and where; or, failing
|
|
403
405
|
// with the islands' rows in, go back to the solution without them.
|
|
406
|
+
// The concentrations at the state Newton stops at, where a statistics model can refuse it (a
|
|
407
|
+
// Debye–Hückel solution past its range, at the converged state of a transient: the error was
|
|
408
|
+
// thrown from here, uncaught, out of solve() and advance()). Its message, for the failure.
|
|
409
|
+
const settle = () => {
|
|
410
|
+
try {
|
|
411
|
+
s.computeConcentrations();
|
|
412
|
+
return null;
|
|
413
|
+
} catch (err) {
|
|
414
|
+
if (!(err instanceof SolverError)) throw err;
|
|
415
|
+
s.modelError = err.message;
|
|
416
|
+
return err.message;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
404
419
|
const fail = (r) => {
|
|
405
420
|
if (kept) {
|
|
406
421
|
s.u.set(kept.u);
|
|
@@ -430,6 +445,7 @@ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands
|
|
|
430
445
|
} catch (err) {
|
|
431
446
|
// A statistics model can fail far from the solution (e.g. Debye–Hückel beyond its range).
|
|
432
447
|
if (!(err instanceof SolverError)) throw err;
|
|
448
|
+
s.modelError = err.message;
|
|
433
449
|
return fail({ converged: false, iterations: it, history, error: err.message });
|
|
434
450
|
}
|
|
435
451
|
equilibrate(s);
|
|
@@ -500,7 +516,8 @@ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands
|
|
|
500
516
|
const done = { converged: true, iterations: it, history, residual: rmax };
|
|
501
517
|
if (probed()) continue;
|
|
502
518
|
if (islandsOff(it, done)) continue;
|
|
503
|
-
|
|
519
|
+
const refused = settle();
|
|
520
|
+
if (refused) return fail({ converged: false, iterations: it, history, error: refused });
|
|
504
521
|
return done;
|
|
505
522
|
}
|
|
506
523
|
// Converged as far as round-off allows: the updates are already tiny (below 1e-6 thermal
|
|
@@ -523,12 +540,13 @@ export function newton(s, dt, { maxIter = 60, tol = 1e-10, maxStep = 10, islands
|
|
|
523
540
|
const done = { converged: true, iterations: it, history, residual: rmax, roundoff: true };
|
|
524
541
|
if (probed()) continue;
|
|
525
542
|
if (islandsOff(it, done)) continue;
|
|
526
|
-
|
|
543
|
+
const refused = settle();
|
|
544
|
+
if (refused) return fail({ converged: false, iterations: it, history, error: refused });
|
|
527
545
|
return done;
|
|
528
546
|
}
|
|
529
547
|
}
|
|
530
|
-
|
|
531
|
-
return fail({ converged: false, iterations: maxIter, history });
|
|
548
|
+
const refused = settle();
|
|
549
|
+
return fail({ converged: false, iterations: maxIter, history, ...(refused ? { error: refused } : {}) });
|
|
532
550
|
}
|
|
533
551
|
|
|
534
552
|
// How far the terminal currents (as last assembled) are from adding up to zero, relative to the
|
package/src/solution.js
CHANGED
|
@@ -311,6 +311,11 @@ export function makeSolution(solver, result = {}) {
|
|
|
311
311
|
`the solve did not converge from a start far from equilibrium: ${solver.violent.join('; ')}, its double layer uncharged. ` +
|
|
312
312
|
'Hold that terminal at a voltage first, solve(), then let it float (set its I: 0): it keeps the state the held one gave it.',
|
|
313
313
|
);
|
|
314
|
+
} else if (result.converged === false && !result.stopped && !un && !ch && !lost && slow.length === 0 && !(cond && cond.digits > 12) && solver.modelError) {
|
|
315
|
+
sol.warnings.push(
|
|
316
|
+
`the solve did not converge: the states it reached were refused by a statistics model: ${solver.modelError}. ` +
|
|
317
|
+
'The model doesn\'t hold there: a smaller concentration or drive, or a model for it (lattice for crowding).',
|
|
318
|
+
);
|
|
314
319
|
} else if (result.converged === false && !result.stopped && !un && !ch && !lost && slow.length === 0 && !(cond && cond.digits > 12)) {
|
|
315
320
|
sol.warnings.push(
|
|
316
321
|
'the solve did not converge, and nothing specific showed why. ' +
|
package/src/solver.js
CHANGED
|
@@ -291,9 +291,15 @@ export class Solver {
|
|
|
291
291
|
this.termDX = this.terms.map(() => new Float64Array(this.terms.length));
|
|
292
292
|
this.termRes = new Float64Array(this.terms.length);
|
|
293
293
|
this._refreshSources();
|
|
294
|
-
// A floating terminal starts level with the first held one (no current)
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
// A floating terminal starts level with the first held one (no current); where none is held,
|
|
295
|
+
// with the source behind the first resistance (it was NaN there, and a metal contact's
|
|
296
|
+
// carrier, its level from that V, asked for a c0 a conductor can't be given), else 0.
|
|
297
|
+
const heldK = this.terms.findIndex((_, k) => !this.floating.includes(k)), behindR = this.terms.find((t) => t.drive.R > 0);
|
|
298
|
+
const level = heldK >= 0 ? this.termV[heldK] : behindR ? sourceAt(behindR.drive.src, this.time) : 0;
|
|
299
|
+
// (One behind a resistance too: at its own source, a silver cell held at 0 V and 16 V behind R
|
|
300
|
+
// began with 16 V across it; left at 0 V, one behind a −30 V source beside a current began
|
|
301
|
+
// with 30 V across it. Level, the circuit rows move them from a device at rest.)
|
|
302
|
+
for (const k of this.floating) this.termV[k] = level;
|
|
297
303
|
// Accepted steps, most recent first: start time, size, start state (for BDF2 and the
|
|
298
304
|
// error estimate of adaptive stepping).
|
|
299
305
|
this.history = [];
|
|
@@ -830,6 +836,7 @@ export class Solver {
|
|
|
830
836
|
}
|
|
831
837
|
this.termV.set(src.termV);
|
|
832
838
|
this.solvedV = src.solvedV;
|
|
839
|
+
this.coldStart = src.coldStart;
|
|
833
840
|
this.time = src.time;
|
|
834
841
|
this.computeConcentrations();
|
|
835
842
|
}
|
|
@@ -855,6 +862,7 @@ export class Solver {
|
|
|
855
862
|
history: this.history.slice(),
|
|
856
863
|
termV: Float64Array.from(this.termV),
|
|
857
864
|
sourceTime: this.sourceTime,
|
|
865
|
+
coldStart: this.coldStart,
|
|
858
866
|
};
|
|
859
867
|
}
|
|
860
868
|
|
|
@@ -878,6 +886,7 @@ export class Solver {
|
|
|
878
886
|
this.history = s.history.slice();
|
|
879
887
|
this.termV.set(s.termV);
|
|
880
888
|
this.sourceTime = s.sourceTime;
|
|
889
|
+
this.coldStart = s.coldStart;
|
|
881
890
|
this.computeConcentrations();
|
|
882
891
|
}
|
|
883
892
|
|
package/src/start.js
CHANGED
|
@@ -27,18 +27,22 @@
|
|
|
27
27
|
// plus each face's dipole. In a region where a level-fixed charged species responds to φ̂,
|
|
28
28
|
// φ̂ is chosen for neutrality (a bisection; node by node where a profile varies);
|
|
29
29
|
// otherwise it's the running φ̂, or where a conductor on the left or an electrode spread
|
|
30
|
-
// through the region has a reaction that can balance, the φ̂ that balances it
|
|
30
|
+
// through the region has a reaction that can balance, the φ̂ that balances it (by bisection;
|
|
31
|
+
// a trial a non-ideal model refuses counts its ideal charge, which has the sign there). Where
|
|
31
32
|
// neutrality is out of reach (c0's amounts leave a charge no held level balances), the
|
|
32
33
|
// running φ̂, and a strictly neutral region is noted in s.unbalanced (for a transient that
|
|
33
34
|
// fails from it to say why). A strictly
|
|
34
35
|
// neutral region (ε = 0) with every charged species' amount given must have a neutral c0:
|
|
35
36
|
// an error otherwise. A non-ideal material's composition comes from its statistics
|
|
36
37
|
// (materialAt).
|
|
37
|
-
// 2.
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
38
|
+
// 2. Marks the state cold (s.coldStart). equilibrateFaces(s), which the first time step from it
|
|
39
|
+
// calls (src/timeSteps.js) and a steady solve doesn't: a face between two strictly neutral
|
|
40
|
+
// ideal regions, its φ type neutral, whose links hold some charged species in equilibrium,
|
|
41
|
+
// its two nodes set to that equilibrium, its t = 0⁺ (each passing species' μ̄ equal across
|
|
42
|
+
// it, the left node neutral, each passing species' amount over the two nodes kept; the jump
|
|
43
|
+
// in φ̂ between them the one unknown), everything else at them unchanged in concentration;
|
|
44
|
+
// s.coldStart false. A solve that converges, a warm start and a carried state set it as
|
|
45
|
+
// their state is.
|
|
42
46
|
// 3. An insertion host (a φ-free material, not a conductor): its φ̂ and every η shifted by z·Δ so that its first
|
|
43
47
|
// charged species shared with a neighbour across a face starts at that neighbour's level.
|
|
44
48
|
// 4. A floating contact facing a held one: V = the held V + the difference between the two
|
|
@@ -55,8 +59,8 @@
|
|
|
55
59
|
// pass the current it's driven at, or behind a resistance (V_src − V)/R, by bisection.
|
|
56
60
|
//
|
|
57
61
|
// Invariants (each says what a reviewer should be able to check here):
|
|
58
|
-
// 1. A species given by its amount starts at c0 (or its profile) at every node
|
|
59
|
-
//
|
|
62
|
+
// 1. A species given by its amount starts at c0 (or its profile) at every node; at a face
|
|
63
|
+
// equilibrated by step 2 (a transient's first step), its amount over the two nodes is kept.
|
|
60
64
|
// 2. A species given by its level starts at the level of what feeds it, as step 1 orders them.
|
|
61
65
|
// 3. Where a level-fixed charged species responds, each node starts neutral (Σ z c + the fixed
|
|
62
66
|
// charge = 0) to the bisection's tolerance, unless neutrality is out of reach (step 1); a
|
|
@@ -65,7 +69,7 @@
|
|
|
65
69
|
// z·Δ), step 2 moves the passing species between a face's two nodes only.
|
|
66
70
|
// 5. Each floating terminal's V is set by the last of steps 4, 6, 7, 9 that applies to it;
|
|
67
71
|
// held terminals' V are untouched.
|
|
68
|
-
// 6. Only u, uLo, termV, s.unbalanced and what computeConcentrations derives are written; the
|
|
72
|
+
// 6. Only u, uLo, termV, s.unbalanced, s.coldStart and what computeConcentrations derives are written; the
|
|
69
73
|
// result depends on the model, every terminal's voltage and s.time, deterministically.
|
|
70
74
|
// Known, and not an invariant (a cold review; see ROADMAP): the gauge (φ̂ and each η by z·Δ)
|
|
71
75
|
// runs from the left, so behind a closed or floating left end the start sits away from a right
|
|
@@ -163,10 +167,22 @@ export function initFromComposition(s) {
|
|
|
163
167
|
const zeta = new Float64Array(n), cc = new Float64Array(n);
|
|
164
168
|
const charge = (ph) => {
|
|
165
169
|
if (!mat.ideal) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
// A model can refuse a trial φ̂ far out, where one ion is many orders denser than the rest
|
|
171
|
+
// (Debye–Hückel's limiting law threw out of the start, for KCl at 10 mM, its neutral φ̂
|
|
172
|
+
// 57 units from where the search began): there the ideal charge has the sign, and the
|
|
173
|
+
// search goes on by it.
|
|
174
|
+
let refused = false;
|
|
175
|
+
try {
|
|
176
|
+
materialAt(s, mat, reg.background, eta, mode, cFix, ph, zeta, cc);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (!(err instanceof SolverError)) throw err;
|
|
179
|
+
refused = true;
|
|
180
|
+
}
|
|
181
|
+
if (!refused) {
|
|
182
|
+
let q = reg.fixedCharge / FARADAY;
|
|
183
|
+
for (let i = 0; i < n; i++) if (mat.present[i]) q += z[i] * cc[i];
|
|
184
|
+
return q;
|
|
185
|
+
}
|
|
170
186
|
}
|
|
171
187
|
let q = reg.fixedCharge / FARADAY;
|
|
172
188
|
for (let i = 0; i < n; i++) {
|
|
@@ -279,53 +295,6 @@ export function initFromComposition(s) {
|
|
|
279
295
|
}
|
|
280
296
|
}
|
|
281
297
|
}
|
|
282
|
-
// Two strictly neutral regions meeting at a face whose links hold ions in equilibrium: where
|
|
283
|
-
// c0 leaves the two sides out of it (a different fixed charge or immobile ion on each, so
|
|
284
|
-
// c₊c₋ differs across it), the face reaches it at t = 0⁺, before anything diffuses, and a
|
|
285
|
-
// first step made to take that jump met an exactly singular pivot. Start there: the ions the
|
|
286
|
-
// face passes shared between its two nodes in equilibrium (μ̄ equal across it), each one's
|
|
287
|
-
// amount over the two nodes kept, everything else as c0 has it. The one unknown is the jump
|
|
288
|
-
// in φ̂ across it (a Donnan potential), set by neutrality on one side; the other's follows,
|
|
289
|
-
// the two nodes' charge summing to what it was, zero.
|
|
290
|
-
for (let f = 0; f < interfaces.length; f++) {
|
|
291
|
-
const itf = interfaces[f], mL = materials[regions[f].material], mR = materials[regions[f + 1].material];
|
|
292
|
-
const strict = (m) => m.epsr === 0 && !m.conductor && !m.phiFree && m.ideal;
|
|
293
|
-
if (itf.phi.type !== 'neutral' || !strict(mL) || !strict(mR)) continue;
|
|
294
|
-
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1], bL = s.blockOfNode[gL] * M, bR = s.blockOfNode[gR] * M;
|
|
295
|
-
const pass = [];
|
|
296
|
-
for (let i = 0; i < n; i++) if (itf.links[i].type === 'equilibrium' && mL.present[i] && mR.present[i]) pass.push(i);
|
|
297
|
-
if (!pass.some((i) => z[i] !== 0)) continue;
|
|
298
|
-
const RT = model.RT, vL = grid.vol[gL], vR = grid.vol[gR];
|
|
299
|
-
const conc = (m, b, i) => m.cRef[i] * Math.exp(u[b + 1 + i] - m.mu0[i] / RT - z[i] * u[b]);
|
|
300
|
-
const amount = pass.map((i) => vL * conc(mL, bL, i) + vR * conc(mR, bR, i));
|
|
301
|
-
let qOther = regions[f].fixedCharge / FARADAY; // (the left node's charge from all it doesn't share)
|
|
302
|
-
for (let i = 0; i < n; i++) if (mL.present[i] && !pass.includes(i) && z[i] !== 0) qOther += z[i] * conc(mL, bL, i);
|
|
303
|
-
// At a jump D = φ̂_R − φ̂_L: c_L/c_R = (cRef_L/cRef_R)·exp((μ°_R − μ°_L)/RT + z D), the amount fixing both.
|
|
304
|
-
const split = (D) =>
|
|
305
|
-
pass.map((i, j) => {
|
|
306
|
-
const K = (mL.cRef[i] / mR.cRef[i]) * Math.exp((mR.mu0[i] - mL.mu0[i]) / RT + z[i] * D), cR = amount[j] / (vL * K + vR);
|
|
307
|
-
return [K * cR, cR];
|
|
308
|
-
});
|
|
309
|
-
const qL = (D) => split(D).reduce((q, [cL], j) => q + z[pass[j]] * cL, qOther); // (increasing in D)
|
|
310
|
-
const D0 = u[bR] - u[bL];
|
|
311
|
-
let lo = D0 - 1, hi = D0 + 1;
|
|
312
|
-
while (qL(lo) > 0 && lo > D0 - 1e3) lo -= 2 * (hi - lo);
|
|
313
|
-
while (qL(hi) < 0 && hi < D0 + 1e3) hi += 2 * (hi - lo);
|
|
314
|
-
if (!(qL(lo) <= 0 && qL(hi) >= 0)) continue; // (nothing the face shares can balance it: leave c0)
|
|
315
|
-
for (let it = 0; it < 200 && hi - lo > 1e-13 * Math.max(1, Math.abs(D0)); it++) {
|
|
316
|
-
const m = 0.5 * (lo + hi);
|
|
317
|
-
if (qL(m) < 0) lo = m;
|
|
318
|
-
else hi = m;
|
|
319
|
-
}
|
|
320
|
-
const D = 0.5 * (lo + hi), cs = split(D);
|
|
321
|
-
const keep = Float64Array.from({ length: n }, (_, i) => (mR.present[i] ? conc(mR, bR, i) : 0)); // (the right node's, before φ̂ moves)
|
|
322
|
-
u[bR] = u[bL] + D;
|
|
323
|
-
for (let i = 0; i < n; i++) if (mR.present[i] && !pass.includes(i)) u[bR + 1 + i] = Math.log(keep[i] / mR.cRef[i]) + mR.mu0[i] / RT + z[i] * u[bR];
|
|
324
|
-
pass.forEach((i, j) => {
|
|
325
|
-
u[bL + 1 + i] = Math.log(cs[j][0] / mL.cRef[i]) + mL.mu0[i] / RT + z[i] * u[bL];
|
|
326
|
-
u[bR + 1 + i] = Math.log(cs[j][1] / mR.cRef[i]) + mR.mu0[i] / RT + z[i] * u[bR];
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
298
|
// An insertion host's φ is a gauge: c0 sets only its neutral combination's level, not how that
|
|
330
299
|
// splits between its ion and its electrons. Start its ion level with the same ion's beside it
|
|
331
300
|
// across a face (the equilibrium a link or a transfer reaction heads for), so its electrons,
|
|
@@ -447,6 +416,69 @@ export function initFromComposition(s) {
|
|
|
447
416
|
}
|
|
448
417
|
s.termV[k] = 0.5 * (lo + hi);
|
|
449
418
|
}
|
|
419
|
+
s.coldStart = true; // (until a solve or a step moves it: see equilibrateFaces)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The t = 0⁺ state at the faces between strictly neutral regions, taken by the first time step
|
|
424
|
+
* from a cold start (src/timeSteps.js) and not by a steady solve, which needs no t = 0⁺: there it
|
|
425
|
+
* was the worse start, its face nodes moved while the rest of a region stayed (a stack whose
|
|
426
|
+
* middle region started 2.2 V from its neighbour's level failed every steady solve, where the
|
|
427
|
+
* cold start converged in one).
|
|
428
|
+
*/
|
|
429
|
+
export function equilibrateFaces(s) {
|
|
430
|
+
const { model, n, M, u, z } = s;
|
|
431
|
+
const { regions, interfaces, materials } = model;
|
|
432
|
+
const grid = model.grid;
|
|
433
|
+
s.coldStart = false;
|
|
434
|
+
// Two strictly neutral regions meeting at a face whose links hold ions in equilibrium: where
|
|
435
|
+
// c0 leaves the two sides out of it (a different fixed charge or immobile ion on each, so
|
|
436
|
+
// c₊c₋ differs across it), the face reaches it at t = 0⁺, before anything diffuses, and a
|
|
437
|
+
// first step made to take that jump met an exactly singular pivot. Start there: the ions the
|
|
438
|
+
// face passes shared between its two nodes in equilibrium (μ̄ equal across it), each one's
|
|
439
|
+
// amount over the two nodes kept, everything else as c0 has it. The one unknown is the jump
|
|
440
|
+
// in φ̂ across it (a Donnan potential), set by neutrality on one side; the other's follows,
|
|
441
|
+
// the two nodes' charge summing to what it was, zero.
|
|
442
|
+
for (let f = 0; f < interfaces.length; f++) {
|
|
443
|
+
const itf = interfaces[f], mL = materials[regions[f].material], mR = materials[regions[f + 1].material];
|
|
444
|
+
const strict = (m) => m.epsr === 0 && !m.conductor && !m.phiFree && m.ideal;
|
|
445
|
+
if (itf.phi.type !== 'neutral' || !strict(mL) || !strict(mR)) continue;
|
|
446
|
+
const gL = grid.regionEnd[f], gR = grid.regionStart[f + 1], bL = s.blockOfNode[gL] * M, bR = s.blockOfNode[gR] * M;
|
|
447
|
+
const pass = [];
|
|
448
|
+
for (let i = 0; i < n; i++) if (itf.links[i].type === 'equilibrium' && mL.present[i] && mR.present[i]) pass.push(i);
|
|
449
|
+
if (!pass.some((i) => z[i] !== 0)) continue;
|
|
450
|
+
const RT = model.RT, vL = grid.vol[gL], vR = grid.vol[gR];
|
|
451
|
+
const conc = (m, b, i) => m.cRef[i] * Math.exp(u[b + 1 + i] - m.mu0[i] / RT - z[i] * u[b]);
|
|
452
|
+
const amount = pass.map((i) => vL * conc(mL, bL, i) + vR * conc(mR, bR, i));
|
|
453
|
+
let qOther = regions[f].fixedCharge / FARADAY; // (the left node's charge from all it doesn't share)
|
|
454
|
+
for (let i = 0; i < n; i++) if (mL.present[i] && !pass.includes(i) && z[i] !== 0) qOther += z[i] * conc(mL, bL, i);
|
|
455
|
+
// At a jump D = φ̂_R − φ̂_L: c_L/c_R = (cRef_L/cRef_R)·exp((μ°_R − μ°_L)/RT + z D), the amount fixing both.
|
|
456
|
+
const split = (D) =>
|
|
457
|
+
pass.map((i, j) => {
|
|
458
|
+
const K = (mL.cRef[i] / mR.cRef[i]) * Math.exp((mR.mu0[i] - mL.mu0[i]) / RT + z[i] * D), cR = amount[j] / (vL * K + vR);
|
|
459
|
+
return [K * cR, cR];
|
|
460
|
+
});
|
|
461
|
+
const qL = (D) => split(D).reduce((q, [cL], j) => q + z[pass[j]] * cL, qOther); // (increasing in D)
|
|
462
|
+
const D0 = u[bR] - u[bL];
|
|
463
|
+
let lo = D0 - 1, hi = D0 + 1;
|
|
464
|
+
while (qL(lo) > 0 && lo > D0 - 1e3) lo -= 2 * (hi - lo);
|
|
465
|
+
while (qL(hi) < 0 && hi < D0 + 1e3) hi += 2 * (hi - lo);
|
|
466
|
+
if (!(qL(lo) <= 0 && qL(hi) >= 0)) continue; // (nothing the face shares can balance it: leave c0)
|
|
467
|
+
for (let it = 0; it < 200 && hi - lo > 1e-13 * Math.max(1, Math.abs(D0)); it++) {
|
|
468
|
+
const m = 0.5 * (lo + hi);
|
|
469
|
+
if (qL(m) < 0) lo = m;
|
|
470
|
+
else hi = m;
|
|
471
|
+
}
|
|
472
|
+
const D = 0.5 * (lo + hi), cs = split(D);
|
|
473
|
+
const keep = Float64Array.from({ length: n }, (_, i) => (mR.present[i] ? conc(mR, bR, i) : 0)); // (the right node's, before φ̂ moves)
|
|
474
|
+
u[bR] = u[bL] + D;
|
|
475
|
+
for (let i = 0; i < n; i++) if (mR.present[i] && !pass.includes(i)) u[bR + 1 + i] = Math.log(keep[i] / mR.cRef[i]) + mR.mu0[i] / RT + z[i] * u[bR];
|
|
476
|
+
pass.forEach((i, j) => {
|
|
477
|
+
u[bL + 1 + i] = Math.log(cs[j][0] / mL.cRef[i]) + mL.mu0[i] / RT + z[i] * u[bL];
|
|
478
|
+
u[bR + 1 + i] = Math.log(cs[j][1] / mR.cRef[i]) + mR.mu0[i] / RT + z[i] * u[bR];
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
s.computeConcentrations();
|
|
450
482
|
}
|
|
451
483
|
|
|
452
484
|
// A non-ideal material's composition at φ̂ = ph: level-fixed species (mode 1) at their η,
|
package/src/steady.js
CHANGED
|
@@ -50,7 +50,10 @@
|
|
|
50
50
|
// start (initFromComposition), whose u and uLo become the stages' start (termV stays the
|
|
51
51
|
// start's: a floating terminal keeps the voltage the transient left it at).
|
|
52
52
|
// 2. Continuation: generation ramped up (_generationContinuation); a terminal driven by a
|
|
53
|
-
//
|
|
53
|
+
// terminals behind a resistance held (at their source voltages, or where every other
|
|
54
|
+
// terminal is driven by a current at V_src − I R, Kirchhoff's I), then the resistances put
|
|
55
|
+
// back and the steady system solved from there (_resistanceContinuation); a terminal
|
|
56
|
+
// driven by a current, not bound (_bound), held at voltages instead (_floatingContinuation); with
|
|
54
57
|
// canContinue, the right contact's voltage ramped to its target (_continuation): from the
|
|
55
58
|
// voltages of the last converged solve (this.solvedV), the left contact's first where it
|
|
56
59
|
// moved, else from level contacts, a cold start.
|
|
@@ -190,6 +193,7 @@ export class SteadySolves {
|
|
|
190
193
|
this.steady = true; // (the steady system's rows; sources at the present time)
|
|
191
194
|
this.steadyAt = this.time; // (which a pseudo-transient's steps don't move on)
|
|
192
195
|
this.drifted = null; // (an amount no row keeps, lost by a giant step: see _steadySteps)
|
|
196
|
+
this.modelError = null; // (a statistics model's refusal, for the failure: see newton)
|
|
193
197
|
// A solve that fails leaves where the state was solved, and whether it was stepped, as they
|
|
194
198
|
// were (Device.solve restores the state): a floating continuation's held solves had set them,
|
|
195
199
|
// and the next held solve ramped warm from a voltage the state was never at, and failed.
|
|
@@ -252,6 +256,7 @@ export class SteadySolves {
|
|
|
252
256
|
if (q.converged) {
|
|
253
257
|
this.solvedV = [this.termV[0], this.termV[1]]; // (a contact's own: driven by a current, not the current)
|
|
254
258
|
this.stepped = false;
|
|
259
|
+
this.coldStart = false; // (solved: no t = 0⁺ left for a transient to start from)
|
|
255
260
|
}
|
|
256
261
|
return q;
|
|
257
262
|
};
|
|
@@ -289,6 +294,15 @@ export class SteadySolves {
|
|
|
289
294
|
if (g.converged) return solved(g);
|
|
290
295
|
r = g;
|
|
291
296
|
}
|
|
297
|
+
// A terminal behind a resistance, cold: held at its source voltage first (with that its own
|
|
298
|
+
// continuations), then the resistance put back and solved from there (a diode behind 7 Ω·m²
|
|
299
|
+
// failed cold, and converged so; round 14's fuzzer, 4 of 9 such re-solves).
|
|
300
|
+
if (allowed && this.floating.some((k) => this.terms[k].drive.kind === 'V' && this.terms[k].drive.R > 0)) {
|
|
301
|
+
restart();
|
|
302
|
+
const q = this._resistanceContinuation(opts, r);
|
|
303
|
+
if (q.converged) return solved(q);
|
|
304
|
+
r = q;
|
|
305
|
+
}
|
|
292
306
|
// A terminal driven by a current (open circuit, say): hold it at a voltage instead, march the
|
|
293
307
|
// voltage until the current crosses its target, and float it from there.
|
|
294
308
|
if (allowed && this.floating.some((k) => this.terms[k].drive.kind === 'I' && !this._bound(k))) {
|
|
@@ -342,9 +356,10 @@ export class SteadySolves {
|
|
|
342
356
|
// voltage instead, whose steady solves have their own continuation, the voltage marched from
|
|
343
357
|
// the start's until the current crosses its target, the crossing narrowed by bisection, and
|
|
344
358
|
// the terminal floated again from beside it.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
359
|
+
// (With `line`, terminal k behind a resistance: the current its load line passes at V,
|
|
360
|
+
// (V_src − V)/R, the target, the march from the first held terminal's level.)
|
|
361
|
+
_floatingContinuation(opts, r, k = this.floating.find((j) => this.terms[j].drive.kind === 'I' && !this._bound(j)), line = null) {
|
|
362
|
+
const drive = this.terms[k].drive, target = line ? NaN : sourceAt(drive.src, this.time), want = (V) => (line ? line(V) : target);
|
|
348
363
|
// Its drive put back: redrive() finds the laws again for it, with the local constraints'
|
|
349
364
|
// references at zero, so they're taken from the state again (an immobile species held at 0
|
|
350
365
|
// at every node: the floated solve failed, 80 steps, and the regula falsi rescued it at 6×).
|
|
@@ -363,11 +378,12 @@ export class SteadySolves {
|
|
|
363
378
|
iterations += q.iterations ?? 0;
|
|
364
379
|
history.push({ held: V, converged: q.converged });
|
|
365
380
|
if (q.converged) seen.push([V, this.termI[k]]);
|
|
366
|
-
return q.converged ? this.termI[k] -
|
|
381
|
+
return q.converged ? this.termI[k] - want(V) : NaN;
|
|
367
382
|
};
|
|
368
383
|
const fail = () => ({ converged: false, steps, iterations, history });
|
|
369
384
|
const seen = []; // [V, I] of each held solve that converged
|
|
370
|
-
|
|
385
|
+
const heldK = this.terms.findIndex((t) => t.drive.kind === 'V' && !(t.drive.R > 0));
|
|
386
|
+
let Va = line ? (heldK >= 0 ? this.termV[heldK] : 0) : this.termV[k], fa, Vb, fb;
|
|
371
387
|
try {
|
|
372
388
|
fa = hold(Va);
|
|
373
389
|
if (!Number.isFinite(fa)) {
|
|
@@ -396,7 +412,7 @@ export class SteadySolves {
|
|
|
396
412
|
}
|
|
397
413
|
if (!Number.isFinite(fb) || (Math.sign(fb) === Math.sign(fa) && fa !== 0)) {
|
|
398
414
|
// What the held voltages passed, at the two extremes reached.
|
|
399
|
-
if (seen.length > 1) {
|
|
415
|
+
if (seen.length > 1 && !line) {
|
|
400
416
|
seen.sort((a, b) => a[0] - b[0]);
|
|
401
417
|
this.unreached = { terminal: this.terms[k].name, target, low: seen[0], high: seen.at(-1) };
|
|
402
418
|
}
|
|
@@ -423,7 +439,7 @@ export class SteadySolves {
|
|
|
423
439
|
// one at the voltage that passes the target: found by held solves, by regula falsi
|
|
424
440
|
// (Illinois) within the bracket, to 1e-10 of the target, or at open circuit of the largest
|
|
425
441
|
// current the march saw (a target of 0 is never met exactly).
|
|
426
|
-
const scale = Math.max(Math.abs(target), ...seen.map(([, I]) => Math.abs(I)));
|
|
442
|
+
const scale = Math.max(line ? 0 : Math.abs(target), ...seen.map(([, I]) => Math.abs(I)));
|
|
427
443
|
try {
|
|
428
444
|
let side = 0;
|
|
429
445
|
for (let it = 0; it < 60; it++) {
|
|
@@ -446,6 +462,53 @@ export class SteadySolves {
|
|
|
446
462
|
}
|
|
447
463
|
}
|
|
448
464
|
|
|
465
|
+
// Each terminal behind a resistance held at its source voltage (R taken off), solved, then the
|
|
466
|
+
// resistances put back and the steady system solved from that state.
|
|
467
|
+
_resistanceContinuation(opts, r) {
|
|
468
|
+
const ks = this.floating.filter((k) => this.terms[k].drive.kind === 'V' && this.terms[k].drive.R > 0);
|
|
469
|
+
const drives = ks.map((k) => this.terms[k].drive);
|
|
470
|
+
let steps = r.steps ?? 0, iterations = r.iterations ?? 0;
|
|
471
|
+
const history = (r.history ?? []).slice();
|
|
472
|
+
// Where every other terminal is driven by a current, Kirchhoff gives the one behind R its
|
|
473
|
+
// current, and so the voltage it ends at, V_src − I R: held there, putting R back moves
|
|
474
|
+
// nothing (held at V_src, it had to slide the whole device by IR, and failed past volts).
|
|
475
|
+
const others = this.terms.every((t, j) => ks.includes(j) || t.drive.kind === 'I');
|
|
476
|
+
const at = (k) => {
|
|
477
|
+
const d = this.terms[k].drive, V = sourceAt(d.src, this.time);
|
|
478
|
+
if (!(others && ks.length === 1)) return V;
|
|
479
|
+
const I = -this.terms.reduce((t, tm, j) => (j === k ? t : t + sourceAt(tm.drive.src, this.time)), 0);
|
|
480
|
+
return V - I * d.R;
|
|
481
|
+
};
|
|
482
|
+
let heldOk = false;
|
|
483
|
+
const start = [Float64Array.from(this.u), Float64Array.from(this.uLo), Float64Array.from(this.termV)];
|
|
484
|
+
try {
|
|
485
|
+
const held = ks.map(at);
|
|
486
|
+
ks.forEach((k, j) => (this.terms[k].drive = { ...this.terms[k].drive, src: { value: held[j] }, R: 0 }));
|
|
487
|
+
this.redrive();
|
|
488
|
+
const q = this._steadyFromHere(opts);
|
|
489
|
+
steps += q.steps ?? 0;
|
|
490
|
+
iterations += q.iterations ?? 0;
|
|
491
|
+
history.push({ resistancesOff: true, converged: q.converged });
|
|
492
|
+
heldOk = q.converged;
|
|
493
|
+
} finally {
|
|
494
|
+
ks.forEach((k, j) => (this.terms[k].drive = drives[j]));
|
|
495
|
+
this.redrive();
|
|
496
|
+
captureLocal(this);
|
|
497
|
+
}
|
|
498
|
+
const q = heldOk ? this._steadySteps(opts) : { converged: false, steps: 0, iterations: 0, history: [] };
|
|
499
|
+
const done = { ...q, steps: steps + q.steps, iterations: iterations + q.iterations, history: [...history, ...q.history] };
|
|
500
|
+
if (q.converged || ks.length !== 1) return done;
|
|
501
|
+
// One terminal behind R, the rest not all driven by currents (so its current isn't known):
|
|
502
|
+
// its crossing with the load line, (V_src − V)/R, by held solves, as an open circuit's is
|
|
503
|
+
// with its target (a silver cell held at 0 V on one side, 16 V behind R on the other, failed).
|
|
504
|
+
const d = drives[0], Vs = sourceAt(d.src, this.time);
|
|
505
|
+
this.u.set(start[0]);
|
|
506
|
+
this.uLo.set(start[1]);
|
|
507
|
+
this.termV.set(start[2]);
|
|
508
|
+
this.computeConcentrations(); // (from where this stage began, not where the failed one left it)
|
|
509
|
+
return this._floatingContinuation(opts, done, ks[0], (V) => (Vs - V) / d.R);
|
|
510
|
+
}
|
|
511
|
+
|
|
449
512
|
// Steady solves with the generation reactions' rates scaled from 1e-30 up to 1, ×100 a step
|
|
450
513
|
// while they converge, smaller steps where they don't.
|
|
451
514
|
_generationContinuation(opts, r) {
|
package/src/timeSteps.js
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
// (ports' charges) }: what BDF2 and the predictor need.
|
|
22
22
|
//
|
|
23
23
|
// step(s, dt, opts) → newton's result (+ bdf)
|
|
24
|
+
// From a cold start (s.coldStart, outside a steady solve), the faces between strictly neutral
|
|
25
|
+
// regions first set to their t = 0⁺ equilibrium (equilibrateFaces, src/start.js).
|
|
24
26
|
// One step from s.time to tEnd = s.time + dt (or s.landing exactly, where it's within 1e-9·dt
|
|
25
27
|
// of that). Backward Euler; BDF2 when opts.method is 'bdf2', a previous step exists and the
|
|
26
28
|
// ratio ω = dt/dt_prev ≤ 2: then c* = b1·c_n − b2·c_{n−1} (b1 = (1+ω)/a0, b2 = ω²/((1+ω)a0),
|
|
@@ -85,6 +87,7 @@
|
|
|
85
87
|
|
|
86
88
|
import { nextBreakpoint } from './device.js';
|
|
87
89
|
import { SolverError } from './solver.js';
|
|
90
|
+
import { equilibrateFaces } from './start.js';
|
|
88
91
|
|
|
89
92
|
// Lagrange weights at x through the points s0, s1, s2.
|
|
90
93
|
function lagrange3(x, s0, s1, s2) {
|
|
@@ -120,6 +123,8 @@ export function advance(s, dt, opts, depth = 0) {
|
|
|
120
123
|
* the start-of-step values, and dt/a0 replaces dt.
|
|
121
124
|
*/
|
|
122
125
|
export function step(s, dt, opts = {}) {
|
|
126
|
+
// A transient's first step from a cold start begins at its t = 0⁺ (see equilibrateFaces).
|
|
127
|
+
if (s.coldStart && !s.steady) equilibrateFaces(s);
|
|
123
128
|
// What the step overwrites before Newton runs, for a failed one to put back: the Solution read
|
|
124
129
|
// after it takes its contacts' fluxes against the start-of-step values (a host emptied by its
|
|
125
130
|
// current read its contact's Li⁺ flux 8× off at the same state).
|
|
@@ -262,6 +267,7 @@ export function integrate(s, tEnd, opts = {}) {
|
|
|
262
267
|
const sample = s._probes(opts.probes);
|
|
263
268
|
const trace = { t: [], current: [], voltage: [], ...(sample ? { probes: sample.out } : {}) };
|
|
264
269
|
let steps = 0, rejected = 0, iterations = 0, failed = false;
|
|
270
|
+
s.modelError = null; // (a statistics model's refusal, for the failure: see newton)
|
|
265
271
|
// Already there (an animation frame with no time to add): nothing to do, and the step size
|
|
266
272
|
// carried to the next call stays as it was. A time already passed, or reached again outside
|
|
267
273
|
// an animation, is most likely advance(dt) read as a duration: say so.
|
|
@@ -269,7 +275,7 @@ export function integrate(s, tEnd, opts = {}) {
|
|
|
269
275
|
const relative = tEnd < s.time || (s.time > 0 && opts.budgetMs === undefined);
|
|
270
276
|
return { converged: true, done: true, steps: 0, rejected: 0, iterations: 0, trace, ...(relative ? { stopped: `advance(${tEnd}) did nothing: ${absolute}. To run for a further dt, advance(sol.time + dt)` } : {}) };
|
|
271
277
|
}
|
|
272
|
-
const t0 = s.time;
|
|
278
|
+
const t0 = s.time, fromLayout = s.history.length === 0 && !s.stepped && !s.atSteady; // (c0's layout, not a solution)
|
|
273
279
|
// A cold start whose floating double layers start uncharged far from the equilibrium of an ion
|
|
274
280
|
// link beside them (a capacitive contact or port passing K⁺, its outside level set for the
|
|
275
281
|
// charged steady state): the link closes the gap in the first instant, which can be far below
|
|
@@ -395,6 +401,12 @@ export function integrate(s, tEnd, opts = {}) {
|
|
|
395
401
|
stopped = `${at}: the start can't be neutral in ${unbalanced.join(', ')}, strictly neutral (ε = 0): the amounts its c0 gives leave a net charge that no species held at a contact's level can balance, and a step can't move a stored amount at once. Give c0 that balances (with any fixed charge), or leave out of c0 the species a contact feeds, so that they start at its level.`;
|
|
396
402
|
else if (failed && violent.length > 0)
|
|
397
403
|
stopped = `${at}: a step did not converge even when shortened to ${dt.toExponential(1)} s. The start is violent: ${violent.join('; ')}. A double layer starts uncharged, and what passes beside it closes such a gap in the first instant, faster than any step can follow. For a start in equilibrium, solve() first (the steady state at the starting drives), then change what the experiment changes, then advance(). Where solve() fails from there too, hold that terminal at a voltage, solve(), then let it float (set its I: 0): it keeps the state the held one gave it.`;
|
|
404
|
+
else if (failed && s.modelError)
|
|
405
|
+
stopped = `${at}: a step did not converge even when shortened to ${dt.toExponential(1)} s, the state it reached refused by a statistics model: ${s.modelError}. The model doesn't hold there: a smaller concentration or drive, or a model for it (lattice for crowding).`;
|
|
406
|
+
else if (failed && fromLayout && steps === 0)
|
|
407
|
+
// (round 15's fuzzer: the pn template advanced from its c0, a step of 3e-24 s failing, 23 of 62
|
|
408
|
+
// plain diodes; nothing said to solve first)
|
|
409
|
+
stopped = `${at}: the first step did not converge even when shortened to ${dt.toExponential(1)} s, from the starting state, which is c0's layout rather than a solution (a junction's depletion not yet formed, say). For a transient from equilibrium, solve() first at the starting drives, then change what the experiment changes, then advance().`;
|
|
398
410
|
else if (failed) stopped = `${at}: a step did not converge even when shortened to ${dt.toExponential(1)} s. Look for: a host or surface driven to its limit (emptied or filled at its surface by a current it can no longer pass); an electrode at open circuit held only by a slow reaction, with no double-layer capacitance to keep its level (give its face one: phi capacitive); a drive's jump too big for one step (ramp it); a driven current the device cannot keep passing (a diode's reverse recovery past its storage time: stop there); or a device the steady solves also find hard.`;
|
|
399
411
|
else if (s.time < tEnd && steps + rejected >= maxSteps) stopped = `${at}: maxSteps (${maxSteps}) used up (${rejected} of them rejected). Call again to go on, or loosen tol.`;
|
|
400
412
|
return { converged: !failed, done: s.time >= tEnd, steps, rejected, iterations, trace, ...(stopped ? { stopped } : {}) };
|
package/types/impedance.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export class SmallSignal {
|
|
|
4
4
|
* gives (J + iωM)·δx + Σ B_k δV_k = rhs for the grid's rows, and (C_k + iωC′_k)·δx +
|
|
5
5
|
* (∂I_k/∂V_k)·δV_k = δI_set,k for each floating terminal's circuit row, with J the steady
|
|
6
6
|
* Jacobian and M the storage part (from J(dt) = J + M/dt), B_k = ∂res/∂V_k and C_k = ∂I_k/∂x.
|
|
7
|
-
* At the measured terminal,
|
|
8
|
-
*
|
|
7
|
+
* At the measured terminal, its voltage is perturbed (rhs = −B δV) and its current read, held
|
|
8
|
+
* or driven by a current alike (one input impedance; see the contract). The other terminals
|
|
9
9
|
* keep their drives: held ones at AC ground, driven ones at AC open circuit.
|
|
10
10
|
* Z = δV/δI with I into the device (positive real part for a passive device).
|
|
11
11
|
* @param {ArrayLike<number>} frequencies Hz
|
package/types/solver.d.ts
CHANGED
|
@@ -273,6 +273,7 @@ export class Solver {
|
|
|
273
273
|
};
|
|
274
274
|
_warmFrom(src: any): void;
|
|
275
275
|
solvedV: any;
|
|
276
|
+
coldStart: any;
|
|
276
277
|
_snapshot(): {
|
|
277
278
|
u: Float64Array<ArrayBuffer>;
|
|
278
279
|
uLo: Float64Array<ArrayBuffer>;
|
|
@@ -299,6 +300,7 @@ export class Solver {
|
|
|
299
300
|
history: any[];
|
|
300
301
|
termV: Float64Array<ArrayBuffer>;
|
|
301
302
|
sourceTime: number;
|
|
303
|
+
coldStart: any;
|
|
302
304
|
};
|
|
303
305
|
_restore(s: any): void;
|
|
304
306
|
contactDEnd: any;
|
package/types/start.d.ts
CHANGED
|
@@ -4,3 +4,11 @@
|
|
|
4
4
|
* profile varies), or continued across the interface dipole if nothing there responds.
|
|
5
5
|
*/
|
|
6
6
|
export function initFromComposition(s: any): void;
|
|
7
|
+
/**
|
|
8
|
+
* The t = 0⁺ state at the faces between strictly neutral regions, taken by the first time step
|
|
9
|
+
* from a cold start (src/timeSteps.js) and not by a steady solve, which needs no t = 0⁺: there it
|
|
10
|
+
* was the worse start, its face nodes moved while the rest of a region stayed (a stack whose
|
|
11
|
+
* middle region started 2.2 V from its neighbour's level failed every steady solve, where the
|
|
12
|
+
* cold start converged in one).
|
|
13
|
+
*/
|
|
14
|
+
export function equilibrateFaces(s: any): void;
|
package/types/steady.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export class SteadySolves {
|
|
|
29
29
|
steady: boolean;
|
|
30
30
|
steadyAt: any;
|
|
31
31
|
drifted: any;
|
|
32
|
+
modelError: any;
|
|
32
33
|
_solveSteadyAll(opts: any): any;
|
|
33
34
|
atSteady: any;
|
|
34
35
|
_steadyFromHere(opts: any): any;
|
|
@@ -36,9 +37,11 @@ export class SteadySolves {
|
|
|
36
37
|
sourceTime: any;
|
|
37
38
|
solvedV: any[];
|
|
38
39
|
stepped: boolean;
|
|
40
|
+
coldStart: boolean;
|
|
39
41
|
dimmer: boolean;
|
|
40
42
|
_bound(k: any): any;
|
|
41
|
-
_floatingContinuation(opts: any, r: any): any;
|
|
43
|
+
_floatingContinuation(opts: any, r: any, k?: any, line?: any): any;
|
|
44
|
+
_resistanceContinuation(opts: any, r: any): any;
|
|
42
45
|
_generationContinuation(opts: any, r: any): {
|
|
43
46
|
converged: boolean;
|
|
44
47
|
steps: any;
|