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.
Files changed (47) hide show
  1. lmhdx-1.5.0/LICENSE +21 -0
  2. lmhdx-1.5.0/MANIFEST.in +1 -0
  3. lmhdx-1.5.0/PKG-INFO +308 -0
  4. lmhdx-1.5.0/README.md +265 -0
  5. lmhdx-1.5.0/pyproject.toml +105 -0
  6. lmhdx-1.5.0/setup.cfg +4 -0
  7. lmhdx-1.5.0/src/lmhdx/__init__.py +178 -0
  8. lmhdx-1.5.0/src/lmhdx/__main__.py +6 -0
  9. lmhdx-1.5.0/src/lmhdx/_fringing_common.py +1278 -0
  10. lmhdx-1.5.0/src/lmhdx/_fringing_duct.py +1534 -0
  11. lmhdx-1.5.0/src/lmhdx/_fringing_pipe.py +1452 -0
  12. lmhdx-1.5.0/src/lmhdx/advect.py +186 -0
  13. lmhdx-1.5.0/src/lmhdx/bc.py +98 -0
  14. lmhdx-1.5.0/src/lmhdx/cases.py +1709 -0
  15. lmhdx-1.5.0/src/lmhdx/cli.py +586 -0
  16. lmhdx-1.5.0/src/lmhdx/core3d.py +652 -0
  17. lmhdx-1.5.0/src/lmhdx/coreflow.py +356 -0
  18. lmhdx-1.5.0/src/lmhdx/data/benchmarks/references/alex-b1-pipe.csv +17 -0
  19. lmhdx-1.5.0/src/lmhdx/data/benchmarks/references/alex-b2-square.csv +19 -0
  20. lmhdx-1.5.0/src/lmhdx/data/benchmarks/references/samper-table-i.toml +79 -0
  21. lmhdx-1.5.0/src/lmhdx/data/benchmarks/specs/alex-b1-pipe.toml +241 -0
  22. lmhdx-1.5.0/src/lmhdx/data/benchmarks/specs/alex-b2-square.toml +292 -0
  23. lmhdx-1.5.0/src/lmhdx/data/benchmarks/specs/hunt-ha20.toml +71 -0
  24. lmhdx-1.5.0/src/lmhdx/data/benchmarks/specs/shercliff-ha20.toml +69 -0
  25. lmhdx-1.5.0/src/lmhdx/design.py +200 -0
  26. lmhdx-1.5.0/src/lmhdx/em.py +314 -0
  27. lmhdx-1.5.0/src/lmhdx/fringing.py +1738 -0
  28. lmhdx-1.5.0/src/lmhdx/grid.py +373 -0
  29. lmhdx-1.5.0/src/lmhdx/io.py +1098 -0
  30. lmhdx-1.5.0/src/lmhdx/mesh.py +1099 -0
  31. lmhdx-1.5.0/src/lmhdx/ops.py +397 -0
  32. lmhdx-1.5.0/src/lmhdx/physics.py +483 -0
  33. lmhdx-1.5.0/src/lmhdx/pipe.py +247 -0
  34. lmhdx-1.5.0/src/lmhdx/poisson.py +1026 -0
  35. lmhdx-1.5.0/src/lmhdx/py.typed +1 -0
  36. lmhdx-1.5.0/src/lmhdx/q2d.py +410 -0
  37. lmhdx-1.5.0/src/lmhdx/solvers.py +1461 -0
  38. lmhdx-1.5.0/src/lmhdx/specs.py +891 -0
  39. lmhdx-1.5.0/src/lmhdx/steady.py +580 -0
  40. lmhdx-1.5.0/src/lmhdx/timeloop.py +245 -0
  41. lmhdx-1.5.0/src/lmhdx/validation.py +1367 -0
  42. lmhdx-1.5.0/src/lmhdx.egg-info/PKG-INFO +308 -0
  43. lmhdx-1.5.0/src/lmhdx.egg-info/SOURCES.txt +45 -0
  44. lmhdx-1.5.0/src/lmhdx.egg-info/dependency_links.txt +1 -0
  45. lmhdx-1.5.0/src/lmhdx.egg-info/entry_points.txt +2 -0
  46. lmhdx-1.5.0/src/lmhdx.egg-info/requires.txt +28 -0
  47. 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.
@@ -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
+ [![CI](https://img.shields.io/github/actions/workflow/status/uwplasma/LMhdX/ci.yml?branch=main&label=ci)](https://github.com/uwplasma/LMhdX/actions/workflows/ci.yml)
49
+ [![Docs](https://img.shields.io/readthedocs/lmx/latest?label=docs)](https://lmx.readthedocs.io/)
50
+ [![Python](https://img.shields.io/badge/python-3.10--3.13-3776ab.svg)](https://www.python.org/)
51
+ [![License](https://img.shields.io/github/license/uwplasma/LMhdX)](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
+ ![Quasi-2D MHD turbulence](docs/_static/q2d_turbulence_256.webp)
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
+ ![Hartmann layers, flow-rate error and mesh convergence](docs/_static/validation_ladder.webp)
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
+ ![Hunt duct side-layer jets from Ha 20 to 1000](docs/_static/hunt_side_layers.webp)
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
+ ![Pipe profiles, cross-section and flow rate against Hartmann number](docs/_static/pipe_flow.webp)
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
+ ![Field, wall and geometry design with gradient descent](docs/_static/blanket_design_optimization.webp)
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
+ ![Q2D turbulence snapshots and energy spectrum](docs/_static/q2d_turbulence_poster.webp)
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
+ ![Time per step against problem size, CPU and GPU, both precisions](docs/_static/device_scaling.webp)
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
+ [![CI](https://img.shields.io/github/actions/workflow/status/uwplasma/LMhdX/ci.yml?branch=main&label=ci)](https://github.com/uwplasma/LMhdX/actions/workflows/ci.yml)
6
+ [![Docs](https://img.shields.io/readthedocs/lmx/latest?label=docs)](https://lmx.readthedocs.io/)
7
+ [![Python](https://img.shields.io/badge/python-3.10--3.13-3776ab.svg)](https://www.python.org/)
8
+ [![License](https://img.shields.io/github/license/uwplasma/LMhdX)](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
+ ![Quasi-2D MHD turbulence](docs/_static/q2d_turbulence_256.webp)
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
+ ![Hartmann layers, flow-rate error and mesh convergence](docs/_static/validation_ladder.webp)
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
+ ![Hunt duct side-layer jets from Ha 20 to 1000](docs/_static/hunt_side_layers.webp)
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
+ ![Pipe profiles, cross-section and flow rate against Hartmann number](docs/_static/pipe_flow.webp)
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
+ ![Field, wall and geometry design with gradient descent](docs/_static/blanket_design_optimization.webp)
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
+ ![Q2D turbulence snapshots and energy spectrum](docs/_static/q2d_turbulence_poster.webp)
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
+ ![Time per step against problem size, CPU and GPU, both precisions](docs/_static/device_scaling.webp)
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).