driftlet 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +285 -0
  3. package/llms.txt +299 -0
  4. package/package.json +54 -0
  5. package/src/bernoulli.js +29 -0
  6. package/src/blockTridiagonal.js +492 -0
  7. package/src/constants.js +36 -0
  8. package/src/data.js +127 -0
  9. package/src/describe.js +190 -0
  10. package/src/device.js +954 -0
  11. package/src/equation.js +188 -0
  12. package/src/errors.js +8 -0
  13. package/src/fermi.js +106 -0
  14. package/src/grid.js +111 -0
  15. package/src/index.js +207 -0
  16. package/src/kit.js +76 -0
  17. package/src/live.js +217 -0
  18. package/src/merge.js +11 -0
  19. package/src/plot.js +267 -0
  20. package/src/solution.js +246 -0
  21. package/src/solver.js +2884 -0
  22. package/src/stack.js +172 -0
  23. package/src/statistics.js +593 -0
  24. package/src/traces.js +125 -0
  25. package/src/types.js +221 -0
  26. package/src/worker.js +24 -0
  27. package/types/bernoulli.d.ts +4 -0
  28. package/types/blockTridiagonal.d.ts +92 -0
  29. package/types/constants.d.ts +33 -0
  30. package/types/data.d.ts +73 -0
  31. package/types/describe.d.ts +15 -0
  32. package/types/device.d.ts +20 -0
  33. package/types/equation.d.ts +102 -0
  34. package/types/errors.d.ts +3 -0
  35. package/types/fermi.d.ts +3 -0
  36. package/types/grid.d.ts +28 -0
  37. package/types/index.d.ts +103 -0
  38. package/types/kit.d.ts +61 -0
  39. package/types/live.d.ts +101 -0
  40. package/types/merge.d.ts +1 -0
  41. package/types/plot.d.ts +60 -0
  42. package/types/solution.d.ts +18 -0
  43. package/types/solver.d.ts +342 -0
  44. package/types/stack.d.ts +59 -0
  45. package/types/statistics.d.ts +10 -0
  46. package/types/traces.d.ts +118 -0
  47. package/types/types.d.ts +574 -0
package/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ BSD Zero Clause License
2
+
3
+ Copyright (c) 2026 Mark Lundeberg
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
9
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
10
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
11
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
12
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
13
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
14
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,285 @@
1
+ # driftlet
2
+
3
+ [![test](https://github.com/markblundeberg/driftlet/actions/workflows/test.yml/badge.svg)](https://github.com/markblundeberg/driftlet/actions/workflows/test.yml)
4
+
5
+ **driftlet solves 1D drift–diffusion–reaction problems properly**: any mix of charged and
6
+ neutral species, Poisson electrostatics (or strict neutrality), bulk and interfacial reactions,
7
+ heterointerfaces, metals, and external circuits, in steady state, in time, and as small-signal
8
+ impedance. It's validated against analytic results, and small enough to run live in a web page:
9
+ pure JavaScript, no dependencies, millisecond solves.
10
+
11
+ **Status: pre-release.** Not yet on npm, and the API may still change.
12
+
13
+ ## Is it for your problem?
14
+
15
+ The same equations go by different names in different fields. If your problem is one-dimensional
16
+ (planar), it's probably here:
17
+
18
+ | If you work on | you may call it | start from | checked against |
19
+ |---|---|---|---|
20
+ | Semiconductor devices | drift–diffusion, van Roosbroeck, quasi-Fermi levels, Scharfetter–Gummel; pn, Schottky, MOS, heterojunctions | [first example](#a-semiconductor-junction), [pn demo](https://markblundeberg.github.io/driftlet/demos/pn.html), [`contacts`](test/contacts.test.js), [`metal`](test/metal.test.js) tests | exact built-in potential, Shockley J–V, depletion charge, MOS C–V |
21
+ | Solar cells | photogeneration, radiative and SRH recombination | [`reactions`](test/reactions.test.js) test: generation is a reaction from a photon reservoir (uniform per material), SRH runs through explicit trap species | `J_sc = qG(L_n + L_p + W)`; Shockley J–V in the dark |
22
+ | Electrochemistry | Nernst–Planck, concentration polarization, limiting current, Butler–Volmer, Warburg, cyclic voltammetry | [second example](#an-electrochemical-cell), [saturation](https://markblundeberg.github.io/driftlet/demos/saturation.html) and [impedance](https://markblundeberg.github.io/driftlet/demos/impedance.html) demos, [`circuit`](test/circuit.test.js), [`kinetics`](test/kinetics.test.js), [`impedance`](test/impedance.test.js) tests | `i_lim·tanh(V/4V_T)`, Butler–Volmer closed form, finite-length Warburg |
23
+ | Batteries, intercalation | OCV, insertion hosts, chemical diffusion | [insertion demo](https://markblundeberg.github.io/driftlet/demos/insertion.html), [`statistics`](test/statistics.test.js) test | composition vs OCV, π²D/4L² relaxation |
24
+ | Double layers, colloids | Poisson–Boltzmann, Gouy–Chapman–Stern, Debye screening, crowding (Bikerman) | [double-layer demo](https://markblundeberg.github.io/driftlet/demos/double-layer.html), [`equilibrium`](test/equilibrium.test.js) test | Gouy–Chapman charge and profile, Kilic–Bazant–Ajdari |
25
+ | Membranes, desalination | Donnan, ion exchange, liquid junctions, water dissociation | [`equilibrium`](test/equilibrium.test.js), [`neutral`](test/neutral.test.js) tests | Donnan partition, Planck EMF |
26
+ | Solid-state ionics | mixed ionic–electronic conduction, defect chemistry, mobile ions | [`statistics`](test/statistics.test.js), [`reactions`](test/reactions.test.js) tests | mass action from standard potentials |
27
+ | Biophysics | Poisson–Nernst–Planck, ion channels, membrane potentials | the electrochemistry setups, with fixed charge in the channel region | no dedicated check yet |
28
+
29
+ The tests are worked setups, each with its analytic check, so they double as recipes. The
30
+ [validation table](#validation) lists them all.
31
+
32
+ ## Install
33
+
34
+ ```sh
35
+ npm install driftlet
36
+ ```
37
+
38
+ ```js nocheck
39
+ import { Device, units } from 'driftlet'; // the solver
40
+ import { build, layer, ohmic } from 'driftlet/kit'; // helpers that write definitions, data, live demos
41
+ import { bandDiagram } from 'driftlet/plot'; // a level diagram as an SVG string
42
+ ```
43
+
44
+ Or straight from a CDN in a page, pinned to a version:
45
+ `https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/index.js` (and `src/kit.js`, `src/plot.js`).
46
+ TypeScript declarations ship in the package. Writing a demo with an LLM agent? Point it at
47
+ [`llms.txt`](llms.txt).
48
+
49
+ ## A semiconductor junction
50
+
51
+ A silicon pn junction with ohmic contacts, at equilibrium and then under forward bias. A device
52
+ is plain data; `build()` from `driftlet/kit` writes it from the stack as it's drawn, with a
53
+ [data library](docs/data.md) for the material:
54
+
55
+ ```js
56
+ import { Device, units } from 'driftlet';
57
+ import { build, layer, ohmic, semiconductor } from 'driftlet/kit';
58
+
59
+ const dev = new Device(
60
+ build({
61
+ T: 300,
62
+ library: [semiconductor('Si')], // e⁻ and h⁺ with Sze's band data at 300 K
63
+ stack: [
64
+ ohmic(0), // e⁻ and h⁺ in equilibrium with a metal, the terminal at 0 V
65
+ layer('Si', units.um(2), { name: 'n', donors: units.perCm3(1e17) }),
66
+ layer('Si', units.um(2), { name: 'p', acceptors: units.perCm3(1e16) }),
67
+ ohmic(0),
68
+ ],
69
+ grid: { hmin: units.nm(0.5), hmax: units.nm(20) },
70
+ }),
71
+ );
72
+
73
+ const eq = dev.solve();
74
+ console.log('built-in potential (V):', eq.phi[0] - eq.phi[eq.phi.length - 1]); // ≈ 0.795
75
+
76
+ dev.set({ contacts: { right: { V: 0.4 } } }); // p side at +0.4 V: forward bias
77
+ const on = dev.solve(); // warm-started from the previous solution
78
+ console.log('current (A/m²):', on.current); // negative: it flows toward −x, from p to n
79
+ ```
80
+
81
+ ## An electrochemical cell
82
+
83
+ Silver nitrate between two silver electrodes, as a macroscopic (strictly neutral) electrolyte.
84
+ A reversible electrode is a contact whose terminal species is the ion it exchanges: Ag⁺ in
85
+ equilibrium with the metal, NO₃⁻ blocked. Polarized, the cell's current saturates at the
86
+ limiting current, `i_lim = 4FD₊c₀/L` for this binary salt:
87
+
88
+ ```js
89
+ import { Device, FARADAY } from 'driftlet';
90
+ import { build, layer, aqueous } from 'driftlet/kit';
91
+
92
+ const L = 100e-6, c0 = 10; // m, mol/m³ (10 mM)
93
+ // Ag⁺ + e⁻ ⇌ Ag at the metal: V is the voltage of Ag⁺ there, i.e. the electrode potential.
94
+ const silver = (V) => ({ V, terminal: 'Ag+', species: { 'Ag+': 'equilibrium', 'NO3-': 'blocked' }, phi: 'bulk' });
95
+ const dev = new Device(
96
+ build({
97
+ library: [aqueous(['Ag+', 'NO3-'], { epsr: 0 })], // epsr 0: no double layers resolved
98
+ // Ag⁺ reaches the contacts and takes their level; NO₃⁻ doesn't, so it needs its amount.
99
+ stack: [silver(0), layer('water', L, { c0: { 'NO3-': c0 } }), silver(0)],
100
+ grid: { hmin: 10e-9, hmax: 2e-6 }, // graded: fine where Ag⁺ depletes at the electrode
101
+ }),
102
+ );
103
+ const iLim = (4 * FARADAY * 1.648e-9 * c0) / L;
104
+ for (const V of [0.05, 0.1, 0.2, 0.4]) {
105
+ dev.set({ contacts: { right: { V } } });
106
+ const sol = dev.solve();
107
+ // V > 0 raises Ag⁺'s level on the right: silver dissolves there and plates on the left, so the
108
+ // current flows toward −x.
109
+ console.log(`${V} V: I/i_lim = ${(-sol.current / iLim).toFixed(4)}`); // ≈ tanh(V/4V_T): 0.451, 0.750, 0.961, 0.999
110
+ }
111
+ ```
112
+
113
+ Solutions are plain objects of `Float64Array`s, ready to plot (fresh arrays from every solve):
114
+ `x`, `phi`, and per species `c`, `mu` (electrochemical potential μ̄), `muStd` (standard level)
115
+ and the voltage views `V`, `Vstd`. They also carry the terminals' voltages and currents, fluxes,
116
+ conservation bookkeeping and warnings; see [the device reference](docs/device.md). The
117
+ [kit](docs/kit.md) adds reactions written as equations (`'Ag+ + e- = Ag(s)'`), redox levels,
118
+ level diagrams, slider demos and `describe()`, which flags likely unit slips.
119
+
120
+ ## Demos
121
+
122
+ Live pages in [`demos/`](demos/), each building its device with `driftlet/kit`, solving it in the
123
+ browser as you move the controls, and showing its whole script as a worked example. Most draw the
124
+ closed-form theory alongside, as a check. They're live at
125
+ [markblundeberg.github.io/driftlet/demos](https://markblundeberg.github.io/driftlet/demos/); to run
126
+ them locally, serve the repository root (e.g. `python3 -m http.server`) and open `/demos/`.
127
+
128
+ | | | |
129
+ |---|---|---|
130
+ | [![pn junction](demos/screenshots/pn.png)](https://markblundeberg.github.io/driftlet/demos/pn.html) | [![solar cell](demos/screenshots/solar.png)](https://markblundeberg.github.io/driftlet/demos/solar.html) | [![MOS capacitor](demos/screenshots/mos.png)](https://markblundeberg.github.io/driftlet/demos/mos.html) |
131
+ | **pn junction**: quasi-Fermi levels under bias; I–V against Shockley | **Solar cell**: J<sub>sc</sub> and V<sub>oc</sub> against collection theory | **MOS capacitor**: band bending; C–V against ideal theory |
132
+ | [![cyclic voltammetry](demos/screenshots/redox.png)](https://markblundeberg.github.io/driftlet/demos/redox.html) | [![Daniell cell](demos/screenshots/daniell.png)](https://markblundeberg.github.io/driftlet/demos/daniell.html) | [![saturation](demos/screenshots/saturation.png)](https://markblundeberg.github.io/driftlet/demos/saturation.html) |
133
+ | **Cyclic voltammetry**: a Fermi level against a redox level | **Daniell cell**: a real salt bridge, leaking, ion by ion | **Saturation**: Ag \| AgNO₃ \| Ag under a square wave, up to its limiting current |
134
+ | [![impedance](demos/screenshots/impedance.png)](https://markblundeberg.github.io/driftlet/demos/impedance.html) | | |
135
+ | **Impedance**: the Warburg arc, and the cell's small-signal response inside | | |
136
+ | [![ion-exchange membrane](demos/screenshots/membrane.png)](https://markblundeberg.github.io/driftlet/demos/membrane.html) | [![double layer](demos/screenshots/double-layer.png)](https://markblundeberg.github.io/driftlet/demos/double-layer.html) | [![insertion host](demos/screenshots/insertion.png)](https://markblundeberg.github.io/driftlet/demos/insertion.html) |
137
+ | **Ion-exchange membrane**: Donnan steps, ion by ion, against TMS theory | **Double layer**: dilute vs crowded ions, against closed forms | **Intercalation host**: cycling between cutoffs against the OCV |
138
+
139
+ ## How to think about it
140
+
141
+ driftlet insists on thermodynamically honest concepts ([conventions](docs/conventions.md)):
142
+
143
+ - **Everything you set or measure is an electrochemical potential μ̄** (or a difference of
144
+ them). A bias sets Δμ̄ of electrons between terminals; a gate sets its metal's `μ̄_e⁻`. Nothing
145
+ you set is an electrostatic potential.
146
+ - **φ is bookkeeping.** Each material's standard chemical potentials anchor its own φ, and φ
147
+ jumps at every interface between different materials.
148
+ - **Band alignment is a property of each interface** and must be given explicitly: nothing is
149
+ assumed by default. If vacuum-level estimates are all you have, helpers in `driftlet/kit`
150
+ take an anchor and an offset per side (electron affinity, work function, absolute SHE
151
+ potential, …) and return the alignment that lines up the vacuum levels. The
152
+ [alignment guide](docs/alignment.md) covers the recipe and its caveats.
153
+ - The species voltage `V_i = μ̄_i / (z_i F)` and standard level `V°_i` are available as views,
154
+ alongside redox levels: [reading level diagrams](docs/visualization.md) explains them.
155
+
156
+ A device is a line of **regions** (each a **material** plus a length, fixed charge and
157
+ initial composition), joined at **interfaces**, with a **contact** at each end. The contacts
158
+ and any internal **ports** are its **terminals**, each held at a voltage or driven by a
159
+ current. Supported physics:
160
+
161
+ - any mix of charged and neutral species; per-material diffusivities and standard potentials;
162
+ species absent from some materials;
163
+ - ideal statistics by default, or per material: Fermi–Dirac (degenerate carriers), lattice
164
+ gas (crowding, site filling), Redlich–Kister, Debye–Hückel, intercalation hosts described by
165
+ their OCV curve, or your own function ([statistics](docs/statistics.md));
166
+ - Poisson electrostatics, or strictly neutral (ε = 0) materials such as macroscopic
167
+ electrolytes;
168
+ - conductor regions (metals, fast ion conductors) with only their carrier's level and a
169
+ conductivity: Schottky and MOS gates as regions, electrodes with reactions at their faces,
170
+ bipolar electrodes;
171
+ - contacts as outside phases with known levels, joined by the same laws as internal faces:
172
+ ohmic contacts, reversible electrodes, baths, pinned barriers, conductance links, gates and
173
+ Stern layers;
174
+ - interfaces with explicit alignment, blocking, interface resistance, and Butler–Volmer
175
+ reactions with participants on either side (electrode reactions, ion and electron transfer,
176
+ mixed potentials), and a choice of electrostatic law (pinned, neutral, Helmholtz);
177
+ - bulk reactions with thermodynamically consistent mass action (recombination, water
178
+ autoionisation, …);
179
+ - imposed flow (advection) and current-free eddy mixing;
180
+ - internal ports: reservoirs feeding a window of nodes, e.g. grounding a MOS channel;
181
+ - terminals held at a voltage, driven by a current (including open circuit) or behind a
182
+ resistance, with piecewise-linear waveforms (cyclic voltammetry); reference electrodes and
183
+ other internal terminals as ports; impedance at any terminal;
184
+ - steady states; transients by backward Euler, BDF2, or adaptive BDF2 with error control and
185
+ frame budgets for animation; exact conservation bookkeeping;
186
+ - small-signal impedance spectra Z(f), with complex profiles.
187
+
188
+ ## What it doesn't do
189
+
190
+ - **More than one dimension.** Planar 1D only, so no porous-electrode (P2D) models. For 2D/3D,
191
+ see [ChargeTransport.jl](https://github.com/WIAS-PDELib/ChargeTransport.jl), TCAD tools or
192
+ COMSOL.
193
+ - **Cross-diffusion.** Each species moves down its own μ̄ with its own D (the statistics can be
194
+ non-ideal); there are no Stefan–Maxwell or Onsager cross terms beyond eddy mixing.
195
+ - **Heat and optics.** Temperature is uniform. Generation is a reaction, uniform within a
196
+ material; there's no optical model or absorption profile. For those, see
197
+ [Driftfusion](https://github.com/barnesgroupICL/Driftfusion),
198
+ [SIMsalabim](https://github.com/kostergroup/SIMsalabim) or
199
+ [PyBaMM](https://github.com/pybamm-team/PyBaMM).
200
+ - **Field-dependent transport.** No high-field mobility, impact ionisation, field-enhanced
201
+ dissociation (second Wien effect) or tunnelling.
202
+ - **Fitting.** It solves forward problems; parameter estimation is up to you.
203
+
204
+ How it works numerically is in [numerics](docs/numerics.md).
205
+
206
+ ## Validation
207
+
208
+ Every physics feature is tested against analytic results (`npm test`, node's built-in runner).
209
+
210
+ | What | Checked against | Tolerance |
211
+ |---|---|---|
212
+ | Equilibrium of any device | μ̄ of every species flat; zero current | 1e-9 RT or better |
213
+ | Gouy–Chapman double layer | analytic charge and full nonlinear profile | 2e-3 |
214
+ | Debye screening (linear limit) | gate in series with `ε/λ_D` | 1e-3 |
215
+ | Donnan potential | analytic partition, between floating layers and real baths | 1e-8 |
216
+ | Heterointerfaces with unequal dipoles (A \| B \| A) | per-face analytic double-layer split | 3e-3 |
217
+ | Floating island | Gauss's law; conserved amounts through gate sweeps | 1e-12 |
218
+ | pn junction | exact built-in potential; depletion charge; short-diode J–V | 1e-12; 1%; 2e-3 |
219
+ | Long pn diode with recombination | Shockley J–V incl. depletion recombination | 2e-3 |
220
+ | Illuminated long pn diode | `J_sc = qG(L_n + L_p + W)` from a cold start; superposition at low injection | 5e-3; 1e-2 |
221
+ | Schottky barrier (metal region \| n-Si) | surface density from the alignment; depletion charge | 5e-3; 2% |
222
+ | Liquid junction, open circuit | cell EMF 2t₊(RT/F) ln(c₁/c₂); Planck diffusion potential | 1e-4 |
223
+ | Concentration polarization | `i = i_lim tanh(V/4V_T)`, incl. galvanostatic and load modes | 2e-4 |
224
+ | Butler–Volmer electrode | Nernst equilibrium; mixed kinetic/diffusion closed form | 5e-4 |
225
+ | Redox couple between inert electrodes | Nernst level of Fe³⁺/Fe²⁺; the couple's total conserved at equilibrium and under current | 1e-9; 1e-10 |
226
+ | Interface conductance and ion transfer | series 1/G; BV rate law at the interface state | 1e-4; 1e-8 |
227
+ | Mass action | `c(H⁺)c(OH⁻) = K_w` from standard potentials; moiety conservation | 1e-9 |
228
+ | Second law | free energy in through the terminals (Σ N μ̄ over every species) equals the dissipation, every term ≥ 0: a pn diode, an open-circuit junction running on chemical input alone, electrodes with a bipolar plate, a port and a contact behind conductances | 1e-10 |
229
+ | Transients | RC charging of a gated island; water relaxation rate | 1%; 2e-3 |
230
+ | Time integration | BE first order, BDF2 second order; adaptive error control | ratios 2, 4 |
231
+ | Advection and eddy mixing | exact convection–diffusion profile; `D + D_mix`; junction EMF unchanged by mixing | 1e-12; 1e-12; 1e-5 |
232
+ | Metal regions | ohmic; Schottky face and MOS gate equal their contact forms; Ag \| AgNO₃ \| Ag with metal electrodes; bipolar electrode at V/2 | 1e-12; 1e-9; 1e-9; 1e-9 |
233
+ | Internal ports | transmission line σV tanh(L/λ)/λ (O(h²)); held level; MOS low-frequency C–V with a grounded channel | 1e-4; 1e-9; 2e-3 |
234
+ | Impedance | Macdonald blocking-electrode spectrum, 100 Hz–1 GHz; finite-length Warburg (Ag \| AgNO₃ \| Ag); DC limit = differential resistance | 3e-4; 3e-5; 1e-6 |
235
+ | Conservation | per step, and against time-integrated contact fluxes | 1e-11 relative |
236
+ | Strictly neutral limit (ε = 0) | Planck EMF; polarization with no overlimiting; Donnan at neutral faces | 1e-5; 1e-4; 1e-9 |
237
+ | Fermi–Dirac statistics | `𝓕_{±1/2}` vs quadrature; degenerate bulk; accumulation charge via `𝓕_{3/2}` | 1e-13; 1e-12; 1e-3 |
238
+ | Crowded double layer (lattice gas) | Kilic–Bazant–Ajdari charge, up to `ψ = 40 V_T`; custom function reproduces it | 5e-4; 1e-10 |
239
+ | Non-ideal transport | steady flux −(D/L)ΔP of the grand potential (lattice exact, Redlich–Kister O(h²)) | 1e-12; 1e-4 |
240
+ | Debye–Hückel | junction EMF 2t₊(RT/F) ln(a₁/a₂) with activities | 1e-6 |
241
+ | Data library | ion μ° against the electrochemical series and `K_w`; D against limiting conductivities; band data against `n_i` | 10 mV; 0.5%; 15% |
242
+ | Intercalation host (OCV) | composition vs table and isotherm; chemical diffusion flux and relaxation rate | 1e-12; 1e-5; 1e-4 |
243
+
244
+ ## Performance
245
+
246
+ `npm run bench` runs typical interactive workloads and compares them with a committed
247
+ baseline. Matrix factorisations (≈ Newton iterations) and block work are deterministic and
248
+ checked in CI; times are from a desktop Ryzen 7600X in Node 22:
249
+
250
+ | Task | Time | Factorisations |
251
+ |---|---|---|
252
+ | Linear solve, 300 nodes × 7 unknowns (factor + solve) | 0.4 ms | 1 |
253
+ | pn diode (264 nodes): cold equilibrium | 2 ms | 9 |
254
+ | pn diode: I–V sweep 0 → 0.6 V, 31 points | 31 ms | 137 |
255
+ | pn diode: warm jump from +0.4 V to −1 V | 7 ms | 51 |
256
+ | pn diode: adaptive transient, 0 → 0.5 V, 100 ns | 150 ms | 1026 |
257
+ | pn diode: impedance at 20 frequencies | 8 ms | 21 |
258
+ | MOS with a metal gate: C–V sweep, 26 points | 11 ms | 126 |
259
+ | n-Si \| KCl (4 species, 3 unknowns per node): Mott–Schottky sweep, 31 points | 29 ms | 156 |
260
+ | n-Si \| KCl: impedance at 20 frequencies | 10 ms | 21 |
261
+ | Ag \| AgNO₃ \| Ag with double layers: sweep 0 → 0.1 V, 21 points | 12 ms | 81 |
262
+ | Ag \| AgNO₃ \| Ag, neutral: adaptive transient over 1 s | 29 ms | 287 |
263
+ | Silver electrodes with a bipolar plate: sweep 0 → 1 V, 11 points | 27 ms | 63 |
264
+
265
+ The library has no dependencies and does no DOM access, so it runs in a Web Worker. (A device
266
+ definition is plain data and can be posted to a worker. Devices using custom-function
267
+ statistics can't be, and must be built inside the worker.)
268
+
269
+ ## Development
270
+
271
+ No dependencies. Tests use node's built-in runner (Node ≥ 20):
272
+
273
+ ```sh
274
+ npm test # the validation suite
275
+ npm run bench # typical workloads against bench/baseline.json (--save to update it)
276
+ npm run types # TypeScript declarations from the JSDoc, into types/ (fetches TypeScript via npx)
277
+ ```
278
+
279
+ Design notes and plans: [numerics](docs/numerics.md), [conventions](docs/conventions.md),
280
+ [statistics](docs/statistics.md), [the kit](docs/kit.md), [reading level diagrams](docs/visualization.md),
281
+ [data](docs/data.md), [roadmap](ROADMAP.md).
282
+
283
+ ## Licence
284
+
285
+ [0BSD](LICENSE): do anything you like with it.
package/llms.txt ADDED
@@ -0,0 +1,299 @@
1
+ # driftlet: a guide for agents
2
+
3
+ driftlet is a dependency-free JavaScript library that solves 1D drift–diffusion–reaction
4
+ (Poisson–Nernst–Planck) problems fast enough for live web demos: semiconductor junctions,
5
+ electrochemical cells, double layers, membranes, mixed conductors. It runs in a browser, a Web
6
+ Worker or Node, from npm or a CDN.
7
+
8
+ This guide is for writing a demo or calculation with it. Every code block below runs in the
9
+ test suite. The full references are docs/device.md (the definition), docs/kit.md (the helpers),
10
+ docs/conventions.md (signs and levels) and docs/data.md (the data library).
11
+
12
+ ## Imports
13
+
14
+ From a CDN, pin the version:
15
+
16
+ ```js nocheck
17
+ import { Device, units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/index.js';
18
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/kit.js';
19
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/plot.js';
20
+ ```
21
+
22
+ From npm: `driftlet`, `driftlet/kit`, `driftlet/plot`.
23
+
24
+ ## What to know first
25
+
26
+ - **Units are SI**: m, mol/m³ (1 M = 1000 mol/m³), J/mol, m²/s, V, A/m², F/m², C/m³. Use
27
+ `units` to convert: `units.um(1)`, `units.perCm3(1e17)` (carriers per cm³ to mol/m³),
28
+ `units.eV(1.12)` (to J/mol), `units.cm2PerS(36)`, `units.molar(0.1)`.
29
+ - **Everything you set or read is an electrochemical potential μ̄** (or a voltage made from one).
30
+ A terminal's `V` is the voltage of its terminal species: for an electron contact,
31
+ V = −μ̄_e/F, what a voltmeter reads. φ is bookkeeping and never a control.
32
+ - **Solutions** give, per species, `c` (concentration), `mu` (μ̄), `V` (the species voltage
33
+ μ̄/(zF), a quasi-Fermi level for e⁻ and h⁺) and `Vstd` (the standard level, a band edge for e⁻
34
+ and h⁺). In voltage units the conduction band sits below the valence band. `NaN` marks where a
35
+ species is absent.
36
+ - **Current**: `sol.current` is positive toward +x through the device. `sol.terminals[name]` gives
37
+ each terminal's `{ V, current }`, current into the device.
38
+ - **Nothing physical is defaulted.** Every interface between different materials needs an
39
+ alignment (`dipole` or `step`), or `phi: 'neutral'` for a macroscopic model. Every metal face
40
+ next to a dielectric needs its φ law. Every species that doesn't reach a contact needs its
41
+ initial amount (`c0`). An error names the path and says what to give.
42
+ - **Check before trusting.** `sol.converged` must be true, and read `sol.warnings`.
43
+ `describe(def)` (kit) summarises a definition and flags likely unit slips.
44
+ - **Exports.** `driftlet`: `Device`, `units`, and the constants `FARADAY`, `GAS_CONSTANT`,
45
+ `EPS0`, `BOLTZMANN`, `ELEMENTARY_CHARGE`, `AVOGADRO` (there's no thermal-voltage constant:
46
+ V_T = GAS_CONSTANT·T/FARADAY). `driftlet/kit`: `build`, `layer`, `ohmic`, `bath`, `half`,
47
+ `level`, `SHE`, `describe`, `live`, `traces`, the data library (`aqueous`, `semiconductor`,
48
+ `metal`, `IONS`, …) and the vacuum-level helpers. `driftlet/plot`: `bandDiagram`, `levelChart`, `labelParts` (labels mark subscripts as `V_{e⁻}`).
49
+ - **Reactions** are written as equations, `{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }`, in
50
+ `new Device`, `build()` and `set()` alike. A coefficient is a number and a space (`2 e-`).
51
+ - **Rate constants carry units.** A bulk reaction's rate is `kf · Π c_reactants` in mol/(m³·s),
52
+ with c in mol/m³ (and 1e16 cm⁻³ is only 0.0166 mol/m³). Band-to-band recombination,
53
+ `e- + h+ = 0`, gives a minority lifetime τ = 1/(kf·N) with N the majority density in mol/m³:
54
+ `kf: 1e8` is τ ≈ 0.6 µs at 1e16 cm⁻³. Generation is a reaction from a reservoir, rate `kf` in
55
+ mol/(m³·s) (`units.perCm3` converts cm⁻³·s⁻¹ too). A face reaction's `k0` is in mol/(m²·s);
56
+ its exchange current density is F·k0 per electron at the reference concentrations.
57
+
58
+ ## Template: a semiconductor junction
59
+
60
+ ```js
61
+ import { Device, units } from 'driftlet';
62
+ import { build, layer, ohmic, semiconductor, describe } from 'driftlet/kit';
63
+
64
+ const def = build({
65
+ T: 300, // the library's semiconductor data are for 300 K
66
+ library: [semiconductor('Si')],
67
+ stack: [
68
+ ohmic(0), // e⁻ and h⁺ in equilibrium with a metal at 0 V
69
+ layer('Si', units.um(1), { name: 'n', donors: units.perCm3(1e17) }),
70
+ layer('Si', units.um(1), { name: 'p', acceptors: units.perCm3(1e16) }),
71
+ ohmic(0.5), // the p side at +0.5 V: forward bias
72
+ ],
73
+ bulkReactions: [{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }], // recombination: τ = 1/(kf·N) ≈ 0.6 µs at N = 1e16 cm⁻³
74
+ grid: { hmin: units.nm(1), hmax: units.nm(20) },
75
+ });
76
+ console.log(describe(def));
77
+ const dev = new Device(def);
78
+ const sol = dev.solve();
79
+ console.log(sol.converged, `I = ${sol.current.toFixed(1)} A/m²`); // negative: toward −x, p to n
80
+ // An I–V sweep: change the drive, solve again (warm-started).
81
+ for (const V of [0, 0.2, 0.4, 0.6]) {
82
+ dev.set({ contacts: { right: { V } } });
83
+ console.log(V, dev.solve().current);
84
+ }
85
+ ```
86
+
87
+ ## Template: an electrochemical cell
88
+
89
+ An electrode is a metal layer with the reaction at its face, behind a contact holding the
90
+ metal's electrons (`ohmic(V, ['e-'])`). Reactions are written as equations. A participant that
91
+ isn't a species (solid silver) is fixed by its μ.
92
+
93
+ ```js
94
+ import { Device } from 'driftlet';
95
+ import { build, layer, ohmic, half, level, SHE, aqueous, metal } from 'driftlet/kit';
96
+
97
+ const silver = half('Ag+ + e- = Ag(s)', { 'Ag(s)': 0 }); // μ of the element: 0 on table conventions
98
+ // A Stern layer at each metal face: its capacitance and the potential of zero charge are
99
+ // properties of that interface, so they're yours to choose.
100
+ const face = { phi: { type: 'capacitive', C: 0.2 }, zeroCharge: 0.1, reactions: [{ ...silver, k0: 1e-3, alpha: 0.5 }] };
101
+ const def = build({
102
+ library: [aqueous(['H+', 'Ag+', 'NO3-']), metal('Ag')],
103
+ stack: [
104
+ ohmic(0, ['e-']),
105
+ layer('Ag', 1e-6),
106
+ face,
107
+ layer('water', 20e-6, { c0: { 'H+': 1, 'Ag+': 10, 'NO3-': 11 } }),
108
+ face,
109
+ layer('Ag', 1e-6),
110
+ ohmic(0.05, ['e-']),
111
+ ],
112
+ grid: { hmin: 0.1e-9, hmax: 200e-9, ratio: 1.15 },
113
+ });
114
+ const sol = new Device(def).solve();
115
+ const mid = sol.x.length >> 1;
116
+ console.log(`I = ${sol.current.toFixed(2)} A/m²`);
117
+ // The silver couple's redox level, and the SHE level, mid-cell (electron voltages).
118
+ console.log(level(sol, silver)[mid] - level(sol, SHE, { standard: true })[mid]); // ≈ 0.80 + (RT/F) ln(c/1 M)
119
+ ```
120
+
121
+ ## Template: a solar cell
122
+
123
+ Light is a reservoir: `photon = e- + h+` with the photons' μ well above the gap generates at
124
+ the rate `kf` (uniform in the material). Open circuit is a terminal driven at `I: 0`, whose
125
+ voltage is then V_oc. The photocurrent flows toward +x here (holes to the p side on the right).
126
+
127
+ ```js
128
+ import { Device, units } from 'driftlet';
129
+ import { build, layer, ohmic, semiconductor } from 'driftlet/kit';
130
+
131
+ const cell = (right) =>
132
+ build({
133
+ T: 300,
134
+ library: [semiconductor('Si')],
135
+ stack: [
136
+ ohmic(0),
137
+ layer('Si', units.um(1), { name: 'n', donors: units.perCm3(1e18) }),
138
+ layer('Si', units.um(100), { name: 'p', acceptors: units.perCm3(1e16) }),
139
+ ohmic(right),
140
+ ],
141
+ bulkReactions: [
142
+ { equation: 'e- + h+ = 0', kf: { Si: 1e8 } }, // τ ≈ 0.6 µs in the p base: L ≈ 48 µm
143
+ { equation: 'photon = e- + h+', fixed: { photon: units.eV(3) }, kf: { Si: 50 } }, // G, mol/(m³·s)
144
+ ],
145
+ grid: { hmin: units.nm(1), hmax: units.um(1) },
146
+ });
147
+ const sc = new Device(cell(0)).solve(); // short circuit
148
+ const oc = new Device(cell({ I: 0 })).solve(); // open circuit
149
+ console.log(`J_sc = ${sc.current.toFixed(1)} A/m², V_oc = ${oc.terminals.right.V.toFixed(3)} V`);
150
+ ```
151
+
152
+ ## Template: an electrode in a stirred solution
153
+
154
+ One working electrode, a stagnant layer of solution, and the well-stirred bulk as a bath. The
155
+ bath's terminal is its reference species (Cl⁻ here, like a Ag/AgCl reference), so terminal
156
+ voltages are against that. With table μ° (μ°_H⁺ = 0), V − φ in the solution reads against SHE.
157
+ A terminal's current is into the device, so at the working electrode it's positive when
158
+ oxidising (anodic). An excess of supporting electrolyte (KCl) suppresses migration but doesn't
159
+ remove it: at 100:1 the limiting current is still about 1% off pure diffusion.
160
+
161
+ ```js
162
+ import { Device, GAS_CONSTANT, FARADAY } from 'driftlet';
163
+ import { build, layer, ohmic, bath, half, aqueous, metal } from 'driftlet/kit';
164
+
165
+ const iron = half('Fe3+ + e- = Fe2+');
166
+ const electrode = (drive) =>
167
+ build({
168
+ library: [aqueous(['K+', 'Cl-', 'Fe3+', 'Fe2+'], { epsr: 0 }), metal('Pt')],
169
+ stack: [
170
+ ohmic(drive, ['e-']), // the working electrode's terminal: the platinum's electrons
171
+ layer('Pt', 1e-6),
172
+ { reactions: [{ ...iron, k0: 1e-3 }] },
173
+ layer('water', 20e-6), // the stagnant layer
174
+ bath({ 'K+': 500, 'Cl-': 535, 'Fe3+': 5, 'Fe2+': 10 }, 'Cl-'), // the stirred bulk (neutral)
175
+ ],
176
+ grid: { hmin: 10e-9, hmax: 0.5e-6 },
177
+ });
178
+ // At rest (no current), the electrode sits at the couple's Nernst potential.
179
+ const rest = new Device(electrode({ I: 0 })).solve();
180
+ const E = rest.V['e-'][0] - rest.phi[rest.x.length - 1]; // against SHE
181
+ const VT = (GAS_CONSTANT * 298.15) / FARADAY;
182
+ console.log(`rest potential ${E.toFixed(4)} V vs SHE; E° + V_T ln(c₃/c₂) = ${(0.769 + VT * Math.log(5 / 10)).toFixed(4)} V`);
183
+ // Polarised 0.2 V positive of rest: oxidising Fe²⁺, toward its limiting current F·D·c/L.
184
+ const dev = new Device(electrode(rest.terminals.left.V + 0.2));
185
+ console.log(`anodic current ${dev.solve().terminals.left.current.toFixed(2)} A/m²`);
186
+ ```
187
+
188
+ ## Template: a live page with a slider
189
+
190
+ `live()` merges slider moves made during a solve, warm-starts each solve and keeps the last
191
+ good solution. `bandDiagram()` returns an SVG string. Pass `worker: true` to `live()` to solve in
192
+ a Web Worker.
193
+
194
+ ```html
195
+ <!doctype html>
196
+ <meta charset="utf-8">
197
+ <title>pn junction</title>
198
+ <label>bias <input id="V" type="range" min="-0.5" max="0.7" step="0.01" value="0"></label>
199
+ <span id="readout"></span>
200
+ <div id="figure"></div>
201
+ <script type="module">
202
+ import { units } from 'https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/index.js';
203
+ import { build, layer, ohmic, semiconductor, live } from 'https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/kit.js';
204
+ import { bandDiagram } from 'https://cdn.jsdelivr.net/npm/driftlet@0.1.0/src/plot.js';
205
+
206
+ const def = build({
207
+ T: 300,
208
+ library: [semiconductor('Si')],
209
+ stack: [ohmic(0), layer('Si', 1e-6, { donors: units.perCm3(1e17) }), layer('Si', 1e-6, { acceptors: units.perCm3(1e16) }), ohmic(0)],
210
+ bulkReactions: [{ equation: 'e- + h+ = 0', kf: { Si: 1e8 } }],
211
+ grid: { hmin: 1e-9, hmax: 20e-9 },
212
+ });
213
+ const slider = document.getElementById('V');
214
+ const dev = live(def, {
215
+ onsolution(sol, info) {
216
+ document.getElementById('figure').innerHTML = bandDiagram(sol, { title: `V = ${sol.terminals.right.V.toFixed(2)} V` });
217
+ document.getElementById('readout').textContent = info.failed ? 'no solution here' : `I = ${sol.current.toExponential(2)} A/m²`;
218
+ },
219
+ });
220
+ slider.addEventListener('input', () => dev.set({ contacts: { right: { V: +slider.value } } }));
221
+ </script>
222
+ ```
223
+
224
+ ## Reading level diagrams
225
+
226
+ What `bandDiagram()` and `traces()` draw, and how to explain it to a reader (docs/visualization.md
227
+ has the long form):
228
+
229
+ - **Fermi level** V_e⁻ = −μ̄_e⁻/F: the circuit voltage, what a voltmeter reads. A cell's voltage is
230
+ the difference between its terminals' V_e⁻.
231
+ - **Redox level** of Ox + z e⁻ ⇌ Red, `level(sol, half)`: V_e⁻(Ox/Red) = (μ̄_Ox − μ̄_Red)/(zF), where
232
+ an electrode exchanging electrons by that couple would sit in equilibrium with the local
233
+ composition. Drawn thick and dashed. At an electrode face, the step from the metal's Fermi level
234
+ to it is the surface overpotential; through the solution it slopes with ohmic drop and
235
+ concentration polarisation. Each couple has its own; they agree only in redox equilibrium.
236
+ - **Standard redox level** (`{ standard: true }`), thin and dashed: every participant at reference
237
+ concentration. The redox level sits RT/(zF) ln(a_Ox/a_Red) from it (Nernst, locally).
238
+ - **Electrode potentials** are gaps to the SHE's standard level V°_e⁻(SHE): E = V_e⁻(electrode) −
239
+ V°_e⁻(SHE), E° = V°_e⁻(Ox/Red) − V°_e⁻(SHE), η = E − E_eq. With the data library's μ°, V°_e⁻(SHE)
240
+ in water is φ, so `phi: true` draws it. It slopes under current: "vs SHE" needs a place (say,
241
+ the stirred bulk).
242
+ - **Species voltage** V_i = μ̄_i/(z_iF) (thick, solid) for every charged species: the Fermi level
243
+ for e⁻, a quasi-Fermi level for e⁻ and h⁺ in a semiconductor, what a reversible electrode reads
244
+ for an ion. A species moves down slopes in its own V_i (drift and diffusion together) and is in
245
+ equilibrium where it's flat. A step at a face means resistance there.
246
+ - **Standard level** V°_i = φ + μ°_i/(z_iF) (thin, solid): a band edge for e⁻ and h⁺; V_i − V°_i =
247
+ (RT/z_iF) ln(c/c_ref) for ideal species. Steps in V°_i at faces (Donnan, junctions, dipoles) are
248
+ bookkeeping. In volts the conduction band sits below the valence band.
249
+ - **What's left out.** φ isn't drawn: it's a per-material convention (meaningless in a metal), and
250
+ −eφ isn't any electron's energy; the V°_i ladder shows its variation within a material and means
251
+ something across faces too. The exceptions: across an insulator with no mobile species, a
252
+ plain sloped φ (`phi: true`) is the line to draw (its slope is the field), and in vacuum −eφ
253
+ really is an electron's energy. Nor is anything drawn against a reference: the diagrams are
254
+ reference-free (shifting everything changes nothing), every gap on the electron diagram is
255
+ physical, so a reader picks a level at a point as their reference. "vs SHE" and "from the vacuum"
256
+ need a place, since both levels vary in space.
257
+ - A gap V_i − V_j between two species is free of φ, but carries one constant fixed by the elements'
258
+ conventional zeros (μ = 0 per element). So `shifts` (marked ⌇) moves a species' lines to share a
259
+ plot without losing anything.
260
+ - Call V_i the species voltage and say what it is; it's new to most readers. Credit Jamnik and
261
+ Maier (Phys. Chem. Chem. Phys. 3, 1668 (2001)) where V_i rails explain impedance or linear
262
+ response.
263
+
264
+ ## Common mistakes
265
+
266
+ - **No alignment at a heterointerface.** `interfaces[0] (between … [Si] and … [GaAs]): an
267
+ interface between different materials needs an alignment`. Give the face `{ dipole }` or
268
+ `{ step: { species: 'e-', value } }` (the conduction-band offset, J/mol), or estimate one from
269
+ electron affinities with `vacuumDipole` (docs/alignment.md). `interfaces[f]` is the f-th face
270
+ of the stack, counting from the left.
271
+ - **A metal face with no φ law.** `… .phi: a face between a conductor and a material with
272
+ ε > 0 needs an explicit φ law`. Give `{ phi: { type: 'capacitive', C }, zeroCharge }`, or
273
+ `phi: 'neutral'`, or make the electrolyte strictly neutral (`aqueous(names, { epsr: 0 })`)
274
+ when double layers don't matter.
275
+ - **`ohmic()` on a metal.** Its default holds both e⁻ and h⁺, for semiconductors. On a metal
276
+ layer, write `ohmic(V, ['e-'])`.
277
+ - **Ions on both sides of a reacting face.** Label them: `'Li+(left) = Li+(right)'`, and give
278
+ their link (`species: { 'Li+': 'blocked' }` if they cross only by the reaction).
279
+ - **Unit slips.** D in cm²/s, μ° in eV, capacitance in µF/cm², T in °C. `describe(def)` flags
280
+ numbers that look like these.
281
+ - **A closed system with no amount.** A species that doesn't reach a contact needs `c0`.
282
+ - **A strictly neutral `c0` that doesn't balance.** With `epsr: 0`, Σ z·c0 must be zero (traces
283
+ included): `… carries a net charge of … mol/m³`. Add the counter-ion's share.
284
+ - **A sharp junction between very different solutions** (3 M KCl against 1 µM traces) can't
285
+ start: grade it over a few thin layers mixing one solution into the other, as at a frit.
286
+ - **A steady solve of a closed cell where every ion can cross.** Its steady state is complete
287
+ mixing. To watch a junction or a salt bridge, step in time from the unmixed `c0` (`advance`).
288
+ - **Everything floating.** At least one terminal must be held at a voltage `V`.
289
+ - **The library's semiconductors are at 300 K.** Give `T: 300`.
290
+ - **Sign of the current.** `sol.current` is positive toward +x: a forward-biased diode with its
291
+ p side on the right carries negative current. A terminal's current is into the device.
292
+ - **Sign of a voltage across a membrane.** `sol.terminalVoltage` is V_right − V_left. Physiology
293
+ quotes V_inside − V_outside, so put the inside on the right or flip the sign.
294
+ - **A coefficient without its space.** `'2e- + …'` reads `2e-` as a species name; the error says
295
+ to write `2 e-`.
296
+ - **Changing c0 with `set()`.** A carried state keeps what it holds; `c0` is only the starting
297
+ composition. Make a new `Device` to start over from it.
298
+ - **An unresolved double layer.** If `sol.warnings` says so, refine the grid (`hmin` below the
299
+ Debye length) or use a neutral model (`epsr: 0`, `phi: 'neutral'`).
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "driftlet",
3
+ "version": "0.1.0",
4
+ "description": "Small, dependency-free 1D drift–diffusion–reaction (Poisson–Nernst–Planck) solver for interactive web demos",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./types/index.d.ts",
9
+ "default": "./src/index.js"
10
+ },
11
+ "./kit": {
12
+ "types": "./types/kit.d.ts",
13
+ "default": "./src/kit.js"
14
+ },
15
+ "./plot": {
16
+ "types": "./types/plot.d.ts",
17
+ "default": "./src/plot.js"
18
+ }
19
+ },
20
+ "main": "./src/index.js",
21
+ "types": "./types/index.d.ts",
22
+ "files": [
23
+ "src",
24
+ "types",
25
+ "llms.txt"
26
+ ],
27
+ "sideEffects": false,
28
+ "scripts": {
29
+ "test": "node --test",
30
+ "bench": "node bench/bench.js",
31
+ "prepublishOnly": "npm test && npm run types",
32
+ "types": "rm -rf types && npx -y -p typescript@5 tsc --allowJs --declaration --emitDeclarationOnly --target es2022 --module nodenext --moduleResolution nodenext --outDir types src/index.js src/kit.js src/plot.js"
33
+ },
34
+ "engines": {
35
+ "node": ">=22"
36
+ },
37
+ "keywords": [
38
+ "drift-diffusion",
39
+ "poisson-nernst-planck",
40
+ "electrochemistry",
41
+ "semiconductor",
42
+ "simulation"
43
+ ],
44
+ "homepage": "https://markblundeberg.github.io/driftlet/demos/",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/markblundeberg/driftlet.git"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/markblundeberg/driftlet/issues"
51
+ },
52
+ "author": "Mark Lundeberg",
53
+ "license": "0BSD"
54
+ }