vmex 0.2.0__py3-none-any.whl
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.
- vmec_jax/__init__.py +45 -0
- vmex/__init__.py +131 -0
- vmex/__main__.py +14 -0
- vmex/_compat.py +310 -0
- vmex/core/__init__.py +29 -0
- vmex/core/bootstrap.py +985 -0
- vmex/core/boozer.py +155 -0
- vmex/core/boozer_tables.py +183 -0
- vmex/core/cli.py +835 -0
- vmex/core/device.py +171 -0
- vmex/core/errors.py +119 -0
- vmex/core/fields.py +703 -0
- vmex/core/forces.py +913 -0
- vmex/core/fourier.py +371 -0
- vmex/core/freeboundary.py +1194 -0
- vmex/core/freeboundary_diff.py +727 -0
- vmex/core/geometry.py +386 -0
- vmex/core/implicit.py +1291 -0
- vmex/core/input.py +649 -0
- vmex/core/mgrid.py +456 -0
- vmex/core/multigrid.py +299 -0
- vmex/core/nyquist.py +1013 -0
- vmex/core/omnigenity.py +589 -0
- vmex/core/optimize.py +1906 -0
- vmex/core/parallel.py +154 -0
- vmex/core/plotting.py +693 -0
- vmex/core/postprocess.py +617 -0
- vmex/core/preconditioner.py +697 -0
- vmex/core/preconditioner_2d.py +179 -0
- vmex/core/printing.py +145 -0
- vmex/core/profiles.py +634 -0
- vmex/core/residuals.py +598 -0
- vmex/core/setup.py +999 -0
- vmex/core/solver.py +1520 -0
- vmex/core/stability.py +432 -0
- vmex/core/statephysics.py +458 -0
- vmex/core/step.py +168 -0
- vmex/core/transforms.py +837 -0
- vmex/core/turbulence.py +571 -0
- vmex/core/vacuum.py +869 -0
- vmex/core/wout.py +865 -0
- vmex/doctor.py +204 -0
- vmex/mirror/__init__.py +84 -0
- vmex/mirror/analytic.py +368 -0
- vmex/mirror/basis.py +600 -0
- vmex/mirror/exterior.py +954 -0
- vmex/mirror/forces.py +885 -0
- vmex/mirror/free_boundary.py +934 -0
- vmex/mirror/geometry.py +596 -0
- vmex/mirror/implicit.py +536 -0
- vmex/mirror/model.py +242 -0
- vmex/mirror/output.py +1015 -0
- vmex/mirror/solver.py +639 -0
- vmex/mirror/splines.py +1177 -0
- vmex/resources/input.nfp4_QH_warm_start +47 -0
- vmex-0.2.0.dist-info/METADATA +615 -0
- vmex-0.2.0.dist-info/RECORD +61 -0
- vmex-0.2.0.dist-info/WHEEL +5 -0
- vmex-0.2.0.dist-info/entry_points.txt +3 -0
- vmex-0.2.0.dist-info/licenses/LICENSE +21 -0
- vmex-0.2.0.dist-info/top_level.txt +2 -0
vmec_jax/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Compatibility shim: ``vmec_jax`` was renamed to :mod:`vmex`.
|
|
2
|
+
|
|
3
|
+
Importing ``vmec_jax`` re-exports everything from :mod:`vmex` and emits a
|
|
4
|
+
``DeprecationWarning``. Submodule access (``vmec_jax.core.solver`` etc.) is
|
|
5
|
+
forwarded by aliasing the already-imported ``vmex`` submodules into
|
|
6
|
+
``sys.modules`` under the old names. This shim is a one-release courtesy;
|
|
7
|
+
update imports to ``vmex``.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib
|
|
12
|
+
import sys
|
|
13
|
+
import warnings
|
|
14
|
+
|
|
15
|
+
warnings.warn(
|
|
16
|
+
"the 'vmec_jax' package has been renamed to 'vmex'; import 'vmex' instead "
|
|
17
|
+
"(this compatibility shim will be removed in a future release)",
|
|
18
|
+
DeprecationWarning,
|
|
19
|
+
stacklevel=2,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
import vmex as _vmex
|
|
23
|
+
|
|
24
|
+
# Re-export the public API.
|
|
25
|
+
from vmex import * # noqa: F401,F403
|
|
26
|
+
__version__ = _vmex.__version__
|
|
27
|
+
if hasattr(_vmex, "__all__"):
|
|
28
|
+
__all__ = list(_vmex.__all__)
|
|
29
|
+
|
|
30
|
+
# Forward submodule access: alias every imported vmex submodule under the old
|
|
31
|
+
# top-level name so `import vmec_jax.core.X` and `from vmec_jax.core import X`
|
|
32
|
+
# resolve to the identical vmex module object.
|
|
33
|
+
for _name, _mod in list(sys.modules.items()):
|
|
34
|
+
if _name == "vmex" or _name.startswith("vmex."):
|
|
35
|
+
sys.modules["vmec_jax" + _name[len("vmex"):]] = _mod
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def __getattr__(name: str):
|
|
39
|
+
"""Lazily import and forward any vmex submodule not yet loaded."""
|
|
40
|
+
try:
|
|
41
|
+
mod = importlib.import_module(f"vmex.{name}")
|
|
42
|
+
except ModuleNotFoundError as exc: # pragma: no cover - mirror vmex
|
|
43
|
+
raise AttributeError(name) from exc
|
|
44
|
+
sys.modules[f"vmec_jax.{name}"] = mod
|
|
45
|
+
return mod
|
vmex/__init__.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""vmex: a JAX implementation of VMEC2000 for fixed and free-boundary equilibria.
|
|
2
|
+
|
|
3
|
+
Public API (lazily imported; ``import vmex as vj``):
|
|
4
|
+
|
|
5
|
+
- :class:`~vmex.core.input.VmecInput` — INDATA / VMEC++-JSON input pytree
|
|
6
|
+
- :func:`~vmex.core.solver.solve` — single-grid fixed-boundary solve
|
|
7
|
+
- :func:`~vmex.core.multigrid.solve_multigrid` — NS_ARRAY ladder (runvmec.f)
|
|
8
|
+
- :func:`~vmex.core.freeboundary.solve_free_boundary` — NESTOR free boundary
|
|
9
|
+
- :func:`~vmex.core.wout.read_wout` / :func:`~vmex.core.wout.write_wout`
|
|
10
|
+
/ :func:`~vmex.core.wout.wout_from_state` / :class:`~vmex.core.wout.WoutData`
|
|
11
|
+
- :func:`~vmex.core.plotting.plot_wout` / :func:`~vmex.core.plotting.plot_boozmn`
|
|
12
|
+
- :func:`~vmex.core.boozer.run_booz_xform` — Boozer transform (booz_xform_jax)
|
|
13
|
+
- :func:`~vmex.core.mgrid.read_mgrid` / :func:`~vmex.core.mgrid.write_mgrid`
|
|
14
|
+
/ :class:`~vmex.core.mgrid.MgridField` (external field is an mgrid or any
|
|
15
|
+
``xyz->B`` callable; coils live in ESSOS, ``essos.coils.Coils``)
|
|
16
|
+
- ``vmex.optimize`` — objectives + least-squares driver (module)
|
|
17
|
+
- ``vmex.implicit`` — implicit differentiation of the equilibrium (module)
|
|
18
|
+
- ``vmex.parallel`` — concurrent ensembles of independent solves (module)
|
|
19
|
+
- ``vmex.errors`` — typed zero-crash exceptions (also exported directly)
|
|
20
|
+
|
|
21
|
+
The ``vmec`` console entry point lives in :mod:`vmex.core.cli`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from importlib import import_module as _import_module
|
|
25
|
+
from importlib.metadata import PackageNotFoundError as _PackageNotFoundError
|
|
26
|
+
from importlib.metadata import version as _package_version
|
|
27
|
+
import os as _os
|
|
28
|
+
from pathlib import Path as _Path
|
|
29
|
+
|
|
30
|
+
from ._compat import _default_compilation_cache_dir as _default_jax_cache_dir
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _source_tree_version() -> str | None:
|
|
34
|
+
pyproject = _Path(__file__).resolve().parents[1] / "pyproject.toml"
|
|
35
|
+
if not pyproject.exists():
|
|
36
|
+
return None
|
|
37
|
+
in_project = False
|
|
38
|
+
for raw_line in pyproject.read_text(encoding="utf-8").splitlines():
|
|
39
|
+
line = raw_line.strip()
|
|
40
|
+
if line == "[project]":
|
|
41
|
+
in_project = True
|
|
42
|
+
continue
|
|
43
|
+
if in_project and line.startswith("["):
|
|
44
|
+
return None
|
|
45
|
+
if in_project and line.startswith("version"):
|
|
46
|
+
return line.split("=", 1)[1].strip().strip('"')
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
__version__ = _source_tree_version() or _package_version("vmex")
|
|
52
|
+
except _PackageNotFoundError: # pragma: no cover - source tree without installed metadata.
|
|
53
|
+
__version__ = "0+unknown"
|
|
54
|
+
|
|
55
|
+
# Suppress noisy C++ warnings from XLA/PjRt backend (e.g. repeated
|
|
56
|
+
# "Assume version compatibility. PjRt-IFRT does not track XLA executable
|
|
57
|
+
# versions." on persistent-cache hits). Must be set before *any* ``import
|
|
58
|
+
# jax`` in the process. Uses setdefault so the user can still override via the
|
|
59
|
+
# environment.
|
|
60
|
+
_os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2")
|
|
61
|
+
_os.environ.setdefault("ABSL_MIN_LOG_LEVEL", "2")
|
|
62
|
+
_os.environ.setdefault("GLOG_minloglevel", "2")
|
|
63
|
+
|
|
64
|
+
# Enable the JAX persistent XLA compilation cache in a machine-scoped
|
|
65
|
+
# directory when requested by the backend/env policy in _compat. Accelerator
|
|
66
|
+
# runs use the cache by default; CPU runs are opt-in to avoid XLA:CPU AOT
|
|
67
|
+
# feature-mismatch warnings on shared or changing runtime environments.
|
|
68
|
+
# ``core.solver._harden_compilation_cache`` re-applies this policy on every
|
|
69
|
+
# solve path in case this module never ran (namespace-package shadowing).
|
|
70
|
+
import jax as _jax
|
|
71
|
+
|
|
72
|
+
_jax_cache_dir = _default_jax_cache_dir()
|
|
73
|
+
if _jax_cache_dir is not None:
|
|
74
|
+
_os.makedirs(_jax_cache_dir, exist_ok=True)
|
|
75
|
+
_jax.config.update("jax_enable_compilation_cache", True)
|
|
76
|
+
_jax.config.update("jax_compilation_cache_dir", _jax_cache_dir)
|
|
77
|
+
|
|
78
|
+
# Lazy public exports: name -> (module, attribute). ``attribute=None``
|
|
79
|
+
# exports the module itself.
|
|
80
|
+
_LAZY_ATTRS: dict[str, tuple[str, str | None]] = {
|
|
81
|
+
# input
|
|
82
|
+
"VmecInput": (".core.input", "VmecInput"),
|
|
83
|
+
# solvers
|
|
84
|
+
"solve": (".core.solver", "solve"),
|
|
85
|
+
"solve_multigrid": (".core.multigrid", "solve_multigrid"),
|
|
86
|
+
"solve_free_boundary": (".core.freeboundary", "solve_free_boundary"),
|
|
87
|
+
# wout IO
|
|
88
|
+
"WoutData": (".core.wout", "WoutData"),
|
|
89
|
+
"read_wout": (".core.wout", "read_wout"),
|
|
90
|
+
"write_wout": (".core.wout", "write_wout"),
|
|
91
|
+
"wout_from_state": (".core.wout", "wout_from_state"),
|
|
92
|
+
# plotting + Boozer
|
|
93
|
+
"plot_wout": (".core.plotting", "plot_wout"),
|
|
94
|
+
"plot_boozmn": (".core.plotting", "plot_boozmn"),
|
|
95
|
+
"run_booz_xform": (".core.boozer", "run_booz_xform"),
|
|
96
|
+
# external fields
|
|
97
|
+
"MgridData": (".core.mgrid", "MgridData"),
|
|
98
|
+
"MgridField": (".core.mgrid", "MgridField"),
|
|
99
|
+
"read_mgrid": (".core.mgrid", "read_mgrid"),
|
|
100
|
+
"write_mgrid": (".core.mgrid", "write_mgrid"),
|
|
101
|
+
# errors
|
|
102
|
+
"VmecError": (".core.errors", "VmecError"),
|
|
103
|
+
"VmecInputError": (".core.errors", "VmecInputError"),
|
|
104
|
+
"VmecJacobianError": (".core.errors", "VmecJacobianError"),
|
|
105
|
+
"VmecConvergenceError": (".core.errors", "VmecConvergenceError"),
|
|
106
|
+
"MgridNotFoundError": (".core.errors", "MgridNotFoundError"),
|
|
107
|
+
# modules
|
|
108
|
+
"core": (".core", None),
|
|
109
|
+
"errors": (".core.errors", None),
|
|
110
|
+
"optimize": (".core.optimize", None),
|
|
111
|
+
"implicit": (".core.implicit", None),
|
|
112
|
+
"parallel": (".core.parallel", None),
|
|
113
|
+
"doctor": (".doctor", None),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
__all__ = ["__version__", *sorted(_LAZY_ATTRS)]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def __getattr__(name: str):
|
|
120
|
+
entry = _LAZY_ATTRS.get(name)
|
|
121
|
+
if entry is None:
|
|
122
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
123
|
+
module_name, attribute = entry
|
|
124
|
+
module = _import_module(module_name, __name__)
|
|
125
|
+
value = module if attribute is None else getattr(module, attribute)
|
|
126
|
+
globals()[name] = value
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def __dir__():
|
|
131
|
+
return sorted(set(globals()) | set(_LAZY_ATTRS))
|
vmex/__main__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Module entry point for `python -m vmex`."""
|
|
2
|
+
|
|
3
|
+
import os as _os
|
|
4
|
+
|
|
5
|
+
# Suppress noisy C++ warnings from XLA/PjRt before any JAX import.
|
|
6
|
+
_os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2")
|
|
7
|
+
_os.environ.setdefault("ABSL_MIN_LOG_LEVEL", "2")
|
|
8
|
+
_os.environ.setdefault("GLOG_minloglevel", "2")
|
|
9
|
+
|
|
10
|
+
from .core.cli import main
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__": # pragma: no cover
|
|
14
|
+
raise SystemExit(main())
|
vmex/_compat.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""JAX environment defaults + persistent compilation-cache policy.
|
|
2
|
+
|
|
3
|
+
Historically this module was a full JAX/NumPy backend shim (``has_jax`` /
|
|
4
|
+
``asarray`` / ``einsum`` / a no-op ``jit`` and a thread-local numpy mode).
|
|
5
|
+
The core became JAX-only long ago and nothing imported that machinery any
|
|
6
|
+
more, so it was deleted (Item I.8a). What remains — and is
|
|
7
|
+
actually used — is:
|
|
8
|
+
|
|
9
|
+
- :func:`_configure_jax_environment` (run at import, i.e. before
|
|
10
|
+
``vmex/__init__`` does ``import jax``): environment defaults that must
|
|
11
|
+
be set before JAX/XLA initializes — float64 (``JAX_ENABLE_X64``, VMEC
|
|
12
|
+
parity), synchronous CPU dispatch, quiet XLA/PjRt C++ logging, GPU
|
|
13
|
+
demand allocation, the machine-scoped persistent compilation-cache
|
|
14
|
+
directory, and the XLA:CPU fast-compile flags;
|
|
15
|
+
- the compilation-cache policy helpers
|
|
16
|
+
:func:`_default_compilation_cache_dir` / :func:`_cache_machine_fingerprint`
|
|
17
|
+
/ :func:`_configure_compilation_cache`, consumed by ``vmex/__init__``
|
|
18
|
+
and re-applied by ``core.solver._harden_compilation_cache`` on every solve
|
|
19
|
+
path (namespace-package shadowing guard).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
import hashlib
|
|
26
|
+
from importlib import metadata as importlib_metadata
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import platform
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _env(name: str, default: str = "") -> str:
|
|
34
|
+
"""Read ``VMEX_<name>``, falling back to the legacy ``VMEC_JAX_<name>``.
|
|
35
|
+
|
|
36
|
+
The package was renamed vmec_jax -> vmex; environment variables a user may
|
|
37
|
+
have set in their shell profile (the ``*_COMPILATION_CACHE*`` knobs in
|
|
38
|
+
particular) keep working under their old names for one release.
|
|
39
|
+
"""
|
|
40
|
+
val = os.environ.get(f"VMEX_{name}")
|
|
41
|
+
if val is not None:
|
|
42
|
+
return val
|
|
43
|
+
return os.environ.get(f"VMEC_JAX_{name}", default)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _cache_machine_fingerprint() -> str:
|
|
47
|
+
"""Return a short cache key for host-specific XLA CPU executables.
|
|
48
|
+
|
|
49
|
+
XLA CPU persistent-cache entries are native executables. On shared home
|
|
50
|
+
directories, reusing an entry compiled on another CPU can trigger XLA AOT
|
|
51
|
+
loader errors or even illegal-instruction failures. The fingerprint keeps
|
|
52
|
+
vmex's default cache portable by separating entries by OS, machine, and
|
|
53
|
+
CPU-feature/model signature. Users who deliberately want a shared cache can
|
|
54
|
+
still set ``VMEX_COMPILATION_CACHE_DIR`` or ``JAX_COMPILATION_CACHE_DIR``.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
parts = [
|
|
58
|
+
platform.system(),
|
|
59
|
+
platform.machine(),
|
|
60
|
+
platform.processor(),
|
|
61
|
+
f"python={sys.version_info.major}.{sys.version_info.minor}",
|
|
62
|
+
]
|
|
63
|
+
for package in ("jax", "jaxlib"):
|
|
64
|
+
try:
|
|
65
|
+
parts.append(f"{package}={importlib_metadata.version(package)}")
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
try:
|
|
69
|
+
if os.path.exists("/proc/cpuinfo"):
|
|
70
|
+
wanted = ("model name", "cpu family", "model", "stepping", "flags", "Features")
|
|
71
|
+
seen: set[str] = set()
|
|
72
|
+
with open("/proc/cpuinfo", encoding="utf-8", errors="ignore") as fh:
|
|
73
|
+
for line in fh:
|
|
74
|
+
if ":" not in line:
|
|
75
|
+
continue
|
|
76
|
+
key, value = (part.strip() for part in line.split(":", 1))
|
|
77
|
+
if key in wanted and key not in seen:
|
|
78
|
+
parts.append(f"{key}={value}")
|
|
79
|
+
seen.add(key)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
# macOS has no /proc/cpuinfo — capture the CPU brand + microarchitecture via
|
|
83
|
+
# sysctl so Intel/Apple-Silicon (and different chip generations) never share
|
|
84
|
+
# an XLA:CPU AOT cache entry.
|
|
85
|
+
if platform.system() == "Darwin":
|
|
86
|
+
try:
|
|
87
|
+
import subprocess
|
|
88
|
+
for key in ("machdep.cpu.brand_string", "hw.optional.arm.FEAT_SME",
|
|
89
|
+
"hw.cpufamily"):
|
|
90
|
+
out = subprocess.run(["sysctl", "-n", key], capture_output=True,
|
|
91
|
+
text=True, timeout=2)
|
|
92
|
+
if out.returncode == 0 and out.stdout.strip():
|
|
93
|
+
parts.append(f"{key}={out.stdout.strip()}")
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
if not any(str(part).strip() for part in parts[:3]):
|
|
97
|
+
try:
|
|
98
|
+
parts.append(platform.node())
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
digest = hashlib.sha256("|".join(parts).encode("utf-8", errors="ignore")).hexdigest()[:16]
|
|
102
|
+
system = platform.system().lower() or "unknown"
|
|
103
|
+
machine = platform.machine().lower() or "unknown"
|
|
104
|
+
return f"{system}-{machine}-{digest}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _default_compilation_cache_dir() -> str | None:
|
|
108
|
+
"""Return the configured JAX compilation-cache directory.
|
|
109
|
+
|
|
110
|
+
The persistent cache is enabled **by default on every backend** (CPU too)
|
|
111
|
+
so that repeated cold-process CLI/API runs reuse compiled kernels instead
|
|
112
|
+
of recompiling — a solovev CLI run drops 4.3 s -> 1.2 s on the second
|
|
113
|
+
invocation (R26c). The XLA:CPU host-feature-mismatch hazard (AOT
|
|
114
|
+
executables tied to a specific instruction set, dangerous on shared home
|
|
115
|
+
filesystems) is handled by :func:`_cache_machine_fingerprint`, whose
|
|
116
|
+
per-machine suffix hashes the CPU model + feature flags (AVX2/AVX512/...),
|
|
117
|
+
so heterogeneous machines never share a cache entry. Opt out with
|
|
118
|
+
``VMEX_COMPILATION_CACHE=disabled`` (or ``VMEX_COMPILATION_CACHE_DIR=
|
|
119
|
+
disabled``); point it elsewhere with ``JAX_COMPILATION_CACHE_DIR=/path``.
|
|
120
|
+
"""
|
|
121
|
+
# Already set by the user — respect it.
|
|
122
|
+
if "JAX_COMPILATION_CACHE_DIR" in os.environ:
|
|
123
|
+
val = os.environ["JAX_COMPILATION_CACHE_DIR"].strip()
|
|
124
|
+
if val.lower() in ("", "disabled", "0", "false", "no"):
|
|
125
|
+
return None
|
|
126
|
+
return val
|
|
127
|
+
|
|
128
|
+
# User can opt out via VMEX_COMPILATION_CACHE_DIR=disabled
|
|
129
|
+
vmec_val = _env("COMPILATION_CACHE_DIR").strip()
|
|
130
|
+
if vmec_val.lower() in ("disabled", "0", "false", "no"):
|
|
131
|
+
return None
|
|
132
|
+
if vmec_val:
|
|
133
|
+
return vmec_val
|
|
134
|
+
|
|
135
|
+
cache_flag = _env("COMPILATION_CACHE").strip().lower()
|
|
136
|
+
if cache_flag in ("disabled", "0", "false", "no", "off"):
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
# Default cache location: ~/.cache/vmex/jax_cache/<machine-fingerprint>
|
|
140
|
+
# The host-specific suffix (CPU model + feature flags) prevents unsafe
|
|
141
|
+
# XLA:CPU AOT reuse on shared home filesystems where different machines see
|
|
142
|
+
# the same ~/.cache directory.
|
|
143
|
+
try:
|
|
144
|
+
import pathlib
|
|
145
|
+
return str(
|
|
146
|
+
pathlib.Path.home()
|
|
147
|
+
/ ".cache"
|
|
148
|
+
/ "vmex"
|
|
149
|
+
/ "jax_cache"
|
|
150
|
+
/ _cache_machine_fingerprint()
|
|
151
|
+
)
|
|
152
|
+
except Exception:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _configure_compilation_cache(jax_module: Any, cache_dir: str | None) -> None:
|
|
157
|
+
"""Apply vmex's persistent-cache defaults to an imported JAX module."""
|
|
158
|
+
if cache_dir is None:
|
|
159
|
+
return
|
|
160
|
+
try:
|
|
161
|
+
jax_module.config.update("jax_enable_compilation_cache", True)
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
try:
|
|
165
|
+
jax_module.config.update("jax_compilation_cache_dir", cache_dir)
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
try:
|
|
169
|
+
min_compile = _env("CACHE_MIN_COMPILE_TIME_SECS", "0")
|
|
170
|
+
jax_module.config.update("jax_persistent_cache_min_compile_time_secs", float(min_compile))
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
try:
|
|
174
|
+
min_entry = _env("CACHE_MIN_ENTRY_SIZE_BYTES", "-1")
|
|
175
|
+
jax_module.config.update("jax_persistent_cache_min_entry_size_bytes", int(min_entry))
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
try:
|
|
179
|
+
xla_caches = _env("PERSISTENT_CACHE_XLA_CACHES").strip()
|
|
180
|
+
if not xla_caches:
|
|
181
|
+
platform_name = os.environ.get("JAX_PLATFORM_NAME", "").strip().lower()
|
|
182
|
+
platforms = os.environ.get("JAX_PLATFORMS", "").strip().lower()
|
|
183
|
+
visible_cuda = os.environ.get("CUDA_VISIBLE_DEVICES", "").strip().lower()
|
|
184
|
+
gpu_requested = (
|
|
185
|
+
platform_name in ("gpu", "cuda")
|
|
186
|
+
or any(part.strip() in ("gpu", "cuda") for part in platforms.split(","))
|
|
187
|
+
or visible_cuda not in ("", "-1", "none", "no")
|
|
188
|
+
)
|
|
189
|
+
xla_caches = "xla_gpu_per_fusion_autotune_cache_dir" if gpu_requested else "none"
|
|
190
|
+
if xla_caches.lower() not in ("", "none", "0", "false", "no", "off"):
|
|
191
|
+
jax_module.config.update("jax_persistent_cache_enable_xla_caches", xla_caches)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
try:
|
|
195
|
+
max_size = _env("COMPILATION_CACHE_MAX_SIZE")
|
|
196
|
+
if max_size:
|
|
197
|
+
jax_module.config.update("jax_compilation_cache_max_size", int(max_size))
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
try:
|
|
201
|
+
explain = _env("EXPLAIN_CACHE_MISSES")
|
|
202
|
+
if explain.strip().lower() not in ("", "0", "false", "no"):
|
|
203
|
+
jax_module.config.update("jax_explain_cache_misses", True)
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _configure_jax_environment() -> None:
|
|
209
|
+
"""Set JAX/XLA environment defaults, then import + configure JAX.
|
|
210
|
+
|
|
211
|
+
Runs once at ``vmex._compat`` import time — before
|
|
212
|
+
``vmex/__init__`` (or anything else in the package) imports JAX — so
|
|
213
|
+
the env-var defaults reliably reach XLA backend initialization. Every
|
|
214
|
+
default uses ``setdefault``: an explicit user environment always wins.
|
|
215
|
+
"""
|
|
216
|
+
try:
|
|
217
|
+
# Enable x64 by default for VMEC parity unless the user opted out.
|
|
218
|
+
os.environ.setdefault("JAX_ENABLE_X64", "1")
|
|
219
|
+
# VMEC/JAX optimization callbacks immediately materialize most results
|
|
220
|
+
# on the host (SciPy residuals/Jacobians, history, wout writing). On
|
|
221
|
+
# CPU, asynchronous dispatch can leave completed XLA/PjRt work and
|
|
222
|
+
# executable state queued across many exact-Jacobian callbacks in one
|
|
223
|
+
# long-lived process. Default CPU dispatch to synchronous execution so
|
|
224
|
+
# memory is reclaimed at callback boundaries; users can still override
|
|
225
|
+
# this before import with JAX_CPU_ENABLE_ASYNC_DISPATCH=true.
|
|
226
|
+
os.environ.setdefault("JAX_CPU_ENABLE_ASYNC_DISPATCH", "false")
|
|
227
|
+
# Suppress noisy C++ warnings from XLA/PjRt backend (e.g.
|
|
228
|
+
# repeated "Assume version compatibility. PjRt-IFRT does not
|
|
229
|
+
# track XLA executable versions." on persistent-cache hits).
|
|
230
|
+
# These are harmless informational messages emitted by the XLA
|
|
231
|
+
# runtime logging stack. Level 0=INFO, 1=WARNING, 2=ERROR — we
|
|
232
|
+
# default to ERROR-only so that genuine errors still surface.
|
|
233
|
+
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2")
|
|
234
|
+
os.environ.setdefault("ABSL_MIN_LOG_LEVEL", "2")
|
|
235
|
+
os.environ.setdefault("GLOG_minloglevel", "2")
|
|
236
|
+
# JAX's default GPU allocator preallocates most device memory. That
|
|
237
|
+
# hurts vmex's exact-optimizer workload in practice: it prevents
|
|
238
|
+
# concurrent profiling/worker processes from starting and can make the
|
|
239
|
+
# accepted-point replay path much slower. Default to demand allocation
|
|
240
|
+
# unless the user already set JAX's allocator env var or explicitly
|
|
241
|
+
# asks vmex to keep JAX's preallocation default.
|
|
242
|
+
_vmec_gpu_prealloc = _env("GPU_PREALLOCATE").strip().lower()
|
|
243
|
+
if (
|
|
244
|
+
"XLA_PYTHON_CLIENT_PREALLOCATE" not in os.environ
|
|
245
|
+
and _vmec_gpu_prealloc not in ("1", "true", "yes", "on")
|
|
246
|
+
):
|
|
247
|
+
os.environ["XLA_PYTHON_CLIENT_PREALLOCATE"] = "false"
|
|
248
|
+
|
|
249
|
+
# Enable the JAX disk compilation cache in a machine-scoped directory.
|
|
250
|
+
# This avoids unsafe XLA:CPU AOT reuse across hosts while preserving
|
|
251
|
+
# repeated cold-process speedups on the same machine.
|
|
252
|
+
_cache_dir = _default_compilation_cache_dir()
|
|
253
|
+
if _cache_dir is not None:
|
|
254
|
+
os.environ.setdefault("JAX_COMPILATION_CACHE_DIR", _cache_dir)
|
|
255
|
+
|
|
256
|
+
# XLA:CPU compile-time flags. The differentiable/optimization pipeline
|
|
257
|
+
# is COMPILE-dominated (the fused adjoint VJP + GMRES graph is ~21 s of a
|
|
258
|
+
# ~24 s cold ``value_and_grad``); XLA's default backend optimization
|
|
259
|
+
# level (3) spends most of that in expensive LLVM passes. Level 1 plus
|
|
260
|
+
# disabling the expensive passes typically cuts compile wall-time
|
|
261
|
+
# ~1.3-2x, at the cost of slightly slower *warm* kernels -- a good trade
|
|
262
|
+
# for this compile-bound workload. Applied on CPU only (LLVM codegen),
|
|
263
|
+
# never with fast-math (that would break float64 parity/determinism),
|
|
264
|
+
# skipped if the user set XLA_FLAGS, and opt-out via
|
|
265
|
+
# VMEX_FAST_COMPILE=0 (e.g. a very long single-process opt loop that
|
|
266
|
+
# amortizes compile over many warm calls prefers level 3).
|
|
267
|
+
_fast_compile = _env("FAST_COMPILE", "1").strip().lower()
|
|
268
|
+
_accel_req = os.environ.get("JAX_PLATFORM_NAME", "").strip().lower()
|
|
269
|
+
_accel_reqs = os.environ.get("JAX_PLATFORMS", "").strip().lower()
|
|
270
|
+
_cuda_vis = os.environ.get("CUDA_VISIBLE_DEVICES", "").strip()
|
|
271
|
+
_on_accel = (
|
|
272
|
+
any(a in f"{_accel_req} {_accel_reqs}" for a in ("cuda", "gpu", "tpu", "rocm"))
|
|
273
|
+
or (_cuda_vis not in ("", "-1"))
|
|
274
|
+
)
|
|
275
|
+
if (
|
|
276
|
+
_fast_compile not in ("0", "false", "no", "off")
|
|
277
|
+
and "XLA_FLAGS" not in os.environ
|
|
278
|
+
and not _on_accel
|
|
279
|
+
):
|
|
280
|
+
os.environ["XLA_FLAGS"] = (
|
|
281
|
+
"--xla_backend_optimization_level=1 "
|
|
282
|
+
"--xla_llvm_disable_expensive_passes=true"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
import jax
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
jax.config.update("jax_enable_x64", os.environ.get("JAX_ENABLE_X64", "0") == "1")
|
|
289
|
+
except Exception:
|
|
290
|
+
pass
|
|
291
|
+
try:
|
|
292
|
+
_cpu_async = os.environ.get("JAX_CPU_ENABLE_ASYNC_DISPATCH", "true")
|
|
293
|
+
jax.config.update(
|
|
294
|
+
"jax_cpu_enable_async_dispatch",
|
|
295
|
+
_cpu_async.strip().lower() not in ("0", "false", "no", "off"),
|
|
296
|
+
)
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
# Wire up the compilation cache via jax.config too; the env-var path
|
|
301
|
+
# alone does not cover all JAX/JAXLIB versions and cache thresholds.
|
|
302
|
+
_configure_compilation_cache(jax, _cache_dir)
|
|
303
|
+
except Exception:
|
|
304
|
+
# Never block a vmex import over environment tuning (e.g. docs
|
|
305
|
+
# builds with a mocked JAX): core.solver enforces the hard
|
|
306
|
+
# requirements (x64, cache hardening) on every solve path anyway.
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
_configure_jax_environment()
|
vmex/core/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Clean-room core of vmex (§5) — will replace the legacy modules.
|
|
2
|
+
|
|
3
|
+
Module map (each header docstring names its VMEC2000 counterpart):
|
|
4
|
+
|
|
5
|
+
- ``errors`` typed zero-crash exceptions + werror table
|
|
6
|
+
- ``printing`` VMEC2000-format console output (byte-exact)
|
|
7
|
+
- ``input`` VmecInput: INDATA + VMEC++-JSON parsing, round-trip writers
|
|
8
|
+
- ``profiles`` pressure/iota/current parameterizations (pure jnp)
|
|
9
|
+
- ``fourier`` Resolution, ModeTable, trig tables (fixaray.f)
|
|
10
|
+
- ``transforms`` totzsps/totzspa/tomnsps/tomnspa as batched matmuls
|
|
11
|
+
- ``geometry`` real-space R/Z/lambda, half-mesh jacobian (jacobian.f)
|
|
12
|
+
- ``fields`` metrics, B components, energies, tcon (bcovar.f)
|
|
13
|
+
- ``forces`` MHD force kernels + spectral condensation (forces.f, alias.f)
|
|
14
|
+
- ``residuals`` m=1 constraint, fsqr/fsqz/fsql, preconditioned lane (residue.f90)
|
|
15
|
+
- ``preconditioner`` 1D radial tridiagonal preconditioner (precondn.f, scalfor.f)
|
|
16
|
+
- ``step`` Richardson stepping + restart control (evolve.f, restart.f)
|
|
17
|
+
- ``setup`` radial profiles + initial guess (profil1d/3d.f, readin.f)
|
|
18
|
+
- ``solver`` single-grid fixed-boundary solve loop (funct3d.f, eqsolve.f)
|
|
19
|
+
- ``statephysics`` shared state-physics primitives (_field_chain, half-mesh iota/sampling)
|
|
20
|
+
- ``implicit`` implicit differentiation of the equilibrium (custom VJP + adjoint GMRES)
|
|
21
|
+
- ``stability`` differentiable ideal-MHD stability (infinite-n ballooning; COBRA port)
|
|
22
|
+
- ``freeboundary_diff`` differentiable free-boundary residual via virtual casing (R15.3/R19)
|
|
23
|
+
- ``device`` CPU/GPU placement policy (measured: benchmarks/gpu_baseline.json)
|
|
24
|
+
|
|
25
|
+
Every module is validated by A/B equivalence tests against the legacy
|
|
26
|
+
parity-proven implementation in ``tests/``; the solve loop is
|
|
27
|
+
validated end-to-end against VMEC2000 golden runs
|
|
28
|
+
(``tests/test_solver_end_to_end.py``).
|
|
29
|
+
"""
|