constellaration 0.3.0__tar.gz → 0.4.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 (58) hide show
  1. {constellaration-0.3.0 → constellaration-0.4.0}/PKG-INFO +13 -12
  2. {constellaration-0.3.0 → constellaration-0.4.0}/README.md +12 -11
  3. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/__about__.py +2 -2
  4. constellaration-0.4.0/constellaration/mhd/desc_utils.py +284 -0
  5. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/mhd/vmec_utils.py +8 -1
  6. {constellaration-0.3.0 → constellaration-0.4.0}/.gitignore +0 -0
  7. {constellaration-0.3.0 → constellaration-0.4.0}/LICENSE +0 -0
  8. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/__init__.py +0 -0
  9. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/boozer/__init__.py +0 -0
  10. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/boozer/boozer.py +0 -0
  11. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/data_generation/__init__.py +0 -0
  12. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/data_generation/desc_optimization.py +0 -0
  13. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/data_generation/desc_optimization_settings.py +0 -0
  14. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/data_generation/rotational_transform_scaling.py +0 -0
  15. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/data_generation/vmec_optimization.py +0 -0
  16. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/data_generation/vmec_optimization_settings.py +0 -0
  17. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/forward_model.py +0 -0
  18. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/generative_model/bootstrap_dataset.py +0 -0
  19. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/generative_model/mcmc_algo.py +0 -0
  20. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/generative_model/optimize_with_mcmc.py +0 -0
  21. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/geometry/__init__.py +0 -0
  22. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/geometry/radial_profile.py +0 -0
  23. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/geometry/spectral_condensation.py +0 -0
  24. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/geometry/surface_rz_fourier.py +0 -0
  25. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/geometry/surface_utils.py +0 -0
  26. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/geometry/surface_utils_desc.py +0 -0
  27. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/initial_guess.py +0 -0
  28. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/mhd/__init__.py +0 -0
  29. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/mhd/flux_power_series.py +0 -0
  30. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/mhd/geometry_utils.py +0 -0
  31. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/mhd/ideal_mhd_parameters.py +0 -0
  32. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/mhd/ideal_mhd_parameters_utils.py +0 -0
  33. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/mhd/magnetics_utils.py +0 -0
  34. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/mhd/near_axis_configuration.py +0 -0
  35. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/mhd/turbulent_transport.py +0 -0
  36. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/mhd/vmec_settings.py +0 -0
  37. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/mhd_stable_qi_scoring.py +0 -0
  38. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/omnigeneity/__init__.py +0 -0
  39. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/omnigeneity/omnigenity_field.py +0 -0
  40. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/omnigeneity/omnigenity_field_desc.py +0 -0
  41. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/omnigeneity/omnigenity_field_plot.py +0 -0
  42. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/omnigeneity/omnigenity_field_sampling.py +0 -0
  43. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/omnigeneity/qi.py +0 -0
  44. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/optimization/__init__.py +0 -0
  45. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/optimization/augmented_lagrangian.py +0 -0
  46. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/optimization/augmented_lagrangian_runner.py +0 -0
  47. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/optimization/scipy_minimize_runner.py +0 -0
  48. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/optimization/settings.py +0 -0
  49. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/problems.py +0 -0
  50. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/utils/__init__.py +0 -0
  51. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/utils/dataset_utils.py +0 -0
  52. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/utils/file_exporter.py +0 -0
  53. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/utils/pytree.py +0 -0
  54. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/utils/seed_util.py +0 -0
  55. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/utils/types.py +0 -0
  56. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/utils/visualization.py +0 -0
  57. {constellaration-0.3.0 → constellaration-0.4.0}/constellaration/utils/visualization_utils.py +0 -0
  58. {constellaration-0.3.0 → constellaration-0.4.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: constellaration
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Code for analyzing and evaluating stellarator plasma shapes
5
5
  Project-URL: Documentation, https://github.com/proximafusion/constellaration#readme
6
6
  Project-URL: Issues, https://github.com/proximafusion/constellaration/issues
@@ -69,33 +69,34 @@ To keep results comparable, the benchmark of [Cadena et al., NeurIPS 2025](https
69
69
 
70
70
  | Use case | Version |
71
71
  | --- | --- |
72
- | [ConStellaration leaderboard](https://huggingface.co/spaces/proxima-fusion/constellaration-bench) (geometrical, simple-to-build QI, MHD-stable QI problems) | [`v0.3.0`](https://github.com/proximafusion/constellaration/releases/tag/v0.3.0) |
72
+ | [ConStellaration leaderboard](https://huggingface.co/spaces/proxima-fusion/constellaration-bench) (geometrical, simple-to-build QI, MHD-stable QI problems) | [`v0.2.6`](https://github.com/proximafusion/constellaration/releases/tag/v0.2.6) |
73
73
  | Original NeurIPS 2025 paper experiments | [`0.2.1`](https://pypi.org/project/constellaration/0.2.1/) |
74
- | Latest development | `main` |
74
+ | Latest development (not comparable to the leaderboard) | `main` / latest release |
75
75
 
76
- If you are working on the challenge or comparing against the leaderboard, install the pinned benchmark release:
76
+ If you are working on the challenge or comparing against the leaderboard, install the benchmark version:
77
77
 
78
78
  ```bash
79
- pip install constellaration==0.3.0
79
+ pip install constellaration==0.2.6
80
80
  ```
81
81
 
82
82
  or, from source:
83
83
 
84
84
  ```bash
85
- git clone --branch v0.3.0 https://github.com/proximafusion/constellaration.git
85
+ git clone --branch v0.2.6 https://github.com/proximafusion/constellaration.git
86
86
  cd constellaration
87
87
  pip install .
88
88
  ```
89
89
 
90
- The leaderboard evaluates every submission with exactly this version, so scores computed locally with `constellaration==0.3.0` match the leaderboard.
91
- Releases newer than `v0.3.0` may produce different metrics and scores and are not used by the leaderboard until explicitly announced here.
90
+ The leaderboard evaluates every submission with exactly this version, so scores computed locally with `constellaration==0.2.6` match the leaderboard.
91
+ Newer releases may produce different metrics and scores and are not used by the leaderboard unless announced here.
92
92
  The PyPI version always equals the git tag of the release it was built from (`constellaration==X.Y.Z` ⇔ tag `vX.Y.Z`).
93
93
 
94
- Differences of `v0.3.0` with respect to `0.2.1` (used for the original paper experiments) that can affect the benchmark scores:
95
- - The rotational transform constraint uses the absolute value of the edge rotational transform, i.e. boundaries with negative iota are no longer penalized.
96
- - Newer versions of VMEC++ and of other numerical dependencies (`booz-xform`, `jax`, `desc-opt`) are used, which can cause small numerical differences.
94
+ Differences that affect benchmark scores:
95
+ - **`0.2.1` → `0.2.6`:** the rotational transform constraint uses the absolute value of the edge rotational transform, so boundaries with negative iota are no longer penalized. Newer VMEC++ and other numerical dependencies cause only negligible numerical differences.
96
+ - **`0.2.6` → `0.3.0`:** the flux compression in regions of bad curvature (a constraint of the MHD-stable problem) is computed with the field-aligned (PEST) Jacobian instead of the VMEC one ([#109](https://github.com/proximafusion/constellaration/pull/109)). This fixes a bug but changes MHD-stable feasibility, so `0.3.0` and later are not comparable to the leaderboard.
97
97
 
98
- To reproduce the exact numbers of the paper, use `pip install constellaration==0.2.1` (this version is no longer maintained and may require pinning older versions of its unpinned dependencies).
98
+ > [!NOTE]
99
+ > From 2026-10-02 to 2026-10-08 the leaderboard was briefly evaluated with `0.3.0`. The affected submissions have been re-evaluated with `0.2.6`.
99
100
 
100
101
  ## Installation
101
102
 
@@ -17,33 +17,34 @@ To keep results comparable, the benchmark of [Cadena et al., NeurIPS 2025](https
17
17
 
18
18
  | Use case | Version |
19
19
  | --- | --- |
20
- | [ConStellaration leaderboard](https://huggingface.co/spaces/proxima-fusion/constellaration-bench) (geometrical, simple-to-build QI, MHD-stable QI problems) | [`v0.3.0`](https://github.com/proximafusion/constellaration/releases/tag/v0.3.0) |
20
+ | [ConStellaration leaderboard](https://huggingface.co/spaces/proxima-fusion/constellaration-bench) (geometrical, simple-to-build QI, MHD-stable QI problems) | [`v0.2.6`](https://github.com/proximafusion/constellaration/releases/tag/v0.2.6) |
21
21
  | Original NeurIPS 2025 paper experiments | [`0.2.1`](https://pypi.org/project/constellaration/0.2.1/) |
22
- | Latest development | `main` |
22
+ | Latest development (not comparable to the leaderboard) | `main` / latest release |
23
23
 
24
- If you are working on the challenge or comparing against the leaderboard, install the pinned benchmark release:
24
+ If you are working on the challenge or comparing against the leaderboard, install the benchmark version:
25
25
 
26
26
  ```bash
27
- pip install constellaration==0.3.0
27
+ pip install constellaration==0.2.6
28
28
  ```
29
29
 
30
30
  or, from source:
31
31
 
32
32
  ```bash
33
- git clone --branch v0.3.0 https://github.com/proximafusion/constellaration.git
33
+ git clone --branch v0.2.6 https://github.com/proximafusion/constellaration.git
34
34
  cd constellaration
35
35
  pip install .
36
36
  ```
37
37
 
38
- The leaderboard evaluates every submission with exactly this version, so scores computed locally with `constellaration==0.3.0` match the leaderboard.
39
- Releases newer than `v0.3.0` may produce different metrics and scores and are not used by the leaderboard until explicitly announced here.
38
+ The leaderboard evaluates every submission with exactly this version, so scores computed locally with `constellaration==0.2.6` match the leaderboard.
39
+ Newer releases may produce different metrics and scores and are not used by the leaderboard unless announced here.
40
40
  The PyPI version always equals the git tag of the release it was built from (`constellaration==X.Y.Z` ⇔ tag `vX.Y.Z`).
41
41
 
42
- Differences of `v0.3.0` with respect to `0.2.1` (used for the original paper experiments) that can affect the benchmark scores:
43
- - The rotational transform constraint uses the absolute value of the edge rotational transform, i.e. boundaries with negative iota are no longer penalized.
44
- - Newer versions of VMEC++ and of other numerical dependencies (`booz-xform`, `jax`, `desc-opt`) are used, which can cause small numerical differences.
42
+ Differences that affect benchmark scores:
43
+ - **`0.2.1` → `0.2.6`:** the rotational transform constraint uses the absolute value of the edge rotational transform, so boundaries with negative iota are no longer penalized. Newer VMEC++ and other numerical dependencies cause only negligible numerical differences.
44
+ - **`0.2.6` → `0.3.0`:** the flux compression in regions of bad curvature (a constraint of the MHD-stable problem) is computed with the field-aligned (PEST) Jacobian instead of the VMEC one ([#109](https://github.com/proximafusion/constellaration/pull/109)). This fixes a bug but changes MHD-stable feasibility, so `0.3.0` and later are not comparable to the leaderboard.
45
45
 
46
- To reproduce the exact numbers of the paper, use `pip install constellaration==0.2.1` (this version is no longer maintained and may require pinning older versions of its unpinned dependencies).
46
+ > [!NOTE]
47
+ > From 2026-10-02 to 2026-10-08 the leaderboard was briefly evaluated with `0.3.0`. The affected submissions have been re-evaluated with `0.2.6`.
47
48
 
48
49
  ## Installation
49
50
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.3.0'
22
- __version_tuple__ = version_tuple = (0, 3, 0)
21
+ __version__ = version = '0.4.0'
22
+ __version_tuple__ = version_tuple = (0, 4, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,284 @@
1
+ """DESC ideal-MHD equilibria: a serializable representation, conversion to and from
2
+ DESC objects, and the pipeline that re-solves a VMEC++ equilibrium with DESC.
3
+
4
+ The `desc_equilibrium` subset of the ConStellaration Hugging Face dataset stores one
5
+ `DescEquilibrium` JSON per plasma configuration, produced by
6
+ `solve_desc_equilibrium_from_vmecpp_wout`.
7
+ """
8
+
9
+ import pathlib
10
+ import tempfile
11
+
12
+ import jaxtyping as jt
13
+ import numpy as np
14
+ import pydantic
15
+ from desc import equilibrium as desc_equilibrium
16
+ from desc import profiles as desc_profiles
17
+ from desc import vmec as desc_vmec
18
+ from typing_extensions import Self
19
+ from vmecpp import _pydantic_numpy as pydantic_numpy
20
+
21
+ from constellaration.mhd import vmec_utils
22
+ from constellaration.utils import file_exporter
23
+
24
+
25
+ class DescSplineProfile(pydantic_numpy.BaseModelWithNumpy):
26
+ """A radial profile represented as a DESC `SplineProfile`."""
27
+
28
+ method: str = "cubic2"
29
+ """Spline interpolation method, e.g. `"linear"`, `"cubic"` or `"cubic2"`."""
30
+
31
+ name: str = ""
32
+ """Name of the profile, e.g. `"pressure"` or `"current"`."""
33
+
34
+ knots: jt.Float[np.ndarray, " n_knots"]
35
+ """Knots of the spline in the normalized effective radius `rho`."""
36
+
37
+ params: jt.Float[np.ndarray, " n_knots"]
38
+ """Values of the profile at the knots."""
39
+
40
+
41
+ class DescEquilibrium(pydantic_numpy.BaseModelWithNumpy):
42
+ """A fixed-boundary DESC equilibrium.
43
+
44
+ The geometry is represented on a Fourier-Zernike basis, see
45
+ https://desc-docs.readthedocs.io/en/stable/notebooks/basis_grid.html.
46
+ """
47
+
48
+ r_lmn: jt.Float[np.ndarray, " n_modes_r"]
49
+ """Fourier-Zernike coefficients of the cylindrical coordinate R."""
50
+
51
+ z_lmn: jt.Float[np.ndarray, " n_modes_z"]
52
+ """Fourier-Zernike coefficients of the cylindrical coordinate Z."""
53
+
54
+ lambda_lmn: jt.Float[np.ndarray, " n_modes_lambda"]
55
+ """Fourier-Zernike coefficients of the poloidal stream function lambda."""
56
+
57
+ radial_resolution: pydantic.NonNegativeInt
58
+ """Radial resolution L of the Fourier-Zernike basis."""
59
+
60
+ poloidal_resolution: pydantic.NonNegativeInt
61
+ """Poloidal resolution M of the Fourier-Zernike basis."""
62
+
63
+ toroidal_resolution: pydantic.NonNegativeInt
64
+ """Toroidal resolution N of the Fourier-Zernike basis."""
65
+
66
+ spectral_indexing: str = "ansi"
67
+ """Indexing convention of the Zernike polynomials, `"ansi"` or `"fringe"`."""
68
+
69
+ n_field_periods: pydantic.PositiveInt
70
+ """Number of toroidal field periods."""
71
+
72
+ is_stellarator_symmetric: bool = True
73
+ """Whether the equilibrium is stellarator symmetric."""
74
+
75
+ total_toroidal_flux: float
76
+ """Toroidal magnetic flux enclosed by the plasma boundary, in Wb."""
77
+
78
+ pressure_profile: DescSplineProfile
79
+ """Plasma pressure, in Pa."""
80
+
81
+ current_profile: DescSplineProfile | None = None
82
+ """Enclosed toroidal current, in A.
83
+
84
+ Exactly one of `current_profile` and `iota_profile` is set.
85
+ """
86
+
87
+ iota_profile: DescSplineProfile | None = None
88
+ """Rotational transform.
89
+
90
+ Exactly one of `current_profile` and `iota_profile` is set.
91
+ """
92
+
93
+ r_axis: jt.Float[np.ndarray, " n_modes_r_axis"] | None = None
94
+ """Fourier coefficients of the R coordinate of the magnetic axis."""
95
+
96
+ z_axis: jt.Float[np.ndarray, " n_modes_z_axis"] | None = None
97
+ """Fourier coefficients of the Z coordinate of the magnetic axis."""
98
+
99
+ r_boundary_lmn: jt.Float[np.ndarray, " n_modes_r_boundary"] | None = None
100
+ """Fourier coefficients of the R coordinate of the plasma boundary."""
101
+
102
+ z_boundary_lmn: jt.Float[np.ndarray, " n_modes_z_boundary"] | None = None
103
+ """Fourier coefficients of the Z coordinate of the plasma boundary."""
104
+
105
+ @pydantic.model_validator(mode="after")
106
+ def _check_exactly_one_of_current_or_iota(self) -> Self:
107
+ if (self.current_profile is None) == (self.iota_profile is None):
108
+ raise ValueError(
109
+ "Exactly one of current_profile and iota_profile must be provided."
110
+ )
111
+ return self
112
+
113
+
114
+ class DescEquilibriumSolution(pydantic_numpy.BaseModelWithNumpy):
115
+ """A DESC equilibrium solved for force balance, with its residual force error."""
116
+
117
+ equilibrium: DescEquilibrium
118
+
119
+ normalized_mean_absolute_force_error: float
120
+ """Mean absolute force-balance error of `equilibrium`, as reported by DESC's
121
+ `ForceBalance` objective, normalized by its characteristic force."""
122
+
123
+ normalized_max_absolute_force_error: float
124
+ """Maximum absolute force-balance error of `equilibrium`, as reported by DESC's
125
+ `ForceBalance` objective, normalized by its characteristic force."""
126
+
127
+
128
+ def solve_desc_equilibrium_from_vmecpp_wout(
129
+ vmecpp_wout: vmec_utils.VmecppWOut,
130
+ ) -> DescEquilibriumSolution:
131
+ """Re-solve a VMEC++ equilibrium with DESC.
132
+
133
+ This is the pipeline that produced the `desc_equilibrium` subset of the
134
+ ConStellaration dataset: DESC is initialized from the VMEC++ equilibrium and
135
+ then solved for force balance with fixed boundary, pressure and toroidal current.
136
+ """
137
+ return solve_desc_equilibrium(desc_equilibrium_from_vmecpp_wout(vmecpp_wout))
138
+
139
+
140
+ def desc_equilibrium_from_vmecpp_wout(
141
+ vmecpp_wout: vmec_utils.VmecppWOut,
142
+ ) -> DescEquilibrium:
143
+ """Fit a DESC equilibrium to a VMEC++ equilibrium, without solving it.
144
+
145
+ The Fourier-Zernike resolution follows DESC's defaults for the given VMEC
146
+ resolution, and the toroidal current (not the rotational transform) is taken
147
+ as the profile constraint, as in the VMEC++ run.
148
+ """
149
+ with tempfile.TemporaryDirectory() as tmp_dir:
150
+ wout_path = pathlib.Path(tmp_dir) / "wout.nc"
151
+ file_exporter.to_vmec2000_wout_file(vmecpp_wout, wout_path)
152
+ equilibrium = desc_vmec.VMECIO.load(wout_path, profile="current")
153
+ return from_desc_equilibrium(equilibrium)
154
+
155
+
156
+ def solve_desc_equilibrium(
157
+ equilibrium: DescEquilibrium,
158
+ verbose: int = 0,
159
+ ) -> DescEquilibriumSolution:
160
+ """Solve a DESC equilibrium for force balance with fixed boundary and profiles."""
161
+ desc_object = to_desc_equilibrium(equilibrium)
162
+ solved, result = desc_object.solve(objective="force", verbose=verbose)
163
+ force_error = result["Objective values"]["Force error: "][0]
164
+ return DescEquilibriumSolution(
165
+ equilibrium=from_desc_equilibrium(solved),
166
+ normalized_mean_absolute_force_error=float(force_error["f_mean_norm"]),
167
+ normalized_max_absolute_force_error=float(force_error["f_max_norm"]),
168
+ )
169
+
170
+
171
+ def to_desc_equilibrium(
172
+ equilibrium: DescEquilibrium,
173
+ ) -> desc_equilibrium.Equilibrium:
174
+ """Converts a `DescEquilibrium` to a `desc.equilibrium.Equilibrium`."""
175
+ desc_object = desc_equilibrium.Equilibrium(
176
+ R_lmn=equilibrium.r_lmn,
177
+ Z_lmn=equilibrium.z_lmn,
178
+ L_lmn=equilibrium.lambda_lmn,
179
+ L=equilibrium.radial_resolution,
180
+ M=equilibrium.poloidal_resolution,
181
+ N=equilibrium.toroidal_resolution,
182
+ spectral_indexing=equilibrium.spectral_indexing,
183
+ Psi=equilibrium.total_toroidal_flux,
184
+ NFP=equilibrium.n_field_periods,
185
+ sym=equilibrium.is_stellarator_symmetric,
186
+ # The coefficients come from a DESC equilibrium, so they are already
187
+ # right-handed.
188
+ check_orientation=False,
189
+ pressure=_to_desc_spline_profile(equilibrium.pressure_profile),
190
+ current=(
191
+ _to_desc_spline_profile(equilibrium.current_profile)
192
+ if equilibrium.current_profile is not None
193
+ else None
194
+ ),
195
+ iota=(
196
+ _to_desc_spline_profile(equilibrium.iota_profile)
197
+ if equilibrium.iota_profile is not None
198
+ else None
199
+ ),
200
+ )
201
+ # Without a `surface` argument the constructor installs a default boundary and
202
+ # axis; a fixed-boundary solve would then be constrained to the wrong boundary.
203
+ if equilibrium.r_axis is not None:
204
+ desc_object.Ra_n = equilibrium.r_axis
205
+ if equilibrium.z_axis is not None:
206
+ desc_object.Za_n = equilibrium.z_axis
207
+ if equilibrium.r_boundary_lmn is not None:
208
+ desc_object.Rb_lmn = equilibrium.r_boundary_lmn
209
+ if equilibrium.z_boundary_lmn is not None:
210
+ desc_object.Zb_lmn = equilibrium.z_boundary_lmn
211
+ return desc_object
212
+
213
+
214
+ def from_desc_equilibrium(
215
+ desc_object: desc_equilibrium.Equilibrium,
216
+ ) -> DescEquilibrium:
217
+ """Converts a `desc.equilibrium.Equilibrium` to a `DescEquilibrium`.
218
+
219
+ Profiles that are not `SplineProfile`s are resampled onto one. If both a current
220
+ and a rotational transform profile are set, only the current is kept.
221
+ """
222
+ if desc_object.pressure is None:
223
+ raise ValueError("The DESC equilibrium must have a pressure profile.")
224
+ current = (
225
+ _from_desc_profile(desc_object.current)
226
+ if desc_object.current is not None
227
+ else None
228
+ )
229
+ iota = (
230
+ _from_desc_profile(desc_object.iota)
231
+ if desc_object.iota is not None and current is None
232
+ else None
233
+ )
234
+ params = desc_object.params_dict
235
+ # DESC types this as optional, but an Equilibrium always has a basis indexing.
236
+ spectral_indexing = desc_object.spectral_indexing
237
+ assert spectral_indexing is not None
238
+ return DescEquilibrium(
239
+ r_lmn=np.asarray(desc_object.R_lmn),
240
+ z_lmn=np.asarray(desc_object.Z_lmn),
241
+ lambda_lmn=np.asarray(desc_object.L_lmn),
242
+ radial_resolution=desc_object.L,
243
+ poloidal_resolution=desc_object.M,
244
+ toroidal_resolution=desc_object.N,
245
+ spectral_indexing=spectral_indexing,
246
+ n_field_periods=desc_object.NFP,
247
+ is_stellarator_symmetric=bool(desc_object.sym),
248
+ total_toroidal_flux=float(desc_object.Psi),
249
+ pressure_profile=_from_desc_profile(desc_object.pressure),
250
+ current_profile=current,
251
+ iota_profile=iota,
252
+ r_axis=np.asarray(params["Ra_n"]),
253
+ z_axis=np.asarray(params["Za_n"]),
254
+ r_boundary_lmn=np.asarray(params["Rb_lmn"]),
255
+ z_boundary_lmn=np.asarray(params["Zb_lmn"]),
256
+ )
257
+
258
+
259
+ def _to_desc_spline_profile(
260
+ profile: DescSplineProfile,
261
+ ) -> desc_profiles.SplineProfile:
262
+ return desc_profiles.SplineProfile(
263
+ values=profile.params,
264
+ knots=profile.knots,
265
+ method=profile.method,
266
+ name=profile.name,
267
+ )
268
+
269
+
270
+ def _from_desc_profile(profile: desc_profiles._Profile) -> DescSplineProfile:
271
+ if not isinstance(profile, desc_profiles.SplineProfile):
272
+ knots = np.linspace(0.0, 1.0, 20)
273
+ profile = desc_profiles.SplineProfile(
274
+ values=np.asarray(profile(knots)),
275
+ knots=knots,
276
+ method="cubic2",
277
+ name=profile.name,
278
+ )
279
+ return DescSplineProfile(
280
+ method=profile._method,
281
+ name=profile.name,
282
+ knots=np.asarray(profile.knots),
283
+ params=np.asarray(profile.params),
284
+ )
@@ -218,6 +218,13 @@ class VmecppWOut(vmecpp.VmecWOut):
218
218
  assert "iota_half" not in data
219
219
  assert "iotas" in data
220
220
 
221
+ # Old wouts have no current-density coefficients. vmecpp would default
222
+ # them to empty 1D arrays, which it can neither re-validate nor save.
223
+ # They are missing data, not zero, hence NaN.
224
+ for field in ("currumnc", "currvmnc"):
225
+ if field not in data:
226
+ data[field] = np.full((data["mnmax_nyq"], data["ns"]), np.nan)
227
+
221
228
  return data
222
229
 
223
230
  @property
@@ -300,7 +307,7 @@ def build_vmecpp_indata(
300
307
  if not boundary.is_stellarator_symmetric:
301
308
  raise NotImplementedError("Only stellarator symmetric surfaces are supported.")
302
309
 
303
- indata = vmecpp.VmecInput.default()
310
+ indata = vmecpp.VmecInput()
304
311
 
305
312
  indata.lasym = not boundary.is_stellarator_symmetric
306
313
  indata.nfp = boundary.n_field_periods
File without changes