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.
Files changed (38) hide show
  1. vmecdash-0.1.0/LICENSE +21 -0
  2. vmecdash-0.1.0/PKG-INFO +145 -0
  3. vmecdash-0.1.0/README.md +113 -0
  4. vmecdash-0.1.0/pyproject.toml +53 -0
  5. vmecdash-0.1.0/setup.cfg +4 -0
  6. vmecdash-0.1.0/tests/test_cross_section_mesh.py +158 -0
  7. vmecdash-0.1.0/tests/test_fieldline_numerics.py +81 -0
  8. vmecdash-0.1.0/tests/test_view_schema.py +94 -0
  9. vmecdash-0.1.0/tests/test_vscode_backend.py +94 -0
  10. vmecdash-0.1.0/vmecdash/__init__.py +8 -0
  11. vmecdash-0.1.0/vmecdash/cli.py +26 -0
  12. vmecdash-0.1.0/vmecdash/core/__init__.py +4 -0
  13. vmecdash-0.1.0/vmecdash/core/vmec_jax.py +966 -0
  14. vmecdash-0.1.0/vmecdash/dash_app/__init__.py +2 -0
  15. vmecdash-0.1.0/vmecdash/dash_app/app.py +790 -0
  16. vmecdash-0.1.0/vmecdash/dash_app/assets/icon_stell.png +0 -0
  17. vmecdash-0.1.0/vmecdash/dash_app/assets/icon_stell_circle.png +0 -0
  18. vmecdash-0.1.0/vmecdash/dash_app/assets/icon_stell_noback.png +0 -0
  19. vmecdash-0.1.0/vmecdash/dash_app/assets/icon_stell_round.png +0 -0
  20. vmecdash-0.1.0/vmecdash/dash_app/cards.py +175 -0
  21. vmecdash-0.1.0/vmecdash/dash_app/controls.py +263 -0
  22. vmecdash-0.1.0/vmecdash/py.typed +1 -0
  23. vmecdash-0.1.0/vmecdash/renderers/__init__.py +18 -0
  24. vmecdash-0.1.0/vmecdash/renderers/fieldline.py +114 -0
  25. vmecdash-0.1.0/vmecdash/renderers/overview.py +57 -0
  26. vmecdash-0.1.0/vmecdash/renderers/profiles.py +34 -0
  27. vmecdash-0.1.0/vmecdash/renderers/three_d.py +58 -0
  28. vmecdash-0.1.0/vmecdash/renderers/two_d.py +231 -0
  29. vmecdash-0.1.0/vmecdash/stats.py +110 -0
  30. vmecdash-0.1.0/vmecdash/theme.py +38 -0
  31. vmecdash-0.1.0/vmecdash/view_schema.py +261 -0
  32. vmecdash-0.1.0/vmecdash/vscode_backend.py +248 -0
  33. vmecdash-0.1.0/vmecdash.egg-info/PKG-INFO +145 -0
  34. vmecdash-0.1.0/vmecdash.egg-info/SOURCES.txt +36 -0
  35. vmecdash-0.1.0/vmecdash.egg-info/dependency_links.txt +1 -0
  36. vmecdash-0.1.0/vmecdash.egg-info/entry_points.txt +2 -0
  37. vmecdash-0.1.0/vmecdash.egg-info/requires.txt +14 -0
  38. 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.
@@ -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
+ [![VMECdash Screenshot](example/summary.png)](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.
@@ -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
+ [![VMECdash Screenshot](example/summary.png)](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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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