lmhdx 1.5.0__tar.gz
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.
- lmhdx-1.5.0/LICENSE +21 -0
- lmhdx-1.5.0/MANIFEST.in +1 -0
- lmhdx-1.5.0/PKG-INFO +308 -0
- lmhdx-1.5.0/README.md +265 -0
- lmhdx-1.5.0/pyproject.toml +105 -0
- lmhdx-1.5.0/setup.cfg +4 -0
- lmhdx-1.5.0/src/lmhdx/__init__.py +178 -0
- lmhdx-1.5.0/src/lmhdx/__main__.py +6 -0
- lmhdx-1.5.0/src/lmhdx/_fringing_common.py +1278 -0
- lmhdx-1.5.0/src/lmhdx/_fringing_duct.py +1534 -0
- lmhdx-1.5.0/src/lmhdx/_fringing_pipe.py +1452 -0
- lmhdx-1.5.0/src/lmhdx/advect.py +186 -0
- lmhdx-1.5.0/src/lmhdx/bc.py +98 -0
- lmhdx-1.5.0/src/lmhdx/cases.py +1709 -0
- lmhdx-1.5.0/src/lmhdx/cli.py +586 -0
- lmhdx-1.5.0/src/lmhdx/core3d.py +652 -0
- lmhdx-1.5.0/src/lmhdx/coreflow.py +356 -0
- lmhdx-1.5.0/src/lmhdx/data/benchmarks/references/alex-b1-pipe.csv +17 -0
- lmhdx-1.5.0/src/lmhdx/data/benchmarks/references/alex-b2-square.csv +19 -0
- lmhdx-1.5.0/src/lmhdx/data/benchmarks/references/samper-table-i.toml +79 -0
- lmhdx-1.5.0/src/lmhdx/data/benchmarks/specs/alex-b1-pipe.toml +241 -0
- lmhdx-1.5.0/src/lmhdx/data/benchmarks/specs/alex-b2-square.toml +292 -0
- lmhdx-1.5.0/src/lmhdx/data/benchmarks/specs/hunt-ha20.toml +71 -0
- lmhdx-1.5.0/src/lmhdx/data/benchmarks/specs/shercliff-ha20.toml +69 -0
- lmhdx-1.5.0/src/lmhdx/design.py +200 -0
- lmhdx-1.5.0/src/lmhdx/em.py +314 -0
- lmhdx-1.5.0/src/lmhdx/fringing.py +1738 -0
- lmhdx-1.5.0/src/lmhdx/grid.py +373 -0
- lmhdx-1.5.0/src/lmhdx/io.py +1098 -0
- lmhdx-1.5.0/src/lmhdx/mesh.py +1099 -0
- lmhdx-1.5.0/src/lmhdx/ops.py +397 -0
- lmhdx-1.5.0/src/lmhdx/physics.py +483 -0
- lmhdx-1.5.0/src/lmhdx/pipe.py +247 -0
- lmhdx-1.5.0/src/lmhdx/poisson.py +1026 -0
- lmhdx-1.5.0/src/lmhdx/py.typed +1 -0
- lmhdx-1.5.0/src/lmhdx/q2d.py +410 -0
- lmhdx-1.5.0/src/lmhdx/solvers.py +1461 -0
- lmhdx-1.5.0/src/lmhdx/specs.py +891 -0
- lmhdx-1.5.0/src/lmhdx/steady.py +580 -0
- lmhdx-1.5.0/src/lmhdx/timeloop.py +245 -0
- lmhdx-1.5.0/src/lmhdx/validation.py +1367 -0
- lmhdx-1.5.0/src/lmhdx.egg-info/PKG-INFO +308 -0
- lmhdx-1.5.0/src/lmhdx.egg-info/SOURCES.txt +45 -0
- lmhdx-1.5.0/src/lmhdx.egg-info/dependency_links.txt +1 -0
- lmhdx-1.5.0/src/lmhdx.egg-info/entry_points.txt +2 -0
- lmhdx-1.5.0/src/lmhdx.egg-info/requires.txt +28 -0
- lmhdx-1.5.0/src/lmhdx.egg-info/top_level.txt +1 -0
lmhdx-1.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LMhdX contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lmhdx-1.5.0/MANIFEST.in
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prune tests
|
lmhdx-1.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lmhdx
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: JAX-native inductionless MHD toolkit for research, benchmarking, and differentiable simulation
|
|
5
|
+
Author: Rogerio Jorge
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/uwplasma/LMhdX
|
|
8
|
+
Project-URL: Documentation, https://lmx.readthedocs.io/
|
|
9
|
+
Project-URL: Issues, https://github.com/uwplasma/LMhdX/issues
|
|
10
|
+
Project-URL: Source, https://github.com/uwplasma/LMhdX
|
|
11
|
+
Keywords: magnetohydrodynamics,liquid-metal,jax,differentiable-simulation
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: <3.14,>=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: jax
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: solvax<1,>=0.19
|
|
23
|
+
Requires-Dist: tomli; python_version < "3.11"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: matplotlib; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-timeout; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-xdist; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff<1,>=0.16; extra == "dev"
|
|
31
|
+
Provides-Extra: docs
|
|
32
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
33
|
+
Requires-Dist: myst-parser; extra == "docs"
|
|
34
|
+
Requires-Dist: furo; extra == "docs"
|
|
35
|
+
Requires-Dist: sphinx-copybutton; extra == "docs"
|
|
36
|
+
Requires-Dist: sphinx-design; extra == "docs"
|
|
37
|
+
Provides-Extra: release
|
|
38
|
+
Requires-Dist: build; extra == "release"
|
|
39
|
+
Requires-Dist: twine; extra == "release"
|
|
40
|
+
Provides-Extra: visualization
|
|
41
|
+
Requires-Dist: matplotlib; extra == "visualization"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# LMhdX
|
|
45
|
+
|
|
46
|
+
**Differentiable inductionless liquid-metal MHD in JAX.**
|
|
47
|
+
|
|
48
|
+
[](https://github.com/uwplasma/LMhdX/actions/workflows/ci.yml)
|
|
49
|
+
[](https://lmx.readthedocs.io/)
|
|
50
|
+
[](https://www.python.org/)
|
|
51
|
+
[](LICENSE)
|
|
52
|
+
|
|
53
|
+
LMhdX solves the flow of liquid metals in strong magnetic fields — the physics of
|
|
54
|
+
fusion blanket channels. Ducts and pipes with insulating or thin conducting
|
|
55
|
+
walls, three-dimensional channels entering a fringing field, and
|
|
56
|
+
quasi-two-dimensional vortex dynamics, all differentiable end to end.
|
|
57
|
+
Reusable solvers and implicit derivatives come from
|
|
58
|
+
[SOLVAX](https://github.com/uwplasma/SOLVAX).
|
|
59
|
+
|
|
60
|
+
- **Resolve the layers:** meshes chosen from `a/Ha` and `a/√Ha`, not from a cell count.
|
|
61
|
+
- **Skip the transient:** the steady state as a differentiable root, not a march.
|
|
62
|
+
- **Differentiate the continuous inputs:** drive and field strength on the duct solves; wall conductance and geometry on the extruded fringing route.
|
|
63
|
+
- **Check against something else:** an independent spectral solve that shares no code.
|
|
64
|
+
- **Run where you like:** CPU or GPU, one compiled trajectory per run.
|
|
65
|
+
|
|
66
|
+

|
|
67
|
+
|
|
68
|
+
*Decaying quasi-2D MHD turbulence with Hartmann-layer friction — 256², 3,000 steps, about 20 s on a laptop CPU with `python scripts/make_showcase_figures.py --only q2d`.*
|
|
69
|
+
|
|
70
|
+
## Install
|
|
71
|
+
|
|
72
|
+
```console
|
|
73
|
+
pip install lmhdx
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
LMhdX was called LMX before version 1.5: `import lmx` is now `import lmhdx`.
|
|
77
|
+
From source:
|
|
78
|
+
|
|
79
|
+
```console
|
|
80
|
+
git clone https://github.com/uwplasma/LMhdX.git
|
|
81
|
+
cd LMhdX
|
|
82
|
+
pip install ".[visualization]"
|
|
83
|
+
lmhdx examples/hartmann_case.toml
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
JAX runs on the CPU by default; install the GPU wheel from the
|
|
87
|
+
[JAX guide](https://docs.jax.dev/en/latest/installation.html) and LMhdX uses it.
|
|
88
|
+
|
|
89
|
+
## Solve a duct in three lines
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import lmhdx
|
|
93
|
+
|
|
94
|
+
problem = lmhdx.duct_problem(hartmann=100.0, cells=48, wall_conductance=0.027)
|
|
95
|
+
solution = lmhdx.solve(problem)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`duct_problem` picks both transverse meshes from the layers the Hartmann number
|
|
99
|
+
implies — `a/Ha` against the walls normal to the field, `a/√Ha` against the
|
|
100
|
+
others — so the answer is converged rather than merely computed. `solve` finds
|
|
101
|
+
the steady state by preconditioned conjugate gradients, or by matrix-free
|
|
102
|
+
Newton–Krylov when advection or a conducting wall makes the problem
|
|
103
|
+
nonsymmetric; neither stores more than a restart cycle of vectors.
|
|
104
|
+
|
|
105
|
+
## Duct flows against an independent reference
|
|
106
|
+
|
|
107
|
+

|
|
108
|
+
|
|
109
|
+
```console
|
|
110
|
+
python scripts/make_showcase_figures.py --only ladder
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- Hartmann profiles at Ha 20, 100 and 300 **collapse onto `1 − e^{−ξ}`** when
|
|
114
|
+
plotted against the wall distance in layer widths; the points are a spectral
|
|
115
|
+
solve, the lines are LMhdX.
|
|
116
|
+
- Flow rate within **0.4 – 2.3 %** on a fixed 48² mesh, across insulating walls
|
|
117
|
+
and wall conductance 0.027 and 0.1.
|
|
118
|
+
- Second order in the mesh, so the error at a fixed mesh growing with the field
|
|
119
|
+
is a resolution statement rather than a model one.
|
|
120
|
+
- The reference, [`validation/shercliff.py`](validation/shercliff.py), is
|
|
121
|
+
Chebyshev collocation of the governing system converged to eight digits. It
|
|
122
|
+
shares no operator, mesh or solver with the package, and at zero field it
|
|
123
|
+
returns the analytic Poiseuille maximum `0.29468541`.
|
|
124
|
+
|
|
125
|
+
## Side layers at blanket-scale Hartmann numbers
|
|
126
|
+
|
|
127
|
+

|
|
128
|
+
|
|
129
|
+
```console
|
|
130
|
+
python examples/hunt_example.py
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- Conducting Hartmann walls drive **jets in the side layers** that carry a
|
|
134
|
+
growing share of the flow as the field rises.
|
|
135
|
+
- The jet maximum tracks `Ha^{−1/2}`, the side-layer thickness, over Ha 20 → 1000.
|
|
136
|
+
- The same steady solver reaches **Ha 1000** in the insulating duct, 0.5 % from
|
|
137
|
+
the spectral reference on a wall-resolving 64² mesh.
|
|
138
|
+
|
|
139
|
+
## Pipes
|
|
140
|
+
|
|
141
|
+

|
|
142
|
+
|
|
143
|
+
```console
|
|
144
|
+
python scripts/make_showcase_figures.py --only pipe
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- A polar grid with the metric in `lmhdx.grid`, so the same flux-form operators
|
|
148
|
+
solve a circular pipe. The axis needs no condition: the face at `r = 0` has
|
|
149
|
+
zero area.
|
|
150
|
+
- The potential Poisson still factorizes exactly — a Fourier transform in the
|
|
151
|
+
azimuth leaves each mode separable in `(r, z)`.
|
|
152
|
+
- `Q/A = 1/8` at zero field, the exact Hagen–Poiseuille value, at second order;
|
|
153
|
+
`Q/A ∝ Ha^{−1}` once the field takes over.
|
|
154
|
+
- Within **0.04 – 0.43 %** of [`validation/pipe.py`](validation/pipe.py) at Ha 0
|
|
155
|
+
to 100 — a Fourier–Chebyshev solve on the diameter, which removes the axis
|
|
156
|
+
singularity by construction rather than treating it.
|
|
157
|
+
|
|
158
|
+
## Design with gradients
|
|
159
|
+
|
|
160
|
+

|
|
161
|
+
|
|
162
|
+
```console
|
|
163
|
+
python examples/variable_field_extruded_demo.py
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
import jax, jax.numpy as jnp, lmhdx
|
|
168
|
+
|
|
169
|
+
problem = lmhdx.duct_problem(hartmann=20.0, cells=24)
|
|
170
|
+
|
|
171
|
+
def throughput(drive, field_scale):
|
|
172
|
+
solution = lmhdx.solve_steady_state(problem, forcing=(drive, 0.0, 0.0), field_scale=field_scale)
|
|
173
|
+
return jnp.mean(solution.velocity[0].data)
|
|
174
|
+
|
|
175
|
+
print(jax.grad(throughput, argnums=(0, 1))(1.0, 1.0))
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- One adjoint solve at the root, through the implicit function theorem — not a
|
|
179
|
+
tape of the iteration.
|
|
180
|
+
- Agrees with central differences to **7e-12** in the drive and **1.2e-10** in
|
|
181
|
+
the field scale on the Ha ≤ 5 test ducts, where the test gate is 1e-6.
|
|
182
|
+
- `solve_steady_state` and `solve_fully_developed_fields` differentiate the drive
|
|
183
|
+
and the field scale. Wall conductance and geometry are differentiable on the
|
|
184
|
+
extruded fringing route of `lmhdx.fringing`, which the demo command above optimizes.
|
|
185
|
+
- A solve that stops short raises, rather than returning a plausible field and a
|
|
186
|
+
gradient taken away from a root.
|
|
187
|
+
|
|
188
|
+
## Quasi-two-dimensional turbulence
|
|
189
|
+
|
|
190
|
+

|
|
191
|
+
|
|
192
|
+
```console
|
|
193
|
+
python examples/q2d_turbulence_demo.py
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- Vortex merging under Hartmann friction. The spectrum panel draws `k^{−3}` as a
|
|
197
|
+
guide line, not a fitted slope.
|
|
198
|
+
- Energy and enstrophy budget identities checked on every run.
|
|
199
|
+
- The figures above come from `python scripts/make_showcase_figures.py --only q2d`:
|
|
200
|
+
256² for 3,000 steps in about 20 s on a laptop CPU. The demo command runs 64²
|
|
201
|
+
for 160 steps.
|
|
202
|
+
|
|
203
|
+
## Performance
|
|
204
|
+
|
|
205
|
+

|
|
206
|
+
|
|
207
|
+
*The figure is from the uncontrolled 2026-09-07 run and is not yet redrawn from the tables below.*
|
|
208
|
+
|
|
209
|
+
```console
|
|
210
|
+
python scripts/run_benchmarks.py --output benchmarks/results/mine.json
|
|
211
|
+
python scripts/make_showcase_figures.py --only scaling
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
G4 is stated as absolute throughput ([ADR 0006](docs/adr/0006-review-2026-09-22.md), D23):
|
|
215
|
+
milliseconds per step and nanoseconds per cell per step, with the float64-accurate
|
|
216
|
+
mode (mixed precision) and true float32 reported separately. Measured on one idle
|
|
217
|
+
RTX A4000 (JAX 0.10.2, matmul precision `highest`, median of 12 timed runs;
|
|
218
|
+
[plan](plan.md) step 2.1):
|
|
219
|
+
|
|
220
|
+
| 3-D core, one A4000 | 64³ | 128³ | 192³ | 256³ |
|
|
221
|
+
|---|---|---|---|---|
|
|
222
|
+
| float64-accurate (mixed), ms per step | 2.14 | 17.5 | 69.7 | 166 |
|
|
223
|
+
| ns per cell per step | 8.2 | 8.4 | 9.8 | 9.9 |
|
|
224
|
+
| true float32, ms per step | 0.515 | 4.84 | 19.3 | 48.0 |
|
|
225
|
+
| ns per cell per step | 2.0 | 2.3 | 2.7 | 2.9 |
|
|
226
|
+
|
|
227
|
+
- **256³ fits on one 16 GB card** in every mode. Q2D at 2048² takes 75.9 ms per
|
|
228
|
+
step in float64 and 16.0 ms in true float32.
|
|
229
|
+
- **Same-code CPU/GPU ratio:** against this JAX code on XLA:CPU on the host's 36
|
|
230
|
+
cores in float64 (138 ms per step at 128³, controlled 2026-09-14 rows), the GPU's
|
|
231
|
+
mixed mode is 7.85× faster. The baseline is XLA:CPU running this code, not a tuned
|
|
232
|
+
CPU solver. The 10× float64 target on an A4000 is withdrawn: GA10x runs float64
|
|
233
|
+
at 1/64 of its float32 rate, which bounds a fair single-card float64 speed-up
|
|
234
|
+
near the memory-bandwidth ratio.
|
|
235
|
+
- **CPU reports:** `benchmarks/results/office-cpu-*.json` are from the 2026-09-07
|
|
236
|
+
run, taken without load control or a recorded matmul precision; no ratio is
|
|
237
|
+
quoted from them.
|
|
238
|
+
- **Trajectory-length scaling:** per step, 80 steps against 20 cost 0.83 in float64
|
|
239
|
+
and 0.92 in float32; this timing ratio alone does not establish absence of host
|
|
240
|
+
synchronization.
|
|
241
|
+
- Every number carries an `accepted` flag judged against the precision it was
|
|
242
|
+
computed in; a run that lost its divergence-free constraint is reported, not quoted.
|
|
243
|
+
|
|
244
|
+
Two GPUs give the **same answer bit for bit** on the Q2D solve, and no speed-up:
|
|
245
|
+
the strong-scaling efficiency of an unaided placement is 0.20 in float64 and
|
|
246
|
+
0.10 in float32 at 2048², because the transforms all-gather every step across
|
|
247
|
+
PCIe. Correct, not yet faster — the numbers are in
|
|
248
|
+
[`benchmarks/results`](benchmarks/results) and the next step is in the [plan](plan.md).
|
|
249
|
+
|
|
250
|
+
## Comparison with other codes
|
|
251
|
+
|
|
252
|
+
| Comparison | What it establishes | Status |
|
|
253
|
+
|---|---|---|
|
|
254
|
+
| [`validation/shercliff.py`](validation/shercliff.py) spectral solve | Duct flow rates, insulating and Hunt walls, Ha 0 → 1000 | independent of the package; 0.4 – 2.3 % on the meshes above |
|
|
255
|
+
| [`validation/pipe.py`](validation/pipe.py) spectral solve | Pipe flow rates, insulating and conducting walls, Ha 0 → 100 | independent of the package; 0.04 – 0.43 % |
|
|
256
|
+
| Analytic Hartmann, Shercliff, Hunt and Poiseuille | Profiles and flow rates in every limit that has a closed form | `python examples/hartmann_example.py` |
|
|
257
|
+
| FreeMHD (OpenFOAM `epotFoam`), pinned [`freemhd_install`](https://github.com/rogeriojorge/freemhd_install) image, B2 case | Same observed contract, executed by both codes | passes: transverse pressure difference RMS 0.0045, max 0.0109, against frozen bounds 0.16 and 0.32 — an integration check on a harness mesh, **not** a production result |
|
|
258
|
+
| ALEX B1 pipe and B2 square duct experiments | Fringing-field pressure drop | production acceptance **open**; specs and digitised references are frozen in [`src/lmhdx/data/benchmarks`](src/lmhdx/data/benchmarks) |
|
|
259
|
+
|
|
260
|
+
The [validation record](https://lmx.readthedocs.io/en/latest/validation/index.html)
|
|
261
|
+
states each gate and what it does not cover.
|
|
262
|
+
|
|
263
|
+
## Examples
|
|
264
|
+
|
|
265
|
+
| Command | Physics |
|
|
266
|
+
|---|---|
|
|
267
|
+
| `lmhdx examples/hartmann_case.toml` | Hartmann duct from a TOML file, terminal diagnostics |
|
|
268
|
+
| `python examples/hartmann_example.py` | analytical error, conservation, mesh convergence |
|
|
269
|
+
| `python examples/hunt_example.py` | conducting walls, prescribed throughput and hydraulic power |
|
|
270
|
+
| `python examples/li_aln_wall_stack_example.py` | explicit wall material layers and interface currents |
|
|
271
|
+
| `python examples/fringing_benchmark_demo.py` | 3-D duct entering a magnetic field |
|
|
272
|
+
| `python examples/variable_field_extruded_demo.py` | gradient-based field, wall and geometry design |
|
|
273
|
+
| `python examples/q2d_turbulence_demo.py` | Q2D vorticity evolution, energy decay, movie |
|
|
274
|
+
|
|
275
|
+
Each example is one editable file that writes to `artifacts/examples/`;
|
|
276
|
+
parameters and evidence status are in [`examples/catalog.toml`](examples/catalog.toml).
|
|
277
|
+
`python scripts/make_showcase_figures.py` regenerates every figure above.
|
|
278
|
+
|
|
279
|
+
## What is validated, what is research
|
|
280
|
+
|
|
281
|
+
- **Validated:** Hartmann, Shercliff and Hunt ducts against an independent
|
|
282
|
+
spectral solve and against analytical profiles; the pipe against a second,
|
|
283
|
+
independent spectral solve over Ha 0 to 100; implicit adjoints against finite differences;
|
|
284
|
+
the steady mechanical power balance within a 1e-10 relative test gate (measured
|
|
285
|
+
3.6e-14 insulating, 6.3e-14 at wall conductance 0.027); Q2D decay identities.
|
|
286
|
+
- **Research stage:** three-dimensional convective transport (`advection="central"`
|
|
287
|
+
or `"limited"`, from `lmhdx.advect`) is tested for conservation, order and
|
|
288
|
+
boundedness but not validated against a reference flow, the
|
|
289
|
+
ALEX B1/B2 fringing benchmarks have production acceptance open, and
|
|
290
|
+
multi-device execution is not yet established. The
|
|
291
|
+
[validation matrix](https://lmx.readthedocs.io/en/latest/validation/index.html)
|
|
292
|
+
and the [plan](plan.md) state each gate.
|
|
293
|
+
|
|
294
|
+
## Documentation
|
|
295
|
+
|
|
296
|
+
[Install](https://lmx.readthedocs.io/en/latest/getting_started/install.html) ·
|
|
297
|
+
[Tutorials](https://lmx.readthedocs.io/en/latest/tutorials/fully_developed.html) ·
|
|
298
|
+
[Equations](https://lmx.readthedocs.io/en/latest/physics/equations.html) ·
|
|
299
|
+
[Validation](https://lmx.readthedocs.io/en/latest/validation/index.html) ·
|
|
300
|
+
[API](https://lmx.readthedocs.io/en/latest/reference/api.html) ·
|
|
301
|
+
[Roadmap](plan.md)
|
|
302
|
+
|
|
303
|
+
## Cite and contribute
|
|
304
|
+
|
|
305
|
+
Cite the commit or release you used; metadata is in [CITATION.cff](CITATION.cff).
|
|
306
|
+
Development: `pip install -e ".[dev,docs]"`, then
|
|
307
|
+
`python scripts/run_full_test_suite.py --changed-from HEAD`. See
|
|
308
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
lmhdx-1.5.0/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# LMhdX
|
|
2
|
+
|
|
3
|
+
**Differentiable inductionless liquid-metal MHD in JAX.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/uwplasma/LMhdX/actions/workflows/ci.yml)
|
|
6
|
+
[](https://lmx.readthedocs.io/)
|
|
7
|
+
[](https://www.python.org/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
LMhdX solves the flow of liquid metals in strong magnetic fields — the physics of
|
|
11
|
+
fusion blanket channels. Ducts and pipes with insulating or thin conducting
|
|
12
|
+
walls, three-dimensional channels entering a fringing field, and
|
|
13
|
+
quasi-two-dimensional vortex dynamics, all differentiable end to end.
|
|
14
|
+
Reusable solvers and implicit derivatives come from
|
|
15
|
+
[SOLVAX](https://github.com/uwplasma/SOLVAX).
|
|
16
|
+
|
|
17
|
+
- **Resolve the layers:** meshes chosen from `a/Ha` and `a/√Ha`, not from a cell count.
|
|
18
|
+
- **Skip the transient:** the steady state as a differentiable root, not a march.
|
|
19
|
+
- **Differentiate the continuous inputs:** drive and field strength on the duct solves; wall conductance and geometry on the extruded fringing route.
|
|
20
|
+
- **Check against something else:** an independent spectral solve that shares no code.
|
|
21
|
+
- **Run where you like:** CPU or GPU, one compiled trajectory per run.
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
*Decaying quasi-2D MHD turbulence with Hartmann-layer friction — 256², 3,000 steps, about 20 s on a laptop CPU with `python scripts/make_showcase_figures.py --only q2d`.*
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```console
|
|
30
|
+
pip install lmhdx
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
LMhdX was called LMX before version 1.5: `import lmx` is now `import lmhdx`.
|
|
34
|
+
From source:
|
|
35
|
+
|
|
36
|
+
```console
|
|
37
|
+
git clone https://github.com/uwplasma/LMhdX.git
|
|
38
|
+
cd LMhdX
|
|
39
|
+
pip install ".[visualization]"
|
|
40
|
+
lmhdx examples/hartmann_case.toml
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
JAX runs on the CPU by default; install the GPU wheel from the
|
|
44
|
+
[JAX guide](https://docs.jax.dev/en/latest/installation.html) and LMhdX uses it.
|
|
45
|
+
|
|
46
|
+
## Solve a duct in three lines
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import lmhdx
|
|
50
|
+
|
|
51
|
+
problem = lmhdx.duct_problem(hartmann=100.0, cells=48, wall_conductance=0.027)
|
|
52
|
+
solution = lmhdx.solve(problem)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`duct_problem` picks both transverse meshes from the layers the Hartmann number
|
|
56
|
+
implies — `a/Ha` against the walls normal to the field, `a/√Ha` against the
|
|
57
|
+
others — so the answer is converged rather than merely computed. `solve` finds
|
|
58
|
+
the steady state by preconditioned conjugate gradients, or by matrix-free
|
|
59
|
+
Newton–Krylov when advection or a conducting wall makes the problem
|
|
60
|
+
nonsymmetric; neither stores more than a restart cycle of vectors.
|
|
61
|
+
|
|
62
|
+
## Duct flows against an independent reference
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
```console
|
|
67
|
+
python scripts/make_showcase_figures.py --only ladder
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- Hartmann profiles at Ha 20, 100 and 300 **collapse onto `1 − e^{−ξ}`** when
|
|
71
|
+
plotted against the wall distance in layer widths; the points are a spectral
|
|
72
|
+
solve, the lines are LMhdX.
|
|
73
|
+
- Flow rate within **0.4 – 2.3 %** on a fixed 48² mesh, across insulating walls
|
|
74
|
+
and wall conductance 0.027 and 0.1.
|
|
75
|
+
- Second order in the mesh, so the error at a fixed mesh growing with the field
|
|
76
|
+
is a resolution statement rather than a model one.
|
|
77
|
+
- The reference, [`validation/shercliff.py`](validation/shercliff.py), is
|
|
78
|
+
Chebyshev collocation of the governing system converged to eight digits. It
|
|
79
|
+
shares no operator, mesh or solver with the package, and at zero field it
|
|
80
|
+
returns the analytic Poiseuille maximum `0.29468541`.
|
|
81
|
+
|
|
82
|
+
## Side layers at blanket-scale Hartmann numbers
|
|
83
|
+
|
|
84
|
+

|
|
85
|
+
|
|
86
|
+
```console
|
|
87
|
+
python examples/hunt_example.py
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- Conducting Hartmann walls drive **jets in the side layers** that carry a
|
|
91
|
+
growing share of the flow as the field rises.
|
|
92
|
+
- The jet maximum tracks `Ha^{−1/2}`, the side-layer thickness, over Ha 20 → 1000.
|
|
93
|
+
- The same steady solver reaches **Ha 1000** in the insulating duct, 0.5 % from
|
|
94
|
+
the spectral reference on a wall-resolving 64² mesh.
|
|
95
|
+
|
|
96
|
+
## Pipes
|
|
97
|
+
|
|
98
|
+

|
|
99
|
+
|
|
100
|
+
```console
|
|
101
|
+
python scripts/make_showcase_figures.py --only pipe
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- A polar grid with the metric in `lmhdx.grid`, so the same flux-form operators
|
|
105
|
+
solve a circular pipe. The axis needs no condition: the face at `r = 0` has
|
|
106
|
+
zero area.
|
|
107
|
+
- The potential Poisson still factorizes exactly — a Fourier transform in the
|
|
108
|
+
azimuth leaves each mode separable in `(r, z)`.
|
|
109
|
+
- `Q/A = 1/8` at zero field, the exact Hagen–Poiseuille value, at second order;
|
|
110
|
+
`Q/A ∝ Ha^{−1}` once the field takes over.
|
|
111
|
+
- Within **0.04 – 0.43 %** of [`validation/pipe.py`](validation/pipe.py) at Ha 0
|
|
112
|
+
to 100 — a Fourier–Chebyshev solve on the diameter, which removes the axis
|
|
113
|
+
singularity by construction rather than treating it.
|
|
114
|
+
|
|
115
|
+
## Design with gradients
|
|
116
|
+
|
|
117
|
+

|
|
118
|
+
|
|
119
|
+
```console
|
|
120
|
+
python examples/variable_field_extruded_demo.py
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import jax, jax.numpy as jnp, lmhdx
|
|
125
|
+
|
|
126
|
+
problem = lmhdx.duct_problem(hartmann=20.0, cells=24)
|
|
127
|
+
|
|
128
|
+
def throughput(drive, field_scale):
|
|
129
|
+
solution = lmhdx.solve_steady_state(problem, forcing=(drive, 0.0, 0.0), field_scale=field_scale)
|
|
130
|
+
return jnp.mean(solution.velocity[0].data)
|
|
131
|
+
|
|
132
|
+
print(jax.grad(throughput, argnums=(0, 1))(1.0, 1.0))
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- One adjoint solve at the root, through the implicit function theorem — not a
|
|
136
|
+
tape of the iteration.
|
|
137
|
+
- Agrees with central differences to **7e-12** in the drive and **1.2e-10** in
|
|
138
|
+
the field scale on the Ha ≤ 5 test ducts, where the test gate is 1e-6.
|
|
139
|
+
- `solve_steady_state` and `solve_fully_developed_fields` differentiate the drive
|
|
140
|
+
and the field scale. Wall conductance and geometry are differentiable on the
|
|
141
|
+
extruded fringing route of `lmhdx.fringing`, which the demo command above optimizes.
|
|
142
|
+
- A solve that stops short raises, rather than returning a plausible field and a
|
|
143
|
+
gradient taken away from a root.
|
|
144
|
+
|
|
145
|
+
## Quasi-two-dimensional turbulence
|
|
146
|
+
|
|
147
|
+

|
|
148
|
+
|
|
149
|
+
```console
|
|
150
|
+
python examples/q2d_turbulence_demo.py
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- Vortex merging under Hartmann friction. The spectrum panel draws `k^{−3}` as a
|
|
154
|
+
guide line, not a fitted slope.
|
|
155
|
+
- Energy and enstrophy budget identities checked on every run.
|
|
156
|
+
- The figures above come from `python scripts/make_showcase_figures.py --only q2d`:
|
|
157
|
+
256² for 3,000 steps in about 20 s on a laptop CPU. The demo command runs 64²
|
|
158
|
+
for 160 steps.
|
|
159
|
+
|
|
160
|
+
## Performance
|
|
161
|
+
|
|
162
|
+

|
|
163
|
+
|
|
164
|
+
*The figure is from the uncontrolled 2026-09-07 run and is not yet redrawn from the tables below.*
|
|
165
|
+
|
|
166
|
+
```console
|
|
167
|
+
python scripts/run_benchmarks.py --output benchmarks/results/mine.json
|
|
168
|
+
python scripts/make_showcase_figures.py --only scaling
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
G4 is stated as absolute throughput ([ADR 0006](docs/adr/0006-review-2026-09-22.md), D23):
|
|
172
|
+
milliseconds per step and nanoseconds per cell per step, with the float64-accurate
|
|
173
|
+
mode (mixed precision) and true float32 reported separately. Measured on one idle
|
|
174
|
+
RTX A4000 (JAX 0.10.2, matmul precision `highest`, median of 12 timed runs;
|
|
175
|
+
[plan](plan.md) step 2.1):
|
|
176
|
+
|
|
177
|
+
| 3-D core, one A4000 | 64³ | 128³ | 192³ | 256³ |
|
|
178
|
+
|---|---|---|---|---|
|
|
179
|
+
| float64-accurate (mixed), ms per step | 2.14 | 17.5 | 69.7 | 166 |
|
|
180
|
+
| ns per cell per step | 8.2 | 8.4 | 9.8 | 9.9 |
|
|
181
|
+
| true float32, ms per step | 0.515 | 4.84 | 19.3 | 48.0 |
|
|
182
|
+
| ns per cell per step | 2.0 | 2.3 | 2.7 | 2.9 |
|
|
183
|
+
|
|
184
|
+
- **256³ fits on one 16 GB card** in every mode. Q2D at 2048² takes 75.9 ms per
|
|
185
|
+
step in float64 and 16.0 ms in true float32.
|
|
186
|
+
- **Same-code CPU/GPU ratio:** against this JAX code on XLA:CPU on the host's 36
|
|
187
|
+
cores in float64 (138 ms per step at 128³, controlled 2026-09-14 rows), the GPU's
|
|
188
|
+
mixed mode is 7.85× faster. The baseline is XLA:CPU running this code, not a tuned
|
|
189
|
+
CPU solver. The 10× float64 target on an A4000 is withdrawn: GA10x runs float64
|
|
190
|
+
at 1/64 of its float32 rate, which bounds a fair single-card float64 speed-up
|
|
191
|
+
near the memory-bandwidth ratio.
|
|
192
|
+
- **CPU reports:** `benchmarks/results/office-cpu-*.json` are from the 2026-09-07
|
|
193
|
+
run, taken without load control or a recorded matmul precision; no ratio is
|
|
194
|
+
quoted from them.
|
|
195
|
+
- **Trajectory-length scaling:** per step, 80 steps against 20 cost 0.83 in float64
|
|
196
|
+
and 0.92 in float32; this timing ratio alone does not establish absence of host
|
|
197
|
+
synchronization.
|
|
198
|
+
- Every number carries an `accepted` flag judged against the precision it was
|
|
199
|
+
computed in; a run that lost its divergence-free constraint is reported, not quoted.
|
|
200
|
+
|
|
201
|
+
Two GPUs give the **same answer bit for bit** on the Q2D solve, and no speed-up:
|
|
202
|
+
the strong-scaling efficiency of an unaided placement is 0.20 in float64 and
|
|
203
|
+
0.10 in float32 at 2048², because the transforms all-gather every step across
|
|
204
|
+
PCIe. Correct, not yet faster — the numbers are in
|
|
205
|
+
[`benchmarks/results`](benchmarks/results) and the next step is in the [plan](plan.md).
|
|
206
|
+
|
|
207
|
+
## Comparison with other codes
|
|
208
|
+
|
|
209
|
+
| Comparison | What it establishes | Status |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| [`validation/shercliff.py`](validation/shercliff.py) spectral solve | Duct flow rates, insulating and Hunt walls, Ha 0 → 1000 | independent of the package; 0.4 – 2.3 % on the meshes above |
|
|
212
|
+
| [`validation/pipe.py`](validation/pipe.py) spectral solve | Pipe flow rates, insulating and conducting walls, Ha 0 → 100 | independent of the package; 0.04 – 0.43 % |
|
|
213
|
+
| Analytic Hartmann, Shercliff, Hunt and Poiseuille | Profiles and flow rates in every limit that has a closed form | `python examples/hartmann_example.py` |
|
|
214
|
+
| FreeMHD (OpenFOAM `epotFoam`), pinned [`freemhd_install`](https://github.com/rogeriojorge/freemhd_install) image, B2 case | Same observed contract, executed by both codes | passes: transverse pressure difference RMS 0.0045, max 0.0109, against frozen bounds 0.16 and 0.32 — an integration check on a harness mesh, **not** a production result |
|
|
215
|
+
| ALEX B1 pipe and B2 square duct experiments | Fringing-field pressure drop | production acceptance **open**; specs and digitised references are frozen in [`src/lmhdx/data/benchmarks`](src/lmhdx/data/benchmarks) |
|
|
216
|
+
|
|
217
|
+
The [validation record](https://lmx.readthedocs.io/en/latest/validation/index.html)
|
|
218
|
+
states each gate and what it does not cover.
|
|
219
|
+
|
|
220
|
+
## Examples
|
|
221
|
+
|
|
222
|
+
| Command | Physics |
|
|
223
|
+
|---|---|
|
|
224
|
+
| `lmhdx examples/hartmann_case.toml` | Hartmann duct from a TOML file, terminal diagnostics |
|
|
225
|
+
| `python examples/hartmann_example.py` | analytical error, conservation, mesh convergence |
|
|
226
|
+
| `python examples/hunt_example.py` | conducting walls, prescribed throughput and hydraulic power |
|
|
227
|
+
| `python examples/li_aln_wall_stack_example.py` | explicit wall material layers and interface currents |
|
|
228
|
+
| `python examples/fringing_benchmark_demo.py` | 3-D duct entering a magnetic field |
|
|
229
|
+
| `python examples/variable_field_extruded_demo.py` | gradient-based field, wall and geometry design |
|
|
230
|
+
| `python examples/q2d_turbulence_demo.py` | Q2D vorticity evolution, energy decay, movie |
|
|
231
|
+
|
|
232
|
+
Each example is one editable file that writes to `artifacts/examples/`;
|
|
233
|
+
parameters and evidence status are in [`examples/catalog.toml`](examples/catalog.toml).
|
|
234
|
+
`python scripts/make_showcase_figures.py` regenerates every figure above.
|
|
235
|
+
|
|
236
|
+
## What is validated, what is research
|
|
237
|
+
|
|
238
|
+
- **Validated:** Hartmann, Shercliff and Hunt ducts against an independent
|
|
239
|
+
spectral solve and against analytical profiles; the pipe against a second,
|
|
240
|
+
independent spectral solve over Ha 0 to 100; implicit adjoints against finite differences;
|
|
241
|
+
the steady mechanical power balance within a 1e-10 relative test gate (measured
|
|
242
|
+
3.6e-14 insulating, 6.3e-14 at wall conductance 0.027); Q2D decay identities.
|
|
243
|
+
- **Research stage:** three-dimensional convective transport (`advection="central"`
|
|
244
|
+
or `"limited"`, from `lmhdx.advect`) is tested for conservation, order and
|
|
245
|
+
boundedness but not validated against a reference flow, the
|
|
246
|
+
ALEX B1/B2 fringing benchmarks have production acceptance open, and
|
|
247
|
+
multi-device execution is not yet established. The
|
|
248
|
+
[validation matrix](https://lmx.readthedocs.io/en/latest/validation/index.html)
|
|
249
|
+
and the [plan](plan.md) state each gate.
|
|
250
|
+
|
|
251
|
+
## Documentation
|
|
252
|
+
|
|
253
|
+
[Install](https://lmx.readthedocs.io/en/latest/getting_started/install.html) ·
|
|
254
|
+
[Tutorials](https://lmx.readthedocs.io/en/latest/tutorials/fully_developed.html) ·
|
|
255
|
+
[Equations](https://lmx.readthedocs.io/en/latest/physics/equations.html) ·
|
|
256
|
+
[Validation](https://lmx.readthedocs.io/en/latest/validation/index.html) ·
|
|
257
|
+
[API](https://lmx.readthedocs.io/en/latest/reference/api.html) ·
|
|
258
|
+
[Roadmap](plan.md)
|
|
259
|
+
|
|
260
|
+
## Cite and contribute
|
|
261
|
+
|
|
262
|
+
Cite the commit or release you used; metadata is in [CITATION.cff](CITATION.cff).
|
|
263
|
+
Development: `pip install -e ".[dev,docs]"`, then
|
|
264
|
+
`python scripts/run_full_test_suite.py --changed-from HEAD`. See
|
|
265
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|