alfvex 1.0.1__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.
- alfvex-1.0.1/CITATION.cff +24 -0
- alfvex-1.0.1/LICENSE +22 -0
- alfvex-1.0.1/MANIFEST.in +3 -0
- alfvex-1.0.1/PKG-INFO +156 -0
- alfvex-1.0.1/README.md +108 -0
- alfvex-1.0.1/examples/benchmark.py +247 -0
- alfvex-1.0.1/examples/boundary_gradient.py +27 -0
- alfvex-1.0.1/examples/ctok_continuum.toml +16 -0
- alfvex-1.0.1/examples/ctok_eigenmodes.toml +20 -0
- alfvex-1.0.1/examples/data/input.ITPA_Konies2018 +9 -0
- alfvex-1.0.1/examples/data/input.LandremanPaul2021_QA_lowres +109 -0
- alfvex-1.0.1/examples/data/input.LandremanPaul2021_QH_reactorScale_lowres +157 -0
- alfvex-1.0.1/examples/knyazev_qa.py +378 -0
- alfvex-1.0.1/examples/koenies_tae_drive.py +40 -0
- alfvex-1.0.1/examples/optimize_alfven.py +267 -0
- alfvex-1.0.1/examples/qa_continuum.toml +16 -0
- alfvex-1.0.1/examples/quickstart.py +35 -0
- alfvex-1.0.1/pyproject.toml +70 -0
- alfvex-1.0.1/setup.cfg +4 -0
- alfvex-1.0.1/src/alfvex.egg-info/PKG-INFO +156 -0
- alfvex-1.0.1/src/alfvex.egg-info/SOURCES.txt +50 -0
- alfvex-1.0.1/src/alfvex.egg-info/dependency_links.txt +1 -0
- alfvex-1.0.1/src/alfvex.egg-info/entry_points.txt +2 -0
- alfvex-1.0.1/src/alfvex.egg-info/requires.txt +31 -0
- alfvex-1.0.1/src/alfvex.egg-info/top_level.txt +1 -0
- alfvex-1.0.1/src/fax/__init__.py +70 -0
- alfvex-1.0.1/src/fax/continuum.py +109 -0
- alfvex-1.0.1/src/fax/drive.py +204 -0
- alfvex-1.0.1/src/fax/eigen.py +342 -0
- alfvex-1.0.1/src/fax/geometry.py +321 -0
- alfvex-1.0.1/src/fax/io.py +131 -0
- alfvex-1.0.1/src/fax/objectives.py +333 -0
- alfvex-1.0.1/src/fax/plot.py +42 -0
- alfvex-1.0.1/tests/conftest.py +14 -0
- alfvex-1.0.1/tests/data/PROVENANCE.md +72 -0
- alfvex-1.0.1/tests/data/ae3d_ctok_modes.npz +0 -0
- alfvex-1.0.1/tests/data/ae3d_v31_ctok.npz +0 -0
- alfvex-1.0.1/tests/data/ae3d_v31_instrumentation.patch +111 -0
- alfvex-1.0.1/tests/data/boozmn_ctok.nc +0 -0
- alfvex-1.0.1/tests/data/knyazev_qa.npz +0 -0
- alfvex-1.0.1/tests/data/make_ae3d_v31_ctok.py +65 -0
- alfvex-1.0.1/tests/data/make_ctok_fixtures.py +31 -0
- alfvex-1.0.1/tests/data/stellgap_ctok_continuum.npz +0 -0
- alfvex-1.0.1/tests/install_smoke.py +25 -0
- alfvex-1.0.1/tests/test_api.py +122 -0
- alfvex-1.0.1/tests/test_continuum.py +188 -0
- alfvex-1.0.1/tests/test_drive.py +347 -0
- alfvex-1.0.1/tests/test_eigen.py +482 -0
- alfvex-1.0.1/tests/test_knyazev.py +58 -0
- alfvex-1.0.1/tests/test_objectives.py +136 -0
- alfvex-1.0.1/tests/test_provenance.py +16 -0
- alfvex-1.0.1/tests/test_vmex.py +255 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use FAX in your research, please cite it as below."
|
|
3
|
+
title: "FAX: differentiable shear-Alfvén spectra and fast-ion drive for stellarators in JAX"
|
|
4
|
+
type: software
|
|
5
|
+
authors:
|
|
6
|
+
- family-names: Jorge
|
|
7
|
+
given-names: Rogerio
|
|
8
|
+
email: rogerio.jorge@wisc.edu
|
|
9
|
+
affiliation: University of Wisconsin-Madison
|
|
10
|
+
abstract: >-
|
|
11
|
+
Differentiable JAX code for the shear-Alfvén continuum, AE3D-form global eigenmodes and the perturbative passing
|
|
12
|
+
fast-ion drive in Boozer geometry, usable inside VMEX boundary optimization.
|
|
13
|
+
repository-code: "https://github.com/uwplasma/fax"
|
|
14
|
+
url: "https://fax.readthedocs.io"
|
|
15
|
+
license: MIT
|
|
16
|
+
version: 1.0.1
|
|
17
|
+
keywords:
|
|
18
|
+
- Alfvén eigenmodes
|
|
19
|
+
- fast ions
|
|
20
|
+
- JAX
|
|
21
|
+
- stellarator
|
|
22
|
+
- automatic differentiation
|
|
23
|
+
- plasma physics
|
|
24
|
+
date-released: 2026-10-30
|
alfvex-1.0.1/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 UWPlasma 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.
|
|
22
|
+
|
alfvex-1.0.1/MANIFEST.in
ADDED
alfvex-1.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alfvex
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Differentiable JAX code for Alfvén eigenmodes and fast-ion physics in stellarators.
|
|
5
|
+
Author-email: Rogerio Jorge <rogerio.jorge@wisc.edu>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/uwplasma/fax
|
|
8
|
+
Project-URL: Repository, https://github.com/uwplasma/fax
|
|
9
|
+
Project-URL: Documentation, https://fax.readthedocs.io
|
|
10
|
+
Project-URL: Issues, https://github.com/uwplasma/fax/issues
|
|
11
|
+
Keywords: alfven eigenmodes,fast ions,jax,stellarator,vmec
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: jax>=0.4.38
|
|
26
|
+
Requires-Dist: solvax>=0.32.1
|
|
27
|
+
Requires-Dist: numpy
|
|
28
|
+
Requires-Dist: netCDF4
|
|
29
|
+
Requires-Dist: tomli; python_version < "3.11"
|
|
30
|
+
Provides-Extra: plot
|
|
31
|
+
Requires-Dist: matplotlib; extra == "plot"
|
|
32
|
+
Provides-Extra: vmex
|
|
33
|
+
Requires-Dist: vmex>=0.11.10; python_version >= "3.11" and extra == "vmex"
|
|
34
|
+
Provides-Extra: test
|
|
35
|
+
Requires-Dist: pytest; extra == "test"
|
|
36
|
+
Requires-Dist: coverage; extra == "test"
|
|
37
|
+
Requires-Dist: matplotlib; extra == "test"
|
|
38
|
+
Provides-Extra: docs
|
|
39
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
40
|
+
Requires-Dist: furo; extra == "docs"
|
|
41
|
+
Requires-Dist: myst-parser; extra == "docs"
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: alfvex[docs,test]; extra == "dev"
|
|
44
|
+
Requires-Dist: ruff; extra == "dev"
|
|
45
|
+
Requires-Dist: build; extra == "dev"
|
|
46
|
+
Requires-Dist: twine; extra == "dev"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# FAX
|
|
50
|
+
|
|
51
|
+
[](https://github.com/uwplasma/fax/actions/workflows/tests.yml)
|
|
52
|
+
[](https://fax.readthedocs.io)
|
|
53
|
+
[](LICENSE)
|
|
54
|
+
|
|
55
|
+
FAX is a lean JAX code for shear-Alfvén physics in stellarators: the continuum, AE3D-form global modes and a perturbative
|
|
56
|
+
fast-ion drive, with first-order derivatives with respect to profiles and, through VMEX, the VMEC boundary.
|
|
57
|
+
|
|
58
|
+
<p align="center">
|
|
59
|
+
<img src="docs/_static/ctok_eigenmodes_continuum.png" width="44%" alt="CTOK continuum and global modes">
|
|
60
|
+
<img src="docs/_static/koenies_tae_drive.png" width="48%" alt="Könies TAE drive">
|
|
61
|
+
</p>
|
|
62
|
+
|
|
63
|
+
*Left: the n = 2 CTOK continuum with the six global modes nearest 384 kHz (red). Right: the passing-only drive of the Könies et al. (2018) TAE.*
|
|
64
|
+
|
|
65
|
+
## Scope
|
|
66
|
+
|
|
67
|
+
| Supported | Not supported |
|
|
68
|
+
|---|---|
|
|
69
|
+
| Low-β continuum on each Boozer surface (STELLGAP form) | Sound coupling, so no BAE or BAAE |
|
|
70
|
+
| AE3D-form reduced ideal-MHD global modes, signed $\omega^2$ | Continuum damping and electron damping |
|
|
71
|
+
| `finite_beta=True`: restricted $J_\parallel$ and pressure terms (experimental) | A full-MHD or compressional model |
|
|
72
|
+
| Drive of a fixed mode: zero orbit width, passing ions only | Trapped particles, finite orbit width, finite Larmor radius |
|
|
73
|
+
|
|
74
|
+
The drive is perturbative and valid up to about 200 keV in the Könies case ($\gamma/\omega \le 4\%$). It is about 0.6 of the CAS3D-K
|
|
75
|
+
zero-orbit-width curve, plausibly the missing trapped share; that attribution is not established. Derivatives are first order and
|
|
76
|
+
are NaN unless the eigensolve converged and its selection is not a tie.
|
|
77
|
+
|
|
78
|
+
## Install
|
|
79
|
+
|
|
80
|
+
Python 3.10 or newer, JAX 0.4.38 or newer, `solvax>=0.32.1`. FAX is on PyPI as **`alfvex`**; the code is imported as `fax`.
|
|
81
|
+
FAX will soon be renamed ALFVEX everywhere (repository, import and command line). Do not `pip install fax`: that is an unrelated package.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install alfvex # core
|
|
85
|
+
pip install "alfvex[vmex]" # VMEC solves and boundary gradients (Python >= 3.11)
|
|
86
|
+
pip install "alfvex[plot]" # matplotlib, for the figure examples
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
From a clone, `pip install .` (with the same extras) installs the development version.
|
|
90
|
+
|
|
91
|
+
## Quickstart
|
|
92
|
+
|
|
93
|
+
This runs from an installed wheel with no data files: a continuum, a global TAE and one gradient on the analytic Könies et al. (2018) torus.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import jax, jax.numpy as jnp, fax
|
|
97
|
+
|
|
98
|
+
s = (jnp.arange(200) + 0.5) / 200
|
|
99
|
+
geo = fax.circular_torus(s=s, major_radius=10.0, minor_radius=1.0, B0=3.0, iota=1 / (1.71 + 0.16 * s), epsilon_B=0.1)
|
|
100
|
+
harmonics, rho = fax.mode_family(6, 16, 5), fax.mass_density(2e19, 1.0)
|
|
101
|
+
f_hz = fax.continuum(geo, harmonics, rho) # (surfaces, harmonics)
|
|
102
|
+
modes = fax.eigenmodes(geo, harmonics, rho, target_hz=6.46e4, count=3, ntheta=32, nzeta=4)
|
|
103
|
+
print(modes.frequencies_hz[-1], modes.status.converged) # the TAE, 64.63 kHz
|
|
104
|
+
f = lambda r: fax.frequency_hz(geo, harmonics, r, 2, 3, target_hz=6.46e4, count=3, ntheta=32, nzeta=4)
|
|
105
|
+
print(jax.grad(f)(rho)) # equals -f / (2 rho)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The command line runs the continuum and global-mode tasks from a TOML case; the drive, objectives and boundary gradients are Python only.
|
|
109
|
+
|
|
110
|
+
## Examples (checkout only)
|
|
111
|
+
|
|
112
|
+
These read files in the repository or need an extra; each has editable settings at the top and no command-line parsing.
|
|
113
|
+
|
|
114
|
+
| File | Needs | Shows |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `examples/quickstart.py` | nothing | the quickstart above |
|
|
117
|
+
| `examples/ctok_continuum.toml`, `ctok_eigenmodes.toml` | `tests/data` | `fax run <case> --plot` for CTOK |
|
|
118
|
+
| `examples/qa_continuum.toml` | `vmex` | VMEC solve, Boozer transform, continuum |
|
|
119
|
+
| `examples/koenies_tae_drive.py` | `plot` | drive against fast-ion temperature |
|
|
120
|
+
| `examples/boundary_gradient.py` | `vmex` | gradient with respect to VMEC boundary coefficients |
|
|
121
|
+
| `examples/optimize_alfven.py` | `vmex`, `plot` | QS plus Paul's continuum cost (QH) or a classified gap-mode window (QA) |
|
|
122
|
+
| `examples/knyazev_qa.py` | Zenodo archive | rebuilds the offline fixture and the AE3D comparison |
|
|
123
|
+
|
|
124
|
+
## Evidence
|
|
125
|
+
|
|
126
|
+
| Check | Result |
|
|
127
|
+
|---|---|
|
|
128
|
+
| STELLGAP CTOK continuum (97 surfaces) | 6.3e-3 mean, 6.6e-3 p95 relative |
|
|
129
|
+
| AE3D v31 CTOK, six global modes | within 1.4% |
|
|
130
|
+
| AE3D CTOK TAE, 337.9 kHz | not reproduced. On identical input three $\iota'$ terms of AE3D's bending blocks differ from the energy (pinned block comparison; real-space quadrature FAX 0.1–0.3%, AE3D 2–37%). The $J_\parallel$ blocks still disagree, cause open. Not reported upstream |
|
|
131
|
+
| Knyazev et al. (2026) precise QA | continuum 3.0e-3 mean from STELLGAP. The AE3D global mode (30.31 kHz) is not a FAX eigenmode (overlap 0.18, 5.1% below); cause unresolved |
|
|
132
|
+
| Könies et al. (2018) TAE (n = 6) | 4.061e5 rad/s (CAS3D 4.01e5, GYGLES 4.13e5), converged to 1e-4 |
|
|
133
|
+
| Drive, 100/200/300/400 keV | 4.26/16.27/32.48/64.02 ×10³ s⁻¹; 0.58–0.60 of CAS3D-K ZOW at 82–191 keV (content unmatched, not a validation) |
|
|
134
|
+
| Gradients | fixed-geometry AD vs finite differences to about 1e-9 at best; LP-QA gap-mode shift of 4 kHz predicted to 0.05 kHz, resolution budget 0.13 kHz; the $m = 1$ gauge leaves a 1e-2 floor on `zs11`, not closed |
|
|
135
|
+
|
|
136
|
+
The [validation page](https://fax.readthedocs.io/en/latest/validation.html) has every gate, its test and what is not done.
|
|
137
|
+
|
|
138
|
+
## Ecosystem
|
|
139
|
+
|
|
140
|
+
[SOLVAX](https://github.com/uwplasma/SOLVAX) provides the generalized eigensolver and its implicit derivatives;
|
|
141
|
+
[VMEX](https://github.com/uwplasma/vmex) the equilibrium and its implicit derivative;
|
|
142
|
+
[booz_xform_jax](https://github.com/uwplasma/booz_xform_jax) the Boozer transform. ESSOS orbit transport of FAX modes is planned, not implemented.
|
|
143
|
+
|
|
144
|
+
## Documentation and testing
|
|
145
|
+
|
|
146
|
+
[fax.readthedocs.io](https://fax.readthedocs.io): getting started and TOML reference, tutorials, theory, validation, API with its derivative contract.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pip install -e ".[dev,vmex]"
|
|
150
|
+
python -m pytest -q -m 'not scheduled'
|
|
151
|
+
ruff check . && ruff format --check . && sphinx-build -W -b html docs docs/_build/html
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Citing and license
|
|
155
|
+
|
|
156
|
+
Cite the repository through [`CITATION.cff`](CITATION.cff) and the papers whose models FAX implements ([references](https://fax.readthedocs.io/en/latest/references.html)). MIT license.
|
alfvex-1.0.1/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# FAX
|
|
2
|
+
|
|
3
|
+
[](https://github.com/uwplasma/fax/actions/workflows/tests.yml)
|
|
4
|
+
[](https://fax.readthedocs.io)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
FAX is a lean JAX code for shear-Alfvén physics in stellarators: the continuum, AE3D-form global modes and a perturbative
|
|
8
|
+
fast-ion drive, with first-order derivatives with respect to profiles and, through VMEX, the VMEC boundary.
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<img src="docs/_static/ctok_eigenmodes_continuum.png" width="44%" alt="CTOK continuum and global modes">
|
|
12
|
+
<img src="docs/_static/koenies_tae_drive.png" width="48%" alt="Könies TAE drive">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
*Left: the n = 2 CTOK continuum with the six global modes nearest 384 kHz (red). Right: the passing-only drive of the Könies et al. (2018) TAE.*
|
|
16
|
+
|
|
17
|
+
## Scope
|
|
18
|
+
|
|
19
|
+
| Supported | Not supported |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Low-β continuum on each Boozer surface (STELLGAP form) | Sound coupling, so no BAE or BAAE |
|
|
22
|
+
| AE3D-form reduced ideal-MHD global modes, signed $\omega^2$ | Continuum damping and electron damping |
|
|
23
|
+
| `finite_beta=True`: restricted $J_\parallel$ and pressure terms (experimental) | A full-MHD or compressional model |
|
|
24
|
+
| Drive of a fixed mode: zero orbit width, passing ions only | Trapped particles, finite orbit width, finite Larmor radius |
|
|
25
|
+
|
|
26
|
+
The drive is perturbative and valid up to about 200 keV in the Könies case ($\gamma/\omega \le 4\%$). It is about 0.6 of the CAS3D-K
|
|
27
|
+
zero-orbit-width curve, plausibly the missing trapped share; that attribution is not established. Derivatives are first order and
|
|
28
|
+
are NaN unless the eigensolve converged and its selection is not a tie.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
Python 3.10 or newer, JAX 0.4.38 or newer, `solvax>=0.32.1`. FAX is on PyPI as **`alfvex`**; the code is imported as `fax`.
|
|
33
|
+
FAX will soon be renamed ALFVEX everywhere (repository, import and command line). Do not `pip install fax`: that is an unrelated package.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install alfvex # core
|
|
37
|
+
pip install "alfvex[vmex]" # VMEC solves and boundary gradients (Python >= 3.11)
|
|
38
|
+
pip install "alfvex[plot]" # matplotlib, for the figure examples
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
From a clone, `pip install .` (with the same extras) installs the development version.
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
This runs from an installed wheel with no data files: a continuum, a global TAE and one gradient on the analytic Könies et al. (2018) torus.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import jax, jax.numpy as jnp, fax
|
|
49
|
+
|
|
50
|
+
s = (jnp.arange(200) + 0.5) / 200
|
|
51
|
+
geo = fax.circular_torus(s=s, major_radius=10.0, minor_radius=1.0, B0=3.0, iota=1 / (1.71 + 0.16 * s), epsilon_B=0.1)
|
|
52
|
+
harmonics, rho = fax.mode_family(6, 16, 5), fax.mass_density(2e19, 1.0)
|
|
53
|
+
f_hz = fax.continuum(geo, harmonics, rho) # (surfaces, harmonics)
|
|
54
|
+
modes = fax.eigenmodes(geo, harmonics, rho, target_hz=6.46e4, count=3, ntheta=32, nzeta=4)
|
|
55
|
+
print(modes.frequencies_hz[-1], modes.status.converged) # the TAE, 64.63 kHz
|
|
56
|
+
f = lambda r: fax.frequency_hz(geo, harmonics, r, 2, 3, target_hz=6.46e4, count=3, ntheta=32, nzeta=4)
|
|
57
|
+
print(jax.grad(f)(rho)) # equals -f / (2 rho)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The command line runs the continuum and global-mode tasks from a TOML case; the drive, objectives and boundary gradients are Python only.
|
|
61
|
+
|
|
62
|
+
## Examples (checkout only)
|
|
63
|
+
|
|
64
|
+
These read files in the repository or need an extra; each has editable settings at the top and no command-line parsing.
|
|
65
|
+
|
|
66
|
+
| File | Needs | Shows |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `examples/quickstart.py` | nothing | the quickstart above |
|
|
69
|
+
| `examples/ctok_continuum.toml`, `ctok_eigenmodes.toml` | `tests/data` | `fax run <case> --plot` for CTOK |
|
|
70
|
+
| `examples/qa_continuum.toml` | `vmex` | VMEC solve, Boozer transform, continuum |
|
|
71
|
+
| `examples/koenies_tae_drive.py` | `plot` | drive against fast-ion temperature |
|
|
72
|
+
| `examples/boundary_gradient.py` | `vmex` | gradient with respect to VMEC boundary coefficients |
|
|
73
|
+
| `examples/optimize_alfven.py` | `vmex`, `plot` | QS plus Paul's continuum cost (QH) or a classified gap-mode window (QA) |
|
|
74
|
+
| `examples/knyazev_qa.py` | Zenodo archive | rebuilds the offline fixture and the AE3D comparison |
|
|
75
|
+
|
|
76
|
+
## Evidence
|
|
77
|
+
|
|
78
|
+
| Check | Result |
|
|
79
|
+
|---|---|
|
|
80
|
+
| STELLGAP CTOK continuum (97 surfaces) | 6.3e-3 mean, 6.6e-3 p95 relative |
|
|
81
|
+
| AE3D v31 CTOK, six global modes | within 1.4% |
|
|
82
|
+
| AE3D CTOK TAE, 337.9 kHz | not reproduced. On identical input three $\iota'$ terms of AE3D's bending blocks differ from the energy (pinned block comparison; real-space quadrature FAX 0.1–0.3%, AE3D 2–37%). The $J_\parallel$ blocks still disagree, cause open. Not reported upstream |
|
|
83
|
+
| Knyazev et al. (2026) precise QA | continuum 3.0e-3 mean from STELLGAP. The AE3D global mode (30.31 kHz) is not a FAX eigenmode (overlap 0.18, 5.1% below); cause unresolved |
|
|
84
|
+
| Könies et al. (2018) TAE (n = 6) | 4.061e5 rad/s (CAS3D 4.01e5, GYGLES 4.13e5), converged to 1e-4 |
|
|
85
|
+
| Drive, 100/200/300/400 keV | 4.26/16.27/32.48/64.02 ×10³ s⁻¹; 0.58–0.60 of CAS3D-K ZOW at 82–191 keV (content unmatched, not a validation) |
|
|
86
|
+
| Gradients | fixed-geometry AD vs finite differences to about 1e-9 at best; LP-QA gap-mode shift of 4 kHz predicted to 0.05 kHz, resolution budget 0.13 kHz; the $m = 1$ gauge leaves a 1e-2 floor on `zs11`, not closed |
|
|
87
|
+
|
|
88
|
+
The [validation page](https://fax.readthedocs.io/en/latest/validation.html) has every gate, its test and what is not done.
|
|
89
|
+
|
|
90
|
+
## Ecosystem
|
|
91
|
+
|
|
92
|
+
[SOLVAX](https://github.com/uwplasma/SOLVAX) provides the generalized eigensolver and its implicit derivatives;
|
|
93
|
+
[VMEX](https://github.com/uwplasma/vmex) the equilibrium and its implicit derivative;
|
|
94
|
+
[booz_xform_jax](https://github.com/uwplasma/booz_xform_jax) the Boozer transform. ESSOS orbit transport of FAX modes is planned, not implemented.
|
|
95
|
+
|
|
96
|
+
## Documentation and testing
|
|
97
|
+
|
|
98
|
+
[fax.readthedocs.io](https://fax.readthedocs.io): getting started and TOML reference, tutorials, theory, validation, API with its derivative contract.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install -e ".[dev,vmex]"
|
|
102
|
+
python -m pytest -q -m 'not scheduled'
|
|
103
|
+
ruff check . && ruff format --check . && sphinx-build -W -b html docs docs/_build/html
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Citing and license
|
|
107
|
+
|
|
108
|
+
Cite the repository through [`CITATION.cff`](CITATION.cff) and the papers whose models FAX implements ([references](https://fax.readthedocs.io/en/latest/references.html)). MIT license.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Equal-accuracy timings of the FAX eigensolvers on one assembled pencil ``F y = lam G y``.
|
|
2
|
+
|
|
3
|
+
Run ``python examples/benchmark.py`` on a quiet machine (each job waits up to ``WAIT_MAX`` seconds for a 1-minute load below
|
|
4
|
+
``LOAD_MAX``; the loads are reported); edit the settings below. Methods:
|
|
5
|
+
|
|
6
|
+
* ``targeted``: ``fax.eigenmodes(target_hz, count)``, SOLVAX shift-invert on the block-tridiagonal pencil;
|
|
7
|
+
* ``dense-jax``: ``fax.eigenmodes()``, FAX's jitted dense generalized solve of all modes (Cholesky + ``eigh``);
|
|
8
|
+
* ``dense-scipy``: ``scipy.linalg.eigh(F, G)`` of all modes, the reference.
|
|
9
|
+
|
|
10
|
+
Each (case, method) runs in a fresh spawned process, so ``cold`` (compile + first call) and the peak RSS (above the
|
|
11
|
+
post-import baseline, compile included) belong to that method alone; ``warm`` is the median of ``REPS`` synchronized calls
|
|
12
|
+
(``block_until_ready``). The forward pass includes assembly. ``eta`` is the largest ``||F y - lam G y|| / ((||F|| + |lam| ||G||)
|
|
13
|
+
||y||)`` of the requested modes (infinity norm of the matrices), recomputed here from the returned vectors; ``err`` is the
|
|
14
|
+
relative eigenvalue error against ``dense-scipy`` and ``gerr`` the relative L2 gradient difference against ``dense-jax``.
|
|
15
|
+
``busy thr`` is CPU time over wall time of the warm forward calls: ``THREADS`` pins BLAS/LAPACK, but XLA's own dots may
|
|
16
|
+
still use several cores on this jaxlib. Speed-ups are printed only when both methods reach ``TOL``. The gradient is that of
|
|
17
|
+
``frequency_hz`` (mean of the requested modes) with respect to every floating-point geometry array.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
THREADS, REPS, SLOW, TOL, LOAD_MAX, WAIT_MAX, DENSE_MAX, ITERS = (
|
|
23
|
+
1,
|
|
24
|
+
5,
|
|
25
|
+
30.0,
|
|
26
|
+
1e-10,
|
|
27
|
+
4.0,
|
|
28
|
+
120,
|
|
29
|
+
5000,
|
|
30
|
+
1000,
|
|
31
|
+
) # SLOW: seconds above which REPS falls to 2
|
|
32
|
+
CASES = [ # (label, kind, N, H, requested modes); N = 0 takes the file's surfaces (CTOK) or the fixture's (49)
|
|
33
|
+
("CTOK n=2", "ctok", 0, 13, 6),
|
|
34
|
+
("Koenies N=200", "koenies", 200, 12, 3),
|
|
35
|
+
("Koenies N=400", "koenies", 400, 12, 3),
|
|
36
|
+
("Knyazev QA", "knyazev", 49, 64, 6),
|
|
37
|
+
]
|
|
38
|
+
BASE = ("koenies", 200, 12, 3) # scaling sweeps vary one of (N, H, modes) at a time from here
|
|
39
|
+
SWEEPS = {"N": (200, 400, 800, 1600, 3200), "H": (12, 16, 24, 32, 48), "modes": (1, 3, 6, 12, 24)} # N H > 2048: no dense fallback
|
|
40
|
+
|
|
41
|
+
os.environ.update( # before numpy and jax load; spawned workers inherit them
|
|
42
|
+
OMP_NUM_THREADS=str(THREADS),
|
|
43
|
+
OPENBLAS_NUM_THREADS=str(THREADS),
|
|
44
|
+
VECLIB_MAXIMUM_THREADS=str(THREADS),
|
|
45
|
+
XLA_FLAGS=f"--xla_cpu_multi_thread_eigen={THREADS > 1} intra_op_parallelism_threads={THREADS}".replace("True", "true").replace(
|
|
46
|
+
"False", "false"
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
import platform # noqa: E402
|
|
51
|
+
import resource # noqa: E402
|
|
52
|
+
import statistics # noqa: E402
|
|
53
|
+
import sys # noqa: E402
|
|
54
|
+
import time # noqa: E402
|
|
55
|
+
from multiprocessing import get_context # noqa: E402
|
|
56
|
+
from pathlib import Path # noqa: E402
|
|
57
|
+
|
|
58
|
+
import jax # noqa: E402
|
|
59
|
+
import jax.numpy as jnp # noqa: E402
|
|
60
|
+
import jaxlib # noqa: E402
|
|
61
|
+
import numpy as np # noqa: E402
|
|
62
|
+
import scipy # noqa: E402
|
|
63
|
+
import scipy.linalg # noqa: E402
|
|
64
|
+
import solvax # noqa: E402
|
|
65
|
+
|
|
66
|
+
import fax # noqa: E402
|
|
67
|
+
from fax.eigen import band, dense, global_matrices # noqa: E402
|
|
68
|
+
|
|
69
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build(kind, N, H):
|
|
73
|
+
"""``(geometry, harmonics, density, target Hz, grid)`` of a case."""
|
|
74
|
+
if kind == "ctok":
|
|
75
|
+
geo = fax.read_boozmn(ROOT / "tests" / "data" / "boozmn_ctok.nc").full_mesh()
|
|
76
|
+
return geo, fax.mode_family(2, 12), fax.mass_density(1e19, 2.0), 384e3, dict(ntheta=32, nzeta=4)
|
|
77
|
+
if kind == "koenies": # Koenies et al. (2018) ITPA TAE, n = 6, q = 1.71 + 0.16 r^2; m = 5 .. 4 + H on a grid that resolves them
|
|
78
|
+
s = (jnp.arange(N) + 0.5) / N
|
|
79
|
+
geo = fax.circular_torus(s=s, major_radius=10.0, minor_radius=1.0, B0=3.0, iota=1 / (1.71 + 0.16 * s), epsilon_B=0.1)
|
|
80
|
+
return geo, fax.mode_family(6, 4 + H, 5), fax.mass_density(2e19, 1.0), 6.46e4, dict(ntheta=32 * -(-H // 12), nzeta=4)
|
|
81
|
+
from knyazev_qa import RHO, geometry, load # examples/ is on sys.path
|
|
82
|
+
|
|
83
|
+
fx = load()
|
|
84
|
+
return geometry(fx, N), jnp.asarray(fx["mn"][fx["order"][:H]], dtype=float), RHO, 30.3e3, dict(ntheta=64, nzeta=48)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def rss():
|
|
88
|
+
"""Peak RSS of this process in MB."""
|
|
89
|
+
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / (1e6 if sys.platform == "darwin" else 1e3)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def cpu():
|
|
93
|
+
"""CPU seconds used by this process so far."""
|
|
94
|
+
u = resource.getrusage(resource.RUSAGE_SELF)
|
|
95
|
+
return u.ru_utime + u.ru_stime
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def timed(f, *args):
|
|
99
|
+
"""``(result, cold, warm, reps, threads)``: compile + first call, the median of ``REPS`` synchronized calls (2 if the
|
|
100
|
+
first is slow) and the mean number of busy threads (CPU time over wall time) of those calls."""
|
|
101
|
+
t = time.perf_counter()
|
|
102
|
+
out = jax.block_until_ready(f(*args))
|
|
103
|
+
cold, runs, c = time.perf_counter() - t, [], cpu()
|
|
104
|
+
for _ in range(REPS if cold < SLOW else 2):
|
|
105
|
+
t = time.perf_counter()
|
|
106
|
+
jax.block_until_ready(f(*args))
|
|
107
|
+
runs.append(time.perf_counter() - t)
|
|
108
|
+
return out, cold, statistics.median(runs), len(runs), (cpu() - c) / sum(runs)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def eta(F, G, lam, y):
|
|
112
|
+
"""Largest relative residual of ``F y = lam G y`` for ``y`` of shape ``(modes, N, H)`` (blocks ``F``, ``G``)."""
|
|
113
|
+
y = jnp.moveaxis(y, 0, -1)
|
|
114
|
+
mv = lambda A: solvax.block_tridiag_matvec(*band(A), y) # noqa: E731
|
|
115
|
+
norm = lambda A: jnp.max(sum(jnp.abs(b).sum(-1) for b in band(A))) # noqa: E731 - infinity norm
|
|
116
|
+
r = jnp.linalg.norm((mv(F) - lam * mv(G)).reshape(-1, len(lam)), axis=0)
|
|
117
|
+
return float(jnp.max(r / ((norm(F) + jnp.abs(lam) * norm(G)) * jnp.linalg.norm(y.reshape(-1, len(lam)), axis=0))))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def work(job):
|
|
121
|
+
"""One (case, method) in this process: forward and gradient timings, peak memory, then accuracy and solve-only diagnostics."""
|
|
122
|
+
kind, N, H, count, method = job
|
|
123
|
+
geo, h, rho, target, grid = build(kind, N, H)
|
|
124
|
+
N, H = geo.s.size, h.shape[0]
|
|
125
|
+
jax.block_until_ready(geo)
|
|
126
|
+
base, w0 = rss(), (2 * np.pi * target) ** 2
|
|
127
|
+
opts = dict(target_hz=target, count=count, iters=ITERS, **grid) if method == "targeted" else grid
|
|
128
|
+
pick = lambda w: np.sort(np.argsort(np.abs(np.asarray(w) - w0))[:count]) # noqa: E731 - the modes nearest the target
|
|
129
|
+
asm = jax.jit(lambda g: tuple(dense(b) for b in global_matrices(g, h, rho, **grid)))
|
|
130
|
+
fwd = {
|
|
131
|
+
"targeted": lambda: jax.jit(lambda g: fax.eigenmodes(g, h, rho, **opts)),
|
|
132
|
+
"dense-jax": lambda: jax.jit(lambda g: fax.eigenmodes(g, h, rho, **opts)),
|
|
133
|
+
"dense-scipy": lambda: lambda g: scipy.linalg.eigh(*map(np.asarray, asm(g))),
|
|
134
|
+
}[method]()
|
|
135
|
+
r, cold, warm, reps, threads = timed(fwd, geo)
|
|
136
|
+
out = dict(N=N, H=H, count=count, base=base, grid=grid, cold=cold, warm=warm, reps=reps, threads=threads, mem=rss() - base)
|
|
137
|
+
if method == "dense-scipy": # (eigenvalues, eigenvectors): the modes nearest the target
|
|
138
|
+
lam, y = r[0][pick(r[0])], r[1][:, pick(r[0])].T.reshape(count, N, H)
|
|
139
|
+
else:
|
|
140
|
+
idx = slice(None) if method == "targeted" else pick(r.omega2)
|
|
141
|
+
lam, y = r.omega2[idx], r.structures[idx]
|
|
142
|
+
start = int(idx[0]) if method == "dense-jax" else 0 # the nearest modes are contiguous in the sorted spectrum
|
|
143
|
+
grad = jax.jit(jax.grad(lambda g: fax.frequency_hz(g, h, rho, start, start + count, **opts), allow_int=True))
|
|
144
|
+
gr, out["gcold"], out["gwarm"], *_ = timed(grad, geo)
|
|
145
|
+
out["gmem"] = rss() - base
|
|
146
|
+
out["grad"] = np.concatenate([np.ravel(a) for a in jax.tree_util.tree_leaves(gr) if a.dtype.kind == "f"])
|
|
147
|
+
out["temp"] = fwd.lower(geo).compile().memory_analysis().temp_size_in_bytes / 1e6 # XLA static buffers of the forward pass
|
|
148
|
+
F, G = global_matrices(geo, h, rho, **grid)
|
|
149
|
+
out["lam"], out["eta"] = np.asarray(lam), eta(F, G, jnp.asarray(lam), jnp.asarray(y))
|
|
150
|
+
if method == "targeted":
|
|
151
|
+
solve = jax.jit(lambda F, G: fax.eigen.targeted_eigenpairs(F, G, w0, count, ITERS)[0])
|
|
152
|
+
fac = jax.jit(lambda F, G: solvax.block_thomas_factor(*(f - w0 * g for f, g in zip(band(F), band(G), strict=True))))
|
|
153
|
+
out["factor"] = timed(fac, F, G)[2] # one factorization of F - sigma G: O(N H^3) work, O(N H^2) storage
|
|
154
|
+
out["fmb"] = sum(a.nbytes for a in jax.tree_util.tree_leaves(fac(F, G))) / 1e6
|
|
155
|
+
out["solve"], out["iters"] = timed(solve, F, G)[2], int(r.status.iterations)
|
|
156
|
+
out["stemp"] = solve.lower(F, G).compile().memory_analysis().temp_size_in_bytes / 1e6
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def wait():
|
|
161
|
+
"""Block until the 1-minute load average is below ``LOAD_MAX`` (at most ``WAIT_MAX`` seconds); return it."""
|
|
162
|
+
t = time.time()
|
|
163
|
+
while (load := os.getloadavg()[0]) >= LOAD_MAX and time.time() - t < WAIT_MAX:
|
|
164
|
+
time.sleep(10)
|
|
165
|
+
return load
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def fit(rows, key, var):
|
|
169
|
+
"""Observed exponent of ``key`` against ``var`` (least squares in log-log)."""
|
|
170
|
+
return np.polyfit(np.log([r[var] for r in rows]), np.log([max(r[key], 1e-6) for r in rows]), 1)[0]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def gerr(r, g0):
|
|
174
|
+
"""Relative L2 difference of the gradient of ``r`` from the reference gradient ``g0``."""
|
|
175
|
+
return np.linalg.norm(r["grad"] - g0) / np.linalg.norm(g0) if g0 is not None and "grad" in r else np.nan
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def show(head, rows):
|
|
179
|
+
"""Print a right-aligned table; numbers get three significant digits (``nan`` where not applicable)."""
|
|
180
|
+
cells = [head] + [[c if isinstance(c, str) else f"{c:.3g}" for c in row] for row in rows]
|
|
181
|
+
for row in cells:
|
|
182
|
+
print(" ".join(c.rjust(max(len(x[i]) for x in cells)) if i else c.ljust(max(len(x[0]) for x in cells)) for i, c in enumerate(row)))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
pool, loads = get_context("spawn").Pool(1, maxtasksperchild=1), []
|
|
187
|
+
|
|
188
|
+
def run(case, method):
|
|
189
|
+
loads.append(wait())
|
|
190
|
+
return {**pool.apply(work, ((*case, method),)), "load": loads[-1]}
|
|
191
|
+
|
|
192
|
+
print(
|
|
193
|
+
f"fax {fax.__version__}, solvax {solvax.__version__}, jax {jax.__version__}, jaxlib {jaxlib.__version__}, "
|
|
194
|
+
f"numpy {np.__version__}, scipy {scipy.__version__}; python {platform.python_version()}, {platform.machine()} {platform.system()}"
|
|
195
|
+
)
|
|
196
|
+
print(
|
|
197
|
+
f"{jax.devices()[0].device_kind} ({jax.default_backend()}), {os.cpu_count()} cores, {THREADS} BLAS thread(s), float64, TOL {TOL:g}"
|
|
198
|
+
)
|
|
199
|
+
nan = np.nan
|
|
200
|
+
for label, *case in CASES:
|
|
201
|
+
res = {
|
|
202
|
+
m: run(case, m) for m in ("dense-scipy", "dense-jax", "targeted") if m == "targeted" or (case[1] or 100) * case[2] <= DENSE_MAX
|
|
203
|
+
}
|
|
204
|
+
ref, g0 = res.get("dense-scipy", res["dense-jax"]), res.get("dense-jax", {}).get("grad")
|
|
205
|
+
r0 = res["targeted"]
|
|
206
|
+
for r in res.values():
|
|
207
|
+
r["err"], r["gerr"] = np.max(np.abs(r["lam"] / ref["lam"] - 1)), gerr(r, g0)
|
|
208
|
+
print(f"\n{label}: N {r0['N']}, H {r0['H']}, {r0['count']} modes, angle grid {r0['grid']}, warm = median of {REPS} (fewer if slow)")
|
|
209
|
+
cols = {
|
|
210
|
+
"eta": lambda r: r["eta"],
|
|
211
|
+
"err": lambda r: r["err"],
|
|
212
|
+
"gerr": lambda r: r["gerr"],
|
|
213
|
+
"fwd cold": lambda r: r["cold"],
|
|
214
|
+
"fwd warm": lambda r: r["warm"],
|
|
215
|
+
"busy thr": lambda r: r["threads"],
|
|
216
|
+
"solve": lambda r: r.get("solve", nan),
|
|
217
|
+
"grad cold": lambda r: r.get("gcold", nan),
|
|
218
|
+
"grad warm": lambda r: r.get("gwarm", nan),
|
|
219
|
+
"RSS f/g MB": lambda r: f"{r['mem']:.0f}/{r.get('gmem', nan):.0f}",
|
|
220
|
+
"XLA MB": lambda r: r.get("temp", nan),
|
|
221
|
+
"iters": lambda r: r.get("iters", 0),
|
|
222
|
+
"load": lambda r: r["load"],
|
|
223
|
+
}
|
|
224
|
+
show(["method", *cols], [[m, *(f(r) for f in cols.values())] for m, r in res.items()])
|
|
225
|
+
if any(r["reps"] < REPS for r in res.values()):
|
|
226
|
+
print("warm forward reps: " + ", ".join(f"{m} {r['reps']}" for m, r in res.items()))
|
|
227
|
+
if "dense-jax" in res and max(res["dense-jax"]["eta"], r0["eta"]) <= TOL:
|
|
228
|
+
d = res["dense-jax"]
|
|
229
|
+
print(
|
|
230
|
+
f"targeted vs dense-jax at eta <= {TOL:g}: forward x{d['warm'] / r0['warm']:.1f}, gradient x{d['gwarm'] / r0['gwarm']:.1f}"
|
|
231
|
+
)
|
|
232
|
+
print(f"\nscaling (targeted, from {BASE[0]} N={BASE[1]} H={BASE[2]} modes={BASE[3]}); factor, solve: SOLVAX on assembled blocks")
|
|
233
|
+
cols = dict(N="N", H="H", modes="count", eta="eta", iters="iters", factor="factor", solve="solve")
|
|
234
|
+
cols |= {"fwd warm": "warm", "grad warm": "gwarm", "factor MB": "fmb", "solve MB": "stemp", "RSS MB": "mem", "load": "load"}
|
|
235
|
+
rows = {
|
|
236
|
+
var: [run((BASE[0], *[v if k == var else b for k, b in zip(SWEEPS, BASE[1:], strict=True)]), "targeted") for v in values]
|
|
237
|
+
for var, values in SWEEPS.items()
|
|
238
|
+
}
|
|
239
|
+
show(["var", *cols], [[var, *(r[c] for c in cols.values())] for var, rs in rows.items() for r in rs])
|
|
240
|
+
for k, rs in rows.items():
|
|
241
|
+
names = {"N": "N", "H": "H", "modes": "count"}
|
|
242
|
+
ok = [r for r in rs if r["eta"] <= TOL] # only points that reached the tolerance
|
|
243
|
+
fits = {n: fit(ok, cols[n], names[k]) for n in ("factor", "solve", "fwd warm", "grad warm", "factor MB", "solve MB", "RSS MB")}
|
|
244
|
+
print(f"observed exponent in {k} (eta <= TOL only): " + ", ".join(f"{n} {e:.2f}" for n, e in fits.items()))
|
|
245
|
+
print("expected: factorization O(N H^3), factor storage O(N H^2); solve = factor + iterations x O(N H^2 (2 modes + 8))")
|
|
246
|
+
print(f"load before each run: min {min(loads):.1f}, median {np.median(loads):.1f}, max {max(loads):.1f}, {len(loads)} runs,")
|
|
247
|
+
print(f"{sum(x >= LOAD_MAX for x in loads)} not below {LOAD_MAX:g}; RSS is above a baseline of {r0['base']:.0f} MB")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Gradient of global Alfvén frequencies with respect to VMEC boundary coefficients.
|
|
2
|
+
|
|
3
|
+
Run: python examples/boundary_gradient.py (needs ``pip install "alfvex[vmex]"``)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import jax
|
|
9
|
+
import jax.numpy as jnp
|
|
10
|
+
|
|
11
|
+
import fax
|
|
12
|
+
|
|
13
|
+
design = fax.prepare_design(
|
|
14
|
+
Path(__file__).parent / "data" / "input.LandremanPaul2021_QA_lowres",
|
|
15
|
+
harmonics=[[1.0, 0.0], [2.0, 0.0], [3.0, 0.0]],
|
|
16
|
+
density=3.4e-8,
|
|
17
|
+
parameters=("rc01", "zs01"),
|
|
18
|
+
ntheta=8,
|
|
19
|
+
nzeta=8,
|
|
20
|
+
)
|
|
21
|
+
objective = lambda p: fax.window_risk(fax.design_frequencies(p, design)[0], (250e3, 290e3), 10e3) # noqa: E731
|
|
22
|
+
p = jnp.zeros(2)
|
|
23
|
+
value, grad = jax.value_and_grad(objective)(p)
|
|
24
|
+
print(f"frequencies (kHz): {fax.design_frequencies(p, design)[0] / 1e3}")
|
|
25
|
+
print(f"risk = {value:.4e}, d risk / d {design.names} = {grad}")
|
|
26
|
+
step = -1e-3 * grad / jnp.linalg.norm(grad)
|
|
27
|
+
print(f"risk after a 1e-3 step along -grad: {objective(p + step):.4e}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# STELLGAP CTOK test case: n = 2 continuum in a circular tokamak, deuterium at 1e19 m^-3.
|
|
2
|
+
[geometry]
|
|
3
|
+
source = "boozmn"
|
|
4
|
+
path = "../tests/data/boozmn_ctok.nc"
|
|
5
|
+
|
|
6
|
+
[modes]
|
|
7
|
+
n = [2]
|
|
8
|
+
m_max = 12
|
|
9
|
+
|
|
10
|
+
[plasma]
|
|
11
|
+
density = 1e19
|
|
12
|
+
ion_mass = 2.0
|
|
13
|
+
|
|
14
|
+
[run]
|
|
15
|
+
task = "continuum"
|
|
16
|
+
output = "../outputs/ctok_continuum"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Global AE3D-form eigenmodes of CTOK nearest 384 kHz (AE3D's m = 2, 3 global mode).
|
|
2
|
+
[geometry]
|
|
3
|
+
source = "boozmn"
|
|
4
|
+
path = "../tests/data/boozmn_ctok.nc"
|
|
5
|
+
|
|
6
|
+
[modes]
|
|
7
|
+
n = [2]
|
|
8
|
+
m_max = 12
|
|
9
|
+
|
|
10
|
+
[plasma]
|
|
11
|
+
density = 1e19
|
|
12
|
+
ion_mass = 2.0
|
|
13
|
+
|
|
14
|
+
[run]
|
|
15
|
+
task = "eigenmodes"
|
|
16
|
+
output = "../outputs/ctok_eigenmodes"
|
|
17
|
+
ntheta = 32
|
|
18
|
+
nzeta = 4
|
|
19
|
+
target_khz = 384.0
|
|
20
|
+
count = 6
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
&INDATA
|
|
2
|
+
LASYM = F, NFP = 1, MPOL = 8, NTOR = 0, NZETA = 1, NTHETA = 32,
|
|
3
|
+
NS_ARRAY = 16 51, FTOL_ARRAY = 1e-10 1e-12, NITER_ARRAY = 2000 5000,
|
|
4
|
+
DELT = 0.9, NSTEP = 200, PHIEDGE = 9.42477796, NCURR = 0, GAMMA = 0.0,
|
|
5
|
+
PRES_SCALE = 1.0, AM = 7.17e3 -6.811e3 -3.585e2,
|
|
6
|
+
AI = 5.84795322e-01 -5.47176905e-02 5.11977817e-03 -4.79011335e-04 4.47304912e-05 -4.05618024e-06 2.86012825e-07,
|
|
7
|
+
RAXIS_CC = 10.0, ZAXIS_CS = 0.0,
|
|
8
|
+
RBC(0,0) = 10.0, RBC(0,1) = 1.0, ZBS(0,1) = 1.0,
|
|
9
|
+
/
|