vmecdash 0.1.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.
- vmecdash-0.1.0/LICENSE +21 -0
- vmecdash-0.1.0/PKG-INFO +145 -0
- vmecdash-0.1.0/README.md +113 -0
- vmecdash-0.1.0/pyproject.toml +53 -0
- vmecdash-0.1.0/setup.cfg +4 -0
- vmecdash-0.1.0/tests/test_cross_section_mesh.py +158 -0
- vmecdash-0.1.0/tests/test_fieldline_numerics.py +81 -0
- vmecdash-0.1.0/tests/test_view_schema.py +94 -0
- vmecdash-0.1.0/tests/test_vscode_backend.py +94 -0
- vmecdash-0.1.0/vmecdash/__init__.py +8 -0
- vmecdash-0.1.0/vmecdash/cli.py +26 -0
- vmecdash-0.1.0/vmecdash/core/__init__.py +4 -0
- vmecdash-0.1.0/vmecdash/core/vmec_jax.py +966 -0
- vmecdash-0.1.0/vmecdash/dash_app/__init__.py +2 -0
- vmecdash-0.1.0/vmecdash/dash_app/app.py +790 -0
- vmecdash-0.1.0/vmecdash/dash_app/assets/icon_stell.png +0 -0
- vmecdash-0.1.0/vmecdash/dash_app/assets/icon_stell_circle.png +0 -0
- vmecdash-0.1.0/vmecdash/dash_app/assets/icon_stell_noback.png +0 -0
- vmecdash-0.1.0/vmecdash/dash_app/assets/icon_stell_round.png +0 -0
- vmecdash-0.1.0/vmecdash/dash_app/cards.py +175 -0
- vmecdash-0.1.0/vmecdash/dash_app/controls.py +263 -0
- vmecdash-0.1.0/vmecdash/py.typed +1 -0
- vmecdash-0.1.0/vmecdash/renderers/__init__.py +18 -0
- vmecdash-0.1.0/vmecdash/renderers/fieldline.py +114 -0
- vmecdash-0.1.0/vmecdash/renderers/overview.py +57 -0
- vmecdash-0.1.0/vmecdash/renderers/profiles.py +34 -0
- vmecdash-0.1.0/vmecdash/renderers/three_d.py +58 -0
- vmecdash-0.1.0/vmecdash/renderers/two_d.py +231 -0
- vmecdash-0.1.0/vmecdash/stats.py +110 -0
- vmecdash-0.1.0/vmecdash/theme.py +38 -0
- vmecdash-0.1.0/vmecdash/view_schema.py +261 -0
- vmecdash-0.1.0/vmecdash/vscode_backend.py +248 -0
- vmecdash-0.1.0/vmecdash.egg-info/PKG-INFO +145 -0
- vmecdash-0.1.0/vmecdash.egg-info/SOURCES.txt +36 -0
- vmecdash-0.1.0/vmecdash.egg-info/dependency_links.txt +1 -0
- vmecdash-0.1.0/vmecdash.egg-info/entry_points.txt +2 -0
- vmecdash-0.1.0/vmecdash.egg-info/requires.txt +14 -0
- vmecdash-0.1.0/vmecdash.egg-info/top_level.txt +1 -0
vmecdash-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hengqian Liu
|
|
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.
|
vmecdash-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vmecdash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dash and VS Code tooling for inspecting VMEC wout NetCDF equilibria
|
|
5
|
+
Author: Hengqian Liu
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/DMCXE/VMECdash
|
|
8
|
+
Project-URL: Repository, https://github.com/DMCXE/VMECdash
|
|
9
|
+
Project-URL: Issues, https://github.com/DMCXE/VMECdash/issues
|
|
10
|
+
Keywords: vmec,stellarator,plasma,equilibrium,netcdf,plotly,fusion
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: plotly>=5.22.0
|
|
20
|
+
Requires-Dist: numpy>=1.24.0
|
|
21
|
+
Requires-Dist: xarray>=2024.1.0
|
|
22
|
+
Requires-Dist: netCDF4>=1.6.5
|
|
23
|
+
Requires-Dist: jax>=0.4.26
|
|
24
|
+
Requires-Dist: jaxlib>=0.4.26
|
|
25
|
+
Provides-Extra: dash
|
|
26
|
+
Requires-Dist: dash>=2.16.1; extra == "dash"
|
|
27
|
+
Requires-Dist: dash-mantine-components>=0.14.5; extra == "dash"
|
|
28
|
+
Requires-Dist: dash-iconify>=0.1.2; extra == "dash"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# VMECdash
|
|
34
|
+
|
|
35
|
+
An interactive Dash application for exploring VMEC `wout_*.nc` stellarator equilibria.
|
|
36
|
+
It reconstructs magnetic surfaces with JAX, renders 1‑D/2‑D/3‑D plots and so on, motivated by the original MATLAB tool **VMECplot.m**.
|
|
37
|
+
|
|
38
|
+
**Motivation:** Quickly inspect the VMEC equilibrium quantities of interest **without relying on** difficult-for-starter-to-compile programs (such as **libstell**) or memory-heavy commercial software (such as **MATLAB**).
|
|
39
|
+
|
|
40
|
+
[](example/summary.png)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **File upload** for standard VMEC `wout` NetCDF output.
|
|
46
|
+
- **Summary dashboard** with key scalar diagnostics and highlighted metadata (free/fixed boundary, mgrid file, etc.).
|
|
47
|
+
- **1‑D profiles** for rotational transform, safety factor, pressure, volume derivative, beta metrics, and more.
|
|
48
|
+
- **2‑D visualisations**:
|
|
49
|
+
- R‑Z cross sections with geometry or interpolated scalar fields.
|
|
50
|
+
- θ‑ζ flux-surface contours across a field period.
|
|
51
|
+
- **3‑D flux surfaces** with optional coordinate‑free background shading.
|
|
52
|
+
- **Field lines viewer** for $\alpha-\zeta$ coordinates on selected flux
|
|
53
|
+
- A nice choice for visualising magnetic ripple
|
|
54
|
+
- 2D plots of $|B|(\alpha, \zeta)$
|
|
55
|
+
- 1D plots of single period field line traces with different initial $\alpha$ values
|
|
56
|
+
- Single field line transistions.
|
|
57
|
+
- **JAX acceleration** for geometry reconstruction.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Requirements
|
|
62
|
+
|
|
63
|
+
Python 3.10+ is recommended. Install dependencies with conda:(optional)
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
conda activate your_env_name
|
|
67
|
+
pip install -r requirements.txt
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> **Note for JAX:** If you don't have a GPU/TPU or your gpu is not supported for FP64(like metal), then simply running `pip install jax[cpu]` to install the CPU-only version is good enough.
|
|
71
|
+
> GPU/TPU wheels require platform-specific instructions from the [JAX documentation](https://github.com/google/jax#pip-installation).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Running the App
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python VMECdash.py
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Dash defaults to `http://127.0.0.1:8050/`. The layout is responsive, so you can resize the browser to focus on plots or the control sidebar.
|
|
82
|
+
|
|
83
|
+
The packaged entry point is also available after installation:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install -e ".[dash]"
|
|
87
|
+
vmecdash serve
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## VS Code Native Preview
|
|
93
|
+
|
|
94
|
+
VMECdash now includes the first native VS Code integration surface:
|
|
95
|
+
|
|
96
|
+
- a Dash-free Python backend at `python -m vmecdash.vscode_backend --stdio`;
|
|
97
|
+
- a workspace VS Code extension under `extension/`;
|
|
98
|
+
- a Custom Readonly Editor for `wout*.nc` files;
|
|
99
|
+
- a Webview UI that renders backend Plotly figures with bundled `plotly.min.js`.
|
|
100
|
+
|
|
101
|
+
For local development, install the Python package in the interpreter VS Code should use:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install -e .
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Then open `extension/` as a VS Code extension development project or package it as a VSIX. In Remote-SSH, install `vmecdash` in the remote Python environment and set `vmecdash.pythonPath` if VS Code does not pick the desired interpreter.
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Usage Tips
|
|
113
|
+
|
|
114
|
+
1. **Upload** a VMEC NetCDF file (`wout_*.nc`) via the drag‑and‑drop area in the sidebar.
|
|
115
|
+
2. Use **Visualization Mode** to switch between:
|
|
116
|
+
- **Summary Dashboard** (shows statistics + multi-panel plots),
|
|
117
|
+
- **1D Profiles**, **2D Cross Sections**, **2D Flux Surfaces**, and **3D Geometry**.
|
|
118
|
+
3. Adjust **sliders** for toroidal angle (`phi`), flux surface index (`s`), and choose different physical quantities from the dropdowns.
|
|
119
|
+
4. Toggle **Coordinate-free background** in 3‑D mode for clean screenshots.
|
|
120
|
+
5. Click **Download Plot** to export the currently visible figure as a PNG.
|
|
121
|
+
|
|
122
|
+
The app caches equilibrium metadata, so switching modes or variables is fast.
|
|
123
|
+
For heavy 2‑D physics overlays, a pre-computation step runs on the server while keeping the UI responsive.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Repository Layout
|
|
128
|
+
|
|
129
|
+
| Path | Description |
|
|
130
|
+
| ------------------ | ---------------------------------------------------------------- |
|
|
131
|
+
| `VMECdash.py`| Main Dash entry point (layout + callbacks). |
|
|
132
|
+
| `vmec_jax.py` | JAX-powered data processor for VMEC equilibria. |
|
|
133
|
+
| `views/` | Modular UI components (overview, profiles, 2D/3D, fieldlines). |
|
|
134
|
+
| `ui/` | Shared UI helper components. |
|
|
135
|
+
| `requirements.txt` | Python dependencies. |
|
|
136
|
+
| `example/wout_PO.nc` | Example VMEC equilibrium (use your own files for new cases). |
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## TroubleShooting
|
|
141
|
+
Feel free to open issues or pull requests to add new physical quantities, UI tweaks, or performance optimisations. Enjoy exploring your VMEC equilibria!
|
|
142
|
+
|
|
143
|
+
## Next step
|
|
144
|
+
- Add jax-based boozer coordinate transformation.
|
|
145
|
+
- Fast evaulation of EffetiveRipple, GammaC, maybe slow without gpu.
|
vmecdash-0.1.0/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# VMECdash
|
|
2
|
+
|
|
3
|
+
An interactive Dash application for exploring VMEC `wout_*.nc` stellarator equilibria.
|
|
4
|
+
It reconstructs magnetic surfaces with JAX, renders 1‑D/2‑D/3‑D plots and so on, motivated by the original MATLAB tool **VMECplot.m**.
|
|
5
|
+
|
|
6
|
+
**Motivation:** Quickly inspect the VMEC equilibrium quantities of interest **without relying on** difficult-for-starter-to-compile programs (such as **libstell**) or memory-heavy commercial software (such as **MATLAB**).
|
|
7
|
+
|
|
8
|
+
[](example/summary.png)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **File upload** for standard VMEC `wout` NetCDF output.
|
|
14
|
+
- **Summary dashboard** with key scalar diagnostics and highlighted metadata (free/fixed boundary, mgrid file, etc.).
|
|
15
|
+
- **1‑D profiles** for rotational transform, safety factor, pressure, volume derivative, beta metrics, and more.
|
|
16
|
+
- **2‑D visualisations**:
|
|
17
|
+
- R‑Z cross sections with geometry or interpolated scalar fields.
|
|
18
|
+
- θ‑ζ flux-surface contours across a field period.
|
|
19
|
+
- **3‑D flux surfaces** with optional coordinate‑free background shading.
|
|
20
|
+
- **Field lines viewer** for $\alpha-\zeta$ coordinates on selected flux
|
|
21
|
+
- A nice choice for visualising magnetic ripple
|
|
22
|
+
- 2D plots of $|B|(\alpha, \zeta)$
|
|
23
|
+
- 1D plots of single period field line traces with different initial $\alpha$ values
|
|
24
|
+
- Single field line transistions.
|
|
25
|
+
- **JAX acceleration** for geometry reconstruction.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
Python 3.10+ is recommended. Install dependencies with conda:(optional)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
conda activate your_env_name
|
|
35
|
+
pip install -r requirements.txt
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> **Note for JAX:** If you don't have a GPU/TPU or your gpu is not supported for FP64(like metal), then simply running `pip install jax[cpu]` to install the CPU-only version is good enough.
|
|
39
|
+
> GPU/TPU wheels require platform-specific instructions from the [JAX documentation](https://github.com/google/jax#pip-installation).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Running the App
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python VMECdash.py
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Dash defaults to `http://127.0.0.1:8050/`. The layout is responsive, so you can resize the browser to focus on plots or the control sidebar.
|
|
50
|
+
|
|
51
|
+
The packaged entry point is also available after installation:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install -e ".[dash]"
|
|
55
|
+
vmecdash serve
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## VS Code Native Preview
|
|
61
|
+
|
|
62
|
+
VMECdash now includes the first native VS Code integration surface:
|
|
63
|
+
|
|
64
|
+
- a Dash-free Python backend at `python -m vmecdash.vscode_backend --stdio`;
|
|
65
|
+
- a workspace VS Code extension under `extension/`;
|
|
66
|
+
- a Custom Readonly Editor for `wout*.nc` files;
|
|
67
|
+
- a Webview UI that renders backend Plotly figures with bundled `plotly.min.js`.
|
|
68
|
+
|
|
69
|
+
For local development, install the Python package in the interpreter VS Code should use:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install -e .
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then open `extension/` as a VS Code extension development project or package it as a VSIX. In Remote-SSH, install `vmecdash` in the remote Python environment and set `vmecdash.pythonPath` if VS Code does not pick the desired interpreter.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Usage Tips
|
|
81
|
+
|
|
82
|
+
1. **Upload** a VMEC NetCDF file (`wout_*.nc`) via the drag‑and‑drop area in the sidebar.
|
|
83
|
+
2. Use **Visualization Mode** to switch between:
|
|
84
|
+
- **Summary Dashboard** (shows statistics + multi-panel plots),
|
|
85
|
+
- **1D Profiles**, **2D Cross Sections**, **2D Flux Surfaces**, and **3D Geometry**.
|
|
86
|
+
3. Adjust **sliders** for toroidal angle (`phi`), flux surface index (`s`), and choose different physical quantities from the dropdowns.
|
|
87
|
+
4. Toggle **Coordinate-free background** in 3‑D mode for clean screenshots.
|
|
88
|
+
5. Click **Download Plot** to export the currently visible figure as a PNG.
|
|
89
|
+
|
|
90
|
+
The app caches equilibrium metadata, so switching modes or variables is fast.
|
|
91
|
+
For heavy 2‑D physics overlays, a pre-computation step runs on the server while keeping the UI responsive.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Repository Layout
|
|
96
|
+
|
|
97
|
+
| Path | Description |
|
|
98
|
+
| ------------------ | ---------------------------------------------------------------- |
|
|
99
|
+
| `VMECdash.py`| Main Dash entry point (layout + callbacks). |
|
|
100
|
+
| `vmec_jax.py` | JAX-powered data processor for VMEC equilibria. |
|
|
101
|
+
| `views/` | Modular UI components (overview, profiles, 2D/3D, fieldlines). |
|
|
102
|
+
| `ui/` | Shared UI helper components. |
|
|
103
|
+
| `requirements.txt` | Python dependencies. |
|
|
104
|
+
| `example/wout_PO.nc` | Example VMEC equilibrium (use your own files for new cases). |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## TroubleShooting
|
|
109
|
+
Feel free to open issues or pull requests to add new physical quantities, UI tweaks, or performance optimisations. Enjoy exploring your VMEC equilibria!
|
|
110
|
+
|
|
111
|
+
## Next step
|
|
112
|
+
- Add jax-based boozer coordinate transformation.
|
|
113
|
+
- Fast evaulation of EffetiveRipple, GammaC, maybe slow without gpu.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vmecdash"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Dash and VS Code tooling for inspecting VMEC wout NetCDF equilibria"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Hengqian Liu" }]
|
|
13
|
+
keywords = ["vmec", "stellarator", "plasma", "equilibrium", "netcdf", "plotly", "fusion"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"plotly>=5.22.0",
|
|
23
|
+
"numpy>=1.24.0",
|
|
24
|
+
"xarray>=2024.1.0",
|
|
25
|
+
"netCDF4>=1.6.5",
|
|
26
|
+
"jax>=0.4.26",
|
|
27
|
+
"jaxlib>=0.4.26",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dash = [
|
|
32
|
+
"dash>=2.16.1",
|
|
33
|
+
"dash-mantine-components>=0.14.5",
|
|
34
|
+
"dash-iconify>=0.1.2",
|
|
35
|
+
]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
vmecdash = "vmecdash.cli:main"
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/DMCXE/VMECdash"
|
|
45
|
+
Repository = "https://github.com/DMCXE/VMECdash"
|
|
46
|
+
Issues = "https://github.com/DMCXE/VMECdash/issues"
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.packages.find]
|
|
49
|
+
where = ["."]
|
|
50
|
+
include = ["vmecdash*"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.package-data]
|
|
53
|
+
vmecdash = ["py.typed", "dash_app/assets/*.png"]
|
vmecdash-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
os.environ.setdefault("MPLCONFIGDIR", "/tmp/mpl-codex")
|
|
9
|
+
|
|
10
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
11
|
+
if str(ROOT) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(ROOT))
|
|
13
|
+
|
|
14
|
+
from vmecdash.core import VMECJaxProcessor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
EXAMPLE_WOUT = "example/wout_PO.nc"
|
|
18
|
+
PS3_WOUT = Path("/Users/dmcxe/Downloads/wout_PS3_ms_dofs.nc")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _eval_fourier_rows(cos_coeffs, sin_coeffs, xm, xn, theta, phi):
|
|
22
|
+
theta = np.asarray(theta)
|
|
23
|
+
angle = np.asarray(xm)[:, None] * theta[None, :] - np.asarray(xn)[:, None] * phi
|
|
24
|
+
cos_angle = np.cos(angle)
|
|
25
|
+
sin_angle = np.sin(angle)
|
|
26
|
+
return np.asarray(cos_coeffs) @ cos_angle + np.asarray(sin_coeffs) @ sin_angle
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _simsopt_full_grid_geometry(phi, theta):
|
|
30
|
+
simsopt_mhd = pytest.importorskip("simsopt.mhd")
|
|
31
|
+
vmec = simsopt_mhd.Vmec(EXAMPLE_WOUT)
|
|
32
|
+
splines = simsopt_mhd.vmec_splines(vmec)
|
|
33
|
+
return simsopt_mhd.vmec_compute_geometry(splines, vmec.s_full_grid, theta, np.array([phi]))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_cross_section_mesh_half_mesh_fields_match_simsopt_full_grid():
|
|
37
|
+
"""Half-mesh fields are spline-interpolated from VMEC half grid to full nodes."""
|
|
38
|
+
vmec = VMECJaxProcessor.from_file(EXAMPLE_WOUT)
|
|
39
|
+
phi = 0.37
|
|
40
|
+
res_u = 32
|
|
41
|
+
theta_nodes = np.linspace(0.0, 2 * np.pi, res_u + 1)
|
|
42
|
+
geometry = _simsopt_full_grid_geometry(phi, theta_nodes)
|
|
43
|
+
|
|
44
|
+
references = {
|
|
45
|
+
"modB": geometry.modB[:, :, 0],
|
|
46
|
+
"jacobian": geometry.sqrt_g_vmec[:, :, 0],
|
|
47
|
+
"lambda": (geometry.theta_pest - geometry.theta_vmec)[:, :, 0],
|
|
48
|
+
"B_u": geometry.B_sub_theta_vmec[:, :, 0],
|
|
49
|
+
"B_v": geometry.B_sub_phi[:, :, 0],
|
|
50
|
+
"B^u": geometry.B_sup_theta_vmec[:, :, 0],
|
|
51
|
+
"B^v": geometry.B_sup_phi[:, :, 0],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for field, expected in references.items():
|
|
55
|
+
r_nodes, z_nodes, values = vmec.get_cross_section_mesh(phi, field, res_u=res_u)
|
|
56
|
+
|
|
57
|
+
assert r_nodes.shape == (vmec.ns, res_u + 1)
|
|
58
|
+
assert z_nodes.shape == (vmec.ns, res_u + 1)
|
|
59
|
+
assert values.shape == (vmec.ns, res_u + 1)
|
|
60
|
+
np.testing.assert_allclose(values[1:], expected[1:], rtol=1e-10, atol=1e-10)
|
|
61
|
+
np.testing.assert_allclose(values[0], values[0, 0], rtol=0.0, atol=0.0)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_cross_section_mesh_full_mesh_evaluates_at_nodes():
|
|
65
|
+
"""Full-mesh fields remain direct Fourier values except for axis single-valuing."""
|
|
66
|
+
vmec = VMECJaxProcessor.from_file(EXAMPLE_WOUT)
|
|
67
|
+
phi = 0.4
|
|
68
|
+
res_u = 24
|
|
69
|
+
|
|
70
|
+
theta_nodes = np.linspace(0.0, 2 * np.pi, res_u + 1)
|
|
71
|
+
for field in ("B_s", "j^u", "j^v"):
|
|
72
|
+
_, _, values = vmec.get_cross_section_mesh(phi, field, res_u=res_u)
|
|
73
|
+
assert values.shape == (vmec.ns, res_u + 1)
|
|
74
|
+
|
|
75
|
+
payload = vmec._field_payload(field)
|
|
76
|
+
cos_coeffs, sin_coeffs = payload["pair"]
|
|
77
|
+
direct = _eval_fourier_rows(cos_coeffs, sin_coeffs, payload["xm"], payload["xn"], theta_nodes, phi)
|
|
78
|
+
|
|
79
|
+
np.testing.assert_allclose(values[1:], direct[1:], rtol=1e-12, atol=1e-8)
|
|
80
|
+
np.testing.assert_allclose(values[0], np.mean(direct[0, :-1]), rtol=1e-12, atol=1e-8)
|
|
81
|
+
|
|
82
|
+
_, _, j_u_values = vmec.get_cross_section_mesh(phi, "j^u", res_u=res_u)
|
|
83
|
+
assert np.max(np.abs(j_u_values[0] - j_u_values[1])) > 1.0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_cross_section_field_renderer_builds_carpet():
|
|
87
|
+
from vmecdash.renderers.two_d import render_cross_section_field
|
|
88
|
+
from vmecdash.theme import build_theme
|
|
89
|
+
|
|
90
|
+
vmec = VMECJaxProcessor.from_file(EXAMPLE_WOUT)
|
|
91
|
+
fig = render_cross_section_field(vmec, 0.0, "modB", "|B| (Mod B)", build_theme(True, 0))
|
|
92
|
+
|
|
93
|
+
trace_types = {trace.type for trace in fig.data}
|
|
94
|
+
assert "carpet" in trace_types
|
|
95
|
+
assert "contourcarpet" in trace_types
|
|
96
|
+
assert len(fig.layout.images) == 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_lambda_cross_section_uses_axis_fill_regularization():
|
|
100
|
+
from vmecdash.renderers.two_d import _carpet_colorscale, _sample_field_color, render_cross_section_field
|
|
101
|
+
from vmecdash.theme import build_theme
|
|
102
|
+
|
|
103
|
+
vmec = VMECJaxProcessor.from_file(EXAMPLE_WOUT)
|
|
104
|
+
_, _, val_nodes = vmec.get_cross_section_mesh(0.0, "lambda", res_u=480)
|
|
105
|
+
colorscale, reversescale, zmin, zmax = _carpet_colorscale(val_nodes)
|
|
106
|
+
axis_value = float(np.nanmean(val_nodes[1, :-1]))
|
|
107
|
+
expected_fill = _sample_field_color(axis_value, colorscale, reversescale, zmin, zmax)
|
|
108
|
+
|
|
109
|
+
fig = render_cross_section_field(vmec, 0.0, "lambda", "Lambda", build_theme(True, 0))
|
|
110
|
+
trace_types = [trace.type for trace in fig.data]
|
|
111
|
+
sample_trace = next(trace for trace in fig.data if trace.name == "Lambda samples")
|
|
112
|
+
|
|
113
|
+
assert trace_types == ["scatter", "scattergl", "scatter"]
|
|
114
|
+
assert "carpet" not in trace_types
|
|
115
|
+
assert "contourcarpet" not in trace_types
|
|
116
|
+
assert not any(trace.name == "Lambda quad fill" for trace in fig.data)
|
|
117
|
+
assert fig.data[0].fill == "toself"
|
|
118
|
+
assert fig.data[0].name == "Axis fill"
|
|
119
|
+
assert fig.data[0].fillcolor == expected_fill
|
|
120
|
+
assert sample_trace.marker.showscale is True
|
|
121
|
+
assert sample_trace.marker.cmin == zmin
|
|
122
|
+
assert sample_trace.marker.cmax == zmax
|
|
123
|
+
assert sample_trace.marker.size == 6
|
|
124
|
+
assert len(sample_trace.x) > 0
|
|
125
|
+
assert len(fig.layout.images) == 0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_modb_cross_section_keeps_degenerate_axis_in_carpet():
|
|
129
|
+
from vmecdash.renderers.two_d import render_cross_section_field
|
|
130
|
+
from vmecdash.theme import build_theme
|
|
131
|
+
|
|
132
|
+
vmec = VMECJaxProcessor.from_file(EXAMPLE_WOUT)
|
|
133
|
+
fig = render_cross_section_field(vmec, 0.0, "modB", "|B| (Mod B)", build_theme(True, 0))
|
|
134
|
+
trace_types = [trace.type for trace in fig.data]
|
|
135
|
+
contour = next(trace for trace in fig.data if trace.type == "contourcarpet")
|
|
136
|
+
|
|
137
|
+
assert trace_types == ["carpet", "contourcarpet", "scatter"]
|
|
138
|
+
assert np.min(np.asarray(contour.b, dtype=float)) == 0.0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_ps3_lambda_cross_section_uses_sampled_field():
|
|
142
|
+
if not PS3_WOUT.exists():
|
|
143
|
+
pytest.skip("PS3 local smoke fixture is not available")
|
|
144
|
+
|
|
145
|
+
from vmecdash.renderers.two_d import render_cross_section_field
|
|
146
|
+
from vmecdash.theme import build_theme
|
|
147
|
+
|
|
148
|
+
vmec = VMECJaxProcessor.from_file(str(PS3_WOUT))
|
|
149
|
+
fig = render_cross_section_field(vmec, 0.0, "lambda", "Lambda", build_theme(True, 0))
|
|
150
|
+
trace_types = [trace.type for trace in fig.data]
|
|
151
|
+
sample_traces = [trace for trace in fig.data if trace.name == "Lambda samples"]
|
|
152
|
+
|
|
153
|
+
assert fig.data[0].fill == "toself"
|
|
154
|
+
assert "contourcarpet" not in trace_types
|
|
155
|
+
assert "carpet" not in trace_types
|
|
156
|
+
assert not any(trace.name == "Lambda quad fill" for trace in fig.data)
|
|
157
|
+
assert len(sample_traces) == 1
|
|
158
|
+
assert len(sample_traces[0].x) > 0
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
+
if str(ROOT) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(ROOT))
|
|
12
|
+
os.environ.setdefault("MPLCONFIGDIR", os.path.join(tempfile.gettempdir(), "vmecdash-mpl"))
|
|
13
|
+
|
|
14
|
+
from vmecdash.core import VMECJaxProcessor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
EXAMPLE_WOUT = "example/wout_PO.nc"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _Var:
|
|
21
|
+
def __init__(self, values):
|
|
22
|
+
self.values = values
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_lambda_pair_keeps_lmns_for_stellarator_symmetric_file():
|
|
26
|
+
vmec = VMECJaxProcessor.from_file(EXAMPLE_WOUT)
|
|
27
|
+
lmnc, lmns = vmec._lambda_pair()
|
|
28
|
+
|
|
29
|
+
assert not vmec.lasym
|
|
30
|
+
assert np.max(np.abs(np.asarray(lmns))) > 0.0
|
|
31
|
+
assert np.max(np.abs(np.asarray(lmnc))) == 0.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_fieldline_axis_index_uses_first_half_mesh_surface():
|
|
35
|
+
vmec = VMECJaxProcessor.from_file(EXAMPLE_WOUT)
|
|
36
|
+
|
|
37
|
+
_, _, b_axis = vmec.compute_field_line_properties(0, alpha_points=4, zeta_points=8)
|
|
38
|
+
_, _, b_first = vmec.compute_field_line_properties(1, alpha_points=4, zeta_points=8)
|
|
39
|
+
|
|
40
|
+
assert np.max(np.abs(b_axis)) > 0.0
|
|
41
|
+
np.testing.assert_allclose(b_axis, b_first, rtol=0.0, atol=0.0)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_asymmetric_lambda_and_b_sine_pairs_are_preserved():
|
|
45
|
+
proc = object.__new__(VMECJaxProcessor)
|
|
46
|
+
proc.lasym = True
|
|
47
|
+
lmnc = np.array([[1.0, 2.0]])
|
|
48
|
+
lmns = np.array([[3.0, 4.0]])
|
|
49
|
+
bmnc = np.array([[5.0, 6.0]])
|
|
50
|
+
bmns = np.array([[7.0, 8.0]])
|
|
51
|
+
proc.ds = {"lmnc": _Var(lmnc), "lmns": _Var(lmns), "bmnc": _Var(bmnc), "bmns": _Var(bmns)}
|
|
52
|
+
|
|
53
|
+
out_lmnc, out_lmns = proc._lambda_pair()
|
|
54
|
+
out_bmnc, out_bmns = proc._coeff_pair("bmnc", "bmns")
|
|
55
|
+
|
|
56
|
+
np.testing.assert_allclose(out_lmnc, lmnc)
|
|
57
|
+
np.testing.assert_allclose(out_lmns, lmns)
|
|
58
|
+
np.testing.assert_allclose(out_bmnc, bmnc)
|
|
59
|
+
np.testing.assert_allclose(out_bmns, bmns)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_fieldline_modb_matches_simsopt_reference():
|
|
63
|
+
if importlib.util.find_spec("simsopt") is None:
|
|
64
|
+
import pytest
|
|
65
|
+
|
|
66
|
+
pytest.skip("simsopt is required for the reference comparison")
|
|
67
|
+
|
|
68
|
+
from simsopt.mhd import Vmec, vmec_fieldlines, vmec_splines
|
|
69
|
+
|
|
70
|
+
s_idx = 60
|
|
71
|
+
vmec = VMECJaxProcessor.from_file(EXAMPLE_WOUT)
|
|
72
|
+
alpha, zeta, b_dash = vmec.compute_field_line_properties(
|
|
73
|
+
s_idx, alpha_points=12, zeta_points=48, single_line=False
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
simsopt_vmec = Vmec(EXAMPLE_WOUT, verbose=False)
|
|
77
|
+
splines = vmec_splines(simsopt_vmec)
|
|
78
|
+
s_half = float(simsopt_vmec.s_half_grid[s_idx - 1])
|
|
79
|
+
ref = vmec_fieldlines(splines, s_half, alpha, phi1d=zeta, phi_center=0, plot=False)
|
|
80
|
+
|
|
81
|
+
np.testing.assert_allclose(b_dash, ref.modB[0], rtol=1e-10, atol=1e-10)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import plotly.graph_objects as go
|
|
7
|
+
|
|
8
|
+
os.environ.setdefault("MPLCONFIGDIR", "/tmp/mpl-codex")
|
|
9
|
+
|
|
10
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
11
|
+
if str(ROOT) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(ROOT))
|
|
13
|
+
|
|
14
|
+
from vmecdash import view_schema
|
|
15
|
+
from vmecdash.core import VMECJaxProcessor
|
|
16
|
+
from vmecdash.theme import build_theme
|
|
17
|
+
from vmecdash.vscode_backend import VmecDashBackend, figure_to_jsonable
|
|
18
|
+
|
|
19
|
+
EXAMPLE_WOUT = "example/wout_PO.nc"
|
|
20
|
+
EXPECTED_VIEWS = ["overview", "1d", "2d", "3d", "fieldline"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _vmec():
|
|
24
|
+
return VMECJaxProcessor.from_file(EXAMPLE_WOUT)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _field_map(vmec):
|
|
28
|
+
return {opt["value"]: opt.get("label", opt["value"]) for opt in vmec.available_fields()}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_registry_covers_expected_views():
|
|
32
|
+
assert list(view_schema.VIEWS) == EXPECTED_VIEWS
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_render_view_smoke_for_every_view_with_default_controls():
|
|
36
|
+
vmec = _vmec()
|
|
37
|
+
theme = build_theme(True, 0)
|
|
38
|
+
field_map = _field_map(vmec)
|
|
39
|
+
for view_id in view_schema.VIEWS:
|
|
40
|
+
fig = view_schema.render_view(view_id, vmec, {}, theme, field_map)
|
|
41
|
+
assert isinstance(fig, go.Figure)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_build_ui_schema_lists_views_and_resolves_ns_tokens():
|
|
45
|
+
vmec = _vmec()
|
|
46
|
+
schema = view_schema.build_ui_schema(vmec)
|
|
47
|
+
assert [v["id"] for v in schema["views"]] == EXPECTED_VIEWS
|
|
48
|
+
|
|
49
|
+
twod = next(v for v in schema["views"] if v["id"] == "2d")
|
|
50
|
+
s_idx = next(c for c in twod["controls"] if c["id"] == "sIdx")
|
|
51
|
+
assert s_idx["max"] == vmec.ns - 1
|
|
52
|
+
assert isinstance(s_idx["max"], int)
|
|
53
|
+
assert s_idx["default"] == vmec.ns - 1
|
|
54
|
+
|
|
55
|
+
fieldline = next(v for v in schema["views"] if v["id"] == "fieldline")
|
|
56
|
+
fl_s = next(c for c in fieldline["controls"] if c["id"] == "fieldlineSIdx")
|
|
57
|
+
assert fl_s["default"] == max(1, vmec.ns - 1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_no_default_drift_between_schema_and_render_fallback():
|
|
61
|
+
"""The default shipped to the Webview must equal the render-time fallback (cv())."""
|
|
62
|
+
vmec = _vmec()
|
|
63
|
+
schema = view_schema.build_ui_schema(vmec)
|
|
64
|
+
for view in schema["views"]:
|
|
65
|
+
for control in view["controls"]:
|
|
66
|
+
assert control["default"] == view_schema.cv({}, view["id"], control["id"], vmec)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_health_features_derived_from_registry():
|
|
70
|
+
backend = VmecDashBackend()
|
|
71
|
+
features = backend.health()["features"]
|
|
72
|
+
assert features == list(view_schema.VIEWS) + ["exportReport"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_render_view_serializes_without_nonfinite():
|
|
76
|
+
vmec = _vmec()
|
|
77
|
+
theme = build_theme(True, 0)
|
|
78
|
+
field_map = _field_map(vmec)
|
|
79
|
+
# lambda cross-section carries NaN/Inf at the axis; the wire writer forbids bare NaN/Inf.
|
|
80
|
+
fig = view_schema.render_view("2d", vmec, {"type2d": "cross_section", "var2d": "lambda"}, theme, field_map)
|
|
81
|
+
payload = figure_to_jsonable(fig)
|
|
82
|
+
json.dumps(payload, allow_nan=False) # must not raise
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_single_trace_uses_no_webgl_trace():
|
|
86
|
+
"""Single Trace must render with SVG scatter, not Scattergl (webview WebGL-context cap)."""
|
|
87
|
+
from vmecdash.renderers.fieldline import render_single_trace
|
|
88
|
+
|
|
89
|
+
vmec = _vmec()
|
|
90
|
+
theme = build_theme(True, 0)
|
|
91
|
+
fig = render_single_trace(vmec, max(1, vmec.ns - 1), 20, 0.0, 64, theme)
|
|
92
|
+
types = [trace.type for trace in fig.data]
|
|
93
|
+
assert "scattergl" not in types
|
|
94
|
+
assert "scatter" in types
|