driftlet 1.3.2 → 1.3.3
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 +6 -1
- package/docs/kit.md +7 -3
- package/docs/numerics.md +11 -0
- package/docs/statistics.md +5 -1
- package/llms.txt +8 -7
- package/package.json +1 -1
- package/src/checks.js +9 -1
- package/src/device.js +16 -0
- package/src/kit.js +6 -2
- package/src/solution.js +7 -1
- package/src/start.js +37 -8
- package/src/steady.js +38 -18
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.3/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
|
@@ -541,7 +541,12 @@ levels are that material's, and the end node takes them through a Donnan step.
|
|
|
541
541
|
- `'pinned'` with `zeroCharge`: the $`C \to \infty`$ limit,
|
|
542
542
|
$`\phi_{\mathrm{edge}} = V - \mathtt{zeroCharge}`$. This is what a "fixed $`\phi`$" boundary honestly
|
|
543
543
|
means. For example, a Schottky barrier $`\phi_B`$ on n-type material with
|
|
544
|
-
$`\mu^\circ_{\mathrm{e}^-} = 0`$ is `zeroCharge: φ_B`.
|
|
544
|
+
$`\mu^\circ_{\mathrm{e}^-} = 0`$ is `zeroCharge: φ_B`. With a carrier linked in equilibrium, a
|
|
545
|
+
pinned contact sets that carrier's density at the edge,
|
|
546
|
+
$`c_{\mathrm{edge}} = c_{\mathrm{ref}} \exp\big((z F\,\mathtt{zeroCharge} - \mu^\circ)/RT\big)`$, so a contact that injects holes at
|
|
547
|
+
$`N_v`$ (their `cRef`, $`\mu^\circ = 0`$) is `zeroCharge: 0`, and a 0.5 eV barrier for them is
|
|
548
|
+
`zeroCharge: -0.5`. This is the contact for a layer of one carrier with nothing to neutralise it
|
|
549
|
+
(a hole-only organic diode): `ohmic()`'s `'bulk'` law, local neutrality, can't hold there.
|
|
545
550
|
|
|
546
551
|
An ideally blocking electrode (inert: no reactions, nothing crossing) is a contact with no
|
|
547
552
|
terminal species and no links, `{ V, phi: 'pinned', zeroCharge }`: the solution's diffuse layer
|
package/docs/kit.md
CHANGED
|
@@ -59,8 +59,9 @@ console.log(`I = ${sol.current.toFixed(1)} A/m²`);
|
|
|
59
59
|
infinite-recombination contact; `['e-']` alone is selective, and is the current collector on
|
|
60
60
|
a metal region). `bath(c, reference, drive, { offset, material })` is a bath whose terminal voltage is
|
|
61
61
|
the level of its `reference` species, as an electrode reversible to it would read (`offset`
|
|
62
|
-
places that species relative to the terminal voltage, e.g. to read a
|
|
63
|
-
against SHE).
|
|
62
|
+
places that species relative to the terminal voltage, its level at V + `offset`, e.g. to read a
|
|
63
|
+
solution's potentials against SHE). Every species a bath lists is held at its level there, so
|
|
64
|
+
one absent from the bulk (a product) is given as a trace, not 0. `bath(c, drive)`, without a reference, is a bath whose terminal voltage is its
|
|
64
65
|
φ: an ideal salt bridge, the convention of membrane potentials, so that between two such baths
|
|
65
66
|
V_right − V_left is the φ difference a voltage clamp sets. A drive is a voltage, a waveform, or
|
|
66
67
|
`{ V }`, `{ I }`, `{ V, R }`. A bath against a charged layer (an ion exchanger) gives its own
|
|
@@ -448,7 +449,10 @@ page can show that its numbers hold up. Pass the solution `solve()` or `advance(
|
|
|
448
449
|
term named by its equation; two with the same equation (radiative and SRH recombination, both
|
|
449
450
|
`e- + h+ = 0`) are told apart by their place, `e- + h+ = 0 (bulkReactions[1])`, and
|
|
450
451
|
`sol.bulkReactions[k]` has each one's rates. A species at rest
|
|
451
|
-
(in equilibrium, flows only of round-off against what it could carry) isn't listed.
|
|
452
|
+
(in equilibrium, flows only of round-off against what it could carry) isn't listed. Where none
|
|
453
|
+
is left, it says "equilibrium" only if no flow comes within ε of what it could carry; otherwise
|
|
454
|
+
it names the largest, which may be round-off or a real flow too small to check this way (a
|
|
455
|
+
reverse diode's 1 µA/m², 4e-15 of what its holes could carry).
|
|
452
456
|
- **conservation**, in a transient: every closed, unreacting stretch keeps what it held plus
|
|
453
457
|
what came in.
|
|
454
458
|
- **order**, in a steady state (unless `refine: false` or `order: false`): the device solved from
|
package/docs/numerics.md
CHANGED
|
@@ -951,6 +951,17 @@ A solve that fails falls back stage by stage:
|
|
|
951
951
|
the floated solve fails, held solves find that voltage instead, by regula falsi within the
|
|
952
952
|
bracket, to 1e-10 of the current. (Not a terminal at no current that keeps an amount or a
|
|
953
953
|
charge: every voltage passes nothing there.)
|
|
954
|
+
- **A resistance.** A terminal behind a resistance R, its source at $`V_{\mathrm{src}}`$, is
|
|
955
|
+
found the same way, held at voltages marched from the held terminal's level until the
|
|
956
|
+
device's current crosses the load line's, $`(V_{\mathrm{src}} - V)/R`$, and floated from
|
|
957
|
+
beside it. Held at its source instead and the resistance put back, the state would sit
|
|
958
|
+
$`IR`$ from the answer, and the pseudo-transient relaxes that over $`RC`$ at the pace Newton
|
|
959
|
+
takes through the swing: a lit Schottky diode passing 0.9 A/m² took 0.05 s behind
|
|
960
|
+
1 Ω·m² and 105 s behind 10⁵ Ω·m² that way, and takes 33 ms behind any R by the load line
|
|
961
|
+
(80 fuzzed diodes from 0.01 to 10⁷ Ω·m²: 1835 s → 34 s). That route remains for terminals the load line doesn't
|
|
962
|
+
fit: several behind resistances, or one beside terminals all driven by currents, where
|
|
963
|
+
Kirchhoff gives its current and it's held at $`V_{\mathrm{src}} - IR`$ (from there, putting
|
|
964
|
+
$`R`$ back moves nothing).
|
|
954
965
|
- **The voltages.** The right terminal's voltage ramped to its target: from the contacts'
|
|
955
966
|
voltages at the last converged solve when the state is that solution (the left one first,
|
|
956
967
|
where it moved), and otherwise from level terminals, where a cold start is consistent. (A
|
package/docs/statistics.md
CHANGED
|
@@ -105,7 +105,11 @@ $`\ln\gamma_i = -z_i^2 \ell_B \kappa / (2(1 + \kappa a))`$, with the Bjerrum len
|
|
|
105
105
|
$`\ell_B = e^2/(4\pi\varepsilon kT)`$ and $`\kappa^2 = F^2 \sum z^2 c / (\varepsilon RT)`$ from the
|
|
106
106
|
solvent permittivity `epsr` (which defaults to the material's, if that's non-zero) and a
|
|
107
107
|
distance of closest approach `a` (default 0, the limiting law). It stops being convex at high
|
|
108
|
-
ionic strength, and the solver then reports a failure.
|
|
108
|
+
ionic strength, and the solver then reports a failure. It goes on a material's `statistics`, as
|
|
109
|
+
any model does; with `aqueous()`, on the piece it returns: `lib.materials.water.statistics =
|
|
110
|
+
[{ type: 'debye-huckel', species: ['H+', 'Cl-'], a: 4.5e-10 }]`. `a` is an ion size (a few
|
|
111
|
+
ångström: tabulated per ion, as Kielland's sizes, or fitted to measured γ±), and it matters:
|
|
112
|
+
in 10 mM HCl, 4.5 Å against the limiting law moves a concentration cell's voltage by 0.6 mV.
|
|
109
113
|
|
|
110
114
|
Single-ion activity coefficients are a convention: only neutral combinations (the mean
|
|
111
115
|
activity $`\gamma_\pm`$) are measurable. In a strictly neutral ($`\varepsilon = 0`$) material only
|
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.3/src/index.js';
|
|
34
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.3/src/kit.js';
|
|
35
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.3/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.3/src/index.js';
|
|
223
|
+
import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.3/src/kit.js';
|
|
224
|
+
import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@1.3.3/src/plot.js';
|
|
225
225
|
|
|
226
226
|
const def = build({
|
|
227
227
|
T: 300,
|
|
@@ -558,7 +558,8 @@ species }` (its flux) or `{ interface: f, gate }`.
|
|
|
558
558
|
(docs/statistics.md). At no current (`I: 0`), `solve()` keeps the charge it holds: a closed
|
|
559
559
|
battery sits at the OCV of its state of charge, a floating gate keeps its charge.
|
|
560
560
|
- **Other steps.** `dev.step(dt)` is one backward-Euler step, and `dev.impedance(frequencies,
|
|
561
|
-
{ terminal })` the small-signal Z(f) about the steady state, in Ω·m
|
|
561
|
+
{ terminal })` the small-signal Z(f) about the steady state, in Ω·m²: `{ f, Z: { re, im } }`,
|
|
562
|
+
each an array over the frequencies (`Z.re[k]`, not `Z[k].re`).
|
|
562
563
|
|
|
563
564
|
```js
|
|
564
565
|
import { Device, units, 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.3",
|
|
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/checks.js
CHANGED
|
@@ -181,6 +181,12 @@ function balance(device, sol) {
|
|
|
181
181
|
// 1e-19 of its scale, to 1e-15), and round-off doesn't. (Above 1e-8 of the scale, any.)
|
|
182
182
|
const balanced = (l) => Math.abs(l.terms.reduce((a, t) => a + t.rate, 0)) <= 1e-9 * size(l);
|
|
183
183
|
const moving = ledgers.filter((l) => size(l) > 1e-8 * scale(l) || (size(l) > 1e-20 * scale(l) && balanced(l)));
|
|
184
|
+
// What's left unbalanced can be round-off or a small real flow the solve didn't settle (a
|
|
185
|
+
// reverse diode behind a resistance, 1.3 µA/m² at 4e-15 of its scale, balanced to 8e-5 by a
|
|
186
|
+
// solve that stalled), and nothing here tells them apart: round-off reaches 8e-12 of the scale
|
|
187
|
+
// in the stress devices, and one in a thousand of those flows balances to 1%. So above ε of the
|
|
188
|
+
// scale it isn't called equilibrium: the largest is named, as what balance can't check.
|
|
189
|
+
const unsure = ledgers.filter((l) => !moving.includes(l) && size(l) > 1e-16 * scale(l)).sort((a, b) => size(b) / scale(b) - size(a) / scale(a))[0];
|
|
184
190
|
let worst = 0;
|
|
185
191
|
const lines = [];
|
|
186
192
|
for (const l of moving) {
|
|
@@ -195,7 +201,9 @@ function balance(device, sol) {
|
|
|
195
201
|
const ok = worst < 1e-6;
|
|
196
202
|
const summary =
|
|
197
203
|
moving.length === 0
|
|
198
|
-
?
|
|
204
|
+
? unsure
|
|
205
|
+
? `nothing flows that it can tell from round-off: the largest, ${unsure.title} at ${sig(size(unsure))} ${units.rate}${unsure.z !== 0 ? ` (${sig(FARADAY * Math.abs(unsure.z) * size(unsure))} ${units.current})` : ''}, is ${sig(size(unsure) / scale(unsure), 1)} of what it could carry and doesn't balance (to ${sig(Math.abs(unsure.terms.reduce((a, t) => a + t.rate, 0)) / size(unsure), 1)}), so it's round-off, or a flow too small for this check`
|
|
206
|
+
: 'nothing flows or reacts (equilibrium)'
|
|
199
207
|
: `${moving.length === 1 ? 'one ledger sums' : `${moving.length} ledgers sum`} to zero ${ok ? `(to ${sig(worst, 1)})` : `only to ${sig(worst, 2)}: not a steady state`}: each species' sources and sinks, ${units.rate}, + in or made`;
|
|
200
208
|
// The largest current any charged species carries anywhere: the scale a net current is read against.
|
|
201
209
|
const gross = Math.max(0, ...moving.filter((l) => l.z !== 0).map((l) => FARADAY * Math.abs(l.z) * size(l)));
|
package/src/device.js
CHANGED
|
@@ -811,6 +811,7 @@ function normalizePort(pdef, path, regions, materials, species, speciesIndex, RT
|
|
|
811
811
|
need(speciesIndex.has(sname), `${path}.bath.c.${sname}: unknown species '${sname}'${known(speciesIndex)}`);
|
|
812
812
|
const i = speciesIndex.get(sname);
|
|
813
813
|
need(mat.present[i], `${path}.bath.c.${sname}: '${sname}' is absent from ${reg.name} (material '${mat.name}'), so its level outside means nothing here`);
|
|
814
|
+
need(v !== 0, `${path}.bath.c.${sname}: a bath holds each species it lists at its level, and at 0 there is none (ln 0); a species absent from the bulk (a product, say) is a trace there, 1e-6 of the rest or so, so that it can leave`);
|
|
814
815
|
cb[i] = positive(v, `${path}.bath.c.${sname}`);
|
|
815
816
|
}
|
|
816
817
|
const zb = bathZeta(mat, cb, reg.background);
|
|
@@ -1359,6 +1360,7 @@ function normalizeContact(cdef, side, region, materials, species, speciesIndex,
|
|
|
1359
1360
|
need(speciesIndex.has(sname), `${path}.bath.c.${sname}: unknown species '${sname}'${known(speciesIndex)}`);
|
|
1360
1361
|
const i = speciesIndex.get(sname);
|
|
1361
1362
|
need(bm.present[i], `${path}.bath.c.${sname}: '${sname}' is absent from ${own ? 'the bath' : 'the end'} material '${bm.name}'`);
|
|
1363
|
+
need(v !== 0, `${path}.bath.c.${sname}: a bath holds each species it lists at its level, and at 0 there is none (ln 0); a species absent from the bulk (a product, say) is a trace there, 1e-6 of the rest or so, so that it can leave`);
|
|
1362
1364
|
cb[i] = positive(v, `${path}.bath.c.${sname}`);
|
|
1363
1365
|
charge += species[i].z * cb[i];
|
|
1364
1366
|
scale += Math.abs(species[i].z) * cb[i];
|
|
@@ -1413,6 +1415,20 @@ function normalizeContact(cdef, side, region, materials, species, speciesIndex,
|
|
|
1413
1415
|
links.some((l, i) => l.type !== 'blocked' && species[i].z !== 0),
|
|
1414
1416
|
`${path}.phi: 'bulk' needs at least one connected charged species at this contact`,
|
|
1415
1417
|
);
|
|
1418
|
+
// Neutral at the contact must be possible: an undoped layer of holes alone never is, and
|
|
1419
|
+
// ohmic(V, ['h+']) on one failed with "nothing specific showed why" (round 16's cold agent).
|
|
1420
|
+
if (!mat.conductor && !mat.phiFree && !mat.models.some((md) => md.type === 'insertion')) {
|
|
1421
|
+
const signs = new Set(species.flatMap((sp, i) => (mat.present[i] && sp.z !== 0 ? [Math.sign(sp.z)] : [])));
|
|
1422
|
+
const fixed = Math.sign(region.fixedCharge ?? 0);
|
|
1423
|
+
if (signs.size === 1 && !(fixed !== 0 && !signs.has(fixed))) {
|
|
1424
|
+
const [only] = signs;
|
|
1425
|
+
throw new DeviceError(
|
|
1426
|
+
`${path}.phi: 'bulk' (what ohmic() and a bath use) holds the contact neutral, but '${region.name ?? region.material}' can't be: ` +
|
|
1427
|
+
`its only charged species are ${only > 0 ? 'positive' : 'negative'} and no fixed charge balances them (an undoped layer of one carrier). ` +
|
|
1428
|
+
"For a contact that sets the carrier's density, pin φ instead: phi: 'pinned' with zeroCharge, the contact's level against the carrier's (see docs/device.md, contacts).",
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1416
1432
|
}
|
|
1417
1433
|
|
|
1418
1434
|
if (terminal !== null) {
|
package/src/kit.js
CHANGED
|
@@ -50,10 +50,14 @@ export function vacuumLevel(def, side, path = 'vacuum') {
|
|
|
50
50
|
if (!Number.isFinite(side.offset)) fail(`${path}.offset must be a finite number (V)`);
|
|
51
51
|
if (side.anchor === 'phi') return -side.offset;
|
|
52
52
|
const sp = (def.species ?? []).find((s) => s.name === side.anchor);
|
|
53
|
-
if (!sp)
|
|
53
|
+
if (!sp)
|
|
54
|
+
fail(
|
|
55
|
+
`${path}.anchor must be 'phi' or a charged species of the device, got ${JSON.stringify(side.anchor)}` +
|
|
56
|
+
(typeof side.anchor === 'string' ? ` (to anchor on it, list it in the library: aqueous([..., '${side.anchor}']), say)` : ''),
|
|
57
|
+
);
|
|
54
58
|
if (!(sp.z !== 0)) fail(`${path}.anchor: '${side.anchor}' is neutral, so it has no level in volts`);
|
|
55
59
|
const entry = mat.species?.[side.anchor];
|
|
56
|
-
if (!isObject(entry)) fail(`${path}.anchor: '${side.anchor}' is absent from '${side.material}'`);
|
|
60
|
+
if (!isObject(entry)) fail(`${path}.anchor: '${side.anchor}' is absent from '${side.material}' (to anchor on it, give the material that species)`);
|
|
57
61
|
if (!Number.isFinite(entry.mu0)) fail(`${path}.anchor: '${side.anchor}' in '${side.material}' has no mu0`);
|
|
58
62
|
return entry.mu0 / (sp.z * FARADAY) - side.offset;
|
|
59
63
|
}
|
package/src/solution.js
CHANGED
|
@@ -272,10 +272,16 @@ export function makeSolution(solver, result = {}) {
|
|
|
272
272
|
'advance() in time instead, or give the still species a small D where it is still.',
|
|
273
273
|
);
|
|
274
274
|
} else if (slow.length > 0) {
|
|
275
|
+
// Too slow to wait out only where the solve waited: where no time step was taken at all, the
|
|
276
|
+
// start was the trouble (a hole-only layer full of the anode's holes, with traps: "raise its
|
|
277
|
+
// rate constant" changed nothing, ×1e4).
|
|
278
|
+
const waited = (result.history ?? []).some((h) => h.converged && h.dt > 0);
|
|
275
279
|
for (const { where, affinity } of slow) {
|
|
276
280
|
sol.warnings.push(
|
|
277
281
|
`the solve did not converge, likely because ${where} is out of equilibrium by ${(affinity * solver.VT * 1e3).toPrecision(3)} mV of affinity, yet nothing else makes or uses what it changes, so a steady state needs it at equilibrium: ` +
|
|
278
|
-
|
|
282
|
+
(waited
|
|
283
|
+
? 'it is too slow for the steady solve to wait out. Raise its rate constant (the steady state does not depend on it), or advance() in time.'
|
|
284
|
+
: 'the solve failed from its first steps, so start it nearer that equilibrium: c0 near it, or solve without the reaction and set() it back.'),
|
|
279
285
|
);
|
|
280
286
|
}
|
|
281
287
|
} else if (result.converged === false && cond && cond.digits > 12) {
|
package/src/start.js
CHANGED
|
@@ -29,9 +29,11 @@
|
|
|
29
29
|
// otherwise it's the running φ̂, or where a conductor on the left or an electrode spread
|
|
30
30
|
// through the region has a reaction that can balance, the φ̂ that balances it (by bisection;
|
|
31
31
|
// a trial a non-ideal model refuses counts its ideal charge, which has the sign there). Where
|
|
32
|
-
// neutrality is out of reach (c0's amounts leave a charge no held level balances)
|
|
33
|
-
//
|
|
34
|
-
// fails from it to say why)
|
|
32
|
+
// neutrality is out of reach (c0's amounts leave a charge no held level balances): in a
|
|
33
|
+
// strictly neutral region the running φ̂, the region noted in s.unbalanced (for a transient
|
|
34
|
+
// that fails from it to say why); otherwise the φ̂ nearest the running one, toward less
|
|
35
|
+
// charge, where the charge is at most ε V_T/(F L²) (L the region's length: what it holds with
|
|
36
|
+
// a thermal voltage across it), or where it stops falling (a fixed charge). A strictly
|
|
35
37
|
// neutral region (ε = 0) with every charged species' amount given must have a neutral c0:
|
|
36
38
|
// an error otherwise. A non-ideal material's composition comes from its statistics
|
|
37
39
|
// (materialAt).
|
|
@@ -63,7 +65,8 @@
|
|
|
63
65
|
// equilibrated by step 2 (a transient's first step), its amount over the two nodes is kept.
|
|
64
66
|
// 2. A species given by its level starts at the level of what feeds it, as step 1 orders them.
|
|
65
67
|
// 3. Where a level-fixed charged species responds, each node starts neutral (Σ z c + the fixed
|
|
66
|
-
// charge = 0) to the bisection's tolerance, unless neutrality is out of reach (step 1
|
|
68
|
+
// charge = 0) to the bisection's tolerance, unless neutrality is out of reach (step 1: then
|
|
69
|
+
// its charge at most ε V_T/(F L²), or the least φ̂ gives, to the bisection's tolerance); a
|
|
67
70
|
// strictly neutral region given by amounts is neutral by its c0, checked.
|
|
68
71
|
// 4. Steps 2 and 3 change no concentration but as they say: step 3 is a gauge (φ̂ and each η by
|
|
69
72
|
// z·Δ), step 2 moves the passing species between a face's two nodes only.
|
|
@@ -81,7 +84,7 @@
|
|
|
81
84
|
// Tests: every test (each device starts here); test/transient.test.js on what a start must be.
|
|
82
85
|
|
|
83
86
|
import { gateRate } from './boundaries.js';
|
|
84
|
-
import { FARADAY } from './constants.js';
|
|
87
|
+
import { EPS0, FARADAY } from './constants.js';
|
|
85
88
|
import { sourceAt } from './device.js';
|
|
86
89
|
import { profileAt, SolverError } from './solver.js';
|
|
87
90
|
|
|
@@ -260,13 +263,39 @@ export function initFromComposition(s) {
|
|
|
260
263
|
while (charge(lo) < 0 && lo > -1e4) lo -= 2 * (hi - lo);
|
|
261
264
|
while (charge(hi) > 0 && hi < 1e4) hi += 2 * (hi - lo);
|
|
262
265
|
// Out of reach (every responding carrier has one sign and nothing balances it, as in an
|
|
263
|
-
// undoped layer that holds only holes):
|
|
266
|
+
// undoped layer that holds only holes): not run to ±∞, but where the charge is no more than
|
|
267
|
+
// the region holds with a thermal voltage across it, ε V_T/(F L²), or as little as it gets
|
|
268
|
+
// (a fixed charge left). Kept at the running φ̂ (a contact's level), a hole-only layer 100
|
|
269
|
+
// nm thick started full of the anode's 1e27 m⁻³ holes, 7 kV of space charge: with traps in
|
|
270
|
+
// it, every step failed from there.
|
|
264
271
|
if (charge(lo) < 0 || charge(hi) > 0) {
|
|
265
272
|
// In a strictly neutral region that's a start that can't be: the amounts c0 gives leave
|
|
266
273
|
// a charge no level-held species can balance. Noted, for a transient that fails from it
|
|
267
274
|
// to say so (a steady solve, its levels from the contacts, can still find the state).
|
|
268
|
-
if (mat.epsr === 0)
|
|
269
|
-
|
|
275
|
+
if (mat.epsr === 0) {
|
|
276
|
+
s.unbalanced.push(r);
|
|
277
|
+
return ph;
|
|
278
|
+
}
|
|
279
|
+
const L = grid.x[grid.regionEnd[r]] - grid.x[grid.regionStart[r]];
|
|
280
|
+
const most = (mat.epsr * EPS0 * model.RT) / (FARADAY * FARADAY * L * L);
|
|
281
|
+
const q0 = charge(ph), dir = q0 > 0 ? 1 : -1; // (the charge falls as φ̂ rises)
|
|
282
|
+
if (!(Math.abs(q0) > most)) return ph;
|
|
283
|
+
let a = ph, qa = Math.abs(q0);
|
|
284
|
+
for (let step = 1; step < 1e4; step *= 2) {
|
|
285
|
+
let b = a + dir * step;
|
|
286
|
+
const qb = Math.abs(charge(b));
|
|
287
|
+
if (qb <= most) {
|
|
288
|
+
for (let it = 0; it < 100 && Math.abs(b - a) > 1e-9; it++) {
|
|
289
|
+
const m = 0.5 * (a + b);
|
|
290
|
+
if (Math.abs(charge(m)) > most) a = m;
|
|
291
|
+
else b = m;
|
|
292
|
+
}
|
|
293
|
+
return b;
|
|
294
|
+
}
|
|
295
|
+
if (!(qb < qa * (1 - 1e-9))) return a; // (a fixed charge: as little as it gets)
|
|
296
|
+
[a, qa] = [b, qb];
|
|
297
|
+
}
|
|
298
|
+
return a;
|
|
270
299
|
}
|
|
271
300
|
for (let it = 0; it < 200 && hi - lo > 1e-12; it++) {
|
|
272
301
|
const m = 0.5 * (lo + hi);
|
package/src/steady.js
CHANGED
|
@@ -49,11 +49,14 @@
|
|
|
49
49
|
// (this.stepped), on a direct device with continuation not off, the same again from a cold
|
|
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
|
-
// 2. Continuation: generation ramped up (_generationContinuation);
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
// driven by a current,
|
|
52
|
+
// 2. Continuation: generation ramped up (_generationContinuation); terminals behind a
|
|
53
|
+
// resistance (_resistanceContinuation): one whose current Kirchhoff doesn't give, held at
|
|
54
|
+
// voltages marched to its load line's crossing (_floatingContinuation with a line); else, or
|
|
55
|
+
// where that fails, each held (at its source voltage, or where every other terminal is
|
|
56
|
+
// driven by a current at V_src − I R, Kirchhoff's I), then the resistances put back and the
|
|
57
|
+
// steady system solved from there (and where that fails for one terminal held by
|
|
58
|
+
// Kirchhoff, its load line); a terminal driven by a current, not bound (_bound), held at
|
|
59
|
+
// voltages instead (_floatingContinuation); with
|
|
57
60
|
// canContinue, the right contact's voltage ramped to its target (_continuation): from the
|
|
58
61
|
// voltages of the last converged solve (this.solvedV), the left contact's first where it
|
|
59
62
|
// moved, else from level contacts, a cold start.
|
|
@@ -462,8 +465,10 @@ export class SteadySolves {
|
|
|
462
465
|
}
|
|
463
466
|
}
|
|
464
467
|
|
|
465
|
-
//
|
|
466
|
-
//
|
|
468
|
+
// One terminal behind a resistance: its load line's crossing by held solves. Otherwise (or where
|
|
469
|
+
// that fails), each terminal behind a resistance held at its source voltage (R taken off; at
|
|
470
|
+
// V_src − I R where Kirchhoff gives I), solved, then the resistances put back and the steady
|
|
471
|
+
// system solved from that state.
|
|
467
472
|
_resistanceContinuation(opts, r) {
|
|
468
473
|
const ks = this.floating.filter((k) => this.terms[k].drive.kind === 'V' && this.terms[k].drive.R > 0);
|
|
469
474
|
const drives = ks.map((k) => this.terms[k].drive);
|
|
@@ -479,8 +484,29 @@ export class SteadySolves {
|
|
|
479
484
|
const I = -this.terms.reduce((t, tm, j) => (j === k ? t : t + sourceAt(tm.drive.src, this.time)), 0);
|
|
480
485
|
return V - I * d.R;
|
|
481
486
|
};
|
|
482
|
-
let heldOk = false;
|
|
483
487
|
const start = [Float64Array.from(this.u), Float64Array.from(this.uLo), Float64Array.from(this.termV)];
|
|
488
|
+
const restore = () => {
|
|
489
|
+
this.u.set(start[0]);
|
|
490
|
+
this.uLo.set(start[1]);
|
|
491
|
+
this.termV.set(start[2]);
|
|
492
|
+
this.computeConcentrations();
|
|
493
|
+
};
|
|
494
|
+
// One terminal behind R, the rest not all driven by currents (so its current isn't known):
|
|
495
|
+
// its crossing with the load line, (V_src − V)/R, by held solves, as an open circuit's is
|
|
496
|
+
// with its target, first. Held at its source instead, the state is I R from the answer and
|
|
497
|
+
// the pseudo-transient relaxes it over R C at Newton's pace through that swing (a lit Schottky
|
|
498
|
+
// diode passing 0.9 A/m²: 0.05 s at 1 Ω·m², 10 s at 1e4, 105 s at 1e5, steps ∝ R).
|
|
499
|
+
const d = drives[0], Vs = sourceAt(d.src, this.time), line = (V) => (Vs - V) / d.R;
|
|
500
|
+
const byLine = ks.length === 1 && !others;
|
|
501
|
+
if (byLine) {
|
|
502
|
+
const q = this._floatingContinuation(opts, r, ks[0], line);
|
|
503
|
+
if (q.converged) return q;
|
|
504
|
+
restore(); // (from where this stage began, not where the failed one left it)
|
|
505
|
+
steps = q.steps ?? steps;
|
|
506
|
+
iterations = q.iterations ?? iterations;
|
|
507
|
+
history.splice(0, history.length, ...(q.history ?? []));
|
|
508
|
+
}
|
|
509
|
+
let heldOk = false;
|
|
484
510
|
try {
|
|
485
511
|
const held = ks.map(at);
|
|
486
512
|
ks.forEach((k, j) => (this.terms[k].drive = { ...this.terms[k].drive, src: { value: held[j] }, R: 0 }));
|
|
@@ -497,16 +523,10 @@ export class SteadySolves {
|
|
|
497
523
|
}
|
|
498
524
|
const q = heldOk ? this._steadySteps(opts) : { converged: false, steps: 0, iterations: 0, history: [] };
|
|
499
525
|
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
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
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);
|
|
526
|
+
if (q.converged || ks.length !== 1 || byLine) return done;
|
|
527
|
+
// Where Kirchhoff's hold failed too, the load line.
|
|
528
|
+
restore();
|
|
529
|
+
return this._floatingContinuation(opts, done, ks[0], line);
|
|
510
530
|
}
|
|
511
531
|
|
|
512
532
|
// Steady solves with the generation reactions' rates scaled from 1e-30 up to 1, ×100 a step
|