Metadata-Version: 2.4
Name: lmhdx
Version: 1.5.0
Summary: JAX-native inductionless MHD toolkit for research, benchmarking, and differentiable simulation
Author: Rogerio Jorge
License-Expression: MIT
Project-URL: Homepage, https://github.com/uwplasma/LMhdX
Project-URL: Documentation, https://lmx.readthedocs.io/
Project-URL: Issues, https://github.com/uwplasma/LMhdX/issues
Project-URL: Source, https://github.com/uwplasma/LMhdX
Keywords: magnetohydrodynamics,liquid-metal,jax,differentiable-simulation
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: <3.14,>=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: jax
Requires-Dist: numpy
Requires-Dist: solvax<1,>=0.19
Requires-Dist: tomli; python_version < "3.11"
Provides-Extra: dev
Requires-Dist: matplotlib; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: pytest-timeout; extra == "dev"
Requires-Dist: pytest-xdist; extra == "dev"
Requires-Dist: ruff<1,>=0.16; extra == "dev"
Provides-Extra: docs
Requires-Dist: sphinx; extra == "docs"
Requires-Dist: myst-parser; extra == "docs"
Requires-Dist: furo; extra == "docs"
Requires-Dist: sphinx-copybutton; extra == "docs"
Requires-Dist: sphinx-design; extra == "docs"
Provides-Extra: release
Requires-Dist: build; extra == "release"
Requires-Dist: twine; extra == "release"
Provides-Extra: visualization
Requires-Dist: matplotlib; extra == "visualization"
Dynamic: license-file

# LMhdX

**Differentiable inductionless liquid-metal MHD in JAX.**

[![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)
[![Docs](https://img.shields.io/readthedocs/lmx/latest?label=docs)](https://lmx.readthedocs.io/)
[![Python](https://img.shields.io/badge/python-3.10--3.13-3776ab.svg)](https://www.python.org/)
[![License](https://img.shields.io/github/license/uwplasma/LMhdX)](LICENSE)

LMhdX solves the flow of liquid metals in strong magnetic fields — the physics of
fusion blanket channels. Ducts and pipes with insulating or thin conducting
walls, three-dimensional channels entering a fringing field, and
quasi-two-dimensional vortex dynamics, all differentiable end to end.
Reusable solvers and implicit derivatives come from
[SOLVAX](https://github.com/uwplasma/SOLVAX).

- **Resolve the layers:** meshes chosen from `a/Ha` and `a/√Ha`, not from a cell count.
- **Skip the transient:** the steady state as a differentiable root, not a march.
- **Differentiate the continuous inputs:** drive and field strength on the duct solves; wall conductance and geometry on the extruded fringing route.
- **Check against something else:** an independent spectral solve that shares no code.
- **Run where you like:** CPU or GPU, one compiled trajectory per run.

![Quasi-2D MHD turbulence](docs/_static/q2d_turbulence_256.webp)

*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`.*

## Install

```console
pip install lmhdx
```

LMhdX was called LMX before version 1.5: `import lmx` is now `import lmhdx`.
From source:

```console
git clone https://github.com/uwplasma/LMhdX.git
cd LMhdX
pip install ".[visualization]"
lmhdx examples/hartmann_case.toml
```

JAX runs on the CPU by default; install the GPU wheel from the
[JAX guide](https://docs.jax.dev/en/latest/installation.html) and LMhdX uses it.

## Solve a duct in three lines

```python
import lmhdx

problem = lmhdx.duct_problem(hartmann=100.0, cells=48, wall_conductance=0.027)
solution = lmhdx.solve(problem)
```

`duct_problem` picks both transverse meshes from the layers the Hartmann number
implies — `a/Ha` against the walls normal to the field, `a/√Ha` against the
others — so the answer is converged rather than merely computed. `solve` finds
the steady state by preconditioned conjugate gradients, or by matrix-free
Newton–Krylov when advection or a conducting wall makes the problem
nonsymmetric; neither stores more than a restart cycle of vectors.

## Duct flows against an independent reference

![Hartmann layers, flow-rate error and mesh convergence](docs/_static/validation_ladder.webp)

```console
python scripts/make_showcase_figures.py --only ladder
```

- Hartmann profiles at Ha 20, 100 and 300 **collapse onto `1 − e^{−ξ}`** when
  plotted against the wall distance in layer widths; the points are a spectral
  solve, the lines are LMhdX.
- Flow rate within **0.4 – 2.3 %** on a fixed 48² mesh, across insulating walls
  and wall conductance 0.027 and 0.1.
- Second order in the mesh, so the error at a fixed mesh growing with the field
  is a resolution statement rather than a model one.
- The reference, [`validation/shercliff.py`](validation/shercliff.py), is
  Chebyshev collocation of the governing system converged to eight digits. It
  shares no operator, mesh or solver with the package, and at zero field it
  returns the analytic Poiseuille maximum `0.29468541`.

## Side layers at blanket-scale Hartmann numbers

![Hunt duct side-layer jets from Ha 20 to 1000](docs/_static/hunt_side_layers.webp)

```console
python examples/hunt_example.py
```

- Conducting Hartmann walls drive **jets in the side layers** that carry a
  growing share of the flow as the field rises.
- The jet maximum tracks `Ha^{−1/2}`, the side-layer thickness, over Ha 20 → 1000.
- The same steady solver reaches **Ha 1000** in the insulating duct, 0.5 % from
  the spectral reference on a wall-resolving 64² mesh.

## Pipes

![Pipe profiles, cross-section and flow rate against Hartmann number](docs/_static/pipe_flow.webp)

```console
python scripts/make_showcase_figures.py --only pipe
```

- A polar grid with the metric in `lmhdx.grid`, so the same flux-form operators
  solve a circular pipe. The axis needs no condition: the face at `r = 0` has
  zero area.
- The potential Poisson still factorizes exactly — a Fourier transform in the
  azimuth leaves each mode separable in `(r, z)`.
- `Q/A = 1/8` at zero field, the exact Hagen–Poiseuille value, at second order;
  `Q/A ∝ Ha^{−1}` once the field takes over.
- Within **0.04 – 0.43 %** of [`validation/pipe.py`](validation/pipe.py) at Ha 0
  to 100 — a Fourier–Chebyshev solve on the diameter, which removes the axis
  singularity by construction rather than treating it.

## Design with gradients

![Field, wall and geometry design with gradient descent](docs/_static/blanket_design_optimization.webp)

```console
python examples/variable_field_extruded_demo.py
```

```python
import jax, jax.numpy as jnp, lmhdx

problem = lmhdx.duct_problem(hartmann=20.0, cells=24)

def throughput(drive, field_scale):
    solution = lmhdx.solve_steady_state(problem, forcing=(drive, 0.0, 0.0), field_scale=field_scale)
    return jnp.mean(solution.velocity[0].data)

print(jax.grad(throughput, argnums=(0, 1))(1.0, 1.0))
```

- One adjoint solve at the root, through the implicit function theorem — not a
  tape of the iteration.
- Agrees with central differences to **7e-12** in the drive and **1.2e-10** in
  the field scale on the Ha ≤ 5 test ducts, where the test gate is 1e-6.
- `solve_steady_state` and `solve_fully_developed_fields` differentiate the drive
  and the field scale. Wall conductance and geometry are differentiable on the
  extruded fringing route of `lmhdx.fringing`, which the demo command above optimizes.
- A solve that stops short raises, rather than returning a plausible field and a
  gradient taken away from a root.

## Quasi-two-dimensional turbulence

![Q2D turbulence snapshots and energy spectrum](docs/_static/q2d_turbulence_poster.webp)

```console
python examples/q2d_turbulence_demo.py
```

- Vortex merging under Hartmann friction. The spectrum panel draws `k^{−3}` as a
  guide line, not a fitted slope.
- Energy and enstrophy budget identities checked on every run.
- The figures above come from `python scripts/make_showcase_figures.py --only q2d`:
  256² for 3,000 steps in about 20 s on a laptop CPU. The demo command runs 64²
  for 160 steps.

## Performance

![Time per step against problem size, CPU and GPU, both precisions](docs/_static/device_scaling.webp)

*The figure is from the uncontrolled 2026-09-07 run and is not yet redrawn from the tables below.*

```console
python scripts/run_benchmarks.py --output benchmarks/results/mine.json
python scripts/make_showcase_figures.py --only scaling
```

G4 is stated as absolute throughput ([ADR 0006](docs/adr/0006-review-2026-09-22.md), D23):
milliseconds per step and nanoseconds per cell per step, with the float64-accurate
mode (mixed precision) and true float32 reported separately. Measured on one idle
RTX A4000 (JAX 0.10.2, matmul precision `highest`, median of 12 timed runs;
[plan](plan.md) step 2.1):

| 3-D core, one A4000 | 64³ | 128³ | 192³ | 256³ |
|---|---|---|---|---|
| float64-accurate (mixed), ms per step | 2.14 | 17.5 | 69.7 | 166 |
| ns per cell per step | 8.2 | 8.4 | 9.8 | 9.9 |
| true float32, ms per step | 0.515 | 4.84 | 19.3 | 48.0 |
| ns per cell per step | 2.0 | 2.3 | 2.7 | 2.9 |

- **256³ fits on one 16 GB card** in every mode. Q2D at 2048² takes 75.9 ms per
  step in float64 and 16.0 ms in true float32.
- **Same-code CPU/GPU ratio:** against this JAX code on XLA:CPU on the host's 36
  cores in float64 (138 ms per step at 128³, controlled 2026-09-14 rows), the GPU's
  mixed mode is 7.85× faster. The baseline is XLA:CPU running this code, not a tuned
  CPU solver. The 10× float64 target on an A4000 is withdrawn: GA10x runs float64
  at 1/64 of its float32 rate, which bounds a fair single-card float64 speed-up
  near the memory-bandwidth ratio.
- **CPU reports:** `benchmarks/results/office-cpu-*.json` are from the 2026-09-07
  run, taken without load control or a recorded matmul precision; no ratio is
  quoted from them.
- **Trajectory-length scaling:** per step, 80 steps against 20 cost 0.83 in float64
  and 0.92 in float32; this timing ratio alone does not establish absence of host
  synchronization.
- Every number carries an `accepted` flag judged against the precision it was
  computed in; a run that lost its divergence-free constraint is reported, not quoted.

Two GPUs give the **same answer bit for bit** on the Q2D solve, and no speed-up:
the strong-scaling efficiency of an unaided placement is 0.20 in float64 and
0.10 in float32 at 2048², because the transforms all-gather every step across
PCIe. Correct, not yet faster — the numbers are in
[`benchmarks/results`](benchmarks/results) and the next step is in the [plan](plan.md).

## Comparison with other codes

| Comparison | What it establishes | Status |
|---|---|---|
| [`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 |
| [`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 % |
| Analytic Hartmann, Shercliff, Hunt and Poiseuille | Profiles and flow rates in every limit that has a closed form | `python examples/hartmann_example.py` |
| 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 |
| 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) |

The [validation record](https://lmx.readthedocs.io/en/latest/validation/index.html)
states each gate and what it does not cover.

## Examples

| Command | Physics |
|---|---|
| `lmhdx examples/hartmann_case.toml` | Hartmann duct from a TOML file, terminal diagnostics |
| `python examples/hartmann_example.py` | analytical error, conservation, mesh convergence |
| `python examples/hunt_example.py` | conducting walls, prescribed throughput and hydraulic power |
| `python examples/li_aln_wall_stack_example.py` | explicit wall material layers and interface currents |
| `python examples/fringing_benchmark_demo.py` | 3-D duct entering a magnetic field |
| `python examples/variable_field_extruded_demo.py` | gradient-based field, wall and geometry design |
| `python examples/q2d_turbulence_demo.py` | Q2D vorticity evolution, energy decay, movie |

Each example is one editable file that writes to `artifacts/examples/`;
parameters and evidence status are in [`examples/catalog.toml`](examples/catalog.toml).
`python scripts/make_showcase_figures.py` regenerates every figure above.

## What is validated, what is research

- **Validated:** Hartmann, Shercliff and Hunt ducts against an independent
  spectral solve and against analytical profiles; the pipe against a second,
  independent spectral solve over Ha 0 to 100; implicit adjoints against finite differences;
  the steady mechanical power balance within a 1e-10 relative test gate (measured
  3.6e-14 insulating, 6.3e-14 at wall conductance 0.027); Q2D decay identities.
- **Research stage:** three-dimensional convective transport (`advection="central"`
  or `"limited"`, from `lmhdx.advect`) is tested for conservation, order and
  boundedness but not validated against a reference flow, the
  ALEX B1/B2 fringing benchmarks have production acceptance open, and
  multi-device execution is not yet established. The
  [validation matrix](https://lmx.readthedocs.io/en/latest/validation/index.html)
  and the [plan](plan.md) state each gate.

## Documentation

[Install](https://lmx.readthedocs.io/en/latest/getting_started/install.html) ·
[Tutorials](https://lmx.readthedocs.io/en/latest/tutorials/fully_developed.html) ·
[Equations](https://lmx.readthedocs.io/en/latest/physics/equations.html) ·
[Validation](https://lmx.readthedocs.io/en/latest/validation/index.html) ·
[API](https://lmx.readthedocs.io/en/latest/reference/api.html) ·
[Roadmap](plan.md)

## Cite and contribute

Cite the commit or release you used; metadata is in [CITATION.cff](CITATION.cff).
Development: `pip install -e ".[dev,docs]"`, then
`python scripts/run_full_test_suite.py --changed-from HEAD`. See
[CONTRIBUTING.md](CONTRIBUTING.md).
